tests: build throwaway data dirs with per-run tmpdirs
Assisted-by: GLM-5.3 via pi
change zztrxqwyytrrzurokqrpkusprmrmvvox
commit c01889f34f65c1bbe173ffcaba7b3249fe620537
author Alpha Chen <alpha@kejadlen.dev>
date
parent ltkvsopt
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)