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,336 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The control-number multiplier: where it comes from and what it refuses to say.
|
||||
|
||||
This is the one number that decides whether the takeoff is right, so the
|
||||
tests are about its FAILURE modes rather than its arithmetic. A source that
|
||||
returns a confident 1 for a mark it knows nothing about is the bug this
|
||||
module exists to make impossible.
|
||||
|
||||
Filenames here are invented. The shape they exercise -- job, control number,
|
||||
mark -- is the plant's export convention, not a job's data.
|
||||
"""
|
||||
# pylint: disable=import-error,invalid-name
|
||||
from vibe import takeoffcount as count
|
||||
|
||||
|
||||
# ---------------------------------------------------------- ticket filenames
|
||||
|
||||
def test_a_ticket_filename_yields_its_mark_and_its_control_number():
|
||||
assert count.parse_ticket_name('11111-20001_PNL101.pdf') == \
|
||||
('PNL101', '20001')
|
||||
|
||||
|
||||
def test_a_control_number_with_no_digit_in_it_is_not_a_control_number():
|
||||
"""The export folder also holds the detail sheets, and their names take
|
||||
the identical three-part shape. Twelve of them were being counted as
|
||||
marks until `join_quality` said so."""
|
||||
assert count.parse_ticket_name('11111-SHEET_X4-1.pdf') == (None, None)
|
||||
assert count.from_ticket_files(['11111-SHEET_X4-1.pdf']) == {}
|
||||
assert count.unparsed_ticket_files(
|
||||
['11111-SHEET_X4-1.pdf', '11111-20001_PNL101.pdf']) == \
|
||||
['11111-SHEET_X4-1.pdf']
|
||||
|
||||
|
||||
def test_a_lettered_control_number_is_still_a_control_number():
|
||||
assert count.parse_ticket_name('11111-A20001_PNL101.pdf') == \
|
||||
('PNL101', 'A20001')
|
||||
|
||||
|
||||
def test_a_file_named_by_mark_alone_is_ignored_not_counted_as_one_piece():
|
||||
"""The older export was named by mark. One file per mark is exactly how
|
||||
one piece per mark came to be believed, so it must contribute nothing."""
|
||||
assert count.parse_ticket_name('PNL101.pdf') == (None, None)
|
||||
assert count.from_ticket_files(['PNL101.pdf']) == {}
|
||||
|
||||
|
||||
def test_a_revision_is_a_reissued_ticket_not_a_second_piece():
|
||||
assert count.from_ticket_files([
|
||||
'11111-20001_PNL101.pdf',
|
||||
'11111-20001_PNL101_R1.pdf',
|
||||
]) == {'PNL101': (1, 'ticket_files')}
|
||||
|
||||
|
||||
def test_several_control_numbers_on_one_mark_are_several_pieces():
|
||||
got = count.from_ticket_files([
|
||||
'11111-20001_PNL101.pdf',
|
||||
'11111-20003_PNL101.pdf',
|
||||
'11111-20005_PNL101.pdf',
|
||||
'11111-20007_PNL102.pdf',
|
||||
])
|
||||
assert got == {'PNL101': (3, 'ticket_files'), 'PNL102': (1, 'ticket_files')}
|
||||
|
||||
|
||||
# ------------------------------------------------------- the other two sources
|
||||
|
||||
def test_duplicate_assembly_names_are_a_multiplier():
|
||||
assert count.from_assembly_names(
|
||||
['PNL101', 'PNL101', 'PNL102']) == {'PNL101': (2, 'assembly_instances')}
|
||||
|
||||
|
||||
def test_a_name_that_appears_once_is_absence_of_evidence_not_a_count_of_one():
|
||||
"""The bug this caught, kept as a test so it cannot come back.
|
||||
|
||||
Measured through the bridge on a real job: 755 assemblies, 755 distinct
|
||||
names, zero duplicates. An earlier version returned 1 for each, every
|
||||
mark got a confident basis, and every per-job figure silently equalled
|
||||
its per-mark figure -- a whole-job undercount that looked sourced.
|
||||
"""
|
||||
assert count.from_assembly_names(['A', 'B', 'C']) == {}
|
||||
|
||||
|
||||
def test_control_numbers_off_the_model_elements_are_the_real_source():
|
||||
got, unreadable = count.from_piece_records([
|
||||
{'id': 1, 'mark': 'PNL101', 'control': '20001'},
|
||||
{'id': 2, 'mark': 'PNL101', 'control': '20003'},
|
||||
{'id': 3, 'mark': 'PNL102', 'control': '20005'},
|
||||
])
|
||||
assert got == {'PNL101': (2, 'control_numbers'),
|
||||
'PNL102': (1, 'control_numbers')}
|
||||
assert unreadable == []
|
||||
|
||||
|
||||
def test_a_control_number_on_several_elements_is_still_one_piece():
|
||||
"""2480 control-numbered elements resolved to 1776 pieces on this job.
|
||||
The difference is exactly this sharing, and counting elements would
|
||||
over-order by it."""
|
||||
got, _bad = count.from_piece_records([
|
||||
{'id': 1, 'mark': 'PNL101', 'control': '20001'},
|
||||
{'id': 2, 'mark': 'PNL101', 'control': '20001'},
|
||||
])
|
||||
assert got == {'PNL101': (1, 'control_numbers')}
|
||||
|
||||
|
||||
def test_a_printed_range_is_expanded_by_the_one_shared_expander():
|
||||
"""`50120-50123` is four pieces. The expansion is jobrecon's, not a
|
||||
second copy of it living here."""
|
||||
got, _bad = count.from_piece_records(
|
||||
[{'id': 1, 'mark': 'PNL101', 'control': '50120-50123'}])
|
||||
assert got == {'PNL101': (4, 'control_numbers')}
|
||||
|
||||
|
||||
def test_an_element_with_a_control_number_and_no_mark_is_named_not_dropped():
|
||||
got, unreadable = count.from_piece_records(
|
||||
[{'id': 7, 'mark': '', 'control': '20001'}])
|
||||
assert got == {}
|
||||
assert unreadable[0]['id'] == 7
|
||||
assert 'no mark' in unreadable[0]['why']
|
||||
|
||||
|
||||
def test_an_unreadable_control_field_is_returned_rather_than_swallowed():
|
||||
got, unreadable = count.from_piece_records(
|
||||
[{'id': 9, 'mark': 'PNL101', 'control': 'PL 1/4" X 28"'}])
|
||||
assert got == {}
|
||||
assert unreadable and unreadable[0]['id'] == 9
|
||||
|
||||
|
||||
def test_a_mark_with_no_readable_control_numbers_is_left_out_entirely():
|
||||
"""Not zero, and not one: absent, so the next source gets a turn."""
|
||||
assert count.from_control_numbers({'PNL101': [], 'PNL102': ['20001']}) == {
|
||||
'PNL102': (1, 'control_numbers')}
|
||||
|
||||
|
||||
def test_control_numbers_are_counted_distinct():
|
||||
assert count.from_control_numbers(
|
||||
{'PNL101': ['20001', '20001', '20003']})['PNL101'][0] == 2
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- precedence
|
||||
|
||||
def test_the_model_control_numbers_outrank_every_other_source():
|
||||
merged = count.merge(
|
||||
{'PNL101': (3, 'control_numbers')},
|
||||
{'PNL101': (1, 'ticket_files')},
|
||||
{'PNL101': (9, 'assembly_instances')})
|
||||
assert merged['PNL101'] == (3, 'control_numbers')
|
||||
|
||||
|
||||
def test_assembly_instances_is_ranked_last_of_the_three():
|
||||
assert count.ORDER[-1] == 'assembly_instances'
|
||||
merged = count.merge({'PNL101': (9, 'assembly_instances')},
|
||||
{'PNL101': (2, 'ticket_files')})
|
||||
assert merged['PNL101'] == (2, 'ticket_files')
|
||||
|
||||
|
||||
def test_a_worse_source_still_covers_a_mark_the_better_one_missed():
|
||||
merged = count.merge({'PNL101': (3, 'control_numbers')},
|
||||
{'PNL102': (2, 'ticket_files')})
|
||||
assert merged['PNL102'] == (2, 'ticket_files')
|
||||
|
||||
|
||||
def test_the_sources_are_compared_because_nothing_else_can_check_them():
|
||||
got = count.disagreements({'PNL101': (2, 'control_numbers')},
|
||||
{'PNL101': (3, 'ticket_files')})
|
||||
assert got == [{'mark': 'PNL101',
|
||||
'left': 2, 'left_basis': 'control_numbers',
|
||||
'right': 3, 'right_basis': 'ticket_files'}]
|
||||
|
||||
|
||||
def test_agreement_is_silent():
|
||||
assert count.disagreements({'PNL101': (3, 'control_numbers')},
|
||||
{'PNL101': (3, 'ticket_files')}) == []
|
||||
|
||||
|
||||
# ------------------------------------------- what one exported ticket covers
|
||||
#
|
||||
# `nomenclature.ticket_unit` decides whether a folder of exported tickets is
|
||||
# a count of PIECES or a count of DRAWINGS, and the same folder reads to two
|
||||
# different numbers under the two answers. The tests below are the pin: for
|
||||
# the declared value the numbers must be exactly what they were when the
|
||||
# ranking was a constant, and every other state must be a visible refusal
|
||||
# rather than a quiet fallback to it.
|
||||
|
||||
# One fixture, exercised by every case, built so that all three sources have
|
||||
# something to say and each of them is the ONLY source for at least one mark.
|
||||
# A mark that only the tickets cover is the whole question: it is the mark
|
||||
# that changes value if the ticket unit is guessed.
|
||||
CONTROL = {'PNL101': (3, 'control_numbers'), 'PNL102': (1, 'control_numbers')}
|
||||
TICKETS = {'PNL101': (2, 'ticket_files'), 'PNL103': (4, 'ticket_files')}
|
||||
ASSEMBLIES = {'PNL101': (9, 'assembly_instances'),
|
||||
'PNL104': (2, 'assembly_instances')}
|
||||
SOURCES = {'control_numbers': CONTROL, 'ticket_files': TICKETS,
|
||||
'assembly_instances': ASSEMBLIES}
|
||||
|
||||
# What the ranking produced when it was hardcoded. Written down rather than
|
||||
# recomputed, because a pin that recomputes both sides pins nothing.
|
||||
AS_IT_ALWAYS_WAS = {
|
||||
'PNL101': (3, 'control_numbers'),
|
||||
'PNL102': (1, 'control_numbers'),
|
||||
'PNL103': (4, 'ticket_files'),
|
||||
'PNL104': (2, 'assembly_instances'),
|
||||
}
|
||||
|
||||
|
||||
def test_one_ticket_per_control_number_ranks_all_three_sources():
|
||||
plan = count.counting_plan('control_number', SOURCES)
|
||||
assert plan['order'] == count.ORDER
|
||||
assert plan['withdrawn'] == ()
|
||||
assert plan['refusal'] is None
|
||||
assert 'one ticket per control number' in plan['reason']
|
||||
|
||||
|
||||
def test_the_declared_unit_does_not_move_a_single_count():
|
||||
"""The pin. Wiring the key changed the SHAPE and must not change a number
|
||||
on a job that declares the value the ranking used to assume."""
|
||||
plan = count.counting_plan('control_number', SOURCES)
|
||||
merged = count.merge(CONTROL, TICKETS, ASSEMBLIES, order=plan['order'])
|
||||
assert merged == AS_IT_ALWAYS_WAS
|
||||
assert count.summarise(merged)['pieces'] == 10
|
||||
|
||||
|
||||
def test_merge_with_no_order_is_still_the_full_ranking():
|
||||
"""Every caller that has not asked the question gets what it always got."""
|
||||
assert count.merge(CONTROL, TICKETS, ASSEMBLIES) == AS_IT_ALWAYS_WAS
|
||||
|
||||
|
||||
def test_one_ticket_per_mark_withdraws_the_exported_tickets():
|
||||
"""A ticket covering a whole mark carries ONE control number in its
|
||||
filename and several pieces on its face. Counting the filename would
|
||||
return a confident 1 for a mark that is really seven."""
|
||||
plan = count.counting_plan('piece_mark', SOURCES)
|
||||
assert plan['withdrawn'] == ('ticket_files',)
|
||||
assert plan['refusal'] is None # somebody declared it
|
||||
merged = count.merge(CONTROL, TICKETS, ASSEMBLIES, order=plan['order'])
|
||||
assert 'PNL103' not in merged # the tickets' own mark is gone
|
||||
assert merged['PNL101'] == (3, 'control_numbers')
|
||||
|
||||
|
||||
def test_an_undeclared_ticket_unit_refuses_rather_than_assuming_one():
|
||||
"""The judgement call, pinned. Falling back to `control_number` would
|
||||
under-count a job ticketed the other way, and it would do it wearing a
|
||||
source's name instead of `assumed_one`."""
|
||||
plan = count.counting_plan(None, SOURCES)
|
||||
assert plan['declared'] is False
|
||||
assert plan['withdrawn'] == ('ticket_files',)
|
||||
assert plan['refusal']
|
||||
assert 'nomenclature.ticket_unit' in plan['refusal']
|
||||
assert 'control_number' in plan['refusal'] # names both accepted values
|
||||
assert 'piece_mark' in plan['refusal']
|
||||
|
||||
|
||||
def test_an_unrecognised_value_is_a_refusal_not_a_fallback():
|
||||
"""A key set to something nobody implemented is an intention, misspelt.
|
||||
Reading it as the default does the opposite of what was asked."""
|
||||
plan = count.counting_plan('one per truck', SOURCES)
|
||||
assert plan['declared'] is True
|
||||
assert plan['recognised'] is False
|
||||
assert 'one per truck' in plan['refusal']
|
||||
assert plan['withdrawn'] == ('ticket_files',)
|
||||
|
||||
|
||||
def test_a_withdrawn_source_is_dropped_and_never_ranked_last():
|
||||
"""Ranking it 99th would let it through on exactly the marks nothing else
|
||||
covers -- which is where a wrong multiplier does its damage."""
|
||||
plan = count.counting_plan(None)
|
||||
merged = count.merge(CONTROL, TICKETS, ASSEMBLIES, order=plan['order'])
|
||||
assert 'PNL103' not in merged
|
||||
got = count.summarise(merged, marks=['PNL101', 'PNL102', 'PNL103'])
|
||||
assert got['by_basis']['assumed_one'] == 1
|
||||
assert got['assumed_one'] == 1
|
||||
|
||||
|
||||
def test_the_plan_says_what_the_withdrawal_actually_cost():
|
||||
"""A rule and a finding are different things: the report opens on the
|
||||
marks lost, not on the key being unset."""
|
||||
plan = count.counting_plan(None, SOURCES)
|
||||
assert plan['withdrawn_marks'] == 2 # PNL101 and PNL103
|
||||
assert plan['cost'] == 1 # only PNL103 had nothing else
|
||||
|
||||
|
||||
def test_an_undeclared_unit_with_no_tickets_to_withdraw_costs_nothing():
|
||||
plan = count.counting_plan(None, {'control_numbers': CONTROL})
|
||||
assert plan['refusal']
|
||||
assert plan['cost'] == 0
|
||||
|
||||
|
||||
def test_the_other_two_sources_do_not_turn_on_the_ticket_unit():
|
||||
"""A control number off a model element is one physical piece however the
|
||||
plant tickets it, and a duplicated assembly name is a duplicated piece
|
||||
either way. Only the ticket files read differently."""
|
||||
for unit in (None, 'control_number', 'piece_mark', 'nonsense'):
|
||||
order = count.counting_plan(unit)['order']
|
||||
assert 'control_numbers' in order and 'assembly_instances' in order
|
||||
assert order[0] == 'control_numbers'
|
||||
|
||||
|
||||
def test_every_state_carries_a_sentence_to_print():
|
||||
for unit in (None, 'control_number', 'piece_mark', 'nonsense'):
|
||||
plan = count.counting_plan(unit)
|
||||
assert plan['reason']
|
||||
assert plan['key'] == 'nomenclature.ticket_unit'
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ summary
|
||||
|
||||
def test_a_mark_nobody_could_source_is_counted_and_named_assumed_one():
|
||||
got = count.summarise({'PNL101': (3, 'ticket_files')},
|
||||
marks=['PNL101', 'PNL102'])
|
||||
assert got['marks'] == 2
|
||||
assert got['pieces'] == 4
|
||||
assert got['assumed_one'] == 1
|
||||
assert got['by_basis'] == {'ticket_files': 1, 'assumed_one': 1}
|
||||
|
||||
|
||||
def test_the_average_multiplier_is_reported_so_a_reader_can_smell_it():
|
||||
got = count.summarise({'A': (2, 'ticket_files'), 'B': (4, 'ticket_files')})
|
||||
assert got['multiplier'] == 3.0
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- the join
|
||||
|
||||
def test_a_source_naming_things_the_takeoff_never_heard_of_is_visible():
|
||||
"""The silent-and-total failure mode: the two sides key on different
|
||||
strings, every mark falls through to assumed_one, and the source still
|
||||
reports hundreds of marks found."""
|
||||
got = count.join_quality({'OTHER_1': (2, 'control_numbers')},
|
||||
marks=['PNL101', 'PNL102'])
|
||||
assert got['matched'] == 0
|
||||
assert got['unmatched_source'] == ['OTHER_1']
|
||||
assert got['unsourced'] == ['PNL101', 'PNL102']
|
||||
|
||||
|
||||
def test_a_clean_join_reports_no_strays():
|
||||
got = count.join_quality({'PNL101': (2, 'control_numbers')},
|
||||
marks=['PNL101'])
|
||||
assert got['matched'] == 1
|
||||
assert got['unmatched_source'] == []
|
||||
assert got['unsourced'] == []
|
||||
Reference in New Issue
Block a user