revitdevtool-pytest 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. revitdevtool_pytest-0.2.0/AGENTS.md +112 -0
  2. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/CHANGELOG.md +28 -0
  3. revitdevtool_pytest-0.2.0/CLAUDE.md +1 -0
  4. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/PKG-INFO +79 -45
  5. revitdevtool_pytest-0.2.0/README.md +166 -0
  6. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/pyproject.toml +2 -2
  7. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/__init__.py +1 -1
  8. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/bridge.py +11 -50
  9. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/connection.py +20 -28
  10. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/constants.py +0 -1
  11. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/dialog_resolver.py +4 -6
  12. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/discovery.py +13 -24
  13. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/models.py +0 -27
  14. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/plugin.py +23 -5
  15. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/reporting.py +1 -1
  16. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/suite_leasing.py +7 -51
  17. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_active_state.py +2 -2
  18. revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-cli/SKILL.md +0 -82
  19. revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md +0 -89
  20. revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md +0 -78
  21. revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-guide/SKILL.md +0 -64
  22. revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md +0 -97
  23. revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md +0 -121
  24. revitdevtool_pytest-0.1.0/AGENTS.md +0 -101
  25. revitdevtool_pytest-0.1.0/CLAUDE.md +0 -101
  26. revitdevtool_pytest-0.1.0/README.md +0 -132
  27. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/.github/workflows/publish.yml +0 -0
  28. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/.gitignore +0 -0
  29. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/.vscode/settings.json +0 -0
  30. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/LICENSE +0 -0
  31. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/suite_lock.py +0 -0
  32. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/_session_probe.py +0 -0
  33. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/conftest.py +0 -0
  34. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/__init__.py +0 -0
  35. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/constants.py +0 -0
  36. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/contracts.py +0 -0
  37. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/custom_workflow.py +0 -0
  38. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/field_mapping.py +0 -0
  39. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/header_workflow.py +0 -0
  40. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/model.py +0 -0
  41. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/serializer.py +0 -0
  42. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/table_cell.py +0 -0
  43. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/template_workflow.py +0 -0
  44. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/test.json +0 -0
  45. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_dependencies.py +0 -0
  46. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_image_export.py +0 -0
  47. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_revit_basic.py +0 -0
  48. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_revit_document.py +0 -0
  49. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_revit_transactions.py +0 -0
  50. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_schedule_custom_copy_revit.py +0 -0
  51. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_schedule_field_mapping_revit.py +0 -0
  52. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_schedule_header_capture_revit.py +0 -0
  53. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_schedule_header_copy_revit.py +0 -0
  54. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_scheduleformatting_revit.py +0 -0
  55. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_session_cleanliness.py +0 -0
  56. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_smoke.py +0 -0
  57. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_streaming.py +0 -0
  58. {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/uv.lock +0 -0
@@ -0,0 +1,112 @@
1
+ # AGENTS
2
+
3
+ ## Overview
4
+
5
+ This is the pytest client plugin for RevitDevTool. It bridges local pytest with a live Revit process via Named Pipe JSON-RPC.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ Local pytest (collect) → Named Pipe → Revit process (PytestRunner.py) → Results → Local pytest (report)
11
+ ```
12
+
13
+ ### Module Map
14
+
15
+ | Module | Role |
16
+ |--------|------|
17
+ | `plugin.py` | Hook orchestrator — lifecycle, options, bridge setup |
18
+ | `connection.py` | Bridge lifecycle — discover, connect, lease, launch |
19
+ | `bridge.py` | Wire protocol — Named Pipe framing, request/response |
20
+ | `discovery.py` | Pipe scan, Revit registry lookup, process launch |
21
+ | `reporting.py` | Map remote CaseResult → pytest TestReport |
22
+ | `suite_leasing.py` | Cross-process Revit instance allocation (file-based) |
23
+ | `suite_lock.py` | Windows mutex for same-suite concurrency guard |
24
+ | `dialog_resolver.py` | Auto-dismiss Revit startup dialogs via Win32 |
25
+ | `models.py` | Wire protocol data models (mirrors C# PytestContracts) |
26
+ | `constants.py` | Shared constants, option names, defaults |
27
+
28
+ ### Revit-Side (in RevitDevTool repo)
29
+
30
+ | File | Role |
31
+ |------|------|
32
+ | `PytestRunner.py` | Embedded script — runs `pytest.main()` inside Revit |
33
+ | `SetupRevit.py` | Runtime setup — API refs, I/O redirection |
34
+ | `PytestExecutionService.cs` | C# orchestrator — receives pipe request, invokes Python |
35
+ | `PytestContracts.cs` | C# wire models (mirrors `models.py`) |
36
+
37
+ ## Key Design Decisions
38
+
39
+ - **Dual pytest model**: Local pytest collects tests; remote pytest executes them. This enables IDE integration (test tree, navigation) while executing in Revit's thread.
40
+ - **`--capture=sys`**: Required because fd-level capture (`os.dup2`) doesn't work in embedded Python.NET.
41
+ - **`--disable-plugin-autoload`**: Prevents third-party plugins from interfering with in-Revit execution.
42
+ - **`sys.__pytest_running__`**: Flag set by PytestRunner to prevent SetupRevit from hijacking stdout/stderr during test runs.
43
+ - **Streaming vs batch**: CLI gets real-time progress notifications; IDE adapters get one batch to avoid double-counting.
44
+ - **Suite leasing**: File-based lease store binds suite → Revit PID across processes. Mutex prevents same-suite parallel runs.
45
+ - **force_launch**: When enabled, spawns new Revit and waits for its exact PID pipe (ignores existing instances).
46
+
47
+ ## Running Tests
48
+
49
+ ```bash
50
+ # From project root with .venv activated:
51
+ pytest
52
+
53
+ # Or via uv / pixi:
54
+ uv run pytest
55
+ pixi run pytest
56
+
57
+ # Specific test:
58
+ pytest tests/test_active_state.py::test_active_view_info -v
59
+ ```
60
+
61
+ Plugin auto-enables `-rP` (show captured stdout for passing tests).
62
+
63
+ ## Configuration
64
+
65
+ All options in `pyproject.toml` under `[tool.pytest.ini_options]`:
66
+
67
+ ```toml
68
+ revit_version = "2025" # Required for launch
69
+ revit_launch = false # true = always spawn new instance
70
+ revit_timeout = "60" # Per-test timeout (seconds)
71
+ revit_launch_timeout = "180" # Startup wait (seconds)
72
+ revit_pipe = "" # Explicit pipe (bypass discovery)
73
+ ```
74
+
75
+ ## Fixtures Pattern
76
+
77
+ ```python
78
+ # conftest.py
79
+ @pytest.fixture(scope="session")
80
+ def revit_uiapp():
81
+ return __revit__ # UIApplication injected by host
82
+
83
+ @pytest.fixture(scope="session")
84
+ def revit_doc(revit_uiapp):
85
+ return revit_uiapp.ActiveUIDocument.Document
86
+
87
+ @pytest.fixture
88
+ def revit_auto_rollback(revit_transaction_service):
89
+ """Start undo tracking, revert after test."""
90
+ revit_transaction_service.StartChanges()
91
+ try:
92
+ yield revit_transaction_service
93
+ finally:
94
+ revit_transaction_service.RevertChanges()
95
+ ```
96
+
97
+ ## Common Traps
98
+
99
+ - Tests execute **inside Revit**, not locally. `import` statements for Revit API (`Autodesk.Revit.DB`) only work at test runtime.
100
+ - `__revit__` is a builtin injected by SetupRevit — always access via fixtures, not direct import.
101
+ - Revit API requires main-thread access. All tests run sequentially via FIFO queue (`IHostContextExecutor`).
102
+ - Named Pipe format: `Revit_{year}_{pid}` (e.g. `Revit_2025_12345`).
103
+ - PEP 723 dependencies in `conftest.py` are auto-installed by `PytestDependencyService` before execution.
104
+ - `force_launch` requires `revit_version` to be set — otherwise exits with config error.
105
+ - Print output inside tests is captured by pytest's `--capture=sys` mechanism and returned via `CaseResult.stdout`.
106
+
107
+ ## Change Rules
108
+
109
+ - Wire protocol changes must update both `models.py` (Python) and `PytestContracts.cs` (C#).
110
+ - Adding CLI options: register in both `pytest_addoption` (CLI) and `parser.addini` (INI).
111
+ - Connection logic is stateless by design — all functions receive parameters, no module-level mutable state except `plugin.py` globals.
112
+ - After modifying connection/discovery logic, manually test with both `revit_launch = true` and `false`.
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.0] - 2026-05-31
6
+
7
+ ### Added
8
+
9
+ - **Auto print capture:** plugin automatically enables `-rP` behavior — captured `print()` output is shown for both passing and failing tests without any user configuration.
10
+ - **PID-aware pipe discovery:** `wait_for_revit_pipe` can now wait for a specific process ID, ensuring `force_launch` connects to the newly spawned instance only.
11
+
12
+ ### Changed
13
+
14
+ - **`revit_launch` → force_launch semantics:** renamed internal parameter from `prefer_fresh` to `force_launch`. When enabled, the plugin disconnects any existing bridge, spawns a new Revit instance, and waits for that exact PID's pipe — never reuses existing instances.
15
+ - **`_opt_bool` fix:** corrected boolean option parsing so `revit_launch = true` in `pyproject.toml` is properly respected when no CLI flag is explicitly passed.
16
+ - **Type annotations refactor:** resolved false-positive IDE warnings across `bridge.py`, `connection.py`, `suite_leasing.py`, and `dialog_resolver.py`.
17
+ - **`SetupRevit.py` (Revit-side):** I/O hijacking is now conditional on `sys.__pytest_running__` flag — prevents conflict with pytest capture during test execution.
18
+ - **`PytestRunner.py` (Revit-side):** sets `sys.__pytest_running__ = True` during execution; top-level error handling wraps entire `_run()` to always return structured JSON.
19
+
20
+ ### Removed
21
+
22
+ - **`discover_tests()`** method from `bridge.py` (client-side dead code; server endpoint remains).
23
+ - **`DiscoverRequest` / `DiscoverResponse`** classes from `models.py` (unused by client).
24
+ - **`BRIDGE_METHOD_TESTS_DISCOVER`** constant (no longer referenced).
25
+ - **`resolve_existing()` / `get_suite_process_id()` / `_prune_stale()`** from `suite_leasing.py` (unused).
26
+ - **`launch_revit()`** from `discovery.py` (replaced by `start_revit` + PID tracking).
27
+ - **`addopts = "-rP"`** from `pyproject.toml` (now automatic via plugin).
28
+
29
+ ### Fixed
30
+
31
+ - **`revit_launch = true` in INI ignored:** `_opt_bool` now correctly reads INI value when CLI flag is not explicitly set.
32
+
5
33
  ## [0.1.0] - 2026-05-03
6
34
 
7
35
  ### Added
@@ -0,0 +1 @@
1
+ Refer to [AGENTS.md](./AGENTS.md) for project rules, conventions, and workflow.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: revitdevtool_pytest
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: pytest plugin for testing Revit API code via RevitDevTool Named Pipe bridge
5
5
  Project-URL: Homepage, https://github.com/trgiangv/RevitDevTool.PyTest
6
6
  Project-URL: Repository, https://github.com/trgiangv/RevitDevTool.PyTest
@@ -40,10 +40,6 @@ Tests run inside a live Revit process — write standard pytest, execute remotel
40
40
  pip install revitdevtool_pytest
41
41
  ```
42
42
 
43
- ```python
44
- import revitdevtool_pytest
45
- ```
46
-
47
43
  ## Dependencies
48
44
 
49
45
  | Package | Version |
@@ -55,11 +51,11 @@ import revitdevtool_pytest
55
51
  ## Requirements
56
52
 
57
53
  - Windows (Named Pipes)
58
- - Revit with [RevitDevTool](https://github.com/trgiangv/RevitDevTool) add-in installed
54
+ - Revit with [RevitDevTool](https://github.com/trgiangv/RevitDevTool) add-in installed and loaded
59
55
 
60
56
  ## Project Setup
61
57
 
62
- **Recommended:** scaffold your project with [uv](https://docs.astral.sh/uv/) or [pixi](https://pixi.sh/).
58
+ **Recommended:** scaffold your project with [uv](https://docs.astral.sh/uv/) or [pixi](https://pixi.sh/):
63
59
 
64
60
  ```bash
65
61
  # uv
@@ -67,92 +63,130 @@ uv init my-revit-tests
67
63
  cd my-revit-tests
68
64
  uv add revitdevtool_pytest
69
65
 
70
- # pixi
71
- pixi init my-revit-tests
66
+ # pixi (uses pyproject.toml as manifest)
67
+ pixi init --format pyproject my-revit-tests
72
68
  cd my-revit-tests
73
- pixi add revitdevtool_pytest
69
+ pixi add --pypi revitdevtool_pytest
74
70
  ```
75
71
 
76
- Both tools automatically manage virtual environments, lock files, and `pyproject.toml` — no separate `pip` setup needed.
77
-
78
72
  ## Configuration
79
73
 
80
74
  Add plugin settings to `[tool.pytest.ini_options]` in your `pyproject.toml`:
81
75
 
82
76
  ```toml
83
77
  [tool.pytest.ini_options]
84
- testpaths = ["tests"]
85
78
  revit_version = "2025"
86
- revit_launch = true
79
+ revit_launch = false
87
80
  revit_launch_timeout = "180"
88
81
  ```
89
82
 
90
- > With `uv` or `pixi`, run tests via `uv run pytest` or `pixi run pytest`.
91
-
92
- All options are settable via `[tool.pytest.ini_options]` or standard INI files (`pytest.ini`, `tox.ini`, `setup.cfg`):
93
-
94
83
  | Option | Type | Default | Description |
95
84
  |---|---|---|---|
96
85
  | `revit_version` | string | — | Revit version year (e.g. `"2025"`). Required when `revit_launch = true`. |
97
- | `revit_launch` | bool | `false` | Auto-launch Revit if no running instance found. |
86
+ | `revit_launch` | bool | `false` | Force-launch a **new** Revit instance (skip reusing existing free instances). |
98
87
  | `revit_timeout` | string | `"60"` | Per-test execution timeout in seconds. |
99
88
  | `revit_launch_timeout` | string | `"120"` | Seconds to wait for Revit to start. |
100
89
  | `revit_pipe` | string | — | Explicit pipe name (bypasses auto-discovery). |
101
90
 
102
- CLI flags override INI settings for one-off runs:
91
+ CLI flags override INI settings:
103
92
 
104
93
  ```bash
105
94
  pytest --revit-launch --revit-version=2025 -v
106
95
  ```
107
96
 
97
+ ### Connection Behavior
98
+
99
+ - **Default** (`revit_launch = false`): plugin auto-discovers a running Revit instance matching `revit_version` via Named Pipe scan. If none found, exits with error.
100
+ - **Force launch** (`revit_launch = true`): always spawns a new Revit process, waits for its pipe to appear, then connects. Existing instances are ignored.
101
+
102
+ ### Print Output
103
+
104
+ Captured `print()` output from tests running inside Revit is automatically shown in the terminal for passing tests (equivalent to `-rP`). No extra flags needed.
105
+
108
106
  ## Usage
109
107
 
108
+ ### Fixtures
109
+
110
+ Define fixtures in `conftest.py` that provide Revit API objects:
111
+
110
112
  ```python
111
- def test_revit_version():
112
- app = __revit__.Application
113
- assert "2025" in app.VersionName
113
+ import pytest
114
+
115
+ @pytest.fixture(scope="session")
116
+ def revit_uiapp():
117
+ return __revit__ # UIApplication injected by RevitDevTool
118
+
119
+ @pytest.fixture(scope="session")
120
+ def revit_app(revit_uiapp):
121
+ return revit_uiapp.Application
122
+
123
+ @pytest.fixture(scope="session")
124
+ def revit_doc(revit_uiapp):
125
+ return revit_uiapp.ActiveUIDocument.Document
114
126
  ```
115
127
 
128
+ ### Writing Tests
129
+
130
+ ```python
131
+ def test_active_view(revit_doc):
132
+ view = revit_doc.ActiveView
133
+ print(f"Active View: {view.Name}")
134
+ assert view.Name is not None
135
+
136
+ def test_revit_version(revit_app):
137
+ assert "2025" in revit_app.VersionName
138
+ ```
139
+
140
+ ### PEP 723 Dependencies
141
+
142
+ Declare test-suite dependencies in `conftest.py` using PEP 723 metadata. RevitDevTool auto-installs them before test execution:
143
+
144
+ ```python
145
+ # /// script
146
+ # dependencies = [
147
+ # "numpy>=2.0",
148
+ # "polars>=1.0",
149
+ # ]
150
+ # ///
151
+ ```
152
+
153
+ ### Running Tests
154
+
116
155
  ```bash
117
- # With pyproject.toml configured, just run:
156
+ # With pyproject.toml configured:
118
157
  pytest
119
158
 
120
- # Or override for a single run:
159
+ # Override version for a single run:
121
160
  pytest --revit-version=2026 -v
161
+
162
+ # Using uv or pixi:
163
+ uv run pytest
164
+ pixi run pytest
122
165
  ```
123
166
 
124
167
  ## How It Works
125
168
 
126
- 1. pytest discovers tests locally as usual
127
- 2. Plugin intercepts execution via `pytest_runtestloop`
128
- 3. Test source is serialized and sent over Named Pipe to Revit
129
- 4. RevitDevTool add-in executes the test inside Revit's Python (pythonnet) environment
130
- 5. Results are mapped back to pytest pass/fail/skip
169
+ 1. pytest discovers and collects tests locally
170
+ 2. Plugin intercepts `pytest_runtestloop`, connects to Revit via Named Pipe
171
+ 3. Test nodeids are sent to Revit's `PytestRunner.py` (embedded in RevitDevTool)
172
+ 4. Tests execute inside Revit's Python.NET environment with full API access
173
+ 5. Results (pass/fail/skip, stdout, stderr, tracebacks) are returned via pipe
174
+ 6. Plugin maps results back to standard pytest reports
131
175
 
132
- ## VSCode Integration
176
+ ## IDE Integration
133
177
 
134
- Add to `.vscode/settings.json`:
178
+ ### VS Code / Cursor
135
179
 
136
180
  ```json
137
181
  {
138
182
  "python.testing.pytestEnabled": true,
139
- "python.testing.pytestArgs": [
140
- "tests"
141
- ]
183
+ "python.testing.pytestArgs": ["tests"]
142
184
  }
143
185
  ```
144
186
 
145
- CLI args can go in `pytestArgs` if not configured in `pyproject.toml`:
187
+ ### PyCharm
146
188
 
147
- ```json
148
- {
149
- "python.testing.pytestArgs": [
150
- "--revit-launch",
151
- "--revit-version=2025",
152
- "tests"
153
- ]
154
- }
155
- ```
189
+ Enable pytest as the test runner. Plugin auto-detects IDE adapters and disables streaming to avoid duplicate results in the test tree.
156
190
 
157
191
  ## License
158
192
 
@@ -0,0 +1,166 @@
1
+ # RevitDevTool.PyTest
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/RevitDevTool.PyTest)](https://pypi.org/project/RevitDevTool.PyTest/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/RevitDevTool.PyTest)](https://pypi.org/project/RevitDevTool.PyTest/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ pytest plugin for testing Revit API code via RevitDevTool Named Pipe bridge.
8
+ Tests run inside a live Revit process — write standard pytest, execute remotely.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install revitdevtool_pytest
14
+ ```
15
+
16
+ ## Dependencies
17
+
18
+ | Package | Version |
19
+ |---|---|
20
+ | Python | >= 3.10 |
21
+ | pytest | >= 9.0 |
22
+ | pywin32 | >= 311 |
23
+
24
+ ## Requirements
25
+
26
+ - Windows (Named Pipes)
27
+ - Revit with [RevitDevTool](https://github.com/trgiangv/RevitDevTool) add-in installed and loaded
28
+
29
+ ## Project Setup
30
+
31
+ **Recommended:** scaffold your project with [uv](https://docs.astral.sh/uv/) or [pixi](https://pixi.sh/):
32
+
33
+ ```bash
34
+ # uv
35
+ uv init my-revit-tests
36
+ cd my-revit-tests
37
+ uv add revitdevtool_pytest
38
+
39
+ # pixi (uses pyproject.toml as manifest)
40
+ pixi init --format pyproject my-revit-tests
41
+ cd my-revit-tests
42
+ pixi add --pypi revitdevtool_pytest
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ Add plugin settings to `[tool.pytest.ini_options]` in your `pyproject.toml`:
48
+
49
+ ```toml
50
+ [tool.pytest.ini_options]
51
+ revit_version = "2025"
52
+ revit_launch = false
53
+ revit_launch_timeout = "180"
54
+ ```
55
+
56
+ | Option | Type | Default | Description |
57
+ |---|---|---|---|
58
+ | `revit_version` | string | — | Revit version year (e.g. `"2025"`). Required when `revit_launch = true`. |
59
+ | `revit_launch` | bool | `false` | Force-launch a **new** Revit instance (skip reusing existing free instances). |
60
+ | `revit_timeout` | string | `"60"` | Per-test execution timeout in seconds. |
61
+ | `revit_launch_timeout` | string | `"120"` | Seconds to wait for Revit to start. |
62
+ | `revit_pipe` | string | — | Explicit pipe name (bypasses auto-discovery). |
63
+
64
+ CLI flags override INI settings:
65
+
66
+ ```bash
67
+ pytest --revit-launch --revit-version=2025 -v
68
+ ```
69
+
70
+ ### Connection Behavior
71
+
72
+ - **Default** (`revit_launch = false`): plugin auto-discovers a running Revit instance matching `revit_version` via Named Pipe scan. If none found, exits with error.
73
+ - **Force launch** (`revit_launch = true`): always spawns a new Revit process, waits for its pipe to appear, then connects. Existing instances are ignored.
74
+
75
+ ### Print Output
76
+
77
+ Captured `print()` output from tests running inside Revit is automatically shown in the terminal for passing tests (equivalent to `-rP`). No extra flags needed.
78
+
79
+ ## Usage
80
+
81
+ ### Fixtures
82
+
83
+ Define fixtures in `conftest.py` that provide Revit API objects:
84
+
85
+ ```python
86
+ import pytest
87
+
88
+ @pytest.fixture(scope="session")
89
+ def revit_uiapp():
90
+ return __revit__ # UIApplication injected by RevitDevTool
91
+
92
+ @pytest.fixture(scope="session")
93
+ def revit_app(revit_uiapp):
94
+ return revit_uiapp.Application
95
+
96
+ @pytest.fixture(scope="session")
97
+ def revit_doc(revit_uiapp):
98
+ return revit_uiapp.ActiveUIDocument.Document
99
+ ```
100
+
101
+ ### Writing Tests
102
+
103
+ ```python
104
+ def test_active_view(revit_doc):
105
+ view = revit_doc.ActiveView
106
+ print(f"Active View: {view.Name}")
107
+ assert view.Name is not None
108
+
109
+ def test_revit_version(revit_app):
110
+ assert "2025" in revit_app.VersionName
111
+ ```
112
+
113
+ ### PEP 723 Dependencies
114
+
115
+ Declare test-suite dependencies in `conftest.py` using PEP 723 metadata. RevitDevTool auto-installs them before test execution:
116
+
117
+ ```python
118
+ # /// script
119
+ # dependencies = [
120
+ # "numpy>=2.0",
121
+ # "polars>=1.0",
122
+ # ]
123
+ # ///
124
+ ```
125
+
126
+ ### Running Tests
127
+
128
+ ```bash
129
+ # With pyproject.toml configured:
130
+ pytest
131
+
132
+ # Override version for a single run:
133
+ pytest --revit-version=2026 -v
134
+
135
+ # Using uv or pixi:
136
+ uv run pytest
137
+ pixi run pytest
138
+ ```
139
+
140
+ ## How It Works
141
+
142
+ 1. pytest discovers and collects tests locally
143
+ 2. Plugin intercepts `pytest_runtestloop`, connects to Revit via Named Pipe
144
+ 3. Test nodeids are sent to Revit's `PytestRunner.py` (embedded in RevitDevTool)
145
+ 4. Tests execute inside Revit's Python.NET environment with full API access
146
+ 5. Results (pass/fail/skip, stdout, stderr, tracebacks) are returned via pipe
147
+ 6. Plugin maps results back to standard pytest reports
148
+
149
+ ## IDE Integration
150
+
151
+ ### VS Code / Cursor
152
+
153
+ ```json
154
+ {
155
+ "python.testing.pytestEnabled": true,
156
+ "python.testing.pytestArgs": ["tests"]
157
+ }
158
+ ```
159
+
160
+ ### PyCharm
161
+
162
+ Enable pytest as the test runner. Plugin auto-detects IDE adapters and disables streaming to avoid duplicate results in the test tree.
163
+
164
+ ## License
165
+
166
+ [MIT](LICENSE)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "revitdevtool_pytest"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "pytest plugin for testing Revit API code via RevitDevTool Named Pipe bridge"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -41,5 +41,5 @@ packages = ["src/revitdevtool_pytest"]
41
41
  testpaths = ["tests"]
42
42
  revit_version = "2025"
43
43
  revit_timeout = "60"
44
- revit_launch = true
44
+ revit_launch = false
45
45
  revit_launch_timeout = "180"
@@ -1,3 +1,3 @@
1
1
  """RevitDevTool.PyTest: Run pytest tests inside a live Revit process via Named Pipe bridge."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
@@ -13,7 +13,6 @@ import time
13
13
  from typing import Any, Callable
14
14
 
15
15
  from .constants import (
16
- BRIDGE_METHOD_TESTS_DISCOVER,
17
16
  BRIDGE_METHOD_TESTS_RUN,
18
17
  BRIDGE_MSG_TYPE_NOTIFICATION,
19
18
  DEFAULT_CONNECT_TIMEOUT_MS,
@@ -24,8 +23,6 @@ from .models import (
24
23
  BridgeRequest,
25
24
  BridgeResponse,
26
25
  CollectionError,
27
- DiscoverRequest,
28
- DiscoverResponse,
29
26
  RunRequest,
30
27
  RunResponse,
31
28
  )
@@ -69,7 +66,7 @@ class RevitBridge:
69
66
  self._handle, win32pipe.PIPE_READMODE_BYTE, None, None,
70
67
  )
71
68
  return
72
- except Exception:
69
+ except Exception: # noqa
73
70
  if time.monotonic() >= deadline:
74
71
  raise ConnectionError(
75
72
  f"Cannot connect to pipe '{self._pipe_name}' "
@@ -85,11 +82,11 @@ class RevitBridge:
85
82
 
86
83
  try:
87
84
  win32file.FlushFileBuffers(handle)
88
- except Exception: # noqa: BLE001
85
+ except Exception: # noqa
89
86
  pass
90
87
  try:
91
88
  win32file.CloseHandle(handle)
92
- except Exception: # noqa: BLE001
89
+ except Exception: # noqa
93
90
  pass
94
91
 
95
92
  @property
@@ -101,31 +98,12 @@ class RevitBridge:
101
98
 
102
99
  win32pipe.PeekNamedPipe(self._handle, 0)
103
100
  return True
104
- except Exception: # noqa: BLE001
101
+ except Exception: # noqa
105
102
  log.debug("Pipe health check failed, marking as disconnected")
106
103
  return False
107
104
 
108
105
  # -- Public RPC methods -------------------------------------------------
109
106
 
110
- def discover_tests(
111
- self,
112
- workspace_root: str,
113
- test_root: str,
114
- *,
115
- pytest_args: list[str] | None = None,
116
- timeout_s: float = DEFAULT_TEST_TIMEOUT_S,
117
- ) -> DiscoverResponse:
118
- request = DiscoverRequest(
119
- workspace_root=workspace_root,
120
- test_root=test_root,
121
- pytest_args=pytest_args or [],
122
- )
123
- response = self._request(
124
- BridgeRequest(method=BRIDGE_METHOD_TESTS_DISCOVER, params=request.to_params()),
125
- timeout_s,
126
- )
127
- return _parse_discover_response(response)
128
-
129
107
  def run_tests(
130
108
  self,
131
109
  workspace_root: str,
@@ -142,7 +120,7 @@ class RevitBridge:
142
120
  nodeids=nodeids,
143
121
  pytest_args=pytest_args or [],
144
122
  )
145
- response = self._request(
123
+ response: BridgeResponse = self._request(
146
124
  BridgeRequest(method=BRIDGE_METHOD_TESTS_RUN, params=request.to_params()),
147
125
  timeout_s,
148
126
  on_notification=on_notification,
@@ -160,20 +138,17 @@ class RevitBridge:
160
138
  ) -> BridgeResponse:
161
139
  self._write_frame(req.to_json_bytes())
162
140
  deadline = time.monotonic() + timeout_s
163
- while True:
164
- remaining = deadline - time.monotonic()
165
- if remaining <= 0:
166
- raise TimeoutError(f"Timed out waiting for response to {req.id}")
167
- data = self._read_frame(remaining)
141
+ while time.monotonic() < deadline:
142
+ data = self._read_frame(deadline - time.monotonic())
168
143
  parsed = json.loads(data)
169
144
 
170
145
  if parsed.get("type") == BRIDGE_MSG_TYPE_NOTIFICATION:
171
146
  _dispatch_notification(parsed, on_notification)
172
147
  continue
173
148
 
174
- resp = BridgeResponse.from_json(parsed)
175
- if resp.id == req.id:
176
- return resp
149
+ return BridgeResponse.from_json(parsed)
150
+
151
+ raise TimeoutError(f"Timed out waiting for response to {req.id}")
177
152
 
178
153
  def _write_frame(self, body: bytes) -> None:
179
154
  import win32file # type: ignore[import-untyped]
@@ -207,20 +182,6 @@ class RevitBridge:
207
182
  # ---------------------------------------------------------------------------
208
183
 
209
184
 
210
- def _parse_discover_response(response: BridgeResponse) -> DiscoverResponse:
211
- if response.is_error:
212
- return DiscoverResponse(
213
- collection_errors=(CollectionError(message=response.error_message),),
214
- )
215
- if isinstance(response.result, dict):
216
- return DiscoverResponse.from_dict(response.result)
217
- return DiscoverResponse(
218
- collection_errors=(
219
- CollectionError(message=f"Unexpected response: {response.result}"),
220
- ),
221
- )
222
-
223
-
224
185
  def _parse_run_response(response: BridgeResponse) -> RunResponse:
225
186
  if response.is_error:
226
187
  return RunResponse(
@@ -247,5 +208,5 @@ def _dispatch_notification(
247
208
  params = parsed.get("params")
248
209
  try:
249
210
  callback(method, params)
250
- except Exception: # noqa: BLE001
211
+ except Exception: # noqa
251
212
  log.debug("Notification callback error for method=%s", method, exc_info=True)