Give each test run its own temp directory (BL-16664) - #8172
Conversation
Our tests name their scratch folders after themselves -- about 180 calls of the
form new TemporaryFolder("SomeFixtureName") -- and those names resolve to
machine-global paths. TemporaryFolder's constructor deletes any existing folder
of the name before creating it, so when two runs share a machine (agents working
in several worktrees at once) one run's setup silently deletes another run's
in-flight folder, and the victim fails somewhere unrelated.
Rather than rename 180 call sites -- which would still leave Bloom's own code
writing to the shared temp directory while under test -- redirect the whole
process's temp directory before any fixture runs, via an assembly-wide
[SetUpFixture] in the global namespace. Every existing call site keeps its
familiar name, now scoped to this run, and production code under test is
isolated too.
A failing run keeps its folder so the evidence survives; passing runs delete
theirs, and anything older than a day is cleared by a later run.
WIP: committed locally to keep the work safe while the BL-16661 PR is finished.
Not pushed, no PR yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Devin's review of PR #8172 pointed out that the two public entry points -- RedirectTempToAFolderOfOurOwn and RemoveOurTempFolderUnlessSomethingFailed -- had no documentation comment, which AGENTS.md ("# Commenting") requires of all public methods. The private helpers were documented; these were not. The two test methods get summaries for the same reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prompted by Devin's review of PR #8172, which asked how anyone would confirm the keep-on-failure behaviour. Checking it turned up a wrong claim in the note I had just added: it said the path of a kept folder "is printed at the end of the run". The code does report it, but through NUnit's progress channel, which dotnet test does not show at its default verbosity -- so a developer following that sentence would look in the output, see nothing, and conclude the folder had not been kept. Say where to look instead. Verified separately that the behaviour itself is right: a run of one failing test leaves its folder behind, and a run of one passing test creates and then removes its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Devin's review of PR #8172 spotted that teardown deleted the run folder but left TMP and TEMP pointing at it. Anything running after that point -- NUnit's own shutdown, or a background thread outliving the tests -- would compute a temp path inside a directory that no longer exists and fail confusingly at the very end of an otherwise good run. Restore the machine's own temp folder first, whether or not we go on to delete ours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
[Claude Opus 5 (1M context)] Consulted Devin on 2026-08-06 17:42 CDT up to commit Four rounds, because each round's findings produced a fix commit, which starts a new review. Three things came out of it, each with its own resolved thread above:
Two notes for anyone reading the thread list. Devin appears to accumulate findings across rounds rather than recomputing them against the current diff: the final round re-reported all three of the above against commit CI: this repo runs no build or test checks on pull requests, so there was no CI signal. Greptile has stayed queued throughout without reviewing. |
Cleanup deleted the run's temp folder and said nothing if it failed. A failure there is worth knowing about: it normally means a test finished without disposing something it opened, which is a small bug of its own and can make later runs behave oddly. So after the delete attempt, if the folder is still there, report it -- naming one file that will not open and the reason the operating system gave, e.g. "1 file(s) remain; at least this one is still in use: <path> -- The process cannot access the file ... because it is being used by another process." Deliberately only a warning. A leaked handle should not turn a green run red at the very last moment. The message goes to standard error, which experiment shows is the only channel dotnet test displays at its default verbosity -- Console.Out, TestContext.Out and TestContext.Progress are all swallowed. The existing "tests failed, so their files were left in ..." message moves there too, so it can actually be seen; it was previously written to TestContext.Progress, where nobody would ever have read it. The part that decides what to report is a separate method so it can be tested: one test locks a file and checks the message names that file rather than its innocent neighbour, and carries the reason; another checks a folder that really did go produces nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From the local review pass. The warning reports the first file it cannot open exclusively, and called it "still in use". That is the commonest cause but not the only one: a read-only file, or an ACL problem, lands in the same catch. Say the file could not be opened, and let the operating system's own message -- which we already pass through -- distinguish the cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Devin's review of PR #8172 found a real hole. The run folder was only created, never emptied, and its name is not unique forever: it is built from a process id, Windows recycles those, and folders from crashed or failing runs are kept for a day. So a run could be handed the path of a dead run and inherit whatever it left there -- producing exactly the confusing stale-file failures this class exists to prevent. Empty it first. That is safe because a process id is unique among *running* processes and this one is ours, so nothing alive can be using the folder. Two tests: one puts stale files and a subfolder in a folder named the way a recycled run would be and checks nothing survives, and one checks the ordinary case where the folder does not exist yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
[Claude Opus 5 (1M context)] Consulted Devin on 2026-08-07 09:32 CDT up to commit Second preflight, after the developer asked for a warning when the run's temp folder cannot be
Its final round re-reported all four findings from this branch's history against Full suite at this commit: 3067 run, 3053 passed, 1 failed — One correction for the record: during this round CI: this repo runs no build or test checks on pull requests. Greptile has stayed queued throughout without reviewing. |
andrew-polk
left a comment
There was a problem hiding this comment.
@andrew-polk reviewed 3 files and all commit messages, made 1 comment, and resolved 1 discussion.
Reviewable status: all files reviewed, 4 unresolved discussions (waiting on JohnThomson).
src/BloomTests/TestTempDirectory.cs line 28 at r2 (raw file):
there is a fixture in the Bloom.Api namespace
Seems like we should fix that.
Maybe doesn't change this other than the comment, of course.
JohnThomson
left a comment
There was a problem hiding this comment.
@JohnThomson made 1 comment.
Reviewable status: 2 of 4 files reviewed, 4 unresolved discussions (waiting on andrew-polk).
src/BloomTests/TestTempDirectory.cs line 28 at r2 (raw file):
Previously, andrew-polk wrote…
there is a fixture in the Bloom.Api namespace
Seems like we should fix that.
Maybe doesn't change this other than the comment, of course.
Done.
andrew-polk
left a comment
There was a problem hiding this comment.
@andrew-polk reviewed 2 files and all commit messages, and resolved 1 discussion.
Reviewable status: all files reviewed, 3 unresolved discussions.
Our C# tests name their scratch folders after themselves — about 180 calls of the form
new TemporaryFolder("SomeFixtureTests")— and those names resolve to machine-global paths. That was harmless when one person ran the suite at a time. It is not harmless now that agents work in several worktrees at once, becauseTemporaryFolder's constructor deletes any existing folder of that name before creating it:So one run's setup silently deletes another run's in-flight folder, and the victim then fails somewhere unrelated, naming a folder it never heard of.
The fix
Rather than rename 180 call sites — which would still leave Bloom's own production code writing to the shared temp directory while under test — redirect the whole process's temp directory before any fixture runs, with an assembly-wide
[SetUpFixture]:Every existing call site keeps its familiar name; the name is now scoped to this run.
Path.GetTempPath()is defined in terms ofTMP/TEMP, so this also covers the dozen tests that build temp paths by hand, and production code under test.The
<key>half comes fromBLOOM_AGENT_BUILD_DIRwhenbuild/agent-dotnet.shset it, so a temp folder can be matched to the build tree beside it; the pid is what guarantees uniqueness.It lives in the global namespace deliberately: an NUnit
[SetUpFixture]applies to its own namespace and those beneath it, and not quite all our fixtures are underBloomTests(there is one inBloom.Api).A failing run keeps its folder so the evidence survives — these bugs are precisely about temp folders appearing and disappearing, so deleting the scene of the crime would be perverse. Passing runs delete theirs; anything older than a day is cleared by a later run.
Effect on the suite
Measured on one developer machine, whose
%TEMP%holds ~25,000 entries:masterThe suite ran to completion, and two consecutive runs gave identical results — so this is deterministic, not luck. The remaining failure,
XMatterHelperTests.InsideBackCover_PaperAndDeviceXMatter_ShareOneCustomLayoutId, also fails on pristine master and is unrelated to temp; note thatAGENTS.mdcalls a failing xmatter-locating test a real wrapper/Directory.Build.propsregression, so it is worth chasing separately.Tests
Two, asserting the invariant directly: that
Path.GetTempPath()is this run's own folder and not the machine's, and that an ordinary fixed-nameTemporaryFolderlands inside it.Ref: https://issues.bloomlibrary.org/youtrack/issue/BL-16664
This change is
Devin review
Devin review