25592fa6c2
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.
97 lines
4.0 KiB
Python
97 lines
4.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Every extension .py with a non-ASCII byte must declare its encoding.
|
|
|
|
This is a test because it already cost a live failure. On 2026-09-03, after a
|
|
Revit restart, pressing Panel Analysis threw
|
|
|
|
Revit encountered a non-ASCII character '\\xe2' in file ...
|
|
PanelAnalysis.pushbutton/script.py
|
|
|
|
-- an IronPython 2.7 SyntaxError, raised at compile time, before a line of the
|
|
button ran. The file carried deliberate typography in `u'...'` labels and
|
|
comments (the prime in f'c, subscripts, the sqrt sign, an em-dash, a Chinese
|
|
note) but no `# -*- coding: utf-8 -*-` cookie, and IronPython 2.7 refuses a
|
|
source file that mixes non-ASCII bytes with no declared encoding (PEP 263).
|
|
|
|
CPython 3 -- what this test runs on, and what `tools/rvt.py` runs on -- defaults
|
|
to UTF-8 and never raises this, so the trap is invisible on the host and only
|
|
appears inside Revit. That asymmetry is exactly why it needs a guard: DSV4F, a
|
|
branch merge, or anyone pasting a nice-looking dash into a comment reintroduces
|
|
it silently, and the next person to feel it is the owner pressing a button.
|
|
|
|
The engine that matters here is IronPython 2.7, so the scan covers the files it
|
|
loads -- everything under the extension except the host-only `tests/` tree
|
|
(those run on CPython 3, where the cookie is not required).
|
|
"""
|
|
import os
|
|
import re
|
|
|
|
# tests/ -> WillTools.extension (mirrors test_button_tooltips._EXT).
|
|
_EXT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
_TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# PEP 263: the cookie is honoured only on the first OR second line, and must
|
|
# match `coding[:=]\s*([-\w.]+)`. We check the first two lines the same way
|
|
# IronPython does, so a cookie on line 3 (too late) still counts as missing.
|
|
_COOKIE = re.compile(rb'coding[:=]')
|
|
|
|
|
|
def _extension_py_files():
|
|
"""Every .py IronPython 2.7 loads from the extension -- tests excluded."""
|
|
for root, dirs, files in os.walk(_EXT):
|
|
# Do not descend into the host-only tests tree.
|
|
if os.path.abspath(root) == _TESTS_DIR:
|
|
dirs[:] = []
|
|
continue
|
|
for name in files:
|
|
if name.endswith('.py'):
|
|
yield os.path.join(root, name)
|
|
|
|
|
|
def _has_non_ascii(data):
|
|
return any(byte >= 0x80 for byte in bytearray(data))
|
|
|
|
|
|
def _declares_encoding(data):
|
|
first_two = b'\n'.join(data.split(b'\n')[:2])
|
|
return bool(_COOKIE.search(first_two))
|
|
|
|
|
|
def _first_non_ascii_line(data):
|
|
for lineno, line in enumerate(data.split(b'\n'), 1):
|
|
if _has_non_ascii(line):
|
|
return lineno
|
|
return None
|
|
|
|
|
|
def test_the_walk_found_scripts():
|
|
"""A scan that silently matched nothing is not a passing guard.
|
|
|
|
If the walk stops finding the button scripts -- a renamed container, a
|
|
walk that no longer descends -- this test must fail rather than pass by
|
|
vacuously scanning an empty set.
|
|
"""
|
|
found = list(_extension_py_files())
|
|
assert found, 'the extension .py walk found nothing -- the scan is broken'
|
|
assert any(p.endswith('script.py') for p in found), (
|
|
'the walk found no button script.py -- it stopped descending; fix the '
|
|
'walk before trusting a green run here')
|
|
|
|
|
|
def test_non_ascii_files_declare_an_encoding():
|
|
"""No IronPython-loaded .py may carry a non-ASCII byte without a cookie."""
|
|
offenders = []
|
|
for path in _extension_py_files():
|
|
with open(path, 'rb') as handle:
|
|
data = handle.read()
|
|
if _has_non_ascii(data) and not _declares_encoding(data):
|
|
offenders.append((os.path.relpath(path, _EXT),
|
|
_first_non_ascii_line(data)))
|
|
assert not offenders, (
|
|
'these files carry a non-ASCII byte but declare no encoding, so '
|
|
"IronPython 2.7 throws a SyntaxError at compile time (\"non-ASCII "
|
|
'character ... but no encoding declared\") the moment Revit loads '
|
|
'them. Add `# -*- coding: utf-8 -*-` as the first line:\n' +
|
|
'\n'.join(' {0} (first non-ASCII on line {1})'.format(p, ln)
|
|
for p, ln in offenders))
|