Add call graph fixtures for various languages and scenarios
Some checks failed
Reachability Corpus Validation / validate-corpus (push) Waiting to run
Reachability Corpus Validation / validate-ground-truths (push) Waiting to run
Reachability Corpus Validation / determinism-check (push) Blocked by required conditions
Scanner Analyzers / Discover Analyzers (push) Waiting to run
Scanner Analyzers / Build Analyzers (push) Blocked by required conditions
Scanner Analyzers / Test Language Analyzers (push) Blocked by required conditions
Scanner Analyzers / Validate Test Fixtures (push) Waiting to run
Scanner Analyzers / Verify Deterministic Output (push) Blocked by required conditions
Signals CI & Image / signals-ci (push) Waiting to run
Signals Reachability Scoring & Events / reachability-smoke (push) Waiting to run
Signals Reachability Scoring & Events / sign-and-upload (push) Blocked by required conditions
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Some checks failed
Reachability Corpus Validation / validate-corpus (push) Waiting to run
Reachability Corpus Validation / validate-ground-truths (push) Waiting to run
Reachability Corpus Validation / determinism-check (push) Blocked by required conditions
Scanner Analyzers / Discover Analyzers (push) Waiting to run
Scanner Analyzers / Build Analyzers (push) Blocked by required conditions
Scanner Analyzers / Test Language Analyzers (push) Blocked by required conditions
Scanner Analyzers / Validate Test Fixtures (push) Waiting to run
Scanner Analyzers / Verify Deterministic Output (push) Blocked by required conditions
Signals CI & Image / signals-ci (push) Waiting to run
Signals Reachability Scoring & Events / reachability-smoke (push) Waiting to run
Signals Reachability Scoring & Events / sign-and-upload (push) Blocked by required conditions
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
- Introduced `all-edge-reasons.json` to test edge resolution reasons in .NET. - Added `all-visibility-levels.json` to validate method visibility levels in .NET. - Created `dotnet-aspnetcore-minimal.json` for a minimal ASP.NET Core application. - Included `go-gin-api.json` for a Go Gin API application structure. - Added `java-spring-boot.json` for the Spring PetClinic application in Java. - Introduced `legacy-no-schema.json` for legacy application structure without schema. - Created `node-express-api.json` for an Express.js API application structure.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using StellaOps.AirGap.Importer.Telemetry;
|
||||
|
||||
namespace StellaOps.AirGap.Importer.Tests;
|
||||
|
||||
public sealed class OfflineKitMetricsTests : IDisposable
|
||||
{
|
||||
private readonly MeterListener _listener;
|
||||
private readonly List<RecordedMeasurement> _measurements = [];
|
||||
|
||||
public OfflineKitMetricsTests()
|
||||
{
|
||||
_listener = new MeterListener();
|
||||
_listener.InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == OfflineKitMetrics.MeterName)
|
||||
{
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
};
|
||||
|
||||
_listener.SetMeasurementEventCallback<double>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
_measurements.Add(new RecordedMeasurement(instrument.Name, measurement, tags.ToArray()));
|
||||
});
|
||||
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
_measurements.Add(new RecordedMeasurement(instrument.Name, measurement, tags.ToArray()));
|
||||
});
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
public void Dispose() => _listener.Dispose();
|
||||
|
||||
[Fact]
|
||||
public void RecordImport_EmitsCounterWithLabels()
|
||||
{
|
||||
using var metrics = new OfflineKitMetrics();
|
||||
|
||||
metrics.RecordImport(status: "success", tenantId: "tenant-a");
|
||||
|
||||
Assert.Contains(_measurements, m =>
|
||||
m.Name == "offlinekit_import_total" &&
|
||||
m.Value is long v &&
|
||||
v == 1 &&
|
||||
m.HasTag(OfflineKitMetrics.TagNames.Status, "success") &&
|
||||
m.HasTag(OfflineKitMetrics.TagNames.TenantId, "tenant-a"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordAttestationVerifyLatency_EmitsHistogramWithLabels()
|
||||
{
|
||||
using var metrics = new OfflineKitMetrics();
|
||||
|
||||
metrics.RecordAttestationVerifyLatency(attestationType: "dsse", seconds: 1.234, success: true);
|
||||
|
||||
Assert.Contains(_measurements, m =>
|
||||
m.Name == "offlinekit_attestation_verify_latency_seconds" &&
|
||||
m.Value is double v &&
|
||||
Math.Abs(v - 1.234) < 0.000_001 &&
|
||||
m.HasTag(OfflineKitMetrics.TagNames.AttestationType, "dsse") &&
|
||||
m.HasTag(OfflineKitMetrics.TagNames.Success, "true"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordRekorSuccess_EmitsCounterWithLabels()
|
||||
{
|
||||
using var metrics = new OfflineKitMetrics();
|
||||
|
||||
metrics.RecordRekorSuccess(mode: "offline");
|
||||
|
||||
Assert.Contains(_measurements, m =>
|
||||
m.Name == "attestor_rekor_success_total" &&
|
||||
m.Value is long v &&
|
||||
v == 1 &&
|
||||
m.HasTag(OfflineKitMetrics.TagNames.Mode, "offline"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordRekorRetry_EmitsCounterWithLabels()
|
||||
{
|
||||
using var metrics = new OfflineKitMetrics();
|
||||
|
||||
metrics.RecordRekorRetry(reason: "stale_snapshot");
|
||||
|
||||
Assert.Contains(_measurements, m =>
|
||||
m.Name == "attestor_rekor_retry_total" &&
|
||||
m.Value is long v &&
|
||||
v == 1 &&
|
||||
m.HasTag(OfflineKitMetrics.TagNames.Reason, "stale_snapshot"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordRekorInclusionLatency_EmitsHistogramWithLabels()
|
||||
{
|
||||
using var metrics = new OfflineKitMetrics();
|
||||
|
||||
metrics.RecordRekorInclusionLatency(seconds: 0.5, success: false);
|
||||
|
||||
Assert.Contains(_measurements, m =>
|
||||
m.Name == "rekor_inclusion_latency" &&
|
||||
m.Value is double v &&
|
||||
Math.Abs(v - 0.5) < 0.000_001 &&
|
||||
m.HasTag(OfflineKitMetrics.TagNames.Success, "false"));
|
||||
}
|
||||
|
||||
private sealed record RecordedMeasurement(string Name, object Value, IReadOnlyList<KeyValuePair<string, object?>> Tags)
|
||||
{
|
||||
public bool HasTag(string key, string expectedValue) =>
|
||||
Tags.Any(t => t.Key == key && string.Equals(t.Value?.ToString(), expectedValue, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.AirGap.Importer.Reconciliation;
|
||||
|
||||
namespace StellaOps.AirGap.Importer.Tests.Reconciliation;
|
||||
|
||||
public sealed class ArtifactIndexTests
|
||||
{
|
||||
[Fact]
|
||||
public void NormalizeDigest_BareHex_AddsPrefixAndLowercases()
|
||||
{
|
||||
var hex = new string('A', 64);
|
||||
ArtifactIndex.NormalizeDigest(hex).Should().Be("sha256:" + new string('a', 64));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeDigest_WithSha256Prefix_IsCanonical()
|
||||
{
|
||||
var hex = new string('B', 64);
|
||||
ArtifactIndex.NormalizeDigest("sha256:" + hex).Should().Be("sha256:" + new string('b', 64));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeDigest_WithOtherAlgorithm_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<FormatException>(() => ArtifactIndex.NormalizeDigest("sha512:" + new string('a', 64)));
|
||||
ex.Message.Should().Contain("Only sha256");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOrUpdate_MergesEntries_DeduplicatesAndSorts()
|
||||
{
|
||||
var digest = new string('c', 64);
|
||||
|
||||
var entryA = ArtifactEntry.Empty(digest) with
|
||||
{
|
||||
Sboms = new[]
|
||||
{
|
||||
new SbomReference("b", "b.json", SbomFormat.CycloneDx, null),
|
||||
new SbomReference("a", "a.json", SbomFormat.Spdx, null),
|
||||
}
|
||||
};
|
||||
|
||||
var entryB = ArtifactEntry.Empty("sha256:" + digest.ToUpperInvariant()) with
|
||||
{
|
||||
Sboms = new[]
|
||||
{
|
||||
new SbomReference("a", "a2.json", SbomFormat.CycloneDx, null),
|
||||
new SbomReference("c", "c.json", SbomFormat.Spdx, null),
|
||||
}
|
||||
};
|
||||
|
||||
var index = new ArtifactIndex();
|
||||
index.AddOrUpdate(entryA);
|
||||
index.AddOrUpdate(entryB);
|
||||
|
||||
var stored = index.Get("sha256:" + digest);
|
||||
stored.Should().NotBeNull();
|
||||
stored!.Digest.Should().Be("sha256:" + digest);
|
||||
|
||||
stored.Sboms.Select(s => (s.ContentHash, s.FilePath)).Should().Equal(
|
||||
("a", "a.json"),
|
||||
("b", "b.json"),
|
||||
("c", "c.json"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using StellaOps.AirGap.Importer.Reconciliation;
|
||||
|
||||
namespace StellaOps.AirGap.Importer.Tests.Reconciliation;
|
||||
|
||||
public sealed class EvidenceDirectoryDiscoveryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Discover_ReturnsDeterministicRelativePathsAndHashes()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "stellaops-evidence-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
|
||||
try
|
||||
{
|
||||
WriteUtf8(Path.Combine(root, "sboms", "a.cdx.json"), "{\"bom\":1}");
|
||||
WriteUtf8(Path.Combine(root, "attestations", "z.intoto.jsonl.dsig"), "dsse");
|
||||
WriteUtf8(Path.Combine(root, "vex", "v.openvex.json"), "{\"vex\":true}");
|
||||
|
||||
var discovered = EvidenceDirectoryDiscovery.Discover(root);
|
||||
discovered.Should().HaveCount(3);
|
||||
|
||||
discovered.Select(d => d.RelativePath).Should().Equal(
|
||||
"attestations/z.intoto.jsonl.dsig",
|
||||
"sboms/a.cdx.json",
|
||||
"vex/v.openvex.json");
|
||||
|
||||
discovered[0].Kind.Should().Be(EvidenceFileKind.Attestation);
|
||||
discovered[1].Kind.Should().Be(EvidenceFileKind.Sbom);
|
||||
discovered[2].Kind.Should().Be(EvidenceFileKind.Vex);
|
||||
|
||||
discovered[0].ContentSha256.Should().Be(HashUtf8("dsse"));
|
||||
discovered[1].ContentSha256.Should().Be(HashUtf8("{\"bom\":1}"));
|
||||
discovered[2].ContentSha256.Should().Be(HashUtf8("{\"vex\":true}"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Discover_WhenDirectoryMissing_Throws()
|
||||
{
|
||||
var missing = Path.Combine(Path.GetTempPath(), "stellaops-missing-" + Guid.NewGuid().ToString("N"));
|
||||
Action act = () => EvidenceDirectoryDiscovery.Discover(missing);
|
||||
act.Should().Throw<DirectoryNotFoundException>();
|
||||
}
|
||||
|
||||
private static void WriteUtf8(string path, string content)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
File.WriteAllText(path, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
}
|
||||
|
||||
private static string HashUtf8(string content)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
var hash = sha256.ComputeHash(bytes);
|
||||
return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,732 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Parsing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Reachability.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for CallgraphSchemaMigrator.
|
||||
/// Verifies schema migration from legacy format to stella.callgraph.v1.
|
||||
/// </summary>
|
||||
public class CallgraphSchemaMigratorTests
|
||||
{
|
||||
#region EnsureV1 - Schema Version Tests
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_SetsSchemaToV1_WhenNotSet()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Schema = string.Empty
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Schema.Should().Be(CallgraphSchemaVersions.V1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesV1Schema_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Schema = CallgraphSchemaVersions.V1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Schema.Should().Be(CallgraphSchemaVersions.V1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_UpdatesLegacySchema_ToV1()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Schema = "legacy-schema-1.0"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Schema.Should().Be(CallgraphSchemaVersions.V1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnsureV1 - Language Parsing Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("dotnet", CallgraphLanguage.DotNet)]
|
||||
[InlineData(".net", CallgraphLanguage.DotNet)]
|
||||
[InlineData("csharp", CallgraphLanguage.DotNet)]
|
||||
[InlineData("c#", CallgraphLanguage.DotNet)]
|
||||
[InlineData("java", CallgraphLanguage.Java)]
|
||||
[InlineData("node", CallgraphLanguage.Node)]
|
||||
[InlineData("nodejs", CallgraphLanguage.Node)]
|
||||
[InlineData("javascript", CallgraphLanguage.Node)]
|
||||
[InlineData("typescript", CallgraphLanguage.Node)]
|
||||
[InlineData("python", CallgraphLanguage.Python)]
|
||||
[InlineData("go", CallgraphLanguage.Go)]
|
||||
[InlineData("golang", CallgraphLanguage.Go)]
|
||||
[InlineData("rust", CallgraphLanguage.Rust)]
|
||||
[InlineData("ruby", CallgraphLanguage.Ruby)]
|
||||
[InlineData("php", CallgraphLanguage.Php)]
|
||||
[InlineData("binary", CallgraphLanguage.Binary)]
|
||||
[InlineData("native", CallgraphLanguage.Binary)]
|
||||
[InlineData("elf", CallgraphLanguage.Binary)]
|
||||
[InlineData("swift", CallgraphLanguage.Swift)]
|
||||
[InlineData("kotlin", CallgraphLanguage.Kotlin)]
|
||||
public void EnsureV1_ParsesLanguageString_ToEnum(string languageString, CallgraphLanguage expected)
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Language = languageString,
|
||||
LanguageType = CallgraphLanguage.Unknown
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.LanguageType.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesLanguageType_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Language = "java",
|
||||
LanguageType = CallgraphLanguage.DotNet // Already set to something different
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.LanguageType.Should().Be(CallgraphLanguage.DotNet);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnsureV1 - Node Visibility Inference Tests
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersPublicVisibility_ForStandardNames()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = "ProcessOrder", Visibility = SymbolVisibility.Unknown }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.Visibility.Should().Be(SymbolVisibility.Public);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersPrivateVisibility_ForUnderscorePrefixed()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = "_privateMethod", Visibility = SymbolVisibility.Unknown }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.Visibility.Should().Be(SymbolVisibility.Private);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersPrivateVisibility_ForAngleBracketNames()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = "<Main>$", Visibility = SymbolVisibility.Unknown }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.Visibility.Should().Be(SymbolVisibility.Private);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersInternalVisibility_ForInternalNamespace()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = "Helper", Namespace = "MyApp.Internal.Utils", Visibility = SymbolVisibility.Unknown }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.Visibility.Should().Be(SymbolVisibility.Internal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesVisibility_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = "_privateMethod", Visibility = SymbolVisibility.Protected }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.Visibility.Should().Be(SymbolVisibility.Protected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnsureV1 - Symbol Key Building Tests
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_BuildsSymbolKey_FromNamespaceAndName()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = "ProcessOrder", Namespace = "MyApp.Services" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.SymbolKey.Should().Be("MyApp.Services.ProcessOrder");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_BuildsSymbolKey_FromNameOnly_WhenNoNamespace()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = "GlobalMethod" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.SymbolKey.Should().Be("GlobalMethod");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesSymbolKey_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = "Method", Namespace = "Ns", SymbolKey = "Custom.Key" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.SymbolKey.Should().Be("Custom.Key");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnsureV1 - Entrypoint Candidate Detection Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("Main")]
|
||||
[InlineData("main")]
|
||||
[InlineData("MAIN")]
|
||||
public void EnsureV1_DetectsEntrypointCandidate_ForMainMethod(string methodName)
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = methodName }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.IsEntrypointCandidate.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("OrdersController")]
|
||||
[InlineData("UserController")]
|
||||
public void EnsureV1_DetectsEntrypointCandidate_ForControllerNames(string name)
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = name }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.IsEntrypointCandidate.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("RequestHandler")]
|
||||
[InlineData("EventHandler")]
|
||||
public void EnsureV1_DetectsEntrypointCandidate_ForHandlerNames(string name)
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = name }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.IsEntrypointCandidate.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(".cctor")]
|
||||
[InlineData("ModuleInitializer")]
|
||||
public void EnsureV1_DetectsEntrypointCandidate_ForModuleInitializers(string name)
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node1", Name = name }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().ContainSingle()
|
||||
.Which.IsEntrypointCandidate.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnsureV1 - Edge Reason Inference Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("call", EdgeReason.DirectCall)]
|
||||
[InlineData("direct", EdgeReason.DirectCall)]
|
||||
[InlineData("virtual", EdgeReason.VirtualCall)]
|
||||
[InlineData("callvirt", EdgeReason.VirtualCall)]
|
||||
[InlineData("newobj", EdgeReason.NewObj)]
|
||||
[InlineData("new", EdgeReason.NewObj)]
|
||||
[InlineData("ldftn", EdgeReason.DelegateCreate)]
|
||||
[InlineData("delegate", EdgeReason.DelegateCreate)]
|
||||
[InlineData("reflection", EdgeReason.ReflectionString)]
|
||||
[InlineData("di", EdgeReason.DiBinding)]
|
||||
[InlineData("injection", EdgeReason.DiBinding)]
|
||||
[InlineData("async", EdgeReason.AsyncContinuation)]
|
||||
[InlineData("continuation", EdgeReason.AsyncContinuation)]
|
||||
[InlineData("event", EdgeReason.EventHandler)]
|
||||
[InlineData("generic", EdgeReason.GenericInstantiation)]
|
||||
[InlineData("native", EdgeReason.NativeInterop)]
|
||||
[InlineData("pinvoke", EdgeReason.NativeInterop)]
|
||||
[InlineData("ffi", EdgeReason.NativeInterop)]
|
||||
public void EnsureV1_InfersEdgeReason_FromLegacyType(string legacyType, EdgeReason expected)
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Edges = new List<CallgraphEdge>
|
||||
{
|
||||
new() { SourceId = "n1", TargetId = "n2", Type = legacyType, Reason = EdgeReason.Unknown }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Edges.Should().ContainSingle()
|
||||
.Which.Reason.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersRuntimeMinted_ForRuntimeKind()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Edges = new List<CallgraphEdge>
|
||||
{
|
||||
new() { SourceId = "n1", TargetId = "n2", Type = "unknown", Kind = EdgeKind.Runtime, Reason = EdgeReason.Unknown }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Edges.Should().ContainSingle()
|
||||
.Which.Reason.Should().Be(EdgeReason.RuntimeMinted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersDynamicImport_ForHeuristicKind()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Edges = new List<CallgraphEdge>
|
||||
{
|
||||
new() { SourceId = "n1", TargetId = "n2", Type = "unknown", Kind = EdgeKind.Heuristic, Reason = EdgeReason.Unknown }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Edges.Should().ContainSingle()
|
||||
.Which.Reason.Should().Be(EdgeReason.DynamicImport);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesEdgeReason_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Edges = new List<CallgraphEdge>
|
||||
{
|
||||
new() { SourceId = "n1", TargetId = "n2", Type = "call", Reason = EdgeReason.VirtualCall }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Edges.Should().ContainSingle()
|
||||
.Which.Reason.Should().Be(EdgeReason.VirtualCall);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnsureV1 - Entrypoint Inference Tests
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersEntrypoints_FromEntrypointCandidateNodes()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
LanguageType = CallgraphLanguage.DotNet,
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "main", Name = "Main", IsEntrypointCandidate = true },
|
||||
new() { Id = "helper", Name = "Helper", IsEntrypointCandidate = false }
|
||||
},
|
||||
Entrypoints = new List<CallgraphEntrypoint>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Entrypoints.Should().ContainSingle()
|
||||
.Which.NodeId.Should().Be("main");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersEntrypoints_FromExplicitRoots()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
LanguageType = CallgraphLanguage.DotNet,
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "init", Name = "Initialize" }
|
||||
},
|
||||
Roots = new List<CallgraphRoot>
|
||||
{
|
||||
new("init", "init", "module_init")
|
||||
},
|
||||
Entrypoints = new List<CallgraphEntrypoint>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Entrypoints.Should().ContainSingle()
|
||||
.Which.NodeId.Should().Be("init");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesEntrypoints_WhenAlreadyPresent()
|
||||
{
|
||||
// Arrange
|
||||
var existingEntrypoint = new CallgraphEntrypoint
|
||||
{
|
||||
NodeId = "existing",
|
||||
Kind = EntrypointKind.Http,
|
||||
Route = "/api/test"
|
||||
};
|
||||
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "main", Name = "Main", IsEntrypointCandidate = true }
|
||||
},
|
||||
Entrypoints = new List<CallgraphEntrypoint> { existingEntrypoint }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Entrypoints.Should().ContainSingle()
|
||||
.Which.NodeId.Should().Be("existing");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnsureV1 - Ordering Tests
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_SortsNodes_ByIdAlphabetically()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "z-node", Name = "Z" },
|
||||
new() { Id = "a-node", Name = "A" },
|
||||
new() { Id = "m-node", Name = "M" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Select(n => n.Id).Should().BeInAscendingOrder();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_SortsEdges_BySourceThenTargetThenTypeThenOffset()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Edges = new List<CallgraphEdge>
|
||||
{
|
||||
new() { SourceId = "b", TargetId = "x", Type = "call", Offset = 10 },
|
||||
new() { SourceId = "a", TargetId = "y", Type = "call", Offset = 5 },
|
||||
new() { SourceId = "a", TargetId = "x", Type = "virtual", Offset = 0 },
|
||||
new() { SourceId = "a", TargetId = "x", Type = "call", Offset = 20 }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
var sortedEdges = result.Edges.ToList();
|
||||
sortedEdges[0].SourceId.Should().Be("a");
|
||||
sortedEdges[0].TargetId.Should().Be("x");
|
||||
sortedEdges[0].Type.Should().Be("call");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_SortsEntrypoints_ByPhaseThenOrder()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
LanguageType = CallgraphLanguage.DotNet,
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "main", Name = "Main", IsEntrypointCandidate = true },
|
||||
new() { Id = "init", Name = ".cctor", IsEntrypointCandidate = true }
|
||||
},
|
||||
Entrypoints = new List<CallgraphEntrypoint>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Entrypoints.Should().HaveCount(2);
|
||||
// ModuleInit phase should come before Runtime
|
||||
result.Entrypoints.First().NodeId.Should().Be("init");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnsureV1 - Null Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_ThrowsArgumentNullException_ForNullDocument()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => CallgraphSchemaMigrator.EnsureV1(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_HandlesEmptyNodes_Gracefully()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Nodes = new List<CallgraphNode>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Nodes.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_HandlesEmptyEdges_Gracefully()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Edges = new List<CallgraphEdge>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Edges.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Framework Inference Tests
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersAspNetCoreFramework_ForDotNetController()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
LanguageType = CallgraphLanguage.DotNet,
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "ctrl", Name = "OrdersController", IsEntrypointCandidate = true }
|
||||
},
|
||||
Entrypoints = new List<CallgraphEntrypoint>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Entrypoints.Should().ContainSingle()
|
||||
.Which.Framework.Should().Be(EntrypointFramework.AspNetCore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureV1_InfersSpringFramework_ForJavaController()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
LanguageType = CallgraphLanguage.Java,
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "ctrl", Name = "OrderController", IsEntrypointCandidate = true }
|
||||
},
|
||||
Entrypoints = new List<CallgraphEntrypoint>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
// Assert
|
||||
result.Entrypoints.Should().ContainSingle()
|
||||
.Which.Framework.Should().Be(EntrypointFramework.Spring);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Parsing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Reachability.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Determinism tests for the stella.callgraph.v1 schema.
|
||||
/// These tests validate:
|
||||
/// - Round-trip serialization produces identical output
|
||||
/// - Schema migration from legacy formats
|
||||
/// - Enum values serialize as expected strings
|
||||
/// - Arrays maintain stable ordering
|
||||
/// </summary>
|
||||
public sealed class CallgraphSchemaV1DeterminismTests
|
||||
{
|
||||
private static readonly string RepoRoot = LocateRepoRoot();
|
||||
private static readonly string FixtureRoot = Path.Combine(RepoRoot, "tests", "reachability", "fixtures", "callgraph-schema-v1");
|
||||
|
||||
private static readonly JsonSerializerOptions DeterministicOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
public static IEnumerable<object[]> GoldenFixtures()
|
||||
{
|
||||
if (!Directory.Exists(FixtureRoot))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var file in Directory.GetFiles(FixtureRoot, "*.json").OrderBy(f => f, StringComparer.Ordinal))
|
||||
{
|
||||
yield return new object[] { Path.GetFileNameWithoutExtension(file) };
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_DeserializesWithoutError(string fixtureName)
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
|
||||
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json);
|
||||
|
||||
document.Should().NotBeNull();
|
||||
document!.Id.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_NodesHaveRequiredFields(string fixtureName)
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
foreach (var node in document.Nodes)
|
||||
{
|
||||
node.Id.Should().NotBeNullOrEmpty($"Node in {fixtureName} must have Id");
|
||||
node.Name.Should().NotBeNullOrEmpty($"Node {node.Id} in {fixtureName} must have Name");
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_EdgesReferenceValidNodes(string fixtureName)
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var nodeIds = document.Nodes.Select(n => n.Id).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (var edge in document.Edges)
|
||||
{
|
||||
nodeIds.Should().Contain(edge.SourceId, $"Edge source {edge.SourceId} in {fixtureName} must reference existing node");
|
||||
nodeIds.Should().Contain(edge.TargetId, $"Edge target {edge.TargetId} in {fixtureName} must reference existing node");
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_EntrypointsReferenceValidNodes(string fixtureName)
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var nodeIds = document.Nodes.Select(n => n.Id).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (var entrypoint in document.Entrypoints)
|
||||
{
|
||||
nodeIds.Should().Contain(entrypoint.NodeId, $"Entrypoint {entrypoint.NodeId} in {fixtureName} must reference existing node");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotNetFixture_HasCorrectLanguageEnum()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
document.LanguageType.Should().Be(CallgraphLanguage.DotNet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JavaFixture_HasCorrectLanguageEnum()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "java-spring-boot.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
document.LanguageType.Should().Be(CallgraphLanguage.Java);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NodeFixture_HasCorrectLanguageEnum()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "node-express-api.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
document.LanguageType.Should().Be(CallgraphLanguage.Node);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GoFixture_HasCorrectLanguageEnum()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "go-gin-api.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
document.LanguageType.Should().Be(CallgraphLanguage.Go);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllEdgeReasonsFixture_ContainsAllEdgeReasons()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var expectedReasons = Enum.GetValues<EdgeReason>();
|
||||
var actualReasons = document.Edges.Select(e => e.Reason).Distinct().ToHashSet();
|
||||
|
||||
foreach (var expected in expectedReasons)
|
||||
{
|
||||
actualReasons.Should().Contain(expected, $"EdgeReason.{expected} should be covered by fixture");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllEdgeReasonsFixture_ContainsAllEdgeKinds()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var expectedKinds = Enum.GetValues<EdgeKind>();
|
||||
var actualKinds = document.Edges.Select(e => e.Kind).Distinct().ToHashSet();
|
||||
|
||||
foreach (var expected in expectedKinds)
|
||||
{
|
||||
actualKinds.Should().Contain(expected, $"EdgeKind.{expected} should be covered by fixture");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllVisibilityFixture_ContainsAllVisibilityLevels()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-visibility-levels.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var expectedVisibilities = Enum.GetValues<SymbolVisibility>();
|
||||
var actualVisibilities = document.Nodes.Select(n => n.Visibility).Distinct().ToHashSet();
|
||||
|
||||
foreach (var expected in expectedVisibilities)
|
||||
{
|
||||
actualVisibilities.Should().Contain(expected, $"SymbolVisibility.{expected} should be covered by fixture");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyFixture_HasNoSchemaField()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "legacy-no-schema.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
// Legacy fixture should deserialize but have default schema (v1) due to property default
|
||||
document.Schema.Should().Be(CallgraphSchemaVersions.V1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyFixture_MigratesToV1Schema()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "legacy-no-schema.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var migrated = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
|
||||
migrated.Schema.Should().Be(CallgraphSchemaVersions.V1);
|
||||
// Verify that nodes have visibility inferred (may be Unknown for some cases)
|
||||
migrated.Nodes.Should().AllSatisfy(n => Enum.IsDefined(n.Visibility).Should().BeTrue());
|
||||
// Verify that edges have reason inferred (defaults to DirectCall for legacy 'call' type)
|
||||
migrated.Edges.Should().AllSatisfy(e => Enum.IsDefined(e.Reason).Should().BeTrue());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dotnet-aspnetcore-minimal")]
|
||||
[InlineData("java-spring-boot")]
|
||||
[InlineData("node-express-api")]
|
||||
[InlineData("go-gin-api")]
|
||||
public void V1Fixture_MigrationIsIdempotent(string fixtureName)
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var migrated1 = CallgraphSchemaMigrator.EnsureV1(document);
|
||||
var migrated2 = CallgraphSchemaMigrator.EnsureV1(migrated1);
|
||||
|
||||
migrated2.Schema.Should().Be(migrated1.Schema);
|
||||
migrated2.Nodes.Should().HaveCount(migrated1.Nodes.Count);
|
||||
migrated2.Edges.Should().HaveCount(migrated1.Edges.Count);
|
||||
migrated2.Entrypoints.Should().HaveCount(migrated1.Entrypoints.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeReason_SerializesAsCamelCaseString()
|
||||
{
|
||||
var edge = new CallgraphEdge
|
||||
{
|
||||
SourceId = "s1",
|
||||
TargetId = "t1",
|
||||
Type = "call",
|
||||
Reason = EdgeReason.DirectCall
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(edge, DeterministicOptions);
|
||||
|
||||
json.Should().Contain("\"reason\": \"directCall\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolVisibility_SerializesAsCamelCaseString()
|
||||
{
|
||||
var node = new CallgraphNode
|
||||
{
|
||||
Id = "n1",
|
||||
Name = "Test",
|
||||
Kind = "method",
|
||||
Visibility = SymbolVisibility.Public
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(node, DeterministicOptions);
|
||||
|
||||
json.Should().Contain("\"visibility\": \"public\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntrypointKind_SerializesAsCamelCaseString()
|
||||
{
|
||||
var entrypoint = new CallgraphEntrypoint
|
||||
{
|
||||
NodeId = "n1",
|
||||
Kind = EntrypointKind.Http,
|
||||
Framework = EntrypointFramework.AspNetCore
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(entrypoint, DeterministicOptions);
|
||||
|
||||
json.Should().Contain("\"kind\": \"http\"");
|
||||
json.Should().Contain("\"framework\": \"aspNetCore\"");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_NodesSortedById(string fixtureName)
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var nodeIds = document.Nodes.Select(n => n.Id).ToList();
|
||||
var sortedIds = nodeIds.OrderBy(id => id, StringComparer.Ordinal).ToList();
|
||||
|
||||
nodeIds.Should().Equal(sortedIds, $"Nodes in {fixtureName} should be sorted by Id for determinism");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_EntrypointsSortedByOrder(string fixtureName)
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var orders = document.Entrypoints.Select(e => e.Order).ToList();
|
||||
var sortedOrders = orders.OrderBy(o => o).ToList();
|
||||
|
||||
orders.Should().Equal(sortedOrders, $"Entrypoints in {fixtureName} should be sorted by Order for determinism");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotNetFixture_HasCorrectAspNetCoreEntrypoints()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Main && e.Framework == EntrypointFramework.AspNetCore);
|
||||
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Http && e.Route == "/weatherforecast");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JavaFixture_HasCorrectSpringEntrypoints()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "java-spring-boot.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Main && e.Framework == EntrypointFramework.SpringBoot);
|
||||
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Http && e.Route == "/owners/{ownerId}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GoFixture_HasModuleInitEntrypoint()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "go-gin-api.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.ModuleInit && e.Phase == EntrypointPhase.ModuleInit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllEdgeReasonsFixture_ReflectionEdgeIsUnresolved()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var reflectionEdge = document.Edges.Single(e => e.Reason == EdgeReason.ReflectionString);
|
||||
reflectionEdge.IsResolved.Should().BeFalse("Reflection edges are typically unresolved");
|
||||
reflectionEdge.Weight.Should().BeLessThan(1.0, "Reflection edges should have lower confidence");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllEdgeReasonsFixture_DiBindingHasProvenance()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
var diEdge = document.Edges.Single(e => e.Reason == EdgeReason.DiBinding);
|
||||
diEdge.Provenance.Should().NotBeNullOrEmpty("DI binding edges should include provenance");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Artifacts_HaveRequiredFields()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
document.Artifacts.Should().NotBeEmpty();
|
||||
foreach (var artifact in document.Artifacts)
|
||||
{
|
||||
artifact.ArtifactKey.Should().NotBeNullOrEmpty();
|
||||
artifact.Kind.Should().NotBeNullOrEmpty();
|
||||
artifact.Sha256.Should().NotBeNullOrEmpty().And.HaveLength(64);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Metadata_HasRequiredToolInfo()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
|
||||
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
|
||||
|
||||
document.GraphMetadata.Should().NotBeNull();
|
||||
document.GraphMetadata!.ToolId.Should().NotBeNullOrEmpty();
|
||||
document.GraphMetadata!.ToolVersion.Should().NotBeNullOrEmpty();
|
||||
document.GraphMetadata!.AnalysisTimestamp.Should().NotBe(default);
|
||||
}
|
||||
|
||||
private static string LocateRepoRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current != null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "Directory.Build.props")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Cannot locate repository root (missing Directory.Build.props).");
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,21 @@ public sealed class ReachabilityScoringTests
|
||||
storage[document.SubjectKey] = document;
|
||||
return Task.FromResult(document);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ReachabilityFactDocument>> GetExpiredAsync(DateTimeOffset olderThan, int limit, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<IReadOnlyList<ReachabilityFactDocument>>(Array.Empty<ReachabilityFactDocument>());
|
||||
|
||||
public Task<bool> DeleteAsync(string subjectKey, CancellationToken cancellationToken)
|
||||
{
|
||||
var removed = storage.Remove(subjectKey);
|
||||
return Task.FromResult(removed);
|
||||
}
|
||||
|
||||
public Task<int> GetRuntimeFactsCountAsync(string subjectKey, CancellationToken cancellationToken) =>
|
||||
Task.FromResult(0);
|
||||
|
||||
public Task TrimRuntimeFactsAsync(string subjectKey, int maxCount, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class InMemoryReachabilityCache : IReachabilityCache
|
||||
@@ -286,6 +301,21 @@ public sealed class ReachabilityScoringTests
|
||||
|
||||
public Task<int> CountBySubjectAsync(string subjectKey, CancellationToken cancellationToken) =>
|
||||
Task.FromResult(0);
|
||||
|
||||
public Task BulkUpdateAsync(IEnumerable<UnknownSymbolDocument> unknowns, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task<IReadOnlyList<string>> GetAllSubjectKeysAsync(CancellationToken cancellationToken) =>
|
||||
Task.FromResult((IReadOnlyList<string>)Array.Empty<string>());
|
||||
|
||||
public Task<IReadOnlyList<UnknownSymbolDocument>> GetDueForRescanAsync(UnknownsBand band, int limit, CancellationToken cancellationToken) =>
|
||||
Task.FromResult((IReadOnlyList<UnknownSymbolDocument>)Array.Empty<UnknownSymbolDocument>());
|
||||
|
||||
public Task<IReadOnlyList<UnknownSymbolDocument>> QueryAsync(UnknownsBand? band, int limit, int offset, CancellationToken cancellationToken) =>
|
||||
Task.FromResult((IReadOnlyList<UnknownSymbolDocument>)Array.Empty<UnknownSymbolDocument>());
|
||||
|
||||
public Task<UnknownSymbolDocument?> GetByIdAsync(string id, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<UnknownSymbolDocument?>(null);
|
||||
}
|
||||
|
||||
private sealed class NullEventsPublisher : IEventsPublisher
|
||||
|
||||
@@ -155,6 +155,18 @@ public sealed class RuntimeFactsIngestionServiceTests
|
||||
|
||||
public Task<ReachabilityFactDocument?> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken) =>
|
||||
Task.FromResult(LastUpsert is { SubjectKey: not null } doc && doc.SubjectKey == subjectKey ? doc : null);
|
||||
|
||||
public Task<IReadOnlyList<ReachabilityFactDocument>> GetExpiredAsync(DateTimeOffset olderThan, int limit, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<IReadOnlyList<ReachabilityFactDocument>>(Array.Empty<ReachabilityFactDocument>());
|
||||
|
||||
public Task<bool> DeleteAsync(string subjectKey, CancellationToken cancellationToken) =>
|
||||
Task.FromResult(true);
|
||||
|
||||
public Task<int> GetRuntimeFactsCountAsync(string subjectKey, CancellationToken cancellationToken) =>
|
||||
Task.FromResult(0);
|
||||
|
||||
public Task TrimRuntimeFactsAsync(string subjectKey, int maxCount, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class FakeReachabilityCache : IReachabilityCache
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"schema": "stella.callgraph.v1",
|
||||
"scanKey": "scan:edge-reasons-test:1.0.0",
|
||||
"language": "DotNet",
|
||||
"artifacts": [
|
||||
{
|
||||
"artifactKey": "TestAssembly.dll",
|
||||
"kind": "assembly",
|
||||
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{ "id": "async", "name": "AsyncTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "caller", "name": "Caller", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "delegate", "name": "DelegateTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "di", "name": "DiTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "direct", "name": "DirectTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "dynamic", "name": "DynamicTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "event", "name": "EventTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "generic", "name": "GenericTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "native", "name": "NativeTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "newobj", "name": "NewObjTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "reflection", "name": "ReflectionTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "runtime", "name": "RuntimeTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "unknown", "name": "UnknownTarget", "kind": "method", "visibility": "Public" },
|
||||
{ "id": "virtual", "name": "VirtualTarget", "kind": "method", "visibility": "Public" }
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "direct",
|
||||
"type": "call",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "virtual",
|
||||
"type": "callvirt",
|
||||
"kind": "Static",
|
||||
"reason": "VirtualCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true,
|
||||
"candidates": ["impl1", "impl2"]
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "reflection",
|
||||
"type": "reflection",
|
||||
"kind": "Heuristic",
|
||||
"reason": "ReflectionString",
|
||||
"weight": 0.5,
|
||||
"isResolved": false,
|
||||
"provenance": "Type.GetMethod"
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "di",
|
||||
"type": "di-binding",
|
||||
"kind": "Heuristic",
|
||||
"reason": "DiBinding",
|
||||
"weight": 0.9,
|
||||
"isResolved": true,
|
||||
"provenance": "Microsoft.Extensions.DependencyInjection"
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "dynamic",
|
||||
"type": "dynamic-import",
|
||||
"kind": "Heuristic",
|
||||
"reason": "DynamicImport",
|
||||
"weight": 0.7,
|
||||
"isResolved": false
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "newobj",
|
||||
"type": "newobj",
|
||||
"kind": "Static",
|
||||
"reason": "NewObj",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "delegate",
|
||||
"type": "ldftn",
|
||||
"kind": "Static",
|
||||
"reason": "DelegateCreate",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "async",
|
||||
"type": "async",
|
||||
"kind": "Static",
|
||||
"reason": "AsyncContinuation",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "event",
|
||||
"type": "event",
|
||||
"kind": "Heuristic",
|
||||
"reason": "EventHandler",
|
||||
"weight": 0.85,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "generic",
|
||||
"type": "generic",
|
||||
"kind": "Static",
|
||||
"reason": "GenericInstantiation",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "native",
|
||||
"type": "pinvoke",
|
||||
"kind": "Static",
|
||||
"reason": "NativeInterop",
|
||||
"weight": 1.0,
|
||||
"isResolved": false,
|
||||
"provenance": "kernel32.dll"
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "runtime",
|
||||
"type": "runtime",
|
||||
"kind": "Runtime",
|
||||
"reason": "RuntimeMinted",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "caller",
|
||||
"targetId": "unknown",
|
||||
"type": "unknown",
|
||||
"kind": "Heuristic",
|
||||
"reason": "Unknown",
|
||||
"weight": 0.3,
|
||||
"isResolved": false
|
||||
}
|
||||
],
|
||||
"entrypoints": [
|
||||
{
|
||||
"nodeId": "caller",
|
||||
"kind": "Test",
|
||||
"framework": "Unknown",
|
||||
"source": "test-runner",
|
||||
"phase": "Runtime",
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"toolId": "stellaops.scanner.test",
|
||||
"toolVersion": "1.0.0",
|
||||
"analysisTimestamp": "2025-01-15T14:00:00Z"
|
||||
},
|
||||
"id": "cg-edge-reasons-001",
|
||||
"component": "EdgeReasonsTest",
|
||||
"version": "1.0.0",
|
||||
"ingestedAt": "2025-01-15T14:00:00Z",
|
||||
"graphHash": "sha256:edge-reasons"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"schema": "stella.callgraph.v1",
|
||||
"scanKey": "scan:visibility-test:1.0.0",
|
||||
"language": "DotNet",
|
||||
"artifacts": [
|
||||
{
|
||||
"artifactKey": "VisibilityTest.dll",
|
||||
"kind": "assembly",
|
||||
"sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "v001",
|
||||
"name": "PublicMethod",
|
||||
"kind": "method",
|
||||
"namespace": "VisibilityTest",
|
||||
"symbolKey": "VisibilityTest::PublicMethod()",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": true
|
||||
},
|
||||
{
|
||||
"id": "v002",
|
||||
"name": "InternalMethod",
|
||||
"kind": "method",
|
||||
"namespace": "VisibilityTest.Internal",
|
||||
"symbolKey": "VisibilityTest.Internal::InternalMethod()",
|
||||
"visibility": "Internal",
|
||||
"isEntrypointCandidate": false
|
||||
},
|
||||
{
|
||||
"id": "v003",
|
||||
"name": "ProtectedMethod",
|
||||
"kind": "method",
|
||||
"namespace": "VisibilityTest",
|
||||
"symbolKey": "VisibilityTest.BaseClass::ProtectedMethod()",
|
||||
"visibility": "Protected",
|
||||
"isEntrypointCandidate": false
|
||||
},
|
||||
{
|
||||
"id": "v004",
|
||||
"name": "PrivateMethod",
|
||||
"kind": "method",
|
||||
"namespace": "VisibilityTest",
|
||||
"symbolKey": "VisibilityTest.SomeClass::PrivateMethod()",
|
||||
"visibility": "Private",
|
||||
"isEntrypointCandidate": false
|
||||
},
|
||||
{
|
||||
"id": "v005",
|
||||
"name": "UnknownMethod",
|
||||
"kind": "method",
|
||||
"namespace": "External",
|
||||
"symbolKey": "External::UnknownMethod()",
|
||||
"visibility": "Unknown",
|
||||
"isEntrypointCandidate": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"sourceId": "v001",
|
||||
"targetId": "v002",
|
||||
"type": "call",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "v002",
|
||||
"targetId": "v003",
|
||||
"type": "call",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "v003",
|
||||
"targetId": "v004",
|
||||
"type": "call",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "v004",
|
||||
"targetId": "v005",
|
||||
"type": "external",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": false
|
||||
}
|
||||
],
|
||||
"entrypoints": [
|
||||
{
|
||||
"nodeId": "v001",
|
||||
"kind": "Http",
|
||||
"route": "/api/visibility",
|
||||
"httpMethod": "GET",
|
||||
"framework": "AspNetCore",
|
||||
"source": "attribute",
|
||||
"phase": "Runtime",
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"toolId": "stellaops.scanner.test",
|
||||
"toolVersion": "1.0.0",
|
||||
"analysisTimestamp": "2025-01-15T15:00:00Z"
|
||||
},
|
||||
"id": "cg-visibility-001",
|
||||
"component": "VisibilityTest",
|
||||
"version": "1.0.0",
|
||||
"ingestedAt": "2025-01-15T15:00:00Z",
|
||||
"graphHash": "sha256:visibility"
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"schema": "stella.callgraph.v1",
|
||||
"scanKey": "scan:dotnet-aspnetcore-minimal:v1.0.0",
|
||||
"language": "DotNet",
|
||||
"artifacts": [
|
||||
{
|
||||
"artifactKey": "SampleApi.dll",
|
||||
"kind": "assembly",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"purl": "pkg:nuget/SampleApi@1.0.0",
|
||||
"buildId": "build-001",
|
||||
"filePath": "/app/SampleApi.dll",
|
||||
"sizeBytes": 12345
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "n001",
|
||||
"nodeId": "n001",
|
||||
"name": "Main",
|
||||
"kind": "method",
|
||||
"namespace": "SampleApi",
|
||||
"file": "Program.cs",
|
||||
"line": 1,
|
||||
"symbolKey": "SampleApi::Main(string[])",
|
||||
"artifactKey": "SampleApi.dll",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": true,
|
||||
"attributes": {
|
||||
"returnType": "void"
|
||||
},
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "n002",
|
||||
"nodeId": "n002",
|
||||
"name": "GetWeatherForecast",
|
||||
"kind": "method",
|
||||
"namespace": "SampleApi.Controllers",
|
||||
"file": "WeatherForecastController.cs",
|
||||
"line": 15,
|
||||
"symbolKey": "SampleApi.Controllers.WeatherForecastController::GetWeatherForecast()",
|
||||
"artifactKey": "SampleApi.dll",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": true,
|
||||
"attributes": {
|
||||
"returnType": "IEnumerable<WeatherForecast>",
|
||||
"httpMethod": "GET",
|
||||
"route": "/weatherforecast"
|
||||
},
|
||||
"flags": 3
|
||||
},
|
||||
{
|
||||
"id": "n003",
|
||||
"nodeId": "n003",
|
||||
"name": "GetRandomSummary",
|
||||
"kind": "method",
|
||||
"namespace": "SampleApi.Services",
|
||||
"file": "WeatherService.cs",
|
||||
"line": 20,
|
||||
"symbolKey": "SampleApi.Services.WeatherService::GetRandomSummary()",
|
||||
"artifactKey": "SampleApi.dll",
|
||||
"visibility": "Internal",
|
||||
"isEntrypointCandidate": false,
|
||||
"attributes": {
|
||||
"returnType": "string"
|
||||
},
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "n004",
|
||||
"nodeId": "n004",
|
||||
"name": "CreateLogger",
|
||||
"kind": "method",
|
||||
"namespace": "SampleApi.Internal",
|
||||
"file": "LoggingHelper.cs",
|
||||
"line": 8,
|
||||
"symbolKey": "SampleApi.Internal.LoggingHelper::CreateLogger()",
|
||||
"artifactKey": "SampleApi.dll",
|
||||
"visibility": "Private",
|
||||
"isEntrypointCandidate": false,
|
||||
"flags": 0
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"sourceId": "n001",
|
||||
"targetId": "n002",
|
||||
"from": "n001",
|
||||
"to": "n002",
|
||||
"type": "call",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "n002",
|
||||
"targetId": "n003",
|
||||
"from": "n002",
|
||||
"to": "n003",
|
||||
"type": "di",
|
||||
"kind": "Heuristic",
|
||||
"reason": "DiBinding",
|
||||
"weight": 0.9,
|
||||
"isResolved": true,
|
||||
"provenance": "Microsoft.Extensions.DependencyInjection"
|
||||
},
|
||||
{
|
||||
"sourceId": "n003",
|
||||
"targetId": "n004",
|
||||
"from": "n003",
|
||||
"to": "n004",
|
||||
"type": "call",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"offset": 42,
|
||||
"isResolved": true
|
||||
}
|
||||
],
|
||||
"entrypoints": [
|
||||
{
|
||||
"nodeId": "n001",
|
||||
"kind": "Main",
|
||||
"framework": "AspNetCore",
|
||||
"source": "attribute",
|
||||
"phase": "AppStart",
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"nodeId": "n002",
|
||||
"kind": "Http",
|
||||
"route": "/weatherforecast",
|
||||
"httpMethod": "GET",
|
||||
"framework": "AspNetCore",
|
||||
"source": "attribute",
|
||||
"phase": "Runtime",
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"toolId": "stellaops.scanner.dotnet",
|
||||
"toolVersion": "1.0.0",
|
||||
"analysisTimestamp": "2025-01-15T10:00:00Z",
|
||||
"sourceCommit": "abc123def456",
|
||||
"buildId": "build-001"
|
||||
},
|
||||
"id": "cg-dotnet-aspnetcore-minimal-001",
|
||||
"languageString": "dotnet",
|
||||
"component": "SampleApi",
|
||||
"version": "1.0.0",
|
||||
"ingestedAt": "2025-01-15T10:00:00Z",
|
||||
"graphHash": "sha256:a1b2c3d4e5f6"
|
||||
}
|
||||
155
tests/reachability/fixtures/callgraph-schema-v1/go-gin-api.json
Normal file
155
tests/reachability/fixtures/callgraph-schema-v1/go-gin-api.json
Normal file
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"schema": "stella.callgraph.v1",
|
||||
"scanKey": "scan:gin-api:1.5.0",
|
||||
"language": "Go",
|
||||
"artifacts": [
|
||||
{
|
||||
"artifactKey": "gin-api",
|
||||
"kind": "go-binary",
|
||||
"sha256": "d5e6f78901234567890abcdef0123456789abcdef0123456789abcdef0123456",
|
||||
"purl": "pkg:golang/github.com/example/gin-api@1.5.0",
|
||||
"filePath": "/app/gin-api",
|
||||
"sizeBytes": 15000000
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "g001",
|
||||
"nodeId": "g001",
|
||||
"name": "main",
|
||||
"kind": "function",
|
||||
"namespace": "main",
|
||||
"file": "main.go",
|
||||
"line": 12,
|
||||
"symbolKey": "main.main",
|
||||
"artifactKey": "gin-api",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": true,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "g002",
|
||||
"nodeId": "g002",
|
||||
"name": "GetProduct",
|
||||
"kind": "function",
|
||||
"namespace": "handlers",
|
||||
"file": "product_handler.go",
|
||||
"line": 28,
|
||||
"symbolKey": "github.com/example/gin-api/handlers.GetProduct",
|
||||
"artifactKey": "gin-api",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": true,
|
||||
"attributes": {
|
||||
"httpMethod": "GET",
|
||||
"route": "/api/products/:id"
|
||||
},
|
||||
"flags": 3
|
||||
},
|
||||
{
|
||||
"id": "g003",
|
||||
"nodeId": "g003",
|
||||
"name": "FindByID",
|
||||
"kind": "function",
|
||||
"namespace": "repository",
|
||||
"file": "product_repo.go",
|
||||
"line": 45,
|
||||
"symbolKey": "github.com/example/gin-api/repository.(*ProductRepo).FindByID",
|
||||
"artifactKey": "gin-api",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": false,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "g004",
|
||||
"nodeId": "g004",
|
||||
"name": "init",
|
||||
"kind": "function",
|
||||
"namespace": "config",
|
||||
"file": "config.go",
|
||||
"line": 8,
|
||||
"symbolKey": "github.com/example/gin-api/config.init",
|
||||
"artifactKey": "gin-api",
|
||||
"visibility": "Unknown",
|
||||
"isEntrypointCandidate": true,
|
||||
"flags": 2
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"sourceId": "g004",
|
||||
"targetId": "g001",
|
||||
"from": "g004",
|
||||
"to": "g001",
|
||||
"type": "init",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true,
|
||||
"provenance": "go-init-order"
|
||||
},
|
||||
{
|
||||
"sourceId": "g001",
|
||||
"targetId": "g002",
|
||||
"from": "g001",
|
||||
"to": "g002",
|
||||
"type": "router-bind",
|
||||
"kind": "Heuristic",
|
||||
"reason": "DelegateCreate",
|
||||
"weight": 0.9,
|
||||
"isResolved": true,
|
||||
"provenance": "gin-router"
|
||||
},
|
||||
{
|
||||
"sourceId": "g002",
|
||||
"targetId": "g003",
|
||||
"from": "g002",
|
||||
"to": "g003",
|
||||
"type": "interface",
|
||||
"kind": "Static",
|
||||
"reason": "VirtualCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
}
|
||||
],
|
||||
"entrypoints": [
|
||||
{
|
||||
"nodeId": "g004",
|
||||
"kind": "ModuleInit",
|
||||
"framework": "Unknown",
|
||||
"source": "convention",
|
||||
"phase": "ModuleInit",
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"nodeId": "g001",
|
||||
"kind": "Main",
|
||||
"framework": "Gin",
|
||||
"source": "convention",
|
||||
"phase": "AppStart",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"nodeId": "g002",
|
||||
"kind": "Http",
|
||||
"route": "/api/products/:id",
|
||||
"httpMethod": "GET",
|
||||
"framework": "Gin",
|
||||
"source": "code-analysis",
|
||||
"phase": "Runtime",
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"toolId": "stellaops.scanner.go",
|
||||
"toolVersion": "1.0.0",
|
||||
"analysisTimestamp": "2025-01-15T13:00:00Z",
|
||||
"sourceCommit": "012def345abc",
|
||||
"buildId": "build-004"
|
||||
},
|
||||
"id": "cg-go-gin-api-001",
|
||||
"languageString": "go",
|
||||
"component": "gin-api",
|
||||
"version": "1.5.0",
|
||||
"ingestedAt": "2025-01-15T13:00:00Z",
|
||||
"graphHash": "sha256:d4e5f6a7b8c9"
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"schema": "stella.callgraph.v1",
|
||||
"scanKey": "scan:spring-petclinic:3.2.0",
|
||||
"language": "Java",
|
||||
"artifacts": [
|
||||
{
|
||||
"artifactKey": "spring-petclinic-3.2.0.jar",
|
||||
"kind": "jar",
|
||||
"sha256": "f4d3c2b1a0987654321fedcba0987654321fedcba0987654321fedcba098765",
|
||||
"purl": "pkg:maven/org.springframework.samples/spring-petclinic@3.2.0",
|
||||
"filePath": "/app/spring-petclinic-3.2.0.jar",
|
||||
"sizeBytes": 54321000
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "j001",
|
||||
"nodeId": "j001",
|
||||
"name": "main",
|
||||
"kind": "method",
|
||||
"namespace": "org.springframework.samples.petclinic",
|
||||
"file": "PetClinicApplication.java",
|
||||
"line": 25,
|
||||
"symbolKey": "org.springframework.samples.petclinic.PetClinicApplication::main(String[])",
|
||||
"artifactKey": "spring-petclinic-3.2.0.jar",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": true,
|
||||
"attributes": {
|
||||
"returnType": "void",
|
||||
"modifiers": "public static"
|
||||
},
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "j002",
|
||||
"nodeId": "j002",
|
||||
"name": "showOwner",
|
||||
"kind": "method",
|
||||
"namespace": "org.springframework.samples.petclinic.owner",
|
||||
"file": "OwnerController.java",
|
||||
"line": 87,
|
||||
"symbolKey": "org.springframework.samples.petclinic.owner.OwnerController::showOwner(int)",
|
||||
"artifactKey": "spring-petclinic-3.2.0.jar",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": true,
|
||||
"attributes": {
|
||||
"returnType": "ModelAndView",
|
||||
"httpMethod": "GET",
|
||||
"route": "/owners/{ownerId}"
|
||||
},
|
||||
"flags": 3
|
||||
},
|
||||
{
|
||||
"id": "j003",
|
||||
"nodeId": "j003",
|
||||
"name": "findById",
|
||||
"kind": "method",
|
||||
"namespace": "org.springframework.samples.petclinic.owner",
|
||||
"file": "OwnerRepository.java",
|
||||
"line": 42,
|
||||
"symbolKey": "org.springframework.samples.petclinic.owner.OwnerRepository::findById(Integer)",
|
||||
"artifactKey": "spring-petclinic-3.2.0.jar",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": false,
|
||||
"attributes": {
|
||||
"returnType": "Owner"
|
||||
},
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "j004",
|
||||
"nodeId": "j004",
|
||||
"name": "validateOwner",
|
||||
"kind": "method",
|
||||
"namespace": "org.springframework.samples.petclinic.owner",
|
||||
"file": "OwnerValidator.java",
|
||||
"line": 30,
|
||||
"symbolKey": "org.springframework.samples.petclinic.owner.OwnerValidator::validateOwner(Owner)",
|
||||
"artifactKey": "spring-petclinic-3.2.0.jar",
|
||||
"visibility": "Protected",
|
||||
"isEntrypointCandidate": false,
|
||||
"flags": 0
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"sourceId": "j001",
|
||||
"targetId": "j002",
|
||||
"from": "j001",
|
||||
"to": "j002",
|
||||
"type": "spring-bean",
|
||||
"kind": "Heuristic",
|
||||
"reason": "DiBinding",
|
||||
"weight": 0.85,
|
||||
"isResolved": true,
|
||||
"provenance": "SpringBoot"
|
||||
},
|
||||
{
|
||||
"sourceId": "j002",
|
||||
"targetId": "j003",
|
||||
"from": "j002",
|
||||
"to": "j003",
|
||||
"type": "virtual",
|
||||
"kind": "Static",
|
||||
"reason": "VirtualCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "j002",
|
||||
"targetId": "j004",
|
||||
"from": "j002",
|
||||
"to": "j004",
|
||||
"type": "call",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"offset": 156,
|
||||
"isResolved": true
|
||||
}
|
||||
],
|
||||
"entrypoints": [
|
||||
{
|
||||
"nodeId": "j001",
|
||||
"kind": "Main",
|
||||
"framework": "SpringBoot",
|
||||
"source": "annotation",
|
||||
"phase": "AppStart",
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"nodeId": "j002",
|
||||
"kind": "Http",
|
||||
"route": "/owners/{ownerId}",
|
||||
"httpMethod": "GET",
|
||||
"framework": "Spring",
|
||||
"source": "annotation",
|
||||
"phase": "Runtime",
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"toolId": "stellaops.scanner.java",
|
||||
"toolVersion": "1.0.0",
|
||||
"analysisTimestamp": "2025-01-15T11:00:00Z",
|
||||
"sourceCommit": "def789abc012",
|
||||
"buildId": "build-002"
|
||||
},
|
||||
"id": "cg-java-spring-petclinic-001",
|
||||
"languageString": "java",
|
||||
"component": "spring-petclinic",
|
||||
"version": "3.2.0",
|
||||
"ingestedAt": "2025-01-15T11:00:00Z",
|
||||
"graphHash": "sha256:b2c3d4e5f6a7"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"id": "cg-legacy-001",
|
||||
"languageString": "csharp",
|
||||
"component": "LegacyApp",
|
||||
"version": "0.9.0",
|
||||
"ingestedAt": "2024-06-15T08:00:00Z",
|
||||
"graphHash": "sha256:legacy123",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "l001",
|
||||
"name": "Main",
|
||||
"kind": "method",
|
||||
"namespace": "LegacyApp"
|
||||
},
|
||||
{
|
||||
"id": "l002",
|
||||
"name": "ProcessData",
|
||||
"kind": "method",
|
||||
"namespace": "LegacyApp.Controllers"
|
||||
},
|
||||
{
|
||||
"id": "l003",
|
||||
"name": "ValidateInput",
|
||||
"kind": "method",
|
||||
"namespace": "LegacyApp.Internal"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"sourceId": "l001",
|
||||
"targetId": "l002",
|
||||
"type": "call"
|
||||
},
|
||||
{
|
||||
"sourceId": "l002",
|
||||
"targetId": "l003",
|
||||
"type": "call"
|
||||
}
|
||||
],
|
||||
"roots": [
|
||||
{
|
||||
"id": "l001",
|
||||
"phase": "startup",
|
||||
"source": "convention"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"schema": "stella.callgraph.v1",
|
||||
"scanKey": "scan:express-api:2.1.0",
|
||||
"language": "Node",
|
||||
"artifacts": [
|
||||
{
|
||||
"artifactKey": "express-api",
|
||||
"kind": "npm-package",
|
||||
"sha256": "c4d5e6f7890123456789abcdef0123456789abcdef0123456789abcdef012345",
|
||||
"purl": "pkg:npm/express-api@2.1.0",
|
||||
"filePath": "/app",
|
||||
"sizeBytes": 2500000
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "e001",
|
||||
"nodeId": "e001",
|
||||
"name": "startServer",
|
||||
"kind": "function",
|
||||
"namespace": "src",
|
||||
"file": "index.js",
|
||||
"line": 15,
|
||||
"symbolKey": "src/index.js::startServer",
|
||||
"artifactKey": "express-api",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": true,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "e002",
|
||||
"nodeId": "e002",
|
||||
"name": "getUserById",
|
||||
"kind": "function",
|
||||
"namespace": "src/routes",
|
||||
"file": "users.js",
|
||||
"line": 22,
|
||||
"symbolKey": "src/routes/users.js::getUserById",
|
||||
"artifactKey": "express-api",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": true,
|
||||
"attributes": {
|
||||
"httpMethod": "GET",
|
||||
"route": "/api/users/:id"
|
||||
},
|
||||
"flags": 3
|
||||
},
|
||||
{
|
||||
"id": "e003",
|
||||
"nodeId": "e003",
|
||||
"name": "findUser",
|
||||
"kind": "function",
|
||||
"namespace": "src/services",
|
||||
"file": "userService.js",
|
||||
"line": 45,
|
||||
"symbolKey": "src/services/userService.js::findUser",
|
||||
"artifactKey": "express-api",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": false,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "e004",
|
||||
"nodeId": "e004",
|
||||
"name": "query",
|
||||
"kind": "function",
|
||||
"namespace": "src/db",
|
||||
"file": "connection.js",
|
||||
"line": 30,
|
||||
"symbolKey": "src/db/connection.js::query",
|
||||
"artifactKey": "express-api",
|
||||
"visibility": "Public",
|
||||
"isEntrypointCandidate": false,
|
||||
"flags": 0
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"sourceId": "e001",
|
||||
"targetId": "e002",
|
||||
"from": "e001",
|
||||
"to": "e002",
|
||||
"type": "require",
|
||||
"kind": "Static",
|
||||
"reason": "DynamicImport",
|
||||
"weight": 0.95,
|
||||
"isResolved": true,
|
||||
"provenance": "express-router"
|
||||
},
|
||||
{
|
||||
"sourceId": "e002",
|
||||
"targetId": "e003",
|
||||
"from": "e002",
|
||||
"to": "e003",
|
||||
"type": "call",
|
||||
"kind": "Static",
|
||||
"reason": "DirectCall",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"sourceId": "e003",
|
||||
"targetId": "e004",
|
||||
"from": "e003",
|
||||
"to": "e004",
|
||||
"type": "async-call",
|
||||
"kind": "Static",
|
||||
"reason": "AsyncContinuation",
|
||||
"weight": 1.0,
|
||||
"isResolved": true
|
||||
}
|
||||
],
|
||||
"entrypoints": [
|
||||
{
|
||||
"nodeId": "e001",
|
||||
"kind": "Main",
|
||||
"framework": "Express",
|
||||
"source": "convention",
|
||||
"phase": "AppStart",
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"nodeId": "e002",
|
||||
"kind": "Http",
|
||||
"route": "/api/users/:id",
|
||||
"httpMethod": "GET",
|
||||
"framework": "Express",
|
||||
"source": "code-analysis",
|
||||
"phase": "Runtime",
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"toolId": "stellaops.scanner.node",
|
||||
"toolVersion": "1.0.0",
|
||||
"analysisTimestamp": "2025-01-15T12:00:00Z",
|
||||
"sourceCommit": "789abc012def",
|
||||
"buildId": "build-003"
|
||||
},
|
||||
"id": "cg-node-express-api-001",
|
||||
"languageString": "javascript",
|
||||
"component": "express-api",
|
||||
"version": "2.1.0",
|
||||
"ingestedAt": "2025-01-15T12:00:00Z",
|
||||
"graphHash": "sha256:c3d4e5f6a7b8"
|
||||
}
|
||||
Reference in New Issue
Block a user