diff --git a/VSharp.API/VSharp.cs b/VSharp.API/VSharp.cs index 9bf3ddc68..37f403837 100644 --- a/VSharp.API/VSharp.cs +++ b/VSharp.API/VSharp.cs @@ -92,7 +92,7 @@ private static Statistics StartExploration(List methods, string resu void HandleInternalFail(MethodBase method, Exception exception) { - Console.WriteLine($"EXCEPTION | {method.DeclaringType}.{method.Name} | {exception.GetType().Name} {exception.Message}"); + Logger.printLogString(Logger.Error, $"Internal exception | {method.DeclaringType}.{method.Name} | {exception.GetType().Name} {exception.Message}"); } foreach (var method in methods) @@ -172,18 +172,52 @@ public static Statistics Cover(Type type, int timeout = -1, string outputDirecto public static Statistics Cover(Assembly assembly, int timeout = -1, string outputDirectory = "") { AssemblyManager.Load(assembly); - List methods; BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly; - methods = new List(); - foreach (var t in assembly.GetTypes()) + var methods = new List(); + var types = new List(); + + try + { + types.AddRange(assembly.GetTypes()); + } + catch (ReflectionTypeLoadException e) + { + foreach (var loaderException in e.LoaderExceptions) + { + Logger.printLogString(Logger.Error, $"Cannot load type: {loaderException?.Message}"); + } + + foreach (var type in e.Types) + { + if (type is not null) + { + types.Add(type); + } + } + } + + foreach (var t in types) { - if (t.IsPublic) + if (t.IsPublic || t.IsNestedPublic) { foreach (var m in t.GetMethods(bindingFlags)) { - if (m.GetMethodBody() != null) + global::System.Reflection.MethodBody methodBody = null; + var hasByRefLikes = true; + + try + { + methodBody = m.GetMethodBody(); + hasByRefLikes = Reflection.hasByRefLikes(m); + } + catch (Exception e) + { + Logger.printLogString(Logger.Error, $"Cannot get method info: {e.Message}"); + } + + if (methodBody is not null && !hasByRefLikes) methods.Add(m); } } diff --git a/VSharp.CSharpUtils/AssemblyResolving/AssemblyResolverUtils.cs b/VSharp.CSharpUtils/AssemblyResolving/AssemblyResolverUtils.cs new file mode 100644 index 000000000..dc8de0181 --- /dev/null +++ b/VSharp.CSharpUtils/AssemblyResolving/AssemblyResolverUtils.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace VSharp.CSharpUtils.AssemblyResolving +{ + public static class AssemblyResolverUtils + { + public static string FindAssemblyWithName(DirectoryInfo directory, AssemblyName targetName) + { + bool IsTargetAssembly(FileInfo assemblyFile) + { + try + { + var foundName = AssemblyName.GetAssemblyName(assemblyFile.FullName); + return foundName.Name == targetName.Name && + foundName.Version == targetName.Version && + foundName.ContentType == targetName.ContentType; + } + catch (Exception) + { + return false; + } + } + + var found = directory.EnumerateFiles("*.dll").FirstOrDefault(IsTargetAssembly); + + if (found is not null) + { + return found.FullName; + } + + foreach (var subDir in directory.EnumerateDirectories()) + { + var foundInSubDir = FindAssemblyWithName(subDir, targetName); + if (foundInSubDir is not null) + { + return foundInSubDir; + } + } + + return null; + } + + public static string GetBaseNuGetDirectory() + { + var baseDirectory = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); + + if (!string.IsNullOrEmpty(baseDirectory)) + { + return baseDirectory; + } + + if (OperatingSystem.IsWindows()) + { + baseDirectory = Environment.GetEnvironmentVariable("USERPROFILE"); + } + else + { + baseDirectory = Environment.GetEnvironmentVariable("HOME"); + } + + if (string.IsNullOrEmpty(baseDirectory)) + { + return null; + } + + return Path.Combine(baseDirectory, ".nuget", "packages"); + } + } +} diff --git a/VSharp.CSharpUtils/AssemblyResolving/CompositeAssemblyResolver.cs b/VSharp.CSharpUtils/AssemblyResolving/CompositeAssemblyResolver.cs new file mode 100644 index 000000000..d2b4cbbe6 --- /dev/null +++ b/VSharp.CSharpUtils/AssemblyResolving/CompositeAssemblyResolver.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace VSharp.CSharpUtils.AssemblyResolving +{ + public class CompositeAssemblyResolver : IAssemblyResolver + { + private readonly List _resolvers = new(); + + public CompositeAssemblyResolver(params IAssemblyResolver[] resolvers) + { + _resolvers.AddRange(resolvers); + } + + public string Resolve(AssemblyName assemblyName) + { + foreach (var resolver in _resolvers) + { + var resolved = resolver.Resolve(assemblyName); + if (resolved is not null) + { + return resolved; + } + } + + return null; + } + } +} diff --git a/VSharp.CSharpUtils/AssemblyResolving/CurrentDirectoryAssemblyResolver.cs b/VSharp.CSharpUtils/AssemblyResolving/CurrentDirectoryAssemblyResolver.cs new file mode 100644 index 000000000..cb700b679 --- /dev/null +++ b/VSharp.CSharpUtils/AssemblyResolving/CurrentDirectoryAssemblyResolver.cs @@ -0,0 +1,27 @@ +using System.IO; +using System.Reflection; + +namespace VSharp.CSharpUtils.AssemblyResolving +{ + public class CurrentDirectoryAssemblyResolver : IAssemblyResolver + { + private readonly string _directoryPath; + + public CurrentDirectoryAssemblyResolver(string directoryPath) + { + _directoryPath = directoryPath; + } + + public string Resolve(AssemblyName assemblyName) + { + var dllPath = Path.Combine(_directoryPath, $"{assemblyName.Name}.dll"); + + if (File.Exists(dllPath)) + { + return dllPath; + } + + return null; + } + } +} diff --git a/VSharp.CSharpUtils/AssemblyResolving/IAssemblyResolver.cs b/VSharp.CSharpUtils/AssemblyResolving/IAssemblyResolver.cs new file mode 100644 index 000000000..3a533ea1f --- /dev/null +++ b/VSharp.CSharpUtils/AssemblyResolving/IAssemblyResolver.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace VSharp.CSharpUtils.AssemblyResolving +{ + public interface IAssemblyResolver + { + string Resolve(AssemblyName assemblyName); + } +} diff --git a/VSharp.CSharpUtils/AssemblyResolving/MicrosoftDependencyModelAssemblyResolver.cs b/VSharp.CSharpUtils/AssemblyResolving/MicrosoftDependencyModelAssemblyResolver.cs new file mode 100644 index 000000000..b146ad468 --- /dev/null +++ b/VSharp.CSharpUtils/AssemblyResolving/MicrosoftDependencyModelAssemblyResolver.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.DependencyModel.Resolution; + +namespace VSharp.CSharpUtils.AssemblyResolving +{ + public class MicrosoftDependencyModelAssemblyResolver : IAssemblyResolver + { + private readonly List _resolvers = new(); + private readonly DependencyContext _context; + + public MicrosoftDependencyModelAssemblyResolver(DependencyContext context, params ICompilationAssemblyResolver[] resolvers) + { + _resolvers.AddRange(resolvers); + _context = context; + } + + public string Resolve(AssemblyName assemblyName) + { + if (_context is null) + { + return null; + } + + var compilationLibrary = _context.CompileLibraries.FirstOrDefault(l => + l.Name.Equals(assemblyName.Name, StringComparison.OrdinalIgnoreCase)); + + if (compilationLibrary is null) + { + var runtimeLibrary = _context.RuntimeLibraries.FirstOrDefault(l => + l.Name.Equals(assemblyName.Name, StringComparison.OrdinalIgnoreCase)); + + if (runtimeLibrary is not null) + { + compilationLibrary = new CompilationLibrary( + runtimeLibrary.Type, + runtimeLibrary.Name, + runtimeLibrary.Version, + runtimeLibrary.Hash, + runtimeLibrary.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths), + runtimeLibrary.Dependencies, + runtimeLibrary.Serviceable + ); + } + } + + if (compilationLibrary is null) + { + return null; + } + + var assemblies = new List(); + + foreach (ICompilationAssemblyResolver resolver in _resolvers) + { + try + { + if (resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies)) + { + return assemblies[0]; + } + } + catch { } + } + + return null; + } + } +} diff --git a/VSharp.CSharpUtils/AssemblyResolving/NuGetGraphAssemblyResolver.cs b/VSharp.CSharpUtils/AssemblyResolving/NuGetGraphAssemblyResolver.cs new file mode 100644 index 000000000..b876c2148 --- /dev/null +++ b/VSharp.CSharpUtils/AssemblyResolving/NuGetGraphAssemblyResolver.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using NuGet.Packaging; +using NuGet.Versioning; + +namespace VSharp.CSharpUtils.AssemblyResolving +{ + public class NuGetGraphAssemblyResolver : IAssemblyResolver + { + private readonly HashSet<(string path, string pkgName)> _discovered = new(); + private readonly HashSet _withResolvedDependencies = new(); + private readonly bool _fallbackToAllPackages; + + private readonly string _baseNuGetDirectory = AssemblyResolverUtils.GetBaseNuGetDirectory(); + + public NuGetGraphAssemblyResolver(Assembly assembly, bool fallbackToAllPackages = true) + { + _fallbackToAllPackages = fallbackToAllPackages; + + var dllDirectory = new DirectoryInfo(Path.GetDirectoryName(assembly.Location)); + var currentDir = dllDirectory; + + do + { + var nupkg = currentDir.EnumerateFiles("*.nupkg").FirstOrDefault(); + if (nupkg is not null) + { + _discovered.Add((currentDir.FullName, nupkg.Name)); + break; + } + currentDir = currentDir.Parent; + + } while (currentDir.FullName != dllDirectory.Root.FullName); + } + + private IEnumerable GetDirectories() + { + if (_baseNuGetDirectory is null || _discovered.Count == 0) + { + yield break; + } + + var queue = new Queue<(string, string)>(_discovered); + + while (queue.Count > 0) + { + var (path, name) = queue.Dequeue(); + + if (!_withResolvedDependencies.Contains(name)) + { + var deps = GetDependencies(path, name); + + foreach (var dep in deps) + { + queue.Enqueue(dep); + } + } + + yield return path; + } + + if (_fallbackToAllPackages) + { + foreach (var packageDir in Directory.EnumerateDirectories(_baseNuGetDirectory)) + { + foreach (var versionDir in Directory.EnumerateDirectories(packageDir)) + { + if (_discovered.All(p => p.path != versionDir)) + { + yield return versionDir; + } + } + } + } + } + + public string Resolve(AssemblyName assemblyName) + { + foreach (var dir in GetDirectories()) + { + var found = AssemblyResolverUtils.FindAssemblyWithName(new DirectoryInfo(dir), assemblyName); + if (found is not null) + { + return found; + } + } + + return null; + } + + private HashSet<(string, string)> GetDependencies(string path, string name) + { + using FileStream inputStream = new FileStream(Path.Combine(path, name), FileMode.Open); + using PackageArchiveReader reader = new PackageArchiveReader(inputStream); + NuspecReader nuspec = reader.NuspecReader; + var toReturn = new HashSet<(string, string)>(); + + foreach (var dependencyGroup in nuspec.GetDependencyGroups()) + { + foreach (var dependency in dependencyGroup.Packages) + { + var basePackagePath = Path.Combine(_baseNuGetDirectory, dependency.Id.ToLower()); + + if (!Directory.Exists(basePackagePath)) + { + continue; + } + + var availableVersions = Directory.EnumerateDirectories(basePackagePath) + .Select(Path.GetFileName); + + foreach (var version in availableVersions) + { + if (dependency.VersionRange.Satisfies(new NuGetVersion(version))) + { + var newEntry = (Path.Combine(basePackagePath, version), GetPackageFileName(dependency.Id.ToLower(), version)); + + if (!_discovered.Contains(newEntry)) + { + _discovered.Add(newEntry); + toReturn.Add(newEntry); + } + + break; + } + } + } + } + + _withResolvedDependencies.Add(name); + return toReturn; + } + + private static string GetPackageFileName(string id, string version) => + $"{id}.{version}.nupkg"; + } +} diff --git a/VSharp.CSharpUtils/AssemblyResolving/NuGetPackageNameMatchAssemblyResolver.cs b/VSharp.CSharpUtils/AssemblyResolving/NuGetPackageNameMatchAssemblyResolver.cs new file mode 100644 index 000000000..c68a69a0c --- /dev/null +++ b/VSharp.CSharpUtils/AssemblyResolving/NuGetPackageNameMatchAssemblyResolver.cs @@ -0,0 +1,23 @@ +using System.IO; +using System.Reflection; + +namespace VSharp.CSharpUtils.AssemblyResolving +{ + public class NuGetPackageNameMatchAssemblyResolver : IAssemblyResolver + { + private readonly string _baseNuGetDirectory = AssemblyResolverUtils.GetBaseNuGetDirectory(); + + public string Resolve(AssemblyName assemblyName) + { + // TODO: consider different frameworks versions + var path = Path.Combine(_baseNuGetDirectory, assemblyName.Name.ToLower()); + + if (!Directory.Exists(path)) + { + return null; + } + + return AssemblyResolverUtils.FindAssemblyWithName(new DirectoryInfo(path), assemblyName);; + } + } +} diff --git a/VSharp.CSharpUtils/VSharp.CSharpUtils.csproj b/VSharp.CSharpUtils/VSharp.CSharpUtils.csproj index b93f21c52..785319656 100644 --- a/VSharp.CSharpUtils/VSharp.CSharpUtils.csproj +++ b/VSharp.CSharpUtils/VSharp.CSharpUtils.csproj @@ -14,6 +14,7 @@ + diff --git a/VSharp.Utils/AssemblyManager.fs b/VSharp.Utils/AssemblyManager.fs index 72988d2a7..ed81622b6 100644 --- a/VSharp.Utils/AssemblyManager.fs +++ b/VSharp.Utils/AssemblyManager.fs @@ -1,101 +1,52 @@ namespace VSharp open System -open System.Collections.Generic open System.IO open System.Reflection open System.Runtime.Loader open Microsoft.Extensions.DependencyModel open Microsoft.Extensions.DependencyModel.Resolution - -type internal CurrentDirectoryAssemblyResolver(assemblyPath : string) = - interface ICompilationAssemblyResolver with - member x.TryResolveAssemblyPaths(library : CompilationLibrary, assemblies : List) = - let path = Path.Combine(assemblyPath, library.Name + ".dll") - if File.Exists path then - assemblies.Add path - true - else false +open VSharp.CSharpUtils.AssemblyResolving [] type internal AssemblyResolveContext(assembly : Assembly) as this = let assemblyDir = Path.GetDirectoryName assembly.Location - - // NB: DependencyContext.Load returns null for .NET Framework assemblies, and dependencies are - // loaded from shared libraries let depsContext = DependencyContext.Load assembly - - let resolver : ICompilationAssemblyResolver = CSharpUtils.CompositeCompilationAssemblyResolver [| - CurrentDirectoryAssemblyResolver assemblyDir; - AppBaseCompilationAssemblyResolver assemblyDir :> ICompilationAssemblyResolver; - ReferenceAssemblyPathResolver() :> ICompilationAssemblyResolver; - PackageCompilationAssemblyResolver() :> ICompilationAssemblyResolver |] :> ICompilationAssemblyResolver let assemblyContext = AssemblyLoadContext.GetLoadContext assembly let resolvingHandler = Func<_,_,_> this.OnResolving let resolvedAssemblies = ResizeArray(seq {assembly}) + let assemblyResolver = CompositeAssemblyResolver( + CurrentDirectoryAssemblyResolver assemblyDir, // Try to get a dll from the directory of the base assembly + MicrosoftDependencyModelAssemblyResolver( // Try Microsoft API ways (they don't work for base assemblies prior .NET Core, depsContext is just null) + depsContext, + [| + AppBaseCompilationAssemblyResolver assemblyDir :> ICompilationAssemblyResolver; + ReferenceAssemblyPathResolver() :> ICompilationAssemblyResolver; + PackageCompilationAssemblyResolver() :> ICompilationAssemblyResolver |] + ), + NuGetPackageNameMatchAssemblyResolver(), // Try to get a dll from the NuGet package directory with name matching assembly name + NuGetGraphAssemblyResolver assembly // If the base assembly is in NuGet package directory, get it's NuGet package dependencies and search transitively in their dirs + ) + let () = assemblyContext.add_Resolving resolvingHandler new(assemblyPath : string) = new AssemblyResolveContext(Assembly.LoadFile(assemblyPath)) - member private x.OnResolving (_ : AssemblyLoadContext) (assemblyName : AssemblyName) : Assembly = - let compLib = x.TryGetFromCompilationLibs(assemblyName) - let compLib = - match compLib with - | None -> x.TryGetFromRuntimeLibs(assemblyName) - | _ -> compLib - + member private x.OnResolving (ctx : AssemblyLoadContext) (assemblyName : AssemblyName) : Assembly = let resolved = - match compLib with - | Some compLib -> - x.LoadLibrary compLib - | None -> - x.LoadFromSharedLibrary assemblyName + try + assemblyResolver.Resolve assemblyName |> ctx.LoadFromAssemblyPath + with ex -> null + if resolved <> null then resolvedAssemblies.Add resolved + else + Logger.error $"[AssemblyManager] Cannot resolve: {assemblyName.FullName}" resolved - member private x.LoadFromSharedLibrary(assemblyName : AssemblyName) = - let dllPath = Path.Combine(assemblyDir, $"%s{(assemblyName.Name.Split(',')).[0]}.dll"); - try - assemblyContext.LoadFromAssemblyPath dllPath - with ex -> - Logger.error $"[AssemblyManager] Assembly resolution failed: {ex}" - null - - member x.TryGetFromCompilationLibs(assemblyName : AssemblyName) : CompilationLibrary option = - match depsContext with - | null -> None - | _ -> depsContext.CompileLibraries |> Seq.tryFind (fun e -> e.Name.Equals(assemblyName.Name, StringComparison.OrdinalIgnoreCase)) - - member private x.TryGetFromRuntimeLibs(assemblyName : AssemblyName) : CompilationLibrary option = - match depsContext with - | null -> None - | _ -> - match depsContext.RuntimeLibraries |> Seq.tryFind (fun e -> e.Name.Equals(assemblyName.Name, StringComparison.OrdinalIgnoreCase)) with - | Some runLib -> - CompilationLibrary( - runLib.Type, - runLib.Name, - runLib.Version, - runLib.Hash, - runLib.RuntimeAssemblyGroups |> Seq.collect (fun g -> g.AssetPaths), - runLib.Dependencies, - runLib.Serviceable) |> Some - | None -> None - - member private x.LoadLibrary(compLib : CompilationLibrary) = - try - let assemblies = List(); - if resolver.TryResolveAssemblyPaths(compLib, assemblies) then - assemblyContext.LoadFromAssemblyPath(assemblies.[0]) - else null - with ex -> - Logger.error "[AssemblyManager] Assembly resolution failed: %O" ex - null - member x.ResolvedAssemblies with get() = ResizeArray(resolvedAssemblies) member x.Assembly with get() = assembly diff --git a/VSharp.Utils/Logger.fs b/VSharp.Utils/Logger.fs index 3f3b6809e..076f9de03 100644 --- a/VSharp.Utils/Logger.fs +++ b/VSharp.Utils/Logger.fs @@ -19,17 +19,17 @@ module Logger = | 4 -> "Trace" | _ -> "Unknown" - let private writeLineString vLevel message = + let public printLogString vLevel message = let res = sprintf "[%s] [%A] %s" (LevelToString vLevel) DateTime.Now message current_text_writer.WriteLine(res) current_text_writer.Flush() let public printLog vLevel format = - Printf.ksprintf (fun message -> if current_log_level >= vLevel then writeLineString vLevel message) format + Printf.ksprintf (fun message -> if current_log_level >= vLevel then printLogString vLevel message) format let public printLogLazy vLevel format (s : Lazy<_>) = if current_log_level >= vLevel then - Printf.ksprintf (writeLineString vLevel) format (s.Force()) + Printf.ksprintf (printLogString vLevel) format (s.Force()) // let public printLogLazy vLevel format ([] s : Lazy<_> array) = // if current_log_level >= vLevel then diff --git a/VSharp.Utils/Reflection.fs b/VSharp.Utils/Reflection.fs index d8f5c7c4b..3df6456cc 100644 --- a/VSharp.Utils/Reflection.fs +++ b/VSharp.Utils/Reflection.fs @@ -197,6 +197,10 @@ module public Reflection = | None -> resolve targetType.BaseType resolve targetType + let hasByRefLikes (method : MethodInfo) = + method.DeclaringType <> null && method.DeclaringType.IsByRefLike || + method.GetParameters() |> Seq.exists (fun pi -> pi.ParameterType.IsByRefLike) || + method.ReturnType.IsByRefLike; // ----------------------------------- Creating objects ----------------------------------