acdream/tests/AcDream.Runtime.Tests/RuntimeDependencyBoundaryTests.cs
Erik cb6502c8a5 feat(platform): Campaign LA LA0 — extract ApplicationPathSet to AcDream.Platform
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>
2026-08-14 15:19:02 +02:00

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));
}
}