Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests/Python/PythonLanguageAnalyzerTests.cs
master 69c59defdc
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat: Implement Runtime Facts ingestion service and NDJSON reader
- 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.
2025-11-10 07:56:15 +02:00

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;
}
}