| File: Commands\Test\MTP\ArtifactPostProcessingPlanner.cs | Web Access |
| Project: src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.Cli.Commands.Test.Terminal; using NuGet.Frameworks; namespace Microsoft.DotNet.Cli.Commands.Test; internal sealed record ArtifactPostProcessingApplication( TestModule Module, string? TargetFramework, string? Architecture, IReadOnlySet<string> SupportedKinds, IReadOnlySet<string> SupportedExtensions); internal sealed record ArtifactPostProcessingArtifact( string Path, string? Kind, string ProducingTestModule, string? TargetFramework, string? Architecture, string ExecutionId); internal sealed record ArtifactPostProcessingGroup( string Key, bool IsKind, IReadOnlyList<ArtifactPostProcessingArtifact> Artifacts, IReadOnlyList<ArtifactPostProcessingApplication> Candidates); internal sealed record ArtifactPostProcessingJob( ArtifactPostProcessingApplication Application, IReadOnlyList<ArtifactPostProcessingGroup> Groups); internal sealed record ArtifactPostProcessingPlan(IReadOnlyList<ArtifactPostProcessingJob> Jobs); internal static class ArtifactPostProcessingPlanner { private const string MicrosoftCodeCoverageKind = "microsoft.codecoverage"; private const string MicrosoftCodeCoverageExtension = ".coverage"; public static ArtifactPostProcessingPlan Plan( IReadOnlyList<ArtifactPostProcessingApplication> applications, IReadOnlyList<ArtifactPostProcessingArtifact> artifacts) { ArtifactPostProcessingArtifact[] distinctArtifacts = [.. artifacts.DistinctBy(artifact => artifact.Path, FileUtilities.PathComparer)]; List<ArtifactPostProcessingGroup> groups = []; foreach (IGrouping<string, ArtifactPostProcessingArtifact> group in distinctArtifacts .Where(artifact => artifact.Kind is not null) .GroupBy(artifact => artifact.Kind!, StringComparer.Ordinal) .OrderBy(group => group.Key, StringComparer.Ordinal)) { AddGroup(group.Key, isKind: true, [.. group]); } foreach (IGrouping<string, ArtifactPostProcessingArtifact> group in distinctArtifacts .Where(artifact => artifact.Kind is null) .GroupBy(artifact => Path.GetExtension(artifact.Path).ToLowerInvariant(), StringComparer.Ordinal) .Where(group => group.Key.Length > 0) .OrderBy(group => group.Key, StringComparer.Ordinal)) { AddGroup(group.Key, isKind: false, [.. group]); } ArtifactPostProcessingGroup[] mergeableGroups = [ .. groups.Where(group => group.Artifacts.Count >= 2 || groups.Any(other => CanCombineKindAndExtensionGroups(group, other))) ]; var uncoveredGroups = new HashSet<ArtifactPostProcessingGroup>(mergeableGroups); List<ArtifactPostProcessingJob> jobs = []; while (uncoveredGroups.Count > 0) { ArtifactPostProcessingApplication? winner = applications .Select(application => new { Application = application, Groups = uncoveredGroups.Where(group => group.Candidates.Contains(application)).ToArray(), }) .Where(candidate => candidate.Groups.Length > 0) .OrderByDescending(candidate => candidate.Groups.Length) .ThenByDescending(candidate => candidate.Groups.Sum(group => group.Artifacts.Count(artifact => FileUtilities.PathComparer.Equals( artifact.ProducingTestModule, candidate.Application.Module.TargetPath)))) .ThenByDescending(candidate => GetFrameworkVersion(candidate.Application.TargetFramework)) .ThenBy(candidate => candidate.Application.Module.TargetPath, FileUtilities.PathComparer) .Select(candidate => candidate.Application) .FirstOrDefault(); if (winner is null) { break; } ArtifactPostProcessingGroup[] coveredGroups = [.. uncoveredGroups.Where(group => group.Candidates.Contains(winner))]; jobs.Add(new ArtifactPostProcessingJob(winner, coveredGroups)); uncoveredGroups.ExceptWith(coveredGroups); } return new ArtifactPostProcessingPlan(jobs); void AddGroup(string key, bool isKind, ArtifactPostProcessingArtifact[] inputs) { ArtifactPostProcessingApplication[] candidates = [ .. applications.Where(application => (isKind ? application.SupportedKinds.Contains(key) : application.SupportedExtensions.Contains(key)) && IsArchitectureCompatible(key, isKind, application, inputs)) ]; if (candidates.Length > 0) { groups.Add(new ArtifactPostProcessingGroup(key, isKind, inputs, candidates)); } } } private static bool CanCombineKindAndExtensionGroups( ArtifactPostProcessingGroup first, ArtifactPostProcessingGroup second) { ArtifactPostProcessingGroup kindGroup = first.IsKind ? first : second; ArtifactPostProcessingGroup extensionGroup = first.IsKind ? second : first; return first.IsKind != second.IsKind && kindGroup.Artifacts.Any(artifact => string.Equals( Path.GetExtension(artifact.Path), extensionGroup.Key, StringComparison.OrdinalIgnoreCase)) && kindGroup.Candidates.Intersect(extensionGroup.Candidates).Any() && kindGroup.Artifacts.Count + extensionGroup.Artifacts.Count >= 2; } private static bool IsArchitectureCompatible( string key, bool isKind, ArtifactPostProcessingApplication application, IReadOnlyList<ArtifactPostProcessingArtifact> artifacts) { bool isArchitectureSensitive = isKind ? string.Equals(key, MicrosoftCodeCoverageKind, StringComparison.Ordinal) : string.Equals(key, MicrosoftCodeCoverageExtension, StringComparison.Ordinal); return !isArchitectureSensitive || artifacts.All(artifact => artifact.Architecture is null || string.Equals(artifact.Architecture, application.Architecture, StringComparison.OrdinalIgnoreCase)); } private static Version GetFrameworkVersion(string? targetFramework) => string.IsNullOrEmpty(targetFramework) ? new Version() : NuGetFramework.ParseFolder(targetFramework).Version; }