using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using StellaOps.Scanner.Reachability; using Xunit; namespace StellaOps.Scanner.Reachability.Tests; public class ReachabilityUnionWriterTests { [Fact] public async Task WritesDeterministicNdjson() { var writer = new ReachabilityUnionWriter(); using var temp = new TempDir(); var graph = new ReachabilityUnionGraph( Nodes: new[] { new ReachabilityUnionNode("sym:dotnet:B", "dotnet", "method"), new ReachabilityUnionNode("sym:dotnet:A", "dotnet", "method") }, Edges: new[] { new ReachabilityUnionEdge("sym:dotnet:A", "sym:dotnet:B", "call") }); var result = await writer.WriteAsync(graph, temp.Path, "analysis-x"); var meta = await JsonDocument.ParseAsync(File.OpenRead(result.MetaPath)); var files = meta.RootElement.GetProperty("files").EnumerateArray().ToList(); Assert.Equal(2, files.Count); // nodes + edges // Deterministic order var nodeLines = await File.ReadAllLinesAsync(Path.Combine(temp.Path, "reachability_graphs/analysis-x/nodes.ndjson")); Assert.Contains(nodeLines, l => l.Contains("sym:dotnet:A")); } [Fact] public async Task WritesNodePurlAndSymbolDigest() { var writer = new ReachabilityUnionWriter(); using var temp = new TempDir(); var graph = new ReachabilityUnionGraph( Nodes: new[] { new ReachabilityUnionNode( "sym:dotnet:A", "dotnet", "method", "TestMethod", null, null, Purl: "pkg:nuget/TestPackage@1.0.0", SymbolDigest: "sha256:abc123") }, Edges: Array.Empty()); var result = await writer.WriteAsync(graph, temp.Path, "analysis-purl"); var nodeLines = await File.ReadAllLinesAsync(result.Nodes.Path); Assert.Single(nodeLines); Assert.Contains("\"purl\":\"pkg:nuget/TestPackage@1.0.0\"", nodeLines[0]); Assert.Contains("\"symbol_digest\":\"sha256:abc123\"", nodeLines[0]); } [Fact] public async Task WritesEdgePurlAndSymbolDigest() { var writer = new ReachabilityUnionWriter(); using var temp = new TempDir(); var graph = new ReachabilityUnionGraph( Nodes: new[] { new ReachabilityUnionNode("sym:dotnet:A", "dotnet", "method"), new ReachabilityUnionNode("sym:dotnet:B", "dotnet", "method") }, Edges: new[] { new ReachabilityUnionEdge( "sym:dotnet:A", "sym:dotnet:B", "call", "high", null, Purl: "pkg:nuget/TargetPackage@2.0.0", SymbolDigest: "sha256:def456") }); var result = await writer.WriteAsync(graph, temp.Path, "analysis-edge-purl"); var edgeLines = await File.ReadAllLinesAsync(result.Edges.Path); Assert.Single(edgeLines); Assert.Contains("\"purl\":\"pkg:nuget/TargetPackage@2.0.0\"", edgeLines[0]); Assert.Contains("\"symbol_digest\":\"sha256:def456\"", edgeLines[0]); } [Fact] public async Task WritesEdgeCandidates() { var writer = new ReachabilityUnionWriter(); using var temp = new TempDir(); var graph = new ReachabilityUnionGraph( Nodes: new[] { new ReachabilityUnionNode("sym:binary:main", "binary", "function"), new ReachabilityUnionNode("sym:binary:openssl_connect", "binary", "function") }, Edges: new[] { new ReachabilityUnionEdge( "sym:binary:main", "sym:binary:openssl_connect", "call", "medium", null, Purl: null, SymbolDigest: null, Candidates: new List { new("pkg:deb/ubuntu/openssl@3.0.2", "sha256:abc", 0.8), new("pkg:deb/debian/openssl@3.0.2", "sha256:def", 0.6) }) }); var result = await writer.WriteAsync(graph, temp.Path, "analysis-candidates"); var edgeLines = await File.ReadAllLinesAsync(result.Edges.Path); Assert.Single(edgeLines); Assert.Contains("\"candidates\":", edgeLines[0]); Assert.Contains("pkg:deb/ubuntu/openssl@3.0.2", edgeLines[0]); Assert.Contains("pkg:deb/debian/openssl@3.0.2", edgeLines[0]); Assert.Contains("\"score\":0.8", edgeLines[0]); } [Fact] public async Task WritesSymbolMetadataAndCodeBlockHash() { var writer = new ReachabilityUnionWriter(); using var temp = new TempDir(); var graph = new ReachabilityUnionGraph( Nodes: new[] { new ReachabilityUnionNode( "sym:binary:foo", "binary", "function", "ssl3_read_bytes", CodeBlockHash: "sha256:deadbeef", Symbol: new ReachabilitySymbol("_Z15ssl3_read_bytes", "ssl3_read_bytes", "DWARF", 0.98)) }, Edges: Array.Empty()); var result = await writer.WriteAsync(graph, temp.Path, "analysis-symbol"); var nodeLines = await File.ReadAllLinesAsync(result.Nodes.Path); Assert.Single(nodeLines); Assert.Contains("\"code_block_hash\":\"sha256:deadbeef\"", nodeLines[0]); Assert.Contains("\"symbol\":{\"mangled\":\"_Z15ssl3_read_bytes\",\"demangled\":\"ssl3_read_bytes\",\"source\":\"DWARF\",\"confidence\":0.98}", nodeLines[0]); } [Fact] public async Task OmitsPurlAndSymbolDigestWhenNull() { var writer = new ReachabilityUnionWriter(); using var temp = new TempDir(); var graph = new ReachabilityUnionGraph( Nodes: new[] { new ReachabilityUnionNode("sym:dotnet:A", "dotnet", "method") }, Edges: new[] { new ReachabilityUnionEdge("sym:dotnet:A", "sym:dotnet:A", "call") }); var result = await writer.WriteAsync(graph, temp.Path, "analysis-null-purl"); var nodeLines = await File.ReadAllLinesAsync(result.Nodes.Path); Assert.DoesNotContain("purl", nodeLines[0]); Assert.DoesNotContain("symbol_digest", nodeLines[0]); var edgeLines = await File.ReadAllLinesAsync(result.Edges.Path); Assert.DoesNotContain("purl", edgeLines[0]); Assert.DoesNotContain("symbol_digest", edgeLines[0]); Assert.DoesNotContain("candidates", edgeLines[0]); } }