Unit testing plugin code

So today I tried setting up unit test project for our plugin. I immediately got this exception:

System.DllNotFoundException: Unable to load DLL ‘rhcommon_c’: The specified module could not be found.

I know executable of rhino sort of loads rhcommon_c for runtime, but I really don’t understand this limitation. Isn’t Rhinocommon open sourced now? For unit tests I really don’t want to start up rhino and do weird things with the project. Unit testing needs to be fast and go hand in hand with development. Is rhcommon_c proprietary somehow and thus closed down? Can someone explain what is happening and what could be a solution to this? Thanks.

1 Like

If it were only so easy…

The majority of RhinoCommon depends on functionality exported from the rhcommon_c.dll
The rhcommon_c.dll depends on Rhino.exe
Due to this dependency chain, Rhino.exe needs to be the host process.

Hi Steve,

Thanks for responding. I don’t want to sound negative, I enjoy everything that has to do with Rhino, but I’m quite frustrated about this point… Is this something that is in McNeel policy and will not be lifted? Or that’s simply architecturally impossible?

I could see amazing scenario’s of making cloud services for rhino on azure or smth, but it will not going to work if even unit tests can’t be decoupled from executables. Is there something that community could help with? Or can executable be run in some container process without starting the whole UI? Or is it better not to expect this?

This has nothing to do with “policy” and is all about the architecture. The code code for computing something like a boolean union of two breps is in the Rhino executable. RhinoCommon can’t compute this boolean union if the necessary code isn’t in place.

@Alain has written our unit testing framework for .NET that uses NUnit. It still requires that Rhino is executing though. He may be able to give a little better insight in how he structured this.

Ok, it is clear, I would appreciate @Alain insights. And thanks for lightning fast replies.

If you’re not using any Rhino SDK functions that are architecturally bound to Rhino.exe (that includes all more advanced stuff, like intersections, surface operations, etc.), you could copy the complete system folder contents into your project folder. Then RhinoCommon will load, and basic functionality will work under a different process.

This is not ideal and when you want to run your plug-in again in Rhino, you should remove any Rhino system files from your project folder.

Hi Matas ,

We just got far enough with this so we can run our tests by loading the gui runner from a plug-in command. You’ll need the NUnit and NUnit.Runners NuGet packages. The code looks like this:

We experimented a bit with scripting the command so the tests could be run without the gui and part of the build process but didn’t get far enough for it to fully work. We’re using v2.6.3 of NUnit but I’ll look at v3 when I get back to this project.

Alain

Hey guys, thanks a lot for replies and an advise. I was literally driving home for christmas for the last few days, so could’nt reply. I’ll try to implement the NUnit as soon as possible and check how far I can get with this idea. It turns out we use quite a lot of intersection math, some boolean unions as well, but I’m hoping I could isolate those parts and test around that… I’ll let you know if I get this working next week. Thanks again!

Ok, so I just wanted to reply on this topic for what I chose to do at the end for my project to be able to write unit tests.

First of all, I fenced my code off with interfaces from rhinocommon. This demanded some major refactoring and I’m still not completely done, but this in general is a good practice and it makes majority of the code coverable. I had to implement rhino specific wrappers and converters to make interfacing easier, but the api surface we’re using from rhino is at this point not yet hugenormous. This interface wall also opens up many neat possibilities for integration with other CAD systems.

This solution treats rhino as a black box and expects nothing to be returned from rhinocommon business logic. It means that all the endpoints need to be generally mocked via Moq or other libraries…

The need of checking if rhino is doing it’s job correctly did not disappear though. To fix that I’ve also set up some integration tests, in a separate rhino-plugin scaffolded project. It is being run through rhino executable, but that’s totally fine as long as I can use unit tests together with integration tests.

I hope this gives new people some idea as of how to begin architecting the plugin for scale and testability so that refactoring would not be needed later on :wink: Cheers!

Hi @Alain, I wanted to followup to see how things went with the nunit 3.x investigation – are there any other updates on your project of running tests inside of a rhino command host?

To add another anecdote to the thread, like Matas, we’re pursuing a path of fencing our code off from rhino-common. This is not a tiny effort, but is closely related to other refactorings that improve our code bases’ testability. We expect to pair this with a test-harness plugin that will handle integration tests with the rhino envirionment. It’s not clear at this point whether or not it will make sense to utilize nunit in that host.

Hi Andrew,
I haven’t looked at nunit 3.x.

Well, we (eventually) got around to investigating nunit 3.x, and it turns out that it was very straightforward to embed in a rhino command.

There’s a sample here demonstrating this in case anybody else comes across this and is interested.

The one hiccup is that as of this writing, the 3.x guirunner is not yet “production-ready” – it can be found here, and should be straightforward to adapt.

3 Likes

This is a great. Thanks a lot. Just started to put up a test framework for our plugin.

Thanks a lot
Christian

Using Rhino3dmIO instead of RhinoCommon solved this for me.
I am using VisualStudio Unit Test, on Visual Studio 2015.

Since it is barely I/O It doesn’t allow any complex method to be used though.

Here is another approach that directly uses Rhino/RhinoCommon

Hi @Andrew_Zukoski ,

Thanks for the template. I get this error when I try to build and run your plugin. Any clues why this might be happening?

Command: RhinoNUnitTestRunnerCommand
NUnitLite 3.8.1 (.NET 4.0)
Copyright (c) 2017 Charlie Poole, Rob Prouse
Runtime Environment
OS Version: Microsoft Windows NT 10.0.19045.0
CLR Version: 4.0.30319.42000
Test Files
C:/Github/RhinoNUnitTestRunner/RhinoNUnitTestRunner/bin/TestsUsingRhinoAPI.DLL
Errors, Failures and Warnings

  1. Failed : TestsUsingRhinoAPI.RhinoCommonTest.TestThatWillFail
    Expected: True
    But was: False
    at TestsUsingRhinoAPI.RhinoCommonTest.TestThatWillFail() in C:\Github\RhinoNUnitTestRunner\TestsUsingRhinoAPI\RhinoCommonTest.cs:line 22
    Run Settings
    Number of Test Workers: 16
    Work Directory: C:\Program Files\Rhino 7\System
    Internal Trace: Off
    Test Run Summary
    Overall result: Failed
    Test Count: 2, Passed: 1, Failed: 1, Warnings: 0, Inconclusive: 0, Skipped: 0 Failed Tests - Failures: 1, Errors: 0, Invalid: 0 Start time: 2024-01-25 19:00:52Z
    End time: 2024-01-25 19:00:53Z
    Duration: 0.068 seconds
    System.UnauthorizedAccessException: Access to the path ‘C:\Program Files\Rhino 7\System\TestResult.xml’ is denied.
    at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
    at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
    at System.IO.FileStream…ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy)
    at System.IO.FileStream…ctor(String path, FileMode mode)
    at NUnitLite.OutputWriter.WriteResultFile(ITestResult result, String outputPath, IDictionary2 runSettings, TestFilter filter) at NUnitLite.OutputManager.WriteResultFile(ITestResult result, OutputSpecification spec, IDictionary2 runSettings, TestFilter filter)
    at NUnitLite.TextRunner.RunTests(TestFilter filter, IDictionary`2 runSettings)