Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests/Container/PythonZipappAdapterTests.cs
StellaOps Bot 05da719048
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
up
2025-11-28 09:41:08 +02:00

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