using System.Reflection;
using System.Xml.Linq;
using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Core;
using ZB.MOM.WW.CBDD.Core.Collections;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Core.Storage;
namespace ZB.MOM.WW.CBDD.Tests;
public class ArchitectureFitnessTests
{
private const string BsonProject = "src/CBDD.Bson/ZB.MOM.WW.CBDD.Bson.csproj";
private const string CoreProject = "src/CBDD.Core/ZB.MOM.WW.CBDD.Core.csproj";
private const string SourceGeneratorsProject = "src/CBDD.SourceGenerators/ZB.MOM.WW.CBDD.SourceGenerators.csproj";
private const string FacadeProject = "src/CBDD/ZB.MOM.WW.CBDD.csproj";
///
/// Executes Solution_DependencyGraph_ShouldRemainAcyclic_AndFollowLayerDirection.
///
[Fact]
public void Solution_DependencyGraph_ShouldRemainAcyclic_AndFollowLayerDirection()
{
var repoRoot = FindRepositoryRoot();
var projectGraph = LoadSolutionProjectGraph(repoRoot);
// Explicit layer rules
projectGraph[BsonProject].ShouldBeEmpty();
projectGraph[SourceGeneratorsProject].ShouldBeEmpty();
projectGraph[CoreProject].ShouldBe(new[] { BsonProject });
projectGraph[FacadeProject]
.OrderBy(v => v, StringComparer.Ordinal)
.ShouldBe(new[] { BsonProject, CoreProject, SourceGeneratorsProject }.OrderBy(v => v, StringComparer.Ordinal));
// Source projects should not depend on tests.
foreach (var kvp in projectGraph.Where(p => p.Key.StartsWith("src/", StringComparison.Ordinal)))
{
kvp.Value.Any(dep => dep.StartsWith("tests/", StringComparison.Ordinal))
.ShouldBeFalse($"{kvp.Key} must not reference test projects.");
}
HasCycle(projectGraph)
.ShouldBeFalse("Project references must remain acyclic.");
}
///
/// Executes HighLevelCollectionApi_ShouldNotExpandRawBsonReaderWriterSurface.
///
[Fact]
public void HighLevelCollectionApi_ShouldNotExpandRawBsonReaderWriterSurface()
{
var lowLevelTypes = new[] { typeof(BsonSpanReader), typeof(BsonSpanWriter) };
var collectionOffenders = typeof(DocumentCollection<,>)
.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(m => lowLevelTypes.Any(t => MethodUsesType(m, t)))
.Select(m => m.Name)
.Distinct()
.OrderBy(n => n)
.ToArray();
collectionOffenders.ShouldBeEmpty();
var dbContextOffenders = typeof(DocumentDbContext)
.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(m => lowLevelTypes.Any(t => MethodUsesType(m, t)))
.Select(m => m.Name)
.Distinct()
.ToArray();
dbContextOffenders.ShouldBeEmpty();
}
///
/// Executes CollectionAndIndexOrchestration_ShouldUseStoragePortInternally.
///
[Fact]
public void CollectionAndIndexOrchestration_ShouldUseStoragePortInternally()
{
var targetTypes = new[]
{
typeof(DocumentCollection<>),
typeof(DocumentCollection<,>),
typeof(BTreeIndex),
typeof(CollectionIndexManager<,>),
typeof(CollectionSecondaryIndex<,>),
typeof(VectorSearchIndex),
};
var fieldOffenders = targetTypes
.SelectMany(t => t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
.Where(f => f.FieldType == typeof(StorageEngine))
.Select(f => $"{t.Name}.{f.Name}"))
.OrderBy(v => v)
.ToArray();
fieldOffenders.ShouldBeEmpty("Collection/index orchestration should hold IStorageEngine instead of concrete StorageEngine.");
}
private static Dictionary> LoadSolutionProjectGraph(string repoRoot)
{
var solutionPath = Path.Combine(repoRoot, "CBDD.slnx");
var solutionDoc = XDocument.Load(solutionPath);
var projects = solutionDoc
.Descendants()
.Where(e => e.Name.LocalName == "Project")
.Select(e => e.Attribute("Path")?.Value)
.Where(p => !string.IsNullOrWhiteSpace(p))
.Select(p => NormalizePath(p!))
.ToHashSet(StringComparer.Ordinal);
var graph = projects.ToDictionary(
p => p,
_ => new List(),
StringComparer.Ordinal);
foreach (var project in projects)
{
var projectFile = Path.Combine(repoRoot, project);
var projectDoc = XDocument.Load(projectFile);
var projectDir = Path.GetDirectoryName(projectFile)!;
var refs = projectDoc
.Descendants()
.Where(e => e.Name.LocalName == "ProjectReference")
.Select(e => e.Attribute("Include")?.Value)
.Where(v => !string.IsNullOrWhiteSpace(v))
.Select(v => v!.Replace('\\', '/'))
.Select(v => NormalizePath(Path.GetRelativePath(repoRoot, Path.GetFullPath(Path.Combine(projectDir, v)))))
.Where(projects.Contains)
.Distinct(StringComparer.Ordinal)
.OrderBy(v => v, StringComparer.Ordinal)
.ToList();
graph[project] = refs;
}
return graph;
}
private static bool HasCycle(Dictionary> graph)
{
var state = graph.Keys.ToDictionary(k => k, _ => 0, StringComparer.Ordinal);
foreach (var node in graph.Keys)
{
if (state[node] == 0 && Visit(node))
{
return true;
}
}
return false;
bool Visit(string node)
{
state[node] = 1; // visiting
foreach (var dep in graph[node])
{
if (state[dep] == 1)
{
return true;
}
if (state[dep] == 0 && Visit(dep))
{
return true;
}
}
state[node] = 2; // visited
return false;
}
}
private static bool MethodUsesType(MethodInfo method, Type forbidden)
{
if (TypeContains(method.ReturnType, forbidden))
{
return true;
}
return method.GetParameters().Any(p => TypeContains(p.ParameterType, forbidden));
}
private static bool TypeContains(Type inspected, Type forbidden)
{
if (inspected == forbidden)
{
return true;
}
if (inspected.HasElementType && inspected.GetElementType() is { } elementType && TypeContains(elementType, forbidden))
{
return true;
}
if (!inspected.IsGenericType)
{
return false;
}
return inspected.GetGenericArguments().Any(t => TypeContains(t, forbidden));
}
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current != null)
{
var solutionPath = Path.Combine(current.FullName, "CBDD.slnx");
if (File.Exists(solutionPath))
{
return current.FullName;
}
current = current.Parent;
}
throw new InvalidOperationException("Unable to find repository root containing CBDD.slnx.");
}
private static string NormalizePath(string path)
=> path.Replace('\\', '/');
}