fix #485: isolate console capture and align Gitea portable test lanes
All checks were successful
CI / linux-portable (push) Successful in 3m40s
CI / windows-gate (push) Successful in 6m16s
CI / release (push) Successful in 2m13s

This commit is contained in:
Erik 2026-09-06 11:41:27 +02:00
parent 3ebb120dd2
commit 0275b4ca5a
7 changed files with 144 additions and 11 deletions

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -36,7 +36,9 @@ namespace AcDream.App.Tests.Rendering;
/// <c>Console.SetOut</c> by the transcript tests to capture output.
/// Interleaved redirection can restore a DISPOSED <c>StringWriter</c> as the
/// process-wide <c>Console.Out</c>, which then throws in unrelated
/// tests.</description></item>
/// tests. Even classes that only write to the console can race a capture's
/// <c>StringWriter.ToString</c> snapshot; Console's synchronized writer
/// does not synchronize reads of the underlying StringBuilder.</description></item>
/// </list>
///
/// <para>
@ -54,13 +56,17 @@ namespace AcDream.App.Tests.Rendering;
/// per-test values (several are <c>[Theory]</c> 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 <c>finally</c> blocks already
/// return the defaults; this attribute supplies the mutual exclusion they were
/// missing. Follows the <c>WorldEnvironmentControllerCollection</c> precedent.
/// return the defaults. <c>DisableParallelization</c> 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
/// <c>ThreadSchedulingCollection</c> exclusivity precedent.
/// </para>
///
/// <para>Evidence + history: <c>docs/ISSUES.md</c> #251.</para>
/// </summary>
[CollectionDefinition(Name)]
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class CameraDiagnosticsCollection
{
public const string Name = "Camera diagnostics globals";

View file

@ -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*'(?<filter>[^']+)'",
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*'(?<filter>[^']+)'", 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+'(?<filter>[^']+)'", 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);

View file

@ -29,11 +29,25 @@ namespace AcDream.App.Tests.Rendering.Walk;
/// comment, issue #251): interleaved
/// <c>Console.SetOut</c> redirection across parallel test classes can
/// restore a disposed <see cref="StringWriter"/> process-wide.
/// The collection also excludes other parallel collections so their writes
/// cannot race snapshots of the capture or contaminate the transcript.
/// </para>
/// </summary>
[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<CollectionDefinitionAttribute>());
Assert.True(definition.DisableParallelization);
}
private static (WalkCell Cell1, WalkCell Cell2) BuildTranscriptFixtureCells(TestContext ctx)
{
var cell1 = new WalkCell