using System.Text.RegularExpressions; namespace AcDream.App.Tests.Diagnostics; /// /// Keeps docs/launch-options.md honest: every environment variable the /// shipped client reads must have a documented row, and every documented row /// must name a variable something actually reads. /// /// /// /// A hand-maintained list of ~170 flags goes stale within a week. #432 is what /// that costs: ACDREAM_AUTOMATION_ARTIFACT_DIR read like an output-path /// setting but also constructed a per-frame diagnostics referee worth ~6 MB and /// ~14 ms every frame, and three days of measurements were taxed before anyone /// noticed. The same audit found CLAUDE.md still advertising /// ACDREAM_RUN_SKILL / ACDREAM_JUMP_SKILL after their read sites /// were deleted. Both directions of drift are failures, so both fail here. /// /// /// Scope is src/ only. Test-owned variables, shader-compiler macro /// tokens under tools/, and historical mentions in dated research /// documents are deliberately out of scope — the doc describes what a launched /// client reads today. /// /// public sealed class LaunchOptionsDocumentationTests { private const string DocRelativePath = "docs/launch-options.md"; /// /// Structure rule 5 wants runtime flags behind diagnostic owner classes and /// rule 4 wants startup configuration in RuntimeOptions. These files /// still read the environment directly, with the exact number of distinct /// flags each one reads today. /// /// /// The counts are FROZEN, not merely the file names: promoting a stray to /// its subsystem's owner lowers a number (update it here), and adding a new /// direct read raises one and fails. A file that reaches zero leaves the /// table entirely. /// private static readonly IReadOnlyDictionary DirectReadDebt = new Dictionary(StringComparer.Ordinal) { ["src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs"] = 2, ["src/AcDream.App/Physics/RemoteServerControlledVelocityCycle.cs"] = 1, ["src/AcDream.App/Platform/GraphicalWindowBackendSelection.cs"] = 1, ["src/AcDream.App/Rendering/GameWindow.cs"] = 1, ["src/AcDream.App/Rendering/PortalVisibilityBuilder.cs"] = 1, ["src/AcDream.App/Rendering/Sky/SkyRenderer.cs"] = 1, ["src/AcDream.App/Rendering/TextureCache.cs"] = 1, ["src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs"] = 2, ["src/AcDream.Core/Physics/PhysicsEngine.cs"] = 1, ["src/AcDream.Core/Physics/TransitionTypes.cs"] = 3, ["src/AcDream.Core/Vfx/PhysicsScriptRunner.cs"] = 1, ["src/AcDream.Core/World/SkyDescLoader.cs"] = 2, ["src/AcDream.Core.Net/GameEventWiring.cs"] = 1, ["src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs"] = 1, ["src/AcDream.Core.Net/Messages/UpdateMotion.cs"] = 1, ["src/AcDream.Core.Net/WorldSession.cs"] = 3, ["src/AcDream.Platform/ApplicationPathSet.cs"] = 3, ["src/AcDream.Platform/BakePublicationGuardPaths.cs"] = 1, ["src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs"] = 1, ["src/AcDream.UI.Abstractions/Settings/QualityPreset.cs"] = 6, }; /// /// Any ACDREAM_* string literal in src/. Matching the literal /// rather than a GetEnvironmentVariable call is deliberate: the /// startup path reads through an injected env delegate /// (RuntimeOptions.Parse) so a call-shaped pattern silently missed /// ACDREAM_LIVE, ACDREAM_PAK_PATH and every other production flag. Comment /// mentions (shader macros such as ACDREAM_SAMPLE_2D, prefix fragments) /// carry no quotes and stay out. /// private static readonly Regex EnvironmentRead = new( @"""(ACDREAM_[A-Z0-9_]+)""", RegexOptions.Compiled); private static readonly Regex DocumentedRow = new( @"^\|\s*`(ACDREAM_[A-Z0-9_]+)`", RegexOptions.Compiled | RegexOptions.Multiline); [Fact] public void EveryEnvironmentVariableTheClientReadsIsDocumented() { IReadOnlySet read = ReadFlags(); IReadOnlySet documented = DocumentedFlags(); List undocumented = read.Except(documented).Order(StringComparer.Ordinal).ToList(); Assert.True( undocumented.Count == 0, $"{DocRelativePath} is missing a row for flags the client reads: " + string.Join(", ", undocumented) + ". Add the row in the same commit that adds the read site."); } [Fact] public void EveryDocumentedEnvironmentVariableStillExists() { IReadOnlySet read = ReadFlags(); IReadOnlySet documented = DocumentedFlags(); List phantom = documented.Except(read).Order(StringComparer.Ordinal).ToList(); Assert.True( phantom.Count == 0, $"{DocRelativePath} documents flags nothing in src/ reads: " + string.Join(", ", phantom) + ". Delete the row (or move it to the retired section with its " + "removal commit) in the same commit that deletes the read site."); } [Fact] public void DirectEnvironmentReadsOutsideOwnerClassesDoNotGrow() { Dictionary actual = SourceFiles() .Where(file => !IsOwnerClass(file.RelativePath)) .Select(file => ( file.RelativePath, Count: DistinctFlagsRead(File.ReadAllText(file.Path)))) .Where(file => file.Count > 0) .ToDictionary(file => file.RelativePath, file => file.Count, StringComparer.Ordinal); List problems = []; foreach ((string path, int count) in actual.OrderBy(pair => pair.Key, StringComparer.Ordinal)) { if (!DirectReadDebt.TryGetValue(path, out int frozen)) { problems.Add( $"{path} reads {count} ACDREAM_* variable(s) directly but is " + "not an owner class. Add the property to the subsystem's " + "diagnostics owner (or RuntimeOptions) and read it there."); } else if (count > frozen) { problems.Add( $"{path} grew from {frozen} to {count} direct reads. New " + "flags belong in an owner class, not here."); } } foreach ((string path, int frozen) in DirectReadDebt.OrderBy(pair => pair.Key, StringComparer.Ordinal)) { int count = actual.GetValueOrDefault(path, 0); if (count < frozen) { problems.Add( $"{path} is down to {count} direct read(s) from {frozen}. " + "Lower the frozen count (or drop the entry at zero) so the " + "debt cannot silently grow back."); } } Assert.True(problems.Count == 0, string.Join("\n", problems)); } private static bool IsOwnerClass(string relativePath) { // The intended homes for environment reads: one static diagnostics // class per subsystem, the typed startup options objects, the // executables' own entry points, and the single-purpose probe/capture // owners (a file that exists only to own one probe already satisfies // the rule the *Diagnostics.cs suffix encodes). string name = Path.GetFileName(relativePath); return name.EndsWith("Diagnostics.cs", StringComparison.Ordinal) || name.EndsWith("Options.cs", StringComparison.Ordinal) || name.EndsWith("Probe.cs", StringComparison.Ordinal) || name.EndsWith("Capture.cs", StringComparison.Ordinal) || name == "Program.cs"; } private static int DistinctFlagsRead(string source) { var flags = new HashSet(StringComparer.Ordinal); foreach (Match match in EnvironmentRead.Matches(source)) flags.Add(match.Groups[1].Value); return flags.Count; } private static IReadOnlySet ReadFlags() { var flags = new HashSet(StringComparer.Ordinal); foreach ((string path, _) in SourceFiles()) { foreach (Match match in EnvironmentRead.Matches(File.ReadAllText(path))) flags.Add(match.Groups[1].Value); } Assert.True( flags.Count > 100, $"Only found {flags.Count} environment reads in src/; the scanner " + "is probably broken rather than the codebase suddenly clean."); return flags; } private static IReadOnlySet DocumentedFlags() { string doc = Path.Combine(FindRepoRoot(), DocRelativePath.Replace('/', Path.DirectorySeparatorChar)); Assert.True(File.Exists(doc), $"{DocRelativePath} is missing."); string text = File.ReadAllText(doc); // Rows below the retired marker describe flags that are deliberately // gone; they document history and must not resurrect the read-site // requirement. int retired = text.IndexOf("", StringComparison.Ordinal); if (retired >= 0) text = text[..retired]; var flags = new HashSet(StringComparer.Ordinal); foreach (Match match in DocumentedRow.Matches(text)) flags.Add(match.Groups[1].Value); return flags; } private static IEnumerable<(string Path, string RelativePath)> SourceFiles() { string root = FindRepoRoot(); string src = Path.Combine(root, "src"); foreach (string path in Directory.EnumerateFiles(src, "*.cs", SearchOption.AllDirectories)) { string relative = Path.GetRelativePath(root, path).Replace('\\', '/'); if (relative.Contains("/bin/", StringComparison.Ordinal) || relative.Contains("/obj/", StringComparison.Ordinal)) { continue; } yield return (path, relative); } } private static string FindRepoRoot() { DirectoryInfo? directory = new(AppContext.BaseDirectory); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) return directory.FullName; directory = directory.Parent; } throw new DirectoryNotFoundException("Could not find AcDream.slnx."); } }