Skip to content

Commit a8c01b6

Browse files
committed
docs(nunit): add portable revit-nunit skill and nuget setup README
Point agents at the HostName/MTP filter/global.json contract. Keep the nuget.org README to consumer setup only.
1 parent 782556a commit a8c01b6

8 files changed

Lines changed: 402 additions & 10 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
name: revit-nunit
3+
description: >
4+
Configure and run in-host NUnit tests with the RevitDevTool.NUnit NuGet
5+
package (Microsoft Testing Platform). Use in any repo that references that
6+
package when writing or running NUnit tests inside Revit, AutoCAD, or
7+
Civil 3D; setting HostName/HostVersion/HostLaunch; using `dotnet test --filter`;
8+
selecting [Explicit] tests; or diagnosing MTP exit code 8 / zero tests.
9+
---
10+
11+
# Host NUnit tests (RevitDevTool.NUnit)
12+
13+
Standalone consumer skill. Copy this folder into any repo (or
14+
`~/.agents/skills/revit-nunit/`).
15+
16+
```
17+
dotnet test (MTP exe) → installed DevTools.NUnit.Runner → host pipe → NUnit 4.6.1
18+
```
19+
20+
The MTP exe never runs test bodies locally. Requires
21+
[RevitDevTool](https://github.com/trgiangv/RevitDevTool) installed and NuGet
22+
`RevitDevTool.NUnit`.
23+
24+
Detect: `PackageReference` `RevitDevTool.NUnit` + `global.json` in **that
25+
project folder** (not the repo root) with
26+
`"test": { "runner": "Microsoft.Testing.Platform" }`.
27+
28+
## Configure
29+
30+
Pin **NUnit 4.6.1**. Do not add `NUnit.Microsoft.Testing.Platform`,
31+
`NUnit3TestAdapter`, or `ricaun.RevitTest.TestAdapter`.
32+
33+
```xml
34+
<PropertyGroup>
35+
<IsTestProject>true</IsTestProject>
36+
<OutputType>Exe</OutputType>
37+
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
38+
<HostName>Revit</HostName>
39+
<HostVersion>2024</HostVersion>
40+
<HostLaunch>false</HostLaunch>
41+
<HostTimeout>60</HostTimeout>
42+
<HostLaunchTimeout>360</HostLaunchTimeout>
43+
</PropertyGroup>
44+
<ItemGroup>
45+
<PackageReference Include="RevitDevTool.NUnit" />
46+
<PackageReference Include="Microsoft.Testing.Platform.MSBuild" />
47+
<PackageReference Include="NUnit" Version="4.6.1" />
48+
</ItemGroup>
49+
```
50+
51+
Create `global.json` **in the test project folder** (same directory as the
52+
`.csproj`). Do **not** put it at the repo root — that forces every test
53+
project in the tree onto MTP.
54+
55+
```json
56+
{
57+
"sdk": { "version": "10.0.0", "rollForward": "latestMinor" },
58+
"test": { "runner": "Microsoft.Testing.Platform" }
59+
}
60+
```
61+
62+
Property meanings, RID/`OutputType`, and conflicting packages:
63+
[project-setup.md](references/project-setup.md).
64+
65+
## Run
66+
67+
Always `cd` to the **test project folder** (where `global.json` and the
68+
`.csproj` live) before `dotnet test`. Do not run from the repo root.
69+
70+
```powershell
71+
cd path/to/Host.Tests
72+
dotnet test --project Host.Tests.csproj -c <Config> --filter MethodName
73+
dotnet test --project Host.Tests.csproj -c <Config> -- --filter MethodName
74+
dotnet test --project Host.Tests.csproj -c <Config> --list-tests
75+
```
76+
77+
`--filter` is the **NUnit method name only**. Same command runs `[Explicit]`.
78+
Do not start `Revit.exe` / `acad.exe` yourself.
79+
80+
Filter / exit 8: [mtp-filter.md](references/mtp-filter.md).
81+
82+
## Write tests
83+
84+
Bodies run on the Autodesk API context. Use the host context type for
85+
`Application`, `TestContext.WorkDirectory` for assets. Patterns:
86+
[test-patterns.md](references/test-patterns.md).
87+
88+
## Common mistakes
89+
90+
| Mistake | Fix |
91+
|---------|-----|
92+
| `--filter "Name=…"` / `FullyQualifiedName~` | `--filter MethodName` |
93+
| `[Explicit]` never runs | Select it with `--filter MethodName` |
94+
| No exe / test project does not run | Set `OutputType=Exe` and `RuntimeIdentifiers=win-x64` in the csproj |
95+
| Ran from repo root / another project | `cd` to the test project folder that has `global.json` |
96+
| Timeout | Raise `HostTimeout` (60s is smoke-only) |
97+
98+
## Package
99+
100+
- NuGet: [RevitDevTool.NUnit](https://www.nuget.org/packages/RevitDevTool.NUnit)
101+
- Installer / Runner: [RevitDevTool](https://github.com/trgiangv/RevitDevTool)
102+
103+
## References
104+
105+
- [project-setup.md](references/project-setup.md)
106+
- [mtp-filter.md](references/mtp-filter.md)
107+
- [test-patterns.md](references/test-patterns.md)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# MTP filter
2+
3+
`--filter` on `dotnet test` is forwarded to the test exe. This package does
4+
**not** parse `Name=` / `FullyQualifiedName~` / `Category=` expressions. The
5+
entire argument becomes NUnit `<name>`.
6+
7+
## Commands
8+
9+
Always `cd` to the **test project folder** (`.csproj` + `global.json`).
10+
Do not run `dotnet test` from the repo root.
11+
12+
```powershell
13+
cd path/to/Host.Tests
14+
dotnet test --project Host.Tests.csproj -c <Config> --filter MethodName
15+
```
16+
17+
If the SDK binds `--filter` itself:
18+
19+
```powershell
20+
dotnet test --project Host.Tests.csproj -c <Config> -- --filter MethodName
21+
```
22+
23+
List tests without starting a host:
24+
25+
```powershell
26+
dotnet test --project Host.Tests.csproj -c <Config> --list-tests
27+
```
28+
29+
Do **not** start `Revit.exe` / `acad.exe`. Runner locates, reuses, or
30+
launches. `HostLaunch=false` still starts a matching-version host on **run**
31+
if none is open. Discovery (`--list-tests`, Test Explorer refresh) is local
32+
PE metadata.
33+
34+
## What matches
35+
36+
| Command | Result |
37+
|---------|--------|
38+
| `--filter Refresh_WritesTheCurrentModel` | Selects that method; unlocks `[Explicit]` |
39+
| `--filter "Name=Refresh_WritesTheCurrentModel"` | No match → **exit 8, Zero tests ran** |
40+
| `--filter "FullyQualifiedName~Refresh_…"` | Same: literal name, no match |
41+
| no `--filter` | Whole assembly; `[Explicit]` is **Skipped**, not run |
42+
43+
Exit **8** after a few seconds with zero cases = wrong filter, not a dead
44+
host. Retry with the bare method name.
45+
46+
Test Explorer click sends FullName UIDs (`--test` on Runner). CLI must use
47+
the method name.
48+
49+
`[Explicit]` runs only when the filter selects that test. Selecting by
50+
method name is enough.
51+
52+
## Host proof
53+
54+
Test output may include `host-pid=…`. Use that PID to confirm execution
55+
inside the Autodesk process, not the MTP exe.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Project setup
2+
3+
Consumer csproj + `global.json` for `RevitDevTool.NUnit`. CLI commands stay
4+
in SKILL.md.
5+
6+
## Required csproj
7+
8+
```xml
9+
<PropertyGroup>
10+
<IsTestProject>true</IsTestProject>
11+
<OutputType>Exe</OutputType>
12+
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
13+
<HostName>Revit</HostName>
14+
<HostVersion>2024</HostVersion>
15+
<HostLaunch>false</HostLaunch>
16+
<HostTimeout>60</HostTimeout>
17+
<HostLaunchTimeout>360</HostLaunchTimeout>
18+
</PropertyGroup>
19+
<ItemGroup>
20+
<PackageReference Include="RevitDevTool.NUnit" />
21+
<PackageReference Include="Microsoft.Testing.Platform.MSBuild" />
22+
<PackageReference Include="NUnit" Version="4.6.1" />
23+
</ItemGroup>
24+
```
25+
26+
Pin **NUnit 4.6.1** (`nunit.framework` file version `4.6.1.0`). The host
27+
generation snapshot rejects a missing or mismatched framework DLL.
28+
29+
Reference `Microsoft.Testing.Platform.MSBuild`, not
30+
`Microsoft.Testing.Platform` as a compile package.
31+
32+
| Property | Role |
33+
|----------|------|
34+
| `OutputType` | Must be `Exe`. MTP is a test application. Revit/CAD `Directory.Build` often resets `Library` — set this in the test csproj so it wins. Package props also set `Exe`; consumer SDK still overrides unless the csproj repeats it |
35+
| `RuntimeIdentifiers` | Must be `win-x64`. Autodesk `PlatformTarget=x64` infers that RID; without this, restore/`dotnet test` cannot find the exe |
36+
| `HostName` | `Revit`, `AutoCad`, `Civil3D`, … |
37+
| `HostVersion` | Year string (`2024`, `2026`). May be `$(RevitVersion)` if the project already defines it |
38+
| `HostLaunch` | `false` = reuse a matching host, start if none. `true` = always start a new host |
39+
| `HostTimeout` | Whole `nunit/run` pipe timeout (seconds). Raise for large suites; 60 is smoke-only |
40+
| `HostLaunchTimeout` | Seconds to wait for a launched host |
41+
42+
`HostName` / `HostVersion` are the runner contract. Do not invent other
43+
MSBuild flags for the runner.
44+
45+
Build writes `devtools.nunit.host.json` beside the test exe. Do not edit it
46+
by hand.
47+
48+
On **net48**, package targets ILRepack the test exe and delete merged DLLs
49+
(`System.Text.Json`, product libraries, MTP, …). Keep `nunit.framework`
50+
4.6.1 loose — do not merge it. Do not add a separate `System.Text.Json`
51+
PackageReference to “fix” host `FileNotFoundException`. Skip merge with
52+
`<DevToolsNUnitRepack>false</DevToolsNUnitRepack>`. Extra exclude names:
53+
`DevToolsNUnitRepackBinariesExcludes` (semicolon-separated file names).
54+
This is not add-in `IsRepackable`.
55+
56+
## global.json
57+
58+
Create it **in the test project folder** (beside the `.csproj`). Do **not**
59+
put it at the repo or solution root.
60+
61+
```text
62+
repo/
63+
global.json ← do not put MTP runner here
64+
tests/
65+
Host.Tests/
66+
Host.Tests.csproj
67+
global.json ← here
68+
```
69+
70+
```json
71+
{
72+
"sdk": {
73+
"version": "10.0.0",
74+
"rollForward": "latestMinor"
75+
},
76+
"test": {
77+
"runner": "Microsoft.Testing.Platform"
78+
}
79+
}
80+
```
81+
82+
A root `global.json` with `"runner": "Microsoft.Testing.Platform"` applies
83+
to every `dotnet test` under that tree and breaks non-MTP projects.
84+
85+
Use a .NET 10 SDK. Match `-c` to the consumer configurations (`Debug.R24`,
86+
`Release`, …).
87+
88+
Always `cd` to that project folder before `dotnet test` so the SDK picks
89+
up this `global.json`. Running from the repo root ignores it.
90+
91+
## Conflicting packages
92+
93+
Package targets fail the build if the same project also references:
94+
95+
- `NUnit.Microsoft.Testing.Platform`
96+
- `NUnit3TestAdapter`
97+
- `ricaun.RevitTest.TestAdapter`
98+
- `Microsoft.Testing.Extensions.VSTestBridge`
99+
100+
Keep one owner: `RevitDevTool.NUnit`.
101+
102+
## Runner install
103+
104+
`%APPDATA%/Autodesk/ApplicationPlugins/RevitDevTool.bundle/Contents/DevTools.NUnit.Runner.exe`
105+
106+
Override with MSBuild `DevToolsNUnitRunnerPath` only when the bundle is not
107+
in the default location. Missing file → `"RevitDevTool is not installed"`.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Test patterns
2+
3+
Bodies run on the Autodesk API context (`RunOnMainThread`). WPF
4+
`Dispatcher.Invoke` is not an API context. Read `Application` from the
5+
host's context type, not from the MTP package.
6+
7+
`Assembly.Location` is empty (stream-load). Locate Content/assets with
8+
`TestContext.WorkDirectory` (generation shadow of the test output).
9+
10+
## 1. Smoke — prove the host process
11+
12+
```csharp
13+
[Test]
14+
public void Runs_inside_revit()
15+
{
16+
var revitApi = AppDomain.CurrentDomain.GetAssemblies()
17+
.FirstOrDefault(a =>
18+
string.Equals(a.GetName().Name, "RevitAPI", StringComparison.OrdinalIgnoreCase));
19+
20+
Assert.That(revitApi, Is.Not.Null, "Host tests must execute inside Revit, not the MTP exe.");
21+
Console.WriteLine($"host-pid={Process.GetCurrentProcess().Id}");
22+
}
23+
```
24+
25+
## 2. Host application context
26+
27+
Use the project's host helper (Inspexel `RevitContext`, Nice3point
28+
`RevitApiContext`, …):
29+
30+
```csharp
31+
[Test]
32+
public void Reads_application_version()
33+
{
34+
var version = RevitApiContext.Application.VersionBuild;
35+
Assert.That(version, Is.Not.Null.And.Not.Empty);
36+
Console.WriteLine(version);
37+
}
38+
```
39+
40+
## 3. Content files via WorkDirectory
41+
42+
```csharp
43+
[Test]
44+
public void Loads_content_from_shadow()
45+
{
46+
var path = Path.Combine(TestContext.WorkDirectory, "Testdata", "model.rvt");
47+
Assert.That(File.Exists(path), Is.True, path);
48+
}
49+
```
50+
51+
Mark files `<Content CopyToOutputDirectory="PreserveNewest" />` so they copy
52+
into the generation shadow.
53+
54+
## 4. One-shot / Explicit
55+
56+
```csharp
57+
[Explicit("Writes the live model; run with --filter Refresh_WritesTheCurrentModel")]
58+
[Test]
59+
public void Refresh_WritesTheCurrentModel()
60+
{
61+
// selected only when --filter names this method
62+
}
63+
```
64+
65+
Without `--filter`, NUnit skips Explicit. Do not expect `Name=` expressions
66+
to unlock it — see [mtp-filter.md](mtp-filter.md).
67+
68+
## 5. Setup lifecycle
69+
70+
`[SetUp]`, `[TearDown]`, `[OneTimeSetUp]`, `[SetUpFixture]` are NUnit's.
71+
They run again on each `nunit/run`. User **static fields** on net48 do
72+
**not** reset between `dotnet test` invocations on the same host PID.
73+
Restart the host if static or event state is dirty.
74+
75+
```csharp
76+
[SetUpFixture]
77+
public sealed class AssemblySetUp
78+
{
79+
[OneTimeSetUp]
80+
public void Init() { /* per run, not per process */ }
81+
}
82+
83+
[TestFixture]
84+
public sealed class Fixture
85+
{
86+
[OneTimeSetUp]
87+
public void FixtureInit() { }
88+
89+
[SetUp]
90+
public void PerTest() { }
91+
}
92+
```
93+
94+
net8+ hosts isolate a **rebuilt** generation by content hash. Live unload
95+
is not guaranteed. Same-generation re-runs still share statics.
96+
97+
## 6. Output
98+
99+
| API | IDE / `dotnet test` stdout | Host log pane (tracing on) |
100+
|-----|----------------------------|----------------------------|
101+
| `Console.WriteLine` / `TestContext.WriteLine` | Yes (`CaseResult.Output`) | Forwarded at case finish |
102+
| `Trace.WriteLine` / `Debug.WriteLine` | Merged into stdout | Process `Trace.Listeners` |
103+
104+
Do not add extra listeners to "help" the pane.
105+
106+
## 7. Timeout
107+
108+
`HostTimeout` covers the entire `nunit/run`, not one assertion. Raise it
109+
for large suites. NUnit `RunOnMainThread` cannot cancel an in-flight test.

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Read the **minimum** layer for the task. Do not duplicate docs into chat.
2727
| Need logs to diagnose host/Daemon | `docs/agents/verification.md` → Diagnostic logs |
2828
| Host pytest/control pipe (in-repo) | `docs/agents/mcp-pytest-bridge.md` |
2929
| Revit API + execute in host | `.agents/skills/revit-developer/SKILL.md` |
30+
| NUnit host tests | `.agents/skills/revit-nunit/SKILL.md` |
31+
| pytest host tests | `.agents/skills/revit-pytest/SKILL.md` |
3032
| Platform / IPC / packaging edit | `.agents/skills/platform-change/SKILL.md` |
3133

3234
## Verify before done

docs/agents/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Build/deploy traps: `.agents/skills/build/SKILL.md`.
99
|------|------|----------|
1010
| Compile / deploy / verify | `build` skill, `verification.md`, `build-matrix.md`, `known-test-gaps.md` | `source/`, `scripts/`, `tests/` |
1111
| MCP integration testing | `mcp-integration-test.md` | Host + daemon |
12-
| NUnit host testing (experimental) | `nunit-host-testing.md`, `nunit-native-runtime-verification.md`, `docs/product/nunit-host-testing.md` | `DevTools.NUnit.*` |
12+
| NUnit host testing (experimental) | `revit-nunit` skill, `nunit-host-testing.md`, `docs/product/nunit-host-testing.md` | `DevTools.NUnit.*` |
1313
| MCP agent efficiency | `docs/plans/completed/2026-07-26-mcp-agent-efficiency.md` | `DevTools.Mcp.*`, daemon |
1414
| Execution / MCP / host pipe / logging | Matching `docs/agents/*.md` + `docs/architecture/<Module>/` + `docs/product/` | `DevTools.*`, hosts |
1515
| Revit API + live execute | `revit-developer` skill, `architecture/MCP/workflows.md` | MCP + rvtdocs-mcp |

0 commit comments

Comments
 (0)