Skip to content

Read acceptance-test binlogs behind a lock - #10658

Open
Jakub Jareš (nohwnd) wants to merge 2 commits into
mainfrom
nohwnd-read-acceptance-binlogs-behind-a-lock
Open

Read acceptance-test binlogs behind a lock#10658
Jakub Jareš (nohwnd) wants to merge 2 commits into
mainfrom
nohwnd-read-acceptance-binlogs-behind-a-lock

Conversation

@nohwnd

Copy link
Copy Markdown
Member

Serialization.Read from MSBuild.StructuredLogger is not safe to call concurrently in a cold process, and both acceptance suites parallelize at method level, so the first overlapping reads race on the library's lazy static initialization. A read that loses the race does not throw, it returns a Build whose only children are an error reading "Error when opening the log file." and a warning, with no build content underneath.

This turned an official main build red on EnableMSTestRunner_True_Will_Run_Standalone. The binlog on disk was fine, the read was not, reading the same file serially finds exactly the ProjectCapability node the assertion was looking for. The same race also lets Assert.DoesNotContain over a binlog pass on an empty tree, so it has been silently weakening those assertions as well.

Adds BinlogReader next to AcceptanceAssert, takes a process-wide lock around the read, and routes all 15 call sites through it. If the tree still looks unusable the helper now fails with the binlog path, its size on disk and the swallowed error text, instead of leaving the test to report notExpected: 0 / actual: 0.

Reading twelve real binlogs from 25 cold processes gives 10 empty reads out of 300 without the lock and 0 out of 300 with it. The lock costs about 270ms across twelve reads.

Verified: build.cmd -pack -c Release passes, and both acceptance suites pass apart from net462, NativeAOT and vstest.console failures that reproduce the same way on unmodified main.

🤖

Serialization.Read from MSBuild.StructuredLogger is not safe to call
concurrently in a cold process, and both acceptance suites parallelize at
method level, so the first overlapping reads race on the library's lazy
static initialization. A read that loses the race does not throw, it
returns a Build whose only children are an error reading "Error when
opening the log file." and a warning, with no build content underneath.
Positive assertions over that tree then fail for no product reason, and
Assert.DoesNotContain passes vacuously.

Add BinlogReader next to AcceptanceAssert, take a process-wide lock
around the read, and route all 15 call sites through it. When the tree
still looks unusable the helper fails with the binlog path, its size on
disk and the swallowed error text, instead of handing back an empty
Build.

Reading twelve real binlogs from 25 cold processes gives 10 empty reads
out of 300 without the lock and 0 out of 300 with it. The lock costs
about 270ms across twelve reads.

🤖
Copilot AI balanced review requested due to automatic review settings August 20, 2026 07:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Serializes StructuredLogger binlog reads to prevent flaky acceptance-test results caused by concurrent initialization.

Changes:

  • Adds BinlogReader with locking, retries, and diagnostic validation.
  • Routes all 15 binlog reads through the helper.
  • Shares the helper with both acceptance-test suites.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
SdkTests.cs Uses synchronized binlog reading.
RunnerTests.cs Uses synchronized binlog reading.
MSTest.Acceptance.IntegrationTests.csproj Links the shared helper.
PackagedApp.MSBuildRegistration.cs Uses synchronized binlog reading.
MSBuildTests.GenerateEntryPoint.cs Migrates all binlog reads to the helper.
MSBuild.KnownExtensionRegistration.cs Uses synchronized binlog reading.
Helpers/BinlogReader.cs Implements locked reads and invalid-tree diagnostics.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

With the lock held a retry can only re-read the same file and get the same
answer, so the loop never did anything. On a binlog that is genuinely
unreadable it read a large file three times while holding the lock, and every
other test waited for it. The corruption check stays and now throws on the
first read.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 20, 2026 09:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/BinlogReader.cs:72

  • AddItem is not a validity marker for a binlog: a readable project can legitimately perform no item additions (for example, a minimal project or a build that fails before item evaluation). This check would therefore turn a successful parse into an infrastructure failure. Check for the project hierarchy, or only the documented error-tree shape, instead.
            : build.FindFirstDescendant<SL.AddItem>() is null
                ? "The tree holds no AddItem nodes at all, which no real build produces."

@github-actions

Copy link
Copy Markdown
Contributor

🧵 Parallel-safety audit — PR #10658

Parallelization — one row per test assembly touched by this PR:

Test assembly Scope Workers Analyzer coverage
MSTest.Acceptance.IntegrationTests MethodLevel CPU count (Workers = 0) coverable once the parallel-safety analyzers ship (attribute-based opt-in)
Microsoft.Testing.Platform.Acceptance.IntegrationTests MethodLevel CPU count (Workers = 0) coverable once the parallel-safety analyzers ship (attribute-based opt-in)

Findings: A (global-state) 0 · B (paths) 0 · C (declaration) 0 · D (over-serialization) 0 — by severity: Critical 0 · High 0 · Warning 0 · Info 1.

