diff --git a/CHANGELOG.md b/CHANGELOG.md index fd55581..2f38ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## [8.1.0-beta] - 2026-03-20 +- Add Windowed Rhino Option +- Add Support for Environment Variables in Paths +- Add Support for specifying Package Directories + ## [8.0.28-beta] - 2025-05-12 - Resolver bug fixes diff --git a/README.md b/README.md index 822233c..0a36670 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,20 @@ Specify Legacy IronPython to be loaded: true ``` +Specify Rhino To Start in Windowed Mode (default is headless): +```xml +true +``` +Important note: if you set `WindowedMode` to true, you must ensure that your tests are running in a static appartment: +To do that, add the following attribute to your setup fixture class `Apartment(ApartmentState.STA)`: +```csharp +[SetupFixture, Apartment(ApartmentState.STA)] +public sealed class SetupFixture : Rhino.Testing.Fixtures.RhinoSetupFixture +{ + // your setup fixture implementation +} +``` + Specify list of plugins to be loaded (These plugins are always loaded before Grasshopper) ```xml @@ -71,6 +85,21 @@ Specify list of plugins to be loaded (These plugins are always loaded before Gra ``` +Specify Directories to Load Packages from +```xml + + + +``` + +Use Environment Variables for Paths in the config file +```xml + + + ${env:ProgramFiles}\Rhino 8\System + +``` + Specify Grasshopper to be loaded: ```xml diff --git a/Rhino.Testing.sln b/Rhino.Testing.sln index 50f5bc4..7cb3235 100644 --- a/Rhino.Testing.sln +++ b/Rhino.Testing.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.8.34511.84 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11111.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rhino.Testing", "src\Rhino.Testing\Rhino.Testing.csproj", "{EE10AB9B-51C3-4025-AA26-C4DD3517E04C}" EndProject @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rhino.Testing.Tests.SetupFi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rhino.Testing.Tests.Utils", "src\Rhino.Testing.Tests.Utils\Rhino.Testing.Tests.Utils.csproj", "{A49EB5F1-7D83-4668-AF75-290BDCF350FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rhino.Testing.Tests.WindowedSetup", "src\Rhino.Testing.Tests.WindowedSetup\Rhino.Testing.Tests.WindowedSetup.csproj", "{31DA1866-70AC-437B-8B97-169EDDFDE4B1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {A49EB5F1-7D83-4668-AF75-290BDCF350FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {A49EB5F1-7D83-4668-AF75-290BDCF350FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {A49EB5F1-7D83-4668-AF75-290BDCF350FA}.Release|Any CPU.Build.0 = Release|Any CPU + {31DA1866-70AC-437B-8B97-169EDDFDE4B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31DA1866-70AC-437B-8B97-169EDDFDE4B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31DA1866-70AC-437B-8B97-169EDDFDE4B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31DA1866-70AC-437B-8B97-169EDDFDE4B1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7217518..1e98e29 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,7 +15,7 @@ - 8.0.28 + 8.1.0 8.0.6-beta 8.0.23304.9001 $(MSBuildThisFileDirectory)src\ @@ -33,11 +33,6 @@ - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - @@ -55,5 +50,4 @@ compile; build; native; contentfiles; analyzers; buildtransitive - - \ No newline at end of file + diff --git a/src/Rhino.Testing.Tests.SetupAttribute/Rhino.Testing.Tests.SetupAttribute.csproj b/src/Rhino.Testing.Tests.SetupAttribute/Rhino.Testing.Tests.SetupAttribute.csproj index 1bd3eca..8b9b024 100644 --- a/src/Rhino.Testing.Tests.SetupAttribute/Rhino.Testing.Tests.SetupAttribute.csproj +++ b/src/Rhino.Testing.Tests.SetupAttribute/Rhino.Testing.Tests.SetupAttribute.csproj @@ -1,6 +1,6 @@  - net48;net7.0-windows;net8.0-windows + net48;net8.0-windows false enable enable @@ -9,12 +9,18 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + Always diff --git a/src/Rhino.Testing.Tests.SetupFixture/Rhino.Testing.Tests.SetupFixture.csproj b/src/Rhino.Testing.Tests.SetupFixture/Rhino.Testing.Tests.SetupFixture.csproj index 121bb3c..f79ca7f 100644 --- a/src/Rhino.Testing.Tests.SetupFixture/Rhino.Testing.Tests.SetupFixture.csproj +++ b/src/Rhino.Testing.Tests.SetupFixture/Rhino.Testing.Tests.SetupFixture.csproj @@ -1,6 +1,6 @@  - net48;net7.0-windows;net8.0-windows + net48;net8.0-windows false enable enable @@ -9,6 +9,11 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -16,6 +21,7 @@ + Always diff --git a/src/Rhino.Testing.Tests.WindowedSetup/EventTest.cs b/src/Rhino.Testing.Tests.WindowedSetup/EventTest.cs new file mode 100644 index 0000000..47bd2b1 --- /dev/null +++ b/src/Rhino.Testing.Tests.WindowedSetup/EventTest.cs @@ -0,0 +1,27 @@ +using System; +using Rhino.Testing.Fixtures; + +namespace Rhino.Testing.Tests.WindowedSetup +{ + internal class EventTest : RhinoTestFixture + { + [Test] + public void NewDocumentTest() + { + bool documentEventFired = false; + + void LocalScopeNewDocument(object? sender, DocumentEventArgs e) + { + documentEventFired = true; + } + + RhinoDoc.NewDocument += LocalScopeNewDocument; + using (var doc = RhinoDoc.Create(null)) + { + Assert.That(documentEventFired, Is.True); + Assert.That(doc, Is.Not.Null); + } + RhinoDoc.NewDocument -= LocalScopeNewDocument; + } + } +} diff --git a/src/Rhino.Testing.Tests.WindowedSetup/Rhino.Testing.Configs.xml b/src/Rhino.Testing.Tests.WindowedSetup/Rhino.Testing.Configs.xml new file mode 100644 index 0000000..8b9af75 --- /dev/null +++ b/src/Rhino.Testing.Tests.WindowedSetup/Rhino.Testing.Configs.xml @@ -0,0 +1,11 @@ + + + C:\Program Files\Rhino 8\System + false + false + false + false + false + false + true + diff --git a/src/Rhino.Testing.Tests.WindowedSetup/Rhino.Testing.Tests.WindowedSetup.csproj b/src/Rhino.Testing.Tests.WindowedSetup/Rhino.Testing.Tests.WindowedSetup.csproj new file mode 100644 index 0000000..d76d4c8 --- /dev/null +++ b/src/Rhino.Testing.Tests.WindowedSetup/Rhino.Testing.Tests.WindowedSetup.csproj @@ -0,0 +1,29 @@ + + + net48;net8.0-windows + false + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + Always + + + \ No newline at end of file diff --git a/src/Rhino.Testing.Tests.WindowedSetup/SetupFixture.cs b/src/Rhino.Testing.Tests.WindowedSetup/SetupFixture.cs new file mode 100644 index 0000000..08522ff --- /dev/null +++ b/src/Rhino.Testing.Tests.WindowedSetup/SetupFixture.cs @@ -0,0 +1,10 @@ +namespace Rhino.Testing.Tests +{ + [SetUpFixture, Apartment(ApartmentState.STA)] + public sealed class SetupFixture : Rhino.Testing.Fixtures.RhinoSetupFixture + { + public override void OneTimeSetup() => base.OneTimeSetup(); + + public override void OneTimeTearDown() => base.OneTimeTearDown(); + } +} diff --git a/src/Rhino.Testing.Tests.WindowedSetup/SimpleTest.cs b/src/Rhino.Testing.Tests.WindowedSetup/SimpleTest.cs new file mode 100644 index 0000000..8420faf --- /dev/null +++ b/src/Rhino.Testing.Tests.WindowedSetup/SimpleTest.cs @@ -0,0 +1,17 @@ +using Rhino.Geometry; +using Rhino.Testing.Fixtures; + +namespace Rhino.Testing.Tests +{ + public class SimpleTest : RhinoTestFixture + { + [Test] + public void Test() + { + Point3d point = new(1, 2, 3); + Assert.That(point.X, Is.GreaterThan(0)); + Assert.That(point.Y, Is.GreaterThan(0)); + Assert.That(point.Z, Is.GreaterThan(0)); + } + } +} diff --git a/src/Rhino.Testing.Tests.WindowedSetup/Usings.cs b/src/Rhino.Testing.Tests.WindowedSetup/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/src/Rhino.Testing.Tests.WindowedSetup/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/src/Rhino.Testing.Tests/Rhino.Testing.Configs.xml b/src/Rhino.Testing.Tests/Rhino.Testing.Configs.xml index 53cd297..69a3a53 100644 --- a/src/Rhino.Testing.Tests/Rhino.Testing.Configs.xml +++ b/src/Rhino.Testing.Tests/Rhino.Testing.Configs.xml @@ -7,4 +7,7 @@ true false false + + + diff --git a/src/Rhino.Testing.Tests/Rhino.Testing.Tests.csproj b/src/Rhino.Testing.Tests/Rhino.Testing.Tests.csproj index 1bd3eca..e0602cf 100644 --- a/src/Rhino.Testing.Tests/Rhino.Testing.Tests.csproj +++ b/src/Rhino.Testing.Tests/Rhino.Testing.Tests.csproj @@ -1,6 +1,6 @@  - net48;net7.0-windows;net8.0-windows + net48;net8.0-windows false enable enable @@ -9,6 +9,11 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/src/Rhino.Testing/Configs.cs b/src/Rhino.Testing/Configs.cs index 205d399..db74e6e 100644 --- a/src/Rhino.Testing/Configs.cs +++ b/src/Rhino.Testing/Configs.cs @@ -50,12 +50,24 @@ public static T Deserialize(XmlSerializer serializer, string settingsFile) [XmlElement] public bool LoadGrasshopper { get; set; } = false; + [XmlElement] + public bool WindowedRhino { get; set; } = false; + + [XmlArray("LoadPlugins")] [XmlArrayItem("Plugin")] #pragma warning disable CA1002 // Do not expose generic lists #pragma warning disable CA2227 // Collection properties should be read only public List LoadPlugins { get; set; } = new List(); #pragma warning restore CA2227 // Collection properties should be read only +#pragma warning restore CA1002 // Do not expose generic lists + + [XmlArray("PackageDirectories")] + [XmlArrayItem("Directory")] +#pragma warning disable CA1002 // Do not expose generic lists +#pragma warning disable CA2227 // Collection properties should be read only + public List PackageDirectories { get; set; } = new List(); +#pragma warning restore CA2227 // Collection properties should be read only #pragma warning restore CA1002 // Do not expose generic lists [XmlIgnore] @@ -70,6 +82,36 @@ public Configs() SettingsDir = Path.GetDirectoryName(SettingsFile); } + /// + /// Support for using environment variables in the rhino system directory, package directories, and plugin paths, in the form of ${env:VAR_NAME} + /// + /// + /// + public static string ReplaceEnvVars(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + int startIndex = 0; + while (true) + { + int envStart = path.IndexOf("${env:", startIndex, StringComparison.OrdinalIgnoreCase); + if (envStart == -1) + break; + int envEnd = path.IndexOf('}', envStart); + if (envEnd == -1) + break; + string envVar = path.Substring(envStart + 6, envEnd - envStart - 6); + string envVal = Environment.GetEnvironmentVariable(envVar) ?? string.Empty; +#if NET7_0_OR_GREATER + path = path[..envStart] + envVal + path[(envEnd + 1)..]; +#else + path = path.Substring(0, envStart) + envVal + path.Substring(envEnd + 1); +#endif + startIndex = envStart + envVal.Length; + } + return path; + } + static Configs() { string cfgFile = GetConfigsFile(); @@ -77,6 +119,27 @@ static Configs() if (File.Exists(cfgFile)) { Current = Deserialize(new XmlSerializer(typeof(Configs)), cfgFile); + Current.RhinoSystemDir = ReplaceEnvVars(Current.RhinoSystemDir); + + // separate our the Package Directories by the path separator and trim whitespace + List dirs = new List(); + foreach (var dir in Current.PackageDirectories) + { + var path = ReplaceEnvVars(dir.Path.Trim()); + string[] splitDirs = path.Split(new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + foreach (var splitDir in splitDirs) + { + dirs.Add(new DirectoryConfigs() { Path = splitDir.Trim() }); + } + } + //add the config file directory as a package directory so that plugins can be specified relative to the config file location + dirs.Add(new DirectoryConfigs() { Path = Path.GetDirectoryName(cfgFile) }); + Current.PackageDirectories = dirs; + + foreach (var plugin in Current.LoadPlugins) + { + plugin.Location = ReplaceEnvVars(plugin.Location); + } if (Path.IsPathRooted(Current.RhinoSystemDir)) { @@ -105,4 +168,12 @@ public sealed class PluginConfigs [XmlAttribute] public string Location { get; set; } = string.Empty; } + + [Serializable] + [XmlRoot("Directory")] + public sealed class DirectoryConfigs + { + [XmlAttribute] + public string Path { get; set; } = string.Empty; + } } diff --git a/src/Rhino.Testing/RhinoCore.cs b/src/Rhino.Testing/RhinoCore.cs index e64ac6c..53f4bcf 100644 --- a/src/Rhino.Testing/RhinoCore.cs +++ b/src/Rhino.Testing/RhinoCore.cs @@ -37,6 +37,14 @@ public static void Initialize() throw new DirectoryNotFoundException(Configs.Current.RhinoSystemDir); } + //add the Package directories to the RHINO_PACKAGE_DIRS environment variable so that RhinoInside can find the plugins in those directories + foreach (var packageDir in Configs.Current.PackageDirectories) + { + var env = Environment.GetEnvironmentVariable("RHINO_PACKAGE_DIRS", EnvironmentVariableTarget.Process); + env = string.IsNullOrEmpty(env) ? packageDir.Path : $"{env};{packageDir.Path}"; + Environment.SetEnvironmentVariable("RHINO_PACKAGE_DIRS", env, EnvironmentVariableTarget.Process); + } + RhinoInside.Resolver.Initialize(Configs.Current.RhinoSystemDir); Configs.Current.RhinoSystemDir = RhinoInside.Resolver.RhinoSystemDirectory; @@ -63,7 +71,7 @@ public static void Initialize() if (Configs.Current.LoadPlugins.Count != 0) { - PluginLoader.LoadPlugins(Configs.Current.LoadPlugins.Select(p => p.Location)); + PluginLoader.LoadPlugins(Configs.Current.LoadPlugins.Select(p => p.Location), Configs.Current.PackageDirectories.Select(d => d.Path)); } if (Configs.Current.LoadGrasshopper) diff --git a/src/Rhino.Testing/_Loaders/PluginLoader.cs b/src/Rhino.Testing/_Loaders/PluginLoader.cs index 9095630..7b3dde7 100644 --- a/src/Rhino.Testing/_Loaders/PluginLoader.cs +++ b/src/Rhino.Testing/_Loaders/PluginLoader.cs @@ -6,13 +6,25 @@ using RhinoInside; using NUnit.Framework; +using System.Diagnostics; namespace Rhino.Testing { static class PluginLoader { - public static string GetRHPPath(string rhpPath) + public static string GetRHPPath(string rhpPath, IEnumerable packageDirs = null) { + if(File.Exists(rhpPath)) + { + if (Path.IsPathRooted(rhpPath)) + { + return rhpPath; + } + var dir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + rhpPath = Path.Combine(dir, rhpPath); + return rhpPath; + } + // first look in the rhino system directory string rhp = Path.Combine(Configs.Current.RhinoSystemDir, rhpPath); if (File.Exists(rhp)) { @@ -38,6 +50,22 @@ public static string GetRHPPath(string rhpPath) } } + if (packageDirs != null) + { + foreach (var packageDir in packageDirs) + { + rhp = Path.Combine(packageDir, rhpPath); + if (File.Exists(rhp)) + { + return rhp; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Directory.Exists(rhp)) + { + return rhp; + } + } + } + throw new FileNotFoundException(rhpPath); } @@ -145,13 +173,13 @@ public static void LoadGrasshopper() } } - public static void LoadPlugins(IEnumerable rhpPaths) + public static void LoadPlugins(IEnumerable rhpPaths, IEnumerable packageDirs) { foreach (var rhpPath in rhpPaths) { - string fullPath = GetRHPPath(rhpPath); - + string fullPath = GetRHPPath(rhpPath, packageDirs); TestContext.WriteLine($"Loading plugin from {fullPath}"); + Debug.WriteLine($"Loading plugin from {fullPath}"); if (PlugIns.PlugIn.LoadPlugIn(fullPath, out Guid _) != PlugIns.LoadPlugInResult.Success) diff --git a/src/Rhino.Testing/_Loaders/RhinoCoreLoader.cs b/src/Rhino.Testing/_Loaders/RhinoCoreLoader.cs index b98f4bd..b1e60c7 100644 --- a/src/Rhino.Testing/_Loaders/RhinoCoreLoader.cs +++ b/src/Rhino.Testing/_Loaders/RhinoCoreLoader.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Threading; using NUnit.Framework; @@ -23,7 +25,23 @@ public static void LoadCore(bool createDoc, bool createView) lock (s_coreLock) { - s_core = new Rhino.Runtime.InProcess.RhinoCore(args); + if (!Configs.Current.WindowedRhino) + { + s_core = new Rhino.Runtime.InProcess.RhinoCore(args); + } + else + { + //Verify apartment state before initialization + var apartmentState = Thread.CurrentThread.GetApartmentState(); + TestContext.WriteLine($"Current thread apartment state: {apartmentState}"); + if(apartmentState != ApartmentState.STA) + { + throw new InvalidOperationException("For windowed mode, thread must be static (STA). Add Apartment(ApartmentState.STA) to test SetupFixture"); + } + List windowedArgs = new List(args); + windowedArgs.AddRange(new string[] { "/notemplate", "/nosplash"}); + s_core = new Rhino.Runtime.InProcess.RhinoCore(windowedArgs.ToArray(), Runtime.InProcess.WindowStyle.Hidden); + } if (createDoc) {