WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user