diff --git a/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs b/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs index 6a02fa1f91..df297f5e2b 100644 --- a/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs +++ b/TUnit.Pipeline/Modules/Abstract/TestBaseModule.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; using ModularPipelines.Context; using ModularPipelines.DotNet.Extensions; using ModularPipelines.DotNet.Options; @@ -12,28 +13,32 @@ public abstract class TestBaseModule : Module> { protected virtual IEnumerable TestableFrameworks { - get - { - yield return "net10.0"; - yield return "net8.0"; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - yield return "net472"; - } - } + get { yield return "net10.0"; } } + /// True on Windows, where legacy .NET Framework TFMs (net4xx) can be tested. + protected static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + protected sealed override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) { var results = new List(); foreach (var framework in TestableFrameworks) { - var testResult = await context.SubModule(framework, async () => + var (testOptions, executionOptions) = await GetTestOptions(context, framework, cancellationToken); + + // Test projects no longer multi-target every TFM by default (see TestProject.props). + // Skip frameworks that this project did not actually build to avoid spurious + // "process cannot find the file" errors for missing per-TFM output binaries. + var configuration = testOptions.Configuration ?? "Release"; + if (!HasFrameworkOutput(context.Logger, executionOptions, framework, configuration)) { - var (testOptions, executionOptions) = await GetTestOptions(context, framework, cancellationToken); + context.Logger.LogInformation("Skipping {Framework}: no build output found for this test project.", framework); + continue; + } + var testResult = await context.SubModule(framework, async () => + { var finalExecutionOptions = SetDefaults(testOptions, executionOptions ?? new CommandExecutionOptions(), framework); return await context.DotNet().Run(testOptions, finalExecutionOptions, cancellationToken); @@ -45,6 +50,32 @@ protected virtual IEnumerable TestableFrameworks return results; } + private static bool HasFrameworkOutput(ILogger logger, CommandExecutionOptions? executionOptions, string framework, string configuration) + { + var workingDirectory = executionOptions?.WorkingDirectory; + if (string.IsNullOrEmpty(workingDirectory)) + { + // Cannot determine — fall through to attempt the run (preserves prior behaviour). + logger.LogWarning("Cannot probe build output for {Framework}: no WorkingDirectory set on execution options.", framework); + return true; + } + + var binPath = Path.Combine(workingDirectory, "bin", configuration, framework); + + // Probe for an actual built binary, not just the directory: a stale empty + // bin// folder (e.g. from `dotnet clean`) would otherwise be treated + // as a successful build and trigger a misleading "process cannot find the file" later. + try + { + return Directory.EnumerateFiles(binPath, "*.dll", SearchOption.TopDirectoryOnly).Any() + || Directory.EnumerateFiles(binPath, "*.exe", SearchOption.TopDirectoryOnly).Any(); + } + catch (DirectoryNotFoundException) + { + return false; + } + } + private CommandExecutionOptions SetDefaults(DotNetRunOptions testOptions, CommandExecutionOptions executionOptions, string framework) { var envVars = executionOptions.EnvironmentVariables ?? new Dictionary(); @@ -70,5 +101,10 @@ private CommandExecutionOptions SetDefaults(DotNetRunOptions testOptions, Comman }; } + /// + /// Called once per framework in , before the + /// missing-output skip check. Keep this cheap — expensive work (e.g. awaiting other modules) + /// is wasted on TFMs the project did not build for. + /// protected abstract Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken); } diff --git a/TUnit.Pipeline/Modules/RunPublicAPITestsModule.cs b/TUnit.Pipeline/Modules/RunPublicAPITestsModule.cs index 47c1b363ca..6b33c7a6af 100644 --- a/TUnit.Pipeline/Modules/RunPublicAPITestsModule.cs +++ b/TUnit.Pipeline/Modules/RunPublicAPITestsModule.cs @@ -11,6 +11,23 @@ namespace TUnit.Pipeline.Modules; [NotInParallel("SnapshotTests")] public class RunPublicAPITestsModule : TestBaseModule { + // Public API snapshots cover every supported consumer TFM — keep aligned with + // in TUnit.PublicAPI.csproj. + protected override IEnumerable TestableFrameworks + { + get + { + yield return "net10.0"; + yield return "net9.0"; + yield return "net8.0"; + + if (IsWindows) + { + yield return "net472"; + } + } + } + protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.PublicAPI.csproj").AssertExists(); diff --git a/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs b/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs index 959bdd7ce1..33d670bacf 100644 --- a/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs +++ b/TUnit.Pipeline/Modules/RunSourceGeneratorTestsModule.cs @@ -11,6 +11,22 @@ namespace TUnit.Pipeline.Modules; [NotInParallel("SnapshotTests")] public class RunSourceGeneratorTestsModule : TestBaseModule { + // Generator output snapshots verify behaviour across consumer TFMs — keep aligned with + // in TUnit.Core.SourceGenerator.Tests.csproj. + protected override IEnumerable TestableFrameworks + { + get + { + yield return "net10.0"; + yield return "net8.0"; + + if (IsWindows) + { + yield return "net472"; + } + } + } + protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) { var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Core.SourceGenerator.Tests.csproj").AssertExists(); diff --git a/TUnit.Pipeline/Modules/TestNugetPackageModule.cs b/TUnit.Pipeline/Modules/TestNugetPackageModule.cs index 2a728ab766..5e91013f87 100644 --- a/TUnit.Pipeline/Modules/TestNugetPackageModule.cs +++ b/TUnit.Pipeline/Modules/TestNugetPackageModule.cs @@ -1,5 +1,4 @@ -using System.Runtime.InteropServices; -using ModularPipelines.Attributes; +using ModularPipelines.Attributes; using ModularPipelines.Configuration; using ModularPipelines.Context; using ModularPipelines.DotNet.Options; @@ -44,7 +43,7 @@ protected override IEnumerable TestableFrameworks yield return "net10.0"; yield return "net8.0"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (IsWindows) { yield return "net481"; yield return "net48";