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,546 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for the shop-ticket revision delta.
|
||||
|
||||
Two modules under test, split so that neither needs a PDF:
|
||||
|
||||
* `vibe.ticketdelta` is pure -- it takes two already-extracted tickets as
|
||||
plain dicts and returns findings. Everything about WHAT COUNTS AS A CHANGE
|
||||
is tested here on hand-written dicts.
|
||||
* `vibe.ticketextract` imports pymupdf only inside `read_spans`, so its
|
||||
parsing and region logic import and test fine on a machine with no PDF
|
||||
library at all.
|
||||
|
||||
Most of these are regressions against false positives measured on the real
|
||||
old/new issue of one job, and each one names the ticket it came from. That is
|
||||
deliberate: for a diff tool the interesting failures are not "missed a change"
|
||||
but "invented forty", and a test that does not pin the invented ones lets them
|
||||
back in the next time the matcher is touched.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe import ticketdelta as td
|
||||
from vibe import ticketextract as tx
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# dimension parsing
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize('text,inches', [
|
||||
('22\'-11"', 275.0),
|
||||
('22\' - 11"', 275.0),
|
||||
('2\' - 5 1/2"', 29.5),
|
||||
('11 1/2"', 11.5),
|
||||
('8"', 8.0),
|
||||
('3/4"', 0.75),
|
||||
('1\'-0"', 12.0),
|
||||
])
|
||||
def test_parse_dim_accepts_every_printed_form(text, inches):
|
||||
assert tx.parse_dim(text) == pytest.approx(inches)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('text', [
|
||||
'',
|
||||
None,
|
||||
'BED PLAN',
|
||||
'P76 W/ SR5602 (S.I.F.)',
|
||||
'(5) @ 1\'-6" = 7\'-6"', # a note that contains dimensions
|
||||
'#3 X 11\'-8"', # a BOM description
|
||||
'20132',
|
||||
])
|
||||
def test_parse_dim_rejects_things_that_merely_contain_a_dimension(text):
|
||||
assert tx.parse_dim(text) is None
|
||||
|
||||
|
||||
def test_the_two_printed_spellings_of_one_length_compare_equal():
|
||||
"""The bed plan prints 1' - 0"; the handling diagrams print 1'-0".
|
||||
|
||||
Comparing the strings instead of the values would report a change on
|
||||
every ticket in the set.
|
||||
"""
|
||||
assert tx.parse_dim('1\' - 0"') == tx.parse_dim('1\'-0"')
|
||||
|
||||
|
||||
def test_format_inches_round_trips_through_parse():
|
||||
for v in (0.75, 8.0, 29.5, 275.0, 221.5):
|
||||
assert tx.parse_dim(tx.format_inches(v)) == pytest.approx(v)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# pairing the two issues
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_parse_name_reads_both_namings():
|
||||
old = td.parse_name('WP199.pdf')
|
||||
new = td.parse_name('25881-20132_WP199.pdf')
|
||||
assert old['mark'] == 'WP199' and old['control'] is None
|
||||
assert new['mark'] == 'WP199'
|
||||
assert new['job'] == '25881' and new['control'] == '20132'
|
||||
|
||||
|
||||
def test_parse_name_reads_a_revision_suffix():
|
||||
assert td.parse_name('25881-20107_WP179_R1.pdf')['revision'] == 'R1'
|
||||
|
||||
|
||||
def test_a_naming_change_between_issues_is_itself_reported():
|
||||
plan = td.pair_sets(['WP199.pdf'], ['25881-20132_WP199.pdf'])
|
||||
assert plan['naming']['changed'] is True
|
||||
assert plan['naming']['old_has_control'] == 0
|
||||
assert plan['naming']['new_has_control'] == 1
|
||||
|
||||
|
||||
def test_one_old_file_pairs_with_every_new_control_number():
|
||||
"""One ticket per control number: three identical panels, three tickets.
|
||||
|
||||
The old issue named files by mark and so had exactly one WP215. Reporting
|
||||
only the first of the three new ones would leave two issued tickets
|
||||
undiffed.
|
||||
"""
|
||||
plan = td.pair_sets(
|
||||
['WP215.pdf'],
|
||||
['25881-20161_WP215.pdf', '25881-20163_WP215.pdf',
|
||||
'25881-20165_WP215.pdf'])
|
||||
assert plan['naming']['fan_out'] == {'WP215': 3}
|
||||
assert len(plan['pairs']) == 1
|
||||
assert len(plan['pairs'][0][1]) == 3
|
||||
|
||||
|
||||
def test_a_mark_in_only_one_issue_is_named_not_dropped():
|
||||
plan = td.pair_sets(['WP199.pdf', 'WP999.pdf'], ['25881-20132_WP199.pdf',
|
||||
'25881-20999_WP888.pdf'])
|
||||
assert plan['old_only'] == ['WP999.pdf']
|
||||
assert plan['new_only'] == ['25881-20999_WP888.pdf']
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# helpers for building fake tickets
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def span(text, page=0, x=100.0, y=100.0, vertical=False, size=9.5):
|
||||
return {'page': page, 'text': text, 'x': x, 'y': y, 'x1': x + 30,
|
||||
'y1': y + 8, 'size': size, 'font': 'Arial', 'vertical': vertical,
|
||||
'block': 0}
|
||||
|
||||
|
||||
def chain(texts, page=0, vertical=False, pos=100.0, along=50.0, step=40.0):
|
||||
items = []
|
||||
for i, t in enumerate(texts):
|
||||
if vertical:
|
||||
items.append(span(t, page=page, x=pos, y=along + i * step,
|
||||
vertical=True))
|
||||
else:
|
||||
items.append(span(t, page=page, x=along + i * step, y=pos))
|
||||
return {'page': page, 'vertical': vertical, 'pos': pos, 'along': along,
|
||||
'items': items}
|
||||
|
||||
|
||||
def ticket(**kw):
|
||||
base = {'pages': 2, 'hazards': [], 'title_block': {}, 'bom': [],
|
||||
'handling': {}, 'notes': [], 'chains': [], 'view_titles': [],
|
||||
'callouts': [], 'timestamps': [], 'bom_bbox': None}
|
||||
base.update(kw)
|
||||
return base
|
||||
|
||||
|
||||
def diff(a, b):
|
||||
return td.compare(a, b, tx.parse_dim, tx.format_inches, tx.locate)
|
||||
|
||||
|
||||
def of(findings, kind=None, severity=None):
|
||||
return [f for f in findings
|
||||
if (kind is None or f['kind'] == kind)
|
||||
and (severity is None or f['severity'] == severity)]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# title block
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def _field(v, ok=True):
|
||||
return {'value': v, 'ok': ok, 'page': 0, 'x': 1000.0, 'y': 700.0}
|
||||
|
||||
|
||||
def test_a_changed_length_is_material():
|
||||
a = ticket(title_block={'LENGTH:': _field('22\'-11"')})
|
||||
b = ticket(title_block={'LENGTH:': _field('24\'-0"')})
|
||||
f = of(diff(a, b), 'TITLE')
|
||||
assert len(f) == 1
|
||||
assert f[0]['severity'] == td.MATERIAL
|
||||
assert f[0]['old'] == '22\'-11"' and f[0]['new'] == '24\'-0"'
|
||||
|
||||
|
||||
def test_a_changed_drafter_is_housekeeping_not_material():
|
||||
a = ticket(title_block={'DRAWN BY:': _field('AB')})
|
||||
b = ticket(title_block={'DRAWN BY:': _field('CD')})
|
||||
assert of(diff(a, b), 'TITLE')[0]['severity'] == td.HOUSEKEEP
|
||||
|
||||
|
||||
def test_a_title_value_that_fails_its_own_shape_is_uncertain_not_a_change():
|
||||
"""A cell read from the wrong side of a boundary is plausible and wrong.
|
||||
|
||||
Two plausible wrong strings diffed against each other is the worst output
|
||||
this tool could produce, so a value that does not look like a weight is
|
||||
reported as unreadable instead.
|
||||
"""
|
||||
a = ticket(title_block={'WEIGHT (lbs.):': _field('24345')})
|
||||
b = ticket(title_block={'WEIGHT (lbs.):': _field('SAMPLE_CLIENT(SAMPLE_CLIENT)', ok=False)})
|
||||
f = of(diff(a, b), 'TITLE')
|
||||
assert len(f) == 1 and f[0]['severity'] == td.UNCERTAIN
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# bill of material
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def _row(qty, mark, desc='', y=100.0):
|
||||
return {'qty': qty, 'mark': mark, 'desc': desc, 'page': 0, 'x': 960.0,
|
||||
'y': y}
|
||||
|
||||
|
||||
def test_a_bom_quantity_change_is_material_and_names_the_part():
|
||||
a = ticket(bom=[_row('4', 'P76', 'Erection Anchor - 8 ton')])
|
||||
b = ticket(bom=[_row('6', 'P76', 'Erection Anchor - 8 ton')])
|
||||
f = of(diff(a, b), 'BOM')
|
||||
assert len(f) == 1
|
||||
assert f[0]['severity'] == td.MATERIAL
|
||||
assert 'P76' in f[0]['where']
|
||||
assert (f[0]['old'], f[0]['new']) == ('4', '6')
|
||||
|
||||
|
||||
def test_a_bar_substitution_shows_as_one_out_one_in():
|
||||
a = ticket(bom=[_row('2', 'SR4183')])
|
||||
b = ticket(bom=[_row('2', 'SR4192')])
|
||||
f = of(diff(a, b), 'BOM')
|
||||
assert {x['what'] for x in f} == {'item removed from the bill of material',
|
||||
'item added to the bill of material'}
|
||||
|
||||
|
||||
def test_overlaid_text_in_the_bom_downgrades_every_bom_finding():
|
||||
"""Measured on a real reissue: the earlier file draws the strand row on
|
||||
top of the bar row above it, and the text layer hands back the wrong mark
|
||||
for the right quantity. Two phantom add/remove pairs and a 376 -> 4
|
||||
quantity, all completely plausible. They are reported -- as uncertain."""
|
||||
haz = [{'mode': 'overlaid-text', 'page': 0, 'x': 990.0, 'y': 384.0,
|
||||
'texts': ['0010001', 'SR5602'], 'detail': 'x'}]
|
||||
box = {'page': 0, 'x0': 950.0, 'y0': 30.0, 'x1': 1200.0, 'y1': 400.0}
|
||||
a = ticket(bom=[_row('376', 'SR5602')], hazards=haz, bom_bbox=box)
|
||||
b = ticket(bom=[_row('4', 'SR5602')], bom_bbox=box)
|
||||
f = of(diff(a, b), 'BOM')
|
||||
assert f and all(x['severity'] == td.UNCERTAIN for x in f)
|
||||
assert all('artefact' in (x['note'] or '') for x in f)
|
||||
|
||||
|
||||
def test_a_clean_bom_is_not_downgraded():
|
||||
box = {'page': 0, 'x0': 950.0, 'y0': 30.0, 'x1': 1200.0, 'y1': 400.0}
|
||||
a = ticket(bom=[_row('376', 'SR5602')], bom_bbox=box)
|
||||
b = ticket(bom=[_row('4', 'SR5602')], bom_bbox=box)
|
||||
assert of(diff(a, b), 'BOM')[0]['severity'] == td.MATERIAL
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# handling diagrams
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_the_dunnage_correction_is_reported_in_printed_order():
|
||||
"""WP199, the engineer's own fix: 4'-9"/4'-9" -> 5'-0"/3'-0".
|
||||
|
||||
The order is the order they are PRINTED in, left to right. Reading the
|
||||
pair the other way round was measured to turn this corrected ticket back
|
||||
into a failure elsewhere in this codebase, and a diff that argues with a
|
||||
fix its reader has already made is a diff he stops opening.
|
||||
"""
|
||||
a = ticket(handling={'DUNNAGE': [span('4\'-9"', x=820), span('4\'-9"', x=915)]})
|
||||
b = ticket(handling={'DUNNAGE': [span('5\'-0"', x=820), span('3\'-0"', x=915)]})
|
||||
f = of(diff(a, b), 'HANDLING')
|
||||
assert len(f) == 2
|
||||
assert all(x['severity'] == td.MATERIAL for x in f)
|
||||
assert (f[0]['old'], f[0]['new']) == ('4\'-9"', '5\'-0"')
|
||||
assert 'dimension 1 of 2' in f[0]['where']
|
||||
assert (f[1]['old'], f[1]['new']) == ('4\'-9"', '3\'-0"')
|
||||
|
||||
|
||||
def test_an_unchanged_handling_diagram_reports_nothing():
|
||||
d = {'DUNNAGE': [span('4\'-9"', x=820), span('4\'-9"', x=915)]}
|
||||
assert of(diff(ticket(handling=d), ticket(handling=d)), 'HANDLING') == []
|
||||
|
||||
|
||||
def test_the_same_station_spelled_two_ways_is_not_a_change():
|
||||
a = ticket(handling={'ERECTION': [span('2\' - 3"')]})
|
||||
b = ticket(handling={'ERECTION': [span('2\'-3"')]})
|
||||
assert of(diff(a, b), 'HANDLING') == []
|
||||
|
||||
|
||||
def test_a_diagram_gaining_a_dimension_is_reported_whole():
|
||||
a = ticket(handling={'PICKING/STRIPPING': [span('2\'-0"', x=820)]})
|
||||
b = ticket(handling={'PICKING/STRIPPING': [span('2\'-0"', x=820),
|
||||
span('6\'-6"', x=860)]})
|
||||
f = of(diff(a, b), 'HANDLING')
|
||||
assert len(f) == 1 and f[0]['severity'] == td.MATERIAL
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# dimension chains -- the false-positive regressions
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_a_replot_that_moves_every_chain_reports_nothing():
|
||||
"""WP194: adding one production note pushed the bed plan 89 points up the
|
||||
page. Every chain changed position; none changed value. The first version
|
||||
of this tool reported 40 material changes on that ticket."""
|
||||
texts = [['1\' - 9"', '7\' - 5"', '4\' - 7"'], ['8"'], ['2\' - 10 1/2"']]
|
||||
a = ticket(chains=[chain(t, pos=200.0 + 20 * i) for i, t in enumerate(texts)])
|
||||
b = ticket(chains=[chain(t, pos=111.0 + 20 * i) for i, t in enumerate(texts)])
|
||||
assert of(diff(a, b), 'DIM') == []
|
||||
|
||||
|
||||
def test_a_redistribution_says_the_total_did_not_change():
|
||||
"""WP219/WP220: a pair of P76 anchors pulled from 1'-0" to 1'-6" apart.
|
||||
|
||||
Two segments moved and the run they sit on is the same length as before.
|
||||
Saying so is the difference between "two numbers changed" and "the anchors
|
||||
moved, the panel did not get longer".
|
||||
"""
|
||||
a = ticket(chains=[chain(['3\' - 3"', '1\' - 0"', '4\' - 0"'])])
|
||||
b = ticket(chains=[chain(['2\' - 9"', '1\' - 6"', '4\' - 0"'])])
|
||||
f = of(diff(a, b), 'DIM')
|
||||
assert len(f) == 2
|
||||
assert all(x['severity'] == td.MATERIAL for x in f)
|
||||
assert (f[0]['old'], f[0]['new']) == ('3\' - 3"', '2\' - 9"')
|
||||
assert 'segment 1 of 3' in f[0]['where']
|
||||
assert 'total unchanged' in f[0]['note']
|
||||
|
||||
|
||||
def test_a_real_size_change_reports_the_new_total():
|
||||
a = ticket(chains=[chain(['1\' - 0"', '4\' - 0"'])])
|
||||
b = ticket(chains=[chain(['1\' - 6"', '4\' - 0"'])])
|
||||
f = of(diff(a, b), 'DIM')
|
||||
assert len(f) == 1
|
||||
assert '5\'-0"' in f[0]['note'] and '5\'-6"' in f[0]['note']
|
||||
|
||||
|
||||
def test_regrouping_the_same_values_is_not_a_change():
|
||||
"""WP196 and WP412: page 2 carries byte-identical dimension text on both
|
||||
issues, and the section detail was still re-laid-out enough that the
|
||||
chain grouping came out differently. Reported as chains that was five
|
||||
confident wrong material findings on two unchanged tickets."""
|
||||
a = ticket(chains=[chain(['4 1/2"', '5 3/4"', '2 1/4"'], vertical=True,
|
||||
pos=186.0),
|
||||
chain(['1 3/4"', '1 3/4"'], vertical=True, pos=197.0)])
|
||||
b = ticket(chains=[chain(['4 1/2"'], vertical=True, pos=150.0),
|
||||
chain(['5 3/4"', '2 1/4"', '1 3/4"', '1 3/4"'],
|
||||
vertical=True, pos=162.0)])
|
||||
assert of(diff(a, b), 'DIM') == []
|
||||
|
||||
|
||||
def test_a_dimension_that_genuinely_appears_is_still_reported_after_regrouping():
|
||||
a = ticket(chains=[chain(['4 1/2"', '5 3/4"'], vertical=True, pos=186.0)])
|
||||
b = ticket(chains=[chain(['4 1/2"'], vertical=True, pos=150.0),
|
||||
chain(['5 3/4"', '6\' - 9"'], vertical=True, pos=162.0)])
|
||||
f = of(diff(a, b), 'DIM')
|
||||
assert len(f) == 1
|
||||
assert f[0]['new'] == '6\' - 9"' and f[0]['severity'] == td.MATERIAL
|
||||
|
||||
|
||||
def test_chains_on_different_pages_are_never_matched_to_each_other():
|
||||
"""8" on page 1 and 1'-0" on page 2 swapping places is two changes, one on
|
||||
each page -- never one chain on page 1 matched to one on page 2."""
|
||||
a = ticket(chains=[chain(['8"'], page=0), chain(['1\'-0"'], page=1)])
|
||||
b = ticket(chains=[chain(['1\'-0"'], page=0), chain(['8"'], page=1)])
|
||||
f = of(diff(a, b), 'DIM')
|
||||
assert len(f) == 2
|
||||
assert {x['loc_old']['page'] for x in f} == {0, 1}
|
||||
for x in f:
|
||||
assert x['loc_old']['page'] == x['loc_new']['page']
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# callouts and notes
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_a_reworded_part_callout_is_one_material_finding_not_two_advisory():
|
||||
"""WP199: P106 (S.I.F.)(E.S.) became P106 (S.I.F.).
|
||||
|
||||
As two multiset lines that is noise a reader skims. As one finding naming
|
||||
the part, it is an instruction about where an embed goes.
|
||||
"""
|
||||
a = ticket(callouts=[span('P106 (S.I.F.)(E.S.)')])
|
||||
b = ticket(callouts=[span('P106 (S.I.F.)')])
|
||||
f = of(diff(a, b), 'CALLOUT')
|
||||
assert len(f) == 1
|
||||
assert f[0]['severity'] == td.MATERIAL
|
||||
assert 'P106' in f[0]['what']
|
||||
|
||||
|
||||
def test_a_callout_naming_no_part_stays_advisory():
|
||||
a = ticket(callouts=[span('BLOCKOUT')])
|
||||
b = ticket(callouts=[span('POCKET')])
|
||||
assert all(x['severity'] == td.ADVISORY for x in of(diff(a, b), 'CALLOUT'))
|
||||
|
||||
|
||||
def test_a_plot_stamp_is_housekeeping():
|
||||
a = ticket(callouts=[span('8/26/2026 3:40:39 PM')])
|
||||
b = ticket(callouts=[span('8/27/2026 9:10:48 AM')])
|
||||
assert all(x['severity'] == td.HOUSEKEEP for x in of(diff(a, b), 'CALLOUT'))
|
||||
|
||||
|
||||
def test_a_callout_that_only_moved_is_not_a_change():
|
||||
a = ticket(callouts=[span('SR333 (E.L.) TYP.', x=470.0, y=490.0)])
|
||||
b = ticket(callouts=[span('SR333 (E.L.) TYP.', x=478.0, y=402.0)])
|
||||
assert of(diff(a, b), 'CALLOUT') == []
|
||||
|
||||
|
||||
def _note(text, y=300.0):
|
||||
return {'text': text, 'page': 0, 'x': 500.0, 'y': y}
|
||||
|
||||
|
||||
def test_an_added_note_arrives_as_one_paragraph():
|
||||
a = ticket(notes=[])
|
||||
b = ticket(notes=[_note('PRODUCTION NOTE: POST POUR CHECK MUST ENSURE '
|
||||
'THAT ALL THREADED EMBEDS ARE CLEAN')])
|
||||
f = of(diff(a, b), 'NOTE')
|
||||
assert len(f) == 1 and f[0]['what'] == 'note added'
|
||||
|
||||
|
||||
def test_a_reworded_note_is_one_finding_with_both_versions():
|
||||
a = ticket(notes=[_note('PRODUCTION NOTE: GROMMET STRAND & SR603')])
|
||||
b = ticket(notes=[_note('PRODUCTION NOTE: GROMMET STRAND & SR625')])
|
||||
f = of(diff(a, b), 'NOTE')
|
||||
assert len(f) == 1
|
||||
assert f[0]['what'] == 'note reworded'
|
||||
assert 'SR603' in f[0]['old'] and 'SR625' in f[0]['new']
|
||||
|
||||
|
||||
def test_a_note_starts_at_its_heading_not_at_its_pdf_block():
|
||||
"""Measured: on page 2 of one ticket the PDF block holding a PRODUCTION
|
||||
NOTE also holds a 6" dimension and a bar tag drawn nearby. Taking the
|
||||
block whole reported the note as removed on one issue and added on the
|
||||
other, because the bar tag next to it had moved."""
|
||||
ss = [span('6"', y=10.0), span('SR567 (E.L.)', y=20.0),
|
||||
span('PRODUCTION NOTE:', y=30.0),
|
||||
span('3/4" CHAMFERS EA. FACE', y=40.0)]
|
||||
got = tx.notes(ss)
|
||||
assert len(got) == 1
|
||||
assert got[0]['text'] == 'PRODUCTION NOTE: 3/4" CHAMFERS EA. FACE'
|
||||
|
||||
|
||||
def test_two_notes_in_one_block_come_out_as_two_notes():
|
||||
ss = [span('PRODUCTION NOTE:', y=10.0), span('GROMMET STRAND', y=20.0),
|
||||
span('PRODUCTION NOTE:', y=30.0), span('3/4" CHAMFERS', y=40.0)]
|
||||
assert [n['text'] for n in tx.notes(ss)] == [
|
||||
'PRODUCTION NOTE: GROMMET STRAND', 'PRODUCTION NOTE: 3/4" CHAMFERS']
|
||||
|
||||
|
||||
def test_the_page_number_never_lands_inside_a_note():
|
||||
ss = [span('Notes:', y=10.0), span('1. 1/2" DIA. 270 ksi', y=20.0),
|
||||
span('PAGE 2 OF 2', y=30.0)]
|
||||
assert tx.notes(ss)[0]['text'] == 'Notes: 1. 1/2" DIA. 270 ksi'
|
||||
|
||||
|
||||
def test_a_changed_page_number_is_housekeeping():
|
||||
a = ticket(callouts=[span('PAGE 1 OF 2')])
|
||||
b = ticket(callouts=[span('PAGE 2 OF 2')])
|
||||
assert all(x['severity'] == td.HOUSEKEEP for x in of(diff(a, b), 'CALLOUT'))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# hazards
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_the_four_text_layer_failure_modes_are_each_detected():
|
||||
spans = [
|
||||
span('1316"'), # collapsed fraction
|
||||
span(u'\x04\x05\x06'), # cipher-shifted font
|
||||
span('SR5602', x=500.0, y=380.0), # overlaid pair ...
|
||||
span('0010001', x=500.4, y=380.2), # ... same point
|
||||
]
|
||||
modes = {h['mode'] for h in tx.hazards(spans, npages=1)}
|
||||
assert 'collapsed-fraction' in modes
|
||||
assert 'cipher-font' in modes
|
||||
assert 'overlaid-text' in modes
|
||||
|
||||
|
||||
def test_a_page_with_almost_no_text_is_flagged_as_unread_not_unchanged():
|
||||
haz = tx.hazards([span('BED PLAN')], npages=2)
|
||||
thin = [h for h in haz if h['mode'] == 'thin-text-layer']
|
||||
assert len(thin) == 2
|
||||
assert 'unread' in thin[0]['detail']
|
||||
|
||||
|
||||
def test_a_clean_file_produces_no_hazards():
|
||||
spans = [span('BED PLAN', x=10.0 * i, y=20.0 * i) for i in range(30)]
|
||||
assert tx.hazards(spans, npages=1) == []
|
||||
|
||||
|
||||
def test_every_hazard_reaches_the_report_as_uncertain():
|
||||
a = ticket(hazards=[{'mode': 'overlaid-text', 'page': 0, 'x': 10.0,
|
||||
'y': 10.0, 'texts': ['a', 'b'], 'detail': 'd'}])
|
||||
f = of(diff(a, ticket()), 'HAZARD')
|
||||
assert len(f) == 1 and f[0]['severity'] == td.UNCERTAIN
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# report shape
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_material_findings_sort_above_everything_else():
|
||||
a = ticket(title_block={'LENGTH:': _field('22\'-11"'),
|
||||
'DRAWN BY:': _field('AB')},
|
||||
notes=[_note('PRODUCTION NOTE: A')])
|
||||
b = ticket(title_block={'LENGTH:': _field('24\'-0"'),
|
||||
'DRAWN BY:': _field('CD')},
|
||||
notes=[_note('PRODUCTION NOTE: B')])
|
||||
sev = [f['severity'] for f in diff(a, b)]
|
||||
assert sev[0] == td.MATERIAL
|
||||
assert sev == sorted(sev, key=lambda s: td._ORDER[s])
|
||||
|
||||
|
||||
def test_a_page_count_change_is_material():
|
||||
f = of(diff(ticket(pages=2), ticket(pages=3)), 'SHEET')
|
||||
assert len(f) == 1 and f[0]['severity'] == td.MATERIAL
|
||||
|
||||
|
||||
def test_two_identical_tickets_produce_nothing_at_all():
|
||||
t = ticket(title_block={'LENGTH:': _field('22\'-11"')},
|
||||
bom=[_row('4', 'P76')],
|
||||
handling={'DUNNAGE': [span('4\'-9"')]},
|
||||
chains=[chain(['1\'-0"', '4\'-0"'])],
|
||||
notes=[_note('PRODUCTION NOTE: A')],
|
||||
callouts=[span('BLOCKOUT')])
|
||||
assert diff(t, t) == []
|
||||
|
||||
|
||||
def test_summarise_counts_every_severity():
|
||||
a = ticket(title_block={'LENGTH:': _field('22\'-11"')})
|
||||
b = ticket(title_block={'LENGTH:': _field('24\'-0"')})
|
||||
counts = td.summarise(diff(a, b))
|
||||
assert counts[td.MATERIAL] == 1
|
||||
assert set(counts) == {td.MATERIAL, td.ADVISORY, td.HOUSEKEEP,
|
||||
td.UNCERTAIN}
|
||||
|
||||
|
||||
def test_a_finding_can_always_be_pointed_at_on_the_sheet():
|
||||
"""Every material finding carries the coordinates its crop is rendered
|
||||
from. "3 differences" sends someone hunting; a picture does not."""
|
||||
a = ticket(chains=[chain(['3\' - 3"', '4\' - 0"'])])
|
||||
b = ticket(chains=[chain(['2\' - 9"', '4\' - 0"'])])
|
||||
f = of(diff(a, b), 'DIM')[0]
|
||||
assert f['loc_old']['page'] == 0 and f['loc_new']['page'] == 0
|
||||
assert f['loc_old']['x'] is not None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# locators
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_a_view_title_is_a_drawing_name_not_a_section_bubble():
|
||||
spans = [span('BED PLAN', size=12.6), span('A', size=12.6),
|
||||
span('MK', size=12.6), span('1/4" = 1\' - 0"', size=9.9),
|
||||
span('SECTION', page=1, size=12.6)]
|
||||
titles = tx.view_titles(spans)
|
||||
assert {t['text'] for t in titles} == {'BED PLAN', 'SECTION'}
|
||||
|
||||
|
||||
def test_locate_names_the_view_and_the_corner():
|
||||
titles = tx.view_titles([span('BED PLAN', size=12.6, x=350.0, y=620.0)])
|
||||
where = tx.locate(titles, 0, 100.0, 100.0)
|
||||
assert 'BED PLAN' in where and 'page 1' in where and 'upper left' in where
|
||||
Reference in New Issue
Block a user