WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,709 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for the job-level reconciliation core.
|
||||
|
||||
NO REAL MARK, CONTROL NUMBER, DESIGN NUMBER OR JOB DETAIL APPEARS HERE.
|
||||
Everything is invented: marks PNL1xx / BM1xx, controls in a 9xxxx band that
|
||||
no real job uses, designs PNL1.0 / RB1. The reconciliation is arithmetic on
|
||||
strings; it does not need a real job to be exercised, and the repo must never
|
||||
carry one.
|
||||
|
||||
The shapes under test are the ones that have actually bitten, or that would
|
||||
bite silently if they broke:
|
||||
|
||||
* a printed RANGE is several pieces, not one token
|
||||
* a stripped page 2 is still a ticket page
|
||||
* 'could not judge' never renders as 'judged and clean'
|
||||
* nothing is filtered for looking out of scope
|
||||
"""
|
||||
from vibe import jobrecon
|
||||
|
||||
|
||||
def piece(control, mark, design='PNL1.0', **kw):
|
||||
row = {'id': kw.pop('id', hash(control) % 100000), 'category': 'X',
|
||||
'control': control, 'mark': mark, 'design': design,
|
||||
'family': kw.pop('family', 'PANEL'), 'in_assembly': None,
|
||||
'ticket_created': None, 'ticket_detailed': None}
|
||||
row.update(kw)
|
||||
return row
|
||||
|
||||
|
||||
def sheet(name, number, controls, design='PNL1.0', **kw):
|
||||
row = {'id': kw.pop('id', 1), 'number': number, 'name': name,
|
||||
'design': design, 'controls': list(controls),
|
||||
'total_required': None, 'sheet_count': None}
|
||||
row.update(kw)
|
||||
return row
|
||||
|
||||
|
||||
# ------------------------------------------------- expand_control_field
|
||||
|
||||
def test_plain_control_number():
|
||||
assert jobrecon.expand_control_field('90001') == (['90001'], [])
|
||||
|
||||
|
||||
def test_comma_list_is_several_pieces():
|
||||
got, bad = jobrecon.expand_control_field('90001, 90003 ,90007')
|
||||
assert got == ['90001', '90003', '90007']
|
||||
assert bad == []
|
||||
|
||||
|
||||
def test_range_expands_to_every_piece_in_it():
|
||||
"""A slot holding a range is four pieces. Reading it as one token
|
||||
under-counts the ticket by three and invents three orphans."""
|
||||
got, bad = jobrecon.expand_control_field('90120-90123')
|
||||
assert got == ['90120', '90121', '90122', '90123']
|
||||
assert bad == []
|
||||
|
||||
|
||||
def test_range_keeps_leading_zero_width():
|
||||
got, _bad = jobrecon.expand_control_field('008-011')
|
||||
assert got == ['008', '009', '010', '011']
|
||||
|
||||
|
||||
def test_en_dash_range_still_expands():
|
||||
got, bad = jobrecon.expand_control_field(u'90120–90122')
|
||||
assert got == ['90120', '90121', '90122']
|
||||
assert bad == []
|
||||
|
||||
|
||||
def test_absurd_range_is_reported_not_materialised():
|
||||
got, bad = jobrecon.expand_control_field('1-99999')
|
||||
assert got == []
|
||||
assert bad == ['1-99999']
|
||||
|
||||
|
||||
def test_backwards_range_is_unreadable_not_empty():
|
||||
got, bad = jobrecon.expand_control_field('90123-90120')
|
||||
assert got == []
|
||||
assert bad == ['90123-90120']
|
||||
|
||||
|
||||
def test_material_text_is_returned_as_unreadable_never_dropped():
|
||||
"""The same title-block slots carry a material list on non-ticket
|
||||
sheets. A slot the tool cannot read is a slot whose pieces it cannot
|
||||
vouch for, so it comes back rather than vanishing."""
|
||||
got, bad = jobrecon.expand_control_field('PL 1/4" X 28" X 4\'-0"')
|
||||
assert got == []
|
||||
assert bad
|
||||
|
||||
|
||||
def test_empty_slot_is_neither_control_nor_complaint():
|
||||
assert jobrecon.expand_control_field(None) == ([], [])
|
||||
assert jobrecon.expand_control_field(' ') == ([], [])
|
||||
|
||||
|
||||
# ------------------------------------------------------- classify_sheet
|
||||
|
||||
def test_sheet_named_after_a_mark_is_a_ticket():
|
||||
kind, _why = jobrecon.classify_sheet(
|
||||
sheet('PNL101', '1 OF 2', ['90001']), {'PNL101'})
|
||||
assert kind == 'ticket'
|
||||
|
||||
|
||||
def test_stripped_page_two_is_still_a_ticket_page():
|
||||
"""A multi-page ticket's later sheets carry no title block: no design
|
||||
number, no length, no weight. An and-gate on both signals drops them
|
||||
and every finding on those pages disappears."""
|
||||
kind, why = jobrecon.classify_sheet(
|
||||
sheet('PNL101', '2 OF 2', [], design=None), {'PNL101'})
|
||||
assert kind == 'ticket'
|
||||
assert 'stripped' in why
|
||||
|
||||
|
||||
def test_page_numbered_title_block_is_a_ticket_even_when_unnamed():
|
||||
kind, _why = jobrecon.classify_sheet(
|
||||
sheet('SOMETHING ELSE', '1 OF 2', ['90001']), {'PNL101'})
|
||||
assert kind == 'ticket'
|
||||
|
||||
|
||||
def test_ordinary_drawing_is_not_a_ticket():
|
||||
kind, _why = jobrecon.classify_sheet(
|
||||
sheet('FOUNDATION PLAN', 'F1.0', [], design=None), {'PNL101'})
|
||||
assert kind == 'other'
|
||||
|
||||
|
||||
def test_missing_space_in_page_number_still_reads():
|
||||
assert jobrecon.page_of(sheet('PNL101', '1OF 1', [])) == (1, 1)
|
||||
|
||||
|
||||
# ------------------------------------------------------ page_set_status
|
||||
|
||||
def test_complete_page_set_is_ok():
|
||||
status, _msg = jobrecon.page_set_status([(1, 2), (2, 2)])
|
||||
assert status == 'ok'
|
||||
|
||||
|
||||
def test_missing_page_is_broken_and_names_the_page():
|
||||
status, msg = jobrecon.page_set_status([(1, 3), (3, 3)])
|
||||
assert status == 'broken'
|
||||
assert '2' in msg
|
||||
|
||||
|
||||
def test_no_page_markers_is_unjudged_not_ok():
|
||||
"""The criterion source is absent, so nothing was judged. 'ok' here is
|
||||
the exact 'ran and said fine while seeing nothing' failure."""
|
||||
status, msg = jobrecon.page_set_status([(None, None), (None, None)])
|
||||
assert status == 'unjudged'
|
||||
assert 'not be judged' in msg
|
||||
|
||||
|
||||
def test_pages_disagreeing_on_the_total_is_broken():
|
||||
status, _msg = jobrecon.page_set_status([(1, 6), (2, 2)])
|
||||
assert status == 'broken'
|
||||
|
||||
|
||||
# ------------------------------------------------------------ reconcile
|
||||
|
||||
def test_a_piece_with_no_ticket_is_named_individually():
|
||||
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL102')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['totals']['pieces_uncovered'] == 1
|
||||
assert [x['control'] for x in r['pieces_without_ticket']] == ['90002']
|
||||
assert r['pieces_without_ticket'][0]['marks'] == ['PNL102']
|
||||
|
||||
|
||||
def test_a_ticket_for_a_piece_that_does_not_exist_is_named():
|
||||
pieces = [piece('90001', 'PNL101')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001, 90099'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert [x['control'] for x in r['controls_without_piece']] == ['90099']
|
||||
assert r['controls_without_piece'][0]['sheets'] == ['PNL101']
|
||||
|
||||
|
||||
def test_range_on_a_ticket_covers_every_piece_in_it():
|
||||
pieces = [piece('90120', 'BM100', design='RB1'),
|
||||
piece('90121', 'BM100', design='RB1'),
|
||||
piece('90122', 'BM100', design='RB1')]
|
||||
sheets = [sheet('BM100', '1 OF 1', ['90120-90122'], design='RB1')]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['totals']['pieces_uncovered'] == 0
|
||||
assert r['mark_count_mismatch'] == []
|
||||
|
||||
|
||||
def test_count_mismatch_names_both_sides():
|
||||
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL101')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001', '90050'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
row = r['mark_count_mismatch'][0]
|
||||
assert row['mark'] == 'PNL101'
|
||||
assert row['issued'] == 2 and row['modelled'] == 2
|
||||
assert row['issued_only'] == ['90050']
|
||||
assert row['modelled_only'] == ['90002']
|
||||
|
||||
|
||||
def test_mark_with_no_ticket_at_all_is_its_own_finding():
|
||||
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL102')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert [x['mark'] for x in r['marks_without_ticket']] == ['PNL102']
|
||||
assert r['marks_without_ticket'][0]['pieces'] == 1
|
||||
|
||||
|
||||
def test_stripped_page_two_does_not_read_as_a_missing_ticket():
|
||||
"""Regression for the classifier trap: page 2 has no design number and
|
||||
no control numbers. If it is dropped the set looks incomplete; if it is
|
||||
counted as a silent page the piece is still covered by page 1."""
|
||||
pieces = [piece('90001', 'PNL101')]
|
||||
sheets = [sheet('PNL101', '1 OF 2', ['90001']),
|
||||
sheet('PNL101', '2 OF 2', [], design=None)]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['totals']['pieces_uncovered'] == 0
|
||||
assert r['page_set_problems'] == []
|
||||
assert len(r['silent_ticket_pages']) == 1
|
||||
|
||||
|
||||
def test_quantity_stated_but_not_printed_is_a_finding():
|
||||
"""A title block with a fixed number of control slots can state a
|
||||
quantity larger than it can print. The pieces past the last slot are
|
||||
unaccounted for on that ticket."""
|
||||
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL101')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001', '90002'],
|
||||
total_required='9')]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['quantity_disagreements'][0]['stated'] == 9
|
||||
assert r['quantity_disagreements'][0]['listed'] == 2
|
||||
|
||||
|
||||
def test_ticket_design_number_disagreeing_with_its_pieces():
|
||||
pieces = [piece('90001', 'PNL101', design='PNL4.0')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001'], design='PNL2.0')]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
row = r['design_disagreements'][0]
|
||||
assert row['printed'] == 'PNL2.0' and row['pieces_say'] == ['PNL4.0']
|
||||
|
||||
|
||||
def test_stated_sheet_count_disagreeing_with_sheets_found():
|
||||
pieces = [piece('90001', 'PNL101')]
|
||||
sheets = [sheet('PNL101', '1 OF 2', ['90001'], sheet_count='1'),
|
||||
sheet('PNL101', '2 OF 2', [], design=None)]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['stated_page_counts'][0]['stated'] == 1
|
||||
assert r['stated_page_counts'][0]['found'] == 2
|
||||
|
||||
|
||||
def test_model_claiming_a_ticket_that_does_not_exist():
|
||||
pieces = [piece('90002', 'PNL102', ticket_created='Yes')]
|
||||
r = jobrecon.reconcile(pieces, [])
|
||||
assert r['state_contradictions'][0]['control'] == '90002'
|
||||
assert 'was created' in r['state_contradictions'][0]['problem']
|
||||
|
||||
|
||||
def test_piece_missing_from_a_ticket_that_exists_is_separated_out():
|
||||
"""The worst shape here, and the one a per-ticket checker cannot see:
|
||||
the ticket for the mark exists and passes every check on it, and this
|
||||
piece's control number is not printed on it."""
|
||||
pieces = [piece('90001', 'PNL101'),
|
||||
piece('90002', 'PNL101', ticket_created='Yes')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
rows = {x['control']: x for x in r['pieces_without_ticket']}
|
||||
assert rows['90002']['mark_has_ticket'] is True
|
||||
assert 'does not print this control number' in \
|
||||
r['state_contradictions'][0]['problem']
|
||||
text = '\n'.join(jobrecon.format_report(r))
|
||||
assert 'PIECES MISSING FROM A TICKET THAT EXISTS -- 1 piece(s)' in text
|
||||
# The other bucket found nothing, and 'found nothing' is still printed --
|
||||
# by name, in the folded section, never as a heading over the word 'none'.
|
||||
assert 'PIECES WITH NO TICKET FOR THEIR MARK AT ALL -- ran, found none' \
|
||||
in text
|
||||
|
||||
|
||||
def test_piece_whose_mark_was_never_drawn_is_the_other_bucket():
|
||||
pieces = [piece('90002', 'PNL102', ticket_created='Yes')]
|
||||
r = jobrecon.reconcile(pieces, [])
|
||||
assert r['pieces_without_ticket'][0]['mark_has_ticket'] is False
|
||||
assert 'there is no ticket' in r['state_contradictions'][0]['problem']
|
||||
|
||||
|
||||
def test_model_saying_no_ticket_while_a_ticket_prints_the_piece():
|
||||
pieces = [piece('90001', 'PNL101', ticket_created='No')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['state_contradictions'][0]['control'] == '90001'
|
||||
|
||||
|
||||
def test_unset_ticket_flag_is_not_a_contradiction():
|
||||
"""None is not False. 'the model never said' and 'the model said no' are
|
||||
different facts, and only the second is a contradiction."""
|
||||
pieces = [piece('90002', 'PNL102', ticket_created=None)]
|
||||
r = jobrecon.reconcile(pieces, [])
|
||||
assert r['state_contradictions'] == []
|
||||
|
||||
|
||||
def test_one_control_number_on_several_elements_is_reported():
|
||||
"""Three elements sharing a control number is one piece modelled three
|
||||
ways. It is counted once as a piece and reported once as a fact -- never
|
||||
filtered, whatever the families are called."""
|
||||
pieces = [piece('90200', 'BM100', family='BEAM', id=1),
|
||||
piece('90200', 'BM100', family='BEAM_FLAT (DO NOT USE)', id=2),
|
||||
piece('90200', 'BM100', family='BEAM_WARPED (DO NOT USE)', id=3)]
|
||||
sheets = [sheet('BM100', '1 OF 1', ['90200'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['totals']['elements'] == 3
|
||||
assert r['totals']['pieces'] == 1
|
||||
assert r['totals']['pieces_uncovered'] == 0
|
||||
row = r['shared_controls'][0]
|
||||
assert row['count'] == 3
|
||||
assert 'BEAM_FLAT (DO NOT USE)' in row['families']
|
||||
|
||||
|
||||
def test_an_element_with_no_control_number_is_a_blind_spot_not_a_pass():
|
||||
pieces = [piece('90001', 'PNL101'), piece('', 'PNL199', id=4242)]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['totals']['pieces'] == 1
|
||||
assert r['pieces_without_control'][0]['id'] == 4242
|
||||
assert any('no control number' in b for b in r['blind_spots'])
|
||||
|
||||
|
||||
def test_unreadable_slot_becomes_a_blind_spot():
|
||||
pieces = [piece('90001', 'PNL101')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001', 'SEE SCHEDULE'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['unreadable_control_slots'][0]['token'] == 'SEE SCHEDULE'
|
||||
assert any('cannot read' in b for b in r['blind_spots'])
|
||||
|
||||
|
||||
def test_a_genuinely_clean_job_says_so_and_has_no_blind_spots():
|
||||
pieces = [piece('90001', 'PNL101'), piece('90002', 'PNL101')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001, 90002'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['totals']['pieces_uncovered'] == 0
|
||||
assert r['controls_without_piece'] == []
|
||||
assert r['mark_count_mismatch'] == []
|
||||
assert r['blind_spots'] == []
|
||||
|
||||
|
||||
def test_coverage_by_design_shows_a_wholly_unticketed_design():
|
||||
pieces = [piece('90001', 'PNL101', design='PNL1.0'),
|
||||
piece('90300', 'BM100', design='RB1'),
|
||||
piece('90301', 'BM101', design='RB1')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['coverage_by_design']['RB1'] == {
|
||||
'pieces': 2, 'covered': 0, 'uncovered': ['90300', '90301']}
|
||||
assert r['coverage_by_design']['PNL1.0']['covered'] == 1
|
||||
|
||||
|
||||
def test_nothing_is_excluded_for_looking_out_of_scope():
|
||||
"""The original sin this module is built against: one unauthorised
|
||||
exclusion hid forty tickets and every check that ran came back clean.
|
||||
A piece whose family is literally named 'DO NOT USE' is still a piece."""
|
||||
pieces = [piece('90400', 'BM199', family='ZZ_SCRATCH (DO NOT USE)')]
|
||||
r = jobrecon.reconcile(pieces, [])
|
||||
assert [x['control'] for x in r['pieces_without_ticket']] == ['90400']
|
||||
|
||||
|
||||
# --------------------------------------------------------- format_report
|
||||
|
||||
def test_report_names_pieces_rather_than_counting_them():
|
||||
pieces = [piece('90002', 'PNL102')]
|
||||
text = '\n'.join(jobrecon.format_report(jobrecon.reconcile(pieces, [])))
|
||||
assert '90002' in text and 'PNL102' in text
|
||||
|
||||
|
||||
def test_report_says_what_it_could_not_judge():
|
||||
pieces = [piece('', 'PNL199', id=7)]
|
||||
text = '\n'.join(jobrecon.format_report(jobrecon.reconcile(pieces, [])))
|
||||
assert 'COULD NOT JUDGE' in text
|
||||
assert 'no control number' in text
|
||||
|
||||
|
||||
def test_report_truncation_admits_it_truncated():
|
||||
pieces = [piece(str(90000 + n), 'PNL1' + str(n)) for n in range(40)]
|
||||
text = '\n'.join(jobrecon.format_report(jobrecon.reconcile(pieces, []),
|
||||
limit=5))
|
||||
assert 'and 35 more (not printed)' in text
|
||||
|
||||
|
||||
def test_report_on_a_clean_job_says_nothing_was_unseen():
|
||||
pieces = [piece('90001', 'PNL101')]
|
||||
sheets = [sheet('PNL101', '1 OF 1', ['90001'])]
|
||||
text = '\n'.join(jobrecon.format_report(jobrecon.reconcile(pieces, sheets)))
|
||||
assert 'every piece and every sheet was read' in text
|
||||
|
||||
|
||||
# ------------------------------------------------------- the declared scope
|
||||
#
|
||||
# The reconciliation is the one entry point that sweeps the WHOLE job, so it
|
||||
# is the one where judging another engineer's pieces does the most damage:
|
||||
# every beam ticket becomes a disagreement, and the panels' real findings
|
||||
# drown in them. These tests pin the three states -- undeclared, declared and
|
||||
# filtering, declared and filtering nothing -- and above all that what was
|
||||
# set aside is COUNTED AND NAMED rather than missing.
|
||||
|
||||
from vibe import scope as scopelib
|
||||
|
||||
|
||||
def _mixed():
|
||||
pieces = [piece('1', 'PNL001'), piece('2', 'PNL002'),
|
||||
piece('3', 'XX001', design='XX1'),
|
||||
piece('4', 'YY001', design='YY1')]
|
||||
sheets = [sheet('PNL001', '1 OF 1', ['1']),
|
||||
sheet('XX001', '1 OF 1', ['3'], design='XX1'),
|
||||
sheet('GENERAL ARRANGEMENT', 'S-101', [], design='')]
|
||||
return pieces, sheets
|
||||
|
||||
|
||||
def test_no_scope_declared_reconciles_every_piece():
|
||||
"""An undeclared scope is not an empty scope. Nothing is set aside and
|
||||
the report still carries the line saying so."""
|
||||
pieces, sheets = _mixed()
|
||||
r = jobrecon.reconcile(pieces, sheets)
|
||||
assert r['totals']['pieces'] == 4
|
||||
assert r['out_of_scope']['declared'] is False
|
||||
assert r['out_of_scope']['piece_count'] == 0
|
||||
assert 'No scope is declared' in r['out_of_scope']['piece_line']
|
||||
|
||||
|
||||
def test_a_declared_scope_sets_the_other_engineers_pieces_aside():
|
||||
pieces, sheets = _mixed()
|
||||
r = jobrecon.reconcile(pieces, sheets,
|
||||
scope=scopelib.Scope(prefixes=('PNL',)))
|
||||
assert r['totals']['pieces'] == 2
|
||||
assert r['out_of_scope']['piece_count'] == 2
|
||||
assert r['out_of_scope']['pieces'] == [('XX', 1), ('YY', 1)]
|
||||
# And nothing about them is reported as a finding -- neither clean nor
|
||||
# failing. They are simply not in the arithmetic. PNL002 genuinely has
|
||||
# no ticket and is still reported; the other engineers' pieces are not.
|
||||
found = [m for row in r['pieces_without_ticket'] for m in row['marks']]
|
||||
assert found == ['PNL002']
|
||||
|
||||
|
||||
def test_the_out_of_scope_ticket_is_not_reported_as_an_orphan():
|
||||
"""The failure this prevents: another engineer's ticket printing control
|
||||
numbers whose pieces were filtered out reads as a fault on his work."""
|
||||
pieces, sheets = _mixed()
|
||||
r = jobrecon.reconcile(pieces, sheets,
|
||||
scope=scopelib.Scope(prefixes=('PNL',)))
|
||||
assert r['controls_without_piece'] == []
|
||||
assert r['out_of_scope']['sheet_count'] == 1
|
||||
|
||||
|
||||
def test_a_sheet_that_is_not_a_mark_is_never_treated_as_somebody_elses():
|
||||
pieces, sheets = _mixed()
|
||||
r = jobrecon.reconcile(pieces, sheets,
|
||||
scope=scopelib.Scope(prefixes=('PNL',)))
|
||||
kinds = dict(r['out_of_scope']['sheets'])
|
||||
assert 'GENERAL' not in kinds
|
||||
assert kinds == {'XX': 1}
|
||||
|
||||
|
||||
def test_a_piece_with_no_readable_mark_is_reconciled_not_dropped():
|
||||
pieces = [piece('1', 'PNL001'), piece('2', '')]
|
||||
r = jobrecon.reconcile(pieces, [], scope=scopelib.Scope(prefixes=('PNL',)))
|
||||
assert r['totals']['pieces'] == 2
|
||||
assert r['out_of_scope']['piece_count'] == 0
|
||||
assert 'no readable mark prefix' in r['out_of_scope']['piece_line']
|
||||
|
||||
|
||||
def test_the_report_prints_what_it_did_not_judge_as_its_own_section():
|
||||
pieces, sheets = _mixed()
|
||||
r = jobrecon.reconcile(pieces, sheets,
|
||||
scope=scopelib.Scope(prefixes=('PNL',),
|
||||
declared_by='somebody'))
|
||||
text = '\n'.join(jobrecon.format_report(r))
|
||||
assert 'WHAT THIS RUN DID NOT JUDGE -- OUTSIDE THE DECLARED SCOPE' in text
|
||||
assert 'XX 1' in text and 'YY 1' in text
|
||||
assert 'scope declared by somebody' in text
|
||||
# The two states stay separate: not judged, and could not be judged.
|
||||
assert 'WHAT THIS RUN COULD NOT JUDGE' in text
|
||||
|
||||
|
||||
def test_the_scope_section_is_printed_even_when_nothing_was_set_aside():
|
||||
pieces = [piece('1', 'PNL001')]
|
||||
r = jobrecon.reconcile(pieces, [], scope=scopelib.Scope(prefixes=('PNL',)))
|
||||
text = '\n'.join(jobrecon.format_report(r))
|
||||
assert 'WHAT THIS RUN DID NOT JUDGE -- OUTSIDE THE DECLARED SCOPE' in text
|
||||
assert 'none -- nothing was set aside' in text
|
||||
|
||||
|
||||
# --------------------------------------------------------- the folding page
|
||||
#
|
||||
# THE RULE, and whose it is: the owner, 2026-08-30, now standing across the
|
||||
# tab. The answer is the headline and is never folded; everything else is
|
||||
# collapsed; a section that carries a problem opens itself; a folded header
|
||||
# still says what it holds. The contract the reader gets is that NOTHING OPEN
|
||||
# BELOW THE HEADLINE MEANS NOTHING NEEDS HIM -- so the tests that matter most
|
||||
# here are the two that guard it in both directions: a clean run opens
|
||||
# nothing, and a run with a finding opens exactly that finding.
|
||||
#
|
||||
# The fold itself is `vibe.foldreport`'s and is tested there. These are about
|
||||
# which sections this report has, what is in the headline, and what may never
|
||||
# be folded into silence.
|
||||
|
||||
from vibe import foldreport as fold
|
||||
|
||||
|
||||
def _clean():
|
||||
return jobrecon.reconcile([piece('90001', 'PNL101')],
|
||||
[sheet('PNL101', '1 OF 1', ['90001'])])
|
||||
|
||||
|
||||
def _by_key(sections):
|
||||
return dict((s.key, s) for s in sections)
|
||||
|
||||
|
||||
def _md(section):
|
||||
return '\n'.join(b[1] for b in section.blocks if b[0] == fold.MD)
|
||||
|
||||
|
||||
def test_the_headline_is_first_and_is_never_folded():
|
||||
sections = jobrecon.render(_clean())
|
||||
assert sections[0].headline is True
|
||||
assert sections[0].is_open is True
|
||||
text = _md(sections[0])
|
||||
assert 'JOB RECONCILIATION' in text
|
||||
# The verdict is the NUMBERS, not the findings.
|
||||
assert '1 of 1 pieces are printed on a ticket' in text
|
||||
assert '1 of 1 marks have a ticket' in text
|
||||
|
||||
|
||||
def test_a_clean_run_opens_nothing_below_the_headline():
|
||||
"""The whole contract. If this fails the page is lying to the reader."""
|
||||
sections = jobrecon.render(_clean())
|
||||
assert [s.key for s in sections[1:] if s.is_open] == []
|
||||
|
||||
|
||||
def test_a_finding_opens_its_own_section_and_says_why():
|
||||
pieces = [piece('90001', 'PNL101'),
|
||||
piece('90002', 'PNL101', ticket_created='Yes')]
|
||||
sections = jobrecon.render(
|
||||
jobrecon.reconcile(pieces, [sheet('PNL101', '1 OF 1', ['90001'])]))
|
||||
opened = _by_key(fold.sections_with_problems(sections[1:]))
|
||||
assert 'missing_from_ticket' in opened
|
||||
# A section that opens has to name what is wrong, not merely be open.
|
||||
assert '1 piece(s)' in opened['missing_from_ticket'].problem
|
||||
|
||||
|
||||
def test_the_headline_carries_the_count_of_every_finding():
|
||||
"""A headline saying 'clean' over an open section below it is the page
|
||||
this replaces. The counts are built from the same list the sections are,
|
||||
so the two cannot disagree."""
|
||||
pieces = [piece('90002', 'PNL102'), piece('90003', 'PNL103')]
|
||||
recon = jobrecon.reconcile(pieces, [])
|
||||
sections = jobrecon.render(recon)
|
||||
tables = [b for b in sections[0].blocks if b[0] == fold.TABLE]
|
||||
counted = dict((row[0], row[1]) for row in tables[0][2])
|
||||
opened = [s for s in sections[1:] if s.problem and s.key != 'blind_spots']
|
||||
assert len(counted) == len(opened)
|
||||
assert counted['PIECES WITH NO TICKET FOR THEIR MARK AT ALL'] == 2
|
||||
|
||||
|
||||
def test_every_check_is_either_a_section_or_named_as_having_found_nothing():
|
||||
"""Folded is not gone, and a check that passed is not gone either:
|
||||
'ran and found none' and 'never ran' have to stay tellable apart."""
|
||||
recon = _clean()
|
||||
sections = jobrecon.render(recon)
|
||||
keys = set(s.key for s in sections)
|
||||
printed = _md(_by_key(sections)['clear'])
|
||||
for key, title, _rows, _phrase, _line, _lead in jobrecon.findings(recon):
|
||||
assert key in keys or title in printed
|
||||
|
||||
|
||||
def test_a_routine_section_is_folded_and_its_header_says_what_it_holds():
|
||||
coverage = _by_key(jobrecon.render(_clean()))['coverage']
|
||||
assert coverage.is_open is False
|
||||
assert '1 design(s)' in coverage.summary
|
||||
|
||||
|
||||
def test_the_out_of_scope_header_carries_its_counts_while_folded():
|
||||
"""Read with the fold SHUT. A header saying 'details' would soften
|
||||
exactly the distinction this section exists to keep."""
|
||||
pieces = [piece('1', 'PNL001'), piece('2', 'BM001', design='BM1'),
|
||||
piece('3', 'BM002', design='BM1'),
|
||||
piece('4', 'SS001', design='SS1')]
|
||||
recon = jobrecon.reconcile(pieces, [],
|
||||
scope=scopelib.Scope(prefixes=('PNL',)))
|
||||
section = _by_key(jobrecon.render(recon))['out_of_scope']
|
||||
assert section.is_open is False
|
||||
assert 'OUT OF SCOPE: 3 model element(s)' in section.summary
|
||||
assert 'BM 2' in section.summary and 'SS 1' in section.summary
|
||||
|
||||
|
||||
def test_the_blind_spot_header_carries_its_count_while_folded():
|
||||
recon = jobrecon.reconcile([piece('90001', 'PNL101'),
|
||||
piece('', 'PNL199', id=7)],
|
||||
[sheet('PNL101', '1 OF 1', ['90001'])])
|
||||
section = _by_key(jobrecon.render(recon))['blind_spots']
|
||||
assert section.is_open is False
|
||||
assert '1 blind spot(s)' in section.summary
|
||||
|
||||
|
||||
def test_could_not_see_and_did_not_judge_are_never_the_same_section():
|
||||
recon = jobrecon.reconcile([piece('1', 'PNL001'),
|
||||
piece('2', 'BM001', design='BM1')],
|
||||
[], scope=scopelib.Scope(prefixes=('PNL',)))
|
||||
keys = [s.key for s in jobrecon.render(recon)]
|
||||
assert 'out_of_scope' in keys and 'blind_spots' in keys
|
||||
|
||||
|
||||
def test_a_refusal_is_a_problem_and_opens_itself():
|
||||
"""Nothing reconciled is not a clean job. It opens the blind spots and
|
||||
it is said again in the headline, because a refusal reported quietly
|
||||
reads exactly like a pass."""
|
||||
recon = jobrecon.reconcile([], [])
|
||||
recon['blind_spots'].insert(
|
||||
0, 'the role member.control_number is not mapped -- no piece could '
|
||||
'be identified, so NOTHING was reconciled.')
|
||||
sections = jobrecon.render(recon)
|
||||
spots = _by_key(sections)['blind_spots']
|
||||
assert spots.is_open is True
|
||||
assert 'NOTHING WAS RECONCILED' in spots.problem
|
||||
headline = _md(sections[0])
|
||||
assert 'NOTHING WAS RECONCILED' in headline
|
||||
assert 'Nothing needs you' not in headline
|
||||
|
||||
|
||||
def test_an_empty_model_is_a_refusal_even_with_no_note_about_it():
|
||||
"""Structural, not only from the gatherer's note: a run can reconcile
|
||||
nothing without anybody having written a note saying so."""
|
||||
assert jobrecon.blind_refusal(jobrecon.reconcile([], [])) is not None
|
||||
assert jobrecon.blind_refusal(_clean()) is None
|
||||
|
||||
|
||||
def test_sheets_that_no_page_classified_as_a_ticket_is_a_refusal():
|
||||
recon = jobrecon.reconcile([piece('90001', 'PNL101')],
|
||||
[sheet('GENERAL ARRANGEMENT', 'S-101', [],
|
||||
design='')])
|
||||
assert 'NO SHEET WAS RECOGNISED AS A TICKET' in \
|
||||
jobrecon.blind_refusal(recon)
|
||||
|
||||
|
||||
def test_a_refusal_does_not_let_the_clean_checks_read_as_a_pass():
|
||||
recon = jobrecon.reconcile([], [])
|
||||
clear = _by_key(jobrecon.render(recon))['clear']
|
||||
assert 'not a pass' in clear.summary
|
||||
|
||||
|
||||
def test_the_page_caps_a_long_list_and_the_file_does_not():
|
||||
pieces = [piece(str(90000 + n), 'PNL1' + str(n)) for n in range(40)]
|
||||
recon = jobrecon.reconcile(pieces, [])
|
||||
page = fold.text(jobrecon.render(recon, limit=5))
|
||||
assert 'and 35 more (not printed)' in page
|
||||
# The file is the copy nothing is lost from: no cap, nothing folded.
|
||||
whole = '\n'.join(jobrecon.format_report(recon))
|
||||
assert 'not printed' not in whole
|
||||
assert '90039' in whole
|
||||
|
||||
|
||||
def test_the_file_prints_the_folded_sections_too():
|
||||
"""A folded section that will not open must cost a file open, never a
|
||||
sentence -- so `format_report` shows everything by default."""
|
||||
text = '\n'.join(jobrecon.format_report(_clean()))
|
||||
assert 'COVERAGE BY DESIGN' in text
|
||||
assert 'PAGE-SET INVENTORY' in text
|
||||
assert 'CHECKS THAT RAN AND FOUND NOTHING' in text
|
||||
|
||||
|
||||
def test_what_was_swept_folds_apart_from_what_could_not_be_seen():
|
||||
"""`jobreconscan` hands over one list of notes: 'I opened these
|
||||
collectors' is an inventory, 'I could not see X' is a blind spot, and the
|
||||
report folds the first while the second can open itself."""
|
||||
recon = _clean()
|
||||
recon['searched'] = ['categories searched: OST_Walls.']
|
||||
section = _by_key(jobrecon.render(recon))['searched']
|
||||
assert section.is_open is False
|
||||
assert 'OST_Walls' in _md(section)
|
||||
|
||||
|
||||
def test_the_page_names_the_file_the_whole_report_went_to():
|
||||
sections = _by_key(jobrecon.render(_clean(), full_path='X/Y.txt'))
|
||||
assert 'X/Y.txt' in _md(sections['run'])
|
||||
|
||||
|
||||
def test_no_header_carries_an_entity_the_panel_will_not_decode():
|
||||
"""This page writes ASCII '--' in a title, a summary and a problem, so the
|
||||
one string is right in the panel and in the .md alike. (`foldreport` does
|
||||
restore the reports' entities in those three since 2026-08-30, so an
|
||||
`—` would no longer leak -- but this page does not need one.)"""
|
||||
pieces = [piece('1', 'PNL001'), piece('2', 'BM001', design='BM1')]
|
||||
recon = jobrecon.reconcile(pieces, [],
|
||||
scope=scopelib.Scope(prefixes=('PNL',)))
|
||||
recon['timing'] = {'total_s': 1, 'pieces_s': 1, 'sheets_s': 0}
|
||||
for section in jobrecon.render(recon):
|
||||
for value in (section.title, section.summary, section.problem):
|
||||
assert '&' not in (value or '')
|
||||
|
||||
|
||||
def test_every_section_key_is_unique_on_the_page():
|
||||
"""They become element ids for the toggle. Two of the same and one fold
|
||||
opens the other one's body."""
|
||||
pieces = [piece('1', 'PNL001'), piece('2', 'BM001', design='BM1')]
|
||||
recon = jobrecon.reconcile(pieces, [],
|
||||
scope=scopelib.Scope(prefixes=('PNL',)))
|
||||
keys = [s.key for s in jobrecon.render(recon)]
|
||||
assert len(keys) == len(set(keys))
|
||||
|
||||
|
||||
def test_the_panel_html_renders_and_a_problem_section_needs_no_script():
|
||||
"""The toggle is IE5-era DOM and has not been exercised inside Revit, so
|
||||
a section with a problem in it must render OPEN as plain HTML."""
|
||||
sections = jobrecon.render(jobrecon.reconcile([piece('90002', 'PNL102')],
|
||||
[]))
|
||||
html = fold.html(sections[1:])
|
||||
# Every newline starts a new entry in pyRevit's panel stream. So: none.
|
||||
assert '\n' not in html
|
||||
assert 'display:block' in html
|
||||
assert '<details' not in html
|
||||
Reference in New Issue
Block a user