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.
This commit is contained in:
Erik 2026-09-05 02:54:40 +02:00
parent 15a796c3a1
commit 7506e5f14e
5 changed files with 112 additions and 5 deletions

View file

@ -311,7 +311,16 @@ jobs:
dotnet test ` dotnet test `
tests/AcDream.App.Tests/AcDream.App.Tests.csproj ` tests/AcDream.App.Tests/AcDream.App.Tests.csproj `
-c Release ` -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 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# (a) + (b): one run, two gates. The harness opens a real window, runs the # (a) + (b): one run, two gates. The harness opens a real window, runs the

View file

@ -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 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 in a fresh Release process. It does not retry failures. Tests carrying an
explicit non-hermetic `Lane` trait (`InstalledDat`, `PreparedPackage`, `Live`, explicit non-hermetic `Lane` trait (`InstalledDat`, `PreparedPackage`, `Live`,
`Manual`, `Timing`, `Windows`, `Linux`, or `SystemFont`), `Purpose=Diagnostic`, or `Manual`, `Timing`, `Windows`, `Linux`, `Vulkan`, or `SystemFont`),
`Status=KnownFailure` are excluded from the hermetic total and run through `Purpose=Diagnostic`, or `Status=KnownFailure` are excluded from the hermetic
their owned lane instead. The graph currently contains 54 projects, 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; 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. 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 ## 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: Installed-DAT tests require an explicit opt-in and a retail DAT directory:
```powershell ```powershell

View file

@ -19,6 +19,7 @@ public sealed unsafe class MeshModernSharedIndexOffscreenTests
private const uint Prefix = 2; private const uint Prefix = 2;
private static readonly object VulkanLock = new(); private static readonly object VulkanLock = new();
[Trait("Lane", "Vulkan")]
[Fact] [Fact]
public void CommittedProductionOrdinaryShader_RendersLocalSidecarsAtNonzeroTransformPrefix() public void CommittedProductionOrdinaryShader_RendersLocalSidecarsAtNonzeroTransformPrefix()
{ {

View file

@ -0,0 +1,84 @@
using System.Text.RegularExpressions;
namespace AcDream.App.Tests.Rendering.Gpu.Vk;
/// <summary>
/// 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.
/// </summary>
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*'(?<filter>[^']+)'",
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.");
}
}

View file

@ -24,7 +24,7 @@ param(
[int]$BuildTimeoutSeconds = 900, [int]$BuildTimeoutSeconds = 900,
[int]$TestTimeoutSeconds = 600, [int]$TestTimeoutSeconds = 600,
[int]$HangTimeoutSeconds = 180, [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]$SkipRestore,
[switch]$SkipBuild [switch]$SkipBuild
) )