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"; [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."); } [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(); } [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('\\', '/'); }