398 lines
14 KiB
C#
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;
|
|
}
|
|
}
|