up
This commit is contained in:
@@ -4,32 +4,32 @@ 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]
|
||||
|
||||
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,
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -109,7 +109,7 @@ public sealed class PythonLanguageAnalyzerTests
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
@@ -199,6 +199,221 @@ public sealed class PythonLanguageAnalyzerTests
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsSitecustomizeStartupHooksAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create site-packages with sitecustomize.py
|
||||
var sitePackages = Path.Combine(fixturePath, "lib", "python3.11", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var sitecustomizePath = Path.Combine(sitePackages, "sitecustomize.py");
|
||||
await File.WriteAllTextAsync(sitecustomizePath, "# Site customization\nprint('startup hook')", cancellationToken);
|
||||
|
||||
// Create a package
|
||||
await CreatePythonPackageAsync(fixturePath, "test-pkg", "1.0.0", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify startup hooks metadata is present
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "startupHooks.detected", "true"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsPthFilesWithImportDirectivesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(fixturePath, "lib", "python3.11", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
// Create a .pth file with import directive
|
||||
var pthPath = Path.Combine(sitePackages, "test-hooks.pth");
|
||||
await File.WriteAllTextAsync(pthPath, "import some_module\n/some/path", cancellationToken);
|
||||
|
||||
// Create a package
|
||||
await CreatePythonPackageAsync(fixturePath, "test-pkg", "1.0.0", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify .pth import warning metadata is present
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "pthFiles.withImports.detected", "true"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsOciLayerSitePackagesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create OCI layer structure with packages
|
||||
var layersDir = Path.Combine(fixturePath, "layers", "layer1", "fs");
|
||||
var sitePackages = Path.Combine(layersDir, "usr", "lib", "python3.11", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
// Create a package in the layer
|
||||
var packageDir = Path.Combine(sitePackages, "layered-pkg");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
|
||||
var modulePath = Path.Combine(packageDir, "__init__.py");
|
||||
await File.WriteAllTextAsync(modulePath, "__version__ = \"1.0.0\"", cancellationToken);
|
||||
|
||||
var distInfoDir = Path.Combine(sitePackages, "layered-pkg-1.0.0.dist-info");
|
||||
Directory.CreateDirectory(distInfoDir);
|
||||
|
||||
var metadataPath = Path.Combine(distInfoDir, "METADATA");
|
||||
await File.WriteAllTextAsync(metadataPath, "Metadata-Version: 2.1\nName: layered-pkg\nVersion: 1.0.0", cancellationToken);
|
||||
|
||||
var wheelPath = Path.Combine(distInfoDir, "WHEEL");
|
||||
await File.WriteAllTextAsync(wheelPath, "Wheel-Version: 1.0", cancellationToken);
|
||||
|
||||
var recordPath = Path.Combine(distInfoDir, "RECORD");
|
||||
await File.WriteAllTextAsync(recordPath, "", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify the package from OCI layers was discovered
|
||||
var found = false;
|
||||
foreach (var component in root.EnumerateArray())
|
||||
{
|
||||
if (component.TryGetProperty("name", out var nameElement) &&
|
||||
string.Equals(nameElement.GetString(), "layered-pkg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(found, "Package from OCI layer should be discovered");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsPythonEnvironmentVariablesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create environment file with PYTHONPATH
|
||||
var envPath = Path.Combine(fixturePath, ".env");
|
||||
await File.WriteAllTextAsync(envPath, "PYTHONPATH=/app/lib:/app/vendor", cancellationToken);
|
||||
|
||||
// Create a package
|
||||
await CreatePythonPackageAsync(fixturePath, "test-pkg", "1.0.0", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify PYTHONPATH warning metadata is present
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "env.pythonpath", "/app/lib:/app/vendor"));
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "env.pythonpath.warning", "PYTHONPATH is set; may affect module resolution"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsPyvenvConfigAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create pyvenv.cfg file
|
||||
var pyvenvPath = Path.Combine(fixturePath, "pyvenv.cfg");
|
||||
await File.WriteAllTextAsync(pyvenvPath, "home = /usr/local/bin\ninclude-system-site-packages = false", cancellationToken);
|
||||
|
||||
// Create a package
|
||||
await CreatePythonPackageAsync(fixturePath, "test-pkg", "1.0.0", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify PYTHONHOME warning metadata is present (from pyvenv.cfg home)
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "env.pythonhome", "/usr/local/bin"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateTemporaryWorkspace()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-python-{Guid.NewGuid():N}");
|
||||
|
||||
Reference in New Issue
Block a user