# -*- 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'] == []