up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,172 +1,172 @@
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
public sealed class JavaClassPathBuilderTests
{
[Fact]
public void Build_ClassPathForSimpleJar()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSampleJar(root, "libs/simple.jar");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var analysis = JavaClassPathBuilder.Build(workspace, CancellationToken.None);
var segment = Assert.Single(analysis.Segments);
Assert.Equal("libs/simple.jar", segment.Identifier.Replace('\\', '/'));
Assert.Contains("com.example.Demo", segment.Classes);
var package = Assert.Single(segment.Packages);
Assert.Equal("com.example", package.Key);
Assert.Equal(1, package.Value.ClassCount);
Assert.Empty(analysis.DuplicateClasses);
Assert.Empty(analysis.SplitPackages);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Build_CapturesServiceDefinitions()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var services = new Dictionary<string, string[]>
{
["java.sql.Driver"] = new[] { "com.example.DriverImpl" },
};
CreateJarWithClasses(root, "libs/spi.jar", new[] { "com.example.DriverImpl" }, services);
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var analysis = JavaClassPathBuilder.Build(workspace, CancellationToken.None);
var segment = Assert.Single(analysis.Segments);
var providers = Assert.Single(segment.ServiceDefinitions);
Assert.Equal("java.sql.Driver", providers.Key);
Assert.Contains("com.example.DriverImpl", providers.Value);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Build_FatJarIncludesNestedLibraries()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSpringBootFatJar(root, "apps/app-fat.jar");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var analysis = JavaClassPathBuilder.Build(workspace, CancellationToken.None);
Assert.Equal(2, analysis.Segments.Length);
var classesSegment = analysis.Segments[0];
Assert.Equal("apps/app-fat.jar!BOOT-INF/classes/", classesSegment.Identifier.Replace('\\', '/'));
Assert.Contains("com.example.App", classesSegment.Classes);
var librarySegment = analysis.Segments[1];
Assert.Equal("apps/app-fat.jar!BOOT-INF/lib/library.jar", librarySegment.Identifier.Replace('\\', '/'));
Assert.Contains("com.example.Lib", librarySegment.Classes);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Build_ReportsDuplicateClassesAndSplitPackages()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
CreateJarWithClasses(root, "libs/a.jar", "com.example.Demo");
CreateJarWithClasses(root, "libs/b.jar", "com.example.Demo", "com.example.Other");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var analysis = JavaClassPathBuilder.Build(workspace, CancellationToken.None);
Assert.Equal(2, analysis.Segments.Length);
var duplicate = Assert.Single(analysis.DuplicateClasses);
Assert.Equal("com.example.Demo", duplicate.ClassName);
Assert.Equal(2, duplicate.SegmentIdentifiers.Length);
var split = Assert.Single(analysis.SplitPackages);
Assert.Equal("com.example", split.PackageName);
Assert.Equal(2, split.SegmentIdentifiers.Length);
}
finally
{
TestPaths.SafeDelete(root);
}
}
private static void CreateJarWithClasses(string rootDirectory, string relativePath, params string[] classNames)
=> CreateJarWithClasses(rootDirectory, relativePath, classNames.AsEnumerable(), serviceDefinitions: null);
private static void CreateJarWithClasses(
string rootDirectory,
string relativePath,
IEnumerable<string> classNames,
IDictionary<string, string[]>? serviceDefinitions)
{
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);
foreach (var className in classNames)
{
var entryPath = className.Replace('.', '/') + ".class";
var entry = archive.CreateEntry(entryPath, CompressionLevel.NoCompression);
entry.LastWriteTime = timestamp;
using var writer = new BinaryWriter(entry.Open(), Encoding.UTF8, leaveOpen: false);
writer.Write(new byte[] { 0xCA, 0xFE, 0xBA, 0xBE });
}
if (serviceDefinitions is not null)
{
foreach (var pair in serviceDefinitions)
{
var entryPath = "META-INF/services/" + pair.Key;
var entry = archive.CreateEntry(entryPath, CompressionLevel.NoCompression);
entry.LastWriteTime = timestamp;
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8, leaveOpen: false);
foreach (var provider in pair.Value)
{
writer.WriteLine(provider);
}
}
}
}
}
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
public sealed class JavaClassPathBuilderTests
{
[Fact]
public void Build_ClassPathForSimpleJar()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSampleJar(root, "libs/simple.jar");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var analysis = JavaClassPathBuilder.Build(workspace, CancellationToken.None);
var segment = Assert.Single(analysis.Segments);
Assert.Equal("libs/simple.jar", segment.Identifier.Replace('\\', '/'));
Assert.Contains("com.example.Demo", segment.Classes);
var package = Assert.Single(segment.Packages);
Assert.Equal("com.example", package.Key);
Assert.Equal(1, package.Value.ClassCount);
Assert.Empty(analysis.DuplicateClasses);
Assert.Empty(analysis.SplitPackages);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Build_CapturesServiceDefinitions()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var services = new Dictionary<string, string[]>
{
["java.sql.Driver"] = new[] { "com.example.DriverImpl" },
};
CreateJarWithClasses(root, "libs/spi.jar", new[] { "com.example.DriverImpl" }, services);
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var analysis = JavaClassPathBuilder.Build(workspace, CancellationToken.None);
var segment = Assert.Single(analysis.Segments);
var providers = Assert.Single(segment.ServiceDefinitions);
Assert.Equal("java.sql.Driver", providers.Key);
Assert.Contains("com.example.DriverImpl", providers.Value);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Build_FatJarIncludesNestedLibraries()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSpringBootFatJar(root, "apps/app-fat.jar");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var analysis = JavaClassPathBuilder.Build(workspace, CancellationToken.None);
Assert.Equal(2, analysis.Segments.Length);
var classesSegment = analysis.Segments[0];
Assert.Equal("apps/app-fat.jar!BOOT-INF/classes/", classesSegment.Identifier.Replace('\\', '/'));
Assert.Contains("com.example.App", classesSegment.Classes);
var librarySegment = analysis.Segments[1];
Assert.Equal("apps/app-fat.jar!BOOT-INF/lib/library.jar", librarySegment.Identifier.Replace('\\', '/'));
Assert.Contains("com.example.Lib", librarySegment.Classes);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Build_ReportsDuplicateClassesAndSplitPackages()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
CreateJarWithClasses(root, "libs/a.jar", "com.example.Demo");
CreateJarWithClasses(root, "libs/b.jar", "com.example.Demo", "com.example.Other");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var analysis = JavaClassPathBuilder.Build(workspace, CancellationToken.None);
Assert.Equal(2, analysis.Segments.Length);
var duplicate = Assert.Single(analysis.DuplicateClasses);
Assert.Equal("com.example.Demo", duplicate.ClassName);
Assert.Equal(2, duplicate.SegmentIdentifiers.Length);
var split = Assert.Single(analysis.SplitPackages);
Assert.Equal("com.example", split.PackageName);
Assert.Equal(2, split.SegmentIdentifiers.Length);
}
finally
{
TestPaths.SafeDelete(root);
}
}
private static void CreateJarWithClasses(string rootDirectory, string relativePath, params string[] classNames)
=> CreateJarWithClasses(rootDirectory, relativePath, classNames.AsEnumerable(), serviceDefinitions: null);
private static void CreateJarWithClasses(
string rootDirectory,
string relativePath,
IEnumerable<string> classNames,
IDictionary<string, string[]>? serviceDefinitions)
{
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);
foreach (var className in classNames)
{
var entryPath = className.Replace('.', '/') + ".class";
var entry = archive.CreateEntry(entryPath, CompressionLevel.NoCompression);
entry.LastWriteTime = timestamp;
using var writer = new BinaryWriter(entry.Open(), Encoding.UTF8, leaveOpen: false);
writer.Write(new byte[] { 0xCA, 0xFE, 0xBA, 0xBE });
}
if (serviceDefinitions is not null)
{
foreach (var pair in serviceDefinitions)
{
var entryPath = "META-INF/services/" + pair.Key;
var entry = archive.CreateEntry(entryPath, CompressionLevel.NoCompression);
entry.LastWriteTime = timestamp;
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8, leaveOpen: false);
foreach (var provider in pair.Value)
{
writer.WriteLine(provider);
}
}
}
}
}

