using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Resolver; using StellaOps.Scanner.Analyzers.Lang.Python.Internal.VirtualFileSystem; namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.Resolver; public sealed class PythonModuleResolverTests { [Fact] public async Task Resolve_BuiltinModule_ReturnsBuiltin() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { await File.WriteAllTextAsync(Path.Combine(tempPath, "dummy.py"), "", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSourceTree(tempPath) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); var result = resolver.Resolve("sys"); Assert.True(result.IsResolved); Assert.Equal(PythonResolutionKind.BuiltinModule, result.Kind); Assert.Equal(PythonResolutionConfidence.Definitive, result.Confidence); } finally { Directory.Delete(tempPath, recursive: true); } } [Fact] public async Task Resolve_SourceModule_FindsModule() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { await File.WriteAllTextAsync(Path.Combine(tempPath, "mymodule.py"), "# my module", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSourceTree(tempPath) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); var result = resolver.Resolve("mymodule"); Assert.True(result.IsResolved); Assert.Equal(PythonResolutionKind.SourceModule, result.Kind); Assert.Equal("mymodule.py", result.VirtualPath); } finally { Directory.Delete(tempPath, recursive: true); } } [Fact] public async Task Resolve_Package_FindsPackage() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { var pkgDir = Path.Combine(tempPath, "mypackage"); Directory.CreateDirectory(pkgDir); await File.WriteAllTextAsync(Path.Combine(pkgDir, "__init__.py"), "", cancellationToken); await File.WriteAllTextAsync(Path.Combine(pkgDir, "module.py"), "", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSourceTree(tempPath) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); var result = resolver.Resolve("mypackage"); Assert.True(result.IsResolved); Assert.Equal(PythonResolutionKind.Package, result.Kind); Assert.True(result.IsPackage); } finally { Directory.Delete(tempPath, recursive: true); } } [Fact] public async Task Resolve_SubModule_FindsSubModule() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { var pkgDir = Path.Combine(tempPath, "mypackage"); Directory.CreateDirectory(pkgDir); await File.WriteAllTextAsync(Path.Combine(pkgDir, "__init__.py"), "", cancellationToken); await File.WriteAllTextAsync(Path.Combine(pkgDir, "submodule.py"), "", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSourceTree(tempPath) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); var result = resolver.Resolve("mypackage.submodule"); Assert.True(result.IsResolved); Assert.Equal(PythonResolutionKind.SourceModule, result.Kind); Assert.Equal("mypackage/submodule.py", result.VirtualPath); } finally { Directory.Delete(tempPath, recursive: true); } } [Fact] public async Task Resolve_NamespacePackage_FindsNamespacePackage() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { // Create a namespace package (directory without __init__.py) var pkgDir = Path.Combine(tempPath, "namespace_pkg"); Directory.CreateDirectory(pkgDir); // Add a submodule to make the directory discoverable await File.WriteAllTextAsync(Path.Combine(pkgDir, "submodule.py"), "", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSourceTree(tempPath) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); var result = resolver.Resolve("namespace_pkg"); Assert.True(result.IsResolved); Assert.Equal(PythonResolutionKind.NamespacePackage, result.Kind); Assert.True(result.IsNamespacePackage); } finally { Directory.Delete(tempPath, recursive: true); } } [Fact] public async Task Resolve_NotFound_ReturnsNotFound() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { await File.WriteAllTextAsync(Path.Combine(tempPath, "dummy.py"), "", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSourceTree(tempPath) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); var result = resolver.Resolve("nonexistent_module"); Assert.False(result.IsResolved); Assert.Equal(PythonResolutionKind.NotFound, result.Kind); } finally { Directory.Delete(tempPath, recursive: true); } } [Fact] public async Task ResolveRelative_Level1_ResolvesFromPackage() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { var pkgDir = Path.Combine(tempPath, "mypackage"); Directory.CreateDirectory(pkgDir); await File.WriteAllTextAsync(Path.Combine(pkgDir, "__init__.py"), "", cancellationToken); await File.WriteAllTextAsync(Path.Combine(pkgDir, "module1.py"), "", cancellationToken); await File.WriteAllTextAsync(Path.Combine(pkgDir, "module2.py"), "", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSourceTree(tempPath) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); // from . import module2 (inside module1.py) var result = resolver.ResolveRelative("module2", 1, "mypackage.module1"); Assert.True(result.IsResolved); Assert.Equal("mypackage/module2.py", result.VirtualPath); } finally { Directory.Delete(tempPath, recursive: true); } } [Fact] public async Task ResolveRelative_Level2_ResolvesFromParentPackage() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { // Create nested package structure var pkgDir = Path.Combine(tempPath, "mypackage"); var subDir = Path.Combine(pkgDir, "subpackage"); Directory.CreateDirectory(subDir); await File.WriteAllTextAsync(Path.Combine(pkgDir, "__init__.py"), "", cancellationToken); await File.WriteAllTextAsync(Path.Combine(pkgDir, "utils.py"), "", cancellationToken); await File.WriteAllTextAsync(Path.Combine(subDir, "__init__.py"), "", cancellationToken); await File.WriteAllTextAsync(Path.Combine(subDir, "module.py"), "", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSourceTree(tempPath) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); // from ..utils import something (inside subpackage/module.py) var result = resolver.ResolveRelative("utils", 2, "mypackage.subpackage.module"); Assert.True(result.IsResolved); Assert.Equal("mypackage/utils.py", result.VirtualPath); } finally { Directory.Delete(tempPath, recursive: true); } } [Fact] public async Task ProcessPthFiles_AddsPaths() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { var sitePackages = Path.Combine(tempPath, "site-packages"); Directory.CreateDirectory(sitePackages); // Create a .pth file var pthContent = @" # This is a comment ./extra_path "; await File.WriteAllTextAsync( Path.Combine(sitePackages, "extra.pth"), pthContent, cancellationToken); // Create the extra path with a module var extraPath = Path.Combine(sitePackages, "extra_path"); Directory.CreateDirectory(extraPath); await File.WriteAllTextAsync(Path.Combine(extraPath, "extra_module.py"), "", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSitePackages(sitePackages) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); // Check that the path was added Assert.Contains(resolver.SearchPaths, p => p.Kind == PythonSearchPathKind.PthFile); } finally { Directory.Delete(tempPath, recursive: true); } } [Fact] public void IsStandardLibraryModule_ReturnsTrue_ForStdlib() { Assert.True(PythonModuleResolver.IsStandardLibraryModule("os")); Assert.True(PythonModuleResolver.IsStandardLibraryModule("os.path")); Assert.True(PythonModuleResolver.IsStandardLibraryModule("sys")); Assert.True(PythonModuleResolver.IsStandardLibraryModule("json")); Assert.True(PythonModuleResolver.IsStandardLibraryModule("collections")); Assert.True(PythonModuleResolver.IsStandardLibraryModule("collections.abc")); } [Fact] public void IsStandardLibraryModule_ReturnsFalse_ForThirdParty() { Assert.False(PythonModuleResolver.IsStandardLibraryModule("requests")); Assert.False(PythonModuleResolver.IsStandardLibraryModule("numpy")); Assert.False(PythonModuleResolver.IsStandardLibraryModule("flask")); Assert.False(PythonModuleResolver.IsStandardLibraryModule("django")); } [Fact] public void IsBuiltinModule_ReturnsTrue_ForBuiltins() { Assert.True(PythonModuleResolver.IsBuiltinModule("sys")); Assert.True(PythonModuleResolver.IsBuiltinModule("builtins")); Assert.True(PythonModuleResolver.IsBuiltinModule("_thread")); } [Fact] public void PythonModuleResolution_ParentModule_ReturnsCorrectly() { var resolution = new PythonModuleResolution( ModuleName: "mypackage.subpackage.module", Kind: PythonResolutionKind.SourceModule, VirtualPath: "mypackage/subpackage/module.py", AbsolutePath: null, SearchPath: "", Source: PythonFileSource.SourceTree, Confidence: PythonResolutionConfidence.Definitive); Assert.Equal("mypackage.subpackage", resolution.ParentModule); Assert.Equal("module", resolution.SimpleName); } [Fact] public void PythonModuleResolution_ToMetadata_GeneratesExpectedKeys() { var resolution = new PythonModuleResolution( ModuleName: "mymodule", Kind: PythonResolutionKind.SourceModule, VirtualPath: "mymodule.py", AbsolutePath: "/path/to/mymodule.py", SearchPath: "/path/to", Source: PythonFileSource.SourceTree, Confidence: PythonResolutionConfidence.Definitive); var metadata = resolution.ToMetadata("resolution").ToDictionary(kv => kv.Key, kv => kv.Value); Assert.Equal("mymodule", metadata["resolution.module"]); Assert.Equal("SourceModule", metadata["resolution.kind"]); Assert.Equal("Definitive", metadata["resolution.confidence"]); Assert.Equal("mymodule.py", metadata["resolution.path"]); } [Fact] public async Task Resolver_CachesResults() { var cancellationToken = CancellationToken.None; var tempPath = CreateTemporaryWorkspace(); try { await File.WriteAllTextAsync(Path.Combine(tempPath, "mymodule.py"), "", cancellationToken); var vfs = PythonVirtualFileSystem.CreateBuilder() .AddSourceTree(tempPath) .Build(); var resolver = new PythonModuleResolver(vfs, tempPath); await resolver.InitializeAsync(cancellationToken); var result1 = resolver.Resolve("mymodule"); var result2 = resolver.Resolve("mymodule"); Assert.Same(result1, result2); var (total, resolved, notFound, cached) = resolver.GetStatistics(); Assert.Equal(1, total); Assert.Equal(1, resolved); Assert.Equal(0, notFound); } finally { Directory.Delete(tempPath, recursive: true); } } private static string CreateTemporaryWorkspace() { var path = Path.Combine(Path.GetTempPath(), $"stellaops-resolver-{Guid.NewGuid():N}"); Directory.CreateDirectory(path); return path; } }