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.
- revitdevtool_pytest-0.2.0/AGENTS.md +112 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/CHANGELOG.md +28 -0
- revitdevtool_pytest-0.2.0/CLAUDE.md +1 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/PKG-INFO +79 -45
- revitdevtool_pytest-0.2.0/README.md +166 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/pyproject.toml +2 -2
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/__init__.py +1 -1
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/bridge.py +11 -50
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/connection.py +20 -28
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/constants.py +0 -1
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/dialog_resolver.py +4 -6
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/discovery.py +13 -24
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/models.py +0 -27
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/plugin.py +23 -5
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/reporting.py +1 -1
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/suite_leasing.py +7 -51
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_active_state.py +2 -2
- revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-cli/SKILL.md +0 -82
- revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md +0 -89
- revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md +0 -78
- revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-guide/SKILL.md +0 -64
- revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md +0 -97
- revitdevtool_pytest-0.1.0/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md +0 -121
- revitdevtool_pytest-0.1.0/AGENTS.md +0 -101
- revitdevtool_pytest-0.1.0/CLAUDE.md +0 -101
- revitdevtool_pytest-0.1.0/README.md +0 -132
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/.github/workflows/publish.yml +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/.gitignore +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/.vscode/settings.json +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/LICENSE +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/src/revitdevtool_pytest/suite_lock.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/_session_probe.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/conftest.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/__init__.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/constants.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/contracts.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/custom_workflow.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/field_mapping.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/header_workflow.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/model.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/serializer.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/table_cell.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/template_workflow.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/schedule/test.json +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_dependencies.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_image_export.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_revit_basic.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_revit_document.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_revit_transactions.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_schedule_custom_copy_revit.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_schedule_field_mapping_revit.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_schedule_header_capture_revit.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_schedule_header_copy_revit.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_scheduleformatting_revit.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_session_cleanliness.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_smoke.py +0 -0
- {revitdevtool_pytest-0.1.0 → revitdevtool_pytest-0.2.0}/tests/test_streaming.py +0 -0
- {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.
|
|
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 =
|
|
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` |
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
156
|
+
# With pyproject.toml configured:
|
|
118
157
|
pytest
|
|
119
158
|
|
|
120
|
-
#
|
|
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
|
|
127
|
-
2. Plugin intercepts
|
|
128
|
-
3. Test
|
|
129
|
-
4.
|
|
130
|
-
5. Results
|
|
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
|
-
##
|
|
176
|
+
## IDE Integration
|
|
133
177
|
|
|
134
|
-
|
|
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
|
-
|
|
187
|
+
### PyCharm
|
|
146
188
|
|
|
147
|
-
|
|
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
|
+
[](https://pypi.org/project/RevitDevTool.PyTest/)
|
|
4
|
+
[](https://pypi.org/project/RevitDevTool.PyTest/)
|
|
5
|
+
[](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.
|
|
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 =
|
|
44
|
+
revit_launch = false
|
|
45
45
|
revit_launch_timeout = "180"
|
|
@@ -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
|
|
85
|
+
except Exception: # noqa
|
|
89
86
|
pass
|
|
90
87
|
try:
|
|
91
88
|
win32file.CloseHandle(handle)
|
|
92
|
-
except Exception: # noqa
|
|
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
|
|
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
|
|
164
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
211
|
+
except Exception: # noqa
|
|
251
212
|
log.debug("Notification callback error for method=%s", method, exc_info=True)
|