feat(python-analyzer): Enhance deterministic output tests and add new fixtures

- Updated TASKS.md to reflect changes in test fixtures for SCAN-PY-405-007.
- Added multiple test cases to ensure deterministic output for various Python package scenarios, including conda environments, requirements files, and vendored directories.
- Created new expected output files for conda packages (numpy, requests) and updated existing test fixtures for container whiteouts, wheel workspaces, and zipapp embedded requirements.
- Introduced helper methods to create wheel and zipapp packages for testing purposes.
- Added metadata files for new test fixtures to validate package detection and dependencies.
This commit is contained in:
StellaOps Bot
2025-12-21 17:51:19 +02:00
parent 22d67f203f
commit 292a6e94e8
29 changed files with 1043 additions and 25 deletions

View File

@@ -85,6 +85,148 @@ public sealed class PythonLanguageAnalyzerTests
usageHints);
}
[Fact]
public async Task CondaEnvFixtureProducesDeterministicOutputAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "python", "conda-env");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new PythonLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task RequirementsIncludesEditableFixtureProducesDeterministicOutputAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "python", "requirements-includes-editable");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new PythonLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task PipfileLockDefaultDevelopFixtureProducesDeterministicOutputAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "python", "pipfile-lock-default-develop");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new PythonLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task WheelWorkspaceFixtureProducesDeterministicOutputAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "python", "wheel-workspace");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var distDir = Path.Combine(fixturePath, "dist");
Directory.CreateDirectory(distDir);
var wheelPath = Path.Combine(distDir, "wheelpkg-1.0.0-py3-none-any.whl");
CreateWheelpkgWheel(wheelPath);
var analyzers = new ILanguageAnalyzer[]
{
new PythonLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task ZipappEmbeddedRequirementsFixtureProducesDeterministicOutputAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "python", "zipapp-embedded-requirements");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var zipappPath = Path.Combine(fixturePath, "myapp.pyz");
CreateZipappWithEmbeddedRequirements(zipappPath);
var analyzers = new ILanguageAnalyzer[]
{
new PythonLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task ContainerWhiteoutsFixtureProducesDeterministicOutputAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "python", "container-whiteouts");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new PythonLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task VendoredDirectoryFixtureProducesDeterministicOutputAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "python", "vendored-directory");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new PythonLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task LockfileCollectorEmitsDeclaredOnlyComponentsAsync()
{
@@ -580,6 +722,77 @@ public sealed class PythonLanguageAnalyzerTests
return path;
}
private static void CreateWheelpkgWheel(string wheelPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(wheelPath)!);
var initBytes = Encoding.UTF8.GetBytes("__version__ = \"1.0.0\"\n");
var metadataBytes = Encoding.UTF8.GetBytes(
$"Metadata-Version: 2.1\nName: wheelpkg\nVersion: 1.0.0\nSummary: Wheel fixture\n{Environment.NewLine}");
var wheelBytes = Encoding.UTF8.GetBytes(
"Wheel-Version: 1.0\nGenerator: stellaops-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n");
var recordContent = new StringBuilder()
.AppendLine($"wheelpkg/__init__.py,sha256={ComputeSha256Base64(initBytes)},{initBytes.Length}")
.AppendLine($"wheelpkg-1.0.0.dist-info/METADATA,sha256={ComputeSha256Base64(metadataBytes)},{metadataBytes.Length}")
.AppendLine($"wheelpkg-1.0.0.dist-info/WHEEL,sha256={ComputeSha256Base64(wheelBytes)},{wheelBytes.Length}")
.AppendLine("wheelpkg-1.0.0.dist-info/RECORD,,")
.ToString();
var recordBytes = Encoding.UTF8.GetBytes(recordContent);
if (File.Exists(wheelPath))
{
File.Delete(wheelPath);
}
using (var stream = File.Create(wheelPath))
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: false))
{
WriteEntry(archive, "wheelpkg/__init__.py", initBytes);
WriteEntry(archive, "wheelpkg-1.0.0.dist-info/METADATA", metadataBytes);
WriteEntry(archive, "wheelpkg-1.0.0.dist-info/WHEEL", wheelBytes);
WriteEntry(archive, "wheelpkg-1.0.0.dist-info/RECORD", recordBytes);
}
static void WriteEntry(ZipArchive archive, string entryName, byte[] content)
{
var entry = archive.CreateEntry(entryName);
entry.LastWriteTime = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero);
using var entryStream = entry.Open();
entryStream.Write(content, 0, content.Length);
}
static string ComputeSha256Base64(byte[] content)
=> Convert.ToBase64String(SHA256.HashData(content));
}
private static void CreateZipappWithEmbeddedRequirements(string zipappPath)
{
if (File.Exists(zipappPath))
{
File.Delete(zipappPath);
}
using var fileStream = File.Create(zipappPath);
var shebangBytes = Encoding.UTF8.GetBytes("#!/usr/bin/python3.11\n");
fileStream.Write(shebangBytes);
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, leaveOpen: true);
WriteTextEntry(archive, "__main__.py", "print('hello')\n");
WriteTextEntry(archive, "requirements.txt", "requests==2.28.0\nflask==2.1.0\n");
static void WriteTextEntry(ZipArchive archive, string name, string content)
{
var entry = archive.CreateEntry(name, CompressionLevel.NoCompression);
entry.LastWriteTime = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero);
using var stream = entry.Open();
using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
writer.Write(content);
}
}
// ===== SCAN-PY-405-007 Fixtures =====
[Fact]