This commit is contained in:
StellaOps Bot
2025-12-09 00:20:52 +02:00
parent 3d01bf9edc
commit bc0762e97d
261 changed files with 14033 additions and 4427 deletions

View File

@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Signals.Models;
using StellaOps.Signals.Options;
using StellaOps.Signals.Parsing;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Services;
using StellaOps.Signals.Storage;
using StellaOps.Signals.Storage.Models;
using Xunit;
namespace StellaOps.Signals.Tests;
public class CallgraphIngestionServiceTests
{
private readonly InMemoryCallgraphRepository _repository = new();
private readonly InMemoryArtifactStore _artifactStore = new();
private readonly CallgraphNormalizationService _normalizer = new();
private readonly TimeProvider _timeProvider = TimeProvider.System;
[Fact]
public async Task IngestAsync_normalizes_graph_and_persists_manifest_hash()
{
var parser = new StubParser("java");
var resolver = new StubParserResolver(parser);
var options = Microsoft.Extensions.Options.Options.Create(new SignalsOptions());
var service = new CallgraphIngestionService(
resolver,
_artifactStore,
_repository,
_normalizer,
options,
_timeProvider,
NullLogger<CallgraphIngestionService>.Instance);
var artifactJson = @"{""nodes"":[{""id"":""com/example/Foo.bar:(I)V"",""kind"":""fn""}],
""edges"":[{""source"":""com/example/Foo.bar:(I)V"",""target"":""com/example/Foo.bar:(I)V""}]}";
var request = new CallgraphIngestRequest(
Language: "java",
Component: "demo",
Version: "1.0.0",
ArtifactFileName: "graph.json",
ArtifactContentType: "application/json",
ArtifactContentBase64: Convert.ToBase64String(Encoding.UTF8.GetBytes(artifactJson)),
SchemaVersion: null,
Metadata: new Dictionary<string, string?> { ["source"] = "test" },
Analyzer: new Dictionary<string, string?> { ["name"] = "stub" });
var response = await service.IngestAsync(request, CancellationToken.None);
response.CallgraphId.Should().NotBeNullOrWhiteSpace();
response.GraphHash.Should().NotBeNullOrWhiteSpace();
response.NodeCount.Should().Be(1);
response.EdgeCount.Should().Be(1);
response.ManifestCasUri.Should().Be("cas://signals/manifests/graph.json");
var stored = _repository.LastUpserted!;
stored.Artifact.Hash.Should().Be(response.ArtifactHash);
stored.Nodes[0].Namespace.Should().Be("com.example.Foo");
stored.Nodes[0].Language.Should().Be("java");
stored.Metadata!["schemaVersion"].Should().Be("1.0");
stored.Metadata!["analyzer.name"].Should().Be("stub");
stored.Artifact.GraphHash.Should().Be(response.GraphHash);
}
private sealed class StubParser : ICallgraphParser
{
public StubParser(string language) => Language = language;
public string Language { get; }
public Task<CallgraphParseResult> ParseAsync(Stream artifactStream, CancellationToken cancellationToken)
{
artifactStream.Position = 0;
using var doc = JsonDocument.Parse(artifactStream);
var nodes = new List<CallgraphNode>();
foreach (var node in doc.RootElement.GetProperty("nodes").EnumerateArray())
{
nodes.Add(new CallgraphNode(node.GetProperty("id").GetString()!, "", "function", null, null, null));
}
var edges = new List<CallgraphEdge>();
foreach (var edge in doc.RootElement.GetProperty("edges").EnumerateArray())
{
edges.Add(new CallgraphEdge(
edge.GetProperty("source").GetString()!,
edge.GetProperty("target").GetString()!,
"call"));
}
return Task.FromResult(new CallgraphParseResult(nodes, edges, Array.Empty<CallgraphRoot>(), "1.0", "1.0", null));
}
}
private sealed class StubParserResolver : ICallgraphParserResolver
{
private readonly ICallgraphParser _parser;
public StubParserResolver(ICallgraphParser parser) => _parser = parser;
public ICallgraphParser Resolve(string language) => _parser;
}
private sealed class InMemoryArtifactStore : ICallgraphArtifactStore
{
private readonly Dictionary<string, byte[]> artifacts = new(StringComparer.Ordinal);
private readonly Dictionary<string, byte[]> manifests = new(StringComparer.Ordinal);
public Task<StoredCallgraphArtifact> SaveAsync(CallgraphArtifactSaveRequest request, Stream content, CancellationToken cancellationToken)
{
using var ms = new MemoryStream();
content.CopyTo(ms);
artifacts[request.Hash] = ms.ToArray();
if (request.ManifestContent is not null)
{
using var manifestMs = new MemoryStream();
request.ManifestContent.CopyTo(manifestMs);
manifests[request.Hash] = manifestMs.ToArray();
}
var path = $"cas://signals/artifacts/{request.FileName}";
var manifestPath = "cas://signals/manifests/graph.json";
return Task.FromResult(new StoredCallgraphArtifact(
Path: path,
Length: ms.Length,
Hash: request.Hash,
ContentType: request.ContentType,
CasUri: path,
ManifestPath: manifestPath,
ManifestCasUri: manifestPath));
}
public Task<Stream?> GetAsync(string hash, string? fileName = null, CancellationToken cancellationToken = default)
{
if (artifacts.TryGetValue(hash, out var bytes))
{
return Task.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
}
return Task.FromResult<Stream?>(null);
}
public Task<Stream?> GetManifestAsync(string hash, CancellationToken cancellationToken = default)
{
if (manifests.TryGetValue(hash, out var bytes))
{
return Task.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
}
return Task.FromResult<Stream?>(null);
}
public Task<bool> ExistsAsync(string hash, CancellationToken cancellationToken = default)
{
return Task.FromResult(artifacts.ContainsKey(hash));
}
}
private sealed class InMemoryCallgraphRepository : ICallgraphRepository
{
public CallgraphDocument? LastUpserted { get; private set; }
public Task<CallgraphDocument?> GetByIdAsync(string id, CancellationToken cancellationToken)
{
return Task.FromResult(LastUpserted?.Id == id ? LastUpserted : null);
}
public Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken)
{
LastUpserted = document;
return Task.FromResult(document);
}
}
}