Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added RuntimeFactsNdjsonReader for reading NDJSON formatted runtime facts. - Introduced IRuntimeFactsIngestionService interface and its implementation. - Enhanced Program.cs to register new services and endpoints for runtime facts. - Updated CallgraphIngestionService to include CAS URI in stored artifacts. - Created RuntimeFactsValidationException for validation errors during ingestion. - Added tests for RuntimeFactsIngestionService and RuntimeFactsNdjsonReader. - Implemented SignalsSealedModeMonitor for compliance checks in sealed mode. - Updated project dependencies for testing utilities.
209 lines
7.9 KiB
C#
209 lines
7.9 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using StellaOps.Scanner.Analyzers.Lang.Python;
|
|
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
|
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests;
|
|
|
|
public sealed class PythonLanguageAnalyzerTests
|
|
{
|
|
[Fact]
|
|
public async Task SimpleVenvFixtureProducesDeterministicOutputAsync()
|
|
{
|
|
var cancellationToken = TestContext.Current.CancellationToken;
|
|
var fixturePath = TestPaths.ResolveFixture("lang", "python", "simple-venv");
|
|
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
|
|
|
var usageHints = new LanguageUsageHints(new[]
|
|
{
|
|
Path.Combine(fixturePath, "bin", "simple-tool")
|
|
});
|
|
|
|
var analyzers = new ILanguageAnalyzer[]
|
|
{
|
|
new PythonLanguageAnalyzer()
|
|
};
|
|
|
|
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
|
fixturePath,
|
|
goldenPath,
|
|
analyzers,
|
|
cancellationToken,
|
|
usageHints);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PipCacheFixtureProducesDeterministicOutputAsync()
|
|
{
|
|
var cancellationToken = TestContext.Current.CancellationToken;
|
|
var fixturePath = TestPaths.ResolveFixture("lang", "python", "pip-cache");
|
|
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
|
|
|
var usageHints = new LanguageUsageHints(new[]
|
|
{
|
|
Path.Combine(fixturePath, "lib", "python3.11", "site-packages", "cache_pkg-1.2.3.data", "scripts", "cache-tool")
|
|
});
|
|
|
|
var analyzers = new ILanguageAnalyzer[]
|
|
{
|
|
new PythonLanguageAnalyzer()
|
|
};
|
|
|
|
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
|
fixturePath,
|
|
goldenPath,
|
|
analyzers,
|
|
cancellationToken,
|
|
usageHints);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LayeredEditableFixtureMergesAcrossLayersAsync()
|
|
{
|
|
var cancellationToken = TestContext.Current.CancellationToken;
|
|
var fixturePath = TestPaths.ResolveFixture("lang", "python", "layered-editable");
|
|
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
|
|
|
var usageHints = new LanguageUsageHints(new[]
|
|
{
|
|
Path.Combine(fixturePath, "layer1", "usr", "bin", "layered-cli")
|
|
});
|
|
|
|
var analyzers = new ILanguageAnalyzer[]
|
|
{
|
|
new PythonLanguageAnalyzer()
|
|
};
|
|
|
|
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
|
fixturePath,
|
|
goldenPath,
|
|
analyzers,
|
|
cancellationToken,
|
|
usageHints);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LockfileCollectorEmitsDeclaredOnlyComponentsAsync()
|
|
{
|
|
var cancellationToken = TestContext.Current.CancellationToken;
|
|
var fixturePath = CreateTemporaryWorkspace();
|
|
try
|
|
{
|
|
await CreatePythonPackageAsync(fixturePath, "locked", "1.0.0", cancellationToken);
|
|
await CreatePythonPackageAsync(fixturePath, "runtime-only", "2.0.0", cancellationToken);
|
|
|
|
var requirementsPath = Path.Combine(fixturePath, "requirements.txt");
|
|
var requirements = new StringBuilder()
|
|
.AppendLine("locked==1.0.0")
|
|
.AppendLine("declared-only==3.0.0")
|
|
.ToString();
|
|
await File.WriteAllTextAsync(requirementsPath, requirements, cancellationToken);
|
|
|
|
var analyzers = new ILanguageAnalyzer[]
|
|
{
|
|
new PythonLanguageAnalyzer()
|
|
};
|
|
|
|
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
|
fixturePath,
|
|
analyzers,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
using var document = JsonDocument.Parse(json);
|
|
var root = document.RootElement;
|
|
|
|
Assert.True(ComponentHasMetadata(root, "declared-only", "declaredOnly", "true"));
|
|
Assert.True(ComponentHasMetadata(root, "declared-only", "lockSource", "requirements.txt"));
|
|
Assert.True(ComponentHasMetadata(root, "locked", "lockSource", "requirements.txt"));
|
|
Assert.True(ComponentHasMetadata(root, "runtime-only", "lockMissing", "true"));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(fixturePath, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static async Task CreatePythonPackageAsync(string root, string name, string version, CancellationToken cancellationToken)
|
|
{
|
|
var sitePackages = Path.Combine(root, "lib", "python3.11", "site-packages");
|
|
Directory.CreateDirectory(sitePackages);
|
|
|
|
var packageDir = Path.Combine(sitePackages, name);
|
|
Directory.CreateDirectory(packageDir);
|
|
|
|
var modulePath = Path.Combine(packageDir, "__init__.py");
|
|
var moduleContent = $"__version__ = \"{version}\"{Environment.NewLine}";
|
|
await File.WriteAllTextAsync(modulePath, moduleContent, cancellationToken);
|
|
|
|
var distInfoDir = Path.Combine(sitePackages, $"{name}-{version}.dist-info");
|
|
Directory.CreateDirectory(distInfoDir);
|
|
|
|
var metadataPath = Path.Combine(distInfoDir, "METADATA");
|
|
var metadataContent = $"Metadata-Version: 2.1{Environment.NewLine}Name: {name}{Environment.NewLine}Version: {version}{Environment.NewLine}";
|
|
await File.WriteAllTextAsync(metadataPath, metadataContent, cancellationToken);
|
|
|
|
var wheelPath = Path.Combine(distInfoDir, "WHEEL");
|
|
await File.WriteAllTextAsync(wheelPath, "Wheel-Version: 1.0", cancellationToken);
|
|
|
|
var entryPointsPath = Path.Combine(distInfoDir, "entry_points.txt");
|
|
await File.WriteAllTextAsync(entryPointsPath, string.Empty, cancellationToken);
|
|
|
|
var recordPath = Path.Combine(distInfoDir, "RECORD");
|
|
var recordContent = new StringBuilder()
|
|
.AppendLine($"{name}/__init__.py,sha256={ComputeSha256Base64(modulePath)},{new FileInfo(modulePath).Length}")
|
|
.AppendLine($"{name}-{version}.dist-info/METADATA,sha256={ComputeSha256Base64(metadataPath)},{new FileInfo(metadataPath).Length}")
|
|
.AppendLine($"{name}-{version}.dist-info/RECORD,,")
|
|
.AppendLine($"{name}-{version}.dist-info/WHEEL,sha256={ComputeSha256Base64(wheelPath)},{new FileInfo(wheelPath).Length}")
|
|
.AppendLine($"{name}-{version}.dist-info/entry_points.txt,sha256={ComputeSha256Base64(entryPointsPath)},{new FileInfo(entryPointsPath).Length}")
|
|
.ToString();
|
|
await File.WriteAllTextAsync(recordPath, recordContent, cancellationToken);
|
|
}
|
|
|
|
private static bool ComponentHasMetadata(JsonElement root, string componentName, string key, string? expectedValue)
|
|
{
|
|
foreach (var component in root.EnumerateArray())
|
|
{
|
|
if (!component.TryGetProperty("name", out var nameElement) ||
|
|
!string.Equals(nameElement.GetString(), componentName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!component.TryGetProperty("metadata", out var metadataElement) || metadataElement.ValueKind != JsonValueKind.Object)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!metadataElement.TryGetProperty(key, out var valueElement))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var actual = valueElement.GetString();
|
|
if (string.Equals(actual, expectedValue, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string ComputeSha256Base64(string path)
|
|
{
|
|
using var sha = SHA256.Create();
|
|
using var stream = File.OpenRead(path);
|
|
var hash = sha.ComputeHash(stream);
|
|
return Convert.ToBase64String(hash);
|
|
}
|
|
|
|
private static string CreateTemporaryWorkspace()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"stellaops-python-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(path);
|
|
return path;
|
|
}
|
|
}
|