Skip to content

Resolving issue with Costura packed assemblies #1747

@MaYoT27

Description

@MaYoT27

Hello!

I am using Fody Costura for packing/embedding assemblies into one DLL. When the method from e.g. a test is called, Costura unpacks the needed assemblies into the AppData\Local\Temp\Costura\{guid} folder. For example the assembly with test is called TestAssembly.dll and inside there is Costura packed assembly EmbeddedAssembly.dll that is not present in the directory of the TestAssembly.dll, but unpacked into that Costura folder on demand.

Now, the issue is, that when I'm trying to run the tests with NUnit.Engine, when the test contains something from the EmbeddedAssembly.dll, it's properly unpacked into the Costura folder, but the assembly cannot be resolved. The internal log file shows:

Debug [17] TestAssemblyLoadContext: Loading EmbeddedAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null assembly
Debug [17] TestAssemblyResolver: Best version dir for C:\Program Files (x86)\dotnet\shared\Microsoft.WindowsDesktop.App is 6.0.31, but there is no C:\Program Files (x86)\dotnet\shared\Microsoft.WindowsDesktop.App\6.0.31\EmbeddedAssembly.dll file
Debug [17] TestAssemblyResolver: Best version dir for C:\Program Files (x86)\dotnet\shared\Microsoft.AspNetCore.App is 6.0.31, but there is no C:\Program Files (x86)\dotnet\shared\Microsoft.AspNetCore.App\6.0.31\EmbeddedAssembly.dll file
Info  [17] TestAssemblyResolver: Cannot resolve assembly 'EmbeddedAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'

The TestAssembly.dll is .NET 8.0. My project that runs those tests is in .NET Standard 2.0 with NUnit.Engine version 3.18.3, but the behavior is the same when I build the project on .NET 8.0 and use the newest engine (3.20.1).

I have my own solution for that issue, but that requires changes in the NUnit.Engine itself. I cloned the repo and added new ResolutionStrategy for the TestAssemblyResolver.cs that gets all the DLLs from the Costura folder and also very important for my case, checks the version of the DLL, not only the name:

public class CosturaDirectoryStrategy : ResolutionStrategy
{
    private string _costuraDirectory;

    public CosturaDirectoryStrategy(string costuraDirectory)
    {
        _costuraDirectory = costuraDirectory;
    }

    public override bool TryToResolve(
        AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly)
    {
        loadedAssembly = null;
        if (assemblyName.Version == null)
            return false;

        try
        {
            var allDirs = Directory.GetDirectories(_costuraDirectory, "*", SearchOption.AllDirectories);

            var sortedDirs = allDirs
                .Select(d => new DirectoryInfo(d))
                .OrderByDescending(di => di.LastWriteTimeUtc)
                .ToList();

            foreach (var dir in sortedDirs)
            {
                string candidate = Path.Combine(dir.FullName, assemblyName.Name + ".dll");
                if (File.Exists(candidate))
                {
                    try
                    {
                        var candidateName = AssemblyName.GetAssemblyName(candidate);

                        if (candidateName.Version == assemblyName.Version)
                        {
                            loadedAssembly = loadContext.LoadFromAssemblyPath(candidate);

                            log.Info("'{0}' ({1}) assembly v{2} is loaded from CosturaDirectory {3}",
                                assemblyName.Name,
                                loadedAssembly.Location,
                                candidateName.Version,
                                dir.FullName);

                            return true;
                        }
                    }
                    catch (BadImageFormatException)
                    {
                        continue;
                    }
                }
            }

            log.Debug("No matching DLL for {0}, version {1} found in {2}",
                assemblyName.Name,
                assemblyName.Version,
                _costuraDirectory);

            return false;
        }
        catch (Exception ex)
        {
            log.Error("Error while resolving from Costura directories: '{0}'", ex.Message);
            return false;
        }
    }
}

With that and pointing to the Path.Combine(Path.GetTempPath(), "Costura") directory it works well.

Is there any possibility to get it done with current implementation of NUnit.Engine, without having to resort to cloning the repo and adding that ResolutionStrategy? I was looking at the TrustedPlatformAssembliesStrategy, but the problem is that the assembly might not be yet available before Costura unpacks it and this is already happening during the test execution.

Also worth to mention that test run from Rider or Visual Studio test explorers works well, without this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    V4All issues related to V4 or later - use -label:V4 to get V3 issues

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions