using System.Reflection; using System.Text.RegularExpressions; using System.Xml.Linq; using ZB.MOM.WW.CBDDC.Core; namespace ZB.MOM.WW.CBDDC.Core.Tests; public class ArchitectureFitnessTests { /// /// Verifies that the core assembly does not reference outer-layer assemblies. /// [Fact] public void CoreAssembly_ShouldNotReferenceOuterAssemblies() { var references = typeof(OplogEntry).Assembly .GetReferencedAssemblies() .Select(a => a.Name) .Where(a => !string.IsNullOrWhiteSpace(a)) .ToHashSet(StringComparer.Ordinal); references.ShouldNotContain("ZB.MOM.WW.CBDDC.Network"); references.ShouldNotContain("ZB.MOM.WW.CBDDC.Persistence"); references.ShouldNotContain("ZB.MOM.WW.CBDDC.Hosting"); } /// /// Verifies that project references under src form an acyclic graph. /// [Fact] public void SourceProjectGraph_ShouldBeAcyclic() { var repoRoot = FindRepoRoot(); var srcRoot = Path.Combine(repoRoot, "src"); var projectFiles = Directory .EnumerateFiles(srcRoot, "*.csproj", SearchOption.AllDirectories) .Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal) && !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) .ToList(); var nodes = projectFiles.ToDictionary( p => Path.GetFileNameWithoutExtension(p), p => new HashSet(StringComparer.Ordinal)); foreach (var projectFile in projectFiles) { var projectName = Path.GetFileNameWithoutExtension(projectFile); var doc = XDocument.Load(projectFile); var refs = doc.Descendants("ProjectReference") .Select(x => x.Attribute("Include")?.Value) .Where(v => !string.IsNullOrWhiteSpace(v)) .Select(v => Path.GetFileNameWithoutExtension(v!.Replace('\\', '/'))); foreach (var reference in refs) { if (nodes.ContainsKey(reference)) { nodes[projectName].Add(reference); } } } HasCycle(nodes).ShouldBeFalse(); } /// /// Verifies the allowed dependency graph between source projects. /// [Fact] public void SourceProjectReferences_ShouldMatchAllowedDependencyGraph() { var repoRoot = FindRepoRoot(); var srcRoot = Path.Combine(repoRoot, "src"); var projectFiles = Directory .EnumerateFiles(srcRoot, "*.csproj", SearchOption.AllDirectories) .Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal) && !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) .ToList(); var allowedDependencies = new Dictionary>(StringComparer.Ordinal) { ["ZB.MOM.WW.CBDDC.Core"] = new HashSet(StringComparer.Ordinal), ["ZB.MOM.WW.CBDDC.Network"] = new HashSet(StringComparer.Ordinal) { "ZB.MOM.WW.CBDDC.Core" }, ["ZB.MOM.WW.CBDDC.Persistence"] = new HashSet(StringComparer.Ordinal) { "ZB.MOM.WW.CBDDC.Core" }, ["ZB.MOM.WW.CBDDC.Hosting"] = new HashSet(StringComparer.Ordinal) { "ZB.MOM.WW.CBDDC.Network" } }; foreach (var projectFile in projectFiles) { var projectName = Path.GetFileNameWithoutExtension(projectFile); allowedDependencies.ContainsKey(projectName) .ShouldBeTrue($"Unexpected source project found: {projectName}"); var doc = XDocument.Load(projectFile); var references = doc.Descendants("ProjectReference") .Select(x => x.Attribute("Include")?.Value) .Where(v => !string.IsNullOrWhiteSpace(v)) .Select(v => Path.GetFileNameWithoutExtension(v!.Replace('\\', '/'))) .ToHashSet(StringComparer.Ordinal); var expected = allowedDependencies[projectName]; var extra = references.Where(r => !expected.Contains(r)).ToList(); var missing = expected.Where(e => !references.Contains(e)).ToList(); extra.ShouldBeEmpty($"Project {projectName} has disallowed references: {string.Join(", ", extra)}"); missing.ShouldBeEmpty($"Project {projectName} is missing required references: {string.Join(", ", missing)}"); } } /// /// Verifies non-generic ILogger usage is restricted to explicit compatibility shims. /// [Fact] public void SourceCode_ShouldRestrictNonGenericILoggerUsage() { var repoRoot = FindRepoRoot(); var srcRoot = Path.Combine(repoRoot, "src"); var loggerPattern = new Regex(@"\bILogger\b(?!\s*<|\s*Factory\b)", RegexOptions.Compiled); var allowedSnippets = new[] { "private readonly ILogger _inner;", "internal ProtocolHandler(ILogger logger", "ILogger? logger = null)", "CreateTypedLogger(ILogger? logger)", "public ForwardingLogger(ILogger inner)" }; var violations = new List(); var sourceFiles = Directory.EnumerateFiles(srcRoot, "*.cs", SearchOption.AllDirectories) .Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal) && !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)); foreach (var file in sourceFiles) { var lines = File.ReadAllLines(file); for (var i = 0; i < lines.Length; i++) { var line = lines[i].Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//", StringComparison.Ordinal)) { continue; } if (!loggerPattern.IsMatch(line)) { continue; } if (allowedSnippets.Any(line.Contains)) { continue; } var relativePath = Path.GetRelativePath(repoRoot, file).Replace('\\', '/'); violations.Add($"{relativePath}:{i + 1} -> {line}"); } } violations.ShouldBeEmpty($"Unexpected non-generic ILogger usage:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}"); } /// /// Verifies log boundaries push operation context for hosted/background entry points. /// [Fact] public void BoundaryServices_ShouldPushOperationLogContext() { var repoRoot = FindRepoRoot(); var boundaryFiles = new[] { "src/ZB.MOM.WW.CBDDC.Network/CBDDCNodeService.cs", "src/ZB.MOM.WW.CBDDC.Network/SyncOrchestrator.cs", "src/ZB.MOM.WW.CBDDC.Network/TcpSyncServer.cs", "src/ZB.MOM.WW.CBDDC.Hosting/HostedServices/DiscoveryServiceHostedService.cs", "src/ZB.MOM.WW.CBDDC.Hosting/HostedServices/TcpSyncServerHostedService.cs", "src/ZB.MOM.WW.CBDDC.Hosting/Services/NoOpDiscoveryService.cs", "src/ZB.MOM.WW.CBDDC.Hosting/Services/NoOpSyncOrchestrator.cs" }; foreach (var relativePath in boundaryFiles) { var filePath = Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); File.Exists(filePath).ShouldBeTrue($"Missing expected boundary file: {relativePath}"); var contents = File.ReadAllText(filePath); contents.Contains("LogContext.PushProperty(\"OperationId\"", StringComparison.Ordinal) .ShouldBeTrue($"Boundary file is missing OperationId log enrichment: {relativePath}"); } } /// /// Verifies boundary projects include Serilog for LogContext support. /// [Fact] public void BoundaryProjects_ShouldReferenceSerilog() { var repoRoot = FindRepoRoot(); var projects = new[] { "src/ZB.MOM.WW.CBDDC.Network/ZB.MOM.WW.CBDDC.Network.csproj", "src/ZB.MOM.WW.CBDDC.Hosting/ZB.MOM.WW.CBDDC.Hosting.csproj", "samples/ZB.MOM.WW.CBDDC.Sample.Console/ZB.MOM.WW.CBDDC.Sample.Console.csproj" }; foreach (var relativePath in projects) { var filePath = Path.Combine(repoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); File.Exists(filePath).ShouldBeTrue($"Missing project file: {relativePath}"); var contents = File.ReadAllText(filePath); contents.Contains("> graph) { var visiting = new HashSet(StringComparer.Ordinal); var visited = new HashSet(StringComparer.Ordinal); bool Dfs(string node) { if (visiting.Contains(node)) { return true; } if (!visited.Add(node)) { return false; } visiting.Add(node); foreach (var next in graph[node]) { if (Dfs(next)) { return true; } } visiting.Remove(node); return false; } return graph.Keys.Any(Dfs); } }