| File: TestTriggerMap\TestTriggerMapModel.cs | Web Access |
| Project: src\tests\Infrastructure.Tests\Infrastructure.Tests.csproj (Infrastructure.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.FileSystemGlobbing; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace Infrastructure.Tests.TestTriggerMap; /// <summary> /// Strongly-typed view of <c>eng/github-ci/test-trigger-map.yml</c>. The map is curated by hand, /// so the verifier tests in <see cref="TestTriggerMapTests"/> load this model and assert it /// stays consistent with repo reality (referenced projects/jobs exist, every source project /// is reachable by some rule, etc.). /// </summary> public sealed class TestTriggerMap { public int Version { get; set; } public Dictionary<string, List<string>> Groups { get; set; } = new(); public List<ConventionRule> Conventions { get; set; } = new(); public PrefilterConfig? Prefilter { get; set; } public List<string> Ignore { get; set; } = new(); public List<PathRule> PathRules { get; set; } = new(); public List<AffectedProjectRule> AffectedProjectRules { get; set; } = new(); public List<DerivedRule> DerivedTargets { get; set; } = new(); /// <summary> /// The <c>conventions</c> patterns as plain globs (the <c><name></c> capture replaced by /// <c>*</c>), so coverage/dead-glob checks can match them against tracked files. /// </summary> public IEnumerable<string> ConventionGlobs() => Conventions.Select(c => c.Pattern.Replace("<name>", "*", StringComparison.Ordinal)); /// <summary> /// Every glob that, when matched, accounts for a changed file — the convention patterns /// (globified), the ignore globs, and every path-rule glob. Drives source-project coverage. /// </summary> public IEnumerable<string> AllSelectingGlobs() { foreach (var g in ConventionGlobs()) { yield return g; } foreach (var g in Ignore) { yield return g; } foreach (var r in PathRules) { foreach (var p in r.Paths) { yield return p; } } } /// <summary> /// Every concrete target string (<c>test:*</c> / <c>job:*</c> / <c>ALL</c> / group name) /// referenced anywhere in the map: group members, every path rule's targets, and the /// derived-target rules (both the keyed test and its targets). The <c>conventions</c> templates /// are excluded because they carry a <c><name></c> placeholder, not a concrete ref. /// </summary> public IEnumerable<string> AllReferencedTargets() { foreach (var members in Groups.Values) { foreach (var t in members) { yield return t; } } foreach (var r in PathRules) { foreach (var t in r.Targets) { yield return t; } } foreach (var r in AffectedProjectRules) { foreach (var t in r.Targets) { yield return t; } } foreach (var d in DerivedTargets) { foreach (var t in d.Tests) { yield return t; } foreach (var t in d.Targets) { yield return t; } } } /// <summary> /// Expands a <c>conventions</c> rule against a changed file (mirror of the tool's /// <c>TriggerMap.TryExpandConvention</c>): a single <c><name></c> captures one path /// segment and a trailing <c>/**</c> matches any file under it. The caller applies the existence /// guard (only a derived test that is a real matrix project is selected). /// </summary> public static bool TryExpandConvention(ConventionRule rule, string path, out string target) { target = ""; var pattern = rule.Pattern; var hasGlobSuffix = pattern.EndsWith("/**", StringComparison.Ordinal); var core = hasGlobSuffix ? pattern[..^"/**".Length] : pattern; var nameIndex = core.IndexOf("<name>", StringComparison.Ordinal); if (nameIndex < 0) { return false; } var before = core[..nameIndex]; var after = core[(nameIndex + "<name>".Length)..]; var regex = "^" + System.Text.RegularExpressions.Regex.Escape(before) + "([^/]+)" + System.Text.RegularExpressions.Regex.Escape(after) + (hasGlobSuffix ? "/.+" : "") + "$"; var match = System.Text.RegularExpressions.Regex.Match(path, regex); if (!match.Success) { return false; } target = rule.Target.Replace("<name>", match.Groups[1].Value, StringComparison.Ordinal); return true; } /// <summary>Matches a repo-relative '/'-separated path against a single map glob.</summary> public static bool GlobMatches(string glob, string path) { var matcher = new Matcher(StringComparison.Ordinal); matcher.AddInclude(glob); return matcher.Match(new[] { path }).HasMatches; } public static TestTriggerMap Load(string repoRoot) { var path = Path.Combine(repoRoot, "eng", "github-ci", "test-trigger-map.yml"); var yaml = File.ReadAllText(path); var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); return deserializer.Deserialize<TestTriggerMap>(yaml); } } /// <summary>A rule keyed by one or more path globs that selects a set of targets.</summary> public sealed class PathRule { public List<string> Paths { get; set; } = new(); public List<string> Targets { get; set; } = new(); public string? Note { get; set; } public string? Reason { get; set; } } /// <summary>A <c>conventions</c> entry: a capture pattern with a single <c><name></c> /// placeholder and a target template that substitutes the captured segment.</summary> public sealed class ConventionRule { public string Pattern { get; set; } = ""; public string Target { get; set; } = ""; } /// <summary>A <c>derived_targets</c> entry: selecting any of <see cref="Tests"/> adds <see cref="Targets"/>.</summary> public sealed class DerivedRule { public List<string> Tests { get; set; } = new(); public List<string> Targets { get; set; } = new(); public string? Reason { get; set; } } /// <summary>An <c>affected_project_rules</c> entry: an affected project (Layer 1) matching one of /// <see cref="Projects"/> by name glob adds <see cref="Targets"/>.</summary> public sealed class AffectedProjectRule { public List<string> Projects { get; set; } = new(); public List<string> Targets { get; set; } = new(); public string? Reason { get; set; } } /// <summary>The <c>prefilter</c> block: the patterns file (CI skip-gate list) read at runtime and the /// keep_routed carve-outs the selector routes to a target.</summary> public sealed class PrefilterConfig { public string? PatternsFile { get; set; } public List<string> KeepRouted { get; set; } = new(); }