namespace HSchool.AppHost.Tests; public class LaunchBuildStampTests { private static readonly DateTime Inputs = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc); private static readonly DateTime Outputs = new(2026, 1, 1, 12, 5, 0, DateTimeKind.Utc); private static readonly DateTime Newer = new(2026, 1, 1, 12, 10, 0, DateTimeKind.Utc); [Fact] public void MissingDll_NeedsBuild() { using var repo = FreshRepo(); File.Delete(repo.Dll("HSchool.Server")); Assert.True(LaunchBuildStamp.NeedsBuild(repo.Root)); } [Fact] public void FreshDlls_SkipBuild() { using var repo = FreshRepo(); Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root)); } [Fact] public void NewerCs_NeedsBuild() { using var repo = FreshRepo(); repo.Touch("src/HSchool.Server/Program.cs", Newer); Assert.True(LaunchBuildStamp.NeedsBuild(repo.Root)); } [Fact] public void NewerTsOrJsonc_SkipBuild() { using var repo = FreshRepo(); repo.Write("src/HSchool.Client/src/main.ts", "export {}"); repo.Write("src/HSchool.Server/mods/core/defs/foo.jsonc", "{}"); repo.Touch("src/HSchool.Client/src/main.ts", Newer); repo.Touch("src/HSchool.Server/mods/core/defs/foo.jsonc", Newer); Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root)); } [Fact] public void FilesInBinObjNodeModules_AreNotInputs() { using var repo = FreshRepo(); repo.Write("src/HSchool.Server/bin/Debug/net10.0/Generated.cs", "class G {}"); repo.Write("src/HSchool.Server/obj/Debug/net10.0/Generated.cs", "class G {}"); repo.Write("src/HSchool.Client/node_modules/pkg/index.cs", "class N {}"); repo.Touch("src/HSchool.Server/bin/Debug/net10.0/Generated.cs", Newer); repo.Touch("src/HSchool.Server/obj/Debug/net10.0/Generated.cs", Newer); repo.Touch("src/HSchool.Client/node_modules/pkg/index.cs", Newer); Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root)); } [Fact] public void NewerDirectoryPackagesProps_NeedsBuild() { using var repo = FreshRepo(); repo.Touch("Directory.Packages.props", Newer); Assert.True(LaunchBuildStamp.NeedsBuild(repo.Root)); } [Fact] public void ReleaseLooksAtReleaseBin() { using var repo = FreshRepo(); Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root, "Debug")); Assert.True(LaunchBuildStamp.NeedsBuild(repo.Root, "Release")); repo.WriteDll("HSchool.AppHost", "Release"); repo.WriteDll("HSchool.Server", "Release"); repo.TouchDlls("Release", Outputs); Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root, "Release")); } private static TempRepo FreshRepo() { var repo = new TempRepo(); repo.Write("global.json", "{}"); repo.Write("Directory.Build.props", ""); repo.Write("Directory.Packages.props", ""); repo.Write("src/HSchool.AppHost/AppHost.cs", "class A {}"); repo.Write("src/HSchool.AppHost/HSchool.AppHost.csproj", ""); repo.Write("src/HSchool.Server/Program.cs", "class S {}"); repo.Write("src/HSchool.Server/HSchool.Server.csproj", ""); repo.WriteDll("HSchool.AppHost"); repo.WriteDll("HSchool.Server"); repo.Touch("global.json", Inputs); repo.Touch("Directory.Build.props", Inputs); repo.Touch("Directory.Packages.props", Inputs); repo.Touch("src/HSchool.AppHost/AppHost.cs", Inputs); repo.Touch("src/HSchool.AppHost/HSchool.AppHost.csproj", Inputs); repo.Touch("src/HSchool.Server/Program.cs", Inputs); repo.Touch("src/HSchool.Server/HSchool.Server.csproj", Inputs); repo.TouchDlls("Debug", Outputs); return repo; } private sealed class TempRepo : IDisposable { public string Root { get; } = Path.Combine(Path.GetTempPath(), "h-school-stamp-" + Guid.NewGuid().ToString("N")); public TempRepo() => Directory.CreateDirectory(Root); public string Write(string relative, string contents) { var path = PathUnder(relative); var dir = Path.GetDirectoryName(path); if (dir is not null) { Directory.CreateDirectory(dir); } File.WriteAllText(path, contents); return path; } public string WriteDll(string project, string configuration = "Debug") => Write($"src/{project}/bin/{configuration}/{LaunchBuildStamp.TargetFramework}/{project}.dll", "dll"); public string Dll(string project, string configuration = "Debug") => PathUnder($"src/{project}/bin/{configuration}/{LaunchBuildStamp.TargetFramework}/{project}.dll"); public void Touch(string relative, DateTime utc) => File.SetLastWriteTimeUtc(PathUnder(relative), utc); public void TouchDlls(string configuration, DateTime utc) { foreach (var project in LaunchBuildStamp.OutputProjects) { Touch($"src/{project}/bin/{configuration}/{LaunchBuildStamp.TargetFramework}/{project}.dll", utc); } } public void Dispose() { try { Directory.Delete(Root, recursive: true); } catch (IOException) { } } private string PathUnder(string relative) => Path.Combine(Root, relative.Replace('/', Path.DirectorySeparatorChar)); } }