diff --git a/.gitignore b/.gitignore index 93d7e05bb..bf09a2a10 100644 --- a/.gitignore +++ b/.gitignore @@ -217,6 +217,8 @@ pip-log.txt #Mr Developer .mr.developer.cfg + +packages/* src/AssemblySharedInfoGenerator/AssemblySharedInfo.cs test/Libraries/RevitIntegrationTests/RevitTestConfiguration.xml test/**/*.txt @@ -225,3 +227,4 @@ test/SystemInJson # Icon resources /src/DynamoRevitIcons/*.resources +logs/* \ No newline at end of file diff --git a/test/Libraries/RevitIntegrationTests/OOTB_D4R_SampleTests.cs b/test/Libraries/RevitIntegrationTests/OOTB_D4R_SampleTests.cs new file mode 100644 index 000000000..e37c3b018 --- /dev/null +++ b/test/Libraries/RevitIntegrationTests/OOTB_D4R_SampleTests.cs @@ -0,0 +1,284 @@ +using System; +using System.IO; +// using System.IO.Compression; // Needed when zip extraction (priorities 2/3) is enabled +using System.Linq; +using System.Reflection; +using Dynamo.Graph.Nodes; +using NUnit.Framework; +using RevitTestServices; +using RTF.Framework; + +namespace RevitSystemTests +{ + /// + /// Tests for the Out-of-the-Box (OOTB) D4R sample scripts shipped as part of the + /// revit-d4r-content-samples artifact. + /// + /// The .dyn files are resolved at runtime via which checks + /// the already-deployed samples at DynamoForRevit\samples\{locale}\Revit\. + /// + /// Future consideration: zip-based extraction is available as commented code below + /// (priorities 2 and 3) for scenarios where the samples are shipped as a zip artifact. + /// + [TestFixture] + class OOTB_D4R_SampleTests : RevitSystemTestBase + { + private static string ResolveSamplePath(string scriptFileName) + { + // Assembly.GetExecutingAssembly().Location is the standard pattern used throughout + // the test framework (RevitSystemTestBase, SystemTest, CoreTests, RegressionTests). + // RTF always loads test assemblies from the deployed DynamoForRevit\Revit\ folder. + string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + // NOTE: We intentionally do NOT use the test configuration's core samples location + // (doc/distrib/Samples/ for Dynamo core samples). The D4R OOTB samples are deployed + // at DynamoForRevit\samples\{locale}\Revit\ alongside the plugin. + // + // When deployed to Revit (via RTF): + // assemblyDir = DynamoForRevit\Revit\ + // parentDir = DynamoForRevit\ + string parentDir = Path.GetDirectoryName(assemblyDir); + + string samplesFolder = Path.Combine(parentDir, "samples"); + + // Priority 1: already-deployed samples — probe current culture then en-US fallback + if (Directory.Exists(samplesFolder)) + { + var localesToProbe = new[] + { + System.Globalization.CultureInfo.CurrentUICulture.Name, // e.g. "fr-FR" + "en-US" + }.Distinct(); + + foreach (var locale in localesToProbe) + { + string localePath = Path.Combine(samplesFolder, locale, "Revit"); + if (Directory.Exists(localePath)) + { + var resolved = Path.Combine(localePath, scriptFileName); + if (File.Exists(resolved)) + return resolved; + } + } + } + + #region Future: zip-based extraction (priorities 2 and 3) + // // Priority 2: zip file — use version-keyed cache in temp to avoid staleness + // if (Directory.Exists(samplesFolder)) + // { + // var zipFiles = Directory.GetFiles( + // samplesFolder, + // "revit-d4r-content-samples-*-net10.zip") + // .OrderBy(path => path) + // .ToArray(); + // + // if (zipFiles.Length > 1) + // { + // var matches = zipFiles.Select(path => $"\n {path}"); + // throw new InvalidOperationException( + // $"Multiple revit-d4r-content-samples archives were found in '{samplesFolder}', " + + // $"so the OOTB D4R sample source is ambiguous. Ensure exactly one matching zip is present." + + // $"{string.Concat(matches)}"); + // } + // + // if (zipFiles.Length == 1) + // { + // // Derive cache folder name from zip filename to invalidate on version change + // string zipName = Path.GetFileNameWithoutExtension(zipFiles[0]); + // string extractedSamplesPath = Path.Combine(Path.GetTempPath(), "D4RSamples", zipName); + // + // // If already extracted for this version, use cached + // var resolved = Path.Combine(extractedSamplesPath, @"Samples\en-US\Revit", scriptFileName); + // if (File.Exists(resolved)) + // return resolved; + // + // // Extract to staging dir, then move into place to avoid partial cache + // string stagingPath = extractedSamplesPath + "_staging_" + Guid.NewGuid().ToString("N")[..8]; + // try + // { + // ZipFile.ExtractToDirectory(zipFiles[0], stagingPath); + // if (Directory.Exists(extractedSamplesPath)) + // Directory.Delete(extractedSamplesPath, recursive: true); + // Directory.Move(stagingPath, extractedSamplesPath); + // } + // catch + // { + // // Clean up staging on failure so next run can retry + // if (Directory.Exists(stagingPath)) + // Directory.Delete(stagingPath, recursive: true); + // throw; + // } + // + // resolved = Path.Combine(extractedSamplesPath, @"Samples\en-US\Revit", scriptFileName); + // if (File.Exists(resolved)) + // return resolved; + // } + // } + // + // // Priority 3: reuse any previously cached extraction (zip may have been removed) + // string cacheRoot = Path.Combine(Path.GetTempPath(), "D4RSamples"); + // if (Directory.Exists(cacheRoot)) + // { + // var cached = Directory.GetDirectories(cacheRoot) + // .OrderByDescending(d => Directory.GetLastWriteTime(d)) + // .Select(d => Path.Combine(d, @"Samples\en-US\Revit", scriptFileName)) + // .FirstOrDefault(File.Exists); + // if (cached != null) + // return cached; + // } + #endregion + + // Provide a useful diagnostic message to help locate the issue + if (Directory.Exists(samplesFolder)) + { + var entries = Directory.EnumerateFileSystemEntries(samplesFolder) + .OrderBy(e => e) + .Select(e => $"\n {e}"); + throw new FileNotFoundException( + $"Cannot locate OOTB D4R sample script '{scriptFileName}'.\n" + + $"Checked samples folder: {samplesFolder}\n" + + $"Contents:{string.Concat(entries)}"); + } + else + { + var parentEntries = Directory.Exists(parentDir) + ? Directory.EnumerateDirectories(parentDir).OrderBy(e => e).Select(e => $"\n {e}") + : Enumerable.Empty(); + throw new FileNotFoundException( + $"Cannot locate OOTB D4R sample script '{scriptFileName}'.\n" + + $"Expected samples folder not found: {samplesFolder}\n" + + $"Parent dir contents:{string.Concat(parentEntries)}"); + } + } + + private void OpenAndRunSample(string scriptFileName) + { + string samplePath = ResolveSamplePath(scriptFileName); + string testPath = Path.GetFullPath(samplePath); + + ViewModel.OpenCommand.Execute(testPath); + + Assert.IsTrue( + ViewModel.Model.CurrentWorkspace.Nodes.Any(), + $"Graph '{scriptFileName}' opened but contains no nodes — file may not have loaded correctly."); + + AssertNoDummyNodes(); + + // Verify no broken connectors — a missing start/end port indicates a load problem + // (e.g. renamed or removed node ports between versions). + var brokenConnectors = ViewModel.Model.CurrentWorkspace.Connectors + .Where(c => c.Start == null || c.End == null).ToList(); + + if (brokenConnectors.Any()) + { + var details = string.Join("\n", brokenConnectors.Select(c => + { + string startInfo = c.Start != null + ? $"{c.Start.Owner?.Name}:{c.Start.Name}[{c.Start.Index}]" + : ""; + string endInfo = c.End != null + ? $"{c.End.Owner?.Name}:{c.End.Name}[{c.End.Index}]" + : ""; + return $" Connector {c.GUID}: Start={startInfo} → End={endInfo}"; + })); + + Assert.Fail( + $"Graph '{scriptFileName}' has {brokenConnectors.Count} broken connector(s) " + + $"with missing start or end port:\n{details}"); + } + + RunCurrentModel(); + + var errorNodes = ViewModel.Model.CurrentWorkspace.Nodes.Where( + n => n.State == ElementState.Error || n.State == ElementState.Warning).ToList(); + + if (errorNodes.Any()) + { + var details = string.Join("\n", errorNodes.Select(n => + { + var messages = n.NodeInfos + .Where(i => i.State == ElementState.Error || i.State == ElementState.Warning) + .Select(i => $" [{i.State}] {i.Message}"); + string msgBlock = messages.Any() + ? "\n" + string.Join("\n", messages) + : " (no message details)"; + return $" [{n.State}] {n.Name} ({n.GetType().Name}, GUID: {n.GUID}){msgBlock}"; + })); + + Assert.Fail( + $"After RunCurrentModel(), {errorNodes.Count} node(s) are in error/warning state " + + $"in '{scriptFileName}':\n{details}"); + } + } + + [Test, Category("SmokeTests")] + [TestModel(@".\empty.rfa")] + public void Revit_Geometry_Creation_Points() + { + OpenAndRunSample("Revit Geometry Creation Points.dyn"); + } + + [Test, Category("SmokeTests")] + [TestModel(@".\empty.rfa")] + public void Revit_Geometry_Creation_Curves() + { + OpenAndRunSample("Revit Geometry Creation Curves.dyn"); + } + + [Test, Category("SmokeTests")] + [TestModel(@".\MassWithBoxAndCone.rfa")] + public void Revit_Geometry_Creation_Solids() + { + OpenAndRunSample("Revit Geometry Creation Solids.dyn"); + } + + [Test, Category("SmokeTests")] + [TestModel(@".\MassWithBoxAndCone.rfa")] + public void Revit_Geometry_Creation_Surfaces() + { + OpenAndRunSample("Revit Geometry Creation Surfaces.dyn"); + } + + [Test, Category("SmokeTests")] + [TestModel(@".\AdaptiveComponents.rfa")] + public void Revit_Adaptive_Component_Placement() + { + OpenAndRunSample("Revit Adaptive Component Placement.dyn"); + } + + [Test, Category("SmokeTests")] + [TestModel(@".\SampleModel.rvt")] + public void Revit_Color() + { + OpenAndRunSample("Revit Color.dyn"); + } + + [Test, Category("SmokeTests")] + [TestModel(@".\SampleModel.rvt")] + public void Revit_Floors_and_Framing() + { + OpenAndRunSample("Revit Floors and Framing.dyn"); + } + + [Test, Category("SmokeTests")] + [TestModel(@".\SampleModel.rvt")] + public void Revit_Import_Solid() + { + OpenAndRunSample("Revit Import Solid.dyn"); + } + + [Test, Category("SmokeTests")] + [TestModel(@".\SampleModel.rvt")] + public void Revit_Place_Families_By_Level_Set_Parameters() + { + OpenAndRunSample("Revit Place Families By Level Set Parameters.dyn"); + } + + [Test, Category("SmokeTests")] + [TestModel(@".\StructuralFraming\StructuralFraming.rvt")] + public void Revit_Structural_Framing() + { + OpenAndRunSample("Revit Structural Framing.dyn"); + } + } +}