# -*- 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