feat(ruby): Implement RubyManifestParser for parsing gem groups and dependencies
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

feat(ruby): Add RubyVendorArtifactCollector to collect vendor artifacts

test(deno): Add golden tests for Deno analyzer with various fixtures

test(deno): Create Deno module and package files for testing

test(deno): Implement Deno lock and import map for dependency management

test(deno): Add FFI and worker scripts for Deno testing

feat(ruby): Set up Ruby workspace with Gemfile and dependencies

feat(ruby): Add expected output for Ruby workspace tests

feat(signals): Introduce CallgraphManifest model for signal processing
This commit is contained in:
master
2025-11-10 09:27:03 +02:00
parent 69c59defdc
commit 56c687253f
87 changed files with 2462 additions and 542 deletions

View File

@@ -62,8 +62,8 @@ public sealed class BundleInspectorTests
var scan = DenoBundleScanner.Scan(temp, CancellationToken.None);
var observations = DenoBundleScanner.ToObservations(scan);
Assert.Equal(1, scan.EszipBundles.Length);
Assert.Equal(1, scan.CompiledBundles.Length);
Assert.Single(scan.EszipBundles);
Assert.Single(scan.CompiledBundles);
Assert.Equal(2, observations.Length);
Assert.Contains(observations, obs => obs.BundleType == "eszip");
Assert.Contains(observations, obs => obs.BundleType == "deno-compile");

View File

@@ -1,3 +1,4 @@
using System.Linq;
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
@@ -21,9 +22,24 @@ public sealed class ContainerAdapterTests
var inputs = DenoContainerAdapter.CollectInputs(workspace, observations);
Assert.NotEmpty(inputs);
Assert.Contains(inputs, input => input.Kind == DenoContainerSourceKind.Cache);
Assert.Contains(inputs, input => input.Kind == DenoContainerSourceKind.Vendor);
Assert.Contains(inputs, input => input.Kind == DenoContainerSourceKind.Bundle);
var cacheInputs = inputs.Where(input => input.Kind == DenoContainerSourceKind.Cache).ToArray();
Assert.Equal(3, cacheInputs.Length);
Assert.All(cacheInputs, cache => Assert.True(cache.Metadata.ContainsKey("path")));
var vendorInputs = inputs.Where(input => input.Kind == DenoContainerSourceKind.Vendor).ToArray();
Assert.Equal(2, vendorInputs.Length);
Assert.Contains(vendorInputs, vendor => vendor.Metadata.TryGetValue("alias", out var alias) && alias?.Contains("vendor", StringComparison.OrdinalIgnoreCase) == true);
var bundleInputs = inputs.Where(input => input.Kind == DenoContainerSourceKind.Bundle).ToArray();
Assert.Equal(2, bundleInputs.Length);
Assert.Contains(bundleInputs, bundle => string.Equals(bundle.Bundle?.BundleType, "eszip", StringComparison.OrdinalIgnoreCase));
Assert.Contains(bundleInputs, bundle => string.Equals(bundle.Bundle?.BundleType, "deno-compile", StringComparison.OrdinalIgnoreCase));
Assert.All(bundleInputs, bundle =>
{
Assert.True(bundle.Metadata.ContainsKey("entrypoint"));
Assert.True(bundle.Metadata.ContainsKey("moduleCount"));
});
}
finally
{

View File

@@ -70,6 +70,7 @@ public sealed class DenoWorkspaceNormalizerTests
var graph = DenoModuleGraphResolver.Resolve(workspace, CancellationToken.None);
var compatibility = DenoNpmCompatibilityAdapter.Analyze(workspace, graph, CancellationToken.None);
var cacheSummary = string.Join(";", workspace.CacheLocations.Select(cache => $"{cache.Kind}:{cache.AbsolutePath}"));
Assert.NotEmpty(graph.Nodes);
Assert.NotEmpty(graph.Edges);
@@ -108,7 +109,7 @@ public sealed class DenoWorkspaceNormalizerTests
usage => usage.Specifier == "node:fs");
var npmResolution = compatibility.NpmResolutions.First(res => res.Specifier == "npm:dayjs@1");
Assert.True(npmResolution.ExistsOnDisk);
Assert.True(npmResolution.ExistsOnDisk, $"ResolvedPath={npmResolution.ResolvedPath ?? "(null)"}");
Assert.Equal("deno", npmResolution.Condition);
Assert.True(npmResolution.ResolvedPath?.EndsWith("deno.mod.ts", StringComparison.OrdinalIgnoreCase));
}

View File

