25592fa6c2
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
448 lines
17 KiB
Python
448 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""The full view of the configuration: label-first, and what opens itself.
|
|
|
|
`vibe.configpage` is the SECONDARY surface of the Project Config button. The
|
|
button itself is a four-module window of controls (`vibe.configform`); this is
|
|
the "Full view" behind it -- every setting, every layer, and the provenance of
|
|
every value -- printed to the output panel and written whole to a file beside
|
|
the model. It has no Revit in it: the button passes in the layers it read, and
|
|
everything below is that contract exercised on host CPython.
|
|
|
|
Two of these tests are regressions for defects the owner hit on one press on
|
|
2026-08-30, and they are assertions rather than comments because both read as
|
|
cosmetic and neither was:
|
|
|
|
* `test_no_row_is_named_by_its_dotted_key` -- the screen offered
|
|
`handling.report_dir` as the name of an option when the row had carried
|
|
the label "Report folder" all along.
|
|
* `test_*_opens_itself` -- the fold contract: if nothing below the headline
|
|
is open, nothing below needs the reader. A section that opens has to say
|
|
why in a sentence.
|
|
|
|
And one is the seam with the window: `test_every_row_says_where_it_is_changed`
|
|
keeps the two surfaces from becoming two answers to the same question.
|
|
"""
|
|
from vibe import configmodel
|
|
from vibe import configpage
|
|
from vibe import foldreport as fold
|
|
|
|
|
|
# --------------------------------------------------------------- the fixture
|
|
|
|
def _tree(pairs):
|
|
out = {}
|
|
for key, value in pairs:
|
|
node = out
|
|
parts = key.split('.')
|
|
for part in parts[:-1]:
|
|
node = node.setdefault(part, {})
|
|
node[parts[-1]] = value
|
|
return out
|
|
|
|
|
|
def _sample(row):
|
|
"""A type-appropriate value for one SETTINGS row."""
|
|
kind = row['kind']
|
|
if kind == 'choice':
|
|
return (row['choices'] or ('fast',))[0]
|
|
if kind == 'bool':
|
|
return True
|
|
if kind in ('float', 'int'):
|
|
return 2 if kind == 'int' else 2.5
|
|
if kind == 'list':
|
|
return ['ANCHOR_FACE_4T']
|
|
if kind == 'map':
|
|
return {'3.3': 'no piece on this job has one'}
|
|
if kind == 'stages':
|
|
return [{'name': 'stripping', 'pci_case': 'stripping',
|
|
'strength': 'fci'}]
|
|
return 'something'
|
|
|
|
|
|
def _full():
|
|
return _tree([(row['key'], _sample(row)) for row in configmodel.SETTINGS])
|
|
|
|
|
|
def layers(defaults=None, plant=None, job=None, config=None,
|
|
plant_exists=True):
|
|
"""[(label, path, exists, data)] the way `configdump.layer_values` gives
|
|
it, with the project config layer added only when there is one."""
|
|
out = [('defaults', None, True, defaults if defaults is not None
|
|
else _full()),
|
|
('catalogue', 'C:/tool/catalogue.yaml', True, {}),
|
|
('plant', 'C:/plant/knowledge.yaml', plant_exists, plant or {}),
|
|
('job', 'C:/job/knowledge.yaml', True, job or {})]
|
|
if config is not None:
|
|
out.append(('config', 'C:/job/sample_job-config.yaml', True, config))
|
|
return out
|
|
|
|
|
|
def rendered(**over):
|
|
kwargs = {'title': 'sample_job', 'layers': layers(),
|
|
'job_path': 'C:/job/knowledge.yaml',
|
|
'env': [('WILLTOOLS_KNOWLEDGE', '(not set)')],
|
|
'run_options': {}, 'run_options_path': 'C:/job/run-options.yaml',
|
|
'locations': {'report_dir': 'C:/job/reports'},
|
|
'report_dir': 'C:/job/reports',
|
|
'subtrees': [('parameters', 'the office template',
|
|
[('member.control_number', 'Control Number',
|
|
'plant')])],
|
|
'muted': {}}
|
|
kwargs.update(over)
|
|
return configpage.render(**kwargs)
|
|
|
|
|
|
def by_key(sections):
|
|
return dict((section.key, section) for section in sections)
|
|
|
|
|
|
# ----------------------------------------------------------------- the rows
|
|
|
|
def test_rows_keep_a_stable_position():
|
|
rows = configpage.editable_rows(layers())
|
|
assert [row['n'] for row in rows] == list(range(1, len(rows) + 1))
|
|
|
|
|
|
def test_every_editable_setting_is_on_the_page_exactly_once():
|
|
rows = configpage.editable_rows(layers())
|
|
keys = [row['key'] for row in rows]
|
|
assert keys == [row['key'] for row in configmodel.editable()]
|
|
assert len(set(keys)) == len(keys)
|
|
|
|
|
|
def test_no_row_is_named_by_its_dotted_key():
|
|
"""The defect: the button offered `handling.report_dir` as an option name.
|
|
|
|
Every SETTINGS row carries a human label and this is what makes sure the
|
|
page uses it. The key is still on the row -- it is what somebody grepping
|
|
the YAML needs -- but it is never the name.
|
|
"""
|
|
for row in configpage.editable_rows(layers()):
|
|
assert row['label'], row['key']
|
|
assert row['label'] != row['key']
|
|
assert '.' not in row['label'] or ' ' in row['label']
|
|
|
|
|
|
def test_rows_group_under_the_human_section_names():
|
|
rows = configpage.editable_rows(layers())
|
|
groups = configpage.group_rows(rows)
|
|
names = [name for name, _rows in groups]
|
|
assert names == [name for name in configmodel.sections()
|
|
if any(row['section'] == name for row in rows)]
|
|
assert 'Output' in names
|
|
# A section name is prose, not a path fragment.
|
|
for name in names:
|
|
assert '.' not in name
|
|
|
|
|
|
def test_grouping_preserves_the_settings_order():
|
|
rows = configpage.editable_rows(layers())
|
|
seen = []
|
|
for _name, group in configpage.group_rows(rows):
|
|
seen.extend([row['n'] for row in group])
|
|
assert seen == sorted(seen)
|
|
|
|
|
|
# ------------------------------------------------------------- value_text()
|
|
|
|
def test_an_unknown_value_is_a_sentence_and_never_a_blank():
|
|
res = configmodel.resolve(layers(defaults={}), 'handling.rig_mode')
|
|
assert not res['known']
|
|
assert configpage.value_text('handling.rig_mode', res) == \
|
|
configpage.NOT_SET
|
|
assert configpage.value_text('handling.rig_mode', res).strip()
|
|
|
|
|
|
def test_fast_precise_is_shown_the_way_the_config_file_writes_it():
|
|
tree = _full()
|
|
tree['handling']['fast_lifter_box'] = False
|
|
res = configmodel.resolve(layers(defaults=tree),
|
|
'handling.fast_lifter_box')
|
|
assert configpage.value_text('handling.fast_lifter_box', res) == 'precise'
|
|
|
|
|
|
# ------------------------------------------------------------- the headline
|
|
|
|
def test_the_headline_is_first_and_is_never_folded():
|
|
sections = rendered()
|
|
assert sections[0].headline is True
|
|
assert sections[0].is_open is True
|
|
assert all(not section.headline for section in sections[1:])
|
|
|
|
|
|
def test_the_headline_is_label_first_with_the_key_as_small_print():
|
|
sections = rendered()
|
|
tables = [block for block in sections[0].blocks if block[0] == fold.TABLE]
|
|
assert tables
|
|
columns = tables[0][1]
|
|
assert columns[0] == 'setting'
|
|
assert columns[-1] == 'key'
|
|
first = tables[0][2][0]
|
|
assert first[0] != first[-1] # the label is not the key
|
|
assert first[-1].startswith('`') # the key is reference small print
|
|
|
|
|
|
def test_every_row_says_where_it_is_changed():
|
|
"""The column that makes this page the other half of the four-module
|
|
window instead of a second, competing surface."""
|
|
sections = rendered(placement={'handling.rig_mode': 'Lifting and handling'})
|
|
table = [b for b in sections[0].blocks if b[0] == fold.TABLE][0]
|
|
assert table[1][-2] == 'change it in'
|
|
wheres = set(row[-2] for row in table[2])
|
|
assert 'Lifting and handling' in wheres or 'the file' in wheres
|
|
for row in table[2]:
|
|
assert row[-2], 'a blank here reads as "nowhere"'
|
|
|
|
|
|
def test_a_setting_no_box_carries_says_it_is_changed_in_the_file():
|
|
sections = rendered(placement={})
|
|
table = [b for b in sections[0].blocks if b[0] == fold.TABLE][0]
|
|
assert set(row[-2] for row in table[2]) == set(['the file'])
|
|
|
|
|
|
def test_without_a_placement_the_column_is_simply_empty():
|
|
"""The page still renders for a caller that has no window to point at."""
|
|
sections = rendered()
|
|
table = [b for b in sections[0].blocks if b[0] == fold.TABLE][0]
|
|
assert set(row[-2] for row in table[2]) == set([''])
|
|
|
|
|
|
def test_the_headline_says_where_the_edit_goes():
|
|
text = '\n'.join(block[1] for block in rendered()[0].blocks
|
|
if block[0] == fold.MD)
|
|
assert 'C:/job/knowledge.yaml' in text
|
|
assert 'job layer' in text
|
|
|
|
|
|
def test_a_build_note_reaches_the_top_of_the_page():
|
|
text = '\n'.join(block[1] for block in
|
|
rendered(notes=['restart for the full view'])[0].blocks
|
|
if block[0] == fold.MD)
|
|
assert 'restart for the full view' in text
|
|
|
|
|
|
# -------------------------------------------------- what opens itself, and why
|
|
|
|
def test_nothing_opens_when_nothing_is_wrong():
|
|
for section in rendered()[1:]:
|
|
assert section.problem is None, section.key
|
|
assert section.is_open is False
|
|
|
|
|
|
def test_a_setting_no_layer_sets_opens_its_own_section():
|
|
tree = _full()
|
|
del tree['handling']['sling_angle_deg']
|
|
sections = by_key(rendered(layers=layers(defaults=tree)))
|
|
assert 'unknown' in sections
|
|
assert sections['unknown'].is_open
|
|
assert 'nothing in any layer decides' in sections['unknown'].problem
|
|
|
|
|
|
def test_a_missing_layer_file_opens_the_layers_section():
|
|
sections = by_key(rendered(layers=layers(plant_exists=False)))
|
|
assert sections['layers'].is_open
|
|
assert 'plant' in sections['layers'].problem
|
|
|
|
|
|
def test_a_live_config_file_opens_its_own_section_and_names_what_it_decides():
|
|
given = layers(config={'handling': {'rig_mode': 'rolling'}})
|
|
sections = by_key(rendered(layers=given, rows=None,
|
|
config_path='C:/job/sample_job-config.yaml'))
|
|
assert sections['config'].is_open
|
|
assert 'NOT' in sections['config'].problem
|
|
body = '\n'.join(block[1] for block in sections['config'].blocks
|
|
if block[0] == fold.MD)
|
|
assert 'sample_job-config.yaml' in body
|
|
|
|
|
|
def test_a_missing_config_file_says_so_rather_than_falling_back_quietly():
|
|
sections = by_key(rendered(config_path='C:/gone/config.yaml',
|
|
config_missing=True))
|
|
assert 'MISSING' in sections['config'].problem
|
|
|
|
|
|
def test_the_config_chip_marks_the_rows_the_file_decides():
|
|
given = layers(config={'handling': {'rig_mode': 'rolling'}})
|
|
rows = configpage.editable_rows(given)
|
|
marked = configpage.config_rows(rows)
|
|
assert [row['key'] for row in marked] == ['handling.rig_mode']
|
|
assert marked[0]['chip'] == '!'
|
|
|
|
|
|
def test_muted_items_open_their_section_and_are_listed_with_the_reason():
|
|
sections = by_key(rendered(muted={'3.3': 'no piece on this job has one'}))
|
|
assert sections['muted'].is_open
|
|
assert '1 checklist item' in sections['muted'].problem
|
|
rows = [block for block in sections['muted'].blocks
|
|
if block[0] == fold.TABLE][0][2]
|
|
assert rows[0][0] == '3.3'
|
|
assert 'no piece' in rows[0][1]
|
|
|
|
|
|
def test_an_unmapped_parameter_tree_opens_and_says_the_checks_go_quiet():
|
|
sections = by_key(rendered(subtrees=[('parameters', 'the template', [])]))
|
|
assert sections['params'].is_open
|
|
assert 'do not run' in sections['params'].problem
|
|
|
|
|
|
# ------------------------------------------------------------- the read-only
|
|
|
|
def test_every_read_only_setting_carries_the_sentence_saying_why():
|
|
section = by_key(rendered())['readonly']
|
|
body = '\n'.join(block[1] for block in section.blocks
|
|
if block[0] == fold.MD)
|
|
for row in configmodel.read_only():
|
|
assert row['label'] in body
|
|
assert (row['why'] or '')[:40] in body
|
|
|
|
|
|
def test_the_read_only_table_is_label_first_with_the_key_alongside():
|
|
section = by_key(rendered())['readonly']
|
|
table = [block for block in section.blocks if block[0] == fold.TABLE][0]
|
|
assert table[1][0] == 'setting'
|
|
assert table[1][-1] == 'key'
|
|
for row in table[2]:
|
|
assert row[0] != row[-1]
|
|
|
|
|
|
# ------------------------------------------------------- one setting up close
|
|
|
|
# --------------------------------------------------------- the render itself
|
|
|
|
def test_the_whole_page_renders_as_text_with_nothing_folded():
|
|
body = fold.text(rendered(), show_all=True)
|
|
assert 'Configuration of sample_job' in body
|
|
for section in rendered()[1:]:
|
|
assert section.title.replace('—', '--') in body
|
|
|
|
|
|
def test_the_folded_half_renders_as_one_line_of_html():
|
|
"""`print_html` is a `print(..., end='')` into the panel's stream and every
|
|
newline in it starts a new output entry, which would cut a div in half."""
|
|
body = fold.html(rendered()[1:])
|
|
assert body
|
|
assert '\n' not in body
|
|
|
|
|
|
def test_a_pipe_in_a_typed_reason_cannot_split_a_table_row():
|
|
sections = by_key(rendered(muted={'3.3': 'a | b'}))
|
|
rows = [block for block in sections['muted'].blocks
|
|
if block[0] == fold.TABLE][0][2]
|
|
assert '|' not in rows[0][1]
|
|
assert '|' in rows[0][1]
|
|
|
|
|
|
def test_the_page_names_the_file_it_was_also_written_to():
|
|
body = fold.text(rendered(full_path='C:/job/config/last-page.txt'))
|
|
assert 'last-page.txt' in body
|
|
|
|
|
|
def test_every_section_has_a_summary_so_folded_is_not_gone():
|
|
for section in rendered()[1:]:
|
|
assert section.summary or section.problem, section.key
|
|
|
|
|
|
# ------------------------------------------- the muted section, and its seam
|
|
|
|
def _mute_rows(*rows):
|
|
"""`configform.mute_table` output, spelled out rather than imported.
|
|
|
|
The page takes these as DATA and never builds them: they need the ticket
|
|
checklist table, which `configpage` has no business loading. Writing the
|
|
shape here is what keeps that contract visible.
|
|
"""
|
|
out = []
|
|
for number, name, reason, layer, editable in rows:
|
|
out.append({'number': number, 'name': name, 'grade': 'A',
|
|
'known': not name.startswith('('), 'muted': True,
|
|
'reason': reason, 'layer': layer, 'editable': editable,
|
|
'why_not': None if editable else 'set by ' + layer,
|
|
'falls_back': None})
|
|
return out
|
|
|
|
|
|
def test_the_muted_section_says_where_the_control_is():
|
|
"""The defect this is for: he went looking for the mute control twice and
|
|
did not find it, and this page -- the one surface that lists what is
|
|
muted -- said nothing at all about where muting is done."""
|
|
section = by_key(rendered(muted={'3.3': 'no piece has one'}))['muted']
|
|
body = '\n'.join(block[1] for block in section.blocks
|
|
if block[0] == fold.MD)
|
|
assert 'Ticket QC' in body
|
|
assert 'speaker' in body
|
|
|
|
|
|
def test_the_muted_section_says_the_check_still_runs():
|
|
"""Muting is a volume control, not a switch. A page that showed a mute
|
|
without saying that would leave the reader believing a check was off."""
|
|
section = by_key(rendered(muted={'3.3': 'no piece has one'}))['muted']
|
|
body = '\n'.join(block[1] for block in section.blocks
|
|
if block[0] == fold.MD)
|
|
assert 'does not stop a check' in body
|
|
assert 'never quietened' in body.lower()
|
|
assert 'still RUNS' in section.problem or 'still' in section.problem
|
|
|
|
|
|
def test_a_mute_this_window_cannot_remove_is_named_with_its_layer():
|
|
rows = _mute_rows(('3.3', 'release ratio', 'the plant never has one',
|
|
'plant', False))
|
|
section = by_key(rendered(muted={'3.3': 'the plant never has one'},
|
|
muted_rows=rows))['muted']
|
|
table = [block for block in section.blocks if block[0] == fold.TABLE][0]
|
|
assert table[1] == ['item', 'checklist row',
|
|
'why this job never has one', 'set by']
|
|
assert table[2][0][3] == 'plant'
|
|
body = '\n'.join(block[1] for block in section.blocks
|
|
if block[0] == fold.MD)
|
|
assert 'not the job layer' in body
|
|
assert 'MERGE' in body
|
|
|
|
|
|
def test_a_muted_number_that_is_no_checklist_item_opens_the_section():
|
|
"""It mutes nothing, and nothing is what it looks like it did."""
|
|
rows = _mute_rows(('9.9', '(not a checklist item -- this mutes nothing)',
|
|
'typed by hand', 'job', True))
|
|
section = by_key(rendered(muted={'9.9': 'typed by hand'},
|
|
muted_rows=rows))['muted']
|
|
assert section.is_open
|
|
assert 'mute NOTHING' in section.problem
|
|
|
|
|
|
def test_the_muted_section_still_renders_from_the_flat_map_alone():
|
|
"""Every older caller hands over `muted` and no rows. The section must
|
|
fall back rather than lose the list."""
|
|
section = by_key(rendered(muted={'3.3': 'no piece has one'}))['muted']
|
|
table = [block for block in section.blocks if block[0] == fold.TABLE][0]
|
|
assert table[1] == ['item', 'why this job never has one']
|
|
assert table[2][0][0] == '3.3'
|
|
|
|
|
|
# ------------------------------------------------ the chip on a merged map
|
|
|
|
def test_a_key_the_layers_merge_is_never_chipped_to_one_layer():
|
|
"""`resolve` names the last layer that set an entry as though it owned
|
|
all of them. `J` on a map the plant also writes is the same lie as a
|
|
silent override, one level down."""
|
|
given = layers(plant={'checks': {'muted': {'3.3': 'plant'}}},
|
|
job={'checks': {'muted': {'5.4': 'job'}}})
|
|
row = [r for r in configpage.editable_rows(given)
|
|
if r['key'] == 'checks.muted'][0]
|
|
assert row['chip'] == '+'
|
|
assert row['res']['layer'] == 'plant + job'
|
|
assert '3.3 (plant)' in row['value']
|
|
assert '5.4 (job)' in row['value']
|
|
|
|
|
|
def test_the_legend_explains_the_merged_chip():
|
|
"""A chip nobody can look up is a chip that reads as a bug."""
|
|
assert '`+`' in configpage.LEGEND
|
|
assert 'MERGE' in configpage.LEGEND
|
|
|
|
|
|
def test_a_merged_map_no_layer_sets_still_says_not_set():
|
|
given = layers(defaults={}, plant={}, job={})
|
|
row = [r for r in configpage.editable_rows(given)
|
|
if r['key'] == 'checks.muted'][0]
|
|
assert row['chip'] == '?'
|
|
assert row['value'] == configpage.NOT_SET
|