using System.Collections.Generic; using System.Linq; using StellaOps.Scanner.Reachability.Ordering; using Xunit; namespace StellaOps.Scanner.Reachability.Tests; public sealed class DeterministicGraphOrdererTests { [Fact] public void Canonicalize_IsDeterministic_AcrossInputOrdering() { var orderer = new DeterministicGraphOrderer(); var graph1 = new RichGraph( Nodes: new[] { Node("B"), Node("A"), Node("C") }, Edges: new[] { Edge("A", "C"), Edge("A", "B"), Edge("B", "C") }, Roots: new[] { new RichGraphRoot("A", "runtime", null) }, Analyzer: new RichGraphAnalyzer("test", "1.0", null)); var graph2 = new RichGraph( Nodes: new[] { Node("C"), Node("A"), Node("B") }, Edges: new[] { Edge("B", "C"), Edge("A", "B"), Edge("A", "C") }, Roots: new[] { new RichGraphRoot("A", "runtime", null) }, Analyzer: new RichGraphAnalyzer("test", "1.0", null)); var canonical1 = orderer.Canonicalize(graph1, GraphOrderingStrategy.TopologicalLexicographic); var canonical2 = orderer.Canonicalize(graph2, GraphOrderingStrategy.TopologicalLexicographic); Assert.Equal(canonical1.ContentHash, canonical2.ContentHash); Assert.Equal(canonical1.Nodes.Select(n => n.Id), canonical2.Nodes.Select(n => n.Id)); Assert.Equal(canonical1.Edges.Select(e => (e.SourceIndex, e.TargetIndex, e.EdgeType)), canonical2.Edges.Select(e => (e.SourceIndex, e.TargetIndex, e.EdgeType))); } [Fact] public void TopologicalLexicographic_UsesLexicographicTiebreakers() { var orderer = new DeterministicGraphOrderer(); var graph = new RichGraph( Nodes: new[] { Node("C"), Node("B"), Node("A") }, Edges: new[] { Edge("A", "C"), Edge("B", "C") }, Roots: Array.Empty(), Analyzer: new RichGraphAnalyzer("test", "1.0", null)); var order = orderer.OrderNodes(graph, GraphOrderingStrategy.TopologicalLexicographic); Assert.Equal(new[] { "A", "B", "C" }, order); } [Fact] public void TopologicalLexicographic_HandlesCyclesByAppendingRemainder() { var orderer = new DeterministicGraphOrderer(); var graph = new RichGraph( Nodes: new[] { Node("B"), Node("A"), Node("C") }, Edges: new[] { Edge("A", "B"), Edge("B", "A") }, Roots: Array.Empty(), Analyzer: new RichGraphAnalyzer("test", "1.0", null)); var order = orderer.OrderNodes(graph, GraphOrderingStrategy.TopologicalLexicographic); Assert.Equal(new[] { "C", "A", "B" }, order); } [Fact] public void BreadthFirstLexicographic_TraversesFromAnchors() { var orderer = new DeterministicGraphOrderer(); var graph = new RichGraph( Nodes: new[] { Node("D"), Node("C"), Node("B"), Node("A") }, Edges: new[] { Edge("A", "C"), Edge("A", "B"), Edge("B", "D") }, Roots: new[] { new RichGraphRoot("A", "runtime", null) }, Analyzer: new RichGraphAnalyzer("test", "1.0", null)); var order = orderer.OrderNodes(graph, GraphOrderingStrategy.BreadthFirstLexicographic); Assert.Equal(new[] { "A", "B", "C", "D" }, order); } [Fact] public void DepthFirstLexicographic_TraversesFromAnchors() { var orderer = new DeterministicGraphOrderer(); var graph = new RichGraph( Nodes: new[] { Node("D"), Node("C"), Node("B"), Node("A") }, Edges: new[] { Edge("A", "C"), Edge("A", "B"), Edge("B", "D") }, Roots: new[] { new RichGraphRoot("A", "runtime", null) }, Analyzer: new RichGraphAnalyzer("test", "1.0", null)); var order = orderer.OrderNodes(graph, GraphOrderingStrategy.DepthFirstLexicographic); Assert.Equal(new[] { "A", "B", "D", "C" }, order); } [Fact] public void OrderEdges_SortsByNodeOrderThenKind() { var orderer = new DeterministicGraphOrderer(); var graph = new RichGraph( Nodes: new[] { Node("C"), Node("B"), Node("A") }, Edges: new[] { Edge("B", "C", kind: "import"), Edge("A", "B", kind: "call"), Edge("A", "C", kind: "call") }, Roots: Array.Empty(), Analyzer: new RichGraphAnalyzer("test", "1.0", null)); var order = new[] { "A", "B", "C" }; var edges = orderer.OrderEdges(graph, order).ToList(); Assert.Equal(("A", "B", "call"), (edges[0].From, edges[0].To, edges[0].Kind)); Assert.Equal(("A", "C", "call"), (edges[1].From, edges[1].To, edges[1].Kind)); Assert.Equal(("B", "C", "import"), (edges[2].From, edges[2].To, edges[2].Kind)); } private static RichGraphNode Node(string id, IReadOnlyDictionary? attributes = null) => new( Id: id, SymbolId: id, CodeId: null, Purl: null, Lang: "dotnet", Kind: "method", Display: id, BuildId: null, Evidence: Array.Empty(), Attributes: attributes, SymbolDigest: null); private static RichGraphEdge Edge(string from, string to, string kind = "call") => new( From: from, To: to, Kind: kind, Purl: null, SymbolDigest: null, Evidence: Array.Empty(), Confidence: 1.0, Candidates: Array.Empty()); }