Top actions (by expected value):

  1. No action needed — this PR is the fix for a live category-A finding (unsynchronized concurrent calls into StructuredLogger.Serialization.Read's lazy static init under MethodLevel parallelism); it introduces the correct remediation.

Info

  • [A · High confidence] test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/BinlogReader.cs:39-48 — the PR adds a process-wide Lock (private static readonly Lock ReadLock = new();) around the previously-unsynchronized call to SL.Serialization.Read, and routes all 15 call sites across both MethodLevel acceptance assemblies through it. This is the textbook category-A fix: Serialization.Read's lazy static initialization is process-global mutable state, both suites run at MethodLevel with Workers = 0 (CPU-count concurrency) so the reads previously did race, and a lock is the right coordination mechanism since the resource can't be made per-test (it's a third-party library's static init, not something under test control). No corresponding restoration step is needed — unlike an env var or CWD mutation, there is no "previous value" to restore; the lock purely serializes initialization, which is exactly what a lazy-static race needs.
  • [C · Medium confidence] test/IntegrationTests/MSTest.Acceptance.IntegrationTests/RunnerTests.cs, SdkTests.cs, and the four call sites in MSBuildTests.GenerateEntryPoint.cs / MSBuild.KnownExtensionRegistration.cs / PackagedApp.MSBuildRegistration.cs — all now correctly declare their protection implicitly via the shared BinlogReader.Read helper rather than a per-test [ResourceLock]. This is appropriate: the hazard is a library-internal static, not a resource the test itself owns or that varies by test identity, so a single process-wide lock in the helper is a better fix than distributing [ResourceLock(WellKnownResources...)] declarations across 15 call sites (WellKnownResources doesn't cover this resource anyway, and a custom string key would only add MSTEST0073 friction for no coordination benefit — the lock already fully serializes the one true hazard).

No Critical/High/Warning findings. This PR eliminates a real cross-test race under MethodLevel parallelism rather than introducing one — a clean, complete fix with no residual gap I could find. git diff shows no removed [ResourceLock]/[DoNotParallelize]/restoration code and no relative-path or shared-file-system-path changes.

Advisory only — heuristic, non-blocking. Re-run with /parallel-audit. This audit answers "is it parallel-safe?"; for testability, smells, or flakiness see the detect-static-dependencies / test-smell-detection / test-anti-patterns analyses.

🤖 Automated content by GitHub Copilot. Generated by the Parallel-safety audit on PR (on open / sync) workflow. · auto · 48.3 AIC · ⌖ 2.9 AIC · ⊞ 24.8K · [◷]( · )

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10658

This PR is a mechanical reliability fix, not new test authorship: it adds a BinlogReader.Read helper (non-test infrastructure, out of scope for grading) and routes 8 existing acceptance-test call sites through it, replacing SL.Serialization.Read(...) / Serialization.Read(...) with BinlogReader.Read(...). Each touched test method has exactly one changed line (the binlog-read call); no assertions, inputs, or test intent were altered.

GradeTestMutationNotesHow to improve
A (90–100) mod RunnerTests.
EnableMSTestRunner_True_
Will_Run_Standalone
N/A Only the binlog-read call site changed; existing assertions and intent are unaffected and this removes a known flaky-read race.
A (90–100) mod RunnerTests.
EnableMSTestRunner_False_
Wont_Flow_TestingPlatformServer
N/A Same mechanical swap; the previously-vacuous Assert.DoesNotContain risk on an empty tree is now closed by BinlogReader.Read's corruption check.
A (90–100) mod SdkTests.
SettingIsTestApplicationToFalse
ReducesAddedExtensionsAndMakes
ProjectNotExecutable
N/A Call-site swap only; test logic unchanged.
A (90–100) mod MSBuild.
KnownExtensionRegistration.
Microsoft_Testing_Platform_
Extensions_ShouldBe_
Correctly_Registered
N/A Call-site swap only; test logic unchanged.
A (90–100) mod MSBuild.
KnownExtensionRegistration.
TestingPlatformBuilderHook_
With_Conflicting_Metadata_
Fails_Build
N/A Call-site swap only; the negative SingleOrDefault/IsNotNull check now benefits from the reader's failure-detection instead of silently tolerating an empty tree.
A (90–100) mod MSBuildTests.
GenerateEntryPoint.
When_GenerateTestingPlatformEntryPoint_
IsFalse_NoEntryPointInjected
N/A Call-site swap only across this multi-tfm test.
A (90–100) mod MSBuildTests.
GenerateEntryPoint.
GenerateVBApplicationHelper
WithoutEntryPoint
N/A Call-site swap only.
A (90–100) mod MSBuildTests.
GenerateEntryPoint.
GeneratedSourcesAreRegenerated
WhenMSBuildTaskChanges
N/A Five sequential rebuild/read cycles in this test are exactly the scenario the lock protects against; the swap strengthens, not weakens, this already-thorough test.
A (90–100) mod MSBuildTests.
GenerateEntryPoint.
GenerateAndVerify
LanguageSpecificEntryPointAsync
N/A Call-site swap only, including the rebuild-and-reread branch.
A (90–100) mod PackagedApp.
MSBuildRegistration.
PackagedApp_TestingPlatform
BuilderHook_IsRegistered_
ViaBuildProps
N/A Call-site swap only; unrelated stray whitespace-only diff on the license header comment in this file has no test impact.

No inline suggestions were posted: every touched test line is a like-for-like replacement of Serialization.Read(...) with the new BinlogReader.Read(...) helper, preserving existing assertions and null-handling (BinlogPath!) conventions already used elsewhere in these files. The new BinlogReader.cs helper itself is infrastructure, not a test, and is out of scope for grading, but its internal DescribeCorruption check is a good backstop against the exact silent-empty-tree failure mode this PR fixes.

This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Re-run with /review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 47.2 AIC · ⌖ 2.83 AIC · ⊞ 16.9K · [◷]( · )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants