# -*- coding: utf-8 -*- """Rendering a takeoff. The tests are about what the page REFUSES to hide. A takeoff is trusted on sight, so the things worth testing are that an undercount says the word, that no quantity ever appears without both of its figures, and -- since the page now folds -- that folding never turns a caveat into an appendix. Layout is not under test; disclosure is. THE FOLDING CONTRACT, WHICH IS THE POINT OF HALF OF THIS FILE --------------------------------------------------------------- The owner's rule (`vibe.foldreport`) is that the answer is the headline and everything else starts closed, so that an OPEN section means "this needs you". A takeoff can break that contract in two directions and both are worse than the flat report was: * folding something that bites -- the refusal, an undercount, an unclassified family -- which hides it behind a click nobody makes; * opening something routine, which spends the contract until an open section stops meaning anything. So the tests assert both: a clean takeoff has NOTHING open below the headline, and each thing that bites opens itself and says why on its header. """ # pylint: disable=import-error,invalid-name from vibe import foldreport as fold from vibe import takeoffcore as core from vibe import takeoffreport as report CFG = core.settings({}) def cache(pieces): return {'schema': 3, 'built_at': 'once', 'pieces': pieces} def one(families, **extra): record = {'name': 'A', 'families': families, 'features': [], 'error': None, 'design_number': None} record.update(extra) return cache({'A': record}) def test_the_quantities_are_the_headline_and_carry_both_figures(): """The order is the answer, so it is what the page opens with.""" out = core.build(one({'P70': 1}), CFG) head = report.sections(out)[0] assert head.headline is True text = '\n'.join(b[1] for b in head.blocks if b[0] == fold.MD) assert 'Hardware, by part number' in text columns = [b[1] for b in head.blocks if b[0] == fold.TABLE][0] assert 'per_mark' in columns and 'per_job' in columns def test_an_unsourced_multiplier_says_the_word_undercount(): out = core.build(one({'P70': 1}), CFG) text = report.markdown(out, counts={ 'marks': 1, 'pieces': 1, 'multiplier': 1.0, 'by_basis': {'assumed_one': 1}, 'assumed_one': 1}) assert 'UNDERCOUNT' in text def test_no_multiplier_source_at_all_is_stated_rather_than_implied(): out = core.build(one({'P70': 1}), CFG) assert 'undercount' in report.markdown(out, counts=None) def test_an_unclassified_family_is_named_on_the_page(): out = core.build(one({'WIDGET_THING': 5}), CFG) text = report.markdown(out) assert 'WIDGET_THING' in text assert 'refuses to count' in text def test_the_unclassified_list_is_not_repeated_as_a_declared_exclusion(): """Two sections saying the same thing reads as two findings.""" out = core.build(one({'WIDGET_THING': 5}), CFG) text = report.markdown(out) assert text.count('WIDGET_THING') == 1 def test_an_attached_bar_appears_with_its_parent_named(): out = core.build(one({'SR9001': 6}), CFG, attached={'SR9001': ['P70']}) text = report.markdown(out) assert 'arrive attached' in text assert 'P70' in text.split('arrive attached')[1] def test_every_csv_row_carries_both_figures(): out = core.build(one({'P70': 4}), CFG, multipliers={'A': (3, 'ticket_files')}) lines = report.csv_totals(out).splitlines() assert lines[0].startswith('class,key,per_mark_total,per_job_total') assert lines[1].startswith('hardware,P70,4,12') def test_the_per_piece_csv_marks_a_piece_nobody_could_judge(): record = {'name': 'A', 'families': {}, 'features': None, 'error': 'members unreadable', 'design_number': None} out = core.build(cache({'A': record}), CFG) assert 'COULD_NOT_JUDGE' in report.csv_by_piece(out) def test_a_comma_in_a_family_name_does_not_shift_a_csv_column(): out = core.build(one({'ODD, NAME': 1}), CFG) assert '"ODD, NAME"' in report.csv_by_piece(out) def test_a_nesting_suspicion_says_nothing_was_subtracted(): pieces = {} for i, n in enumerate([2, 4, 8]): pieces['P%d' % i] = {'name': 'P%d' % i, 'features': [], 'error': None, 'design_number': None, 'families': {'PARENT': n, 'CHILD': n}} out = core.build(cache(pieces), CFG) text = report.markdown(out) assert 'Nothing has been subtracted' in text def test_an_unmapped_multiplier_role_is_printed_as_a_refusal(): """Not as a job in which every mark happens to be one piece.""" out = core.build(one({'P70': 1}), CFG) text = report.markdown(out, counts={ 'marks': 1, 'pieces': 1, 'multiplier': 1.0, 'by_basis': {'assumed_one': 1}, 'assumed_one': 1, 'refusal': 'the role is not mapped'}) assert 'could not be sourced' in text assert text.index('could not be sourced') < text.index('Quantities to order') def test_a_source_that_matches_nothing_is_shouted_about(): out = core.build(one({'P70': 1}), CFG) text = report.markdown(out, counts={ 'marks': 1, 'pieces': 1, 'multiplier': 1.0, 'by_basis': {'assumed_one': 1}, 'assumed_one': 1, 'join': {'source_marks': 400, 'matched': 0, 'unmatched_source': ['OTHER_1'], 'unsourced': ['A']}}) assert 'do not match any mark in this takeoff' in text def test_the_categories_the_multiplier_searched_are_named_on_the_page(): out = core.build(one({'P70': 1}), CFG) text = report.markdown(out, counts=None, extras={ 'multiplier': {'categories': ['OST_StructuralFraming']}}) assert 'OST_StructuralFraming' in text def test_an_unreadable_control_field_is_named_on_the_page(): out = core.build(one({'P70': 1}), CFG) text = report.markdown(out, counts=None, extras={ 'multiplier': {'unreadable': [ {'id': 9, 'mark': 'A', 'token': 'PL 1/4', 'why': 'not readable'}]}}) assert 'could not turn into pieces' in text assert 'PL 1/4' in text # ------------------------------------------------------- the declared scope # # A takeoff is an ORDER. Counting somebody else's pieces into it is not an # extra service, it is a wrong order -- and a takeoff that quietly counted # only some of the job would be worse still. So the narrowing is stated on # the page, above the quantities, in all three states. from vibe import scope as scopelib from vibe import takeoffgather as gather def _pop(names): return {'assemblies': len(set(names)), 'assembly_instances': len(names), 'names': list(names)} def test_apply_scope_narrows_index_and_population_together(): """Narrowing one and not the other makes the report claim a hole in the job where there is only somebody else's work.""" the_cache = cache({'PNL001': {}, 'XX001': {}, 'YY001': {}}) new_cache, new_pop, info = gather._apply_scope( scopelib.Scope(prefixes=('PNL',)), the_cache, _pop(['PNL001', 'XX001', 'YY001'])) assert sorted(new_cache['pieces']) == ['PNL001'] assert new_pop['names'] == ['PNL001'] assert info['left_alone'] == [('XX', 1), ('YY', 1)] assert info['counted_marks'] == 1 def test_apply_scope_never_edits_the_shared_index_in_place(): the_cache = cache({'PNL001': {}, 'XX001': {}}) gather._apply_scope(scopelib.Scope(prefixes=('PNL',)), the_cache, _pop(['PNL001', 'XX001'])) assert sorted(the_cache['pieces']) == ['PNL001', 'XX001'] def test_apply_scope_undeclared_counts_everything_and_still_says_so(): the_cache = cache({'PNL001': {}, 'XX001': {}}) new_cache, _pop_out, info = gather._apply_scope( scopelib.Scope(), the_cache, _pop(['PNL001', 'XX001'])) assert sorted(new_cache['pieces']) == ['PNL001', 'XX001'] assert info['declared'] is False assert 'No scope is declared' in info['line'] def test_the_page_prints_what_it_did_not_count(): takeoff = core.build(one({'SR100': 1}), CFG) _c, _p, info = gather._apply_scope( scopelib.Scope(prefixes=('PNL',), note='cladding only'), cache({'PNL001': {}, 'XX001': {}, 'XX002': {}}), _pop(['PNL001', 'XX001', 'XX002'])) text = report.markdown(takeoff, extras={'scope': info}) assert 'What this takeoff did not count' in text assert 'XX 2' in text assert 'cladding only' in text # And it stays separate from what the index could not see. assert 'What this takeoff could not see' in text def test_the_page_says_so_when_nothing_was_left_out(): takeoff = core.build(one({'SR100': 1}), CFG) _c, _p, info = gather._apply_scope( scopelib.Scope(prefixes=('PNL',)), cache({'PNL001': {}}), _pop(['PNL001'])) text = report.markdown(takeoff, extras={'scope': info}) assert 'nothing was set aside' in text # ------------------------------------------------------- the folding contract # # "If nothing below the headline is open, nothing needs me." Everything from # here down is that sentence, tested from both sides. def _clean(): """A takeoff with nothing wrong with it: every family classified, every mark in the index, every multiplier sourced.""" the_cache = cache({'A': {'name': 'A', 'families': {'P70': 2}, 'features': [], 'error': None, 'design_number': '101'}}) return core.build(the_cache, CFG, multipliers={'A': (3, 'control_numbers')}, population={'assemblies': 1, 'assembly_instances': 1, 'names': ['A']}) CLEAN_COUNTS = {'marks': 1, 'pieces': 3, 'multiplier': 3.0, 'by_basis': {'control_numbers': 1}, 'assumed_one': 0, 'sources': {'control_numbers': 1, 'ticket_files': 0, 'assembly_instances': 0}, 'join': {'source_marks': 1, 'matched': 1, 'unmatched_source': [], 'unsourced': []}} def _tail(takeoff, counts=None, extras=None): return report.sections(takeoff, counts, extras=extras)[1:] def _open(takeoff, counts=None, extras=None): return [s.key for s in fold.sections_with_problems( _tail(takeoff, counts, extras))] def test_a_takeoff_with_nothing_wrong_opens_nothing_below_the_headline(): """The contract itself. If a routine press opens a section, an open section stops meaning 'this needs you' and the whole page is furniture.""" assert _open(_clean(), CLEAN_COUNTS) == [] def test_every_folded_section_says_what_it_holds(): """A header that says only 'details' tells the reader nothing about whether to open it, and folded is only acceptable while it is not gone.""" for section in _tail(_clean(), CLEAN_COUNTS): assert section.summary, section.key def test_the_headline_says_what_multiplied_the_per_job_column(): """The totals can be lifted off this page alone, so the thing that scaled them has to be on it -- not only in the section that owns it.""" head = report.sections(_clean(), CLEAN_COUNTS)[0] text = '\n'.join(b[1] for b in head.blocks if b[0] == fold.MD) assert 'control_numbers 1' in text assert 'per_job' in text and 'per_mark' in text def test_a_refused_multiplier_is_open_and_printed_whole(): """The refusal is the one sentence on the page saying the numbers were never sourced. Folding it into an appendix would be the exact failure the flat report existed to prevent.""" out = core.build(one({'P70': 1}), CFG) counts = dict(CLEAN_COUNTS) counts['refusal'] = ('the role `member.control_number` is not mapped, so ' 'the model was not asked how many pieces each mark ' 'is') sections = report.sections(out, counts) multiplier = [s for s in sections if s.key == 'multiplier'][0] assert multiplier.is_open is True assert 'not asked how many pieces' in '\n'.join( b[1] for b in multiplier.blocks if b[0] == fold.MD) # and the headline itself does not let the totals be read without it head = '\n'.join(b[1] for b in sections[0].blocks if b[0] == fold.MD) assert 'UNDERCOUNT' in head def test_a_refused_multiplier_survives_a_dead_toggle(): """pyRevit's panel is an IE-engine WebBrowser and the fold is an inline onclick. If the script does not run, an open section must still be on the page as plain HTML.""" out = core.build(one({'P70': 1}), CFG) counts = dict(CLEAN_COUNTS) counts['refusal'] = 'nobody mapped the control-number role' html = fold.html(_tail(out, counts)) assert 'nobody mapped the control-number role' in html assert 'display:block' in html def test_the_panel_html_is_one_line_and_uses_nothing_this_engine_lacks(): html = fold.html(_tail(_clean(), CLEAN_COUNTS)) assert chr(10) not in html assert '