@@ -0,0 +1,47 @@
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Deno;
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Golden;
public sealed class DenoAnalyzerGoldenTests
{
[Fact]
public async Task AnalyzerMatchesGoldenSnapshotAsync()
{
var fixture = TestPaths.ResolveFixture("lang", "deno", "full");
var golden = Path.Combine(fixture, "expected.json");
var analyzers = new ILanguageAnalyzer[] { new DenoLanguageAnalyzer() };
var cancellationToken = TestContext.Current.CancellationToken;
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixture, analyzers, cancellationToken).ConfigureAwait(false);
var normalized = Normalize(json, fixture);
var expected = await File.ReadAllTextAsync(golden, cancellationToken).ConfigureAwait(false);
normalized = normalized.TrimEnd();
expected = expected.TrimEnd();
if (!string.Equals(expected, normalized, StringComparison.Ordinal))
{
var actualPath = golden + ".actual";
await File.WriteAllTextAsync(actualPath, normalized, cancellationToken).ConfigureAwait(false);
}
Assert.Equal(expected, normalized);
}
private static string Normalize(string json, string workspaceRoot)
{
if (string.IsNullOrWhiteSpace(json))
{
return string.Empty;
}
var normalizedRoot = workspaceRoot.Replace("\\", "/", StringComparison.Ordinal);
var builder = json.Replace(normalizedRoot, "<workspace>", StringComparison.Ordinal);
var altRoot = workspaceRoot.Replace("/", "\\", StringComparison.Ordinal);
builder = builder.Replace(altRoot, "<workspace>", StringComparison.Ordinal);
return builder;
}
}

View File

