diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 94fc7dd310b..2cef99c797d 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -72,23 +72,11 @@ jobs: if: matrix.os == 'windows-latest' uses: microsoft/setup-msbuild@v3 + # No NuGet or Playwright cache: PR-scoped entries can't be shared between PRs, and at + # ~2.4 GB each they pushed the repository past its cache quota, so every run missed and + # then spent up to 3 minutes saving an entry that was evicted within the hour. - name: Setup .NET uses: actions/setup-dotnet@v6.0.0 - with: - cache: true - cache-dependency-path: Directory.Packages.props - - - name: Cache Playwright Browsers - uses: actions/cache@v6 - continue-on-error: true - with: - path: | - ~/.cache/ms-playwright - ~/Library/Caches/ms-playwright - ~/AppData/Local/ms-playwright - key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/package.json', '**/Directory.Packages.props') }} - restore-keys: | - playwright-${{ runner.os }}- - name: Enable Long Paths on Windows if: matrix.os == 'windows-latest' @@ -136,17 +124,6 @@ jobs: "-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}" "-p:PackageVersion=${{ steps.gitversion.outputs.fullSemVer }}" - - name: Publish AOT - shell: bash - run: >- - dotnet publish tests/TUnit.TestProject/TUnit.TestProject.csproj -c Release - --use-current-runtime -p:Aot=true -o TESTPROJECT_AOT --framework net10.0 - "-p:Version=${{ steps.gitversion.outputs.semVer }}" - "-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }}" - "-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }}" - "-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}" - "-p:PackageVersion=${{ steps.gitversion.outputs.fullSemVer }}" - - name: Expose GitHub Actions Runtime uses: actions/github-script@v9 with: @@ -169,11 +146,6 @@ jobs: nuget-apikey: ${{ steps.login.outputs.NUGET_API_KEY }} publish-packages: ${{ (github.event.inputs.publish-packages || false) && matrix.os == 'ubuntu-latest' }} - - name: Compile documentation snippets - if: matrix.os == 'ubuntu-latest' - shell: pwsh - run: ./scripts/Verify-DocSnippets.ps1 -PackagesPath . -Version $env:PackageVersion - - name: Upload Diagnostic Logs uses: actions/upload-artifact@v7.0.2 if: always() @@ -207,3 +179,72 @@ jobs: with: name: 'NuGetPackages-${{matrix.os}}' path: '**/*.*nupkg' + + # Native AOT publishes of the mock test projects, one runner per framework. Run inside the + # Ubuntu pipeline they took ~12 minutes serially and held up packaging behind them. + mock-aot: + name: mock AOT (${{ matrix.framework }}) + strategy: + matrix: + framework: [net8.0, net9.0, net10.0] + fail-fast: false + runs-on: ubuntu-latest + permissions: + contents: read + env: + DISABLE_GITHUB_REPORTER: 'true' + + steps: + - uses: actions/checkout@v7.0.1 + + - name: Setup .NET + uses: actions/setup-dotnet@v6.0.0 + + # TUnit.Mocks.Aot.Tests sets TreatWarningsAsErrors, so any trim/AOT warning ILC reports + # for everyday mock usage fails its publish (#7027). + - name: Publish + shell: bash + run: | + for project in TUnit.Mocks.Tests TUnit.Mocks.Aot.Tests; do + dotnet publish "tests/$project/$project.csproj" -c Release --use-current-runtime \ + -p:Aot=true --framework ${{ matrix.framework }} -o "aot/$project" + done + + - name: Run + shell: bash + run: | + for project in TUnit.Mocks.Tests TUnit.Mocks.Aot.Tests; do + "aot/$project/$project" + done + + # Compiles the C# fences in the documentation against freshly packed packages, alongside the + # test matrix rather than after the Ubuntu pipeline. + doc-snippets: + runs-on: ubuntu-latest + permissions: + contents: read + env: + DocsVersion: 1.0.0-docs + + steps: + - uses: actions/checkout@v7.0.1 + + - name: Setup .NET + uses: actions/setup-dotnet@v6.0.0 + + # Packing reads the Roslyn analyzer/generator variants from their own bin folders, so build + # every library project, not just the packed ones. + - name: Build libraries + shell: bash + run: | + grep -oE 'src/[^"]+\.(cs|fs|vb)proj' TUnit.CI.slnx | grep -v '/content/' \ + | jq -R . | jq -s '{solution: {path: "TUnit.CI.slnx", projects: .}}' > libraries.slnf + dotnet build libraries.slnf -c Release "-p:Version=$DocsVersion" + + - name: Pack + shell: bash + run: dotnet pack libraries.slnf -c Release --no-build "-p:Version=$DocsVersion" -o artifacts/doc-packages + + - name: Compile documentation snippets + shell: pwsh + run: ./scripts/Verify-DocSnippets.ps1 -PackagesPath artifacts/doc-packages -Version $env:DocsVersion diff --git a/tests/TUnit.RpcTests/TestHostSession.cs b/tests/TUnit.RpcTests/TestHostSession.cs index bcb83d5c09b..0ec670dae4e 100644 --- a/tests/TUnit.RpcTests/TestHostSession.cs +++ b/tests/TUnit.RpcTests/TestHostSession.cs @@ -51,7 +51,7 @@ public static async Task StartAsync(string framework, Cancellat .WithArguments([ "run", "--no-build", - "-c", "Debug", + "-c", TestProjectBuilds.Configuration, "-f", framework, "--server", "--client-port", port.ToString() diff --git a/tests/TUnit.RpcTests/TestProjectBuilds.cs b/tests/TUnit.RpcTests/TestProjectBuilds.cs index 94cff67706c..06a78a15c27 100644 --- a/tests/TUnit.RpcTests/TestProjectBuilds.cs +++ b/tests/TUnit.RpcTests/TestProjectBuilds.cs @@ -13,14 +13,25 @@ internal static class TestProjectBuilds public static string WorkingDirectory { get; } = Sourcy.DotNet.Projects.TUnit_TestProject.DirectoryName!; + /// The test host is built and run in the same configuration as this suite. +#if DEBUG + public const string Configuration = "Debug"; +#else + public const string Configuration = "Release"; +#endif + + // Set by the CI pipeline, whose solution build has already produced the test host. + private static readonly bool IsPrebuilt = + Environment.GetEnvironmentVariable("TUNIT_RPC_TESTPROJECT_PREBUILT") == "true"; + public static Task EnsureBuiltAsync(string framework, CancellationToken cancellationToken) - => _builds.GetOrAdd(framework, tfm => BuildAsync(tfm, cancellationToken)); + => IsPrebuilt ? Task.CompletedTask : _builds.GetOrAdd(framework, tfm => BuildAsync(tfm, cancellationToken)); private static async Task BuildAsync(string framework, CancellationToken cancellationToken) { var result = await Cli.Wrap("dotnet") .WithWorkingDirectory(WorkingDirectory) - .WithArguments(["build", "-c", "Debug", "-f", framework, "--nologo", "-v", "quiet"]) + .WithArguments(["build", "-c", Configuration, "-f", framework, "--nologo", "-v", "quiet"]) .WithValidation(CommandResultValidation.None) .ExecuteAsync(cancellationToken); diff --git a/tools/TUnit.Pipeline/Modules/PackTUnitFilesModule.cs b/tools/TUnit.Pipeline/Modules/PackTUnitFilesModule.cs index feb015efad4..ff4ae942d40 100644 --- a/tools/TUnit.Pipeline/Modules/PackTUnitFilesModule.cs +++ b/tools/TUnit.Pipeline/Modules/PackTUnitFilesModule.cs @@ -8,14 +8,15 @@ namespace TUnit.Pipeline.Modules; -// Must run after any module that may recompile packable projects (TUnit.Mocks et al). +// Must run after any module that may recompile packable projects (the AOT publish rebuilds its +// TUnit.* references). // This module uses NoBuild=true and packs whatever bin/Release/{tfm}/*.dll is on disk, so // a racing recompile without version props would ship strong-name-mismatched packages. // Version props come from $GITHUB_ENV (see .github/workflows/dotnet.yml) so recompiles // also stamp the correct AssemblyVersion. See issue #5622. [DependsOn] [DependsOn] -[DependsOn] +[DependsOn] public class PackTUnitFilesModule : Module> { // Packages in beta ship with a "-beta" suffix appended to their version. The suffix is diff --git a/tools/TUnit.Pipeline/Modules/PublishMockTestsAOTModule.cs b/tools/TUnit.Pipeline/Modules/PublishMockTestsAOTModule.cs deleted file mode 100644 index 2edbd829c9f..00000000000 --- a/tools/TUnit.Pipeline/Modules/PublishMockTestsAOTModule.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Runtime.InteropServices; -using ModularPipelines.Attributes; -using ModularPipelines.Configuration; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; -using ModularPipelines.DotNet.Options; -using ModularPipelines.Extensions; -using ModularPipelines.Git.Extensions; -using ModularPipelines.Models; -using ModularPipelines.Modules; -using ModularPipelines.Options; - -namespace TUnit.Pipeline.Modules; - -[DependsOn] -public class PublishMockTestsAOTModule : Module> -{ - internal static readonly (string ProjectName, string OutputPrefix)[] AotProjects = - [ - ("TUnit.Mocks.Tests", "MOCKTESTS_AOT"), - ("TUnit.Mocks.Aot.Tests", "MOCKAOTTESTS_AOT"), - ]; - - protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() - .WithSkipWhen(_ => EnvironmentVariables.IsNetFramework || !RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? SkipDecision.Skip("Only runs on Linux") - : SkipDecision.DoNotSkip) - .Build(); - - protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) - { - var results = new List(); - - var rootDir = context.Git().RootDirectory.AssertExists().Path; - - // TUnit.Mocks.Aot.Tests sets TreatWarningsAsErrors, so any trim/AOT warning ILC reports - // for everyday mock usage fails its publish (#7027). - foreach (var (projectName, outputPrefix) in AotProjects) - { - var testProject = context.Git() - .RootDirectory - .AssertExists() - .FindFile(x => x.Name == $"{projectName}.csproj") - .AssertExists(); - - foreach (var framework in new[] { "net8.0", "net9.0", "net10.0" }) - { - var result = await context.SubModule($"AOT-Publish-{projectName}-{framework}", async () => - { - return await context.DotNet().Publish(new DotNetPublishOptions - { - ProjectSolution = testProject.Path, - Runtime = RuntimeIdentifiers.Current, - Configuration = "Release", - Output = Path.Combine(rootDir, $"{outputPrefix}_{framework}"), - Properties = - [ - new KeyValue("Aot", "true"), - ], - Framework = framework, - }, new CommandExecutionOptions - { - LogSettings = new CommandLoggingOptions - { - ShowCommandArguments = true, - ShowStandardError = true, - ShowExecutionTime = true, - ShowExitCode = true - } - }, cancellationToken); - }); - - results.Add(result); - } - } - - return results; - } -} diff --git a/tools/TUnit.Pipeline/Modules/PublishTestProjectAOTModule.cs b/tools/TUnit.Pipeline/Modules/PublishTestProjectAOTModule.cs new file mode 100644 index 00000000000..32e46e273b8 --- /dev/null +++ b/tools/TUnit.Pipeline/Modules/PublishTestProjectAOTModule.cs @@ -0,0 +1,45 @@ +using ModularPipelines.Context; +using ModularPipelines.DotNet.Extensions; +using ModularPipelines.DotNet.Options; +using ModularPipelines.Extensions; +using ModularPipelines.Git.Extensions; +using ModularPipelines.Models; +using ModularPipelines.Modules; +using ModularPipelines.Options; + +namespace TUnit.Pipeline.Modules; + +// Publishes the TESTPROJECT_AOT app that the engine tests' AOT mode runs. As a module it overlaps +// the first test modules instead of delaying the whole pipeline. Version props come from +// $GITHUB_ENV (see .github/workflows/dotnet.yml), so any recompile it triggers stamps the correct +// AssemblyVersion; PackTUnitFilesModule still waits for it (issue #5622). +public class PublishTestProjectAOTModule : Module +{ + protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + var rootDir = context.Git().RootDirectory.AssertExists(); + var project = rootDir.FindFile(x => x.Name == "TUnit.TestProject.csproj").AssertExists(); + + return await context.DotNet().Publish(new DotNetPublishOptions + { + ProjectSolution = project.Path, + Runtime = RuntimeIdentifiers.Current, + Configuration = "Release", + Output = Path.Combine(rootDir.Path, "TESTPROJECT_AOT"), + Properties = + [ + new KeyValue("Aot", "true"), + ], + Framework = "net10.0", + }, new CommandExecutionOptions + { + LogSettings = new CommandLoggingOptions + { + ShowCommandArguments = true, + ShowStandardError = true, + ShowExecutionTime = true, + ShowExitCode = true + } + }, cancellationToken); + } +} diff --git a/tools/TUnit.Pipeline/Modules/RunEngineTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunEngineTestsModule.cs index b57042b0c05..bdc5d7ef829 100644 --- a/tools/TUnit.Pipeline/Modules/RunEngineTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunEngineTestsModule.cs @@ -25,6 +25,7 @@ namespace TUnit.Pipeline.Modules; [DependsOn] [DependsOn] [DependsOn] +[DependsOn] public class RunEngineTestsModule : Module { protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) diff --git a/tools/TUnit.Pipeline/Modules/RunMockTestsAOTModule.cs b/tools/TUnit.Pipeline/Modules/RunMockTestsAOTModule.cs deleted file mode 100644 index 2298bf49d20..00000000000 --- a/tools/TUnit.Pipeline/Modules/RunMockTestsAOTModule.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Runtime.InteropServices; -using ModularPipelines.Attributes; -using ModularPipelines.Configuration; -using ModularPipelines.Context; -using ModularPipelines.Extensions; -using ModularPipelines.Git.Extensions; -using ModularPipelines.Models; -using ModularPipelines.Modules; -using ModularPipelines.Options; - -namespace TUnit.Pipeline.Modules; - -[DependsOn] -public class RunMockTestsAOTModule : Module> -{ - protected override ModuleConfiguration Configure() => ModuleConfiguration.Create() - .WithSkipWhen(_ => EnvironmentVariables.IsNetFramework || !RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? SkipDecision.Skip("Only runs on Linux") - : SkipDecision.DoNotSkip) - .Build(); - - protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) - { - var results = new List(); - - var rootDir = context.Git().RootDirectory.AssertExists().Path; - - foreach (var (projectName, outputPrefix) in PublishMockTestsAOTModule.AotProjects) - { - foreach (var framework in new[] { "net8.0", "net9.0", "net10.0" }) - { - var exePath = Path.Combine(rootDir, $"{outputPrefix}_{framework}", projectName); - - var runResult = await context.SubModule($"Run-AOT-{projectName}-{framework}", async () => - { - return await context.Shell.Bash.Command( - new BashCommandOptions($"DISABLE_GITHUB_REPORTER=true \"{exePath}\""), - cancellationToken); - }); - - results.Add(runResult); - } - } - - return results; - } -} diff --git a/tools/TUnit.Pipeline/Modules/RunRpcTestsModule.cs b/tools/TUnit.Pipeline/Modules/RunRpcTestsModule.cs index b422fff5b42..0ff61a436b7 100644 --- a/tools/TUnit.Pipeline/Modules/RunRpcTestsModule.cs +++ b/tools/TUnit.Pipeline/Modules/RunRpcTestsModule.cs @@ -9,6 +9,9 @@ namespace TUnit.Pipeline.Modules; [NotInParallel("NetworkTests")] +// The RPC tests `dotnet run` TUnit.TestProject, which reads the obj/project.assets.json the AOT +// publish rewrites for its RID; keep them apart (an overlapping restore caused NETSDK1047). +[DependsOn] public class RunRpcTestsModule : TestBaseModule { protected override IEnumerable TestableFrameworks => @@ -34,6 +37,8 @@ public class RunRpcTestsModule : TestBaseModule EnvironmentVariables = new Dictionary { ["DISABLE_GITHUB_REPORTER"] = "true", + // The workflow's solution build already produced the Release test host. + ["TUNIT_RPC_TESTPROJECT_PREBUILT"] = "true", } } ));