View File

@@ -1,481 +1,481 @@
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
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.Java.Tests;
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);
}
}
[Fact]
public async Task LockfilesProduceDeclaredOnlyComponentsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = CreateSampleJar(root, "com.example", "runtime-only", "1.0.0");
var lockPath = Path.Combine(root, "gradle.lockfile");
var lockContent = new StringBuilder()
.AppendLine("com.example:declared-only:2.0.0=runtimeClasspath")
.ToString();
await File.WriteAllTextAsync(lockPath, lockContent, cancellationToken);
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
root,
analyzers,
cancellationToken,
new LanguageUsageHints(new[] { jarPath }));
using var document = JsonDocument.Parse(json);
var rootElement = document.RootElement;
Assert.True(ComponentHasMetadata(rootElement, "declared-only", "declaredOnly", "true"));
Assert.True(ComponentHasMetadata(rootElement, "declared-only", "lockSource", "gradle.lockfile"));
Assert.True(ComponentHasMetadata(rootElement, "runtime-only", "lockMissing", "true"));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task CapturesFrameworkConfigurationHintsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = Path.Combine(root, "demo-framework.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
{
WritePomProperties(archive, "com.example", "demo-framework", "1.0.0");
WriteManifest(archive, "demo-framework", "1.0.0", "com.example");
CreateTextEntry(archive, "META-INF/spring.factories");
CreateTextEntry(archive, "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports");
CreateTextEntry(archive, "META-INF/spring/org.springframework.boot.actuate.autoconfigure.AutoConfiguration.imports");
CreateTextEntry(archive, "BOOT-INF/classes/application.yml");
CreateTextEntry(archive, "WEB-INF/web.xml");
CreateTextEntry(archive, "META-INF/web-fragment.xml");
CreateTextEntry(archive, "META-INF/persistence.xml");
CreateTextEntry(archive, "META-INF/beans.xml");
CreateTextEntry(archive, "META-INF/jaxb.index");
CreateTextEntry(archive, "META-INF/services/jakarta.ws.rs.ext.RuntimeDelegate");
CreateTextEntry(archive, "logback.xml");
CreateTextEntry(archive, "META-INF/native-image/demo/reflect-config.json");
}
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
root,
analyzers,
cancellationToken,
new LanguageUsageHints(new[] { jarPath }));
using var document = JsonDocument.Parse(json);
var component = document.RootElement
.EnumerateArray()
.First(element => string.Equals(element.GetProperty("name").GetString(), "demo-framework", StringComparison.Ordinal));
var metadata = component.GetProperty("metadata");
Assert.Equal("demo-framework.jar!META-INF/spring.factories", metadata.GetProperty("config.spring.factories").GetString());
Assert.Equal(
"demo-framework.jar!META-INF/spring/org.springframework.boot.actuate.autoconfigure.AutoConfiguration.imports,demo-framework.jar!META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
metadata.GetProperty("config.spring.imports").GetString());
Assert.Equal("demo-framework.jar!BOOT-INF/classes/application.yml", metadata.GetProperty("config.spring.properties").GetString());
Assert.Equal("demo-framework.jar!WEB-INF/web.xml", metadata.GetProperty("config.web.xml").GetString());
Assert.Equal("demo-framework.jar!META-INF/web-fragment.xml", metadata.GetProperty("config.web.fragment").GetString());
Assert.Equal("demo-framework.jar!META-INF/persistence.xml", metadata.GetProperty("config.jpa").GetString());
Assert.Equal("demo-framework.jar!META-INF/beans.xml", metadata.GetProperty("config.cdi").GetString());
Assert.Equal("demo-framework.jar!META-INF/jaxb.index", metadata.GetProperty("config.jaxb").GetString());
Assert.Equal("demo-framework.jar!META-INF/services/jakarta.ws.rs.ext.RuntimeDelegate", metadata.GetProperty("config.jaxrs").GetString());
Assert.Equal("demo-framework.jar!logback.xml", metadata.GetProperty("config.logging").GetString());
Assert.Equal("demo-framework.jar!META-INF/native-image/demo/reflect-config.json", metadata.GetProperty("config.graal").GetString());
var evidence = component.GetProperty("evidence").EnumerateArray().ToArray();
Assert.Contains(evidence, e =>
string.Equals(e.GetProperty("source").GetString(), "framework-config", StringComparison.OrdinalIgnoreCase) &&
string.Equals(e.GetProperty("locator").GetString(), "demo-framework.jar!META-INF/spring.factories", StringComparison.OrdinalIgnoreCase) &&
e.TryGetProperty("sha256", out var sha) &&
!string.IsNullOrWhiteSpace(sha.GetString()));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task CapturesJniHintsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = Path.Combine(root, "demo-jni.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
{
WritePomProperties(archive, "com.example", "demo-jni", "1.0.0");
WriteManifest(archive, "demo-jni", "1.0.0", "com.example");
CreateBinaryEntry(archive, "com/example/App.class", "System.loadLibrary(\"foo\")");
CreateTextEntry(archive, "lib/native/libfoo.so");
CreateTextEntry(archive, "META-INF/native-image/demo/jni-config.json");
}
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
root,
analyzers,
cancellationToken,
new LanguageUsageHints(new[] { jarPath }));
using var document = JsonDocument.Parse(json);
var component = document.RootElement
.EnumerateArray()
.First(element => string.Equals(element.GetProperty("name").GetString(), "demo-jni", StringComparison.Ordinal));
var metadata = component.GetProperty("metadata");
Assert.Equal("libfoo.so", metadata.GetProperty("jni.nativeLibs").GetString());
Assert.Equal("demo-jni.jar!META-INF/native-image/demo/jni-config.json", metadata.GetProperty("jni.graalConfig").GetString());
Assert.Equal("demo-jni.jar!com/example/App.class", metadata.GetProperty("jni.loadCalls").GetString());
}
finally
{
TestPaths.SafeDelete(root);
}
}
#region Build File Fixture Integration Tests
[Fact]
public async Task ParsesGradleGroovyBuildFileAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "gradle-groovy");
var goldenPath = TestPaths.ResolveFixture("java", "gradle-groovy", "expected.json");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify key dependencies are detected
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "guava"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "commons-lang3"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "slf4j-api"));
// Verify declaredOnly flag is set for build file dependencies
var guava = components.First(c => c.GetProperty("name").GetString() == "guava");
Assert.True(guava.GetProperty("metadata").TryGetProperty("declaredOnly", out var declaredOnly));
Assert.Equal("true", declaredOnly.GetString());
}
[Fact]
public async Task ParsesGradleKotlinBuildFileAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "gradle-kotlin");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify Kotlin DSL dependencies are detected
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "kotlin-stdlib"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "jackson-databind"));
// Verify kapt/ksp dependencies are detected
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "mapstruct-processor"));
}
[Fact]
public async Task ParsesGradleVersionCatalogAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "gradle-catalog");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "logback-classic"));
var logback = components.First(c => c.GetProperty("name").GetString() == "logback-classic");
Assert.Equal("1.4.14", logback.GetProperty("version").GetString());
}
[Fact]
public async Task ParsesMavenParentPomAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "maven-parent");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify dependencies with inherited versions are detected
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "slf4j-api"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "spring-core"));
// Verify version is inherited from parent
var springCore = components.First(c => c.GetProperty("name").GetString() == "spring-core");
Assert.Equal("6.1.0", springCore.GetProperty("version").GetString());
}
[Fact]
public async Task ParsesMavenBomImportsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "maven-bom");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "commons-lang3"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "lombok"));
var commonsLang = components.First(c => c.GetProperty("name").GetString() == "commons-lang3");
Assert.Equal("3.14.0", commonsLang.GetProperty("version").GetString());
}
[Fact]
public async Task ParsesMavenPropertyPlaceholdersAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "maven-properties");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify property placeholders are resolved
var springCore = components.FirstOrDefault(c => c.GetProperty("name").GetString() == "spring-core");
Assert.NotEqual(JsonValueKind.Undefined, springCore.ValueKind);
Assert.Equal("6.1.0", springCore.GetProperty("version").GetString());
// Verify versionProperty metadata is captured
var metadata = springCore.GetProperty("metadata");
Assert.True(metadata.TryGetProperty("maven.versionProperty", out var versionProp));
Assert.Equal("spring.version", versionProp.GetString());
}
[Fact]
public async Task ParsesMavenScopesAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "maven-scopes");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify different scopes are captured
var guava = components.First(c => c.GetProperty("name").GetString() == "guava");
Assert.Equal("compile", guava.GetProperty("metadata").GetProperty("declaredScope").GetString());
var servletApi = components.First(c => c.GetProperty("name").GetString() == "jakarta.servlet-api");
Assert.Equal("provided", servletApi.GetProperty("metadata").GetProperty("declaredScope").GetString());
var postgresql = components.First(c => c.GetProperty("name").GetString() == "postgresql");
Assert.Equal("runtime", postgresql.GetProperty("metadata").GetProperty("declaredScope").GetString());
var junit = components.First(c => c.GetProperty("name").GetString() == "junit-jupiter");
Assert.Equal("test", junit.GetProperty("metadata").GetProperty("declaredScope").GetString());
// Verify optional flag
var springContext = components.First(c => c.GetProperty("name").GetString() == "spring-context");
Assert.True(springContext.GetProperty("metadata").TryGetProperty("optional", out var optional));
Assert.Equal("true", optional.GetString());
}
[Fact]
public async Task DetectsVersionConflictsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "version-conflict");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify Jackson version conflict is detected
var jacksonDatabind = components.First(c => c.GetProperty("name").GetString() == "jackson-databind");
var metadata = jacksonDatabind.GetProperty("metadata");
if (metadata.TryGetProperty("versionConflict.group", out var conflictGroup))
{
Assert.Equal("com.fasterxml.jackson.core", conflictGroup.GetString());
}
// Verify Spring version conflict is detected
var springCore = components.First(c => c.GetProperty("name").GetString() == "spring-core");
var springMetadata = springCore.GetProperty("metadata");
if (springMetadata.TryGetProperty("versionConflict.group", out var springConflictGroup))
{
Assert.Equal("org.springframework", springConflictGroup.GetString());
}
}
#endregion
private static bool ComponentHasMetadata(JsonElement root, string componentName, string key, string expected)
{
foreach (var element in root.EnumerateArray())
{
if (!element.TryGetProperty("name", out var nameElement) ||
!string.Equals(nameElement.GetString(), componentName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!element.TryGetProperty("metadata", out var metadataElement) || metadataElement.ValueKind != JsonValueKind.Object)
{
continue;
}
if (!metadataElement.TryGetProperty(key, out var valueElement))
{
continue;
}
if (string.Equals(valueElement.GetString(), expected, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static void WritePomProperties(ZipArchive archive, string groupId, string artifactId, string version)
{
var pomPropertiesPath = $"META-INF/maven/{groupId}/{artifactId}/pom.properties";
var pomPropertiesEntry = archive.CreateEntry(pomPropertiesPath);
using var writer = new StreamWriter(pomPropertiesEntry.Open(), Encoding.UTF8);
writer.WriteLine($"groupId={groupId}");
writer.WriteLine($"artifactId={artifactId}");
writer.WriteLine($"version={version}");
writer.WriteLine("packaging=jar");
writer.WriteLine("name=Sample");
}
private static void WriteManifest(ZipArchive archive, string artifactId, string version, string groupId)
{
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
using var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8);
writer.WriteLine("Manifest-Version: 1.0");
writer.WriteLine($"Implementation-Title: {artifactId}");
writer.WriteLine($"Implementation-Version: {version}");
writer.WriteLine($"Implementation-Vendor: {groupId}");
}
private static void CreateTextEntry(ZipArchive archive, string path, string? content = null)
{
var entry = archive.CreateEntry(path);
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
if (!string.IsNullOrEmpty(content))
{
writer.Write(content);
}
}
private static void CreateBinaryEntry(ZipArchive archive, string path, string content)
{
var entry = archive.CreateEntry(path);
using var stream = entry.Open();
var bytes = Encoding.UTF8.GetBytes(content);
stream.Write(bytes, 0, bytes.Length);
}
private static string CreateSampleJar(string root, string groupId, string artifactId, string version)
{
var jarPath = Path.Combine(root, $"{artifactId}-{version}.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create);
var pomPropertiesPath = $"META-INF/maven/{groupId}/{artifactId}/pom.properties";
var pomPropertiesEntry = archive.CreateEntry(pomPropertiesPath);
using (var writer = new StreamWriter(pomPropertiesEntry.Open(), Encoding.UTF8))
{
writer.WriteLine($"groupId={groupId}");
writer.WriteLine($"artifactId={artifactId}");
writer.WriteLine($"version={version}");
writer.WriteLine("packaging=jar");
writer.WriteLine("name=Sample");
}
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
using (var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8))
{
writer.WriteLine("Manifest-Version: 1.0");
writer.WriteLine($"Implementation-Title: {artifactId}");
writer.WriteLine($"Implementation-Version: {version}");
writer.WriteLine($"Implementation-Vendor: {groupId}");
}
var classEntry = archive.CreateEntry($"{artifactId.Replace('-', '_')}/Main.class");
using (var stream = classEntry.Open())
{
stream.Write(new byte[] { 0xCA, 0xFE, 0xBA, 0xBE });
}
return jarPath;
}
}
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
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.Java.Tests;
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);
}
}
[Fact]
public async Task LockfilesProduceDeclaredOnlyComponentsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = CreateSampleJar(root, "com.example", "runtime-only", "1.0.0");
var lockPath = Path.Combine(root, "gradle.lockfile");
var lockContent = new StringBuilder()
.AppendLine("com.example:declared-only:2.0.0=runtimeClasspath")
.ToString();
await File.WriteAllTextAsync(lockPath, lockContent, cancellationToken);
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
root,
analyzers,
cancellationToken,
new LanguageUsageHints(new[] { jarPath }));
using var document = JsonDocument.Parse(json);
var rootElement = document.RootElement;
Assert.True(ComponentHasMetadata(rootElement, "declared-only", "declaredOnly", "true"));
Assert.True(ComponentHasMetadata(rootElement, "declared-only", "lockSource", "gradle.lockfile"));
Assert.True(ComponentHasMetadata(rootElement, "runtime-only", "lockMissing", "true"));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task CapturesFrameworkConfigurationHintsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = Path.Combine(root, "demo-framework.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
{
WritePomProperties(archive, "com.example", "demo-framework", "1.0.0");
WriteManifest(archive, "demo-framework", "1.0.0", "com.example");
CreateTextEntry(archive, "META-INF/spring.factories");
CreateTextEntry(archive, "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports");
CreateTextEntry(archive, "META-INF/spring/org.springframework.boot.actuate.autoconfigure.AutoConfiguration.imports");
CreateTextEntry(archive, "BOOT-INF/classes/application.yml");
CreateTextEntry(archive, "WEB-INF/web.xml");
CreateTextEntry(archive, "META-INF/web-fragment.xml");
CreateTextEntry(archive, "META-INF/persistence.xml");
CreateTextEntry(archive, "META-INF/beans.xml");
CreateTextEntry(archive, "META-INF/jaxb.index");
CreateTextEntry(archive, "META-INF/services/jakarta.ws.rs.ext.RuntimeDelegate");
CreateTextEntry(archive, "logback.xml");
CreateTextEntry(archive, "META-INF/native-image/demo/reflect-config.json");
}
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
root,
analyzers,
cancellationToken,
new LanguageUsageHints(new[] { jarPath }));
using var document = JsonDocument.Parse(json);
var component = document.RootElement
.EnumerateArray()
.First(element => string.Equals(element.GetProperty("name").GetString(), "demo-framework", StringComparison.Ordinal));
var metadata = component.GetProperty("metadata");
Assert.Equal("demo-framework.jar!META-INF/spring.factories", metadata.GetProperty("config.spring.factories").GetString());
Assert.Equal(
"demo-framework.jar!META-INF/spring/org.springframework.boot.actuate.autoconfigure.AutoConfiguration.imports,demo-framework.jar!META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
metadata.GetProperty("config.spring.imports").GetString());
Assert.Equal("demo-framework.jar!BOOT-INF/classes/application.yml", metadata.GetProperty("config.spring.properties").GetString());
Assert.Equal("demo-framework.jar!WEB-INF/web.xml", metadata.GetProperty("config.web.xml").GetString());
Assert.Equal("demo-framework.jar!META-INF/web-fragment.xml", metadata.GetProperty("config.web.fragment").GetString());
Assert.Equal("demo-framework.jar!META-INF/persistence.xml", metadata.GetProperty("config.jpa").GetString());
Assert.Equal("demo-framework.jar!META-INF/beans.xml", metadata.GetProperty("config.cdi").GetString());
Assert.Equal("demo-framework.jar!META-INF/jaxb.index", metadata.GetProperty("config.jaxb").GetString());
Assert.Equal("demo-framework.jar!META-INF/services/jakarta.ws.rs.ext.RuntimeDelegate", metadata.GetProperty("config.jaxrs").GetString());
Assert.Equal("demo-framework.jar!logback.xml", metadata.GetProperty("config.logging").GetString());
Assert.Equal("demo-framework.jar!META-INF/native-image/demo/reflect-config.json", metadata.GetProperty("config.graal").GetString());
var evidence = component.GetProperty("evidence").EnumerateArray().ToArray();
Assert.Contains(evidence, e =>
string.Equals(e.GetProperty("source").GetString(), "framework-config", StringComparison.OrdinalIgnoreCase) &&
string.Equals(e.GetProperty("locator").GetString(), "demo-framework.jar!META-INF/spring.factories", StringComparison.OrdinalIgnoreCase) &&
e.TryGetProperty("sha256", out var sha) &&
!string.IsNullOrWhiteSpace(sha.GetString()));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task CapturesJniHintsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = Path.Combine(root, "demo-jni.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
{
WritePomProperties(archive, "com.example", "demo-jni", "1.0.0");
WriteManifest(archive, "demo-jni", "1.0.0", "com.example");
CreateBinaryEntry(archive, "com/example/App.class", "System.loadLibrary(\"foo\")");
CreateTextEntry(archive, "lib/native/libfoo.so");
CreateTextEntry(archive, "META-INF/native-image/demo/jni-config.json");
}
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
root,
analyzers,
cancellationToken,
new LanguageUsageHints(new[] { jarPath }));
using var document = JsonDocument.Parse(json);
var component = document.RootElement
.EnumerateArray()
.First(element => string.Equals(element.GetProperty("name").GetString(), "demo-jni", StringComparison.Ordinal));
var metadata = component.GetProperty("metadata");
Assert.Equal("libfoo.so", metadata.GetProperty("jni.nativeLibs").GetString());
Assert.Equal("demo-jni.jar!META-INF/native-image/demo/jni-config.json", metadata.GetProperty("jni.graalConfig").GetString());
Assert.Equal("demo-jni.jar!com/example/App.class", metadata.GetProperty("jni.loadCalls").GetString());
}
finally
{
TestPaths.SafeDelete(root);
}
}
#region Build File Fixture Integration Tests
[Fact]
public async Task ParsesGradleGroovyBuildFileAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "gradle-groovy");
var goldenPath = TestPaths.ResolveFixture("java", "gradle-groovy", "expected.json");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify key dependencies are detected
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "guava"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "commons-lang3"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "slf4j-api"));
// Verify declaredOnly flag is set for build file dependencies
var guava = components.First(c => c.GetProperty("name").GetString() == "guava");
Assert.True(guava.GetProperty("metadata").TryGetProperty("declaredOnly", out var declaredOnly));
Assert.Equal("true", declaredOnly.GetString());
}
[Fact]
public async Task ParsesGradleKotlinBuildFileAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "gradle-kotlin");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify Kotlin DSL dependencies are detected
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "kotlin-stdlib"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "jackson-databind"));
// Verify kapt/ksp dependencies are detected
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "mapstruct-processor"));
}
[Fact]
public async Task ParsesGradleVersionCatalogAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "gradle-catalog");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "logback-classic"));
var logback = components.First(c => c.GetProperty("name").GetString() == "logback-classic");
Assert.Equal("1.4.14", logback.GetProperty("version").GetString());
}
[Fact]
public async Task ParsesMavenParentPomAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "maven-parent");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify dependencies with inherited versions are detected
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "slf4j-api"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "spring-core"));
// Verify version is inherited from parent
var springCore = components.First(c => c.GetProperty("name").GetString() == "spring-core");
Assert.Equal("6.1.0", springCore.GetProperty("version").GetString());
}
[Fact]
public async Task ParsesMavenBomImportsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "maven-bom");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "commons-lang3"));
Assert.True(components.Any(c => c.GetProperty("name").GetString() == "lombok"));
var commonsLang = components.First(c => c.GetProperty("name").GetString() == "commons-lang3");
Assert.Equal("3.14.0", commonsLang.GetProperty("version").GetString());
}
[Fact]
public async Task ParsesMavenPropertyPlaceholdersAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "maven-properties");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify property placeholders are resolved
var springCore = components.FirstOrDefault(c => c.GetProperty("name").GetString() == "spring-core");
Assert.NotEqual(JsonValueKind.Undefined, springCore.ValueKind);
Assert.Equal("6.1.0", springCore.GetProperty("version").GetString());
// Verify versionProperty metadata is captured
var metadata = springCore.GetProperty("metadata");
Assert.True(metadata.TryGetProperty("maven.versionProperty", out var versionProp));
Assert.Equal("spring.version", versionProp.GetString());
}
[Fact]
public async Task ParsesMavenScopesAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "maven-scopes");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify different scopes are captured
var guava = components.First(c => c.GetProperty("name").GetString() == "guava");
Assert.Equal("compile", guava.GetProperty("metadata").GetProperty("declaredScope").GetString());
var servletApi = components.First(c => c.GetProperty("name").GetString() == "jakarta.servlet-api");
Assert.Equal("provided", servletApi.GetProperty("metadata").GetProperty("declaredScope").GetString());
var postgresql = components.First(c => c.GetProperty("name").GetString() == "postgresql");
Assert.Equal("runtime", postgresql.GetProperty("metadata").GetProperty("declaredScope").GetString());
var junit = components.First(c => c.GetProperty("name").GetString() == "junit-jupiter");
Assert.Equal("test", junit.GetProperty("metadata").GetProperty("declaredScope").GetString());
// Verify optional flag
var springContext = components.First(c => c.GetProperty("name").GetString() == "spring-context");
Assert.True(springContext.GetProperty("metadata").TryGetProperty("optional", out var optional));
Assert.Equal("true", optional.GetString());
}
[Fact]
public async Task DetectsVersionConflictsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("java", "version-conflict");
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken);
using var document = JsonDocument.Parse(json);
var components = document.RootElement.EnumerateArray().ToArray();
// Verify Jackson version conflict is detected
var jacksonDatabind = components.First(c => c.GetProperty("name").GetString() == "jackson-databind");
var metadata = jacksonDatabind.GetProperty("metadata");
if (metadata.TryGetProperty("versionConflict.group", out var conflictGroup))
{
Assert.Equal("com.fasterxml.jackson.core", conflictGroup.GetString());
}
// Verify Spring version conflict is detected
var springCore = components.First(c => c.GetProperty("name").GetString() == "spring-core");
var springMetadata = springCore.GetProperty("metadata");
if (springMetadata.TryGetProperty("versionConflict.group", out var springConflictGroup))
{
Assert.Equal("org.springframework", springConflictGroup.GetString());
}
}
#endregion
private static bool ComponentHasMetadata(JsonElement root, string componentName, string key, string expected)
{
foreach (var element in root.EnumerateArray())
{
if (!element.TryGetProperty("name", out var nameElement) ||
!string.Equals(nameElement.GetString(), componentName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!element.TryGetProperty("metadata", out var metadataElement) || metadataElement.ValueKind != JsonValueKind.Object)
{
continue;
}
if (!metadataElement.TryGetProperty(key, out var valueElement))
{
continue;
}
if (string.Equals(valueElement.GetString(), expected, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static void WritePomProperties(ZipArchive archive, string groupId, string artifactId, string version)
{
var pomPropertiesPath = $"META-INF/maven/{groupId}/{artifactId}/pom.properties";
var pomPropertiesEntry = archive.CreateEntry(pomPropertiesPath);
using var writer = new StreamWriter(pomPropertiesEntry.Open(), Encoding.UTF8);
writer.WriteLine($"groupId={groupId}");
writer.WriteLine($"artifactId={artifactId}");
writer.WriteLine($"version={version}");
writer.WriteLine("packaging=jar");
writer.WriteLine("name=Sample");
}
private static void WriteManifest(ZipArchive archive, string artifactId, string version, string groupId)
{
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
using var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8);
writer.WriteLine("Manifest-Version: 1.0");
writer.WriteLine($"Implementation-Title: {artifactId}");
writer.WriteLine($"Implementation-Version: {version}");
writer.WriteLine($"Implementation-Vendor: {groupId}");
}
private static void CreateTextEntry(ZipArchive archive, string path, string? content = null)
{
var entry = archive.CreateEntry(path);
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
if (!string.IsNullOrEmpty(content))
{
writer.Write(content);
}
}
private static void CreateBinaryEntry(ZipArchive archive, string path, string content)
{
var entry = archive.CreateEntry(path);
using var stream = entry.Open();
var bytes = Encoding.UTF8.GetBytes(content);
stream.Write(bytes, 0, bytes.Length);
}
private static string CreateSampleJar(string root, string groupId, string artifactId, string version)
{
var jarPath = Path.Combine(root, $"{artifactId}-{version}.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create);
var pomPropertiesPath = $"META-INF/maven/{groupId}/{artifactId}/pom.properties";
var pomPropertiesEntry = archive.CreateEntry(pomPropertiesPath);
using (var writer = new StreamWriter(pomPropertiesEntry.Open(), Encoding.UTF8))
{
writer.WriteLine($"groupId={groupId}");
writer.WriteLine($"artifactId={artifactId}");
writer.WriteLine($"version={version}");
writer.WriteLine("packaging=jar");
writer.WriteLine("name=Sample");
}
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
using (var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8))
{
writer.WriteLine("Manifest-Version: 1.0");
writer.WriteLine($"Implementation-Title: {artifactId}");
writer.WriteLine($"Implementation-Version: {version}");
writer.WriteLine($"Implementation-Vendor: {groupId}");
}
var classEntry = archive.CreateEntry($"{artifactId.Replace('-', '_')}/Main.class");
using (var stream = classEntry.Open())
{
stream.Write(new byte[] { 0xCA, 0xFE, 0xBA, 0xBE });
}
return jarPath;
}
}

View File

@@ -1,82 +1,82 @@
using System.IO.Compression;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Reflection;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
public sealed class JavaReflectionAnalyzerTests
{
[Fact]
public void Analyze_ClassForNameLiteral_ProducesEdge()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = Path.Combine(root, "libs", "reflect.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
{
var entry = archive.CreateEntry("com/example/Reflective.class");
var bytes = JavaClassFileFactory.CreateClassForNameInvoker("com/example/Reflective", "com.example.Plugin");
using var stream = entry.Open();
stream.Write(bytes);
}
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaReflectionAnalyzer.Analyze(classPath, cancellationToken);
var edge = Assert.Single(analysis.Edges);
Assert.Equal("com.example.Reflective", edge.SourceClass);
Assert.Equal("com.example.Plugin", edge.TargetType);
Assert.Equal(JavaReflectionReason.ClassForName, edge.Reason);
Assert.Equal(JavaReflectionConfidence.High, edge.Confidence);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Analyze_TcclUsage_ProducesWarning()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = Path.Combine(root, "libs", "tccl.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
{
var entry = archive.CreateEntry("com/example/Tccl.class");
var bytes = JavaClassFileFactory.CreateTcclChecker("com/example/Tccl");
using var stream = entry.Open();
stream.Write(bytes);
}
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaReflectionAnalyzer.Analyze(classPath, cancellationToken);
Assert.Empty(analysis.Edges);
var warning = Assert.Single(analysis.Warnings);
Assert.Equal("tccl", warning.WarningCode);
Assert.Equal("com.example.Tccl", warning.SourceClass);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
using System.IO.Compression;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Reflection;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
public sealed class JavaReflectionAnalyzerTests
{
[Fact]
public void Analyze_ClassForNameLiteral_ProducesEdge()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = Path.Combine(root, "libs", "reflect.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
{
var entry = archive.CreateEntry("com/example/Reflective.class");
var bytes = JavaClassFileFactory.CreateClassForNameInvoker("com/example/Reflective", "com.example.Plugin");
using var stream = entry.Open();
stream.Write(bytes);
}
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaReflectionAnalyzer.Analyze(classPath, cancellationToken);
var edge = Assert.Single(analysis.Edges);
Assert.Equal("com.example.Reflective", edge.SourceClass);
Assert.Equal("com.example.Plugin", edge.TargetType);
Assert.Equal(JavaReflectionReason.ClassForName, edge.Reason);
Assert.Equal(JavaReflectionConfidence.High, edge.Confidence);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Analyze_TcclUsage_ProducesWarning()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = Path.Combine(root, "libs", "tccl.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
{
var entry = archive.CreateEntry("com/example/Tccl.class");
var bytes = JavaClassFileFactory.CreateTcclChecker("com/example/Tccl");
using var stream = entry.Open();
stream.Write(bytes);
}
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaReflectionAnalyzer.Analyze(classPath, cancellationToken);
Assert.Empty(analysis.Edges);
var warning = Assert.Single(analysis.Warnings);
Assert.Equal("tccl", warning.WarningCode);
Assert.Equal("com.example.Tccl", warning.SourceClass);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Analyze_SpringBootFatJar_ScansEmbeddedAndBootSegments()
{
var root = TestPaths.CreateTemporaryDirectory();

View File

@@ -1,147 +1,147 @@
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
using Xunit;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
public sealed class JavaServiceProviderScannerTests
{
[Fact]
public void Scan_SelectsFirstProviderByClasspathOrder()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var servicesA = new Dictionary<string, string[]>
{
["java.sql.Driver"] = new[] { "com.example.ADriver" },
};
var servicesB = new Dictionary<string, string[]>
{
["java.sql.Driver"] = new[] { "com.example.BDriver" },
};
CreateJarWithClasses(root, "libs/a.jar", new[] { "com.example.ADriver" }, servicesA);
CreateJarWithClasses(root, "libs/b.jar", new[] { "com.example.BDriver" }, servicesB);
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaServiceProviderScanner.Scan(classPath, JavaSpiCatalog.Default, cancellationToken);
var service = Assert.Single(analysis.Services, record => record.ServiceId == "java.sql.Driver");
Assert.Equal("jdk", service.Category);
var selected = Assert.Single(service.Candidates.Where(candidate => candidate.IsSelected));
Assert.Equal("com.example.ADriver", selected.ProviderClass);
Assert.Empty(service.Warnings);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Scan_FlagsDuplicateProviders()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var services = new Dictionary<string, string[]>
{
["java.sql.Driver"] = new[] { "com.example.DuplicateDriver" },
};
CreateJarWithClasses(root, "libs/a.jar", new[] { "com.example.DuplicateDriver" }, services);
CreateJarWithClasses(root, "libs/b.jar", new[] { "com.example.Other" }, services);
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaServiceProviderScanner.Scan(classPath, JavaSpiCatalog.Default, cancellationToken);
var service = Assert.Single(analysis.Services, record => record.ServiceId == "java.sql.Driver");
Assert.NotEmpty(service.Warnings);
Assert.Contains(service.Warnings, warning => warning.Contains("duplicate-provider", StringComparison.OrdinalIgnoreCase));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Scan_RespectsBootFatJarOrdering()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSpringBootFatJar(root, "apps/app-fat.jar");
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaServiceProviderScanner.Scan(classPath, JavaSpiCatalog.Default, cancellationToken);
var service = Assert.Single(analysis.Services, record => record.ServiceId == "java.sql.Driver");
var selected = Assert.Single(service.Candidates.Where(candidate => candidate.IsSelected));
Assert.Equal("com.example.AppDriver", selected.ProviderClass);
Assert.Contains(service.Candidates.Select(candidate => candidate.ProviderClass), provider => provider == "com.example.LibDriver");
}
finally
{
TestPaths.SafeDelete(root);
}
}
private static void CreateJarWithClasses(
string rootDirectory,
string relativePath,
IEnumerable<string> classNames,
IDictionary<string, string[]> serviceDefinitions)
{
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);
foreach (var className in classNames)
{
var entryPath = className.Replace('.', '/') + ".class";
var entry = archive.CreateEntry(entryPath, CompressionLevel.NoCompression);
entry.LastWriteTime = timestamp;
using var writer = new BinaryWriter(entry.Open(), Encoding.UTF8, leaveOpen: false);
writer.Write(new byte[] { 0xCA, 0xFE, 0xBA, 0xBE });
}
foreach (var pair in serviceDefinitions)
{
var entryPath = "META-INF/services/" + pair.Key;
var entry = archive.CreateEntry(entryPath, CompressionLevel.NoCompression);
entry.LastWriteTime = timestamp;
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8, leaveOpen: false);
foreach (var provider in pair.Value)
{
writer.WriteLine(provider);
}
}
}
}
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
using Xunit;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
public sealed class JavaServiceProviderScannerTests
{
[Fact]
public void Scan_SelectsFirstProviderByClasspathOrder()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var servicesA = new Dictionary<string, string[]>
{
["java.sql.Driver"] = new[] { "com.example.ADriver" },
};
var servicesB = new Dictionary<string, string[]>
{
["java.sql.Driver"] = new[] { "com.example.BDriver" },
};
CreateJarWithClasses(root, "libs/a.jar", new[] { "com.example.ADriver" }, servicesA);
CreateJarWithClasses(root, "libs/b.jar", new[] { "com.example.BDriver" }, servicesB);
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaServiceProviderScanner.Scan(classPath, JavaSpiCatalog.Default, cancellationToken);
var service = Assert.Single(analysis.Services, record => record.ServiceId == "java.sql.Driver");
Assert.Equal("jdk", service.Category);
var selected = Assert.Single(service.Candidates.Where(candidate => candidate.IsSelected));
Assert.Equal("com.example.ADriver", selected.ProviderClass);
Assert.Empty(service.Warnings);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Scan_FlagsDuplicateProviders()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var services = new Dictionary<string, string[]>
{
["java.sql.Driver"] = new[] { "com.example.DuplicateDriver" },
};
CreateJarWithClasses(root, "libs/a.jar", new[] { "com.example.DuplicateDriver" }, services);
CreateJarWithClasses(root, "libs/b.jar", new[] { "com.example.Other" }, services);
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaServiceProviderScanner.Scan(classPath, JavaSpiCatalog.Default, cancellationToken);
var service = Assert.Single(analysis.Services, record => record.ServiceId == "java.sql.Driver");
Assert.NotEmpty(service.Warnings);
Assert.Contains(service.Warnings, warning => warning.Contains("duplicate-provider", StringComparison.OrdinalIgnoreCase));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Scan_RespectsBootFatJarOrdering()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSpringBootFatJar(root, "apps/app-fat.jar");
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaServiceProviderScanner.Scan(classPath, JavaSpiCatalog.Default, cancellationToken);
var service = Assert.Single(analysis.Services, record => record.ServiceId == "java.sql.Driver");
var selected = Assert.Single(service.Candidates.Where(candidate => candidate.IsSelected));
Assert.Equal("com.example.AppDriver", selected.ProviderClass);
Assert.Contains(service.Candidates.Select(candidate => candidate.ProviderClass), provider => provider == "com.example.LibDriver");
}
finally
{
TestPaths.SafeDelete(root);
}
}
private static void CreateJarWithClasses(
string rootDirectory,
string relativePath,
IEnumerable<string> classNames,
IDictionary<string, string[]> serviceDefinitions)
{
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);
foreach (var className in classNames)
{
var entryPath = className.Replace('.', '/') + ".class";
var entry = archive.CreateEntry(entryPath, CompressionLevel.NoCompression);
entry.LastWriteTime = timestamp;
using var writer = new BinaryWriter(entry.Open(), Encoding.UTF8, leaveOpen: false);
writer.Write(new byte[] { 0xCA, 0xFE, 0xBA, 0xBE });
}
foreach (var pair in serviceDefinitions)
{
var entryPath = "META-INF/services/" + pair.Key;
var entry = archive.CreateEntry(entryPath, CompressionLevel.NoCompression);
entry.LastWriteTime = timestamp;
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8, leaveOpen: false);
foreach (var provider in pair.Value)
{
writer.WriteLine(provider);
}
}
}
}

View File

@@ -1,93 +1,93 @@
using System.Linq;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
public sealed class JavaWorkspaceNormalizerTests
{
[Fact]
public void Normalize_ClassifiesPackagingAndLayers()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSampleJar(root, "libs/simple.jar");
JavaFixtureBuilder.CreateSpringBootFatJar(root, "libs/app-fat.jar");
JavaFixtureBuilder.CreateWarArchive(root, "apps/sample.war");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var archivesByPath = workspace.Archives.ToDictionary(
archive => archive.RelativePath.Replace('\\', '/'),
archive => archive,
StringComparer.Ordinal);
var simpleJar = Assert.Contains("libs/simple.jar", archivesByPath);
Assert.Equal(JavaPackagingKind.Jar, simpleJar.Packaging);
Assert.Empty(simpleJar.LayeredDirectories);
var fatJar = Assert.Contains("libs/app-fat.jar", archivesByPath);
Assert.Equal(JavaPackagingKind.SpringBootFatJar, fatJar.Packaging);
Assert.Contains("BOOT-INF", fatJar.LayeredDirectories);
var war = Assert.Contains("apps/sample.war", archivesByPath);
Assert.Equal(JavaPackagingKind.War, war.Packaging);
Assert.Contains("WEB-INF", war.LayeredDirectories);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Normalize_SelectsMultiReleaseOverlay()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateMultiReleaseJar(root, "libs/mr.jar");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var archive = Assert.Single(workspace.Archives);
Assert.True(archive.IsMultiRelease);
Assert.False(archive.HasModuleInfo);
Assert.True(archive.TryGetEntry("com/example/App.class", out var entry));
Assert.Equal(11, entry.Version);
Assert.Equal("META-INF/versions/11/com/example/App.class", entry.OriginalPath.Replace('\\', '/'));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Normalize_DetectsRuntimeImageMetadata()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateRuntimeImage(root, "runtime/jre");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var runtime = Assert.Single(workspace.RuntimeImages);
Assert.Equal("17.0.8", runtime.JavaVersion);
Assert.Equal("Eclipse Adoptium", runtime.Vendor);
Assert.Equal("runtime/jre", runtime.RelativePath.Replace('\\', '/'));
}
finally
{
TestPaths.SafeDelete(root);
}
}
}
using System.Linq;
using System.Threading;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
public sealed class JavaWorkspaceNormalizerTests
{
[Fact]
public void Normalize_ClassifiesPackagingAndLayers()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSampleJar(root, "libs/simple.jar");
JavaFixtureBuilder.CreateSpringBootFatJar(root, "libs/app-fat.jar");
JavaFixtureBuilder.CreateWarArchive(root, "apps/sample.war");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var archivesByPath = workspace.Archives.ToDictionary(
archive => archive.RelativePath.Replace('\\', '/'),
archive => archive,
StringComparer.Ordinal);
var simpleJar = Assert.Contains("libs/simple.jar", archivesByPath);
Assert.Equal(JavaPackagingKind.Jar, simpleJar.Packaging);
Assert.Empty(simpleJar.LayeredDirectories);
var fatJar = Assert.Contains("libs/app-fat.jar", archivesByPath);
Assert.Equal(JavaPackagingKind.SpringBootFatJar, fatJar.Packaging);
Assert.Contains("BOOT-INF", fatJar.LayeredDirectories);
var war = Assert.Contains("apps/sample.war", archivesByPath);
Assert.Equal(JavaPackagingKind.War, war.Packaging);
Assert.Contains("WEB-INF", war.LayeredDirectories);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Normalize_SelectsMultiReleaseOverlay()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateMultiReleaseJar(root, "libs/mr.jar");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var archive = Assert.Single(workspace.Archives);
Assert.True(archive.IsMultiRelease);
Assert.False(archive.HasModuleInfo);
Assert.True(archive.TryGetEntry("com/example/App.class", out var entry));
Assert.Equal(11, entry.Version);
Assert.Equal("META-INF/versions/11/com/example/App.class", entry.OriginalPath.Replace('\\', '/'));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Normalize_DetectsRuntimeImageMetadata()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateRuntimeImage(root, "runtime/jre");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, CancellationToken.None);
var runtime = Assert.Single(workspace.RuntimeImages);
Assert.Equal("17.0.8", runtime.JavaVersion);
Assert.Equal("Eclipse Adoptium", runtime.Vendor);
Assert.Equal("runtime/jre", runtime.RelativePath.Replace('\\', '/'));
}
finally
{
TestPaths.SafeDelete(root);
}
}
}