Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests/Resolver/PythonModuleResolverTests.cs

398 lines
14 KiB
C#

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