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