Resolve Concelier/Excititor merge conflicts
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.Core;
|
||||
|
||||
public sealed class LanguageAnalyzerResultTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MergesDuplicateComponentsDeterministicallyAsync()
|
||||
{
|
||||
var analyzer = new DuplicateComponentAnalyzer();
|
||||
var engine = new LanguageAnalyzerEngine(new[] { analyzer });
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var result = await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
|
||||
var component = Assert.Single(result.Components);
|
||||
Assert.Equal("purl::pkg:example/acme@2.0.0", component.ComponentKey);
|
||||
Assert.Equal("pkg:example/acme@2.0.0", component.Purl);
|
||||
Assert.True(component.UsedByEntrypoint);
|
||||
Assert.Equal(2, component.Evidence.Count);
|
||||
Assert.Equal(3, component.Metadata.Count);
|
||||
|
||||
// Metadata retains stable ordering (sorted by key)
|
||||
var keys = component.Metadata.Keys.ToArray();
|
||||
Assert.Equal(new[] { "artifactId", "groupId", "path" }, keys);
|
||||
|
||||
// Evidence de-duplicates via comparison key
|
||||
Assert.Equal(2, component.Evidence.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DuplicateComponentAnalyzer : ILanguageAnalyzer
|
||||
{
|
||||
public string Id => "duplicate";
|
||||
|
||||
public string DisplayName => "Duplicate Analyzer";
|
||||
|
||||
public async ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
var metadataA = new[]
|
||||
{
|
||||
new KeyValuePair<string, string?>("groupId", "example"),
|
||||
new KeyValuePair<string, string?>("artifactId", "acme")
|
||||
};
|
||||
|
||||
var metadataB = new[]
|
||||
{
|
||||
new KeyValuePair<string, string?>("artifactId", "acme"),
|
||||
new KeyValuePair<string, string?>("path", ".")
|
||||
};
|
||||
|
||||
var evidence = new[]
|
||||
{
|
||||
new LanguageComponentEvidence(LanguageEvidenceKind.File, "manifest", "META-INF/MANIFEST.MF", null, null),
|
||||
new LanguageComponentEvidence(LanguageEvidenceKind.Metadata, "pom", "pom.xml", "groupId=example", null)
|
||||
};
|
||||
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: "pkg:example/acme@2.0.0",
|
||||
name: "acme",
|
||||
version: "2.0.0",
|
||||
type: "example",
|
||||
metadata: metadataA,
|
||||
evidence: evidence,
|
||||
usedByEntrypoint: true);
|
||||
|
||||
// duplicate insert with different metadata ordering
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: "pkg:example/acme@2.0.0",
|
||||
name: "acme",
|
||||
version: "2.0.0",
|
||||
type: "example",
|
||||
metadata: metadataB,
|
||||
evidence: evidence,
|
||||
usedByEntrypoint: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.Core;
|
||||
|
||||
public sealed class LanguageComponentMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToComponentRecordsProjectsDeterministicComponents()
|
||||
{
|
||||
// Arrange
|
||||
var analyzerId = "node";
|
||||
var records = new[]
|
||||
{
|
||||
LanguageComponentRecord.FromPurl(
|
||||
analyzerId: analyzerId,
|
||||
purl: "pkg:npm/example@1.0.0",
|
||||
name: "example",
|
||||
version: "1.0.0",
|
||||
type: "npm",
|
||||
metadata: new Dictionary<string, string?>()
|
||||
{
|
||||
["path"] = "packages/app",
|
||||
["license"] = "MIT"
|
||||
},
|
||||
evidence: new[]
|
||||
{
|
||||
new LanguageComponentEvidence(LanguageEvidenceKind.File, "package.json", "packages/app/package.json", null, "abc123")
|
||||
},
|
||||
usedByEntrypoint: true),
|
||||
LanguageComponentRecord.FromExplicitKey(
|
||||
analyzerId: analyzerId,
|
||||
componentKey: "bin::sha256:deadbeef",
|
||||
purl: null,
|
||||
name: "app-binary",
|
||||
version: null,
|
||||
type: "binary",
|
||||
metadata: new Dictionary<string, string?>()
|
||||
{
|
||||
["description"] = "Utility binary"
|
||||
},
|
||||
evidence: new[]
|
||||
{
|
||||
new LanguageComponentEvidence(LanguageEvidenceKind.Derived, "entrypoint", "/usr/local/bin/app", "ENTRYPOINT", null)
|
||||
})
|
||||
};
|
||||
|
||||
// Act
|
||||
var layerDigest = LanguageComponentMapper.ComputeLayerDigest(analyzerId);
|
||||
var results = LanguageComponentMapper.ToComponentRecords(analyzerId, records, layerDigest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, results.Length);
|
||||
Assert.All(results, component => Assert.Equal(layerDigest, component.LayerDigest));
|
||||
|
||||
var first = results[0];
|
||||
Assert.Equal("bin::sha256:deadbeef", first.Identity.Key);
|
||||
Assert.Equal("Utility binary", first.Metadata!.Properties!["stellaops.lang.meta.description"]);
|
||||
Assert.Equal("derived", first.Evidence.Single().Kind);
|
||||
|
||||
var second = results[1];
|
||||
Assert.Equal("pkg:npm/example@1.0.0", second.Identity.Key); // prefix removed
|
||||
Assert.True(second.Usage.UsedByEntrypoint);
|
||||
Assert.Contains("MIT", second.Metadata!.Licenses!);
|
||||
Assert.Equal("packages/app", second.Metadata.Properties!["stellaops.lang.meta.path"]);
|
||||
Assert.Equal("abc123", second.Metadata.Properties!["stellaops.lang.evidence.0.sha256"]);
|
||||
Assert.Equal("file", second.Evidence.Single().Kind);
|
||||
Assert.Equal("packages/app/package.json", second.Evidence.Single().Value);
|
||||
Assert.Equal("package.json", second.Evidence.Single().Source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.Determinism;
|
||||
|
||||
public sealed class LanguageAnalyzerHarnessTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HarnessProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("determinism", "basic", "input");
|
||||
var goldenPath = TestPaths.ResolveFixture("determinism", "basic", "expected.json");
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new FakeLanguageAnalyzer(
|
||||
"fake-java",
|
||||
LanguageComponentRecord.FromPurl(
|
||||
analyzerId: "fake-java",
|
||||
purl: "pkg:maven/org.example/example-lib@1.2.3",
|
||||
name: "example-lib",
|
||||
version: "1.2.3",
|
||||
type: "maven",
|
||||
metadata: new Dictionary<string, string?>
|
||||
{
|
||||
["groupId"] = "org.example",
|
||||
["artifactId"] = "example-lib",
|
||||
},
|
||||
evidence: new []
|
||||
{
|
||||
new LanguageComponentEvidence(LanguageEvidenceKind.File, "pom.properties", "META-INF/maven/org.example/example-lib/pom.properties", null, "abc123"),
|
||||
}),
|
||||
LanguageComponentRecord.FromExplicitKey(
|
||||
analyzerId: "fake-java",
|
||||
componentKey: "bin::sha256:deadbeef",
|
||||
purl: null,
|
||||
name: "example-cli",
|
||||
version: null,
|
||||
type: "bin",
|
||||
metadata: new Dictionary<string, string?>
|
||||
{
|
||||
["sha256"] = "deadbeef",
|
||||
},
|
||||
evidence: new []
|
||||
{
|
||||
new LanguageComponentEvidence(LanguageEvidenceKind.File, "binary", "usr/local/bin/example", null, "deadbeef"),
|
||||
})),
|
||||
new FakeLanguageAnalyzer(
|
||||
"fake-node",
|
||||
LanguageComponentRecord.FromPurl(
|
||||
analyzerId: "fake-node",
|
||||
purl: "pkg:npm/example-package@4.5.6",
|
||||
name: "example-package",
|
||||
version: "4.5.6",
|
||||
type: "npm",
|
||||
metadata: new Dictionary<string, string?>
|
||||
{
|
||||
["workspace"] = "packages/example",
|
||||
},
|
||||
evidence: new []
|
||||
{
|
||||
new LanguageComponentEvidence(LanguageEvidenceKind.File, "package.json", "packages/example/package.json", null, null),
|
||||
},
|
||||
usedByEntrypoint: true)),
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(fixturePath, goldenPath, analyzers, cancellationToken);
|
||||
|
||||
var first = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
|
||||
var second = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
|
||||
Assert.Equal(first, second);
|
||||
}
|
||||
|
||||
private sealed class FakeLanguageAnalyzer : ILanguageAnalyzer
|
||||
{
|
||||
private readonly IReadOnlyList<LanguageComponentRecord> _components;
|
||||
|
||||
public FakeLanguageAnalyzer(string id, params LanguageComponentRecord[] components)
|
||||
{
|
||||
Id = id;
|
||||
DisplayName = id;
|
||||
_components = components ?? Array.Empty<LanguageComponentRecord>();
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public string DisplayName { get; }
|
||||
|
||||
public async ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(5, cancellationToken).ConfigureAwait(false); // ensure asynchrony is handled
|
||||
|
||||
// Intentionally add in reverse order to prove determinism.
|
||||
foreach (var component in _components.Reverse())
|
||||
{
|
||||
writer.Add(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "fake-java",
|
||||
"componentKey": "bin::sha256:deadbeef",
|
||||
"name": "example-cli",
|
||||
"type": "bin",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"sha256": "deadbeef"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "binary",
|
||||
"locator": "usr/local/bin/example",
|
||||
"sha256": "deadbeef"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "fake-java",
|
||||
"componentKey": "purl::pkg:maven/org.example/example-lib@1.2.3",
|
||||
"purl": "pkg:maven/org.example/example-lib@1.2.3",
|
||||
"name": "example-lib",
|
||||
"version": "1.2.3",
|
||||
"type": "maven",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"artifactId": "example-lib",
|
||||
"groupId": "org.example"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "pom.properties",
|
||||
"locator": "META-INF/maven/org.example/example-lib/pom.properties",
|
||||
"sha256": "abc123"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "fake-node",
|
||||
"componentKey": "purl::pkg:npm/example-package@4.5.6",
|
||||
"purl": "pkg:npm/example-package@4.5.6",
|
||||
"name": "example-package",
|
||||
"version": "4.5.6",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"workspace": "packages/example"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/example/package.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
sample
|
||||
@@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "java",
|
||||
"componentKey": "purl::pkg:maven/com/example/demo@1.0.0",
|
||||
"purl": "pkg:maven/com/example/demo@1.0.0",
|
||||
"name": "demo",
|
||||
"version": "1.0.0",
|
||||
"type": "maven",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"artifactId": "demo",
|
||||
"displayName": "Demo Library",
|
||||
"groupId": "com.example",
|
||||
"jarPath": "libs/demo.jar",
|
||||
"manifestTitle": "Demo",
|
||||
"manifestVendor": "Example Corp",
|
||||
"manifestVersion": "1.0.0",
|
||||
"packaging": "jar"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "MANIFEST.MF",
|
||||
"locator": "libs/demo.jar!META-INF/MANIFEST.MF",
|
||||
"value": "title=Demo;version=1.0.0;vendor=Example Corp"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "pom.properties",
|
||||
"locator": "libs/demo.jar!META-INF/maven/com.example/demo/pom.properties",
|
||||
"sha256": "c20f36aa1b9d89d28cf9ed131519ffd6287a4dac0c7cb926130496f3f8157bf1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,134 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/left-pad@1.3.0",
|
||||
"purl": "pkg:npm/left-pad@1.3.0",
|
||||
"name": "left-pad",
|
||||
"version": "1.3.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"integrity": "sha512-LEFTPAD",
|
||||
"path": "packages/app/node_modules/left-pad",
|
||||
"resolved": "https://registry.example/left-pad-1.3.0.tgz"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/app/node_modules/left-pad/package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/lib@2.0.1",
|
||||
"purl": "pkg:npm/lib@2.0.1",
|
||||
"name": "lib",
|
||||
"version": "2.0.1",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"integrity": "sha512-LIB",
|
||||
"path": "packages/lib",
|
||||
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
||||
"workspaceLink": "packages/app/node_modules/lib",
|
||||
"workspaceMember": "true",
|
||||
"workspaceRoot": "packages/lib"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/app/node_modules/lib/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/lib/package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/root-workspace@1.0.0",
|
||||
"purl": "pkg:npm/root-workspace@1.0.0",
|
||||
"name": "root-workspace",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"path": ".",
|
||||
"private": "true"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/shared@3.1.4",
|
||||
"purl": "pkg:npm/shared@3.1.4",
|
||||
"name": "shared",
|
||||
"version": "3.1.4",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"integrity": "sha512-SHARED",
|
||||
"path": "packages/shared",
|
||||
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
||||
"workspaceLink": "packages/app/node_modules/shared",
|
||||
"workspaceMember": "true",
|
||||
"workspaceRoot": "packages/shared",
|
||||
"workspaceTargets": "packages/lib"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/app/node_modules/shared/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/shared/package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/workspace-app@1.0.0",
|
||||
"purl": "pkg:npm/workspace-app@1.0.0",
|
||||
"name": "workspace-app",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"installScripts": "true",
|
||||
"path": "packages/app",
|
||||
"policyHint.installLifecycle": "postinstall",
|
||||
"script.postinstall": "node scripts/setup.js",
|
||||
"workspaceMember": "true",
|
||||
"workspaceRoot": "packages/app",
|
||||
"workspaceTargets": "packages/lib;packages/shared"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/app/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:scripts",
|
||||
"locator": "packages/app/package.json#scripts.postinstall",
|
||||
"value": "node scripts/setup.js",
|
||||
"sha256": "f9ae4e4c9313857d1acc31947cee9984232cbefe93c8a56c718804744992728a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
49
src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/node/workspaces/package-lock.json
generated
Normal file
49
src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/node/workspaces/package-lock.json
generated
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "root-workspace",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "root-workspace",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
]
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "workspace-app",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"packages/lib": {
|
||||
"name": "lib",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
||||
"integrity": "sha512-LIB"
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "shared",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
||||
"integrity": "sha512-SHARED"
|
||||
},
|
||||
"packages/app/node_modules/lib": {
|
||||
"name": "lib",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
||||
"integrity": "sha512-LIB"
|
||||
},
|
||||
"packages/app/node_modules/shared": {
|
||||
"name": "shared",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
||||
"integrity": "sha512-SHARED"
|
||||
},
|
||||
"packages/app/node_modules/left-pad": {
|
||||
"name": "left-pad",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.example/left-pad-1.3.0.tgz",
|
||||
"integrity": "sha512-LEFTPAD"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "root-workspace",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/app",
|
||||
"packages/lib",
|
||||
"packages/shared"
|
||||
]
|
||||
}
|
||||
5
src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/node/workspaces/packages/app/node_modules/left-pad/package.json
generated
vendored
Normal file
5
src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/node/workspaces/packages/app/node_modules/left-pad/package.json
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "left-pad",
|
||||
"version": "1.3.0",
|
||||
"main": "index.js"
|
||||
}
|
||||
5
src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/node/workspaces/packages/app/node_modules/lib/package.json
generated
vendored
Normal file
5
src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/node/workspaces/packages/app/node_modules/lib/package.json
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "lib",
|
||||
"version": "2.0.1",
|
||||
"main": "index.js"
|
||||
}
|
||||
5
src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/node/workspaces/packages/app/node_modules/shared/package.json
generated
vendored
Normal file
5
src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/node/workspaces/packages/app/node_modules/shared/package.json
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "shared",
|
||||
"version": "3.1.4",
|
||||
"main": "index.js"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "workspace-app",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"lib": "workspace:../lib",
|
||||
"shared": "workspace:../shared"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node scripts/setup.js"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
console.log('setup');
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "lib",
|
||||
"version": "2.0.1",
|
||||
"dependencies": {
|
||||
"left-pad": "1.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "shared",
|
||||
"version": "3.1.4",
|
||||
"dependencies": {
|
||||
"lib": "workspace:../lib"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
|
||||
public static class LanguageAnalyzerTestHarness
|
||||
{
|
||||
public static async Task<string> RunToJsonAsync(string fixturePath, IEnumerable<ILanguageAnalyzer> analyzers, CancellationToken cancellationToken = default, LanguageUsageHints? usageHints = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fixturePath))
|
||||
{
|
||||
throw new ArgumentException("Fixture path is required", nameof(fixturePath));
|
||||
}
|
||||
|
||||
var engine = new LanguageAnalyzerEngine(analyzers ?? Array.Empty<ILanguageAnalyzer>());
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System, usageHints);
|
||||
var result = await engine.AnalyzeAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
return result.ToJson(indent: true);
|
||||
}
|
||||
|
||||
public static async Task AssertDeterministicAsync(string fixturePath, string goldenPath, IEnumerable<ILanguageAnalyzer> analyzers, CancellationToken cancellationToken = default, LanguageUsageHints? usageHints = null)
|
||||
{
|
||||
var actual = await RunToJsonAsync(fixturePath, analyzers, cancellationToken, usageHints).ConfigureAwait(false);
|
||||
var expected = await File.ReadAllTextAsync(goldenPath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Normalize newlines for portability.
|
||||
actual = NormalizeLineEndings(actual).TrimEnd();
|
||||
expected = NormalizeLineEndings(expected).TrimEnd();
|
||||
|
||||
if (!string.Equals(expected, actual, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = goldenPath + ".actual";
|
||||
var directory = Path.GetDirectoryName(actualPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(actualPath, actual, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
private static string NormalizeLineEndings(string value)
|
||||
=> value.Replace("\r\n", "\n", StringComparison.Ordinal);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.Java;
|
||||
|
||||
public sealed class JavaLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExtractsMavenArtifactFromJarAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = JavaFixtureBuilder.CreateSampleJar(root);
|
||||
var usageHints = new LanguageUsageHints(new[] { jarPath });
|
||||
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
|
||||
var goldenPath = TestPaths.ResolveFixture("java", "basic", "expected.json");
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath: root,
|
||||
goldenPath: goldenPath,
|
||||
analyzers: analyzers,
|
||||
cancellationToken: cancellationToken,
|
||||
usageHints: usageHints);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Node;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.Node;
|
||||
|
||||
public sealed class NodeLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task WorkspaceFixtureProducesDeterministicOutputAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "workspaces");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new NodeLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Remove="xunit" />
|
||||
<PackageReference Remove="xunit.runner.visualstudio" />
|
||||
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Remove="Mongo2Go" />
|
||||
<PackageReference Remove="coverlet.collector" />
|
||||
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
|
||||
<Using Remove="StellaOps.Concelier.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Java\StellaOps.Scanner.Analyzers.Lang.Java.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Node\StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
public static class JavaFixtureBuilder
|
||||
{
|
||||
public static string CreateSampleJar(string rootDirectory, string relativePath = "libs/demo.jar")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rootDirectory);
|
||||
ArgumentException.ThrowIfNullOrEmpty(relativePath);
|
||||
|
||||
var jarPath = Path.Combine(rootDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
|
||||
using var fileStream = new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, leaveOpen: false);
|
||||
|
||||
var timestamp = new DateTimeOffset(2024, 01, 01, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var pomEntry = archive.CreateEntry("META-INF/maven/com.example/demo/pom.properties", CompressionLevel.NoCompression);
|
||||
pomEntry.LastWriteTime = timestamp;
|
||||
using (var writer = new StreamWriter(pomEntry.Open(), Encoding.UTF8, leaveOpen: false))
|
||||
{
|
||||
writer.WriteLine("# Test pom.properties");
|
||||
writer.WriteLine("groupId=com.example");
|
||||
writer.WriteLine("artifactId=demo");
|
||||
writer.WriteLine("version=1.0.0");
|
||||
writer.WriteLine("name=Demo Library");
|
||||
writer.WriteLine("packaging=jar");
|
||||
}
|
||||
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF", CompressionLevel.NoCompression);
|
||||
manifestEntry.LastWriteTime = timestamp;
|
||||
using (var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8, leaveOpen: false))
|
||||
{
|
||||
writer.WriteLine("Manifest-Version: 1.0");
|
||||
writer.WriteLine("Implementation-Title: Demo");
|
||||
writer.WriteLine("Implementation-Version: 1.0.0");
|
||||
writer.WriteLine("Implementation-Vendor: Example Corp");
|
||||
writer.WriteLine();
|
||||
}
|
||||
|
||||
return jarPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
public static class TestPaths
|
||||
{
|
||||
public static string ResolveFixture(params string[] segments)
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var parts = new List<string> { baseDirectory };
|
||||
parts.AddRange(new[] { "Fixtures" });
|
||||
parts.AddRange(segments);
|
||||
return Path.GetFullPath(Path.Combine(parts.ToArray()));
|
||||
}
|
||||
|
||||
public static string CreateTemporaryDirectory()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "tmp", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
public static void SafeDelete(string directory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Swallow cleanup exceptions to avoid masking test failures.
|
||||
}
|
||||
}
|
||||
|
||||
public static string ResolveProjectRoot()
|
||||
{
|
||||
var directory = AppContext.BaseDirectory;
|
||||
while (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory, "StellaOps.Scanner.Analyzers.Lang.Tests.csproj")))
|
||||
{
|
||||
return directory;
|
||||
}
|
||||
|
||||
directory = Path.GetDirectoryName(directory) ?? string.Empty;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to locate project root.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json"
|
||||
}
|
||||
Reference in New Issue
Block a user