mcpiroman / UnityNativeTool

Allows to unload native plugins in Unity3d editor
MIT License
183 stars 18 forks source link

How to use this to lazy load plugins for Unity Test Framework tests? #34

Closed nicolashahn closed 3 years ago

nicolashahn commented 3 years ago

I'm using this successfully in play mode but when I run play mode tests in Unity Test Framework, I'm seeing a System.DllNotFoundException. When I replace the DllImport("<plugin>") with DllImport("__<plugin>") in the test file it works, but then I have to restart Unity to load a new version of the plugin. Is this something you can help with?

Here's my test file if it helps:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

public class PlayTestScript
{
    [DllImport("__RenderingPlugin")]
    private static extern int TestFunction();

    // A Test behaves as an ordinary method
    [Test]
    public void PlayTestScriptSimplePasses()
    {
        // Use the Assert class to test conditions
        Assert.AreEqual(42, TestFunction());
    }
}
mcpiroman commented 3 years ago

I didn't have tests in mind at all, so as we can see they are probably not supported ¯_(ツ)_/¯.

That said, I don't see why they shouldn't work (though I don't know/remember how the unit test frameworks works either). The most important thing is whether the DllManipulatorScript is instantiated in the scene (I guess there is some scene?) before the test method runs, just like in play mode. Can you confirm that?

If you cannot ensure that by the UI as you can in the editor, you can probably use the setup functionality of NUnit. Try adding a [Setup] method which instantiates the DllManipulatorScript script and maybe call it's Initialize method.

Btw. if you do DllImport("__<plugin>") then you're just using the native Unity's plugin handling (not this tool), so it won't be reloadable either way.

nicolashahn commented 3 years ago

The most important thing is whether the DllManipulatorScript is instantiated in the scene (I guess there is some scene?) before the test method runs, just like in play mode. Can you confirm that?

Yes, I added a log inside DllManipulatorScript's Initialize() function that appeared when I ran the test. Maybe this is getting called after the test method?

If you cannot ensure that by the UI as you can in the editor, you can probably use the setup functionality of NUnit. Try adding a [Setup] method which instantiates the DllManipulatorScript script and maybe call it's Initialize method.

I'm not sure what the import statement should be (using UnityNativeTool or Plugins.UnityNativeTool don't work). This is my project layout:

Capture

Btw. if you do DllImport("__") then you're just using the native Unity's plugin handling (not this tool), so it won't be reloadable either way.

I'm aware, I copied the file after I had done the change just to make sure the test would pass at all.

mcpiroman commented 3 years ago

Yes, I added a log inside DllManipulatorScript's Initialize() function that appeared when I ran the test. Maybe this is getting called after the test method?

Yup, that's important it's called before the test method, you should check that.

using UnityNativeTool or Plugins.UnityNativeTool don't work

Hmm.. that should be UnityNativeTool, but if you don't see that, then it probably means you don't target the tools assembly (mcpiroman.UnityNativeTool). AFAIK there should be an option somewhere in Unity to specify them for tests.

Speaking of assemblies, that might be the issue here. If the test framework uses different assembly than the main one, Assembly-CSharp, (and I'm pretty sure it does), than it is not considered by default and you need to add it in options (Only Assembly-CSharp(-Editor)).

nicolashahn commented 3 years ago

I'm not sure exactly what you mean by changing assemblies, however: I was able to add using UnityNativeTool by changing PlayTestScript.asmdef:

{
  "name": "PlayModeTests",
  "optionalUnityReferences": [
    "TestAssemblies"
  ],
  "references": [
    "mcpiroman.UnityNativeTool"
  ]
}

Now my PlayTestScript is:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.SceneManagement;
using UnityNativeTool;

public class PlayTestScript
{

    [DllImport("RenderingPlugin")]
    private static extern int TestFunction();

    [SetUp]
    public void Setup()
    {
        var script = new DllManipulatorScript();
        script.Initialize();
        Debug.Log("did DllManipulatorScript.Initialize() in test Setup()");
    }

    [Test]
    public void PlayTestScriptSimplePasses()
    {
        Debug.Log("doing test");
        Assert.AreEqual(42, TestFunction());
    }
}

and when I run the test, the setup log shows up before the test's log, but I'm still seeing System.DllNotFoundException : RenderingPlugin error on the line that calls TestFunction().

mcpiroman commented 3 years ago

I'm not sure exactly what you mean by changing assemblies, however: I was able to add using UnityNativeTool by changing PlayTestScript.asmdef:

Well, I meant just that what you did there.

Secondly, you will need to add the test assembly to the list of mocked assemblies, because by default only Assembly-CSharp(-Editor) is being considered. If you setup this from code, then you probably need to do something like

var script = new DllManipulatorScript();
script.Options.assemblyNames = new List() { "PlayModeTests" // or whatever the test assembly name is }
script.Initialize();

You may try to find the assembly name by something like AppDomain.Current.Assembly.Name, sth like that, don't remember now. And if you find out the name and it works, please share it here so I can add it to defaults without checking myself.

nicolashahn commented 3 years ago

Thanks for that code, it worked and now I can run the test. However, now it appears that Unity won't unlock the __RenderingPlugin.dll file after the test has run and I need to restart Unity to be able to change the plugin, defeating the point of this tool :( any idea what's going on?

Thanks for all your help so far, by the way.

mcpiroman commented 3 years ago

Oh yes, you'll need to unload the DLLs somehow, there is no 'stop' button after all. So I suggest you just use the teardown method (analogous to setup) and call DllManipulator.UnloadAll(). Or DllManipulator.Reset() if you don't use it anymore - you'll have to find out which is more correct for you, depending on how the tests work - remember DllManipulator is static tho.

nicolashahn commented 3 years ago

It looks like Unity still won't give up the file. Here's my setup and teardown:

    [SetUp]
    public void Setup()
    {
        SceneManager.LoadScene("TestScene", LoadSceneMode.Single);

        var obj = new GameObject();
        var script = obj.AddComponent<DllManipulatorScript>();
        script.Options.assemblyNames = new List<string> { "PlayModeTests" };
        script.Initialize();
    }

    [TearDown]
    public void Teardown()
    {

        DllManipulator.UnloadAll();

        SceneManager.UnloadSceneAsync("TestScene");
    }

edit: Reset() does not work either

nicolashahn commented 3 years ago

Oh, my bad! I had the Dll loading mode set to "Preload" for the script settings. After I changed it to "Lazy" it's working as intended. Thank you again!