@@ -1,3 +1,5 @@
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Scanner.Analyzers.Lang;
@@ -37,6 +39,27 @@ public sealed class DenoLanguageAnalyzerObservationTests
Assert.True(payload.Content.Length > 0);
Assert.NotNull(payload.Metadata);
Assert.True(payload.Metadata!.ContainsKey("deno.observation.hash"));
using var document = JsonDocument.Parse(payload.Content.Span);
var root = document.RootElement;
var entrypoints = root.GetProperty("entrypoints").EnumerateArray().Select(element => element.GetString()).ToArray();
Assert.Contains("src/main.ts", entrypoints);
var capabilities = root.GetProperty("capabilities").EnumerateArray().ToArray();
Assert.Contains(capabilities, capability => capability.GetProperty("reason").GetString() == "builtin.deno.ffi");
Assert.Contains(capabilities, capability => capability.GetProperty("reason").GetString() == "builtin.node.worker_threads");
Assert.Contains(capabilities, capability => capability.GetProperty("reason").GetString() == "builtin.node.fs");
var dynamicImports = root.GetProperty("dynamicImports").EnumerateArray().Select(element => element.GetProperty("specifier").GetString()).ToArray();
Assert.Contains("https://cdn.example.com/dynamic/mod.ts", dynamicImports);
var literalFetches = root.GetProperty("literalFetches").EnumerateArray().Select(element => element.GetProperty("url").GetString()).ToArray();
Assert.Contains("https://api.example.com/data.json", literalFetches);
var bundles = root.GetProperty("bundles").EnumerateArray().ToArray();
Assert.Contains(bundles, bundle => bundle.GetProperty("type").GetString() == "eszip");
Assert.Contains(bundles, bundle => bundle.GetProperty("type").GetString() == "deno-compile");
}
finally
{

View File

@@ -1,6 +1,8 @@
using System.IO.Compression;
using System.Text;
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Fixtures;
internal static class BundleFixtureBuilder
{
@@ -8,6 +10,10 @@ internal static class BundleFixtureBuilder
{
Directory.CreateDirectory(directory);
var bundlePath = Path.Combine(directory, "sample.eszip");
if (File.Exists(bundlePath))
{
File.Delete(bundlePath);
}
using (var archive = ZipFile.Open(bundlePath, ZipArchiveMode.Create))
{
@@ -52,6 +58,10 @@ internal static class BundleFixtureBuilder
{
Directory.CreateDirectory(directory);
var binaryPath = Path.Combine(directory, "sample.deno");
if (File.Exists(binaryPath))
{
File.Delete(binaryPath);
}
var eszipPath = CreateSampleEszip(directory);
var eszipBytes = File.ReadAllBytes(eszipPath);

View File

@@ -1,3 +1,4 @@
using StellaOps.Scanner.Analyzers.Lang.Deno.Fixtures;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
@@ -19,106 +20,7 @@ internal static class DenoWorkspaceTestFixture
private static void CreateDenoFixture(string root, out string envDenoDir)
{
Directory.CreateDirectory(root);
File.WriteAllText(Path.Combine(root, "deno.jsonc"), """
// sample deno config
{
"importMap": "./import_map.json",
"lock": {
"enabled": true,
"path": "./deno.lock"
},
"vendor": {
"enabled": true,
"path": "./vendor"
},
"nodeModulesDir": false,
"imports": {
"$std/": "https://deno.land/std@0.207.0/",
"app/": "./src/app/",
"data": "./data/data.json",
"npmDynamic": "npm:dayjs@1",
"nodeFs": "node:fs"
},
"scopes": {
"https://deno.land/": {
"fmt/": "https://deno.land/std@0.207.0/fmt/"
}
}
}
""");
Directory.CreateDirectory(Path.Combine(root, "src", "app"));
Directory.CreateDirectory(Path.Combine(root, "data"));
File.WriteAllText(Path.Combine(root, "data", "data.json"), "{ \"ok\": true }");
File.WriteAllText(
Path.Combine(root, "import_map.json"),
"""
{
"imports": {
"app/": "./src/app/main.ts",
"vendor/": "https://deno.land/std@0.207.0/"
}
}
""");
File.WriteAllText(
Path.Combine(root, "deno.lock"),
"""
{
"version": "4",
"remote": {
"https://deno.land/std@0.207.0/http/server.ts": "sha256-deadbeef",
"https://example.com/mod.ts": "sha256-feedface",
"node:fs": "builtin"
},
"redirects": {
"https://deno.land/std/http/server.ts": "https://deno.land/std@0.207.0/http/server.ts"
},
"npm": {
"specifiers": {
"npm:dayjs@1": "dayjs@1.11.12"
},
"packages": {
"dayjs@1.11.12": {
"integrity": "sha512-sample",
"dependencies": {
"tslib": "tslib@2.6.3"
}
},
"tslib@2.6.3": {
"integrity": "sha512-tslib",
"dependencies": {}
}
}
}
}
""");
var vendorRoot = Path.Combine(root, "vendor", "https", "deno.land", "std@0.207.0", "http");
Directory.CreateDirectory(vendorRoot);
File.WriteAllText(Path.Combine(vendorRoot, "server.ts"), "export const vendor = true;");
var vendorBase = Path.Combine(root, "vendor");
File.WriteAllText(
Path.Combine(vendorBase, "import_map.json"),
"""
{
"imports": {
"std/http/server.ts": "https://deno.land/std@0.207.0/http/server.ts"
}
}
""");
File.WriteAllText(
Path.Combine(vendorBase, "deno.lock"),
"""
{
"version": "1",
"remote": {}
}
""");
CreateDenoDir(Path.Combine(root, ".deno"), "workspace.ts", includeRegistry: true);
CopyTemplateWorkspace(root);
envDenoDir = Path.Combine(root, "env-deno");
CreateDenoDir(envDenoDir, "env.ts");
@@ -130,6 +32,39 @@ internal static class DenoWorkspaceTestFixture
var layerVendor = Path.Combine(layerFs, "vendor", "https", "layer.example");
Directory.CreateDirectory(layerVendor);
File.WriteAllText(Path.Combine(layerFs, "vendor", "import_map.json"), "{\"imports\":{\"layer/\": \"https://layer.example/\"}}");
var bundlesRoot = Path.Combine(root, "bundles");
Directory.CreateDirectory(bundlesRoot);
BundleFixtureBuilder.CreateSampleEszip(bundlesRoot);
BundleFixtureBuilder.CreateSampleCompiledBinary(bundlesRoot);
}
private static void CopyTemplateWorkspace(string destination)
{
var template = TestPaths.ResolveFixture("lang", "deno", "full");
CopyDirectory(template, destination);
}
private static void CopyDirectory(string source, string destination)
{
foreach (var directory in Directory.EnumerateDirectories(source, "*", SearchOption.AllDirectories))
{
var relative = Path.GetRelativePath(source, directory);
Directory.CreateDirectory(Path.Combine(destination, relative));
}
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
{
if (file.EndsWith(".actual", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var relative = Path.GetRelativePath(source, file);
var target = Path.Combine(destination, relative);
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
File.Copy(file, target, overwrite: true);
}
}
private static void CreateDenoDir(string root, string fileName, bool includeRegistry = false)
@@ -142,7 +77,7 @@ internal static class DenoWorkspaceTestFixture
Directory.CreateDirectory(npm);
File.WriteAllText(Path.Combine(deps, fileName), "export const cache = true;");
File.WriteAllText(Path.Combine(gen, $"{Path.GetFileNameWithoutExtension(fileName)}.js"), "console.log('gen');");
File.WriteAllText(Path.Combine(gen, $"{Path.GetFileNameWithoutExtension(fileName)}.js"), "console.log(gen);");
File.WriteAllText(Path.Combine(npm, "package.json"), "{}");
if (includeRegistry)