Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
339 lines
11 KiB
C#
339 lines
11 KiB
C#
using System.IO.Compression;
|
|
using System.Text;
|
|
using StellaOps.Scanner.Analyzers.Lang.Python.Internal;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.Container;
|
|
|
|
public sealed class PythonZipappAdapterTests : IDisposable
|
|
{
|
|
private readonly string _tempDir;
|
|
|
|
public PythonZipappAdapterTests()
|
|
{
|
|
_tempDir = Path.Combine(Path.GetTempPath(), $"zipapp-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_tempDir);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(_tempDir, recursive: true);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DiscoverZipapps_FindsPyzFiles()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "app.pyz");
|
|
CreateMinimalZipapp(pyzPath, "#!/usr/bin/env python3\n");
|
|
|
|
// Act
|
|
var discovered = PythonZipappAdapter.DiscoverZipapps(_tempDir);
|
|
|
|
// Assert
|
|
Assert.Single(discovered);
|
|
Assert.Contains(discovered, p => p.EndsWith("app.pyz"));
|
|
}
|
|
|
|
[Fact]
|
|
public void DiscoverZipapps_FindsPyzwFiles()
|
|
{
|
|
// Arrange
|
|
var pyzwPath = Path.Combine(_tempDir, "app.pyzw");
|
|
CreateMinimalZipapp(pyzwPath, "#!/usr/bin/env pythonw\n");
|
|
|
|
// Act
|
|
var discovered = PythonZipappAdapter.DiscoverZipapps(_tempDir);
|
|
|
|
// Assert
|
|
Assert.Single(discovered);
|
|
Assert.Contains(discovered, p => p.EndsWith("app.pyzw"));
|
|
}
|
|
|
|
[Fact]
|
|
public void DiscoverZipapps_FindsInContainerLayers()
|
|
{
|
|
// Arrange
|
|
var layersDir = Path.Combine(_tempDir, "layers", "layer1", "fs", "app");
|
|
Directory.CreateDirectory(layersDir);
|
|
var pyzPath = Path.Combine(layersDir, "container-app.pyz");
|
|
CreateMinimalZipapp(pyzPath, "#!/usr/bin/python3.11\n");
|
|
|
|
// Act
|
|
var discovered = PythonZipappAdapter.DiscoverZipapps(_tempDir);
|
|
|
|
// Assert
|
|
Assert.Single(discovered);
|
|
Assert.Contains(discovered, p => p.EndsWith("container-app.pyz"));
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_ExtractsShebang()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "app.pyz");
|
|
CreateMinimalZipapp(pyzPath, "#!/usr/bin/python3.11\n");
|
|
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzPath);
|
|
|
|
// Assert
|
|
Assert.NotNull(info);
|
|
Assert.Equal("/usr/bin/python3.11", info.Shebang);
|
|
Assert.Equal("3.11", info.PythonVersion);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_ExtractsEnvShebang()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "app.pyz");
|
|
CreateMinimalZipapp(pyzPath, "#!/usr/bin/env python3.10\n");
|
|
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzPath);
|
|
|
|
// Assert
|
|
Assert.NotNull(info);
|
|
Assert.Contains("/usr/bin/env python3.10", info.Shebang);
|
|
Assert.Equal("3.10", info.PythonVersion);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_DetectsMainPy()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "app.pyz");
|
|
CreateZipappWithMain(pyzPath, "#!/usr/bin/python3\n", "print('Hello')");
|
|
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzPath);
|
|
|
|
// Assert
|
|
Assert.NotNull(info);
|
|
Assert.True(info.HasMainPy);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_DetectsMissingMain_GeneratesWarning()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "app.pyz");
|
|
CreateMinimalZipapp(pyzPath, "#!/usr/bin/python3\n");
|
|
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzPath);
|
|
|
|
// Assert
|
|
Assert.NotNull(info);
|
|
Assert.False(info.HasMainPy);
|
|
Assert.Contains(info.Warnings, w => w.Contains("missing __main__.py"));
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_DetectsWindowsApp()
|
|
{
|
|
// Arrange
|
|
var pyzwPath = Path.Combine(_tempDir, "app.pyzw");
|
|
CreateZipappWithMain(pyzwPath, "#!/usr/bin/pythonw\n", "print('Hello')");
|
|
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzwPath);
|
|
|
|
// Assert
|
|
Assert.NotNull(info);
|
|
Assert.True(info.IsWindowsApp);
|
|
Assert.Contains(info.Warnings, w => w.Contains("Windows-specific"));
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_DetectsEnvShebangWarning()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "app.pyz");
|
|
CreateZipappWithMain(pyzPath, "#!/usr/bin/env python3\n", "print('Hello')");
|
|
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzPath);
|
|
|
|
// Assert
|
|
Assert.NotNull(info);
|
|
Assert.Contains(info.Warnings, w => w.Contains("/usr/bin/env") && w.Contains("may vary"));
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_ExtractsEmbeddedRequirements()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "app.pyz");
|
|
var requirements = "requests>=2.0\nflask==2.1.0\n# Comment\nnumpy";
|
|
CreateZipappWithRequirements(pyzPath, "#!/usr/bin/python3\n", requirements);
|
|
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzPath);
|
|
|
|
// Assert
|
|
Assert.NotNull(info);
|
|
Assert.Contains("requests", info.EmbeddedDependencies);
|
|
Assert.Contains("flask", info.EmbeddedDependencies);
|
|
Assert.Contains("numpy", info.EmbeddedDependencies);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeAll_ReturnsAnalysisForMultipleZipapps()
|
|
{
|
|
// Arrange
|
|
var pyz1 = Path.Combine(_tempDir, "app1.pyz");
|
|
var pyz2 = Path.Combine(_tempDir, "app2.pyz");
|
|
CreateZipappWithMain(pyz1, "#!/usr/bin/python3.10\n", "print('App1')");
|
|
CreateZipappWithMain(pyz2, "#!/usr/bin/python3.11\n", "print('App2')");
|
|
|
|
// Act
|
|
var analysis = PythonZipappAdapter.AnalyzeAll(_tempDir);
|
|
|
|
// Assert
|
|
Assert.Equal(2, analysis.Zipapps.Count);
|
|
Assert.True(analysis.HasZipapps);
|
|
Assert.Contains(analysis.Warnings, w => w.Contains("Multiple zipapps"));
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeAll_CollectsVersionsFromShebangs()
|
|
{
|
|
// Arrange
|
|
var pyz1 = Path.Combine(_tempDir, "app1.pyz");
|
|
var pyz2 = Path.Combine(_tempDir, "app2.pyz");
|
|
CreateZipappWithMain(pyz1, "#!/usr/bin/python3.10\n", "print('App1')");
|
|
CreateZipappWithMain(pyz2, "#!/usr/bin/python3.11\n", "print('App2')");
|
|
|
|
// Act
|
|
var analysis = PythonZipappAdapter.AnalyzeAll(_tempDir);
|
|
|
|
// Assert
|
|
var versioned = analysis.Zipapps.Where(z => z.PythonVersion != null).ToList();
|
|
Assert.Equal(2, versioned.Count);
|
|
Assert.Contains(versioned, z => z.PythonVersion == "3.10");
|
|
Assert.Contains(versioned, z => z.PythonVersion == "3.11");
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_ExtractsEntryModuleFromRunpy()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "app.pyz");
|
|
var mainContent = @"
|
|
import runpy
|
|
runpy.run_module('mypackage.main')
|
|
";
|
|
CreateZipappWithMain(pyzPath, "#!/usr/bin/python3\n", mainContent);
|
|
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzPath);
|
|
|
|
// Assert
|
|
Assert.NotNull(info);
|
|
Assert.Equal("mypackage.main", info.EntryModule);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToMetadata_GeneratesExpectedKeys()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "app.pyz");
|
|
CreateZipappWithMain(pyzPath, "#!/usr/bin/python3.11\n", "print('Hello')");
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzPath);
|
|
|
|
// Act
|
|
var metadata = info!.ToMetadata();
|
|
|
|
// Assert
|
|
Assert.Contains(metadata, m => m.Key == "zipapp.path");
|
|
Assert.Contains(metadata, m => m.Key == "zipapp.hasMain" && m.Value == "true");
|
|
Assert.Contains(metadata, m => m.Key == "zipapp.shebang");
|
|
Assert.Contains(metadata, m => m.Key == "zipapp.pythonVersion" && m.Value == "3.11");
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_ReturnsNull_ForNonExistentFile()
|
|
{
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(Path.Combine(_tempDir, "nonexistent.pyz"));
|
|
|
|
// Assert
|
|
Assert.Null(info);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeZipapp_HandlesCorruptedArchive()
|
|
{
|
|
// Arrange
|
|
var pyzPath = Path.Combine(_tempDir, "corrupt.pyz");
|
|
File.WriteAllText(pyzPath, "#!/usr/bin/python3\nNot a valid zip archive");
|
|
|
|
// Act
|
|
var info = PythonZipappAdapter.AnalyzeZipapp(pyzPath);
|
|
|
|
// Assert - should return null for corrupted archives
|
|
Assert.Null(info);
|
|
}
|
|
|
|
private static void CreateMinimalZipapp(string path, string shebang)
|
|
{
|
|
using var fileStream = File.Create(path);
|
|
// Write shebang
|
|
var shebangBytes = Encoding.UTF8.GetBytes(shebang);
|
|
fileStream.Write(shebangBytes);
|
|
|
|
// Write minimal zip archive
|
|
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, leaveOpen: true);
|
|
var entry = archive.CreateEntry("placeholder.txt");
|
|
using var entryStream = entry.Open();
|
|
using var writer = new StreamWriter(entryStream);
|
|
writer.Write("placeholder");
|
|
}
|
|
|
|
private static void CreateZipappWithMain(string path, string shebang, string mainContent)
|
|
{
|
|
using var fileStream = File.Create(path);
|
|
// Write shebang
|
|
var shebangBytes = Encoding.UTF8.GetBytes(shebang);
|
|
fileStream.Write(shebangBytes);
|
|
|
|
// Write zip archive with __main__.py
|
|
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, leaveOpen: true);
|
|
var entry = archive.CreateEntry("__main__.py");
|
|
using var entryStream = entry.Open();
|
|
using var writer = new StreamWriter(entryStream);
|
|
writer.Write(mainContent);
|
|
}
|
|
|
|
private static void CreateZipappWithRequirements(string path, string shebang, string requirements)
|
|
{
|
|
using var fileStream = File.Create(path);
|
|
// Write shebang
|
|
var shebangBytes = Encoding.UTF8.GetBytes(shebang);
|
|
fileStream.Write(shebangBytes);
|
|
|
|
// Write zip archive with __main__.py and requirements.txt
|
|
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, leaveOpen: true);
|
|
|
|
var mainEntry = archive.CreateEntry("__main__.py");
|
|
using (var mainStream = mainEntry.Open())
|
|
using (var mainWriter = new StreamWriter(mainStream))
|
|
{
|
|
mainWriter.Write("print('Hello')");
|
|
}
|
|
|
|
var reqEntry = archive.CreateEntry("requirements.txt");
|
|
using var reqStream = reqEntry.Open();
|
|
using var reqWriter = new StreamWriter(reqStream);
|
|
reqWriter.Write(requirements);
|
|
}
|
|
}
|