The launcher (LA3/LA4) needs the XDG/Windows path contract (ApplicationPathSet/IApplicationPathEnvironment) without pulling in any gameplay assembly. Move it out of AcDream.Runtime into a new BCL-only AcDream.Platform project so the launcher-side Launcher.Core project can reference it directly per the campaign plan (docs/plans/2026-08-14-launcher-campaign.md, LA0). Namespace renamed AcDream.Runtime.Platform -> AcDream.Platform; code is otherwise byte-identical (no logic changes). AcDream.Runtime now carries a ProjectReference to AcDream.Platform and re-exports it transitively, so App and Headless keep resolving the type without a direct reference and K0's Headless single-ProjectReference guard (HeadlessAssemblyReferencesOnlyTheRuntimeProject) stands unchanged. The sibling Runtime dependency-boundary guard (RuntimeProjectDeclaresOnlyApprovedProjectDependencies) does assert Runtime's own project-reference set, so it needed a deliberate, documented addition of AcDream.Platform to its expected list. Moved tests/AcDream.Runtime.Tests/Platform/ApplicationPathSetTests.cs to a new tests/AcDream.Platform.Tests/ project (namespace AcDream.Platform.Tests) referencing only AcDream.Platform. Registered both new projects in AcDream.slnx. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
154 lines
4.9 KiB
C#
154 lines
4.9 KiB
C#
using System.Text.Json;
|
|
using System.Xml.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace AcDream.Runtime.Tests;
|
|
|
|
public sealed class RuntimeDependencyBoundaryTests
|
|
{
|
|
private static readonly string[] ForbiddenDependencyPrefixes =
|
|
[
|
|
"AcDream.App",
|
|
"AcDream.UI.",
|
|
"Silk.NET",
|
|
"OpenAL",
|
|
"Arch",
|
|
"ImGui",
|
|
];
|
|
|
|
[Fact]
|
|
public void RuntimeAssemblyHasNoDirectPresentationOrBackendReferences()
|
|
{
|
|
var references = typeof(RuntimeAssemblyMarker).Assembly
|
|
.GetReferencedAssemblies()
|
|
.Select(static reference => reference.Name ?? string.Empty)
|
|
.ToArray();
|
|
|
|
Assert.DoesNotContain(references, IsForbidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void RuntimeDependencyClosureHasNoPresentationOrBackendLibraries()
|
|
{
|
|
var depsPath = Path.ChangeExtension(
|
|
typeof(RuntimeDependencyBoundaryTests).Assembly.Location,
|
|
".deps.json");
|
|
|
|
using var document = JsonDocument.Parse(File.ReadAllText(depsPath));
|
|
var libraries = document.RootElement
|
|
.GetProperty("libraries")
|
|
.EnumerateObject()
|
|
.Select(static library => LibraryName(library.Name))
|
|
.ToArray();
|
|
|
|
Assert.DoesNotContain(libraries, IsForbidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void RuntimeProjectDeclaresOnlyApprovedProjectDependencies()
|
|
{
|
|
var repositoryRoot = FindRepositoryRoot();
|
|
var projectPath = Path.Combine(
|
|
repositoryRoot,
|
|
"src",
|
|
"AcDream.Runtime",
|
|
"AcDream.Runtime.csproj");
|
|
var project = XDocument.Load(projectPath);
|
|
var projectDirectory = Path.GetDirectoryName(projectPath)!;
|
|
|
|
var actualReferences = project
|
|
.Descendants("ProjectReference")
|
|
.Select(reference => reference.Attribute("Include")?.Value)
|
|
.Where(static include => !string.IsNullOrWhiteSpace(include))
|
|
.Select(include => Path.GetFullPath(Path.Combine(
|
|
projectDirectory,
|
|
include!.Replace(
|
|
'\\',
|
|
Path.DirectorySeparatorChar))))
|
|
.Order(StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
var expectedReferences = new[]
|
|
{
|
|
"AcDream.Content",
|
|
"AcDream.Core",
|
|
"AcDream.Core.Net",
|
|
// Campaign LA LA0: AcDream.Platform is the new BCL-only
|
|
// ApplicationPathSet home; Runtime re-exports it
|
|
// transitively so App/Headless keep reaching it without a
|
|
// direct reference.
|
|
"AcDream.Platform",
|
|
"AcDream.Plugin.Abstractions",
|
|
}
|
|
.Select(projectName => Path.Combine(
|
|
repositoryRoot,
|
|
"src",
|
|
projectName,
|
|
$"{projectName}.csproj"))
|
|
.Order(StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
|
|
Assert.Equal(expectedReferences, actualReferences);
|
|
Assert.Empty(project.Descendants("PackageReference"));
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadingRuntimeMarkerDoesNotLoadPresentationOrBackendAssemblies()
|
|
{
|
|
_ = typeof(RuntimeAssemblyMarker).Assembly;
|
|
|
|
var loadedAssemblies = AppDomain.CurrentDomain
|
|
.GetAssemblies()
|
|
.Select(static assembly => assembly.GetName().Name ?? string.Empty)
|
|
.ToArray();
|
|
|
|
Assert.DoesNotContain(loadedAssemblies, IsForbidden);
|
|
}
|
|
|
|
private static string FindRepositoryRoot(
|
|
[CallerFilePath] string sourcePath = "")
|
|
{
|
|
string[] starts =
|
|
{
|
|
Path.GetDirectoryName(sourcePath) ?? string.Empty,
|
|
Directory.GetCurrentDirectory(),
|
|
AppContext.BaseDirectory,
|
|
};
|
|
foreach (string start in starts)
|
|
{
|
|
if (string.IsNullOrEmpty(start))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var directory = new DirectoryInfo(start);
|
|
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 above the source, working, or output directory.");
|
|
}
|
|
|
|
private static string LibraryName(string libraryIdentity)
|
|
{
|
|
var separatorIndex = libraryIdentity.IndexOf('/');
|
|
return separatorIndex < 0
|
|
? libraryIdentity
|
|
: libraryIdentity[..separatorIndex];
|
|
}
|
|
|
|
private static bool IsForbidden(string assemblyName)
|
|
{
|
|
return ForbiddenDependencyPrefixes.Any(prefix =>
|
|
assemblyName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
}
|