Public demo snapshot of an in-house pyRevit extension for precast concrete shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel handling solver (PCI Chapter 8). - No client, job, plant or product data: knowledge layers ship as structural templates only. - Ticket QC's model-read escalation targets a LAN model server endpoint (configurable), not a cloud provider. - Built from the private development repo via tools/make_dist.py.
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Reports that fold: what opens, what stays shut, and what is never lost.
|
||||
|
||||
The rule under test is the owner's, 2026-08-30: everything below the answer
|
||||
starts folded, and a section opens itself only when it has a problem in it.
|
||||
The value of that is a contract -- "if nothing below the headline is open,
|
||||
nothing needs me" -- and the contract is worth more than any single section
|
||||
being visible. So the tests are mostly about the two ways it could be broken:
|
||||
a section that has a problem and does not open, and a section that folds a
|
||||
sentence out of existence rather than folding it away.
|
||||
|
||||
The HTML half is written for pyRevit's output panel, which is an IE-engine
|
||||
`System.Windows.Forms.WebBrowser`. `<details>` does not exist there and fails
|
||||
SILENTLY -- the "collapsed" content simply renders. That is the trap this
|
||||
module exists to avoid, so it is asserted directly.
|
||||
"""
|
||||
# pylint: disable=import-error,invalid-name
|
||||
import re
|
||||
|
||||
from vibe import foldreport as fold
|
||||
|
||||
|
||||
def _section(**kw):
|
||||
kw.setdefault('key', 's1')
|
||||
kw.setdefault('title', 'Some detail')
|
||||
return fold.Section(**kw)
|
||||
|
||||
|
||||
# --------------------------------------------------------- what opens itself
|
||||
|
||||
def test_a_routine_section_is_folded():
|
||||
assert _section(summary='3 things').is_open is False
|
||||
|
||||
|
||||
def test_a_section_with_a_problem_opens_itself():
|
||||
assert _section(problem='2 checks did not run').is_open is True
|
||||
|
||||
|
||||
def test_the_answer_is_never_folded():
|
||||
assert _section(headline=True).is_open is True
|
||||
|
||||
|
||||
def test_a_problem_is_a_sentence_not_a_flag():
|
||||
"""A section that opens has to say why it opened, or the reader has to
|
||||
read the whole of it to find out."""
|
||||
section = _section(problem='the index is 4 day(s) old')
|
||||
assert 'day(s) old' in fold.blocks([section])[0][1]
|
||||
|
||||
|
||||
def test_the_contract_is_readable_off_the_section_list():
|
||||
sections = [_section(key='a'), _section(key='b', problem='something')]
|
||||
assert [s.key for s in fold.sections_with_problems(sections)] == ['b']
|
||||
assert fold.problem_count(sections) == 1
|
||||
|
||||
|
||||
# ------------------------------------------------------- folded is not gone
|
||||
|
||||
def test_a_folded_section_still_names_itself_and_what_it_holds():
|
||||
section = _section(title='The donor pool', summary='690 looked at, 204 kept',
|
||||
blocks=[('md', 'a long table nobody reads')])
|
||||
line = fold.blocks([section])[0][1]
|
||||
assert 'The donor pool' in line
|
||||
assert '690 looked at, 204 kept' in line
|
||||
assert 'folded' in line
|
||||
assert 'nobody reads' not in line # folded away, not printed
|
||||
|
||||
|
||||
def test_show_all_prints_every_section_in_full():
|
||||
section = _section(blocks=[('md', 'the detail')])
|
||||
text = fold.text([section], show_all=True)
|
||||
assert 'the detail' in text
|
||||
|
||||
|
||||
def test_the_headline_is_printed_without_a_fold_header():
|
||||
head = _section(key='headline', title='Answer', headline=True,
|
||||
blocks=[('md', '# The answer')])
|
||||
got = fold.blocks([head])
|
||||
assert got == [('md', '# The answer')]
|
||||
|
||||
|
||||
# ------------------------------------------------------------- as plain text
|
||||
|
||||
def test_entities_become_ascii_and_the_words_are_left_alone():
|
||||
got = fold.md_text('This is **bold** — and `code`.')
|
||||
assert 'This is bold -- and code.' in got
|
||||
assert '—' not in got
|
||||
|
||||
|
||||
def test_a_quote_block_is_marked_so_a_caveat_cannot_look_like_prose():
|
||||
assert fold.md_text('> a warning').lstrip().startswith('!')
|
||||
|
||||
|
||||
def test_a_path_is_never_broken_across_two_lines():
|
||||
"""A wrapped path is not a wrapped sentence, it is an unusable value --
|
||||
the reader copies these into a shell or into Select by ID."""
|
||||
path = 'C:\\a\\very\\long\\folder\\name\\that\\exceeds\\the\\margin\\x.txt'
|
||||
got = fold.md_text('written to `' + path + '`')
|
||||
assert path in got
|
||||
|
||||
|
||||
def test_a_table_never_truncates_a_mark_however_wide_the_row_gets():
|
||||
"""A silently cut mark is a wrong answer that looks like a right one."""
|
||||
got = fold.table_text(['donor', 'what'],
|
||||
[['PNL803', 'x' * 120], ['PNL804', 'y']])
|
||||
assert 'PNL803' in got and 'PNL804' in got
|
||||
assert 'x' * 120 in got
|
||||
|
||||
|
||||
def test_an_empty_table_says_nothing_rather_than_printing_a_header():
|
||||
assert 'nothing' in fold.table_text(['a'], [])
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- as HTML
|
||||
|
||||
def test_the_html_never_uses_details_which_this_panel_cannot_render():
|
||||
"""pyRevit's output is an IE-engine WebBrowser. `<details>` renders as an
|
||||
inline span there, so a 'collapsed' section would simply be visible --
|
||||
the failure is silent, which is why it is asserted rather than trusted."""
|
||||
got = fold.html([_section(blocks=[('md', 'x')])])
|
||||
assert '<details' not in got and '<summary' not in got
|
||||
assert 'addEventListener' not in got and 'querySelector' not in got
|
||||
|
||||
|
||||
def test_a_folded_section_is_hidden_and_a_problem_section_is_not():
|
||||
folded = fold.html([_section(key='a', blocks=[('md', 'x')])])
|
||||
opened = fold.html([_section(key='b', problem='wrong',
|
||||
blocks=[('md', 'x')])])
|
||||
assert 'display:none' in folded
|
||||
assert 'display:block' in opened
|
||||
|
||||
|
||||
def test_a_problem_section_needs_no_javascript_to_be_read():
|
||||
"""If the toggle is dead in the panel, everything that matters is still
|
||||
on the page. That is the whole fallback."""
|
||||
got = fold.html([_section(problem='wrong', blocks=[('md', 'the detail')])])
|
||||
assert 'the detail' in got
|
||||
assert 'display:none' not in got
|
||||
|
||||
|
||||
def test_the_toggle_only_uses_dom_that_exists_in_ie7():
|
||||
assert 'getElementById' in fold.SCRIPT
|
||||
assert 'style.display' in fold.SCRIPT
|
||||
assert 'classList' not in fold.SCRIPT
|
||||
|
||||
|
||||
def test_the_header_is_always_in_the_html_even_when_folded():
|
||||
got = fold.html([_section(title='The donor pool', summary='690 looked at',
|
||||
blocks=[('md', 'x')])])
|
||||
assert 'The donor pool' in got
|
||||
assert '690 looked at' in got
|
||||
|
||||
|
||||
def test_markup_in_a_cell_cannot_break_the_row():
|
||||
got = fold.table_html(['a'], [['<script>bad()</script>']])
|
||||
assert '<script>' not in got
|
||||
assert '<script>' in got
|
||||
|
||||
|
||||
def test_the_entities_the_reports_write_on_purpose_survive_escaping():
|
||||
"""The reports write `—` and `†` as ASCII source, because
|
||||
IronPython 2.7 makes a non-ASCII literal a byte string. Escaping would
|
||||
turn those into visible text, so they are restored by name."""
|
||||
assert '—' in fold.md_html('a **b** — c')
|
||||
assert '†' in fold.table_html(['a'], [['PNL1†']])
|
||||
|
||||
|
||||
def test_an_ampersand_in_the_text_is_still_escaped():
|
||||
assert '&' in fold.md_html('Smith & Sons')
|
||||
|
||||
|
||||
def test_a_header_carries_its_entities_too():
|
||||
"""Title, summary and problem go through the same escape-then-restore as
|
||||
md blocks and table cells. They did not until 2026-08-30, and
|
||||
`takeoffreport` wrote `—` into a dozen headers on that assumption --
|
||||
every one of which reached the panel as literal source text."""
|
||||
got = fold.html([_section(key='a', title='Other quantities',
|
||||
summary='4 kinds — none checked'),
|
||||
_section(key='b', title='Bars — unread',
|
||||
problem='2 marks — no length')])
|
||||
assert '&mdash;' not in got
|
||||
assert 'Bars — unread' in got
|
||||
assert '4 kinds — none checked' in got
|
||||
assert '2 marks — no length' in got
|
||||
|
||||
|
||||
def test_an_ampersand_in_a_header_is_still_escaped():
|
||||
"""The restore is by name, so a bare ampersand in a title stays escaped
|
||||
and cannot break the header markup."""
|
||||
got = fold.html([_section(key='a', title='Smith & Sons',
|
||||
summary='Jones & Co')])
|
||||
assert '&' in got
|
||||
assert 'Smith & Sons' not in got
|
||||
|
||||
|
||||
def test_the_panel_html_carries_no_newline_at_all():
|
||||
"""pyRevit's `print_html` is a `print(..., end='')` into the panel's
|
||||
stream, and a newline in that stream starts a NEW output entry -- which
|
||||
would cut a <div> in half and leave a fold's body outside its container."""
|
||||
got = fold.html([_section(key='a', blocks=[('md', 'x'), ('md', 'y'),
|
||||
('table', ['c'], [['1']])]),
|
||||
_section(key='b', problem='wrong', blocks=[('md', 'z')])])
|
||||
assert chr(10) not in got
|
||||
|
||||
|
||||
# ------------- the text report may not drop a character of what it prints
|
||||
#
|
||||
# `last-ticketqc.txt` and every other `text(sections, show_all=True)` file is
|
||||
# documented as the complete record -- "folded is not gone". A live run on
|
||||
# 2026-08-31 wrote the ticket one with every CJK character of the job file's
|
||||
# mute sentences REMOVED, ASCII fragments and the gaps between them
|
||||
# surviving, while the same sentences rendered correctly in the panel and in
|
||||
# `qc-run.json`.
|
||||
#
|
||||
# The mechanism is engine-specific and CANNOT be reproduced on the host
|
||||
# interpreter this suite runs on: `textwrap` munges whitespace with
|
||||
# `text.translate(<256-entry table>)` for anything it takes for a `str`, and
|
||||
# IronPython 2.7's `str` holds .NET UTF-16 text, so a character above U+00FF
|
||||
# has no entry in that table. `ticketpage` was cured first; this renderer had
|
||||
# the identical call, plus a `str()` in `_cell` that `md_text` does not have.
|
||||
#
|
||||
# So two kinds of test: that non-ASCII survives (true on any engine, and the
|
||||
# promise itself) and a tombstone pinning the SHAPE that keeps it true on the
|
||||
# engine that ships.
|
||||
|
||||
_CJK = u'\u5f00\u53e3\u672a\u5efa\u6a21' # 开口未建模
|
||||
|
||||
|
||||
def test_a_non_ascii_line_survives_md_text():
|
||||
assert _CJK in fold.md_text(_CJK + ' -- muted by the owner')
|
||||
|
||||
|
||||
def test_a_non_ascii_line_survives_being_wrapped_onto_several_lines():
|
||||
"""The wrap is where the characters were lost, so the long case is the
|
||||
one that matters: a short line might never reach `textwrap` at all."""
|
||||
got = fold.md_text((_CJK + ' muted ') * 12, width=40)
|
||||
assert got.count(_CJK) == 12
|
||||
assert len(got.split(chr(10))) > 3 # it really did wrap
|
||||
|
||||
|
||||
def test_a_non_ascii_heading_and_folded_header_survive_too():
|
||||
assert _CJK in fold.md_text('## ' + _CJK)
|
||||
assert _CJK in fold.md_text('- **' + _CJK + '** _(folded)_')
|
||||
|
||||
|
||||
def test_a_non_ascii_table_cell_survives():
|
||||
"""`_cell` is the text path's other coercion boundary -- `md_text` has
|
||||
none of its own."""
|
||||
got = fold.table_text(['mark', 'why'], [['PNL101', _CJK]])
|
||||
assert _CJK in got
|
||||
|
||||
|
||||
def test_a_non_ascii_section_survives_the_whole_text_report():
|
||||
"""End to end, the way the buttons write the file: every block, folded
|
||||
and open, through one join."""
|
||||
got = fold.text([_section(key='a', title=_CJK + ' section',
|
||||
problem=_CJK + ' problem',
|
||||
blocks=[('md', _CJK + ' body'),
|
||||
('table', ['c'], [[_CJK]])]),
|
||||
_section(key='b', title='folded ' + _CJK,
|
||||
summary=_CJK + ' summary')],
|
||||
show_all=False)
|
||||
assert got.count(_CJK) >= 2
|
||||
# and it is TEXT, so the caller's one encode('utf-8') is all it takes
|
||||
assert got.encode('utf-8').decode('utf-8') == got
|
||||
|
||||
|
||||
def test_the_text_path_never_hands_a_line_to_textwraps_translate():
|
||||
"""A TOMBSTONE, and the only kind of test that can hold here: the defect
|
||||
lives on an engine this suite does not run on, so what is pinned is the
|
||||
shape that avoids it -- whitespace flattened before the wrap, `textwrap`
|
||||
told not to do it again, and no bare `str()` on anything printed."""
|
||||
import inspect
|
||||
|
||||
def code_of(fn):
|
||||
"""The function without its docstring: these docstrings TALK about
|
||||
`str()`, and a prose mention is not a call."""
|
||||
return inspect.getsource(fn).replace(fn.__doc__ or chr(10) * 2, '')
|
||||
|
||||
src = code_of(fold._fill)
|
||||
assert 'replace_whitespace=False' in src
|
||||
assert 'expand_tabs=False' in src
|
||||
assert '_text(body)' in src
|
||||
assert 'textwrap' not in code_of(fold.md_text) # it goes through _fill
|
||||
for name in ('_cell', '_flat', 'md_text', 'text', '_open_header',
|
||||
'_folded_header'):
|
||||
assert 'str(' not in code_of(getattr(fold, name)), name
|
||||
|
||||
|
||||
# ------------------------------------------------------ demo-safe: no CJK
|
||||
#
|
||||
# Owner, 2026-09-01: no Chinese anywhere in a report or a UI surface, even
|
||||
# when quoting him -- he is demoing this to colleagues before the weekend.
|
||||
# This module is content-agnostic -- the CJK survival tests above are about
|
||||
# a JOB'S OWN notes surviving intact, which is a different, still-valid
|
||||
# guarantee -- but a WillTools-authored report bundle, the kind this module
|
||||
# actually renders for the ticket-QC and reinforcing buttons, must render
|
||||
# with none. This fixture is the shape those callers build: an answer
|
||||
# headline plus foldable detail sections with tables.
|
||||
_ENGLISH_SECTIONS = [
|
||||
_section(key='answer', title='Result', headline=True,
|
||||
blocks=[('md', 'OK -- every check ran.')]),
|
||||
_section(key='findings', title='Findings', summary='2 items',
|
||||
problem='2 checks need a look',
|
||||
blocks=[('md', 'Everything of this kind goes out as WARN.'),
|
||||
('table', ['check', 'status'],
|
||||
[['4.5', 'WARN'], ['6.4', 'OK']])]),
|
||||
]
|
||||
|
||||
|
||||
def test_a_willtools_report_bundle_renders_with_no_chinese_text_or_html():
|
||||
text_out = fold.text(_ENGLISH_SECTIONS, show_all=True)
|
||||
html_out = fold.html(_ENGLISH_SECTIONS)
|
||||
cjk = re.compile(u'[一-鿿]')
|
||||
assert not cjk.search(text_out), text_out
|
||||
assert not cjk.search(html_out), html_out
|
||||
Reference in New Issue
Block a user