Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 2m27s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 2m30s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m45s
Add ErsatzTV.Architecture.Tests asserting the solution's layering, so a "just import it here" violation fails the build instead of eroding the architecture over time: - Core must not depend on Application/Infrastructure*/Scanner, nor on EF Core / Pomelo / Microsoft.Data.Sqlite / Dapper. - FFmpeg (lowest layer) must not depend on any other ErsatzTV layer. - Application must not depend on the concrete Infrastructure.Sqlite/.MySql providers (only the Infrastructure abstraction). - Infrastructure must not depend on Application or the concrete providers. Uses NetArchTest.eNhancedEdition — the maintained fork; the original NetArchTest.Rules is unmaintained since 2021 and its older Mono.Cecil doesn't reliably parse .NET 10 assemblies. Runs via the existing `dotnet test` in CI. Rules verified with a negative control (a known-true dependency asserted forbidden fails as expected — so they're not vacuous). Refs #12 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
3.5 KiB
C#
105 lines
3.5 KiB
C#
using System.Reflection;
|
|
using NetArchTest.Rules;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Architecture.Tests;
|
|
|
|
// Enforces the dependency direction of the solution's layers (see docs/ci-cd.md / contributors guide).
|
|
// NetArchTest analyses the compiled assemblies, so a "just import it here" violation fails the build
|
|
// instead of eroding the architecture over time. Rules below reflect the intended layering and the
|
|
// current project-reference graph:
|
|
// FFmpeg (lowest) <- Core <- Infrastructure <- {Infrastructure.Sqlite, Infrastructure.MySql}
|
|
// Core <- Application; everything <- ErsatzTV (host / composition root)
|
|
[TestFixture]
|
|
public class LayeringTests
|
|
{
|
|
private static Assembly Core => Assembly.Load("ErsatzTV.Core");
|
|
private static Assembly FFmpeg => Assembly.Load("ErsatzTV.FFmpeg");
|
|
private static Assembly Application => Assembly.Load("ErsatzTV.Application");
|
|
private static Assembly Infrastructure => Assembly.Load("ErsatzTV.Infrastructure");
|
|
|
|
[Test]
|
|
public void Core_should_not_depend_on_outer_layers()
|
|
{
|
|
TestResult result = Types.InAssembly(Core)
|
|
.Should()
|
|
.NotHaveDependencyOnAny(
|
|
"ErsatzTV.Application",
|
|
"ErsatzTV.Infrastructure",
|
|
"ErsatzTV.Scanner")
|
|
.GetResult();
|
|
|
|
result.IsSuccessful.ShouldBeTrue(Describe(result));
|
|
}
|
|
|
|
[Test]
|
|
public void Core_should_not_depend_on_a_database_or_orm()
|
|
{
|
|
TestResult result = Types.InAssembly(Core)
|
|
.Should()
|
|
.NotHaveDependencyOnAny(
|
|
"Microsoft.EntityFrameworkCore",
|
|
"Pomelo.EntityFrameworkCore",
|
|
"Microsoft.Data.Sqlite",
|
|
"Dapper")
|
|
.GetResult();
|
|
|
|
result.IsSuccessful.ShouldBeTrue(Describe(result));
|
|
}
|
|
|
|
[Test]
|
|
public void FFmpeg_should_not_depend_on_other_layers()
|
|
{
|
|
TestResult result = Types.InAssembly(FFmpeg)
|
|
.Should()
|
|
.NotHaveDependencyOnAny(
|
|
"ErsatzTV.Core",
|
|
"ErsatzTV.Application",
|
|
"ErsatzTV.Infrastructure",
|
|
"ErsatzTV.Scanner")
|
|
.GetResult();
|
|
|
|
result.IsSuccessful.ShouldBeTrue(Describe(result));
|
|
}
|
|
|
|
[Test]
|
|
public void Application_should_not_depend_on_concrete_database_providers()
|
|
{
|
|
TestResult result = Types.InAssembly(Application)
|
|
.Should()
|
|
.NotHaveDependencyOnAny(
|
|
"ErsatzTV.Infrastructure.Sqlite",
|
|
"ErsatzTV.Infrastructure.MySql")
|
|
.GetResult();
|
|
|
|
result.IsSuccessful.ShouldBeTrue(Describe(result));
|
|
}
|
|
|
|
[Test]
|
|
public void Infrastructure_should_not_depend_on_application_or_concrete_providers()
|
|
{
|
|
TestResult result = Types.InAssembly(Infrastructure)
|
|
.Should()
|
|
.NotHaveDependencyOnAny(
|
|
"ErsatzTV.Application",
|
|
"ErsatzTV.Infrastructure.Sqlite",
|
|
"ErsatzTV.Infrastructure.MySql")
|
|
.GetResult();
|
|
|
|
result.IsSuccessful.ShouldBeTrue(Describe(result));
|
|
}
|
|
|
|
private static string Describe(TestResult result)
|
|
{
|
|
if (result.IsSuccessful)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
IEnumerable<string> names = result.FailingTypes?.Select(t => t.FullName ?? t.Name)
|
|
?? Enumerable.Empty<string>();
|
|
return "Layering violation — offending types:\n " + string.Join("\n ", names);
|
|
}
|
|
}
|