From 7506e5f14e1d13f3f0dc1b98afa52fc03187d14a Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 5 Sep 2026 02:54:40 +0200 Subject: [PATCH] test(vulkan): isolate the hardware witness lane Tag the production offscreen witness as Lane=Vulkan, exclude capability-owned tests from the portable Release filter, and give lavapipe an explicit trait-only invocation after the portable Vulkan contracts. Document the lane and pin witness, portable-filter, and workflow ownership without initializing Vulkan. Mutations performed and restored: 1. Removed the witness trait: VulkanLaneOwnershipContractTests first failed Assert.Matches because the Lane=Vulkan/Fact/method pattern was absent. 2. Removed Lane!=Vulkan from the default filter: the contract first failed Assert.Contains, not found Lane!=Vulkan. 3. Weakened the dedicated invocation to Lane!=Vulkan: the contract first failed Assert.Contains, not found --filter Lane=Vulkan in the hardware step. 4. Narrowed the dedicated invocation with FullyQualifiedName: the contract first failed Assert.DoesNotContain because FullyQualifiedName was present at position 231. --- .github/workflows/headless-portability.yml | 11 ++- docs/release-gate.md | 19 ++++- .../Vk/MeshModernSharedIndexOffscreenTests.cs | 1 + .../Vk/VulkanLaneOwnershipContractTests.cs | 84 +++++++++++++++++++ tools/run-release-gate.ps1 | 2 +- 5 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanLaneOwnershipContractTests.cs diff --git a/.github/workflows/headless-portability.yml b/.github/workflows/headless-portability.yml index 2149e6b3..7d498278 100644 --- a/.github/workflows/headless-portability.yml +++ b/.github/workflows/headless-portability.yml @@ -311,7 +311,16 @@ jobs: dotnet test ` tests/AcDream.App.Tests/AcDream.App.Tests.csproj ` -c Release ` - --filter "FullyQualifiedName~AcDream.App.Tests.Rendering.Gpu.Vk" + --filter "FullyQualifiedName~AcDream.App.Tests.Rendering.Gpu.Vk&Lane!=Vulkan" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Test the Vulkan hardware lane on lavapipe + shell: pwsh + run: | + dotnet test ` + tests/AcDream.App.Tests/AcDream.App.Tests.csproj ` + -c Release ` + --filter "Lane=Vulkan" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # (a) + (b): one run, two gates. The harness opens a real window, runs the diff --git a/docs/release-gate.md b/docs/release-gate.md index 4aba7774..8bdfcfd6 100644 --- a/docs/release-gate.md +++ b/docs/release-gate.md @@ -12,9 +12,10 @@ The command verifies that `AcDream.slnx` contains every `.csproj` under `src/`, then discovers and runs every hermetic test in every default test assembly once in a fresh Release process. It does not retry failures. Tests carrying an explicit non-hermetic `Lane` trait (`InstalledDat`, `PreparedPackage`, `Live`, -`Manual`, `Timing`, `Windows`, `Linux`, or `SystemFont`), `Purpose=Diagnostic`, or -`Status=KnownFailure` are excluded from the hermetic total and run through -their owned lane instead. The graph currently contains 54 projects, +`Manual`, `Timing`, `Windows`, `Linux`, `Vulkan`, or `SystemFont`), +`Purpose=Diagnostic`, or `Status=KnownFailure` are excluded from the hermetic +total and run through their owned lane instead. The graph currently contains +54 projects, including all 17 maintained .NET tools and three render-pack SDK samples; data-dependent tools and SDK samples are built but are not executed as tests. @@ -108,6 +109,18 @@ and Forgejo fails a task that stops reporting as a zombie. ## Non-hermetic test lanes +`Lane=Vulkan` owns tests that require a Vulkan loader, a Vulkan 1.3-capable +physical device with a graphics queue, and coherent host-visible readback +memory. The portable Release gate excludes this capability lane. The +`linux-vulkan` CI job installs lavapipe and runs it explicitly; missing Vulkan +capability is a failure there, not a skip. Run the same dedicated lane locally +on a capable device with: + +```powershell +dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release ` + --filter 'Lane=Vulkan' +``` + Installed-DAT tests require an explicit opt-in and a retail DAT directory: ```powershell diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs index 4959eb55..b4fae7e1 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs @@ -19,6 +19,7 @@ public sealed unsafe class MeshModernSharedIndexOffscreenTests private const uint Prefix = 2; private static readonly object VulkanLock = new(); + [Trait("Lane", "Vulkan")] [Fact] public void CommittedProductionOrdinaryShader_RendersLocalSidecarsAtNonzeroTransformPrefix() { diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanLaneOwnershipContractTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanLaneOwnershipContractTests.cs new file mode 100644 index 00000000..90b9f860 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanLaneOwnershipContractTests.cs @@ -0,0 +1,84 @@ +using System.Text.RegularExpressions; + +namespace AcDream.App.Tests.Rendering.Gpu.Vk; + +/// +/// Keeps the capability-owned pixel witness out of the portable gate while +/// requiring the lavapipe job to execute the complete trait-owned lane. +/// This contract reads repository text only and never initializes Vulkan. +/// +public sealed class VulkanLaneOwnershipContractTests +{ + private const string WitnessMethod = + "CommittedProductionOrdinaryShader_RendersLocalSidecarsAtNonzeroTransformPrefix"; + + [Fact] + public void HardwareWitness_PortableFilterAndLavapipeOwnerStayAligned() + { + string root = RepositoryRoot(); + string witness = File.ReadAllText(Path.Combine( + root, + "tests", "AcDream.App.Tests", "Rendering", "Gpu", "Vk", + "MeshModernSharedIndexOffscreenTests.cs")); + Assert.Matches( + new Regex( + "\\[Trait\\(\"Lane\", \"Vulkan\"\\)\\]\\s*" + + "\\[Fact\\]\\s*public void " + WitnessMethod + "\\(", + RegexOptions.CultureInvariant), + witness); + Assert.Equal(1, Count(witness, "[Trait(\"Lane\", \"Vulkan\")]")); + + string releaseGate = File.ReadAllText(Path.Combine(root, "tools", "run-release-gate.ps1")); + Match defaultFilter = Regex.Match( + releaseGate, + "\\[string\\]\\$TestFilter\\s*=\\s*'(?[^']+)'", + RegexOptions.CultureInvariant); + Assert.True(defaultFilter.Success, "Could not locate the portable Release gate's default filter."); + Assert.Contains("Lane!=Vulkan", defaultFilter.Groups["filter"].Value, StringComparison.Ordinal); + Assert.Equal(1, Count(defaultFilter.Groups["filter"].Value, "Lane!=Vulkan")); + + string workflow = File.ReadAllText(Path.Combine( + root, ".github", "workflows", "headless-portability.yml")); + int install = workflow.IndexOf("- name: Install lavapipe, the Vulkan loader and Xvfb", StringComparison.Ordinal); + int portable = workflow.IndexOf("- name: Test the Vulkan backend's platform-independent decisions", StringComparison.Ordinal); + int hardware = workflow.IndexOf("- name: Test the Vulkan hardware lane on lavapipe", StringComparison.Ordinal); + Assert.True(install >= 0 && install < portable && portable < hardware); + + string portableStep = Step(workflow, portable); + Assert.Contains( + "--filter \"FullyQualifiedName~AcDream.App.Tests.Rendering.Gpu.Vk&Lane!=Vulkan\"", + portableStep, + StringComparison.Ordinal); + + string hardwareStep = Step(workflow, hardware); + Assert.DoesNotContain("FullyQualifiedName", hardwareStep, StringComparison.Ordinal); + Assert.DoesNotContain(WitnessMethod, hardwareStep, StringComparison.Ordinal); + Assert.Contains("--filter \"Lane=Vulkan\"", hardwareStep, StringComparison.Ordinal); + } + + private static string Step(string workflow, int start) + { + int next = workflow.IndexOf(" - name:", start + 1, StringComparison.Ordinal); + return next < 0 ? workflow[start..] : workflow[start..next]; + } + + private static int Count(string value, string needle) + { + int count = 0; + for (int index = 0; (index = value.IndexOf(needle, index, StringComparison.Ordinal)) >= 0;) + { + count++; + index += needle.Length; + } + return count; + } + + private static string RepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) + directory = directory.Parent; + return directory?.FullName + ?? throw new InvalidOperationException("Could not locate the repository root."); + } +} diff --git a/tools/run-release-gate.ps1 b/tools/run-release-gate.ps1 index 8e7194e3..c041d59d 100644 --- a/tools/run-release-gate.ps1 +++ b/tools/run-release-gate.ps1 @@ -24,7 +24,7 @@ param( [int]$BuildTimeoutSeconds = 900, [int]$TestTimeoutSeconds = 600, [int]$HangTimeoutSeconds = 180, - [string]$TestFilter = 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure', + [string]$TestFilter = 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=Vulkan&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure', [switch]$SkipRestore, [switch]$SkipBuild )