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.
316 lines
13 KiB
Python
316 lines
13 KiB
Python
# -*- 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
|