From 60e54edb8df98022fedbf89c00b24ff1d1607b2b Mon Sep 17 00:00:00 2001 From: Paine Zeng Date: Sat, 18 Jul 2026 21:38:07 +0800 Subject: [PATCH 1/9] Make the reliability repair scope reviewable Record the test-first task boundaries, COM safety constraints, and publication checks before production code changes begin. Constraint: Preserve .NET Framework 4.7.2 and add no project dependencies Confidence: high Scope-risk: narrow Directive: Execute each task with a failing regression test before implementation Tested: Baseline Release x64 build with XCadRegDll=false Not-tested: Plan-only commit contains no runtime behavior changes --- .gitignore | 5 +- ...7-18-solidworkslookup-reliability-fixes.md | 387 ++++++++++++++++++ 2 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/plans/2026-07-18-solidworkslookup-reliability-fixes.md diff --git a/.gitignore b/.gitignore index 8bd2635..d691a1c 100644 --- a/.gitignore +++ b/.gitignore @@ -355,4 +355,7 @@ MigrationBackup/ # ExceptionLessConfig -exceptionless.txt \ No newline at end of file +exceptionless.txt + +# Local agent workflow state +.superpowers/ diff --git a/docs/superpowers/plans/2026-07-18-solidworkslookup-reliability-fixes.md b/docs/superpowers/plans/2026-07-18-solidworkslookup-reliability-fixes.md new file mode 100644 index 0000000..56d475f --- /dev/null +++ b/docs/superpowers/plans/2026-07-18-solidworkslookup-reliability-fixes.md @@ -0,0 +1,387 @@ +# SolidWorksLookup Reliability Fixes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix the confirmed path-processing, COM lifecycle, exception-reporting, reflection, and packaging defects and publish the changes as a draft pull request against `develop`. + +**Architecture:** Add a dependency-free .NET Framework regression-test executable that exercises pure path topology, resource lifetime, configuration parsing, reflection, and object-array behavior without starting SolidWorks. Keep COM-facing changes local to existing UI/add-in classes, and extract helpers only for pure algorithms or `try/finally` resource boundaries. + +**Tech Stack:** C# 7.3, .NET Framework 4.7.2, WPF, SolidWorks interop, Xarial.XCad, MSBuild, PowerShell. + +## Global Constraints + +- Do not add NuGet or other project dependencies. +- Keep the production target at `.NET Framework 4.7.2` and `x64`. +- Tests must run without starting or connecting to SolidWorks. +- All build and test commands must set `XCadRegDll=false`; validation must not register the add-in. +- Preserve current public behavior except where this plan explicitly fixes a confirmed defect. +- Use minimal, locally readable C# changes; extract only pure algorithms and resource-lifetime boundaries. +- Every behavior change follows RED → GREEN and records the observed failing and passing command. +- Every commit follows the workspace Lore Commit Protocol. + +--- + +### Task 1: Dependency-free test runner and path reliability + +**Files:** +- Create: `tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj` +- Create: `tests/SldWorksLookup.RegressionTests/Program.cs` +- Create: `SldWorksLookup/PathSplit/SketchChainTopology.cs` +- Create: `SldWorksLookup/PathSplit/SegmentSamplingPlan.cs` +- Modify: `SldWorksLookup/PathSplit/SketchWrapper.cs` +- Modify: `SldWorksLookup/PathSplit/SketchSegmentWrapper.cs` +- Modify: `SldWorksLookup/PathSplit/SketchChain.cs` +- Modify: `SldWorksLookup/PathSplit/ExtensionMethods.cs` +- Modify: `SldWorksLookup/Helper/PathExportUtil.cs` +- Modify: `SldWorksLookup/Properties/AssemblyInfo.cs` +- Modify: `SldWorksLookup/SldWorksLookup.csproj` +- Modify: `SldWorksLookup.sln` + +**Interfaces:** +- Produces: `SketchChainTopology.Build(IList, Func, Func, Action, Func)` +- Produces: `SegmentSamplingPlan.Create(double segmentLength, double stepLength, double distanceToNextPoint)` +- Produces: executable `tests/SldWorksLookup.RegressionTests/bin/Release/net472/SldWorksLookup.RegressionTests.exe` + +- [ ] **Step 1: Create the regression runner and failing path tests** + +Create an SDK-style `net472` console project with only a `ProjectReference` to the add-in project. Add a tiny runner that executes named `Action` tests and returns `1` on any failure. + +The initial tests must include: + +```csharp +PathTopologyReturnsEveryDisconnectedSegment(); +PathTopologyReturnsClosedLoop(); +SamplingPlanCarriesSpacingAcrossShortSegment(); +SamplingPlanRejectsNonPositiveOrNonFiniteStep(); +``` + +Use a local test-only segment class: + +```csharp +private sealed class Segment +{ + public Segment(Point3D start, Point3D end) + { + Start = start; + End = end; + } + + public Point3D Start { get; private set; } + public Point3D End { get; private set; } + + public void Reverse() + { + var start = Start; + Start = End; + End = start; + } +} +``` + +- [ ] **Step 2: Run the tests and verify RED** + +Run: + +```powershell +& $msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj ` + /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo +``` + +Expected: compilation fails because `SketchChainTopology` and `SegmentSamplingPlan` do not exist. + +- [ ] **Step 3: Implement topology and sampling** + +`SketchChainTopology.Build` must: + +```csharp +var remaining = new List(segments); +while (remaining.Count > 0) +{ + // Prefer a segment with an open endpoint; if none exists, start a closed loop. + // Reverse the first segment only when its start is connected and its end is open. + // Repeatedly consume a segment connected to the current end, reversing it when needed. + // Add the completed chain, then continue until remaining is empty. +} +``` + +`SegmentSamplingPlan.Create` must: + +```csharp +if (stepLength <= 0 || double.IsNaN(stepLength) || double.IsInfinity(stepLength)) + throw new ArgumentOutOfRangeException(nameof(stepLength)); +if (segmentLength < 0 || double.IsNaN(segmentLength) || double.IsInfinity(segmentLength)) + throw new ArgumentOutOfRangeException(nameof(segmentLength)); + +if (distanceToNextPoint > segmentLength) + return new SegmentSamplingPlan(0, 0, distanceToNextPoint - segmentLength); + +var count = (int)Math.Floor((segmentLength - distanceToNextPoint) / stepLength) + 1; +var lastDistance = distanceToNextPoint + (count - 1) * stepLength; +var nextDistance = stepLength - (segmentLength - lastDistance); +return new SegmentSamplingPlan(count, distanceToNextPoint, nextDistance); +``` + +Replace `SketchWrapper.GetChains` index mutation with the topology helper. Replace `SketchSegmentWrapper.SplitCurve` division-based logic with the sampling plan and normalized parameter interpolation. `SketchChain.Split` must validate the step once and allow segments shorter than the step. + +Delete the duplicate endpoint switch in `PathExportUtil`; construct `SketchSegmentWrapper` and reuse its `SourceStartPoint` and `SourceEndPoint`. +Validate the active document, selected feature, sketch, segment array, and generated path array before dereferencing COM results. + +In `ExtensionMethods.GetSkeFeat`, inspect and yield `subfeat` inside the subfeature loop instead of repeatedly testing and yielding the parent `feat`. + +- [ ] **Step 4: Run tests and verify GREEN** + +Run the test project, then execute its output: + +```powershell +& $msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj ` + /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo +& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe +``` + +Expected: build exit `0`; four path tests pass. + +- [ ] **Step 5: Commit** + +Commit intent: `Preserve complete sketch paths during export`. + +--- + +### Task 2: COM lifetime and actionable exception reporting + +**Files:** +- Create: `SldWorksLookup/Helper/SelectionAccessScope.cs` +- Create: `SldWorksLookup/Helper/ExceptionUtil.cs` +- Modify: `tests/SldWorksLookup.RegressionTests/Program.cs` +- Modify: `SldWorksLookup/AddIn.cs` +- Modify: `SldWorksLookup/LogExtension.cs` +- Modify: `SldWorksLookup/Model/Value/LookupValue.cs` +- Modify: `SldWorksLookup/ViewModel/CaptureCmdViewModel.cs` +- Modify: `SldWorksLookup/View/CaptureCmd.xaml.cs` +- Modify: `SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs` +- Modify: `SldWorksLookup/SldWorksLookup.csproj` + +**Interfaces:** +- Produces: `SelectionAccessScope.Run(Func acquire, Action release, Action body)` +- Produces: `ExceptionUtil.GetUserMessage(Exception exception)` +- Produces: `LogExtension.TryReadConfiguration(string path, out string serverUrl, out string apiKey)` + +- [ ] **Step 1: Write failing lifetime, logging, and exception tests** + +Add tests that prove: + +```csharp +SelectionAccessScopeReleasesWhenBodyThrows(); +SelectionAccessScopeDoesNotRunBodyWhenAcquireFails(); +ExceptionUtilUnwrapsTargetInvocationException(); +LogConfigurationReadsTwoTrimmedValues(); +LogConfigurationRejectsMissingOrIncompleteFile(); +``` + +The release-on-throw test must catch the body exception and assert `releaseCount == 1`. Configuration tests must use a temporary file and delete it in `finally`. + +- [ ] **Step 2: Run tests and verify RED** + +Expected: compilation fails because the three new helper APIs do not exist. + +- [ ] **Step 3: Implement minimal lifetime and reporting fixes** + +`SelectionAccessScope.Run` must acquire once and always release in `finally`: + +```csharp +if (!acquire()) + throw new InvalidOperationException("Cannot access the feature selections."); +try +{ + body(); +} +finally +{ + release(); +} +``` + +Use it around `IAdvancedHoleFeatureData.AccessSelections` and `ReleaseSelectionAccess`. Check `GetDefinition()` and near-side element results before enumeration. + +`LogExtension.TryReadConfiguration` must return `false` for a missing file, fewer than two lines, or blank values. `LogStart` must use the helper and write initialization failures to `Debug` instead of using an empty catch. + +`CmdGroup_CommandClick` and `LookupValue.OpenClick` must show `ExceptionUtil.GetUserMessage(ex)` and submit telemetry only when a client exists. Add the missing `return` after “No active doc”. + +Track open `CaptureCmd` windows in `AddIn`; close them during `OnDisconnect`. Make `CaptureCmdViewModel` idempotently `IDisposable`, and call `Dispose()` from the window’s `Closed` handler. + +- [ ] **Step 4: Run tests and verify GREEN** + +Expected: all Task 1 and Task 2 tests pass; production build has no unused-exception warnings. + +- [ ] **Step 5: Commit** + +Commit intent: `Keep SolidWorks state recoverable when commands fail`. + +--- + +### Task 3: Reflection and COM-object browsing resilience + +**Files:** +- Modify: `tests/SldWorksLookup.RegressionTests/Program.cs` +- Modify: `SldWorksLookup/Helper/ObjectMatcherUtil.cs` +- Modify: `SldWorksLookup/Helper/TypeMatcherUtil.cs` +- Modify: `SldWorksLookup/Helper/TypeMatcherUtil.tt` +- Modify: `SldWorksLookup/Model/Instance/InstanceProperty.cs` +- Modify: `SldWorksLookup/Model/Instance/MethodInstanceProperty.cs` +- Modify: `SldWorksLookup/Model/Property/LookupParameterProperty.cs` +- Modify: `SldWorksLookup/Model/Value/LookupValue.cs` +- Modify: `SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs` +- Modify: `SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs` + +**Interfaces:** +- Consumes: the regression runner from Task 1 +- Produces: safe array inspection, accessor enumeration, parameter defaults, and runtime COM-class-to-interface matching + +- [ ] **Step 1: Write failing reflection tests** + +Add tests: + +```csharp +ValueArrayInspectionHandlesNullElements(); +PropertyBrowsingHandlesSetterOnlyProperty(); +ReferenceParametersDefaultToNull(); +ComClassWithInternalIMapsToInterface(); +``` + +Use a local class with a setter-only property for the property test. Assert: + +```csharp +ObjectMatcherUtil.IsValueArray(new object[] { null, 42 }) == true; +LookupParameterProperty.CreateInstace(typeof(string)) == null; +TypeMatcherUtil.Match(typeof(ImportDxfDwgDataClass)) == typeof(IImportDxfDwgData); +``` + +- [ ] **Step 2: Run tests and verify RED** + +Expected: at least the null-array, reference-default, setter-only, and internal-`I` mapping assertions fail. + +- [ ] **Step 3: Implement minimal reflection fixes** + +Use the first non-null array element for type checks and render null items as ``. Skip null elements when opening object arrays. + +In `InstanceProperty.GetProperties`, independently inspect `GetMethod` and `SetMethod`. A property without a getter must become a message-only property rather than calling `GetValue`. + +Return `null` for reference parameter defaults and allow null values for reference-type method parameters. Reject null only for non-nullable value types; validate non-null values against the effective parameter type, including by-ref element types. + +Before consulting the generated tuple list, `TypeMatcherUtil.Match` must resolve: + +```csharp +var interfaceType = sourceType.Assembly.GetType($"{sourceType.Namespace}.I{name}"); +if (interfaceType != null && interfaceType.IsInterface) + return interfaceType; +``` + +Update the T4 template to include only interfaces and strip only the leading `I`. + +Set `NodeStatus = NodeStatus.Ok` after feature/component lazy loading so repeated clicks do not duplicate children. + +- [ ] **Step 4: Run tests and verify GREEN** + +Expected: all regression tests pass. + +- [ ] **Step 5: Commit** + +Commit intent: `Keep reflection browsing usable across COM edge cases`. + +--- + +### Task 4: Reproducible and safer build/installer configuration + +**Files:** +- Create: `tests/Verify-ProjectConfiguration.ps1` +- Modify: `SldWorksLookup.sln` +- Modify: `SldWorksLookup/SldWorksLookup.csproj` +- Modify: `SldWorksLookup/Install.bat` +- Modify: `SldWorksLookup/UnInstall.bat` +- Modify: `Installer/SolidWorksLookup.aip` + +**Interfaces:** +- Produces: PowerShell configuration contract test +- Produces: solution `Release` mappings that build project `Release` +- Produces: builds that do not auto-register unless explicitly overridden + +- [ ] **Step 1: Write the failing configuration test** + +The script must read repository files and throw unless: + +```powershell +$solution -match 'Release\|Any CPU\.ActiveCfg = Release\|Any CPU' +$solution -match 'Release\|x64\.ActiveCfg = Release\|x64' +$project -match 'false' +$install -match 'if errorlevel 1 exit /b' +$uninstall -match 'if errorlevel 1 exit /b' +$installer -match 'AI_REQUIRED_DOTNET_VERSION".*4\.7\.2' +$installer -notmatch 'File="exceptionless\.txt"' +``` + +- [ ] **Step 2: Run the script and verify RED** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\Verify-ProjectConfiguration.ps1 +``` + +Expected: script exits non-zero on the current Release mapping and registration settings. + +- [ ] **Step 3: Fix configuration** + +Map both solution Release configurations to project Release. Set `false` in the project so compilation is side-effect free by default. + +Both batch scripts must quote paths, validate files, propagate `RegAsm` failure, and return zero only after success: + +```bat +@echo off +setlocal +cd /d "%~dp0" || exit /b 1 +if not exist "%~dp0RegAsm.exe" exit /b 2 +if not exist "%~dp0SldWorksLookup.dll" exit /b 3 +"%~dp0RegAsm.exe" "%~dp0SldWorksLookup.dll" /codebase +if errorlevel 1 exit /b %errorlevel% +exit /b 0 +``` + +Use `/u` in the uninstall variant. Change the installer minimum runtime to `4.7.2`, remove the `exceptionless.txt` file row, and exclude `exceptionless.txt`, `*.pdb`, and `*.xml` from synchronized `bin` content. + +- [ ] **Step 4: Verify GREEN and full integration** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\Verify-ProjectConfiguration.ps1 +& $msbuild .\SldWorksLookup.sln /t:Restore /p:RestorePackagesConfig=true /v:minimal /nologo +& $msbuild .\SldWorksLookup.sln /p:Configuration=Release '/p:Platform=Any CPU' /p:XCadRegDll=false /v:minimal /nologo +& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe +``` + +Expected: configuration test, solution build, and all regression tests exit `0`; no COM registration runs. + +- [ ] **Step 5: Commit** + +Commit intent: `Make release builds safe and reproducible`. + +--- + +### Task 5: Final verification and publication + +**Files:** +- Review all changed files from `origin/develop...HEAD` + +**Interfaces:** +- Produces: draft pull request targeting `weianweigan/SolidWorksLookup:develop` + +- [ ] **Step 1: Run fresh full verification** + +Run configuration tests, regression tests, `Release|Any CPU`, and `Release|x64` builds with `XCadRegDll=false`. Confirm `git status --short` contains only intended tracked changes before the final commit. + +- [ ] **Step 2: Run whole-branch code review** + +Review the complete diff for correctness, scope, exception safety, installer safety, and test adequacy. Resolve every Critical or Important finding and rerun covering tests. + +- [ ] **Step 3: Publish** + +Push `agent/fix-solidworkslookup-reliability` to an authenticated user fork or the upstream repository when write permission exists. Open a draft PR against `weianweigan/SolidWorksLookup:develop` describing root causes, behavior changes, validation commands, and SolidWorks runtime limitations. From a464555f3d0326aeadf668bd45de46d73e3a4e09 Mon Sep 17 00:00:00 2001 From: Paine Zeng Date: Sat, 18 Jul 2026 21:48:01 +0800 Subject: [PATCH 2/9] Preserve complete sketch paths during export Add a dependency-free regression runner for path topology and segment sampling, then route sketch-chain construction and sampling through small helpers so disconnected segments, closed loops, and short segments preserve path spacing. Constraint: No new dependencies; test/build commands pass XCadRegDll=false to avoid add-in registration Confidence: high Scope-risk: moderate Tested: MSBuild regression project Release with XCadRegDll=false; SldWorksLookup.RegressionTests.exe four-test runner; git diff --check Not-tested: Manual SolidWorks COM export workflow Co-authored-by: OmX --- .superpowers/sdd/task-1-report.md | 111 ++++++++++++ SldWorksLookup.sln | 10 ++ SldWorksLookup/Helper/PathExportUtil.cs | 121 ++++++------- SldWorksLookup/PathSplit/ExtensionMethods.cs | 4 +- .../PathSplit/SegmentSamplingPlan.cs | 36 ++++ SldWorksLookup/PathSplit/SketchChain.cs | 5 +- .../PathSplit/SketchChainTopology.cs | 135 ++++++++++++++ .../PathSplit/SketchSegmentWrapper.cs | 63 ++----- SldWorksLookup/PathSplit/SketchWrapper.cs | 94 ++-------- SldWorksLookup/Properties/AssemblyInfo.cs | 1 + SldWorksLookup/SldWorksLookup.csproj | 4 +- .../SldWorksLookup.RegressionTests/Program.cs | 168 ++++++++++++++++++ .../SldWorksLookup.RegressionTests.csproj | 14 ++ 13 files changed, 557 insertions(+), 209 deletions(-) create mode 100644 .superpowers/sdd/task-1-report.md create mode 100644 SldWorksLookup/PathSplit/SegmentSamplingPlan.cs create mode 100644 SldWorksLookup/PathSplit/SketchChainTopology.cs create mode 100644 tests/SldWorksLookup.RegressionTests/Program.cs create mode 100644 tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj diff --git a/.superpowers/sdd/task-1-report.md b/.superpowers/sdd/task-1-report.md new file mode 100644 index 0000000..0df5f5e --- /dev/null +++ b/.superpowers/sdd/task-1-report.md @@ -0,0 +1,111 @@ +# Task 1 Report: Dependency-free test runner and path reliability + +## Status + +Complete. + +## Files + +- Created `tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj` +- Created `tests/SldWorksLookup.RegressionTests/Program.cs` +- Created `SldWorksLookup/PathSplit/SketchChainTopology.cs` +- Created `SldWorksLookup/PathSplit/SegmentSamplingPlan.cs` +- Modified `SldWorksLookup/PathSplit/SketchWrapper.cs` +- Modified `SldWorksLookup/PathSplit/SketchSegmentWrapper.cs` +- Modified `SldWorksLookup/PathSplit/SketchChain.cs` +- Modified `SldWorksLookup/PathSplit/ExtensionMethods.cs` +- Modified `SldWorksLookup/Helper/PathExportUtil.cs` +- Modified `SldWorksLookup/Properties/AssemblyInfo.cs` +- Modified `SldWorksLookup/SldWorksLookup.csproj` +- Modified `SldWorksLookup.sln` + +## RED + +Initial command: + +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo +``` + +Initial result: failed before compilation because the SDK-style project needed a restore-generated `project.assets.json`. + +Restore prerequisite: + +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /t:Restore /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo +``` + +Restore output: + +```text +正在确定要还原的项目… +已还原 ...\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj +``` + +Correct RED command: + +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo +``` + +Correct RED output excerpt: + +```text +SldWorksLookup -> ...\SldWorksLookup\bin\Release\SldWorksLookup.dll +Program.cs(75,24): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” +Program.cs(84,61): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” +Program.cs(85,61): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” +Program.cs(86,61): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” +Program.cs(87,61): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” +Program.cs(92,20): error CS0103: 当前上下文中不存在名称“SketchChainTopology” +``` + +The RED failure was expected because the tests referenced the missing topology and sampling helpers. + +## GREEN + +Build command: + +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo +``` + +Build output: + +```text +SldWorksLookup -> ...\SldWorksLookup\bin\Release\SldWorksLookup.dll +SldWorksLookup.RegressionTests -> ...\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe +``` + +Runner command: + +```powershell +& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe +``` + +Runner output: + +```text +PASS PathTopologyReturnsEveryDisconnectedSegment +PASS PathTopologyReturnsClosedLoop +PASS SamplingPlanCarriesSpacingAcrossShortSegment +PASS SamplingPlanRejectsNonPositiveOrNonFiniteStep +``` + +## Self-review + +- `SketchChainTopology.Build` consumes all disconnected chains and closed-loop chains without mutating the source list. +- `SegmentSamplingPlan.Create` follows the requested spacing carry formula and rejects non-positive or non-finite step lengths. +- `SketchWrapper.GetChains` now delegates ordering/reversal to the topology helper. +- `SketchSegmentWrapper.SplitCurve` uses distance-based sampling and normalized curve parameter interpolation, avoiding divide-by-zero behavior from the old spare-length logic. +- `SketchChain.Split` validates step length once and no longer rejects short segments. +- `PathExportUtil` reuses `SketchSegmentWrapper.SourceStartPoint` and `SourceEndPoint`, removing duplicated endpoint switching logic. +- COM dereferences in `PathExportUtil` are guarded for active document, selection, sketch, sketch segments, generated paths, path segments, and segment curves. +- `ExtensionMethods.GetSkeFeat` now inspects and yields `subfeat` inside the subfeature loop. +- `git diff --check` exits 0; warnings only report LF-to-CRLF normalization. +- No temporary debug code was added. The only `Console.WriteLine` calls are the dependency-free test runner output. + +## Concerns + +- Build still emits pre-existing CS0168 warnings in `SldWorksLookup/AddIn.cs` and `SldWorksLookup/LogExtension.cs`; those files are outside Task 1 ownership and were not changed. +- The SolidWorks COM export path was build-verified but not manually exercised in SolidWorks. diff --git a/SldWorksLookup.sln b/SldWorksLookup.sln index 8c90cd7..5dcec7c 100644 --- a/SldWorksLookup.sln +++ b/SldWorksLookup.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.29613.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SldWorksLookup", "SldWorksLookup\SldWorksLookup.csproj", "{94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SldWorksLookup.RegressionTests", "tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj", "{4F5C2952-0D72-4F69-9294-9C8E5764086D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,14 @@ Global {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|Any CPU.Build.0 = Debug|Any CPU {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.ActiveCfg = Debug|x64 {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.Build.0 = Debug|x64 + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|x64.Build.0 = Debug|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|Any CPU.Build.0 = Release|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|x64.ActiveCfg = Release|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SldWorksLookup/Helper/PathExportUtil.cs b/SldWorksLookup/Helper/PathExportUtil.cs index e14a6c0..e46c199 100644 --- a/SldWorksLookup/Helper/PathExportUtil.cs +++ b/SldWorksLookup/Helper/PathExportUtil.cs @@ -1,11 +1,10 @@ -using Microsoft.VisualBasic; +using Microsoft.VisualBasic; +using SldWorksLookup.PathSplit; using SolidWorks.Interop.sldworks; using SolidWorks.Interop.swconst; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Media.Media3D; namespace SldWorksLookup.Helper @@ -14,17 +13,36 @@ public static class PathExportUtil { public static void Export(SolidWorks.Interop.sldworks.ISldWorks sw) { + if (sw == null) + throw new ArgumentNullException(nameof(sw)); + var modeler = sw.GetModeler() as IModeler; + if (modeler == null) + throw new InvalidOperationException("Cannot get SolidWorks modeler."); var doc = sw.IActiveDoc2; + if (doc == null) + throw new InvalidOperationException("No active document."); + + var selectionManager = doc.ISelectionManager; + if (selectionManager == null) + throw new InvalidOperationException("Cannot get selection manager."); - var feat = doc.ISelectionManager.GetSelectedObject6(1, -1) as IFeature; + var feat = selectionManager.GetSelectedObject6(1, -1) as IFeature; + if (feat == null) + throw new InvalidOperationException("Select a sketch feature before exporting."); var ske = feat.GetSpecificFeature2() as ISketch; + if (ske == null) + throw new InvalidOperationException("Selected feature is not a sketch."); doc.EditSketch(); - var ses = (ske.GetSketchSegments() as object[]).Cast().ToList(); + var sketchSegmentArray = ske.GetSketchSegments() as object[]; + if (sketchSegmentArray == null || sketchSegmentArray.Length == 0) + throw new InvalidOperationException("Selected sketch has no sketch segments."); + + var ses = sketchSegmentArray.Cast().ToList(); doc.ClearSelection2(true); @@ -36,96 +54,58 @@ public static void Export(SolidWorks.Interop.sldworks.ISldWorks sw) doc.SketchManager.MakeSketchChain(); doc.ClearSelection2(true); - var path = (ske.GetSketchPaths() as object[]).Cast().First(); - - var segs = (path.GetSketchSegments() as object[]).Cast(); + var pathArray = ske.GetSketchPaths() as object[]; + if (pathArray == null || pathArray.Length == 0) + throw new InvalidOperationException("No sketch path was generated."); + + var path = pathArray.Cast().FirstOrDefault(); + if (path == null) + throw new InvalidOperationException("Generated sketch path is invalid."); + + var pathSegmentArray = path.GetSketchSegments() as object[]; + if (pathSegmentArray == null || pathSegmentArray.Length == 0) + throw new InvalidOperationException("Generated sketch path has no segments."); + + var segs = pathSegmentArray.Cast(); ICurve curve = null; foreach (var seg in segs) { var seCurve = seg.GetCurve() as ICurve; + if (seCurve == null) + throw new InvalidOperationException("Cannot get sketch segment curve."); - //剪裁曲线 - GetSpAndEp(seg,out Point3D sp,out Point3D ep); + var wrapper = new SketchSegmentWrapper(seg); + var sp = wrapper.SourceStartPoint; + var ep = wrapper.SourceEndPoint; seCurve = seCurve.CreateTrimmedCurve2(sp.X, sp.Y, sp.Z, ep.X, ep.Y, ep.Z); var body = seCurve.CreateWireBody(); body.Display2(doc as PartDoc, Information.RGB(255, 0, 0), (int)swTempBodySelectOptions_e.swTempBodySelectOptionNone); - if (curve == null) - { - curve = seCurve; - } - else - { - curve = modeler.MergeCurves(new object[] { curve, seCurve }); - } + curve = curve == null + ? seCurve + : modeler.MergeCurves(new object[] { curve, seCurve }); } + if (curve == null) + throw new InvalidOperationException("Cannot create a merged curve from the generated sketch path."); + doc.InsertSketch(); - var points = SplitCurve(curve,10); + var points = SplitCurve(curve, 10); doc.Insert3DSketch(); var ske3D = doc.SketchManager.ActiveSketch; foreach (var point in points) { - doc.SketchManager.CreatePoint(point.X,point.Y,point.Z); - - } - } - - private static void GetSpAndEp(ISketchSegment seg, out Point3D sp, out Point3D ep) - { - sp = default;ep = default; - switch ((swSketchSegments_e)seg.GetType()) - { - case swSketchSegments_e.swSketchLINE: - var line = seg as ISketchLine; - sp = (line.GetStartPoint2() as ISketchPoint).ToPoint(); - ep = (line.GetEndPoint2() as ISketchPoint).ToPoint(); - break; - case swSketchSegments_e.swSketchARC: - var arc = seg as ISketchArc; - sp = (arc.GetStartPoint2() as ISketchPoint).ToPoint(); - ep = (arc.GetStartPoint2() as ISketchPoint).ToPoint(); - break; - case swSketchSegments_e.swSketchELLIPSE: - var eli= seg as ISketchEllipse; - sp = (eli.GetStartPoint2() as ISketchPoint).ToPoint(); - ep = (eli.GetStartPoint2() as ISketchPoint).ToPoint(); - break; - case swSketchSegments_e.swSketchSPLINE: - var spline = seg as SketchSpline; - var points = (spline.GetPoints2() as object[]).Cast().ToList(); - sp = new Point3D(points[0].X, points[0].Y, points[0].Z); - ep = new Point3D(points[points.Count-1].X, points[points.Count - 1].Y, points[points.Count - 1].Z); - break; - case swSketchSegments_e.swSketchTEXT: - throw new NotSupportedException(); - case swSketchSegments_e.swSketchPARABOLA: - var para = seg as SketchParabola; - sp = (para.GetStartPoint2() as ISketchPoint).ToPoint(); - ep = (para.GetStartPoint2() as ISketchPoint).ToPoint(); - break; - default: - throw new NotSupportedException(); + doc.SketchManager.CreatePoint(point.X, point.Y, point.Z); } } - public static Point3D ToPoint(this double[] point) - { - return new Point3D(point[0], point[1], point[2]); - } - - public static Point3D ToPoint(this ISketchPoint skePoint) - { - return new Point3D(skePoint.X, skePoint.Y, skePoint.Z); - } - - public static List SplitCurve(ICurve curve,int num) + public static List SplitCurve(ICurve curve, int num) { var points = new List(); @@ -142,5 +122,4 @@ public static List SplitCurve(ICurve curve,int num) return points; } } - } diff --git a/SldWorksLookup/PathSplit/ExtensionMethods.cs b/SldWorksLookup/PathSplit/ExtensionMethods.cs index 7f02828..2045bbd 100644 --- a/SldWorksLookup/PathSplit/ExtensionMethods.cs +++ b/SldWorksLookup/PathSplit/ExtensionMethods.cs @@ -39,9 +39,9 @@ public static IEnumerable> GetSkeFeat(this IComponen var subFeats = feat.GetSubFeats(); foreach (var subfeat in subFeats) { - if (feat.GetTypeName2() == "ProfileFeature") + if (subfeat.GetTypeName2() == "ProfileFeature") { - yield return new Tuple(feat,comp); + yield return new Tuple(subfeat,comp); } } if (feat.GetTypeName2() == "ProfileFeature") diff --git a/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs b/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs new file mode 100644 index 0000000..27df90e --- /dev/null +++ b/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs @@ -0,0 +1,36 @@ +using System; + +namespace SldWorksLookup.PathSplit +{ + public sealed class SegmentSamplingPlan + { + private SegmentSamplingPlan(int pointCount, double firstDistance, double distanceToNextPoint) + { + PointCount = pointCount; + FirstDistance = firstDistance; + DistanceToNextPoint = distanceToNextPoint; + } + + public int PointCount { get; } + + public double FirstDistance { get; } + + public double DistanceToNextPoint { get; } + + public static SegmentSamplingPlan Create(double segmentLength, double stepLength, double distanceToNextPoint) + { + if (stepLength <= 0 || double.IsNaN(stepLength) || double.IsInfinity(stepLength)) + throw new ArgumentOutOfRangeException(nameof(stepLength)); + if (segmentLength < 0 || double.IsNaN(segmentLength) || double.IsInfinity(segmentLength)) + throw new ArgumentOutOfRangeException(nameof(segmentLength)); + + if (distanceToNextPoint > segmentLength) + return new SegmentSamplingPlan(0, 0, distanceToNextPoint - segmentLength); + + var count = (int)Math.Floor((segmentLength - distanceToNextPoint) / stepLength) + 1; + var lastDistance = distanceToNextPoint + (count - 1) * stepLength; + var nextDistance = stepLength - (segmentLength - lastDistance); + return new SegmentSamplingPlan(count, distanceToNextPoint, nextDistance); + } + } +} diff --git a/SldWorksLookup/PathSplit/SketchChain.cs b/SldWorksLookup/PathSplit/SketchChain.cs index c7b1edc..4383921 100644 --- a/SldWorksLookup/PathSplit/SketchChain.cs +++ b/SldWorksLookup/PathSplit/SketchChain.cs @@ -71,6 +71,9 @@ public double GetLength() /// public List Split(double stepLength) { + if (stepLength <= 0 || double.IsNaN(stepLength) || double.IsInfinity(stepLength)) + throw new ArgumentOutOfRangeException(nameof(stepLength)); + //初始化列表 List points = new List(); @@ -78,8 +81,6 @@ public List Split(double stepLength) double spareLength = 0; foreach (var seg in Segs) { - var length = seg.Segment.GetLength(); - //是否有上根线段的剩余长度 var curvePts = seg.SplitSegment(stepLength, spareLength,out var nextSpareLength); diff --git a/SldWorksLookup/PathSplit/SketchChainTopology.cs b/SldWorksLookup/PathSplit/SketchChainTopology.cs new file mode 100644 index 0000000..12b74fe --- /dev/null +++ b/SldWorksLookup/PathSplit/SketchChainTopology.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Media.Media3D; + +namespace SldWorksLookup.PathSplit +{ + public static class SketchChainTopology + { + public static List> Build( + IList segments, + Func getStartPoint, + Func getEndPoint, + Action reverse, + Func pointsEqual) + { + if (segments == null) + throw new ArgumentNullException(nameof(segments)); + if (getStartPoint == null) + throw new ArgumentNullException(nameof(getStartPoint)); + if (getEndPoint == null) + throw new ArgumentNullException(nameof(getEndPoint)); + if (reverse == null) + throw new ArgumentNullException(nameof(reverse)); + if (pointsEqual == null) + throw new ArgumentNullException(nameof(pointsEqual)); + + var chains = new List>(); + var remaining = new List(segments); + + while (remaining.Count > 0) + { + var firstIndex = FindOpenEndpointSegmentIndex(remaining, getStartPoint, getEndPoint, pointsEqual); + if (firstIndex < 0) + firstIndex = 0; + + var first = remaining[firstIndex]; + var startConnected = IsPointConnected(first, getStartPoint(first), remaining, getStartPoint, getEndPoint, pointsEqual); + var endConnected = IsPointConnected(first, getEndPoint(first), remaining, getStartPoint, getEndPoint, pointsEqual); + + if (startConnected && !endConnected) + reverse(first); + + remaining.RemoveAt(firstIndex); + + var chain = new List { first }; + ConsumeConnectedSegments(chain, remaining, getStartPoint, getEndPoint, reverse, pointsEqual); + chains.Add(chain); + } + + return chains; + } + + private static int FindOpenEndpointSegmentIndex( + List segments, + Func getStartPoint, + Func getEndPoint, + Func pointsEqual) + { + for (var i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + var startConnected = IsPointConnected(segment, getStartPoint(segment), segments, getStartPoint, getEndPoint, pointsEqual); + var endConnected = IsPointConnected(segment, getEndPoint(segment), segments, getStartPoint, getEndPoint, pointsEqual); + + if (!startConnected || !endConnected) + return i; + } + + return -1; + } + + private static bool IsPointConnected( + T source, + Point3D point, + List segments, + Func getStartPoint, + Func getEndPoint, + Func pointsEqual) + { + foreach (var segment in segments) + { + if (ReferenceEquals(source, segment)) + continue; + + if (pointsEqual(point, getStartPoint(segment)) || pointsEqual(point, getEndPoint(segment))) + return true; + } + + return false; + } + + private static void ConsumeConnectedSegments( + List chain, + List remaining, + Func getStartPoint, + Func getEndPoint, + Action reverse, + Func pointsEqual) + { + while (remaining.Count > 0) + { + var currentEnd = getEndPoint(chain.Last()); + var nextIndex = -1; + var reverseNext = false; + + for (var i = 0; i < remaining.Count; i++) + { + if (pointsEqual(currentEnd, getStartPoint(remaining[i]))) + { + nextIndex = i; + break; + } + + if (pointsEqual(currentEnd, getEndPoint(remaining[i]))) + { + nextIndex = i; + reverseNext = true; + break; + } + } + + if (nextIndex < 0) + return; + + var next = remaining[nextIndex]; + if (reverseNext) + reverse(next); + + remaining.RemoveAt(nextIndex); + chain.Add(next); + } + } + } +} diff --git a/SldWorksLookup/PathSplit/SketchSegmentWrapper.cs b/SldWorksLookup/PathSplit/SketchSegmentWrapper.cs index 412c63f..eb89d8b 100644 --- a/SldWorksLookup/PathSplit/SketchSegmentWrapper.cs +++ b/SldWorksLookup/PathSplit/SketchSegmentWrapper.cs @@ -104,11 +104,6 @@ public bool IsEndConnected(SketchSegmentWrapper segment) /// 分割后的点 public List SplitSegment(double stepLength, double spareLength ,out double newSpareLength) { - if (stepLength > GetLength()) - { - throw new InvalidOperationException($"步长:{stepLength} 大于 当前草图元素长度"); - } - //获取草图线段曲线 var seCurve = Segment.GetCurve() as ICurve; @@ -131,61 +126,23 @@ private List SplitCurve(ICurve curve, double stepLength, double spareLe { //初始化点几何 var points = new List(); + var length = GetLength(); + var plan = SegmentSamplingPlan.Create(length, stepLength, spareLength); //获取曲线元素 curve.GetEndParams(out var startParam, out var endParam, out bool isClosed, out bool isPeriodic); - if (ReverseSpAndEp) - { - //数量,取整了 - int num = (int)((GetLength() - spareLength )/ stepLength); - - //步长参数 - var incr = (endParam - startParam) / (num); - - for (int i = 0; i < num; i++) - { - var param = curve.Evaluate(startParam + i * incr) as double[]; - points.Add(new Point3D(param[0], param[1], param[2])); - } - - if (spareLength < ExtensionMethods.Eplision) - { - //第一个点参数 - var lastParam = endParam - ((endParam - startParam) / (GetLength() / spareLength)); - var lastPoint = curve.Evaluate(lastParam) as double[]; - points.Add(lastPoint.ToPoint()); - } - - //逆序 - points.Reverse(); - - newSpareLength = GetLength() - spareLength - stepLength * num; - } - else + for (var i = 0; i < plan.PointCount; i++) { - //第一个点参数 - var firstParam = ((endParam - startParam) / (GetLength() / spareLength)) + startParam; - - //数量,取整了 - int num = (int)(GetLength() / stepLength); - - //步长参数 - var incr = (endParam - firstParam) / (num); - - for (int i = 0; i < num; i++) - { - var param = curve.Evaluate(firstParam + i * incr) as double[]; - points.Add(new Point3D(param[0], param[1], param[2])); - } - - newSpareLength = GetLength() - spareLength - stepLength * num; + var distance = plan.FirstDistance + i * stepLength; + var ratio = length < ExtensionMethods.Eplision ? 0 : distance / length; + var curveRatio = ReverseSpAndEp ? 1 - ratio : ratio; + var curveParameter = startParam + (endParam - startParam) * curveRatio; + var point = curve.Evaluate(curveParameter) as double[]; + points.Add(point.ToPoint()); } - if (newSpareLength < stepLength) - { - newSpareLength = stepLength - newSpareLength; - } + newSpareLength = plan.DistanceToNextPoint; return points; } diff --git a/SldWorksLookup/PathSplit/SketchWrapper.cs b/SldWorksLookup/PathSplit/SketchWrapper.cs index dded0ad..1ea5bb5 100644 --- a/SldWorksLookup/PathSplit/SketchWrapper.cs +++ b/SldWorksLookup/PathSplit/SketchWrapper.cs @@ -31,65 +31,25 @@ public SketchWrapper(IFeature feat, IComponent2 comp) #region Public Methods public IEnumerable GetChains() { - var ses = (_sketch.GetSketchSegments() as object[]) + var sketchSegments = _sketch.GetSketchSegments() as object[]; + if (sketchSegments == null) + yield break; + + var ses = sketchSegments .Cast() .Select(p => new SketchSegmentWrapper(p)) .ToList(); - //挑选出起点 - for (int i = 0; i < ses.Count; i++) - { - //判断当前直线是否可以作为路径其实直线 - bool startConnected = false; - bool endConnected = false; - for (int j = 0; j < ses.Count; j++) - { - if (i == j) - continue; - - if(!startConnected) - startConnected = ses[i].IsStartConnected(ses[j]); - if (!endConnected) - endConnected = ses[i].IsEndConnected(ses[j]); - - if (startConnected && endConnected) - break; - } - - if (!startConnected || !endConnected) - { - //起点和其他直线相连,交换起点和终点 - if (startConnected) - ses[i].ReverseSpAndEp = true; - - var chainSes = new List() { ses[i] }; - ses.RemoveAt(i); - - //构建新列表 - var newSes = new List(); - newSes.AddRange(ses); - - //寻找其他相连直线 - var next = chainSes.Last(); - - //递归查找 - while (next != null) - { - next = SearchNext(chainSes, next, newSes); - } + var chains = SketchChainTopology.Build( + ses, + segment => segment.StartPoint, + segment => segment.EndPoint, + segment => segment.ReverseSpAndEp = !segment.ReverseSpAndEp, + (left, right) => left.ValueEqual(right)); - //从现有列表中剔除 - foreach (var usedSe in chainSes) - { - ses.Remove(usedSe); - } - - //重新查找 - i = 0; - - //返回草图链条 - yield return new SketchChain(_sketch,chainSes,_comp); - } + foreach (var chain in chains) + { + yield return new SketchChain(_sketch, chain, _comp); } } @@ -99,31 +59,5 @@ public override string ToString() } #endregion - #region Private Methods - private static SketchSegmentWrapper SearchNext(List chainSes, SketchSegmentWrapper current, List newSes) - { - var next = default(SketchSegmentWrapper); - for (int k = 0; k < newSes.Count; k++) - { - if (current.EndPoint.ValueEqual(newSes[k].StartPoint)) - { - chainSes.Add(newSes[k]); - newSes.RemoveAt(k--); - next = chainSes.Last(); - break; - } - else if (current.EndPoint.ValueEqual(newSes[k].EndPoint)) - { - newSes[k].ReverseSpAndEp = true; - chainSes.Add(newSes[k]); - newSes.RemoveAt(k--); - next = chainSes.Last(); - break; - } - } - return next; - } - #endregion - } } diff --git a/SldWorksLookup/Properties/AssemblyInfo.cs b/SldWorksLookup/Properties/AssemblyInfo.cs index 8b6feeb..b8d3204 100644 --- a/SldWorksLookup/Properties/AssemblyInfo.cs +++ b/SldWorksLookup/Properties/AssemblyInfo.cs @@ -13,6 +13,7 @@ [assembly: AssemblyCopyright("Copyright © 2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] +[assembly: InternalsVisibleTo("SldWorksLookup.RegressionTests")] // 将 ComVisible 设置为 false 会使此程序集中的类型 //对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 diff --git a/SldWorksLookup/SldWorksLookup.csproj b/SldWorksLookup/SldWorksLookup.csproj index b123393..6dd0471 100644 --- a/SldWorksLookup/SldWorksLookup.csproj +++ b/SldWorksLookup/SldWorksLookup.csproj @@ -216,6 +216,8 @@ + + @@ -337,4 +339,4 @@ - \ No newline at end of file + diff --git a/tests/SldWorksLookup.RegressionTests/Program.cs b/tests/SldWorksLookup.RegressionTests/Program.cs new file mode 100644 index 0000000..2541fa0 --- /dev/null +++ b/tests/SldWorksLookup.RegressionTests/Program.cs @@ -0,0 +1,168 @@ +using SldWorksLookup.PathSplit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Media.Media3D; + +namespace SldWorksLookup.RegressionTests +{ + internal static class Program + { + private static int Main() + { + var tests = new Action[] + { + PathTopologyReturnsEveryDisconnectedSegment, + PathTopologyReturnsClosedLoop, + SamplingPlanCarriesSpacingAcrossShortSegment, + SamplingPlanRejectsNonPositiveOrNonFiniteStep + }; + + var failed = 0; + foreach (var test in tests) + { + try + { + test(); + Console.WriteLine("PASS " + test.Method.Name); + } + catch (Exception ex) + { + failed++; + Console.WriteLine("FAIL " + test.Method.Name); + Console.WriteLine(ex.GetType().Name + ": " + ex.Message); + } + } + + return failed == 0 ? 0 : 1; + } + + private static void PathTopologyReturnsEveryDisconnectedSegment() + { + var segments = new List + { + new Segment(Point(0, 0), Point(1, 0)), + new Segment(Point(2, 0), Point(3, 0)) + }; + + var chains = BuildChains(segments); + + AssertEqual(2, chains.Count, "Disconnected segment count"); + AssertEqual(1, chains[0].Count, "First chain segment count"); + AssertEqual(1, chains[1].Count, "Second chain segment count"); + AssertSame(segments[0], chains[0][0], "First chain segment"); + AssertSame(segments[1], chains[1][0], "Second chain segment"); + } + + private static void PathTopologyReturnsClosedLoop() + { + var segments = new List + { + new Segment(Point(0, 0), Point(1, 0)), + new Segment(Point(1, 0), Point(1, 1)), + new Segment(Point(1, 1), Point(0, 0)) + }; + + var chains = BuildChains(segments); + + AssertEqual(1, chains.Count, "Closed loop chain count"); + AssertEqual(3, chains[0].Count, "Closed loop segment count"); + AssertPointEqual(chains[0][0].Start, chains[0][2].End, "Closed loop endpoints"); + } + + private static void SamplingPlanCarriesSpacingAcrossShortSegment() + { + var plan = SegmentSamplingPlan.Create(0.5, 1.0, 0.75); + + AssertEqual(0, plan.PointCount, "Point count"); + AssertClose(0, plan.FirstDistance, "First distance"); + AssertClose(0.25, plan.DistanceToNextPoint, "Distance to next point"); + } + + private static void SamplingPlanRejectsNonPositiveOrNonFiniteStep() + { + AssertThrows(() => SegmentSamplingPlan.Create(1.0, 0, 0), "Zero step"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, -1.0, 0), "Negative step"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, double.NaN, 0), "NaN step"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, double.PositiveInfinity, 0), "Infinite step"); + } + + private static List> BuildChains(List segments) + { + return SketchChainTopology.Build( + segments, + segment => segment.Start, + segment => segment.End, + segment => segment.Reverse(), + (left, right) => left.ValueEqual(right)).ToList(); + } + + private static Point3D Point(double x, double y) + { + return new Point3D(x, y, 0); + } + + private static void AssertEqual(int expected, int actual, string message) + { + if (expected != actual) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static void AssertSame(object expected, object actual, string message) + { + if (!ReferenceEquals(expected, actual)) + throw new InvalidOperationException(message + ". Expected same instance."); + } + + private static void AssertPointEqual(Point3D expected, Point3D actual, string message) + { + if (!expected.ValueEqual(actual)) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static void AssertClose(double expected, double actual, string message) + { + if (Math.Abs(expected - actual) > ExtensionMethods.Eplision) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static void AssertThrows(Action action, string message) + where TException : Exception + { + try + { + action(); + } + catch (TException) + { + return; + } + catch (Exception ex) + { + throw new InvalidOperationException(message + ". Expected " + typeof(TException).Name + ", got " + ex.GetType().Name + "."); + } + + throw new InvalidOperationException(message + ". Expected " + typeof(TException).Name + "."); + } + + private sealed class Segment + { + public Segment(Point3D start, Point3D end) + { + Start = start; + End = end; + } + + public Point3D Start { get; private set; } + + public Point3D End { get; private set; } + + public void Reverse() + { + var start = Start; + Start = End; + End = start; + } + } + } +} diff --git a/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj b/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj new file mode 100644 index 0000000..f92108c --- /dev/null +++ b/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj @@ -0,0 +1,14 @@ + + + Exe + net472 + 7.3 + + + + + + + + + From 27a4e8f09f8d2a0bd8a32400852898e6d6cc6b21 Mon Sep 17 00:00:00 2001 From: Paine Zeng Date: Sat, 18 Jul 2026 21:56:38 +0800 Subject: [PATCH 3/9] Fail fast on incomplete path export curves Add review-driven regression coverage for path direction, spacing boundaries, and merge failure handling, then guard each COM-produced export value before it can be reused. Constraint: No new dependencies; append-only fix after CHANGES_REQUESTED; all verification uses XCadRegDll=false Confidence: high Scope-risk: narrow Tested: MSBuild regression project Release with XCadRegDll=false; SldWorksLookup.RegressionTests.exe 11-test runner; git diff --check Not-tested: Manual SolidWorks COM export workflow Co-authored-by: OmX --- .superpowers/sdd/task-1-report.md | 82 ++++++++++++ SldWorksLookup/Helper/PathExportUtil.cs | 45 ++++++- .../PathSplit/SketchChainTopology.cs | 4 + .../SldWorksLookup.RegressionTests/Program.cs | 124 +++++++++++++++++- 4 files changed, 246 insertions(+), 9 deletions(-) diff --git a/.superpowers/sdd/task-1-report.md b/.superpowers/sdd/task-1-report.md index 0df5f5e..48d0421 100644 --- a/.superpowers/sdd/task-1-report.md +++ b/.superpowers/sdd/task-1-report.md @@ -109,3 +109,85 @@ PASS SamplingPlanRejectsNonPositiveOrNonFiniteStep - Build still emits pre-existing CS0168 warnings in `SldWorksLookup/AddIn.cs` and `SldWorksLookup/LogExtension.cs`; those files are outside Task 1 ownership and were not changed. - The SolidWorks COM export path was build-verified but not manually exercised in SolidWorks. + +## CHANGES_REQUESTED Follow-up + +### Follow-up RED + +Added tests for: + +- first segment reversal +- connected successor reversal +- per-segment continuity +- preserving original input list membership/order +- normal sampling +- `distanceToNextPoint == segmentLength` +- immediate contextual export merge failure + +Command: + +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo +``` + +RED output excerpt: + +```text +SldWorksLookup -> ...\SldWorksLookup\bin\Release\SldWorksLookup.dll +Program.cs(185,40): error CS0117: “PathExportUtil”未包含“MergeCurveOrThrow”的定义 +Program.cs(189,38): error CS0117: “PathExportUtil”未包含“MergeCurveOrThrow”的定义 +``` + +The RED failure was expected because the new dependency-free export state helper did not exist. + +### Follow-up GREEN + +Build command: + +```powershell +& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo +``` + +Build output excerpt: + +```text +SldWorksLookup -> ...\SldWorksLookup\bin\Release\SldWorksLookup.dll +SldWorksLookup.RegressionTests -> ...\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe +``` + +Runner command: + +```powershell +& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe +``` + +Runner output: + +```text +PASS PathTopologyReturnsEveryDisconnectedSegment +PASS PathTopologyReturnsClosedLoop +PASS PathTopologyReversesOpenFirstSegment +PASS PathTopologyReversesConnectedSuccessor +PASS PathTopologyKeepsEveryStepContinuous +PASS PathTopologyDoesNotReorderOrRemoveInputSegments +PASS SamplingPlanCarriesSpacingAcrossShortSegment +PASS SamplingPlanRejectsNonPositiveOrNonFiniteStep +PASS SamplingPlanReturnsNormalSpacing +PASS SamplingPlanHandlesExactCarryBoundary +PASS ExportMergeFailureThrowsContext +``` + +Additional verification: + +```text +git diff --check +``` + +Result: exit 0; warnings only report LF-to-CRLF normalization. + +### Follow-up Self-review + +- `PathExportUtil` now guards the result of `CreateTrimmedCurve2`, `CreateWireBody`, `doc as PartDoc`, and `MergeCurves`. +- Merge failure is handled immediately by `MergeCurveOrThrow` with contextual `InvalidOperationException`; a null merge cannot silently become the next segment on a later loop. +- `SketchChainTopology.Build` and its private helpers now use `where T : class`, matching the `ReferenceEquals` identity semantics. +- No AssemblyInfo version values changed in either commit; the only AssemblyInfo change remains `InternalsVisibleTo("SldWorksLookup.RegressionTests")`. diff --git a/SldWorksLookup/Helper/PathExportUtil.cs b/SldWorksLookup/Helper/PathExportUtil.cs index e46c199..c8e5e93 100644 --- a/SldWorksLookup/Helper/PathExportUtil.cs +++ b/SldWorksLookup/Helper/PathExportUtil.cs @@ -80,13 +80,19 @@ public static void Export(SolidWorks.Interop.sldworks.ISldWorks sw) var ep = wrapper.SourceEndPoint; seCurve = seCurve.CreateTrimmedCurve2(sp.X, sp.Y, sp.Z, ep.X, ep.Y, ep.Z); + seCurve = RequireExportValue(seCurve, "Cannot trim sketch segment curve."); var body = seCurve.CreateWireBody(); - body.Display2(doc as PartDoc, Information.RGB(255, 0, 0), (int)swTempBodySelectOptions_e.swTempBodySelectOptionNone); + body = RequireExportValue(body, "Cannot create wire body for trimmed sketch segment curve."); - curve = curve == null - ? seCurve - : modeler.MergeCurves(new object[] { curve, seCurve }); + var partDoc = RequireExportValue(doc as PartDoc, "Active document is not a part document."); + body.Display2(partDoc, Information.RGB(255, 0, 0), (int)swTempBodySelectOptions_e.swTempBodySelectOptionNone); + + curve = MergeCurveOrThrow( + curve, + seCurve, + (left, right) => modeler.MergeCurves(new object[] { left, right }) as ICurve, + "while merging sketch path segment"); } if (curve == null) @@ -97,7 +103,6 @@ public static void Export(SolidWorks.Interop.sldworks.ISldWorks sw) var points = SplitCurve(curve, 10); doc.Insert3DSketch(); - var ske3D = doc.SketchManager.ActiveSketch; foreach (var point in points) { @@ -105,6 +110,36 @@ public static void Export(SolidWorks.Interop.sldworks.ISldWorks sw) } } + internal static TCurve MergeCurveOrThrow( + TCurve currentCurve, + TCurve nextCurve, + Func mergeCurves, + string context) + where TCurve : class + { + if (nextCurve == null) + throw new InvalidOperationException("Cannot merge a null sketch path segment curve. " + context); + if (currentCurve == null) + return nextCurve; + if (mergeCurves == null) + throw new ArgumentNullException(nameof(mergeCurves)); + + var merged = mergeCurves(currentCurve, nextCurve); + if (merged == null) + throw new InvalidOperationException("Cannot merge sketch path curves. " + context); + + return merged; + } + + private static TValue RequireExportValue(TValue value, string message) + where TValue : class + { + if (value == null) + throw new InvalidOperationException(message); + + return value; + } + public static List SplitCurve(ICurve curve, int num) { var points = new List(); diff --git a/SldWorksLookup/PathSplit/SketchChainTopology.cs b/SldWorksLookup/PathSplit/SketchChainTopology.cs index 12b74fe..4fb1ebf 100644 --- a/SldWorksLookup/PathSplit/SketchChainTopology.cs +++ b/SldWorksLookup/PathSplit/SketchChainTopology.cs @@ -13,6 +13,7 @@ public static List> Build( Func getEndPoint, Action reverse, Func pointsEqual) + where T : class { if (segments == null) throw new ArgumentNullException(nameof(segments)); @@ -56,6 +57,7 @@ private static int FindOpenEndpointSegmentIndex( Func getStartPoint, Func getEndPoint, Func pointsEqual) + where T : class { for (var i = 0; i < segments.Count; i++) { @@ -77,6 +79,7 @@ private static bool IsPointConnected( Func getStartPoint, Func getEndPoint, Func pointsEqual) + where T : class { foreach (var segment in segments) { @@ -97,6 +100,7 @@ private static void ConsumeConnectedSegments( Func getEndPoint, Action reverse, Func pointsEqual) + where T : class { while (remaining.Count > 0) { diff --git a/tests/SldWorksLookup.RegressionTests/Program.cs b/tests/SldWorksLookup.RegressionTests/Program.cs index 2541fa0..047304f 100644 --- a/tests/SldWorksLookup.RegressionTests/Program.cs +++ b/tests/SldWorksLookup.RegressionTests/Program.cs @@ -1,4 +1,5 @@ using SldWorksLookup.PathSplit; +using SldWorksLookup.Helper; using System; using System.Collections.Generic; using System.Linq; @@ -14,8 +15,15 @@ private static int Main() { PathTopologyReturnsEveryDisconnectedSegment, PathTopologyReturnsClosedLoop, + PathTopologyReversesOpenFirstSegment, + PathTopologyReversesConnectedSuccessor, + PathTopologyKeepsEveryStepContinuous, + PathTopologyDoesNotReorderOrRemoveInputSegments, SamplingPlanCarriesSpacingAcrossShortSegment, - SamplingPlanRejectsNonPositiveOrNonFiniteStep + SamplingPlanRejectsNonPositiveOrNonFiniteStep, + SamplingPlanReturnsNormalSpacing, + SamplingPlanHandlesExactCarryBoundary, + ExportMergeFailureThrowsContext }; var failed = 0; @@ -79,6 +87,70 @@ private static void SamplingPlanCarriesSpacingAcrossShortSegment() AssertClose(0.25, plan.DistanceToNextPoint, "Distance to next point"); } + private static void PathTopologyReversesOpenFirstSegment() + { + var first = new Segment(Point(1, 0), Point(0, 0)); + var second = new Segment(Point(1, 0), Point(2, 0)); + var segments = new List { first, second }; + + var chains = BuildChains(segments); + + AssertEqual(1, chains.Count, "Chain count"); + AssertSame(first, chains[0][0], "First segment instance"); + AssertPointEqual(Point(0, 0), chains[0][0].Start, "Reversed first start"); + AssertPointEqual(Point(1, 0), chains[0][0].End, "Reversed first end"); + AssertPointEqual(chains[0][0].End, chains[0][1].Start, "First-to-second continuity"); + } + + private static void PathTopologyReversesConnectedSuccessor() + { + var first = new Segment(Point(0, 0), Point(1, 0)); + var second = new Segment(Point(2, 0), Point(1, 0)); + var segments = new List { first, second }; + + var chains = BuildChains(segments); + + AssertEqual(1, chains.Count, "Chain count"); + AssertSame(second, chains[0][1], "Second segment instance"); + AssertPointEqual(Point(1, 0), chains[0][1].Start, "Reversed successor start"); + AssertPointEqual(Point(2, 0), chains[0][1].End, "Reversed successor end"); + AssertPointEqual(chains[0][0].End, chains[0][1].Start, "Successor continuity"); + } + + private static void PathTopologyKeepsEveryStepContinuous() + { + var segments = new List + { + new Segment(Point(3, 0), Point(2, 0)), + new Segment(Point(0, 0), Point(1, 0)), + new Segment(Point(2, 0), Point(1, 0)), + new Segment(Point(3, 0), Point(4, 0)) + }; + + var chains = BuildChains(segments); + + AssertEqual(1, chains.Count, "Chain count"); + for (var i = 1; i < chains[0].Count; i++) + { + AssertPointEqual(chains[0][i - 1].End, chains[0][i].Start, "Continuity at segment " + i); + } + } + + private static void PathTopologyDoesNotReorderOrRemoveInputSegments() + { + var first = new Segment(Point(1, 0), Point(0, 0)); + var second = new Segment(Point(1, 0), Point(2, 0)); + var third = new Segment(Point(3, 0), Point(4, 0)); + var segments = new List { first, second, third }; + + BuildChains(segments); + + AssertEqual(3, segments.Count, "Input segment count"); + AssertSame(first, segments[0], "Input first segment"); + AssertSame(second, segments[1], "Input second segment"); + AssertSame(third, segments[2], "Input third segment"); + } + private static void SamplingPlanRejectsNonPositiveOrNonFiniteStep() { AssertThrows(() => SegmentSamplingPlan.Create(1.0, 0, 0), "Zero step"); @@ -87,6 +159,40 @@ private static void SamplingPlanRejectsNonPositiveOrNonFiniteStep() AssertThrows(() => SegmentSamplingPlan.Create(1.0, double.PositiveInfinity, 0), "Infinite step"); } + private static void SamplingPlanReturnsNormalSpacing() + { + var plan = SegmentSamplingPlan.Create(5.0, 2.0, 1.0); + + AssertEqual(3, plan.PointCount, "Point count"); + AssertClose(1.0, plan.FirstDistance, "First distance"); + AssertClose(2.0, plan.DistanceToNextPoint, "Distance to next point"); + } + + private static void SamplingPlanHandlesExactCarryBoundary() + { + var plan = SegmentSamplingPlan.Create(3.0, 2.0, 3.0); + + AssertEqual(1, plan.PointCount, "Point count"); + AssertClose(3.0, plan.FirstDistance, "First distance"); + AssertClose(2.0, plan.DistanceToNextPoint, "Distance to next point"); + } + + private static void ExportMergeFailureThrowsContext() + { + var existing = new CurveToken("existing"); + var next = new CurveToken("next"); + + var first = PathExportUtil.MergeCurveOrThrow(null, next, (left, right) => new CurveToken("unused"), "segment 1"); + AssertSame(next, first, "First curve should initialize export state"); + + var ex = AssertThrows( + () => PathExportUtil.MergeCurveOrThrow(existing, next, (left, right) => null, "segment 2"), + "Merge failure"); + + if (!ex.Message.Contains("segment 2")) + throw new InvalidOperationException("Merge failure should include context. Message: " + ex.Message); + } + private static List> BuildChains(List segments) { return SketchChainTopology.Build( @@ -126,16 +232,16 @@ private static void AssertClose(double expected, double actual, string message) throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); } - private static void AssertThrows(Action action, string message) + private static TException AssertThrows(Action action, string message) where TException : Exception { try { action(); } - catch (TException) + catch (TException ex) { - return; + return ex; } catch (Exception ex) { @@ -145,6 +251,16 @@ private static void AssertThrows(Action action, string message) throw new InvalidOperationException(message + ". Expected " + typeof(TException).Name + "."); } + private sealed class CurveToken + { + public CurveToken(string name) + { + Name = name; + } + + public string Name { get; private set; } + } + private sealed class Segment { public Segment(Point3D start, Point3D end) From a3a2cc8f8fb055846b6d0f2abf837dfda87a64bf Mon Sep 17 00:00:00 2001 From: Paine Zeng Date: Sat, 18 Jul 2026 22:05:20 +0800 Subject: [PATCH 4/9] Keep SolidWorks state recoverable when commands fail Command failures previously left selection access, command capture hooks, and user-facing exception paths in recoverability gaps. This change adds dependency-free regression coverage for lifetime, logging, and TargetInvocationException reporting, then keeps COM release and telemetry submission explicit at the failing command boundary. Constraint: No new dependencies and regression tests must run without SolidWorks Rejected: Broad command framework rewrite | Task 2 only needs local lifetime and reporting repair Confidence: high Scope-risk: moderate Tested: Regression test build with /p:XCadRegDll=false; regression runner 16 PASS; Release Any CPU and x64 solution builds with /p:XCadRegDll=false; git diff --check Not-tested: Live SolidWorks COM interaction Co-authored-by: OmX --- .superpowers/sdd/task-2-report.md | 52 +++++++++ SldWorksLookup/AddIn.cs | 79 +++++++++----- SldWorksLookup/Helper/ExceptionUtil.cs | 21 ++++ SldWorksLookup/Helper/SelectionAccessScope.cs | 22 ++++ SldWorksLookup/LogExtension.cs | 76 ++++++++----- SldWorksLookup/Model/Value/LookupValue.cs | 6 +- SldWorksLookup/SldWorksLookup.csproj | 2 + SldWorksLookup/View/CaptureCmd.xaml.cs | 2 +- .../ViewModel/CaptureCmdViewModel.cs | 9 +- .../GetObjectByPIDWindowViewModel.cs | 3 +- .../SldWorksLookup.RegressionTests/Program.cs | 102 +++++++++++++++++- 11 files changed, 315 insertions(+), 59 deletions(-) create mode 100644 .superpowers/sdd/task-2-report.md create mode 100644 SldWorksLookup/Helper/ExceptionUtil.cs create mode 100644 SldWorksLookup/Helper/SelectionAccessScope.cs diff --git a/.superpowers/sdd/task-2-report.md b/.superpowers/sdd/task-2-report.md new file mode 100644 index 0000000..f39f091 --- /dev/null +++ b/.superpowers/sdd/task-2-report.md @@ -0,0 +1,52 @@ +# Task 2 Report: COM lifetime and actionable exception reporting + +## RED + +- Command: + `msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo` +- Result: failed as expected after adding Task 2 tests. +- Evidence: + - `CS0103`: `SelectionAccessScope` did not exist. + - `CS0103`: `ExceptionUtil` did not exist. + - `CS0117`: `LogExtension.TryReadConfiguration` did not exist. + +## GREEN + +- Command: + `msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo` +- Result: passed. + +- Command: + `.\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe` +- Result: passed 16 regression tests, including all Task 1 and Task 2 tests. + +- Command: + `msbuild .\SldWorksLookup.sln /p:Configuration=Release '/p:Platform=Any CPU' /p:XCadRegDll=false /v:minimal /nologo` +- Result: passed. Remaining warning is the existing test-project x86/AMD64 processor architecture mismatch, not CS0168. + +- Command: + `msbuild .\SldWorksLookup.sln /p:Configuration=Release /p:Platform=x64 /p:XCadRegDll=false /v:minimal /nologo` +- Result: passed. Remaining warning is the existing test-project x86/AMD64 processor architecture mismatch, not CS0168. + +- Command: + `git diff --check` +- Result: passed. Git reported CRLF normalization warnings only. + +## Files + +- Created `SldWorksLookup/Helper/SelectionAccessScope.cs`. +- Created `SldWorksLookup/Helper/ExceptionUtil.cs`. +- Modified `tests/SldWorksLookup.RegressionTests/Program.cs`. +- Modified `SldWorksLookup/AddIn.cs`. +- Modified `SldWorksLookup/LogExtension.cs`. +- Modified `SldWorksLookup/Model/Value/LookupValue.cs`. +- Modified `SldWorksLookup/ViewModel/CaptureCmdViewModel.cs`. +- Modified `SldWorksLookup/View/CaptureCmd.xaml.cs`. +- Modified `SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs`. +- Modified `SldWorksLookup/SldWorksLookup.csproj`. + +## Risks + +- SolidWorks COM UI paths were build-verified and covered where dependency-free tests can reach them, but not manually exercised in a live SolidWorks session. +- Solution builds still report an existing architecture mismatch warning for the regression test executable referencing the x64 add-in assembly. +- `AGENTS.md` was not present in this worktree root; the task-provided AGENTS instructions were followed. diff --git a/SldWorksLookup/AddIn.cs b/SldWorksLookup/AddIn.cs index fd103ad..7e0b5a4 100644 --- a/SldWorksLookup/AddIn.cs +++ b/SldWorksLookup/AddIn.cs @@ -15,6 +15,7 @@ using System; using System.Linq; using Xarial.XCad.SolidWorks.Enums; +using SldWorksLookup.Helper; namespace SldWorksLookup { @@ -24,6 +25,7 @@ namespace SldWorksLookup [Icon(typeof(Resource),nameof(Resource.BrowseData_16x))] public class AddIn:SwAddInEx { + private readonly List _captureWindows = new List(); public override void OnConnect() { @@ -135,9 +137,13 @@ private void CmdGroup_CommandClick(Command_e spec) } catch (Exception ex) { - //ex.ToExceptionless(LogExtension.Client) - // .AddTags($"CmdError:{spec}") - // .Submit(); + Application.ShowMessageBox(ExceptionUtil.GetUserMessage(ex), MessageBoxIcon_e.Error); + if (LogExtension.Client != null) + { + ex.ToExceptionless(LogExtension.Client) + .AddTags($"CmdError:{spec}") + .Submit(); + } } } @@ -173,35 +179,48 @@ private void SnoopAdvancedHole() try { var featData = feat.GetDefinition() as IAdvancedHoleFeatureData; - - featData.AccessSelections(doc, null); - - var elems =(featData.GetNearSideElements() as object[]).Cast().ToList(); - - var types = new List() { - typeof(ICounterboreElementData), - typeof(ICountersinkElementData), - typeof(IStraightElementData), - typeof(IStraightTapElementData ), - typeof(ITaperedTapElementData)}; - - foreach (var ele in elems) - { - var matchtype = types.FirstOrDefault(p => p.IsInstanceOfType(ele)); - - ins.Add(InstanceProperty.Create(ele,matchtype ?? typeof(IAdvancedHoleElementData))); - } - ins.Add(InstanceProperty.Create("Base Type", typeof(string))); - foreach (var ele in elems) + if (featData == null) { - ins.Add(InstanceProperty.Create(ele,typeof(IAdvancedHoleElementData))); + Application.ShowMessageBox("Cannot read advancedhole definition"); + return; } - featData.ReleaseSelectionAccess(); + SelectionAccessScope.Run( + () => featData.AccessSelections(doc, null), + () => featData.ReleaseSelectionAccess(), + () => + { + var nearSideElements = featData.GetNearSideElements() as object[]; + if (nearSideElements == null) + throw new InvalidOperationException("Advancedhole near-side elements are unavailable."); + + var elems = nearSideElements + .OfType() + .ToList(); + + var types = new List() { + typeof(ICounterboreElementData), + typeof(ICountersinkElementData), + typeof(IStraightElementData), + typeof(IStraightTapElementData ), + typeof(ITaperedTapElementData)}; + + foreach (var ele in elems) + { + var matchtype = types.FirstOrDefault(p => p.IsInstanceOfType(ele)); + + ins.Add(InstanceProperty.Create(ele, matchtype ?? typeof(IAdvancedHoleElementData))); + } + ins.Add(InstanceProperty.Create("Base Type", typeof(string))); + foreach (var ele in elems) + { + ins.Add(InstanceProperty.Create(ele, typeof(IAdvancedHoleElementData))); + } + }); } catch (System.Exception ex) { - Application.Sw.SendMsgToUser($"{ex.Message},{type} Cannot match a SolidWorks Interface"); + Application.Sw.SendMsgToUser($"{ExceptionUtil.GetUserMessage(ex)},{type} Cannot match a SolidWorks Interface"); } var selPpopWindow = CreatePopupWindow(); @@ -218,6 +237,8 @@ private void ShowColorWindow() private void ShowCaptureWindow() { var window = new CaptureCmd(Application); + _captureWindows.Add(window); + window.Closed += (sender, args) => _captureWindows.Remove(window); window?.Show(); } @@ -227,6 +248,7 @@ private void GetObject() if (doc == null) { Application.ShowMessageBox($"No active doc"); + return; } var getObjectVM = new GetObjectByPIDWindowViewModel(doc, this.Application); var window = CreatePopupWindow(); @@ -324,6 +346,11 @@ private void SnoopPID() public override void OnDisconnect() { + foreach (var window in _captureWindows.ToList()) + { + window.Close(); + } + _captureWindows.Clear(); LogExtension.LogEnded(); } } diff --git a/SldWorksLookup/Helper/ExceptionUtil.cs b/SldWorksLookup/Helper/ExceptionUtil.cs new file mode 100644 index 0000000..34b4cb4 --- /dev/null +++ b/SldWorksLookup/Helper/ExceptionUtil.cs @@ -0,0 +1,21 @@ +using System; +using System.Reflection; + +namespace SldWorksLookup.Helper +{ + internal static class ExceptionUtil + { + public static string GetUserMessage(Exception exception) + { + while (exception is TargetInvocationException && exception.InnerException != null) + { + exception = exception.InnerException; + } + + if (!string.IsNullOrWhiteSpace(exception?.Message)) + return exception.Message; + + return "Command failed."; + } + } +} diff --git a/SldWorksLookup/Helper/SelectionAccessScope.cs b/SldWorksLookup/Helper/SelectionAccessScope.cs new file mode 100644 index 0000000..86227d1 --- /dev/null +++ b/SldWorksLookup/Helper/SelectionAccessScope.cs @@ -0,0 +1,22 @@ +using System; + +namespace SldWorksLookup.Helper +{ + internal static class SelectionAccessScope + { + public static void Run(Func acquire, Action release, Action body) + { + if (!acquire()) + throw new InvalidOperationException("Cannot access the feature selections."); + + try + { + body(); + } + finally + { + release(); + } + } + } +} diff --git a/SldWorksLookup/LogExtension.cs b/SldWorksLookup/LogExtension.cs index e8c8c72..1d4c473 100644 --- a/SldWorksLookup/LogExtension.cs +++ b/SldWorksLookup/LogExtension.cs @@ -1,57 +1,47 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless; -using System.Text; -using System.Threading.Tasks; +using System; +using System.Diagnostics; using System.IO; -using Xarial.XCad.SolidWorks.Enums; +using Exceptionless; using Exceptionless.Logging; +using Xarial.XCad.SolidWorks.Enums; namespace SldWorksLookup { internal static class LogExtension { public static readonly string LogFolder = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),"SldWorksLookup", + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SldWorksLookup", "Log"); public static ExceptionlessClient Client { get; private set; } internal static void LogStart( - Version version, - SwVersion_e sldWorksVersion, + Version version, + SwVersion_e sldWorksVersion, string userName) { try { var configFile = Path.Combine( - Path.GetDirectoryName(typeof(LogExtension).Assembly.Location), - "exceptionless.txt"); - - string[] data = new string[] { "", "" }; - if (!File.Exists(configFile)) - { - data = File.ReadAllLines(configFile); - } + Path.GetDirectoryName(typeof(LogExtension).Assembly.Location), + "exceptionless.txt"); - - if (data.Length < 2) + string serverUrl; + string apiKey; + if (!TryReadConfiguration(configFile, out serverUrl, out apiKey)) return; Client = new ExceptionlessClient(c => { - c.ServerUrl = data[0].Trim(); - c.ApiKey = data[1].Trim(); + c.ServerUrl = serverUrl; + c.ApiKey = apiKey; c.SetVersion(version); }); - //服务信息收集配置 Client.Configuration.IncludePrivateInformation = true; Client.Configuration.IncludeMachineName = true; Client.Configuration.IncludeIpAddress = true; - - //设置本地存储日志文件夹 + try { if (!Directory.Exists(LogFolder)) @@ -65,7 +55,6 @@ internal static void LogStart( .Submit(); } - //开启心跳追踪 var uid = $"{Environment.UserName}@{Environment.MachineName}"; Client.Configuration.SetUserIdentity(uid, userName ?? uid); Client.Configuration.UseSessions(); @@ -77,7 +66,42 @@ internal static void LogStart( } catch (Exception ex) { + Debug.WriteLine(ex); + } + } + + internal static bool TryReadConfiguration(string path, out string serverUrl, out string apiKey) + { + serverUrl = null; + apiKey = null; + + try + { + if (!File.Exists(path)) + return false; + + var data = File.ReadAllLines(path); + if (data.Length < 2) + return false; + + serverUrl = data[0].Trim(); + apiKey = data[1].Trim(); + if (string.IsNullOrWhiteSpace(serverUrl) || string.IsNullOrWhiteSpace(apiKey)) + { + serverUrl = null; + apiKey = null; + return false; + } + + return true; + } + catch (Exception ex) + { + Debug.WriteLine(ex); + serverUrl = null; + apiKey = null; + return false; } } diff --git a/SldWorksLookup/Model/Value/LookupValue.cs b/SldWorksLookup/Model/Value/LookupValue.cs index c971429..de17247 100644 --- a/SldWorksLookup/Model/Value/LookupValue.cs +++ b/SldWorksLookup/Model/Value/LookupValue.cs @@ -247,7 +247,9 @@ private void OpenClick() } catch (Exception ex) { - ex.ToExceptionless(LogExtension.Client).Submit(); + MessageBox.Show(ExceptionUtil.GetUserMessage(ex)); + if (LogExtension.Client != null) + ex.ToExceptionless(LogExtension.Client).Submit(); } } @@ -298,7 +300,7 @@ protected void MethodSnoop() } catch (Exception ex) { - MessageBox.Show(ex.Message); + MessageBox.Show(ExceptionUtil.GetUserMessage(ex)); return; } } diff --git a/SldWorksLookup/SldWorksLookup.csproj b/SldWorksLookup/SldWorksLookup.csproj index 6dd0471..96dfd7a 100644 --- a/SldWorksLookup/SldWorksLookup.csproj +++ b/SldWorksLookup/SldWorksLookup.csproj @@ -169,6 +169,8 @@ + + True True diff --git a/SldWorksLookup/View/CaptureCmd.xaml.cs b/SldWorksLookup/View/CaptureCmd.xaml.cs index 53c01a8..a41e89e 100644 --- a/SldWorksLookup/View/CaptureCmd.xaml.cs +++ b/SldWorksLookup/View/CaptureCmd.xaml.cs @@ -34,7 +34,7 @@ private void CaptureCmd_Closed(object sender, EventArgs e) { this.Closed -= CaptureCmd_Closed; - _viewModel.DeAttachEvent(); + _viewModel.Dispose(); } } } diff --git a/SldWorksLookup/ViewModel/CaptureCmdViewModel.cs b/SldWorksLookup/ViewModel/CaptureCmdViewModel.cs index ee31f43..1db1339 100644 --- a/SldWorksLookup/ViewModel/CaptureCmdViewModel.cs +++ b/SldWorksLookup/ViewModel/CaptureCmdViewModel.cs @@ -9,11 +9,12 @@ namespace SldWorksLookup.ViewModel { - public class CaptureCmdViewModel : ViewModelBase + public class CaptureCmdViewModel : ViewModelBase, IDisposable { private SwApplication _app; private SldWorks _sw; private RelayCommand _closeCommand; + private bool _disposed; public CaptureCmdViewModel(SwApplication app) { @@ -36,8 +37,12 @@ private int _sw_CommandOpenPreNotify(int Command, int UserCommand) return 0; } - internal void DeAttachEvent() + public void Dispose() { + if (_disposed) + return; + + _disposed = true; _sw.CommandOpenPreNotify -= _sw_CommandOpenPreNotify; } } diff --git a/SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs b/SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs index 6ac73fd..b580cc9 100644 --- a/SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs +++ b/SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs @@ -5,6 +5,7 @@ using SolidWorks.Interop.swconst; using System; using Xarial.XCad; +using SldWorksLookup.Helper; namespace SldWorksLookup.View { @@ -61,7 +62,7 @@ private void GetObjectClick() } catch (Exception ex) { - _application.ShowMessageBox(ex.Message); + _application.ShowMessageBox(ExceptionUtil.GetUserMessage(ex)); } } diff --git a/tests/SldWorksLookup.RegressionTests/Program.cs b/tests/SldWorksLookup.RegressionTests/Program.cs index 047304f..cac7f51 100644 --- a/tests/SldWorksLookup.RegressionTests/Program.cs +++ b/tests/SldWorksLookup.RegressionTests/Program.cs @@ -2,7 +2,9 @@ using SldWorksLookup.Helper; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; using System.Windows.Media.Media3D; namespace SldWorksLookup.RegressionTests @@ -23,7 +25,12 @@ private static int Main() SamplingPlanRejectsNonPositiveOrNonFiniteStep, SamplingPlanReturnsNormalSpacing, SamplingPlanHandlesExactCarryBoundary, - ExportMergeFailureThrowsContext + ExportMergeFailureThrowsContext, + SelectionAccessScopeReleasesWhenBodyThrows, + SelectionAccessScopeDoesNotRunBodyWhenAcquireFails, + ExceptionUtilUnwrapsTargetInvocationException, + LogConfigurationReadsTwoTrimmedValues, + LogConfigurationRejectsMissingOrIncompleteFile }; var failed = 0; @@ -193,6 +200,87 @@ private static void ExportMergeFailureThrowsContext() throw new InvalidOperationException("Merge failure should include context. Message: " + ex.Message); } + private static void SelectionAccessScopeReleasesWhenBodyThrows() + { + var releaseCount = 0; + var ex = AssertThrows( + () => SelectionAccessScope.Run( + () => true, + () => releaseCount++, + () => { throw new InvalidOperationException("body failed"); }), + "Body failure"); + + AssertEqual("body failed", ex.Message, "Body exception message"); + AssertEqual(1, releaseCount, "Release count"); + } + + private static void SelectionAccessScopeDoesNotRunBodyWhenAcquireFails() + { + var bodyRunCount = 0; + var releaseCount = 0; + + AssertThrows( + () => SelectionAccessScope.Run( + () => false, + () => releaseCount++, + () => bodyRunCount++), + "Acquire failure"); + + AssertEqual(0, bodyRunCount, "Body run count"); + AssertEqual(0, releaseCount, "Release count"); + } + + private static void ExceptionUtilUnwrapsTargetInvocationException() + { + var inner = new InvalidOperationException("SOLIDWORKS refused the command"); + var outer = new TargetInvocationException(inner); + + AssertEqual("SOLIDWORKS refused the command", ExceptionUtil.GetUserMessage(outer), "User message"); + } + + private static void LogConfigurationReadsTwoTrimmedValues() + { + var path = Path.GetTempFileName(); + try + { + File.WriteAllLines(path, new[] { " https://logs.example.test ", " api-key " }); + + string serverUrl; + string apiKey; + var result = LogExtension.TryReadConfiguration(path, out serverUrl, out apiKey); + + AssertEqual(true, result, "Configuration result"); + AssertEqual("https://logs.example.test", serverUrl, "Server URL"); + AssertEqual("api-key", apiKey, "API key"); + } + finally + { + File.Delete(path); + } + } + + private static void LogConfigurationRejectsMissingOrIncompleteFile() + { + string serverUrl; + string apiKey; + var missingPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".txt"); + AssertEqual(false, LogExtension.TryReadConfiguration(missingPath, out serverUrl, out apiKey), "Missing file"); + + var path = Path.GetTempFileName(); + try + { + File.WriteAllLines(path, new[] { "https://logs.example.test" }); + AssertEqual(false, LogExtension.TryReadConfiguration(path, out serverUrl, out apiKey), "Incomplete file"); + + File.WriteAllLines(path, new[] { "https://logs.example.test", " " }); + AssertEqual(false, LogExtension.TryReadConfiguration(path, out serverUrl, out apiKey), "Blank api key"); + } + finally + { + File.Delete(path); + } + } + private static List> BuildChains(List segments) { return SketchChainTopology.Build( @@ -214,6 +302,18 @@ private static void AssertEqual(int expected, int actual, string message) throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); } + private static void AssertEqual(bool expected, bool actual, string message) + { + if (expected != actual) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + + private static void AssertEqual(string expected, string actual, string message) + { + if (expected != actual) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + private static void AssertSame(object expected, object actual, string message) { if (!ReferenceEquals(expected, actual)) From 01e645c877ab0bf57caaaf0738b0aeaed20878ae Mon Sep 17 00:00:00 2001 From: Paine Zeng Date: Sat, 18 Jul 2026 22:13:02 +0800 Subject: [PATCH 5/9] Recover capture command hooks when window setup fails Capture command view models subscribe to SolidWorks command notifications before the rest of the window constructor finishes. The constructor now uses a tiny construction cleanup guard so later initialization failures dispose the view model exactly once while preserving the original exception. Constraint: No new dependencies and the regression test runner must stay dependency-free Rejected: Move CaptureCmd creation ownership into AddIn | the leak happens before AddIn receives the window Confidence: high Scope-risk: narrow Tested: Regression test build with /p:XCadRegDll=false; regression runner 18 PASS; Release Any CPU and x64 solution builds with /p:XCadRegDll=false; git diff --check Not-tested: Live SolidWorks window construction failure Co-authored-by: OmX --- .superpowers/sdd/task-2-report.md | 9 +++++- SldWorksLookup/Helper/ConstructionCleanup.cs | 20 ++++++++++++ SldWorksLookup/SldWorksLookup.csproj | 1 + SldWorksLookup/View/CaptureCmd.xaml.cs | 18 +++++++---- .../SldWorksLookup.RegressionTests/Program.cs | 32 ++++++++++++++++++- 5 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 SldWorksLookup/Helper/ConstructionCleanup.cs diff --git a/.superpowers/sdd/task-2-report.md b/.superpowers/sdd/task-2-report.md index f39f091..aefa078 100644 --- a/.superpowers/sdd/task-2-report.md +++ b/.superpowers/sdd/task-2-report.md @@ -10,6 +10,12 @@ - `CS0103`: `ExceptionUtil` did not exist. - `CS0117`: `LogExtension.TryReadConfiguration` did not exist. +- Review-fix command: + `msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo` +- Result: failed as expected after adding construction-cleanup tests. +- Evidence: + - `CS0103`: `ConstructionCleanup` did not exist. + ## GREEN - Command: @@ -18,7 +24,7 @@ - Command: `.\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe` -- Result: passed 16 regression tests, including all Task 1 and Task 2 tests. +- Result: passed 18 regression tests, including all Task 1 and Task 2 tests. - Command: `msbuild .\SldWorksLookup.sln /p:Configuration=Release '/p:Platform=Any CPU' /p:XCadRegDll=false /v:minimal /nologo` @@ -36,6 +42,7 @@ - Created `SldWorksLookup/Helper/SelectionAccessScope.cs`. - Created `SldWorksLookup/Helper/ExceptionUtil.cs`. +- Created `SldWorksLookup/Helper/ConstructionCleanup.cs`. - Modified `tests/SldWorksLookup.RegressionTests/Program.cs`. - Modified `SldWorksLookup/AddIn.cs`. - Modified `SldWorksLookup/LogExtension.cs`. diff --git a/SldWorksLookup/Helper/ConstructionCleanup.cs b/SldWorksLookup/Helper/ConstructionCleanup.cs new file mode 100644 index 0000000..188444c --- /dev/null +++ b/SldWorksLookup/Helper/ConstructionCleanup.cs @@ -0,0 +1,20 @@ +using System; + +namespace SldWorksLookup.Helper +{ + internal static class ConstructionCleanup + { + public static void Run(Action initialize, Action cleanup) + { + try + { + initialize(); + } + catch + { + cleanup(); + throw; + } + } + } +} diff --git a/SldWorksLookup/SldWorksLookup.csproj b/SldWorksLookup/SldWorksLookup.csproj index 96dfd7a..d3d326d 100644 --- a/SldWorksLookup/SldWorksLookup.csproj +++ b/SldWorksLookup/SldWorksLookup.csproj @@ -169,6 +169,7 @@ + diff --git a/SldWorksLookup/View/CaptureCmd.xaml.cs b/SldWorksLookup/View/CaptureCmd.xaml.cs index a41e89e..054e43c 100644 --- a/SldWorksLookup/View/CaptureCmd.xaml.cs +++ b/SldWorksLookup/View/CaptureCmd.xaml.cs @@ -1,4 +1,5 @@ -using SldWorksLookup.ViewModel; +using SldWorksLookup.Helper; +using SldWorksLookup.ViewModel; using System; using System.Windows; using System.Windows.Interop; @@ -20,14 +21,19 @@ public CaptureCmd(SwApplication app) InitializeComponent(); _viewModel = new CaptureCmdViewModel(_app); - _viewModel.CloseAction = new Action(() => this.Close()); + ConstructionCleanup.Run( + () => + { + _viewModel.CloseAction = new Action(() => this.Close()); - var interopHelper = new WindowInteropHelper(this); - interopHelper.Owner = _app.WindowHandle; + var interopHelper = new WindowInteropHelper(this); + interopHelper.Owner = _app.WindowHandle; - this.Closed += CaptureCmd_Closed; + this.Closed += CaptureCmd_Closed; - DataContext = _viewModel; + DataContext = _viewModel; + }, + () => _viewModel.Dispose()); } private void CaptureCmd_Closed(object sender, EventArgs e) diff --git a/tests/SldWorksLookup.RegressionTests/Program.cs b/tests/SldWorksLookup.RegressionTests/Program.cs index cac7f51..75e7a60 100644 --- a/tests/SldWorksLookup.RegressionTests/Program.cs +++ b/tests/SldWorksLookup.RegressionTests/Program.cs @@ -30,7 +30,9 @@ private static int Main() SelectionAccessScopeDoesNotRunBodyWhenAcquireFails, ExceptionUtilUnwrapsTargetInvocationException, LogConfigurationReadsTwoTrimmedValues, - LogConfigurationRejectsMissingOrIncompleteFile + LogConfigurationRejectsMissingOrIncompleteFile, + ConstructionCleanupRunsCleanupOnceWhenInitializeThrows, + ConstructionCleanupDoesNotCleanupWhenInitializeSucceeds }; var failed = 0; @@ -281,6 +283,34 @@ private static void LogConfigurationRejectsMissingOrIncompleteFile() } } + private static void ConstructionCleanupRunsCleanupOnceWhenInitializeThrows() + { + var cleanupCount = 0; + var expected = new InvalidOperationException("owner failed"); + + var actual = AssertThrows( + () => ConstructionCleanup.Run( + () => { throw expected; }, + () => cleanupCount++), + "Initialize failure"); + + AssertSame(expected, actual, "Original exception"); + AssertEqual(1, cleanupCount, "Cleanup count"); + } + + private static void ConstructionCleanupDoesNotCleanupWhenInitializeSucceeds() + { + var cleanupCount = 0; + var initializeCount = 0; + + ConstructionCleanup.Run( + () => initializeCount++, + () => cleanupCount++); + + AssertEqual(1, initializeCount, "Initialize count"); + AssertEqual(0, cleanupCount, "Cleanup count"); + } + private static List> BuildChains(List segments) { return SketchChainTopology.Build( From e8bfef0e01b22d13977b1ca865dbce18713218cc Mon Sep 17 00:00:00 2001 From: Paine Zeng Date: Sat, 18 Jul 2026 22:27:30 +0800 Subject: [PATCH 6/9] Keep reflection browsing usable across COM edge cases Reflection browsing assumed arrays and accessor metadata were fully populated, which made COM edge cases crash or misclassify values. This keeps inspection local and defensive without adding dependencies. Constraint: Regression tests must run without launching SolidWorks and all builds use XCadRegDll=false Rejected: Regenerate the full TypeMatcher list | produced broad generated churn beyond the Task 3 surface Confidence: high Scope-risk: moderate Tested: Added four RED Task 3 regression tests, then passed all 22 regression tests plus Release solution builds for Any CPU and x64 with XCadRegDll=false and git diff --check Not-tested: Live SolidWorks COM browsing runtime; plan requires dependency-free regression coverage Co-authored-by: OmX --- .superpowers/sdd/task-3-report.md | 68 +++++++++++++++++++ SldWorksLookup/Helper/ObjectMatcherUtil.cs | 11 +-- SldWorksLookup/Helper/TypeMatcherUtil.cs | 33 +++++---- SldWorksLookup/Helper/TypeMatcherUtil.tt | 22 +++++- .../Model/Instance/InstanceProperty.cs | 27 ++++++-- .../Model/Instance/MethodInstanceProperty.cs | 23 ++++++- .../Model/Property/LookupParameterProperty.cs | 20 +++--- .../Model/Tree/IComponent2InstanceTree.cs | 13 +++- .../Model/Tree/IFeatureInstanceTree.cs | 4 ++ SldWorksLookup/Model/Value/LookupValue.cs | 44 ++++++++---- .../SldWorksLookup.RegressionTests/Program.cs | 54 ++++++++++++++- .../SldWorksLookup.RegressionTests.csproj | 8 +++ 12 files changed, 270 insertions(+), 57 deletions(-) create mode 100644 .superpowers/sdd/task-3-report.md diff --git a/.superpowers/sdd/task-3-report.md b/.superpowers/sdd/task-3-report.md new file mode 100644 index 0000000..dbedb5c --- /dev/null +++ b/.superpowers/sdd/task-3-report.md @@ -0,0 +1,68 @@ +# Task 3 Report: Reflection and COM-object browsing resilience + +## RED + +Command: + +```powershell +$msbuild = (Get-Command msbuild -ErrorAction SilentlyContinue).Source +if (-not $msbuild) { + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe | Select-Object -First 1 +} +& $msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo +& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe +``` + +Observed failures after adding the four Task 3 tests: + +- `ValueArrayInspectionHandlesNullElements` failed with `NullReferenceException`. +- `PropertyBrowsingHandlesSetterOnlyProperty` failed with `NullReferenceException`. +- `ReferenceParametersDefaultToNull` failed because `typeof(string)` defaulted to `System.Object`. +- `ComClassWithInternalIMapsToInterface` failed because `ImportDxfDwgDataClass` did not map to `IImportDxfDwgData`. + +## GREEN + +Implemented: + +- Array inspection now uses the first non-null element; value/string arrays render null entries as ``. +- Object-array browsing skips null entries instead of dereferencing them. +- Setter-only properties are surfaced as message-only properties without calling `GetValue` or dereferencing `GetMethod`. +- Method parameters use by-ref element types, return `null` defaults for reference/nullable types, and reject null only for non-nullable value types. +- `TypeMatcherUtil.Match` resolves runtime `I{name}` interfaces before the generated tuple list. +- `TypeMatcherUtil.tt` now emits only standard `IThing` interfaces and strips only the leading `I`; the checked-in generated file was updated only for affected internal-`I` tuple keys, avoiding a broad generated-list reorder. +- Feature/component lazy loading sets `NodeStatus = NodeStatus.Ok` after lazy-load attempts so repeated clicks do not duplicate children. + +Command result: + +```text +PASS ValueArrayInspectionHandlesNullElements +PASS PropertyBrowsingHandlesSetterOnlyProperty +PASS ReferenceParametersDefaultToNull +PASS ComClassWithInternalIMapsToInterface +``` + +All 22 regression tests passed. + +## Full Verification + +Commands: + +```powershell +& $msbuild .\SldWorksLookup.sln /p:Configuration=Release '/p:Platform=Any CPU' /p:XCadRegDll=false /v:minimal /nologo +& $msbuild .\SldWorksLookup.sln /p:Configuration=Release /p:Platform=x64 /p:XCadRegDll=false /v:minimal /nologo +& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe +git diff --check +``` + +Results: + +- `Release|Any CPU` solution build exited `0`. +- `Release|x64` solution build exited `0`. +- Regression executable exited `0` with all 22 tests passing. +- `git diff --check` exited `0`. + +## Risks + +- Both solution builds still emit existing `MSB3270` architecture warnings because the solution Release mappings build the add-in as Debug x64 while the SDK test project builds as AnyCPU. That configuration mismatch is explicitly assigned to Task 4, so Task 3 did not change solution mappings. +- COM behavior was verified through reflection/regression tests without launching SolidWorks, per the plan constraint. diff --git a/SldWorksLookup/Helper/ObjectMatcherUtil.cs b/SldWorksLookup/Helper/ObjectMatcherUtil.cs index ebada21..f8e0b6e 100644 --- a/SldWorksLookup/Helper/ObjectMatcherUtil.cs +++ b/SldWorksLookup/Helper/ObjectMatcherUtil.cs @@ -15,15 +15,18 @@ public static bool IsArray(this object obj) public static bool IsValueArray(this object obj) { - bool flag = false; if (obj is Array array) { - if (array.Length > 0 && array.GetValue(0).GetType().IsValueType) + foreach (var item in array) { - flag = true; + if (item != null) + { + return item.GetType().IsValueType || item is string; + } } } - return flag; + + return false; } public static IEnumerable ObjToArray(this object obj) diff --git a/SldWorksLookup/Helper/TypeMatcherUtil.cs b/SldWorksLookup/Helper/TypeMatcherUtil.cs index 0bdc718..47585c0 100644 --- a/SldWorksLookup/Helper/TypeMatcherUtil.cs +++ b/SldWorksLookup/Helper/TypeMatcherUtil.cs @@ -17,6 +17,12 @@ public static Type Match(Type sourceType) name = name.Remove(name.Length - 5, 5); } + var interfaceType = sourceType.Assembly.GetType($"{sourceType.Namespace}.I{name}"); + if (interfaceType != null && interfaceType.IsInterface) + { + return interfaceType; + } + var hasValue = SolidWorksTypes.FirstOrDefault(p => p.Item1 == name); return hasValue != null ? hasValue.Item2: sourceType; } @@ -42,7 +48,7 @@ public static Type Match(Type sourceType) new Tuple("BendTable", typeof(IBendTable)), new Tuple("BendTableAnnotation", typeof(IBendTableAnnotation)), new Tuple("BlockDefinition", typeof(IBlockDefinition)), - new Tuple("Blocknstance", typeof(IBlockInstance)), + new Tuple("BlockInstance", typeof(IBlockInstance)), new Tuple("Body", typeof(IBody)), new Tuple("Body2", typeof(IBody2)), new Tuple("BodyFolder", typeof(IBodyFolder)), @@ -205,14 +211,14 @@ public static Type Match(Type sourceType) new Tuple("HoleStandardsData", typeof(IHoleStandardsData)), new Tuple("HoleTable", typeof(IHoleTable)), new Tuple("HoleTableAnnotation", typeof(IHoleTableAnnotation)), - new Tuple("mportDxfDwgData", typeof(IImportDxfDwgData)), - new Tuple("mportedCurveFeatureData", typeof(IImportedCurveFeatureData)), - new Tuple("mportgesData", typeof(IImportIgesData)), - new Tuple("mportStepData", typeof(IImportStepData)), - new Tuple("ndentFeatureData", typeof(IIndentFeatureData)), - new Tuple("nterference", typeof(IInterference)), - new Tuple("nterferenceDetectionMgr", typeof(IInterferenceDetectionMgr)), - new Tuple("ntersectFeatureData", typeof(IIntersectFeatureData)), + new Tuple("ImportDxfDwgData", typeof(IImportDxfDwgData)), + new Tuple("ImportedCurveFeatureData", typeof(IImportedCurveFeatureData)), + new Tuple("ImportIgesData", typeof(IImportIgesData)), + new Tuple("ImportStepData", typeof(IImportStepData)), + new Tuple("IndentFeatureData", typeof(IIndentFeatureData)), + new Tuple("Interference", typeof(IInterference)), + new Tuple("InterferenceDetectionMgr", typeof(IInterferenceDetectionMgr)), + new Tuple("IntersectFeatureData", typeof(IIntersectFeatureData)), new Tuple("JogFeatureData", typeof(IJogFeatureData)), new Tuple("JoinFeatureData", typeof(IJoinFeatureData)), new Tuple("JournalManager", typeof(IJournalManager)), @@ -304,7 +310,7 @@ public static Type Match(Type sourceType) new Tuple("PMDatumFeature", typeof(IPMIDatumFeature)), new Tuple("PMDatumTarget", typeof(IPMIDatumTarget)), new Tuple("PMDimensionData", typeof(IPMIDimensionData)), - new Tuple("PMDimensiontem", typeof(IPMIDimensionItem)), + new Tuple("PMDimensionItem", typeof(IPMIDimensionItem)), new Tuple("PMFrameData", typeof(IPMIFrameData)), new Tuple("PMGtolBoxData", typeof(IPMIGtolBoxData)), new Tuple("PMGtolData", typeof(IPMIGtolData)), @@ -370,7 +376,7 @@ public static Type Match(Type sourceType) new Tuple("SelectionMgr", typeof(ISelectionMgr)), new Tuple("SelectionSet", typeof(ISelectionSet)), new Tuple("SelectionSetFolder", typeof(ISelectionSetFolder)), - new Tuple("SelectionSettem", typeof(ISelectionSetItem)), + new Tuple("SelectionSetItem", typeof(ISelectionSetItem)), new Tuple("Sensor", typeof(ISensor)), new Tuple("SFSymbol", typeof(ISFSymbol)), new Tuple("Sheet", typeof(ISheet)), @@ -394,7 +400,7 @@ public static Type Match(Type sourceType) new Tuple("Sketch", typeof(ISketch)), new Tuple("SketchArc", typeof(ISketchArc)), new Tuple("SketchBlockDefinition", typeof(ISketchBlockDefinition)), - new Tuple("SketchBlocknstance", typeof(ISketchBlockInstance)), + new Tuple("SketchBlockInstance", typeof(ISketchBlockInstance)), new Tuple("SketchContour", typeof(ISketchContour)), new Tuple("SketchedBendFeatureData", typeof(ISketchedBendFeatureData)), new Tuple("SketchEllipse", typeof(ISketchEllipse)), @@ -468,7 +474,7 @@ public static Type Match(Type sourceType) new Tuple("TitleBlockTableAnnotation", typeof(ITitleBlockTableAnnotation)), new Tuple("TitleBlockTableFeature", typeof(ITitleBlockTableFeature)), new Tuple("ToolingSplitFeatureData", typeof(IToolingSplitFeatureData)), - new Tuple("TreeControltem", typeof(ITreeControlItem)), + new Tuple("TreeControlItem", typeof(ITreeControlItem)), new Tuple("TriadManipulator", typeof(ITriadManipulator)), new Tuple("UniversalJointMateFeatureData", typeof(IUniversalJointMateFeatureData)), new Tuple("UserProgressBar", typeof(IUserProgressBar)), @@ -493,4 +499,3 @@ public static Type Match(Type sourceType) } } - diff --git a/SldWorksLookup/Helper/TypeMatcherUtil.tt b/SldWorksLookup/Helper/TypeMatcherUtil.tt index 0b59ffc..6019373 100644 --- a/SldWorksLookup/Helper/TypeMatcherUtil.tt +++ b/SldWorksLookup/Helper/TypeMatcherUtil.tt @@ -21,6 +21,19 @@ namespace SldWorksLookup public static Type Match(Type sourceType) { var name = sourceType.Name.Split('.').Last(); + + //处理包含 [Type]Class的情况 例如 MathTransformClass + if (name.EndsWith("Class")) + { + name = name.Remove(name.Length - 5, 5); + } + + var interfaceType = sourceType.Assembly.GetType($"{sourceType.Namespace}.I{name}"); + if (interfaceType != null && interfaceType.IsInterface) + { + return interfaceType; + } + var hasValue = SolidWorksTypes.FirstOrDefault(p => p.Item1 == name); return hasValue != null ? hasValue.Item2: sourceType; } @@ -77,14 +90,17 @@ public class TypeMatcher:IComparable foreach (Type type in types) { - if (Regex.IsMatch(type.FullName,"SolidWorks.Interop.sldworks.I[A-Za-z]+")) + if (type.IsInterface && + Regex.IsMatch(type.FullName,"SolidWorks.Interop.sldworks.I[A-Za-z]+") && + type.Name.Length > 1 && + char.IsUpper(type.Name[1])) { var name = type.FullName.Split('.').Last(); - list.Add(new TypeMatcher(name.Replace("I",""), $"typeof({name})")); + list.Add(new TypeMatcher(name.Substring(1), $"typeof({name})")); } } list.Distinct(); return list; } -#> \ No newline at end of file +#> diff --git a/SldWorksLookup/Model/Instance/InstanceProperty.cs b/SldWorksLookup/Model/Instance/InstanceProperty.cs index 2e90dac..81b5cad 100644 --- a/SldWorksLookup/Model/Instance/InstanceProperty.cs +++ b/SldWorksLookup/Model/Instance/InstanceProperty.cs @@ -212,22 +212,35 @@ protected void GetProperties() if (IsPropertySupport(property.Name,out string msg)) { //带有索引器的属性 - if (property.GetMethod.GetParameters().Length > 0) + if ((property.GetMethod != null && property.GetMethod.GetParameters().Length > 0) || + (property.SetMethod != null && property.SetMethod.GetParameters().Length > 1)) { //生成方法 - var flag = TryMethodToLookup(property.GetMethod, Instance, out var getLookupProperty); - if (flag) + if (property.GetMethod != null) { - Properties.Add(getLookupProperty); + var flag = TryMethodToLookup(property.GetMethod, Instance, out var getLookupProperty); + if (flag) + { + Properties.Add(getLookupProperty); + } } - flag = TryMethodToLookup(property.SetMethod, Instance, out var setLookupProperty); - if (flag) + if (property.SetMethod != null) { - Properties.Add(setLookupProperty); + var flag = TryMethodToLookup(property.SetMethod, Instance, out var setLookupProperty); + if (flag) + { + Properties.Add(setLookupProperty); + } } } else//普通属性 { + if (property.GetMethod == null) + { + Properties.Add(LookupPropertyProperty.CreateMsgOnly(property, "Write-only property")); + continue; + } + var flag = TryPropertyToLookup(property, Instance, out var lookupProperty); if (flag) { diff --git a/SldWorksLookup/Model/Instance/MethodInstanceProperty.cs b/SldWorksLookup/Model/Instance/MethodInstanceProperty.cs index 802d1b4..2f2ea40 100644 --- a/SldWorksLookup/Model/Instance/MethodInstanceProperty.cs +++ b/SldWorksLookup/Model/Instance/MethodInstanceProperty.cs @@ -51,11 +51,28 @@ internal void Invoke() { //找到参数 var parameters = Properties.Properties.OfType(); + var methodParameters = MethodInfo.GetParameters(); - var nullParamenter = parameters.FirstOrDefault(p => p.Value == null); - if (nullParamenter != null) + var order = 0; + foreach (var parameter in parameters) { - throw new ArgumentNullException($"{nullParamenter.PropertyType.Name} is Null"); + var parameterInfo = methodParameters[order++]; + var parameterType = LookupParameterProperty.GetEffectiveType(parameterInfo.ParameterType); + if (parameter.Value == null) + { + if (parameterType.IsValueType && Nullable.GetUnderlyingType(parameterType) == null) + { + throw new ArgumentNullException($"{parameterType.Name} is Null"); + } + + continue; + } + + var validationType = Nullable.GetUnderlyingType(parameterType) ?? parameterType; + if (!validationType.IsInstanceOfType(parameter.Value)) + { + throw new ArgumentException($"{parameter.DisplayName} must be {validationType.Name}"); + } } var parametersValue = parameters.Select(p => p.Value).ToArray(); diff --git a/SldWorksLookup/Model/Property/LookupParameterProperty.cs b/SldWorksLookup/Model/Property/LookupParameterProperty.cs index 6287415..8c962d6 100644 --- a/SldWorksLookup/Model/Property/LookupParameterProperty.cs +++ b/SldWorksLookup/Model/Property/LookupParameterProperty.cs @@ -5,26 +5,30 @@ namespace SldWorksLookup.Model { public class LookupParameterProperty : LookupProperty { - public LookupParameterProperty(ParameterInfo parameter, object value) : base(parameter.Name, value, parameter.ParameterType) + public LookupParameterProperty(ParameterInfo parameter, object value) : base(parameter.Name, value, GetEffectiveType(parameter.ParameterType)) { IsReadOnly = false; } - public LookupParameterProperty(ParameterInfo parameter) : base(parameter.Name, CreateInstace(parameter.ParameterType), parameter.ParameterType) + public LookupParameterProperty(ParameterInfo parameter) : base(parameter.Name, CreateInstace(parameter.ParameterType), GetEffectiveType(parameter.ParameterType)) { IsReadOnly = false; } public static object CreateInstace(Type type) { - if (type.IsValueType) + var effectiveType = GetEffectiveType(type); + if (effectiveType.IsValueType && Nullable.GetUnderlyingType(effectiveType) == null) { - return Activator.CreateInstance(type); - } - else - { - return new object(); + return Activator.CreateInstance(effectiveType); } + + return null; + } + + internal static Type GetEffectiveType(Type type) + { + return type.IsByRef ? type.GetElementType() : type; } } diff --git a/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs b/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs index a83d5e1..fc14d2e 100644 --- a/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs +++ b/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs @@ -13,8 +13,15 @@ public IComponent2InstanceTree(InstanceProperty instanceProperty) : base(instanc public override void AddNodesLazy() { - AddNodes(comp => comp.GetFeatures().ToArray(), - feat => $"{feat.Name}({nameof(IFeature)})"); + try + { + AddNodes(comp => comp.GetFeatures().ToArray(), + feat => $"{feat.Name}({nameof(IFeature)})"); + } + finally + { + NodeStatus = NodeStatus.Ok; + } } } -} \ No newline at end of file +} diff --git a/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs b/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs index 558fa8f..293a326 100644 --- a/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs +++ b/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs @@ -50,6 +50,10 @@ public override void AddNodesLazy() { MessageBox.Show(ex.Message); } + finally + { + NodeStatus = NodeStatus.Ok; + } } [Obsolete] diff --git a/SldWorksLookup/Model/Value/LookupValue.cs b/SldWorksLookup/Model/Value/LookupValue.cs index de17247..87ac1ce 100644 --- a/SldWorksLookup/Model/Value/LookupValue.cs +++ b/SldWorksLookup/Model/Value/LookupValue.cs @@ -173,11 +173,12 @@ public bool CanSnoop if (Value is Array array) { - if (array.Length == 0) + var firstValue = array.Cast().FirstOrDefault(item => item != null); + if (firstValue == null) { return false; } - if (array.GetValue(0).GetType().IsValueType) + if (firstValue.GetType().IsValueType || firstValue is string) { return false; } @@ -261,6 +262,11 @@ protected void PropertySnoop() List properties = new List(); foreach (var item in array) { + if (item == null) + { + continue; + } + var reDirectType = PropertyReDirectType(item.GetType()); var ins = InstanceProperty.Create(item, reDirectType); @@ -320,18 +326,26 @@ protected void MethodSnoop() { if (valueResult.IsValueArray()) { - var insProperties = valueResult.ObjToArray() - .Select(p => InstanceProperty.Create(p, p.GetType())) - .ToList(); - var propertyWindow = new LookupPropertyWindow(insProperties); - propertyWindow.ShowDialog(); + ValueName = $"{methodInfo.Name} => {LookupValue.CreateValue(valueResult, valueResult.GetType()).ValueName}"; } else { - var reDirectType = ReturnValueReDirectType(returnType); - var instanceProperty = InstanceProperty.Create(valueResult, reDirectType); - var propertyWindow = new LookupPropertyWindow(instanceProperty); - propertyWindow.ShowDialog(); + if (valueResult is Array array) + { + var insProperties = valueResult.ObjToArray() + .Where(p => p != null) + .Select(p => InstanceProperty.Create(p, ReturnValueReDirectType(p.GetType()))) + .ToList(); + var propertyWindow = new LookupPropertyWindow(insProperties); + propertyWindow.ShowDialog(); + } + else + { + var reDirectType = ReturnValueReDirectType(returnType); + var instanceProperty = InstanceProperty.Create(valueResult, reDirectType); + var propertyWindow = new LookupPropertyWindow(instanceProperty); + propertyWindow.ShowDialog(); + } } } } @@ -419,15 +433,17 @@ private string ToPropertyValueString() //值类型数组的显示值 if (Value is Array array) { - if (array.Length > 0) + var firstValue = array.Cast().FirstOrDefault(item => item != null); + if (firstValue != null) { - var arrayItemType = array.GetValue(0).GetType(); + var arrayItemType = firstValue.GetType(); if (arrayItemType.IsValueType || arrayItemType == typeof(string)) { string strValue = string.Empty; foreach (var item in array) { - strValue += string.IsNullOrEmpty(strValue) ? item.ToString() : $",{item.ToString()}"; + var itemValue = item == null ? "" : item.ToString(); + strValue += string.IsNullOrEmpty(strValue) ? itemValue : $",{itemValue}"; } return strValue; } diff --git a/tests/SldWorksLookup.RegressionTests/Program.cs b/tests/SldWorksLookup.RegressionTests/Program.cs index 75e7a60..06f5770 100644 --- a/tests/SldWorksLookup.RegressionTests/Program.cs +++ b/tests/SldWorksLookup.RegressionTests/Program.cs @@ -1,5 +1,7 @@ using SldWorksLookup.PathSplit; using SldWorksLookup.Helper; +using SldWorksLookup.Model; +using SolidWorks.Interop.sldworks; using System; using System.Collections.Generic; using System.IO; @@ -32,7 +34,11 @@ private static int Main() LogConfigurationReadsTwoTrimmedValues, LogConfigurationRejectsMissingOrIncompleteFile, ConstructionCleanupRunsCleanupOnceWhenInitializeThrows, - ConstructionCleanupDoesNotCleanupWhenInitializeSucceeds + ConstructionCleanupDoesNotCleanupWhenInitializeSucceeds, + ValueArrayInspectionHandlesNullElements, + PropertyBrowsingHandlesSetterOnlyProperty, + ReferenceParametersDefaultToNull, + ComClassWithInternalIMapsToInterface }; var failed = 0; @@ -311,6 +317,38 @@ private static void ConstructionCleanupDoesNotCleanupWhenInitializeSucceeds() AssertEqual(0, cleanupCount, "Cleanup count"); } + private static void ValueArrayInspectionHandlesNullElements() + { + var values = new object[] { null, 42 }; + + AssertEqual(true, ObjectMatcherUtil.IsValueArray(values), "Null-leading value array"); + + var lookup = LookupValue.CreateValue(values, typeof(object[])); + AssertEqual(",42", lookup.ValueName, "Null-leading value array display"); + } + + private static void PropertyBrowsingHandlesSetterOnlyProperty() + { + var instanceProperty = InstanceProperty.Create(new SetterOnlyPropertyOwner(), typeof(SetterOnlyPropertyOwner)); + var property = instanceProperty.Properties.Properties + .FirstOrDefault(p => p.DisplayName == nameof(SetterOnlyPropertyOwner.WriteOnly)); + + if (property == null) + throw new InvalidOperationException("Setter-only property was not surfaced."); + + AssertEqual("Write-only property", property.Value as string, "Setter-only property message"); + } + + private static void ReferenceParametersDefaultToNull() + { + AssertEqual(null, LookupParameterProperty.CreateInstace(typeof(string)), "String default"); + } + + private static void ComClassWithInternalIMapsToInterface() + { + AssertSame(typeof(IImportDxfDwgData), TypeMatcherUtil.Match(typeof(ImportDxfDwgDataClass)), "Import DXF/DWG interface"); + } + private static List> BuildChains(List segments) { return SketchChainTopology.Build( @@ -344,6 +382,12 @@ private static void AssertEqual(string expected, string actual, string message) throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); } + private static void AssertEqual(object expected, object actual, string message) + { + if (!object.Equals(expected, actual)) + throw new InvalidOperationException(message + ". Expected " + expected + ", got " + actual + "."); + } + private static void AssertSame(object expected, object actual, string message) { if (!ReferenceEquals(expected, actual)) @@ -410,5 +454,13 @@ public void Reverse() End = start; } } + + private sealed class SetterOnlyPropertyOwner + { + public int WriteOnly + { + set { } + } + } } } diff --git a/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj b/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj index f92108c..efca6e9 100644 --- a/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj +++ b/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj @@ -5,7 +5,15 @@ 7.3 + + ..\..\packages\MvvmLightLibs.5.4.1.1\lib\net45\GalaSoft.MvvmLight.dll + + + + ..\..\packages\Xarial.XCad.SolidWorks.Interops.0.2.0\lib\net40\SolidWorks.Interop.sldworks.dll + False + From c6b1eeb8306c7c5ebcc70466d2074eb6e858931a Mon Sep 17 00:00:00 2001 From: Paine Zeng Date: Sat, 18 Jul 2026 22:40:53 +0800 Subject: [PATCH 7/9] Keep reflection retries honest after review Review found that lazy loading could hide failed COM expansion by marking nodes complete, and that the checked-in TypeMatcher table still drifted from the corrected T4 rule. This adds regression coverage for those review findings and keeps retry state visible when load work throws. Constraint: Do not rewrite Task 3 history; append a follow-up commit Constraint: Keep TypeMatcher changes targeted without regenerated sorting or formatting churn Rejected: Mark lazy-load nodes Ok in finally | hides failed loads and prevents retry Rejected: Regenerate the full SolidWorksTypes table | creates broad unrelated generated churn Confidence: high Scope-risk: narrow Tested: RED for missing LazyLoadCompletion, invalid TypeMatcher entries, and all-null array display; then all 26 regression tests passed, Release Any CPU and x64 solution builds passed with XCadRegDll=false, and git diff --check passed Not-tested: Live SolidWorks COM runtime expansion Co-authored-by: OmX --- .superpowers/sdd/task-3-report.md | 29 ++++++++++ SldWorksLookup/Helper/TypeMatcherUtil.cs | 37 ++++-------- SldWorksLookup/Helper/TypeMatcherUtil.tt | 6 +- .../Model/Tree/IComponent2InstanceTree.cs | 16 +++--- .../Model/Tree/IFeatureInstanceTree.cs | 33 +++++------ .../Model/Tree/LazyLoadCompletion.cs | 13 +++++ SldWorksLookup/Model/Value/LookupValue.cs | 22 +++---- SldWorksLookup/SldWorksLookup.csproj | 1 + .../SldWorksLookup.RegressionTests/Program.cs | 57 ++++++++++++++++++- 9 files changed, 149 insertions(+), 65 deletions(-) create mode 100644 SldWorksLookup/Model/Tree/LazyLoadCompletion.cs diff --git a/.superpowers/sdd/task-3-report.md b/.superpowers/sdd/task-3-report.md index dbedb5c..a8a246c 100644 --- a/.superpowers/sdd/task-3-report.md +++ b/.superpowers/sdd/task-3-report.md @@ -66,3 +66,32 @@ Results: - Both solution builds still emit existing `MSB3270` architecture warnings because the solution Release mappings build the add-in as Debug x64 while the SDK test project builds as AnyCPU. That configuration mismatch is explicitly assigned to Task 4, so Task 3 did not change solution mappings. - COM behavior was verified through reflection/regression tests without launching SolidWorks, per the plan constraint. + +## CHANGES_REQUESTED Follow-up + +Additional RED coverage added: + +- `LazyLoadCompletionMarksOkAfterSuccess` +- `LazyLoadCompletionLeavesNeedRunWhenLoadThrows` +- `TypeMatcherGeneratedEntriesUseInterfacesWithSingleLeadingI` +- `ValueArrayInspectionDisplaysAllNullElements` + +Observed RED: + +- `LazyLoadCompletion` was missing before the helper was added. +- After the helper compiled, `TypeMatcherGeneratedEntriesUseInterfacesWithSingleLeadingI` failed on non-interface COM class tuples plus internal-`I` key errors such as `MatenPlace` and `PMDatumData`. +- `ValueArrayInspectionDisplaysAllNullElements` failed because all-null arrays displayed as `System.Object[]`. + +Follow-up implementation: + +- Added `LazyLoadCompletion.Run` and changed feature/component lazy loading to mark `NodeStatus.Ok` only after successful load completion. Feature load exceptions are still shown to the user and leave `NodeStatus.NeedRun` for retry. +- Removed checked-in `SolidWorksTypes` class tuples that do not satisfy the T4 rule and fixed internal-`I` tuple keys without reordering the generated table. +- Changed `TypeMatcherUtil.tt` to return a grouped/distinct list instead of calling ineffective `list.Distinct()` and discarding the result. +- Made all-null arrays render as `,`. + +Follow-up verification: + +- `Release|Any CPU` solution build exited `0`. +- `Release|x64` solution build exited `0`. +- Regression executable exited `0` with all 26 tests passing. +- `git diff --check` exited `0`. diff --git a/SldWorksLookup/Helper/TypeMatcherUtil.cs b/SldWorksLookup/Helper/TypeMatcherUtil.cs index 47585c0..4b5f1ff 100644 --- a/SldWorksLookup/Helper/TypeMatcherUtil.cs +++ b/SldWorksLookup/Helper/TypeMatcherUtil.cs @@ -247,7 +247,7 @@ public static Type Match(Type sourceType) new Tuple("MateEntity", typeof(IMateEntity)), new Tuple("MateEntity2", typeof(IMateEntity2)), new Tuple("MateFeatureData", typeof(IMateFeatureData)), - new Tuple("MatenPlace", typeof(IMateInPlace)), + new Tuple("MateInPlace", typeof(IMateInPlace)), new Tuple("MateLoadReference", typeof(IMateLoadReference)), new Tuple("MateReference", typeof(IMateReference)), new Tuple("MaterialVisualPropertiesData", typeof(IMaterialVisualPropertiesData)), @@ -277,24 +277,8 @@ public static Type Match(Type sourceType) new Tuple("Mouse", typeof(IMouse)), new Tuple("MoveCopyBodyFeatureData", typeof(IMoveCopyBodyFeatureData)), new Tuple("MoveFaceFeatureData", typeof(IMoveFaceFeatureData)), - new Tuple("mportDxfDwgData", typeof(ImportDxfDwgData)), - new Tuple("mportDxfDwgDataClass", typeof(ImportDxfDwgDataClass)), - new Tuple("mportedCurveFeatureData", typeof(ImportedCurveFeatureData)), - new Tuple("mportedCurveFeatureDataClass", typeof(ImportedCurveFeatureDataClass)), - new Tuple("mportgesData", typeof(ImportIgesData)), - new Tuple("mportgesDataClass", typeof(ImportIgesDataClass)), - new Tuple("mportStepData", typeof(ImportStepData)), - new Tuple("mportStepDataClass", typeof(ImportStepDataClass)), new Tuple("MultiJogLeader", typeof(IMultiJogLeader)), - new Tuple("ndentFeatureData", typeof(IndentFeatureData)), - new Tuple("ndentFeatureDataClass", typeof(IndentFeatureDataClass)), new Tuple("Note", typeof(INote)), - new Tuple("nterference", typeof(Interference)), - new Tuple("nterferenceClass", typeof(InterferenceClass)), - new Tuple("nterferenceDetectionMgr", typeof(InterferenceDetectionMgr)), - new Tuple("nterferenceDetectionMgrClass", typeof(InterferenceDetectionMgrClass)), - new Tuple("ntersectFeatureData", typeof(IntersectFeatureData)), - new Tuple("ntersectFeatureDataClass", typeof(IntersectFeatureDataClass)), new Tuple("OneBendFeatureData", typeof(IOneBendFeatureData)), new Tuple("PackAndGo", typeof(IPackAndGo)), new Tuple("PageSetup", typeof(IPageSetup)), @@ -306,15 +290,15 @@ public static Type Match(Type sourceType) new Tuple("PartingSurfaceFeatureData", typeof(IPartingSurfaceFeatureData)), new Tuple("PerpendicularMateFeatureData", typeof(IPerpendicularMateFeatureData)), new Tuple("PlaneManipulator", typeof(IPlaneManipulator)), - new Tuple("PMDatumData", typeof(IPMIDatumData)), - new Tuple("PMDatumFeature", typeof(IPMIDatumFeature)), - new Tuple("PMDatumTarget", typeof(IPMIDatumTarget)), - new Tuple("PMDimensionData", typeof(IPMIDimensionData)), - new Tuple("PMDimensionItem", typeof(IPMIDimensionItem)), - new Tuple("PMFrameData", typeof(IPMIFrameData)), - new Tuple("PMGtolBoxData", typeof(IPMIGtolBoxData)), - new Tuple("PMGtolData", typeof(IPMIGtolData)), - new Tuple("PMGtolFrameDatum", typeof(IPMIGtolFrameDatum)), + new Tuple("PMIDatumData", typeof(IPMIDatumData)), + new Tuple("PMIDatumFeature", typeof(IPMIDatumFeature)), + new Tuple("PMIDatumTarget", typeof(IPMIDatumTarget)), + new Tuple("PMIDimensionData", typeof(IPMIDimensionData)), + new Tuple("PMIDimensionItem", typeof(IPMIDimensionItem)), + new Tuple("PMIFrameData", typeof(IPMIFrameData)), + new Tuple("PMIGtolBoxData", typeof(IPMIGtolBoxData)), + new Tuple("PMIGtolData", typeof(IPMIGtolData)), + new Tuple("PMIGtolFrameDatum", typeof(IPMIGtolFrameDatum)), new Tuple("Print3DDialog", typeof(IPrint3DDialog)), new Tuple("PrintSpecification", typeof(IPrintSpecification)), new Tuple("ProfileCenterMateFeatureData", typeof(IProfileCenterMateFeatureData)), @@ -498,4 +482,3 @@ public static Type Match(Type sourceType) }; } } - diff --git a/SldWorksLookup/Helper/TypeMatcherUtil.tt b/SldWorksLookup/Helper/TypeMatcherUtil.tt index 6019373..c7b8a83 100644 --- a/SldWorksLookup/Helper/TypeMatcherUtil.tt +++ b/SldWorksLookup/Helper/TypeMatcherUtil.tt @@ -99,8 +99,10 @@ public class TypeMatcher:IComparable list.Add(new TypeMatcher(name.Substring(1), $"typeof({name})")); } } - list.Distinct(); - return list; + return list + .GroupBy(p => p.TypeName) + .Select(p => p.First()) + .ToList(); } #> diff --git a/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs b/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs index fc14d2e..5610812 100644 --- a/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs +++ b/SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs @@ -13,15 +13,13 @@ public IComponent2InstanceTree(InstanceProperty instanceProperty) : base(instanc public override void AddNodesLazy() { - try - { - AddNodes(comp => comp.GetFeatures().ToArray(), - feat => $"{feat.Name}({nameof(IFeature)})"); - } - finally - { - NodeStatus = NodeStatus.Ok; - } + LazyLoadCompletion.Run( + () => + { + AddNodes(comp => comp.GetFeatures().ToArray(), + feat => $"{feat.Name}({nameof(IFeature)})"); + }, + () => NodeStatus = NodeStatus.Ok); } } } diff --git a/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs b/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs index 293a326..ee6e9f7 100644 --- a/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs +++ b/SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs @@ -31,29 +31,30 @@ public override void AddNodesLazy() try { - if (InstanceProperty?.Instance is IFeature feat) - { - var typeName = feat.GetTypeName2(); + LazyLoadCompletion.Run( + () => + { + if (InstanceProperty?.Instance is IFeature feat) + { + var typeName = feat.GetTypeName2(); - types = TypeNameToDefinitionUtil.Match(typeName); + types = TypeNameToDefinitionUtil.Match(typeName); - foreach (var type in types) - { - AddNode(f => f.GetDefinition(), - type, _ => $"{type.Name}({typeName})"); - AddNode(f => f.GetSpecificFeature2(), - type, _ => $"{type.Name}({typeName})"); - } - } + foreach (var type in types) + { + AddNode(f => f.GetDefinition(), + type, _ => $"{type.Name}({typeName})"); + AddNode(f => f.GetSpecificFeature2(), + type, _ => $"{type.Name}({typeName})"); + } + } + }, + () => NodeStatus = NodeStatus.Ok); } catch (Exception ex) { MessageBox.Show(ex.Message); } - finally - { - NodeStatus = NodeStatus.Ok; - } } [Obsolete] diff --git a/SldWorksLookup/Model/Tree/LazyLoadCompletion.cs b/SldWorksLookup/Model/Tree/LazyLoadCompletion.cs new file mode 100644 index 0000000..84de5fd --- /dev/null +++ b/SldWorksLookup/Model/Tree/LazyLoadCompletion.cs @@ -0,0 +1,13 @@ +using System; + +namespace SldWorksLookup.Model +{ + public static class LazyLoadCompletion + { + public static void Run(Action load, Action markOk) + { + load(); + markOk(); + } + } +} diff --git a/SldWorksLookup/Model/Value/LookupValue.cs b/SldWorksLookup/Model/Value/LookupValue.cs index 87ac1ce..ae1d3bd 100644 --- a/SldWorksLookup/Model/Value/LookupValue.cs +++ b/SldWorksLookup/Model/Value/LookupValue.cs @@ -434,19 +434,21 @@ private string ToPropertyValueString() if (Value is Array array) { var firstValue = array.Cast().FirstOrDefault(item => item != null); - if (firstValue != null) + if (firstValue == null) { - var arrayItemType = firstValue.GetType(); - if (arrayItemType.IsValueType || arrayItemType == typeof(string)) + return string.Join(",", array.Cast().Select(item => item == null ? "" : item.ToString())); + } + + var arrayItemType = firstValue.GetType(); + if (arrayItemType.IsValueType || arrayItemType == typeof(string)) + { + string strValue = string.Empty; + foreach (var item in array) { - string strValue = string.Empty; - foreach (var item in array) - { - var itemValue = item == null ? "" : item.ToString(); - strValue += string.IsNullOrEmpty(strValue) ? itemValue : $",{itemValue}"; - } - return strValue; + var itemValue = item == null ? "" : item.ToString(); + strValue += string.IsNullOrEmpty(strValue) ? itemValue : $",{itemValue}"; } + return strValue; } } diff --git a/SldWorksLookup/SldWorksLookup.csproj b/SldWorksLookup/SldWorksLookup.csproj index d3d326d..cab27b2 100644 --- a/SldWorksLookup/SldWorksLookup.csproj +++ b/SldWorksLookup/SldWorksLookup.csproj @@ -197,6 +197,7 @@ + diff --git a/tests/SldWorksLookup.RegressionTests/Program.cs b/tests/SldWorksLookup.RegressionTests/Program.cs index 06f5770..ce16ba5 100644 --- a/tests/SldWorksLookup.RegressionTests/Program.cs +++ b/tests/SldWorksLookup.RegressionTests/Program.cs @@ -38,7 +38,11 @@ private static int Main() ValueArrayInspectionHandlesNullElements, PropertyBrowsingHandlesSetterOnlyProperty, ReferenceParametersDefaultToNull, - ComClassWithInternalIMapsToInterface + ComClassWithInternalIMapsToInterface, + LazyLoadCompletionMarksOkAfterSuccess, + LazyLoadCompletionLeavesNeedRunWhenLoadThrows, + TypeMatcherGeneratedEntriesUseInterfacesWithSingleLeadingI, + ValueArrayInspectionDisplaysAllNullElements }; var failed = 0; @@ -349,6 +353,57 @@ private static void ComClassWithInternalIMapsToInterface() AssertSame(typeof(IImportDxfDwgData), TypeMatcherUtil.Match(typeof(ImportDxfDwgDataClass)), "Import DXF/DWG interface"); } + private static void LazyLoadCompletionMarksOkAfterSuccess() + { + var nodeStatus = NodeStatus.NeedRun; + var loadCount = 0; + + LazyLoadCompletion.Run( + () => loadCount++, + () => nodeStatus = NodeStatus.Ok); + + AssertEqual(1, loadCount, "Load count"); + AssertEqual(NodeStatus.Ok, nodeStatus, "Node status"); + } + + private static void LazyLoadCompletionLeavesNeedRunWhenLoadThrows() + { + var nodeStatus = NodeStatus.NeedRun; + var markOkCount = 0; + + AssertThrows( + () => LazyLoadCompletion.Run( + () => { throw new InvalidOperationException("load failed"); }, + () => + { + markOkCount++; + nodeStatus = NodeStatus.Ok; + }), + "Lazy load failure"); + + AssertEqual(0, markOkCount, "Mark OK count"); + AssertEqual(NodeStatus.NeedRun, nodeStatus, "Node status"); + } + + private static void TypeMatcherGeneratedEntriesUseInterfacesWithSingleLeadingI() + { + var errors = TypeMatcherUtil.SolidWorksTypes + .Where(tuple => !tuple.Item2.IsInterface || tuple.Item1 != tuple.Item2.Name.Substring(1)) + .Select(tuple => tuple.Item1 + " => " + tuple.Item2.Name) + .ToArray(); + + if (errors.Length > 0) + throw new InvalidOperationException("Invalid TypeMatcher entries: " + string.Join("; ", errors)); + } + + private static void ValueArrayInspectionDisplaysAllNullElements() + { + var values = new object[] { null, null }; + var lookup = LookupValue.CreateValue(values, typeof(object[])); + + AssertEqual(",", lookup.ValueName, "All-null array display"); + } + private static List> BuildChains(List segments) { return SketchChainTopology.Build( From 87d5ffc5f0b885777a73333594d030c229114bdd Mon Sep 17 00:00:00 2001 From: Paine Zeng Date: Sat, 18 Jul 2026 22:51:22 +0800 Subject: [PATCH 8/9] Make release builds safe and reproducible Release builds previously routed through Debug configurations and inherited XCad registration side effects. The configuration contract now locks the build and installer safety expectations, while the project default keeps local builds registration-free unless callers explicitly opt in. Constraint: Release validation must not register the SolidWorks add-in Rejected: Always pass /p:XCadRegDll=false from scripts | leaves normal developer and CI builds side-effectful by default Confidence: high Scope-risk: narrow Tested: Verify-ProjectConfiguration.ps1; packages.config restore; Release Any CPU build without XCadRegDll override and no RegAsm/regsvr32/MSB3270/warning matches; Release x64 build with XCadRegDll=false; 26 regression tests from Any CPU and x64 output paths; git diff --check Not-tested: Advanced Installer package build because AdvancedInstaller.com is not installed Co-authored-by: OmX --- .superpowers/sdd/task-4-report.md | 93 +++++++++++++++++++ Installer/SolidWorksLookup.aip | 30 +----- SldWorksLookup.sln | 16 ++-- SldWorksLookup/Install.bat | 13 ++- SldWorksLookup/SldWorksLookup.csproj | 2 + SldWorksLookup/UnInstall.bat | 13 ++- .../SldWorksLookup.RegressionTests.csproj | 1 + tests/Verify-ProjectConfiguration.ps1 | 71 ++++++++++++++ 8 files changed, 195 insertions(+), 44 deletions(-) create mode 100644 .superpowers/sdd/task-4-report.md create mode 100644 tests/Verify-ProjectConfiguration.ps1 diff --git a/.superpowers/sdd/task-4-report.md b/.superpowers/sdd/task-4-report.md new file mode 100644 index 0000000..5703e5f --- /dev/null +++ b/.superpowers/sdd/task-4-report.md @@ -0,0 +1,93 @@ +# Task 4 Report: Reproducible and safer build/installer configuration + +## RED + +Created `tests/Verify-ProjectConfiguration.ps1` as a configuration contract test covering: + +- Release solution mappings must map to Release project configurations and must not map to Debug. +- `SldWorksLookup.csproj` must default `XCadRegDll` to `false` while allowing command-line override. +- install/uninstall batch files must run from the script directory, quote paths, check required files, propagate `RegAsm` errorlevel, and return `0` only after success. +- installer .NET minimum must be 4.7.2. +- installer must not include explicit `exceptionless.txt`, `*.pdb`, or `*.xml` file rows. +- synchronized `..\bin` content must exclude `exceptionless.txt`, `*.pdb`, and `*.xml`. +- regression test runner must target x64. +- project and AIP XML must parse. + +Command: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\Verify-ProjectConfiguration.ps1 +``` + +Observed RED: + +```text +Release|x64 must map to project Release|x64. +``` + +The same pre-fix state also lacked the `XCadRegDll=false` default and safe batch/installer contract. + +## GREEN + +Implemented: + +- Mapped add-in solution `Release|Any CPU` and `Release|x64` to project Release configurations instead of Debug. +- Mapped regression test solution x64 configurations to project x64 and set the SDK test runner `PlatformTarget` to x64 to remove the known `MSB3270` mismatch. +- Added `false` so normal builds are side-effect free while `/p:XCadRegDll=true` still wins. +- Hardened `Install.bat` and `UnInstall.bat` with script-directory execution, quoted paths, file existence checks, raw `RegAsm` errorlevel propagation, and explicit success exit. +- Raised Advanced Installer launch-condition/display minimum .NET to 4.7.2. +- Removed explicit installer rows for `exceptionless.txt`, `.pdb`, and `.xml` files. +- Updated the existing Advanced Installer synchronized folder `ExcludePattern` with `exceptionless.txt|*.pdb|*.xml`. + +## Verification + +Commands and results: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\Verify-ProjectConfiguration.ps1 +``` + +Result: exited `0`, `Project configuration contract verified.` + +```powershell +& $msbuild .\SldWorksLookup.sln /t:Restore /p:RestorePackagesConfig=true /v:minimal /nologo +``` + +Result: exited `0`. + +```powershell +& $msbuild .\SldWorksLookup.sln /p:Configuration=Release '/p:Platform=Any CPU' /v:normal /nologo +``` + +Result: exited `0`. Captured output contained no `RegAsm`, `regsvr32`, `MSB3270`, or `warning` matches, proving the default `XCadRegDll=false` build did not run registration. + +```powershell +& $msbuild .\SldWorksLookup.sln /p:Configuration=Release /p:Platform=x64 /p:XCadRegDll=false /v:minimal /nologo +``` + +Result: exited `0`. + +```powershell +& $msbuild .\SldWorksLookup\SldWorksLookup.csproj /getProperty:XCadRegDll /nologo +& $msbuild .\SldWorksLookup\SldWorksLookup.csproj /getProperty:XCadRegDll /p:XCadRegDll=true /nologo +``` + +Results: default printed `false`; command-line override printed `true`. + +```powershell +.\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe +.\tests\SldWorksLookup.RegressionTests\bin\x64\Release\net472\SldWorksLookup.RegressionTests.exe +``` + +Results: both executables exited `0`; all 26 regression tests passed in each location. + +```powershell +git diff --check +``` + +Result: exited `0`. + +## Notes + +- `AdvancedInstaller.com` is not installed on this machine, so no `.aip` build was attempted. +- Installer validation was limited to XML parsing plus the configuration contract test against the actual AIP XML structure. diff --git a/Installer/SolidWorksLookup.aip b/Installer/SolidWorksLookup.aip index 2db9f6d..592e620 100644 --- a/Installer/SolidWorksLookup.aip +++ b/Installer/SolidWorksLookup.aip @@ -7,8 +7,8 @@ - - + + @@ -93,7 +93,7 @@ - + @@ -107,13 +107,7 @@ - - - - - - @@ -123,7 +117,6 @@ - @@ -132,10 +125,7 @@ - - - @@ -143,26 +133,14 @@ - - - - - - - - - - - - @@ -394,6 +372,6 @@ - + diff --git a/SldWorksLookup.sln b/SldWorksLookup.sln index 5dcec7c..7daa64a 100644 --- a/SldWorksLookup.sln +++ b/SldWorksLookup.sln @@ -19,18 +19,18 @@ Global {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Debug|x64.ActiveCfg = Debug|x64 {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Debug|x64.Build.0 = Debug|x64 - {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|Any CPU.Build.0 = Debug|Any CPU - {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.ActiveCfg = Debug|x64 - {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.Build.0 = Debug|x64 + {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|Any CPU.Build.0 = Release|Any CPU + {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.ActiveCfg = Release|x64 + {94D6A6F6-D6AF-4B2F-8D72-AFC8746C97BB}.Release|x64.Build.0 = Release|x64 {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|x64.ActiveCfg = Debug|Any CPU - {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|x64.Build.0 = Debug|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|x64.ActiveCfg = Debug|x64 + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Debug|x64.Build.0 = Debug|x64 {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|Any CPU.Build.0 = Release|Any CPU - {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|x64.ActiveCfg = Release|Any CPU - {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|x64.Build.0 = Release|Any CPU + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|x64.ActiveCfg = Release|x64 + {4F5C2952-0D72-4F69-9294-9C8E5764086D}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SldWorksLookup/Install.bat b/SldWorksLookup/Install.bat index 52ddd48..5a9cb42 100644 --- a/SldWorksLookup/Install.bat +++ b/SldWorksLookup/Install.bat @@ -1,5 +1,8 @@ -set path=%~d0 -cd %path% -cd /d %~dp0 - -RegAsm.exe SldWorksLookup.dll /codebase +@echo off +setlocal +cd /d "%~dp0" || exit /b 1 +if not exist "%~dp0RegAsm.exe" exit /b 2 +if not exist "%~dp0SldWorksLookup.dll" exit /b 3 +"%~dp0RegAsm.exe" "%~dp0SldWorksLookup.dll" /codebase +if errorlevel 1 exit /b %errorlevel% +exit /b 0 diff --git a/SldWorksLookup/SldWorksLookup.csproj b/SldWorksLookup/SldWorksLookup.csproj index cab27b2..4011499 100644 --- a/SldWorksLookup/SldWorksLookup.csproj +++ b/SldWorksLookup/SldWorksLookup.csproj @@ -10,6 +10,7 @@ SldWorksLookup SldWorksLookup v4.7.2 + false 512 true @@ -32,6 +33,7 @@ TRACE prompt 4 + x64 true diff --git a/SldWorksLookup/UnInstall.bat b/SldWorksLookup/UnInstall.bat index 8ea76e2..0bf16eb 100644 --- a/SldWorksLookup/UnInstall.bat +++ b/SldWorksLookup/UnInstall.bat @@ -1,5 +1,8 @@ -set path=%~d0 -cd %path% -cd /d %~dp0 - -RegAsm.exe SldWorksLookup.dll /u +@echo off +setlocal +cd /d "%~dp0" || exit /b 1 +if not exist "%~dp0RegAsm.exe" exit /b 2 +if not exist "%~dp0SldWorksLookup.dll" exit /b 3 +"%~dp0RegAsm.exe" "%~dp0SldWorksLookup.dll" /u +if errorlevel 1 exit /b %errorlevel% +exit /b 0 diff --git a/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj b/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj index efca6e9..49f62d9 100644 --- a/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj +++ b/tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj @@ -3,6 +3,7 @@ Exe net472 7.3 + x64 diff --git a/tests/Verify-ProjectConfiguration.ps1 b/tests/Verify-ProjectConfiguration.ps1 new file mode 100644 index 0000000..ee75f80 --- /dev/null +++ b/tests/Verify-ProjectConfiguration.ps1 @@ -0,0 +1,71 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot + +function Read-RepoFile([string]$relativePath) { + return Get-Content -Raw -LiteralPath (Join-Path $repoRoot $relativePath) +} + +function Assert-Matches([string]$text, [string]$pattern, [string]$message) { + if ($text -notmatch $pattern) { + throw $message + } +} + +function Assert-NotMatches([string]$text, [string]$pattern, [string]$message) { + if ($text -match $pattern) { + throw $message + } +} + +function Assert-XmlFile([string]$relativePath) { + $xml = New-Object System.Xml.XmlDocument + $xml.PreserveWhitespace = $true + $xml.Load((Join-Path $repoRoot $relativePath)) + return $xml +} + +$solution = Read-RepoFile 'SldWorksLookup.sln' +$project = Read-RepoFile 'SldWorksLookup\SldWorksLookup.csproj' +$testsProject = Read-RepoFile 'tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj' +$install = Read-RepoFile 'SldWorksLookup\Install.bat' +$uninstall = Read-RepoFile 'SldWorksLookup\UnInstall.bat' +$installer = Read-RepoFile 'Installer\SolidWorksLookup.aip' + +Assert-Matches $solution 'Release\|Any CPU\.ActiveCfg = Release\|Any CPU' 'Release|Any CPU must map to project Release|Any CPU.' +Assert-Matches $solution 'Release\|x64\.ActiveCfg = Release\|x64' 'Release|x64 must map to project Release|x64.' +Assert-NotMatches $solution 'Release\|Any CPU\.(ActiveCfg|Build\.0) = Debug\|' 'Release|Any CPU must not build a Debug project configuration.' +Assert-NotMatches $solution 'Release\|x64\.(ActiveCfg|Build\.0) = Debug\|' 'Release|x64 must not build a Debug project configuration.' + +Assert-Matches $project 'false' 'XCadRegDll must default to false while allowing command-line overrides.' + +Assert-Matches $install 'cd /d "%~dp0" \|\| exit /b 1' 'Install.bat must run from its own directory.' +Assert-Matches $install 'if not exist "%~dp0RegAsm\.exe" exit /b 2' 'Install.bat must verify RegAsm.exe exists.' +Assert-Matches $install 'if not exist "%~dp0SldWorksLookup\.dll" exit /b 3' 'Install.bat must verify SldWorksLookup.dll exists.' +Assert-Matches $install '"%~dp0RegAsm\.exe" "%~dp0SldWorksLookup\.dll" /codebase' 'Install.bat must quote RegAsm and target paths.' +Assert-Matches $install 'if errorlevel 1 exit /b %errorlevel%' 'Install.bat must propagate RegAsm failures.' +Assert-Matches $install 'exit /b 0' 'Install.bat must return 0 only after success.' + +Assert-Matches $uninstall 'cd /d "%~dp0" \|\| exit /b 1' 'UnInstall.bat must run from its own directory.' +Assert-Matches $uninstall 'if not exist "%~dp0RegAsm\.exe" exit /b 2' 'UnInstall.bat must verify RegAsm.exe exists.' +Assert-Matches $uninstall 'if not exist "%~dp0SldWorksLookup\.dll" exit /b 3' 'UnInstall.bat must verify SldWorksLookup.dll exists.' +Assert-Matches $uninstall '"%~dp0RegAsm\.exe" "%~dp0SldWorksLookup\.dll" /u' 'UnInstall.bat must quote RegAsm and target paths.' +Assert-Matches $uninstall 'if errorlevel 1 exit /b %errorlevel%' 'UnInstall.bat must propagate RegAsm failures.' +Assert-Matches $uninstall 'exit /b 0' 'UnInstall.bat must return 0 only after success.' + +Assert-Matches $installer 'AI_REQUIRED_DOTNET_DISPLAY".*4\.7\.2' 'Installer display minimum .NET version must be 4.7.2.' +Assert-Matches $installer 'AI_REQUIRED_DOTNET_VERSION".*4\.7\.2' 'Installer launch condition minimum .NET version must be 4.7.2.' +Assert-NotMatches $installer 'File="exceptionless\.txt"' 'Installer must not include an explicit exceptionless.txt file row.' +Assert-NotMatches $installer 'File="[^"]+\.(pdb|xml)"' 'Installer must not include explicit PDB or XML file rows.' +Assert-Matches $installer 'ExcludePattern="[^"]*(^|[|])exceptionless\.txt([|]|")' 'Synchronized bin content must exclude exceptionless.txt.' +Assert-Matches $installer 'ExcludePattern="[^"]*(^|[|])\*\.pdb([|]|")' 'Synchronized bin content must exclude PDB files.' +Assert-Matches $installer 'ExcludePattern="[^"]*(^|[|])\*\.xml([|]|")' 'Synchronized bin content must exclude XML documentation files.' + +Assert-Matches $testsProject 'x64' 'Regression test runner must target x64 to avoid MSB3270.' + +[void](Assert-XmlFile 'SldWorksLookup\SldWorksLookup.csproj') +[void](Assert-XmlFile 'tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj') +[void](Assert-XmlFile 'Installer\SolidWorksLookup.aip') + +Write-Host 'Project configuration contract verified.' From 1f3b3fd590c2aa7c9eecd3fde25be59e156e5179 Mon Sep 17 00:00:00 2001 From: Paine Zeng Date: Sat, 18 Jul 2026 23:10:20 +0800 Subject: [PATCH 9/9] Close final state leaks before review Path export now balances sketch edit entry and exit around the operations that can throw, so failures do not leave SolidWorks in 2D or 3D edit state. The Release AnyCPU build writes to the installer-synchronized root bin directory, and final-review-only workflow notes are removed from the PR surface while keeping regression coverage. Constraint: Keep existing feature history intact and avoid new dependencies Rejected: Sample the merged curve inside the 2D sketch edit scope | sampling should only run after a merged curve is successfully acquired and the sketch scope has exited Confidence: high Scope-risk: narrow Tested: powershell -NoProfile -ExecutionPolicy Bypass -File tests\\Verify-ProjectConfiguration.ps1 Tested: dotnet restore SldWorksLookup.sln Tested: MSBuild SldWorksLookup.sln Release Any CPU XCadRegDll=false Tested: MSBuild SldWorksLookup.sln Release x64 XCadRegDll=false Tested: tests\\SldWorksLookup.RegressionTests\\bin\\Release\\net472\\SldWorksLookup.RegressionTests.exe Tested: tests\\SldWorksLookup.RegressionTests\\bin\\x64\\Release\\net472\\SldWorksLookup.RegressionTests.exe Tested: git diff --check Not-tested: Live SolidWorks COM export flow --- .superpowers/sdd/task-1-report.md | 193 --------- .superpowers/sdd/task-2-report.md | 59 --- .superpowers/sdd/task-3-report.md | 97 ----- .superpowers/sdd/task-4-report.md | 93 ----- SldWorksLookup/Helper/EditScope.cs | 20 + SldWorksLookup/Helper/PathExportUtil.cs | 113 ++--- .../Model/Instance/InstanceProperty.cs | 6 +- .../PathSplit/SegmentSamplingPlan.cs | 2 + SldWorksLookup/SldWorksLookup.csproj | 3 +- ...7-18-solidworkslookup-reliability-fixes.md | 387 ------------------ .../SldWorksLookup.RegressionTests/Program.cs | 44 ++ tests/Verify-ProjectConfiguration.ps1 | 17 + 12 files changed, 150 insertions(+), 884 deletions(-) delete mode 100644 .superpowers/sdd/task-1-report.md delete mode 100644 .superpowers/sdd/task-2-report.md delete mode 100644 .superpowers/sdd/task-3-report.md delete mode 100644 .superpowers/sdd/task-4-report.md create mode 100644 SldWorksLookup/Helper/EditScope.cs delete mode 100644 docs/superpowers/plans/2026-07-18-solidworkslookup-reliability-fixes.md diff --git a/.superpowers/sdd/task-1-report.md b/.superpowers/sdd/task-1-report.md deleted file mode 100644 index 48d0421..0000000 --- a/.superpowers/sdd/task-1-report.md +++ /dev/null @@ -1,193 +0,0 @@ -# Task 1 Report: Dependency-free test runner and path reliability - -## Status - -Complete. - -## Files - -- Created `tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj` -- Created `tests/SldWorksLookup.RegressionTests/Program.cs` -- Created `SldWorksLookup/PathSplit/SketchChainTopology.cs` -- Created `SldWorksLookup/PathSplit/SegmentSamplingPlan.cs` -- Modified `SldWorksLookup/PathSplit/SketchWrapper.cs` -- Modified `SldWorksLookup/PathSplit/SketchSegmentWrapper.cs` -- Modified `SldWorksLookup/PathSplit/SketchChain.cs` -- Modified `SldWorksLookup/PathSplit/ExtensionMethods.cs` -- Modified `SldWorksLookup/Helper/PathExportUtil.cs` -- Modified `SldWorksLookup/Properties/AssemblyInfo.cs` -- Modified `SldWorksLookup/SldWorksLookup.csproj` -- Modified `SldWorksLookup.sln` - -## RED - -Initial command: - -```powershell -& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo -``` - -Initial result: failed before compilation because the SDK-style project needed a restore-generated `project.assets.json`. - -Restore prerequisite: - -```powershell -& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /t:Restore /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo -``` - -Restore output: - -```text -正在确定要还原的项目… -已还原 ...\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj -``` - -Correct RED command: - -```powershell -& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo -``` - -Correct RED output excerpt: - -```text -SldWorksLookup -> ...\SldWorksLookup\bin\Release\SldWorksLookup.dll -Program.cs(75,24): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” -Program.cs(84,61): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” -Program.cs(85,61): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” -Program.cs(86,61): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” -Program.cs(87,61): error CS0103: 当前上下文中不存在名称“SegmentSamplingPlan” -Program.cs(92,20): error CS0103: 当前上下文中不存在名称“SketchChainTopology” -``` - -The RED failure was expected because the tests referenced the missing topology and sampling helpers. - -## GREEN - -Build command: - -```powershell -& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo -``` - -Build output: - -```text -SldWorksLookup -> ...\SldWorksLookup\bin\Release\SldWorksLookup.dll -SldWorksLookup.RegressionTests -> ...\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe -``` - -Runner command: - -```powershell -& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe -``` - -Runner output: - -```text -PASS PathTopologyReturnsEveryDisconnectedSegment -PASS PathTopologyReturnsClosedLoop -PASS SamplingPlanCarriesSpacingAcrossShortSegment -PASS SamplingPlanRejectsNonPositiveOrNonFiniteStep -``` - -## Self-review - -- `SketchChainTopology.Build` consumes all disconnected chains and closed-loop chains without mutating the source list. -- `SegmentSamplingPlan.Create` follows the requested spacing carry formula and rejects non-positive or non-finite step lengths. -- `SketchWrapper.GetChains` now delegates ordering/reversal to the topology helper. -- `SketchSegmentWrapper.SplitCurve` uses distance-based sampling and normalized curve parameter interpolation, avoiding divide-by-zero behavior from the old spare-length logic. -- `SketchChain.Split` validates step length once and no longer rejects short segments. -- `PathExportUtil` reuses `SketchSegmentWrapper.SourceStartPoint` and `SourceEndPoint`, removing duplicated endpoint switching logic. -- COM dereferences in `PathExportUtil` are guarded for active document, selection, sketch, sketch segments, generated paths, path segments, and segment curves. -- `ExtensionMethods.GetSkeFeat` now inspects and yields `subfeat` inside the subfeature loop. -- `git diff --check` exits 0; warnings only report LF-to-CRLF normalization. -- No temporary debug code was added. The only `Console.WriteLine` calls are the dependency-free test runner output. - -## Concerns - -- Build still emits pre-existing CS0168 warnings in `SldWorksLookup/AddIn.cs` and `SldWorksLookup/LogExtension.cs`; those files are outside Task 1 ownership and were not changed. -- The SolidWorks COM export path was build-verified but not manually exercised in SolidWorks. - -## CHANGES_REQUESTED Follow-up - -### Follow-up RED - -Added tests for: - -- first segment reversal -- connected successor reversal -- per-segment continuity -- preserving original input list membership/order -- normal sampling -- `distanceToNextPoint == segmentLength` -- immediate contextual export merge failure - -Command: - -```powershell -& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo -``` - -RED output excerpt: - -```text -SldWorksLookup -> ...\SldWorksLookup\bin\Release\SldWorksLookup.dll -Program.cs(185,40): error CS0117: “PathExportUtil”未包含“MergeCurveOrThrow”的定义 -Program.cs(189,38): error CS0117: “PathExportUtil”未包含“MergeCurveOrThrow”的定义 -``` - -The RED failure was expected because the new dependency-free export state helper did not exist. - -### Follow-up GREEN - -Build command: - -```powershell -& 'C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe' .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo -``` - -Build output excerpt: - -```text -SldWorksLookup -> ...\SldWorksLookup\bin\Release\SldWorksLookup.dll -SldWorksLookup.RegressionTests -> ...\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe -``` - -Runner command: - -```powershell -& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe -``` - -Runner output: - -```text -PASS PathTopologyReturnsEveryDisconnectedSegment -PASS PathTopologyReturnsClosedLoop -PASS PathTopologyReversesOpenFirstSegment -PASS PathTopologyReversesConnectedSuccessor -PASS PathTopologyKeepsEveryStepContinuous -PASS PathTopologyDoesNotReorderOrRemoveInputSegments -PASS SamplingPlanCarriesSpacingAcrossShortSegment -PASS SamplingPlanRejectsNonPositiveOrNonFiniteStep -PASS SamplingPlanReturnsNormalSpacing -PASS SamplingPlanHandlesExactCarryBoundary -PASS ExportMergeFailureThrowsContext -``` - -Additional verification: - -```text -git diff --check -``` - -Result: exit 0; warnings only report LF-to-CRLF normalization. - -### Follow-up Self-review - -- `PathExportUtil` now guards the result of `CreateTrimmedCurve2`, `CreateWireBody`, `doc as PartDoc`, and `MergeCurves`. -- Merge failure is handled immediately by `MergeCurveOrThrow` with contextual `InvalidOperationException`; a null merge cannot silently become the next segment on a later loop. -- `SketchChainTopology.Build` and its private helpers now use `where T : class`, matching the `ReferenceEquals` identity semantics. -- No AssemblyInfo version values changed in either commit; the only AssemblyInfo change remains `InternalsVisibleTo("SldWorksLookup.RegressionTests")`. diff --git a/.superpowers/sdd/task-2-report.md b/.superpowers/sdd/task-2-report.md deleted file mode 100644 index aefa078..0000000 --- a/.superpowers/sdd/task-2-report.md +++ /dev/null @@ -1,59 +0,0 @@ -# Task 2 Report: COM lifetime and actionable exception reporting - -## RED - -- Command: - `msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo` -- Result: failed as expected after adding Task 2 tests. -- Evidence: - - `CS0103`: `SelectionAccessScope` did not exist. - - `CS0103`: `ExceptionUtil` did not exist. - - `CS0117`: `LogExtension.TryReadConfiguration` did not exist. - -- Review-fix command: - `msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo` -- Result: failed as expected after adding construction-cleanup tests. -- Evidence: - - `CS0103`: `ConstructionCleanup` did not exist. - -## GREEN - -- Command: - `msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo` -- Result: passed. - -- Command: - `.\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe` -- Result: passed 18 regression tests, including all Task 1 and Task 2 tests. - -- Command: - `msbuild .\SldWorksLookup.sln /p:Configuration=Release '/p:Platform=Any CPU' /p:XCadRegDll=false /v:minimal /nologo` -- Result: passed. Remaining warning is the existing test-project x86/AMD64 processor architecture mismatch, not CS0168. - -- Command: - `msbuild .\SldWorksLookup.sln /p:Configuration=Release /p:Platform=x64 /p:XCadRegDll=false /v:minimal /nologo` -- Result: passed. Remaining warning is the existing test-project x86/AMD64 processor architecture mismatch, not CS0168. - -- Command: - `git diff --check` -- Result: passed. Git reported CRLF normalization warnings only. - -## Files - -- Created `SldWorksLookup/Helper/SelectionAccessScope.cs`. -- Created `SldWorksLookup/Helper/ExceptionUtil.cs`. -- Created `SldWorksLookup/Helper/ConstructionCleanup.cs`. -- Modified `tests/SldWorksLookup.RegressionTests/Program.cs`. -- Modified `SldWorksLookup/AddIn.cs`. -- Modified `SldWorksLookup/LogExtension.cs`. -- Modified `SldWorksLookup/Model/Value/LookupValue.cs`. -- Modified `SldWorksLookup/ViewModel/CaptureCmdViewModel.cs`. -- Modified `SldWorksLookup/View/CaptureCmd.xaml.cs`. -- Modified `SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs`. -- Modified `SldWorksLookup/SldWorksLookup.csproj`. - -## Risks - -- SolidWorks COM UI paths were build-verified and covered where dependency-free tests can reach them, but not manually exercised in a live SolidWorks session. -- Solution builds still report an existing architecture mismatch warning for the regression test executable referencing the x64 add-in assembly. -- `AGENTS.md` was not present in this worktree root; the task-provided AGENTS instructions were followed. diff --git a/.superpowers/sdd/task-3-report.md b/.superpowers/sdd/task-3-report.md deleted file mode 100644 index a8a246c..0000000 --- a/.superpowers/sdd/task-3-report.md +++ /dev/null @@ -1,97 +0,0 @@ -# Task 3 Report: Reflection and COM-object browsing resilience - -## RED - -Command: - -```powershell -$msbuild = (Get-Command msbuild -ErrorAction SilentlyContinue).Source -if (-not $msbuild) { - $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - $msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe | Select-Object -First 1 -} -& $msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo -& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe -``` - -Observed failures after adding the four Task 3 tests: - -- `ValueArrayInspectionHandlesNullElements` failed with `NullReferenceException`. -- `PropertyBrowsingHandlesSetterOnlyProperty` failed with `NullReferenceException`. -- `ReferenceParametersDefaultToNull` failed because `typeof(string)` defaulted to `System.Object`. -- `ComClassWithInternalIMapsToInterface` failed because `ImportDxfDwgDataClass` did not map to `IImportDxfDwgData`. - -## GREEN - -Implemented: - -- Array inspection now uses the first non-null element; value/string arrays render null entries as ``. -- Object-array browsing skips null entries instead of dereferencing them. -- Setter-only properties are surfaced as message-only properties without calling `GetValue` or dereferencing `GetMethod`. -- Method parameters use by-ref element types, return `null` defaults for reference/nullable types, and reject null only for non-nullable value types. -- `TypeMatcherUtil.Match` resolves runtime `I{name}` interfaces before the generated tuple list. -- `TypeMatcherUtil.tt` now emits only standard `IThing` interfaces and strips only the leading `I`; the checked-in generated file was updated only for affected internal-`I` tuple keys, avoiding a broad generated-list reorder. -- Feature/component lazy loading sets `NodeStatus = NodeStatus.Ok` after lazy-load attempts so repeated clicks do not duplicate children. - -Command result: - -```text -PASS ValueArrayInspectionHandlesNullElements -PASS PropertyBrowsingHandlesSetterOnlyProperty -PASS ReferenceParametersDefaultToNull -PASS ComClassWithInternalIMapsToInterface -``` - -All 22 regression tests passed. - -## Full Verification - -Commands: - -```powershell -& $msbuild .\SldWorksLookup.sln /p:Configuration=Release '/p:Platform=Any CPU' /p:XCadRegDll=false /v:minimal /nologo -& $msbuild .\SldWorksLookup.sln /p:Configuration=Release /p:Platform=x64 /p:XCadRegDll=false /v:minimal /nologo -& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe -git diff --check -``` - -Results: - -- `Release|Any CPU` solution build exited `0`. -- `Release|x64` solution build exited `0`. -- Regression executable exited `0` with all 22 tests passing. -- `git diff --check` exited `0`. - -## Risks - -- Both solution builds still emit existing `MSB3270` architecture warnings because the solution Release mappings build the add-in as Debug x64 while the SDK test project builds as AnyCPU. That configuration mismatch is explicitly assigned to Task 4, so Task 3 did not change solution mappings. -- COM behavior was verified through reflection/regression tests without launching SolidWorks, per the plan constraint. - -## CHANGES_REQUESTED Follow-up - -Additional RED coverage added: - -- `LazyLoadCompletionMarksOkAfterSuccess` -- `LazyLoadCompletionLeavesNeedRunWhenLoadThrows` -- `TypeMatcherGeneratedEntriesUseInterfacesWithSingleLeadingI` -- `ValueArrayInspectionDisplaysAllNullElements` - -Observed RED: - -- `LazyLoadCompletion` was missing before the helper was added. -- After the helper compiled, `TypeMatcherGeneratedEntriesUseInterfacesWithSingleLeadingI` failed on non-interface COM class tuples plus internal-`I` key errors such as `MatenPlace` and `PMDatumData`. -- `ValueArrayInspectionDisplaysAllNullElements` failed because all-null arrays displayed as `System.Object[]`. - -Follow-up implementation: - -- Added `LazyLoadCompletion.Run` and changed feature/component lazy loading to mark `NodeStatus.Ok` only after successful load completion. Feature load exceptions are still shown to the user and leave `NodeStatus.NeedRun` for retry. -- Removed checked-in `SolidWorksTypes` class tuples that do not satisfy the T4 rule and fixed internal-`I` tuple keys without reordering the generated table. -- Changed `TypeMatcherUtil.tt` to return a grouped/distinct list instead of calling ineffective `list.Distinct()` and discarding the result. -- Made all-null arrays render as `,`. - -Follow-up verification: - -- `Release|Any CPU` solution build exited `0`. -- `Release|x64` solution build exited `0`. -- Regression executable exited `0` with all 26 tests passing. -- `git diff --check` exited `0`. diff --git a/.superpowers/sdd/task-4-report.md b/.superpowers/sdd/task-4-report.md deleted file mode 100644 index 5703e5f..0000000 --- a/.superpowers/sdd/task-4-report.md +++ /dev/null @@ -1,93 +0,0 @@ -# Task 4 Report: Reproducible and safer build/installer configuration - -## RED - -Created `tests/Verify-ProjectConfiguration.ps1` as a configuration contract test covering: - -- Release solution mappings must map to Release project configurations and must not map to Debug. -- `SldWorksLookup.csproj` must default `XCadRegDll` to `false` while allowing command-line override. -- install/uninstall batch files must run from the script directory, quote paths, check required files, propagate `RegAsm` errorlevel, and return `0` only after success. -- installer .NET minimum must be 4.7.2. -- installer must not include explicit `exceptionless.txt`, `*.pdb`, or `*.xml` file rows. -- synchronized `..\bin` content must exclude `exceptionless.txt`, `*.pdb`, and `*.xml`. -- regression test runner must target x64. -- project and AIP XML must parse. - -Command: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\Verify-ProjectConfiguration.ps1 -``` - -Observed RED: - -```text -Release|x64 must map to project Release|x64. -``` - -The same pre-fix state also lacked the `XCadRegDll=false` default and safe batch/installer contract. - -## GREEN - -Implemented: - -- Mapped add-in solution `Release|Any CPU` and `Release|x64` to project Release configurations instead of Debug. -- Mapped regression test solution x64 configurations to project x64 and set the SDK test runner `PlatformTarget` to x64 to remove the known `MSB3270` mismatch. -- Added `false` so normal builds are side-effect free while `/p:XCadRegDll=true` still wins. -- Hardened `Install.bat` and `UnInstall.bat` with script-directory execution, quoted paths, file existence checks, raw `RegAsm` errorlevel propagation, and explicit success exit. -- Raised Advanced Installer launch-condition/display minimum .NET to 4.7.2. -- Removed explicit installer rows for `exceptionless.txt`, `.pdb`, and `.xml` files. -- Updated the existing Advanced Installer synchronized folder `ExcludePattern` with `exceptionless.txt|*.pdb|*.xml`. - -## Verification - -Commands and results: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\Verify-ProjectConfiguration.ps1 -``` - -Result: exited `0`, `Project configuration contract verified.` - -```powershell -& $msbuild .\SldWorksLookup.sln /t:Restore /p:RestorePackagesConfig=true /v:minimal /nologo -``` - -Result: exited `0`. - -```powershell -& $msbuild .\SldWorksLookup.sln /p:Configuration=Release '/p:Platform=Any CPU' /v:normal /nologo -``` - -Result: exited `0`. Captured output contained no `RegAsm`, `regsvr32`, `MSB3270`, or `warning` matches, proving the default `XCadRegDll=false` build did not run registration. - -```powershell -& $msbuild .\SldWorksLookup.sln /p:Configuration=Release /p:Platform=x64 /p:XCadRegDll=false /v:minimal /nologo -``` - -Result: exited `0`. - -```powershell -& $msbuild .\SldWorksLookup\SldWorksLookup.csproj /getProperty:XCadRegDll /nologo -& $msbuild .\SldWorksLookup\SldWorksLookup.csproj /getProperty:XCadRegDll /p:XCadRegDll=true /nologo -``` - -Results: default printed `false`; command-line override printed `true`. - -```powershell -.\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe -.\tests\SldWorksLookup.RegressionTests\bin\x64\Release\net472\SldWorksLookup.RegressionTests.exe -``` - -Results: both executables exited `0`; all 26 regression tests passed in each location. - -```powershell -git diff --check -``` - -Result: exited `0`. - -## Notes - -- `AdvancedInstaller.com` is not installed on this machine, so no `.aip` build was attempted. -- Installer validation was limited to XML parsing plus the configuration contract test against the actual AIP XML structure. diff --git a/SldWorksLookup/Helper/EditScope.cs b/SldWorksLookup/Helper/EditScope.cs new file mode 100644 index 0000000..9812163 --- /dev/null +++ b/SldWorksLookup/Helper/EditScope.cs @@ -0,0 +1,20 @@ +using System; + +namespace SldWorksLookup.Helper +{ + internal static class EditScope + { + public static void Run(Action enter, Action exit, Action body) + { + enter(); + try + { + body(); + } + finally + { + exit(); + } + } + } +} diff --git a/SldWorksLookup/Helper/PathExportUtil.cs b/SldWorksLookup/Helper/PathExportUtil.cs index c8e5e93..8d6164e 100644 --- a/SldWorksLookup/Helper/PathExportUtil.cs +++ b/SldWorksLookup/Helper/PathExportUtil.cs @@ -36,78 +36,85 @@ public static void Export(SolidWorks.Interop.sldworks.ISldWorks sw) if (ske == null) throw new InvalidOperationException("Selected feature is not a sketch."); - doc.EditSketch(); + ICurve curve = null; - var sketchSegmentArray = ske.GetSketchSegments() as object[]; - if (sketchSegmentArray == null || sketchSegmentArray.Length == 0) - throw new InvalidOperationException("Selected sketch has no sketch segments."); + EditScope.Run( + () => doc.EditSketch(), + () => doc.InsertSketch(), + () => + { + var sketchSegmentArray = ske.GetSketchSegments() as object[]; + if (sketchSegmentArray == null || sketchSegmentArray.Length == 0) + throw new InvalidOperationException("Selected sketch has no sketch segments."); - var ses = sketchSegmentArray.Cast().ToList(); + var ses = sketchSegmentArray.Cast().ToList(); - doc.ClearSelection2(true); + doc.ClearSelection2(true); - for (int i = 0; i < ses.Count; i++) - { - ses[i].Select4(true, null); - } + for (int i = 0; i < ses.Count; i++) + { + ses[i].Select4(true, null); + } - doc.SketchManager.MakeSketchChain(); - doc.ClearSelection2(true); + doc.SketchManager.MakeSketchChain(); + doc.ClearSelection2(true); - var pathArray = ske.GetSketchPaths() as object[]; - if (pathArray == null || pathArray.Length == 0) - throw new InvalidOperationException("No sketch path was generated."); + var pathArray = ske.GetSketchPaths() as object[]; + if (pathArray == null || pathArray.Length == 0) + throw new InvalidOperationException("No sketch path was generated."); - var path = pathArray.Cast().FirstOrDefault(); - if (path == null) - throw new InvalidOperationException("Generated sketch path is invalid."); + var path = pathArray.Cast().FirstOrDefault(); + if (path == null) + throw new InvalidOperationException("Generated sketch path is invalid."); - var pathSegmentArray = path.GetSketchSegments() as object[]; - if (pathSegmentArray == null || pathSegmentArray.Length == 0) - throw new InvalidOperationException("Generated sketch path has no segments."); + var pathSegmentArray = path.GetSketchSegments() as object[]; + if (pathSegmentArray == null || pathSegmentArray.Length == 0) + throw new InvalidOperationException("Generated sketch path has no segments."); - var segs = pathSegmentArray.Cast(); - - ICurve curve = null; - foreach (var seg in segs) - { - var seCurve = seg.GetCurve() as ICurve; - if (seCurve == null) - throw new InvalidOperationException("Cannot get sketch segment curve."); + var segs = pathSegmentArray.Cast(); - var wrapper = new SketchSegmentWrapper(seg); - var sp = wrapper.SourceStartPoint; - var ep = wrapper.SourceEndPoint; + foreach (var seg in segs) + { + var seCurve = seg.GetCurve() as ICurve; + if (seCurve == null) + throw new InvalidOperationException("Cannot get sketch segment curve."); - seCurve = seCurve.CreateTrimmedCurve2(sp.X, sp.Y, sp.Z, ep.X, ep.Y, ep.Z); - seCurve = RequireExportValue(seCurve, "Cannot trim sketch segment curve."); + var wrapper = new SketchSegmentWrapper(seg); + var sp = wrapper.SourceStartPoint; + var ep = wrapper.SourceEndPoint; - var body = seCurve.CreateWireBody(); - body = RequireExportValue(body, "Cannot create wire body for trimmed sketch segment curve."); + seCurve = seCurve.CreateTrimmedCurve2(sp.X, sp.Y, sp.Z, ep.X, ep.Y, ep.Z); + seCurve = RequireExportValue(seCurve, "Cannot trim sketch segment curve."); - var partDoc = RequireExportValue(doc as PartDoc, "Active document is not a part document."); - body.Display2(partDoc, Information.RGB(255, 0, 0), (int)swTempBodySelectOptions_e.swTempBodySelectOptionNone); + var body = seCurve.CreateWireBody(); + body = RequireExportValue(body, "Cannot create wire body for trimmed sketch segment curve."); - curve = MergeCurveOrThrow( - curve, - seCurve, - (left, right) => modeler.MergeCurves(new object[] { left, right }) as ICurve, - "while merging sketch path segment"); - } + var partDoc = RequireExportValue(doc as PartDoc, "Active document is not a part document."); + body.Display2(partDoc, Information.RGB(255, 0, 0), (int)swTempBodySelectOptions_e.swTempBodySelectOptionNone); - if (curve == null) - throw new InvalidOperationException("Cannot create a merged curve from the generated sketch path."); + curve = MergeCurveOrThrow( + curve, + seCurve, + (left, right) => modeler.MergeCurves(new object[] { left, right }) as ICurve, + "while merging sketch path segment"); + } - doc.InsertSketch(); + if (curve == null) + throw new InvalidOperationException("Cannot create a merged curve from the generated sketch path."); + }); var points = SplitCurve(curve, 10); - doc.Insert3DSketch(); - - foreach (var point in points) - { - doc.SketchManager.CreatePoint(point.X, point.Y, point.Z); - } + EditScope.Run( + () => doc.Insert3DSketch(), + () => doc.Insert3DSketch(), + () => + { + foreach (var point in points) + { + doc.SketchManager.CreatePoint(point.X, point.Y, point.Z); + } + }); } internal static TCurve MergeCurveOrThrow( diff --git a/SldWorksLookup/Model/Instance/InstanceProperty.cs b/SldWorksLookup/Model/Instance/InstanceProperty.cs index 81b5cad..fdd3b24 100644 --- a/SldWorksLookup/Model/Instance/InstanceProperty.cs +++ b/SldWorksLookup/Model/Instance/InstanceProperty.cs @@ -2,6 +2,7 @@ using SolidWorks.Interop.sldworks; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using System.Windows; @@ -278,7 +279,10 @@ protected bool TryMethodToLookup(MethodInfo method, object instance, out LookupP { lookupProperty = LookupMethodProperty.Create(method, instance); } - catch { } + catch (Exception ex) + { + Debug.WriteLine(ex); + } return lookupProperty != null; } diff --git a/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs b/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs index 27df90e..09f09df 100644 --- a/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs +++ b/SldWorksLookup/PathSplit/SegmentSamplingPlan.cs @@ -23,6 +23,8 @@ public static SegmentSamplingPlan Create(double segmentLength, double stepLength throw new ArgumentOutOfRangeException(nameof(stepLength)); if (segmentLength < 0 || double.IsNaN(segmentLength) || double.IsInfinity(segmentLength)) throw new ArgumentOutOfRangeException(nameof(segmentLength)); + if (distanceToNextPoint < 0 || double.IsNaN(distanceToNextPoint) || double.IsInfinity(distanceToNextPoint)) + throw new ArgumentOutOfRangeException(nameof(distanceToNextPoint)); if (distanceToNextPoint > segmentLength) return new SegmentSamplingPlan(0, 0, distanceToNextPoint - segmentLength); diff --git a/SldWorksLookup/SldWorksLookup.csproj b/SldWorksLookup/SldWorksLookup.csproj index 4011499..7d704f0 100644 --- a/SldWorksLookup/SldWorksLookup.csproj +++ b/SldWorksLookup/SldWorksLookup.csproj @@ -29,7 +29,7 @@ pdbonly true - bin\Release\ + ..\bin\ TRACE prompt 4 @@ -172,6 +172,7 @@ + diff --git a/docs/superpowers/plans/2026-07-18-solidworkslookup-reliability-fixes.md b/docs/superpowers/plans/2026-07-18-solidworkslookup-reliability-fixes.md deleted file mode 100644 index 56d475f..0000000 --- a/docs/superpowers/plans/2026-07-18-solidworkslookup-reliability-fixes.md +++ /dev/null @@ -1,387 +0,0 @@ -# SolidWorksLookup Reliability Fixes Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Fix the confirmed path-processing, COM lifecycle, exception-reporting, reflection, and packaging defects and publish the changes as a draft pull request against `develop`. - -**Architecture:** Add a dependency-free .NET Framework regression-test executable that exercises pure path topology, resource lifetime, configuration parsing, reflection, and object-array behavior without starting SolidWorks. Keep COM-facing changes local to existing UI/add-in classes, and extract helpers only for pure algorithms or `try/finally` resource boundaries. - -**Tech Stack:** C# 7.3, .NET Framework 4.7.2, WPF, SolidWorks interop, Xarial.XCad, MSBuild, PowerShell. - -## Global Constraints - -- Do not add NuGet or other project dependencies. -- Keep the production target at `.NET Framework 4.7.2` and `x64`. -- Tests must run without starting or connecting to SolidWorks. -- All build and test commands must set `XCadRegDll=false`; validation must not register the add-in. -- Preserve current public behavior except where this plan explicitly fixes a confirmed defect. -- Use minimal, locally readable C# changes; extract only pure algorithms and resource-lifetime boundaries. -- Every behavior change follows RED → GREEN and records the observed failing and passing command. -- Every commit follows the workspace Lore Commit Protocol. - ---- - -### Task 1: Dependency-free test runner and path reliability - -**Files:** -- Create: `tests/SldWorksLookup.RegressionTests/SldWorksLookup.RegressionTests.csproj` -- Create: `tests/SldWorksLookup.RegressionTests/Program.cs` -- Create: `SldWorksLookup/PathSplit/SketchChainTopology.cs` -- Create: `SldWorksLookup/PathSplit/SegmentSamplingPlan.cs` -- Modify: `SldWorksLookup/PathSplit/SketchWrapper.cs` -- Modify: `SldWorksLookup/PathSplit/SketchSegmentWrapper.cs` -- Modify: `SldWorksLookup/PathSplit/SketchChain.cs` -- Modify: `SldWorksLookup/PathSplit/ExtensionMethods.cs` -- Modify: `SldWorksLookup/Helper/PathExportUtil.cs` -- Modify: `SldWorksLookup/Properties/AssemblyInfo.cs` -- Modify: `SldWorksLookup/SldWorksLookup.csproj` -- Modify: `SldWorksLookup.sln` - -**Interfaces:** -- Produces: `SketchChainTopology.Build(IList, Func, Func, Action, Func)` -- Produces: `SegmentSamplingPlan.Create(double segmentLength, double stepLength, double distanceToNextPoint)` -- Produces: executable `tests/SldWorksLookup.RegressionTests/bin/Release/net472/SldWorksLookup.RegressionTests.exe` - -- [ ] **Step 1: Create the regression runner and failing path tests** - -Create an SDK-style `net472` console project with only a `ProjectReference` to the add-in project. Add a tiny runner that executes named `Action` tests and returns `1` on any failure. - -The initial tests must include: - -```csharp -PathTopologyReturnsEveryDisconnectedSegment(); -PathTopologyReturnsClosedLoop(); -SamplingPlanCarriesSpacingAcrossShortSegment(); -SamplingPlanRejectsNonPositiveOrNonFiniteStep(); -``` - -Use a local test-only segment class: - -```csharp -private sealed class Segment -{ - public Segment(Point3D start, Point3D end) - { - Start = start; - End = end; - } - - public Point3D Start { get; private set; } - public Point3D End { get; private set; } - - public void Reverse() - { - var start = Start; - Start = End; - End = start; - } -} -``` - -- [ ] **Step 2: Run the tests and verify RED** - -Run: - -```powershell -& $msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj ` - /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo -``` - -Expected: compilation fails because `SketchChainTopology` and `SegmentSamplingPlan` do not exist. - -- [ ] **Step 3: Implement topology and sampling** - -`SketchChainTopology.Build` must: - -```csharp -var remaining = new List(segments); -while (remaining.Count > 0) -{ - // Prefer a segment with an open endpoint; if none exists, start a closed loop. - // Reverse the first segment only when its start is connected and its end is open. - // Repeatedly consume a segment connected to the current end, reversing it when needed. - // Add the completed chain, then continue until remaining is empty. -} -``` - -`SegmentSamplingPlan.Create` must: - -```csharp -if (stepLength <= 0 || double.IsNaN(stepLength) || double.IsInfinity(stepLength)) - throw new ArgumentOutOfRangeException(nameof(stepLength)); -if (segmentLength < 0 || double.IsNaN(segmentLength) || double.IsInfinity(segmentLength)) - throw new ArgumentOutOfRangeException(nameof(segmentLength)); - -if (distanceToNextPoint > segmentLength) - return new SegmentSamplingPlan(0, 0, distanceToNextPoint - segmentLength); - -var count = (int)Math.Floor((segmentLength - distanceToNextPoint) / stepLength) + 1; -var lastDistance = distanceToNextPoint + (count - 1) * stepLength; -var nextDistance = stepLength - (segmentLength - lastDistance); -return new SegmentSamplingPlan(count, distanceToNextPoint, nextDistance); -``` - -Replace `SketchWrapper.GetChains` index mutation with the topology helper. Replace `SketchSegmentWrapper.SplitCurve` division-based logic with the sampling plan and normalized parameter interpolation. `SketchChain.Split` must validate the step once and allow segments shorter than the step. - -Delete the duplicate endpoint switch in `PathExportUtil`; construct `SketchSegmentWrapper` and reuse its `SourceStartPoint` and `SourceEndPoint`. -Validate the active document, selected feature, sketch, segment array, and generated path array before dereferencing COM results. - -In `ExtensionMethods.GetSkeFeat`, inspect and yield `subfeat` inside the subfeature loop instead of repeatedly testing and yielding the parent `feat`. - -- [ ] **Step 4: Run tests and verify GREEN** - -Run the test project, then execute its output: - -```powershell -& $msbuild .\tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj ` - /p:Configuration=Release /p:XCadRegDll=false /v:minimal /nologo -& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe -``` - -Expected: build exit `0`; four path tests pass. - -- [ ] **Step 5: Commit** - -Commit intent: `Preserve complete sketch paths during export`. - ---- - -### Task 2: COM lifetime and actionable exception reporting - -**Files:** -- Create: `SldWorksLookup/Helper/SelectionAccessScope.cs` -- Create: `SldWorksLookup/Helper/ExceptionUtil.cs` -- Modify: `tests/SldWorksLookup.RegressionTests/Program.cs` -- Modify: `SldWorksLookup/AddIn.cs` -- Modify: `SldWorksLookup/LogExtension.cs` -- Modify: `SldWorksLookup/Model/Value/LookupValue.cs` -- Modify: `SldWorksLookup/ViewModel/CaptureCmdViewModel.cs` -- Modify: `SldWorksLookup/View/CaptureCmd.xaml.cs` -- Modify: `SldWorksLookup/ViewModel/GetObjectByPIDWindowViewModel.cs` -- Modify: `SldWorksLookup/SldWorksLookup.csproj` - -**Interfaces:** -- Produces: `SelectionAccessScope.Run(Func acquire, Action release, Action body)` -- Produces: `ExceptionUtil.GetUserMessage(Exception exception)` -- Produces: `LogExtension.TryReadConfiguration(string path, out string serverUrl, out string apiKey)` - -- [ ] **Step 1: Write failing lifetime, logging, and exception tests** - -Add tests that prove: - -```csharp -SelectionAccessScopeReleasesWhenBodyThrows(); -SelectionAccessScopeDoesNotRunBodyWhenAcquireFails(); -ExceptionUtilUnwrapsTargetInvocationException(); -LogConfigurationReadsTwoTrimmedValues(); -LogConfigurationRejectsMissingOrIncompleteFile(); -``` - -The release-on-throw test must catch the body exception and assert `releaseCount == 1`. Configuration tests must use a temporary file and delete it in `finally`. - -- [ ] **Step 2: Run tests and verify RED** - -Expected: compilation fails because the three new helper APIs do not exist. - -- [ ] **Step 3: Implement minimal lifetime and reporting fixes** - -`SelectionAccessScope.Run` must acquire once and always release in `finally`: - -```csharp -if (!acquire()) - throw new InvalidOperationException("Cannot access the feature selections."); -try -{ - body(); -} -finally -{ - release(); -} -``` - -Use it around `IAdvancedHoleFeatureData.AccessSelections` and `ReleaseSelectionAccess`. Check `GetDefinition()` and near-side element results before enumeration. - -`LogExtension.TryReadConfiguration` must return `false` for a missing file, fewer than two lines, or blank values. `LogStart` must use the helper and write initialization failures to `Debug` instead of using an empty catch. - -`CmdGroup_CommandClick` and `LookupValue.OpenClick` must show `ExceptionUtil.GetUserMessage(ex)` and submit telemetry only when a client exists. Add the missing `return` after “No active doc”. - -Track open `CaptureCmd` windows in `AddIn`; close them during `OnDisconnect`. Make `CaptureCmdViewModel` idempotently `IDisposable`, and call `Dispose()` from the window’s `Closed` handler. - -- [ ] **Step 4: Run tests and verify GREEN** - -Expected: all Task 1 and Task 2 tests pass; production build has no unused-exception warnings. - -- [ ] **Step 5: Commit** - -Commit intent: `Keep SolidWorks state recoverable when commands fail`. - ---- - -### Task 3: Reflection and COM-object browsing resilience - -**Files:** -- Modify: `tests/SldWorksLookup.RegressionTests/Program.cs` -- Modify: `SldWorksLookup/Helper/ObjectMatcherUtil.cs` -- Modify: `SldWorksLookup/Helper/TypeMatcherUtil.cs` -- Modify: `SldWorksLookup/Helper/TypeMatcherUtil.tt` -- Modify: `SldWorksLookup/Model/Instance/InstanceProperty.cs` -- Modify: `SldWorksLookup/Model/Instance/MethodInstanceProperty.cs` -- Modify: `SldWorksLookup/Model/Property/LookupParameterProperty.cs` -- Modify: `SldWorksLookup/Model/Value/LookupValue.cs` -- Modify: `SldWorksLookup/Model/Tree/IComponent2InstanceTree.cs` -- Modify: `SldWorksLookup/Model/Tree/IFeatureInstanceTree.cs` - -**Interfaces:** -- Consumes: the regression runner from Task 1 -- Produces: safe array inspection, accessor enumeration, parameter defaults, and runtime COM-class-to-interface matching - -- [ ] **Step 1: Write failing reflection tests** - -Add tests: - -```csharp -ValueArrayInspectionHandlesNullElements(); -PropertyBrowsingHandlesSetterOnlyProperty(); -ReferenceParametersDefaultToNull(); -ComClassWithInternalIMapsToInterface(); -``` - -Use a local class with a setter-only property for the property test. Assert: - -```csharp -ObjectMatcherUtil.IsValueArray(new object[] { null, 42 }) == true; -LookupParameterProperty.CreateInstace(typeof(string)) == null; -TypeMatcherUtil.Match(typeof(ImportDxfDwgDataClass)) == typeof(IImportDxfDwgData); -``` - -- [ ] **Step 2: Run tests and verify RED** - -Expected: at least the null-array, reference-default, setter-only, and internal-`I` mapping assertions fail. - -- [ ] **Step 3: Implement minimal reflection fixes** - -Use the first non-null array element for type checks and render null items as ``. Skip null elements when opening object arrays. - -In `InstanceProperty.GetProperties`, independently inspect `GetMethod` and `SetMethod`. A property without a getter must become a message-only property rather than calling `GetValue`. - -Return `null` for reference parameter defaults and allow null values for reference-type method parameters. Reject null only for non-nullable value types; validate non-null values against the effective parameter type, including by-ref element types. - -Before consulting the generated tuple list, `TypeMatcherUtil.Match` must resolve: - -```csharp -var interfaceType = sourceType.Assembly.GetType($"{sourceType.Namespace}.I{name}"); -if (interfaceType != null && interfaceType.IsInterface) - return interfaceType; -``` - -Update the T4 template to include only interfaces and strip only the leading `I`. - -Set `NodeStatus = NodeStatus.Ok` after feature/component lazy loading so repeated clicks do not duplicate children. - -- [ ] **Step 4: Run tests and verify GREEN** - -Expected: all regression tests pass. - -- [ ] **Step 5: Commit** - -Commit intent: `Keep reflection browsing usable across COM edge cases`. - ---- - -### Task 4: Reproducible and safer build/installer configuration - -**Files:** -- Create: `tests/Verify-ProjectConfiguration.ps1` -- Modify: `SldWorksLookup.sln` -- Modify: `SldWorksLookup/SldWorksLookup.csproj` -- Modify: `SldWorksLookup/Install.bat` -- Modify: `SldWorksLookup/UnInstall.bat` -- Modify: `Installer/SolidWorksLookup.aip` - -**Interfaces:** -- Produces: PowerShell configuration contract test -- Produces: solution `Release` mappings that build project `Release` -- Produces: builds that do not auto-register unless explicitly overridden - -- [ ] **Step 1: Write the failing configuration test** - -The script must read repository files and throw unless: - -```powershell -$solution -match 'Release\|Any CPU\.ActiveCfg = Release\|Any CPU' -$solution -match 'Release\|x64\.ActiveCfg = Release\|x64' -$project -match 'false' -$install -match 'if errorlevel 1 exit /b' -$uninstall -match 'if errorlevel 1 exit /b' -$installer -match 'AI_REQUIRED_DOTNET_VERSION".*4\.7\.2' -$installer -notmatch 'File="exceptionless\.txt"' -``` - -- [ ] **Step 2: Run the script and verify RED** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\Verify-ProjectConfiguration.ps1 -``` - -Expected: script exits non-zero on the current Release mapping and registration settings. - -- [ ] **Step 3: Fix configuration** - -Map both solution Release configurations to project Release. Set `false` in the project so compilation is side-effect free by default. - -Both batch scripts must quote paths, validate files, propagate `RegAsm` failure, and return zero only after success: - -```bat -@echo off -setlocal -cd /d "%~dp0" || exit /b 1 -if not exist "%~dp0RegAsm.exe" exit /b 2 -if not exist "%~dp0SldWorksLookup.dll" exit /b 3 -"%~dp0RegAsm.exe" "%~dp0SldWorksLookup.dll" /codebase -if errorlevel 1 exit /b %errorlevel% -exit /b 0 -``` - -Use `/u` in the uninstall variant. Change the installer minimum runtime to `4.7.2`, remove the `exceptionless.txt` file row, and exclude `exceptionless.txt`, `*.pdb`, and `*.xml` from synchronized `bin` content. - -- [ ] **Step 4: Verify GREEN and full integration** - -Run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\Verify-ProjectConfiguration.ps1 -& $msbuild .\SldWorksLookup.sln /t:Restore /p:RestorePackagesConfig=true /v:minimal /nologo -& $msbuild .\SldWorksLookup.sln /p:Configuration=Release '/p:Platform=Any CPU' /p:XCadRegDll=false /v:minimal /nologo -& .\tests\SldWorksLookup.RegressionTests\bin\Release\net472\SldWorksLookup.RegressionTests.exe -``` - -Expected: configuration test, solution build, and all regression tests exit `0`; no COM registration runs. - -- [ ] **Step 5: Commit** - -Commit intent: `Make release builds safe and reproducible`. - ---- - -### Task 5: Final verification and publication - -**Files:** -- Review all changed files from `origin/develop...HEAD` - -**Interfaces:** -- Produces: draft pull request targeting `weianweigan/SolidWorksLookup:develop` - -- [ ] **Step 1: Run fresh full verification** - -Run configuration tests, regression tests, `Release|Any CPU`, and `Release|x64` builds with `XCadRegDll=false`. Confirm `git status --short` contains only intended tracked changes before the final commit. - -- [ ] **Step 2: Run whole-branch code review** - -Review the complete diff for correctness, scope, exception safety, installer safety, and test adequacy. Resolve every Critical or Important finding and rerun covering tests. - -- [ ] **Step 3: Publish** - -Push `agent/fix-solidworkslookup-reliability` to an authenticated user fork or the upstream repository when write permission exists. Open a draft PR against `weianweigan/SolidWorksLookup:develop` describing root causes, behavior changes, validation commands, and SolidWorks runtime limitations. diff --git a/tests/SldWorksLookup.RegressionTests/Program.cs b/tests/SldWorksLookup.RegressionTests/Program.cs index ce16ba5..c5f9702 100644 --- a/tests/SldWorksLookup.RegressionTests/Program.cs +++ b/tests/SldWorksLookup.RegressionTests/Program.cs @@ -25,9 +25,12 @@ private static int Main() PathTopologyDoesNotReorderOrRemoveInputSegments, SamplingPlanCarriesSpacingAcrossShortSegment, SamplingPlanRejectsNonPositiveOrNonFiniteStep, + SamplingPlanRejectsInvalidCarryDistance, SamplingPlanReturnsNormalSpacing, SamplingPlanHandlesExactCarryBoundary, ExportMergeFailureThrowsContext, + EditScopeExitsOnceAndRethrowsBodyException, + EditScopeExitsOnceAfterSuccess, SelectionAccessScopeReleasesWhenBodyThrows, SelectionAccessScopeDoesNotRunBodyWhenAcquireFails, ExceptionUtilUnwrapsTargetInvocationException, @@ -178,6 +181,13 @@ private static void SamplingPlanRejectsNonPositiveOrNonFiniteStep() AssertThrows(() => SegmentSamplingPlan.Create(1.0, double.PositiveInfinity, 0), "Infinite step"); } + private static void SamplingPlanRejectsInvalidCarryDistance() + { + AssertThrows(() => SegmentSamplingPlan.Create(1.0, 1.0, -0.1), "Negative carry distance"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, 1.0, double.NaN), "NaN carry distance"); + AssertThrows(() => SegmentSamplingPlan.Create(1.0, 1.0, double.PositiveInfinity), "Infinite carry distance"); + } + private static void SamplingPlanReturnsNormalSpacing() { var plan = SegmentSamplingPlan.Create(5.0, 2.0, 1.0); @@ -212,6 +222,40 @@ private static void ExportMergeFailureThrowsContext() throw new InvalidOperationException("Merge failure should include context. Message: " + ex.Message); } + private static void EditScopeExitsOnceAndRethrowsBodyException() + { + var enterCount = 0; + var exitCount = 0; + var expected = new InvalidOperationException("body failed"); + + var actual = AssertThrows( + () => EditScope.Run( + () => enterCount++, + () => exitCount++, + () => { throw expected; }), + "Body failure"); + + AssertSame(expected, actual, "Original exception"); + AssertEqual(1, enterCount, "Enter count"); + AssertEqual(1, exitCount, "Exit count"); + } + + private static void EditScopeExitsOnceAfterSuccess() + { + var enterCount = 0; + var bodyCount = 0; + var exitCount = 0; + + EditScope.Run( + () => enterCount++, + () => exitCount++, + () => bodyCount++); + + AssertEqual(1, enterCount, "Enter count"); + AssertEqual(1, bodyCount, "Body count"); + AssertEqual(1, exitCount, "Exit count"); + } + private static void SelectionAccessScopeReleasesWhenBodyThrows() { var releaseCount = 0; diff --git a/tests/Verify-ProjectConfiguration.ps1 b/tests/Verify-ProjectConfiguration.ps1 index ee75f80..5ecc5c0 100644 --- a/tests/Verify-ProjectConfiguration.ps1 +++ b/tests/Verify-ProjectConfiguration.ps1 @@ -26,6 +26,22 @@ function Assert-XmlFile([string]$relativePath) { return $xml } +function Assert-ProjectProperty([string]$relativePath, [string]$condition, [string]$propertyName, [string]$expectedValue, [string]$message) { + $xml = Assert-XmlFile $relativePath + $namespaceManager = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) + $namespaceManager.AddNamespace('msb', 'http://schemas.microsoft.com/developer/msbuild/2003') + + $propertyGroup = $xml.SelectSingleNode("//msb:PropertyGroup[@Condition=`"$condition`"]", $namespaceManager) + if ($null -eq $propertyGroup) { + throw "Missing project property group: $condition" + } + + $property = $propertyGroup.SelectSingleNode("msb:$propertyName", $namespaceManager) + if ($null -eq $property -or $property.InnerText -ne $expectedValue) { + throw $message + } +} + $solution = Read-RepoFile 'SldWorksLookup.sln' $project = Read-RepoFile 'SldWorksLookup\SldWorksLookup.csproj' $testsProject = Read-RepoFile 'tests\SldWorksLookup.RegressionTests\SldWorksLookup.RegressionTests.csproj' @@ -39,6 +55,7 @@ Assert-NotMatches $solution 'Release\|Any CPU\.(ActiveCfg|Build\.0) = Debug\|' ' Assert-NotMatches $solution 'Release\|x64\.(ActiveCfg|Build\.0) = Debug\|' 'Release|x64 must not build a Debug project configuration.' Assert-Matches $project 'false' 'XCadRegDll must default to false while allowing command-line overrides.' +Assert-ProjectProperty 'SldWorksLookup\SldWorksLookup.csproj' ' ''$(Configuration)|$(Platform)'' == ''Release|AnyCPU'' ' 'OutputPath' '..\bin\' 'Release|AnyCPU output must refresh the root bin directory used by the installer.' Assert-Matches $install 'cd /d "%~dp0" \|\| exit /b 1' 'Install.bat must run from its own directory.' Assert-Matches $install 'if not exist "%~dp0RegAsm\.exe" exit /b 2' 'Install.bat must verify RegAsm.exe exists.'