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/TransitionTypes.cs"] = 2,
["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.");
}
}