tests: build throwaway data dirs with per-run tmpdirs
Assisted-by: GLM-5.3 via pi
diff --git a/AGENTS.md b/AGENTS.md
index 926140d..b79dc95 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -15,8 +15,8 @@ rake # Tests (the default task)
rake steep # Type check lib against the RBS comments in it
rake fixtures # Re-record response fixtures from current behavior
rake index:rebuild # Derive the parsed index again from the stored cards
-rake dev # Dev server, seeded from test/fixtures/cards into
- # tmp/dev-data on every start, reloading on change
+rake dev # Dev server, seeded from test/fixtures/cards into a
+ # throwaway tmpdir on every start, reloading on change
# (needs fd and entr)
rake profile:install # Render and stage carddav.mobileconfig for approval
```
@@ -66,7 +66,7 @@ A fixture diff you didn't intend is the test suite doing its job. Read it
before regenerating.
`test/fixtures/cards/` holds the vCards the test database is seeded from.
-The database itself is built under `tmp/test-data` on every run and is
+The database itself is built in a throwaway tmpdir on every run and is
not a fixture; edit a `.vcf` to change what the replay serves.
## Gotchas
diff --git a/Rakefile b/Rakefile
index 77c3cf7..165f359 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,5 +1,6 @@
require "pathname"
+require "tmpdir"
$LOAD_PATH.unshift(Pathname.new(__dir__) / "lib")
require "minitest/test_task"
@@ -16,25 +17,32 @@ task :dev do
# test/fixtures/cards on every start, so a client always sees known
# state and the real data/ directory stays out of the dev loop. Only
# rackup reloads under entr, so a client's edits survive a restart and
- # a fresh `rake dev` is what resets to the fixtures.
- data_dir = Pathname.new(__dir__) / "tmp" / "dev-data"
- ENV["PRO_TACTS_DATA_DIR"] = data_dir.to_s
- require_relative "test/fixture_data"
- FixtureData.install(data_dir).close
- sh "fd -e rb . lib | entr -r rackup -o localhost"
+ # a fresh `rake dev` is what resets to the fixtures. The data lives in
+ # a session-scoped tmpdir: two servers running at once each get their
+ # own database, and the directory goes when the task does.
+ Dir.mktmpdir("pro-tacts-dev") do |dir|
+ data_dir = Pathname.new(dir)
+ ENV["PRO_TACTS_DATA_DIR"] = data_dir.to_s
+ require_relative "test/fixture_data"
+ FixtureData.install(data_dir).close
+ sh "fd -e rb . lib | entr -r rackup -o localhost"
+ end
end
desc "Regenerate macOS exchange response fixtures from current responses"
task :fixtures do
# Mirrors test/test_helper.rb, which cannot be required here without
- # minitest/autorun running its at_exit hook inside rake.
- data_dir = Pathname.new(__dir__) / "tmp" / "test-data"
- ENV["PRO_TACTS_DATA_DIR"] = data_dir.to_s
- require "pro_tacts/web"
- require_relative "test/fixture_data"
- require_relative "test/pro_tacts/exchange_fixtures"
- ProTacts::Web.store = FixtureData.install(data_dir)
- ExchangeFixtures.record_responses(ProTacts::Web)
+ # minitest/autorun running its at_exit hook inside rake. Its own
+ # tmpdir, so a fixture re-record cannot race a concurrent test run.
+ Dir.mktmpdir("pro-tacts-test") do |dir|
+ data_dir = Pathname.new(dir)
+ ENV["PRO_TACTS_DATA_DIR"] = data_dir.to_s
+ require "pro_tacts/web"
+ require_relative "test/fixture_data"
+ require_relative "test/pro_tacts/exchange_fixtures"
+ ProTacts::Web.store = FixtureData.install(data_dir)
+ ExchangeFixtures.record_responses(ProTacts::Web)
+ end
end
desc "Type check lib against the RBS comments in it"
diff --git a/test/fixture_data.rb b/test/fixture_data.rb
index 8fda05d..f09bcf0 100644
--- a/test/fixture_data.rb
+++ b/test/fixture_data.rb
@@ -12,7 +12,8 @@ require "pro_tacts/store"
# The cards are files rather than a checked-in database because they are
# evidence, the same standing the request files in macos-exchange have:
# a reviewer can read a .vcf and a diff means something. The database is
-# derived from them, and lives under tmp/ with everything else that is.
+# derived from them, and lives in a throwaway tmpdir with everything
+# else that does.
module FixtureData
CARDS = Pathname.new(__dir__) / "fixtures" / "cards"
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 80a7269..4a16f66 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,20 +1,40 @@
# A throwaway data directory holds the database the tests run against,
# seeded from test/fixtures/cards so the recorded macOS exchange resolves
-# the same hrefs the real client did.
+# the same hrefs the real client did. Every run builds its own tmpdirs,
+# so nothing survives a run and two runs cannot collide.
+#
+# Cleanup runs through Minitest.after_run, not a plain at_exit: minitest
+# 6's autorun nests one at_exit inside another and runs the tests inside
+# the outer handler, so an at_exit registered after minitest/autorun —
+# which the TestTask command and several test files require first —
+# would fire BEFORE the tests and delete the database mid-suite.
+#
+# The removal is rm_rf rather than remove_entry because WebTest's
+# wired-middleware tests rm_rf the configured unhandled directory to
+# assert capture behavior from a clean slate; whether that directory
+# still exists at cleanup depends on test order, and rm_rf tolerates
+# it being gone.
#
# The environment is set before the app is required rather than after it,
# so the requires cannot all sit at the top: the app reads configuration
# as it loads, because the unhandled-request middleware is given its
# directory at class-definition time. Set it afterwards and the captures
-# land in log/ instead of tmp/.
+# land in log/ instead of the tmpdir.
+require "fileutils"
+require "minitest"
require "pathname"
+require "tmpdir"
-ENV["PRO_TACTS_DATA_DIR"] = (Pathname.new(__dir__).parent / "tmp" / "test-data").to_s
+data_dir = Pathname.new(Dir.mktmpdir("pro-tacts-test"))
+ENV["PRO_TACTS_DATA_DIR"] = data_dir.to_s
+Minitest.after_run { FileUtils.rm_rf(data_dir) }
# Several tests provoke 404s. Keep the captures out of log/ and out of the
# fixtures; UnhandledRequestsTest points the middleware at its own tmpdir.
-ENV["PRO_TACTS_UNHANDLED_DIR"] = (Pathname.new(__dir__).parent / "tmp" / "test-unhandled").to_s
+unhandled_dir = Pathname.new(Dir.mktmpdir("pro-tacts-unhandled"))
+ENV["PRO_TACTS_UNHANDLED_DIR"] = unhandled_dir.to_s
+Minitest.after_run { FileUtils.rm_rf(unhandled_dir) }
require_relative "fixture_data"
require "pro_tacts/web"
@@ -22,4 +42,4 @@ require "minitest/autorun"
# Standing in for config.ru, which never runs here: build the store and
# hand it to the app.
-ProTacts::Web.store = FixtureData.install(ENV.fetch("PRO_TACTS_DATA_DIR"))
+ProTacts::Web.store = FixtureData.install(data_dir)