Initialize CBDD solution and add a .NET-focused gitignore for generated artifacts.
This commit is contained in:
217
tests/CBDD.Tests/ArchitectureFitnessTests.cs
Normal file
217
tests/CBDD.Tests/ArchitectureFitnessTests.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
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<string, List<string>> 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<string>(),
|
||||
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<string, List<string>> 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('\\', '/');
|
||||
}
|
||||
Reference in New Issue
Block a user