From 0275b4ca5a9d305f23f9a36f93aafd2db0298a2c Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 6 Sep 2026 11:41:27 +0200 Subject: [PATCH] fix #485: isolate console capture and align Gitea portable test lanes --- .gitea/workflows/ci.yml | 14 ++-- docs/ISSUES.md | 14 ++++ docs/ci-and-releases.md | 8 +++ .../2026-09-06-ci-209-correction.md | 65 +++++++++++++++++++ .../Rendering/CameraDiagnosticsCollection.cs | 14 ++-- .../Vk/VulkanLaneOwnershipContractTests.cs | 26 ++++++++ .../Walk/WalkFrameDriverTranscriptTests.cs | 14 ++++ 7 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 docs/research/2026-09-01-overhaul/2026-09-06-ci-209-correction.md diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 102fe6f4..f26f8f7f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -5,10 +5,10 @@ # self-hosted runners carry the pinned SDK band from global.json already. # actions/checkout IS mirrored, so it is used normally. # -# The suite runs through tools/run-release-gate.ps1 rather than a bare -# `dotnet test`: that script owns the xUnit trait-lane filter which excludes -# the InstalledDat / Live / Manual / OS-specific lanes. A bare `dotnet test` -# fails ~36 tests by design because those lanes assert their own preconditions. +# The streaming suite below mirrors tools/run-release-gate.ps1's portable +# filter, including the capability-owned Vulkan lane. The ordinary runners +# do not promise a Vulkan device. VulkanLaneOwnershipContractTests guards +# filter parity; a bare dotnet test also selects environment-dependent lanes. name: CI on: push: @@ -52,7 +52,7 @@ jobs: shell: pwsh run: | $ErrorActionPreference = 'Stop' - $filter = 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' + $filter = 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=Vulkan&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' $failed = @() foreach ($proj in Get-ChildItem tests -Directory | Sort-Object Name) { $csproj = Join-Path $proj.FullName "$($proj.Name).csproj" @@ -84,7 +84,7 @@ jobs: echo '::group::AcDream.Core.Net.Tests (single-threaded)' dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj \ -c Release --nologo \ - --filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' \ + --filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Vulkan&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' \ -- xUnit.MaxParallelThreads=1 echo '::endgroup::' @@ -98,7 +98,7 @@ jobs: tests/AcDream.UI.Abstractions.Tests ; do echo "::group::$p" dotnet test "$p" -c Release --nologo \ - --filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' + --filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Vulkan&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' echo "::endgroup::" done diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 1c0ea87d..7f375347 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -39,6 +39,20 @@ confirmed closed by the owner, 11 need a focused live gate, and 43 are safe to remain closed. See [`docs/research/2026-08-28-owner-closed-issue-validity-audit.md`](research/2026-08-28-owner-closed-issue-validity-audit.md). +## #485 — Gitea portable CI races console capture and selects Vulkan-only tests + +**Status:** DONE — correction implemented and locally verified, 2026-09-06; +new remote run to be verified after push. Run209 on3ebb120dd built cleanly, +but Windows App tests failed: one StringBuilder capture race, four +ErrorIncompatibleDriver failures. Linux passed; release was skipped. +The camera diagnostics collection now excludes other parallel collections; +Gitea filters match the canonical portable filter (including Lane!=Vulkan). +Two regression guards added. Release test assembly build0W/0E and corrected +App lane7,156/0/0 PASS; no production code changed. The dedicated Vulkan lane +remains available and strict, not a runtime skip. +Evidence and separate alternate-output build limitation: +`research/2026-09-01-overhaul/2026-09-06-ci-209-correction.md`. + ## #484 — Distant mountain silhouettes flicker and change with viewpoint **Status:** OPEN — OWNER-DEFERRED, 2026-09-06, after report on #483 candidate. diff --git a/docs/ci-and-releases.md b/docs/ci-and-releases.md index 0155696f..acf16f0c 100644 --- a/docs/ci-and-releases.md +++ b/docs/ci-and-releases.md @@ -22,6 +22,14 @@ test can fail on them and a run costs ~7 minutes plus a 121 MB release. A red ga cannot publish: `release` uses `needs:`, not a `workflow_run` trigger, whose Forgejo support is unreliable. +The streaming Windows filter must exactly match the default in +`tools/run-release-gate.ps1`; Linux differs only by enabling `Lane=Linux`. +`VulkanLaneOwnershipContractTests` enforces both copies. These ordinary Gitea +runners do not promise Vulkan capability, so `Lane=Vulkan` is excluded here, +not silently skipped inside a test. The explicit hardware lane remains in +the GitHub `linux-vulkan` workflow and can be run locally on a capable device +(see `release-gate.md`); a green Gitea run alone does not certify GPU pixels. + ## Why Gitea and not GitHub GitHub Actions is **billing-blocked** on this account ("recent account payments diff --git a/docs/research/2026-09-01-overhaul/2026-09-06-ci-209-correction.md b/docs/research/2026-09-01-overhaul/2026-09-06-ci-209-correction.md new file mode 100644 index 00000000..6980922e --- /dev/null +++ b/docs/research/2026-09-01-overhaul/2026-09-06-ci-209-correction.md @@ -0,0 +1,65 @@ +# Post-closeout Gitea CI correction — #485, 2026-09-06 + +## Evidence and cause + +[Run209](https://git.snakedesert.se/erik/acdream/actions/runs/209) tested +`3ebb120dd271c873476cd071b2a33f09d17ce710`. Linux passed. Windows built +successfully (0 warnings/errors), then App.Tests failed five cases: + +- `Collect_WeatherTurnFiredMatchesThePrintedOcLineExactly(true)`: + `StringBuilder.ToString` threw ArgumentOutOfRangeException (chunkLength). + The camera diagnostics collection serialized its members but still ran + beside other collections. Other tests could write to the process-wide + captured Console.Out while the test read its underlying StringWriter. + They could also observe the temporary global walk-transcript flag. +- The four `Lane=Vulkan` pixel cases (ordinary shared-index, both + world-shader light strides, atmospheric receivers) failed at + `vkCreateInstance: ErrorIncompatibleDriver`. They were already correctly + tagged, but Gitea's copied portable filter lacked the Vulkan exclusion + present in `tools/run-release-gate.ps1`. + +Windows App result: 7,153 passed / 5 failed / 0 skipped. No other Windows +test assembly failed. Release was skipped because it depends on both gates. +These are test isolation and CI lane-routing defects, not a new distance +algorithm failure. + +## Bounded correction + +Make the existing camera diagnostics collection exclusive with +`DisableParallelization=true`. Other ordinary collections remain parallel. +No timer, retry, assertion weakening or production change. A deterministic +metadata regression protects exclusivity. + +Align the Windows Gitea filter with the canonical portable filter; both Linux +filters retain only their intentional difference (enable Lane=Linux). +A contract regression compares all three filters to that canonical source. +The Vulkan tests are unchanged, still fail on missing capability when selected, +and remain explicitly owned by the separate hardware lane. Gitea's ordinary +runners do not certify GPU pixels; this distinction is documented. + +## Verification before push + +- Changed App test assembly Release build, reusing unchanged production + references (`BuildProjectReferences=false`): **PASS, 0 warnings/errors**. +- Full App suite under the exact corrected Gitea Windows filter: + **7,156 passed / 0 failed / 0 skipped**. The total is run209's7,158 minus + four hardware-only cases plus two new regression tests, not lost coverage. + TRX: `artifacts/ci-209-fix-tests/ci-209-app-corrected.trx`. +- Lead reviewed the console/global-isolation fix; independent review of + CI filters, ownership contract and documentation: **PASS**. +- Running client binary SHA256 remained + `869BA14CB2C610E006C80BDFFCBD75629339A3486B8B98D7708B23050700FF34`. + Neither ACDream nor retail was closed or controlled. + +An attempted full solution build with alternate +`--artifacts-path artifacts/ci-209-fix-build` failed in existing packaging +path assumptions: smoke-plugin GetTargetPath resolves against App's absolute +output path (MSB3030), and nested Bake publication misses its win-x64 assets +target (NETSDK1047). That attempt is **FAIL**, not the passing test-only build +above. No packaging workaround was added to this CI correction. The ordinary +CI build already passed on the original commit; the subsequent remote run +must verify the correction in CI's actual clean-build layout. + +This note records pre-push evidence. Remote success is reported only after +observing the new commit's Gitea results. Campaign closeout and deferred #484 +are unchanged; no retail-behavior deviation is introduced. diff --git a/tests/AcDream.App.Tests/Rendering/CameraDiagnosticsCollection.cs b/tests/AcDream.App.Tests/Rendering/CameraDiagnosticsCollection.cs index d04df6e4..7ddc4f91 100644 --- a/tests/AcDream.App.Tests/Rendering/CameraDiagnosticsCollection.cs +++ b/tests/AcDream.App.Tests/Rendering/CameraDiagnosticsCollection.cs @@ -36,7 +36,9 @@ namespace AcDream.App.Tests.Rendering; /// Console.SetOut by the transcript tests to capture output. /// Interleaved redirection can restore a DISPOSED StringWriter as the /// process-wide Console.Out, which then throws in unrelated -/// tests. +/// tests. Even classes that only write to the console can race a capture's +/// StringWriter.ToString snapshot; Console's synchronized writer +/// does not synchronize reads of the underlying StringBuilder. /// /// /// @@ -54,13 +56,17 @@ namespace AcDream.App.Tests.Rendering; /// per-test values (several are [Theory] cases that set different knobs /// per case), so a fixture cannot own the save/restore without rewriting every /// member's internals. The existing per-test finally blocks already -/// return the defaults; this attribute supplies the mutual exclusion they were -/// missing. Follows the WorldEnvironmentControllerCollection precedent. +/// return the defaults. DisableParallelization additionally excludes +/// OTHER collections for this collection's duration: member-only serialization +/// cannot protect process-wide console captures or prevent unrelated walk tests +/// from observing the temporary transcript flag. Ordinary collections can still +/// run in parallel with each other. Follows the +/// ThreadSchedulingCollection exclusivity precedent. /// /// /// Evidence + history: docs/ISSUES.md #251. /// -[CollectionDefinition(Name)] +[CollectionDefinition(Name, DisableParallelization = true)] public sealed class CameraDiagnosticsCollection { public const string Name = "Camera diagnostics globals"; diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanLaneOwnershipContractTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanLaneOwnershipContractTests.cs index 90b9f860..d44365ee 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanLaneOwnershipContractTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanLaneOwnershipContractTests.cs @@ -56,6 +56,32 @@ public sealed class VulkanLaneOwnershipContractTests Assert.Contains("--filter \"Lane=Vulkan\"", hardwareStep, StringComparison.Ordinal); } + [Fact] + public void GiteaPortableFilters_MatchCanonicalReleaseGateWithOnlyLinuxLaneEnabledOnLinux() + { + string root = RepositoryRoot(); + string releaseGate = File.ReadAllText(Path.Combine(root, "tools", "run-release-gate.ps1")); + Match canonical = Regex.Match( + releaseGate, + "\\[string\\]\\$TestFilter\\s*=\\s*'(?[^']+)'", + RegexOptions.CultureInvariant); + Assert.True(canonical.Success, "Could not locate the canonical portable filter."); + string expected = canonical.Groups["filter"].Value; + + string workflow = File.ReadAllText(Path.Combine(root, ".gitea", "workflows", "ci.yml")); + Match windows = Regex.Match( + workflow, "\\$filter\\s*=\\s*'(?[^']+)'", RegexOptions.CultureInvariant); + Assert.True(windows.Success, "Could not locate Gitea's Windows test filter."); + Assert.Equal(expected, windows.Groups["filter"].Value); + + MatchCollection linux = Regex.Matches( + workflow, "--filter\\s+'(?[^']+)'", RegexOptions.CultureInvariant); + Assert.Equal(2, linux.Count); + string expectedLinux = string.Join('&', expected.Split('&').Where(term => term != "Lane!=Linux")); + foreach (Match filter in linux) + Assert.Equal(expectedLinux, filter.Groups["filter"].Value); + } + private static string Step(string workflow, int start) { int next = workflow.IndexOf(" - name:", start + 1, StringComparison.Ordinal); diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs index 0f464860..a1f74f8c 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs @@ -29,11 +29,25 @@ namespace AcDream.App.Tests.Rendering.Walk; /// comment, issue #251): interleaved /// Console.SetOut redirection across parallel test classes can /// restore a disposed process-wide. +/// The collection also excludes other parallel collections so their writes +/// cannot race snapshots of the capture or contaminate the transcript. /// /// [Collection(CameraDiagnosticsCollection.Name)] public sealed partial class WalkFrameDriverTests { + [Fact] + public void TranscriptCaptureCollection_ExcludesOtherParallelCollections() + { + // A shared collection alone only serializes its members. CI run 209 + // raced unrelated console output against capture.ToString(), and other + // walk tests can also emit transcript lines while our global flag is on. + var definition = Assert.Single(typeof(CameraDiagnosticsCollection) + .GetCustomAttributes(typeof(CollectionDefinitionAttribute), inherit: false) + .Cast()); + Assert.True(definition.DisableParallelization); + } + private static (WalkCell Cell1, WalkCell Cell2) BuildTranscriptFixtureCells(TestContext ctx) { var cell1 = new WalkCell