Files
willtools-demo/WillTools.extension/tests/test_wpf_smoke.py
T
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at cbe45c5
(2026-09-09). Code, docs and public catalogue only; no plant or job
knowledge layers, no client or job names (tools/scrub.py clean). The
intro deck is left out of this snapshot because its slide images could
not be checked for job drawings.
2026-09-09 08:32:15 -05:00

329 lines
14 KiB
Python

# -*- coding: utf-8 -*-
"""Tests for tools/wpf_smoke.py -- the bridge smoke-import for WPF buttons.
WHY IT EXISTS
-------------
A wrong WPF namespace is invisible to host tests: host CPython cannot import
System.Windows at all, so `from System.Windows.Media import StretchDirection`
(a type that lives in System.Windows.Controls) passes every host gate and
dies at import the first time the owner clicks the button. wpf_smoke collects
the button's `System.*` / `System.Windows.*` import statements and execs them
inside Revit through the bridge, where the .NET assemblies exist.
WHAT IS TESTED HERE
-------------------
The two pieces that are pure host CPython, exercised with no network and no
bridge:
* the statement COLLECTOR -- single-line imports, parenthesised multi-line
imports, and the two failure shapes that matter: a non-System import that
must be ignored, and an *unbalanced* parenthesised import (a syntax error
in the source) that must still terminate instead of consuming the rest of
the file.
* the SNIPPET BUILDER -- one `_n += 1` / `try: exec(...)` pair per
statement, and the final `result = {...}` line that is /exec's contract
for sending data back.
Plus one verdict test, monkeypatching the bridge call so no socket is opened:
an /exec answer that reports import errors must judge FAIL, and a bridge
REFUSAL must judge COULD NOT JUDGE (never OK). The verdict text is read via
capsys rather than returned, because wpf_smoke mirrors the repo's other tools
(scrub.py, rvt.py): `main()` prints and returns the exit code.
The bridge-dependent pieces (the real /exec round-trip) are deliberately NOT
tested here -- that needs a live Revit session and belongs to a dry-run, the
same boundary test_rvt_bridges.py draws.
Module-level imports below are Python 2 compatible, matching the other test
files in this suite.
"""
import os
import sys
import pytest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
TOOLS = os.path.join(ROOT, 'tools')
if TOOLS not in sys.path:
sys.path.insert(0, TOOLS)
wpf_smoke = pytest.importorskip('wpf_smoke')
# ------------------------------------------------------------ the collector
def test_single_line_system_import_is_collected_verbatim():
src = 'from System.Windows.Media import Stretch, Brushes\n'
assert wpf_smoke.collect_system_imports(src) == \
['from System.Windows.Media import Stretch, Brushes']
def test_plain_import_system_is_collected():
src = 'import System\n'
assert wpf_smoke.collect_system_imports(src) == ['import System']
def test_parenthesised_multi_line_import_is_collapsed_to_one_line():
"""The real WPF shape -- PanelAnalysis.pushbutton's import block is
parenthesised across several physical lines. Each physical line is
stripped and the parts joined with one space, so the result is a single
valid one-line statement (which is what exec inside the snippet needs)."""
src = ('from System.Windows import (Window, Thickness, FontWeights,\n'
' TextWrapping, TextDecorations,\n'
' SizeToContent)\n'
'from System.Windows.Controls import StackPanel\n')
got = wpf_smoke.collect_system_imports(src)
assert got == [
'from System.Windows import (Window, Thickness, FontWeights, '
'TextWrapping, TextDecorations, SizeToContent)',
'from System.Windows.Controls import StackPanel',
]
def test_import_inside_a_function_is_still_collected():
"""A `from System...` on an indented line (a helper that only reaches for
WPF when it runs) is still a System import that has to load inside Revit."""
src = ('def run():\n'
' from System.Diagnostics import Process, ProcessStartInfo\n'
' return Process\n')
assert wpf_smoke.collect_system_imports(src) == \
['from System.Diagnostics import Process, ProcessStartInfo']
def test_non_system_imports_are_ignored():
src = ('import os\n'
'from Autodesk.Revit.DB import FilteredElementCollector\n'
'import re\n'
'from System import Guid\n')
assert wpf_smoke.collect_system_imports(src) == ['from System import Guid']
def test_system_import_after_code_on_the_same_line_is_ignored():
"""The regex is anchored to the START of a line, so an import that appears
mid-line (in a comment, a string, or as a continuation of an expression)
is not a top-level import statement."""
src = ('x = 1 # from System.Windows import Window\n'
"s = 'import System.IO'\n")
assert wpf_smoke.collect_system_imports(src) == []
def test_an_unbalanced_parenthesised_import_stops_cleanly():
"""A source file with a broken (never-closed) parenthesised import is a
syntax error in Revit too; the collector must terminate at the end of the
file instead of hanging, and the trailing import must not be swallowed."""
src = ('from System.Windows import (\n'
' Window, Thickness\n'
'from System.Windows.Controls import StackPanel\n')
assert wpf_smoke.collect_system_imports(src) == [
'from System.Windows import (Window, Thickness',
'from System.Windows.Controls import StackPanel',
]
def test_comments_and_blank_lines_between_imports_are_stepped_over():
src = ('import os\n'
'\n'
'# a comment\n'
'from System import Guid\n')
assert wpf_smoke.collect_system_imports(src) == ['from System import Guid']
# ------------------------------------------------------------ the builder
def test_snippet_has_one_checked_exec_pair_per_statement():
stmts = ['from System.Windows import Window',
'from System.Windows.Controls import StackPanel']
snippet = wpf_smoke.build_snippet(stmts)
assert snippet.count('_n += 1') == 2
assert snippet.count('try:\n exec(') == 2
assert snippet.count('except Exception as _e:') == 2
def test_snippet_execs_the_statements_verbatim():
stmt = 'from System.Windows.Media import Stretch'
snippet = wpf_smoke.build_snippet([stmt])
assert "exec('from System.Windows.Media import Stretch')" in snippet
def test_snippet_closes_with_the_result_contract():
"""startup.py runs the snippet then reads `result` out of its namespace;
the errors list and checked count are what wpf_smoke judges on."""
snippet = wpf_smoke.build_snippet(['from System import Guid'])
assert snippet.rstrip().endswith(
"result = {'import_statements_checked': _n, 'errors': _errors}")
assert snippet.count("result = {'import_statements_checked': _n, "
"'errors': _errors}") == 1
def test_snippet_adds_the_references_first():
snippet = wpf_smoke.build_snippet(['from System import Guid'])
assert snippet.startswith('import clr\n')
assert 'clr.AddReference' in snippet
assert 'PresentationFramework' in snippet
def test_empty_statement_list_builds_a_clean_no_op_snippet():
"""Zero collected imports still produce a snippet whose `result` line is
the /exec contract -- the count and errors ride the runtime variables,
and with no statements `_n` stays 0 and `_errors` stays []."""
snippet = wpf_smoke.build_snippet([])
assert '_n += 1' not in snippet
assert 'exec(' not in snippet
assert snippet.rstrip().endswith(
"result = {'import_statements_checked': _n, 'errors': _errors}")
def test_error_message_carries_a_statement_prefix_and_the_exception():
"""The line a failing namespace produces names the statement it came from
-- that is the whole point of the prefix: the owner sees which import is
wrong, not just that one of them raised."""
snippet = wpf_smoke.build_snippet(['from System.Windows.Media import X'])
assert snippet.count("'from System.Windows.Media import X' + ' -> ' + "
"str(_e)") == 1
# every statement is wrapped in its own guard, whatever its length
assert snippet.count('_errors.append(') == 1
def test_snippet_is_ironpython_27_syntax_only():
"""The snippet runs inside Revit on IronPython 2.7: no f-strings, no
pathlib, no walrus. A non-ASCII byte with no coding cookie is the other
engine-only failure (see test_coding_cookie.py)."""
import ast
tree = ast.parse(wpf_smoke.build_snippet(
['from System.Windows import Window']))
assert tree is not None
assert isinstance(tree, ast.Module)
# ------------------------------------------------------------ the verdict
class _FakeBridge(object):
"""Records the request it was handed and answers with a canned body."""
def __init__(self, body):
self.body = body
self.seen = []
def __call__(self, code, port=None):
self.seen.append((code, port))
return self.body
def test_fail_verdict_lists_every_import_error(monkeypatch, tmp_path, capsys):
script = tmp_path / 'btn.py'
script.write_text('from System.Windows.Media import StretchDirection\n',
encoding='utf-8')
fake = _FakeBridge({
'ok': True,
'result': {'import_statements_checked': 1,
'errors': ['bad stmt -> System.MissingMemberException: '
'StretchDirection']},
})
monkeypatch.setattr(wpf_smoke, '_bridge_call', fake)
code = wpf_smoke.main([str(script)])
out = capsys.readouterr().out
assert code == 1
assert out.splitlines()[0].startswith('FAIL {0} -- 1 import '
'error(s):'.format(script))
assert 'StretchDirection' in out
def test_ok_verdict_counts_the_checked_statements(monkeypatch, tmp_path,
capsys):
script = tmp_path / 'btn.py'
script.write_text('from System.Windows import Window\n', encoding='utf-8')
fake = _FakeBridge({'ok': True,
'result': {'import_statements_checked': 1,
'errors': []}})
monkeypatch.setattr(wpf_smoke, '_bridge_call', fake)
code = wpf_smoke.main([str(script)])
out = capsys.readouterr().out
assert code == 0
assert out == 'OK {0} -- 1 WPF import statements load inside Revit\n' \
.format(script)
def test_ok_verdict_when_the_script_has_no_system_imports(monkeypatch,
tmp_path, capsys):
script = tmp_path / 'btn.py'
script.write_text('print("no wpf here")\n', encoding='utf-8')
fake = _FakeBridge({'ok': True,
'result': {'import_statements_checked': 0,
'errors': []}})
monkeypatch.setattr(wpf_smoke, '_bridge_call', fake)
code = wpf_smoke.main([str(script)])
assert code == 0
assert '0 WPF import statements' in capsys.readouterr().out
def test_a_refused_bridge_is_could_not_judge_never_ok(monkeypatch, tmp_path,
capsys):
script = tmp_path / 'btn.py'
script.write_text('from System.Windows import Window\n', encoding='utf-8')
fake = _FakeBridge({'ok': False, 'refused': True,
'doc_title': 'sample_job',
'error': 'REFUSED: the live model is read-only'})
monkeypatch.setattr(wpf_smoke, '_bridge_call', fake)
code = wpf_smoke.main([str(script)])
out = capsys.readouterr().out
assert code == 2
assert out.startswith('COULD NOT JUDGE --')
def test_an_exec_that_did_not_run_is_could_not_judge(monkeypatch, tmp_path,
capsys):
script = tmp_path / 'btn.py'
script.write_text('from System.Windows import Window\n', encoding='utf-8')
fake = _FakeBridge({'ok': False, 'error': 'missing "code" in body'})
monkeypatch.setattr(wpf_smoke, '_bridge_call', fake)
assert wpf_smoke.main([str(script)]) == 2
assert capsys.readouterr().out.startswith('COULD NOT JUDGE --')
def test_a_bridge_that_raises_systemexit_is_could_not_judge(monkeypatch,
tmp_path, capsys):
"""rvt.call refuses (or cannot reach the bridge) by raising SystemExit
after printing why on stderr. The verdict must say it was not judged."""
script = tmp_path / 'btn.py'
script.write_text('from System.Windows import Window\n', encoding='utf-8')
def _raise(code, port=None):
raise SystemExit(1)
monkeypatch.setattr(wpf_smoke, '_bridge_call', _raise)
code = wpf_smoke.main([str(script)])
out = capsys.readouterr().out
assert code == 2
assert out.startswith('COULD NOT JUDGE --')
def test_a_missing_script_is_could_not_judge(tmp_path, capsys):
code = wpf_smoke.main([str(tmp_path / 'no_such_button.py')])
assert code == 2
assert capsys.readouterr().out.startswith('COULD NOT JUDGE --')
def test_the_collected_statements_are_what_reaches_the_bridge(monkeypatch,
tmp_path):
"""One integration-shaped test without a socket: the file on disk is
collected and shipped through the (faked) bridge call, so the collector
and the wire-up agree with each other."""
script = tmp_path / 'btn.py'
script.write_text(
'from System.Windows import (\n'
' Window, Thickness)\n'
'from System.Windows.Controls import StackPanel\n',
encoding='utf-8')
fake = _FakeBridge({'ok': True,
'result': {'import_statements_checked': 2,
'errors': []}})
monkeypatch.setattr(wpf_smoke, '_bridge_call', fake)
assert wpf_smoke.main([str(script)]) == 0
assert len(fake.seen) == 1
sent, port = fake.seen[0]
assert port is None
# the real collector output for that source: the first line's `(` is
# glued to the first name, its `)` to the last, so no padding spaces.
assert "exec('from System.Windows import (Window, Thickness)')" in sent