WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
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.
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for `tools/deckcheck.py`.
|
||||
|
||||
Loaded by path, like `test_knowledge_drift.py` and `test_snapshot_defer.py`:
|
||||
this is host CPython code sitting in a repository whose extension is
|
||||
IronPython 2.7, and `conftest.py` in this folder registers the bare `vibe`
|
||||
stub every one of these imports relies on.
|
||||
|
||||
The regression this exists to catch, spelled out as a test rather than left
|
||||
as a paragraph: incident 1 in `tools/deckcheck.py`'s own docstring --
|
||||
`test_regression_catalogue_value_changed_after_export_is_named_stale` builds
|
||||
a deck frozen against an OLD catalogue rating, changes the catalogue the way
|
||||
the 2026-09-01 P76 correction did, and asserts the STALE finding names the
|
||||
mark and the field rather than passing silently.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__))))
|
||||
TOOLS = os.path.join(ROOT, 'tools')
|
||||
if TOOLS not in sys.path:
|
||||
sys.path.insert(0, TOOLS)
|
||||
|
||||
from vibe import knowledge # noqa: E402 (registers the vibe stub)
|
||||
from vibe import plantpath # noqa: E402
|
||||
|
||||
deckcheck = pytest.importorskip('deckcheck')
|
||||
D = deckcheck
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ helpers
|
||||
|
||||
def _write_yaml(path, data):
|
||||
if not os.path.isdir(os.path.dirname(path)):
|
||||
os.makedirs(os.path.dirname(path))
|
||||
with io.open(path, 'w', encoding='utf-8') as fh:
|
||||
yaml.safe_dump(data, fh)
|
||||
|
||||
|
||||
def _write_json(path, data):
|
||||
if not os.path.isdir(os.path.dirname(path)):
|
||||
os.makedirs(os.path.dirname(path))
|
||||
with io.open(path, 'w', encoding='utf-8') as fh:
|
||||
json.dump(data, fh)
|
||||
|
||||
|
||||
def _lifter(mark='M1', family='TESTLIFT', **extra):
|
||||
row = {'mark': mark, 'family': family, 'x': 0.0, 'y': 0.0,
|
||||
'pinned': False, 'pinned_y': False, 'on_edge': False,
|
||||
'capacity_lb': None, 'shear_capacity_lb': None,
|
||||
'basis_fc_psi': None, 'min_edge_distance_in': None,
|
||||
'tension_fos': None, 'shear_fos': None}
|
||||
row.update(extra)
|
||||
return row
|
||||
|
||||
|
||||
def _deck(mark='WP1', lifters=None, stages=None):
|
||||
return {
|
||||
'schema': 'panellift/1',
|
||||
'panel': {'mark': mark, 'lifters': lifters or []},
|
||||
'stages': stages or [],
|
||||
'source': {},
|
||||
}
|
||||
|
||||
|
||||
class Repo(object):
|
||||
"""A fixture knowledge tree: catalogue + plant + job layers on disk,
|
||||
plus the job_root deckcheck should be told (or made to guess) about.
|
||||
|
||||
`plant_env` points WILLTOOLS_KNOWLEDGE at `<tmp>/plantroot/knowledge.yaml`
|
||||
(a FILE, exactly as the real machine this was built on has it set --
|
||||
`plantpath.plant_root()` reads that as "its directory is the root"), so
|
||||
`plantpath.plant_knowledge_path('testplant')` resolves to
|
||||
`<tmp>/plantroot/testplant/knowledge.yaml` with no other wiring needed.
|
||||
"""
|
||||
|
||||
def __init__(self, tmp_path, monkeypatch, plant='testplant'):
|
||||
self.tmp = tmp_path
|
||||
self.job_root = str(tmp_path / 'job')
|
||||
self.jobs_dir = os.path.join(self.job_root, 'sweep', 'jobs')
|
||||
os.makedirs(self.jobs_dir)
|
||||
self.catalogue_path = str(tmp_path / 'catalogue.yaml')
|
||||
self.plant_path = str(tmp_path / 'plantroot' / plant / 'knowledge.yaml')
|
||||
self.job_path = os.path.join(self.job_root, 'knowledge.yaml')
|
||||
self.plant = plant
|
||||
|
||||
monkeypatch.setenv('WILLTOOLS_KNOWLEDGE',
|
||||
str(tmp_path / 'plantroot' / 'knowledge.yaml'))
|
||||
monkeypatch.setattr(D.knowledge, 'catalogue_path',
|
||||
lambda: self.catalogue_path)
|
||||
|
||||
_write_yaml(self.catalogue_path, {'catalogue': {'inserts': {}}})
|
||||
_write_yaml(self.job_path, {'job': {'plant': plant}})
|
||||
_write_yaml(self.plant_path, {})
|
||||
|
||||
def set_catalogue(self, inserts):
|
||||
# catalogue.yaml wraps its data in a top-level `catalogue:` key --
|
||||
# `knowledge.get(know, 'catalogue.inserts', ...)` is what every
|
||||
# lookup reads, so the fixture file has to nest the same way the
|
||||
# real one does (see knowledge/catalogue.yaml itself, `catalogue:`
|
||||
# at top level, `inserts:` inside it).
|
||||
_write_yaml(self.catalogue_path, {'catalogue': {'inserts': inserts}})
|
||||
|
||||
def set_plant(self, data):
|
||||
_write_yaml(self.plant_path, data)
|
||||
|
||||
def set_job(self, data):
|
||||
data = dict(data)
|
||||
data['job'] = {'plant': self.plant}
|
||||
_write_yaml(self.job_path, data)
|
||||
|
||||
def deck_path(self, name):
|
||||
return os.path.join(self.jobs_dir, name)
|
||||
|
||||
def build(self):
|
||||
return D.build_knowledge(self.job_root)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo(tmp_path, monkeypatch):
|
||||
return Repo(tmp_path, monkeypatch)
|
||||
|
||||
|
||||
# --------------------------------------------------------- build_knowledge
|
||||
|
||||
def test_layers_merge_defaults_catalogue_plant_job_in_that_order(repo):
|
||||
repo.set_catalogue({'inserts': {}}) # catalogue: no override
|
||||
repo.set_plant({'handling': {'tension_limit_sqrt_fc': 6.0}})
|
||||
repo.set_job({'handling': {'tension_limit_sqrt_fc': 9.0}}) # job wins
|
||||
know, layers = repo.build()
|
||||
assert knowledge.get(know, 'handling.tension_limit_sqrt_fc') == 9.0
|
||||
assert layers.plant_id == 'testplant'
|
||||
assert layers.plant_status == 'merged'
|
||||
assert layers.job_status == 'merged'
|
||||
assert layers.catalogue_status == 'merged'
|
||||
|
||||
|
||||
def test_plant_layer_alone_is_read_when_job_states_none(repo):
|
||||
repo.set_plant({'handling': {'tension_limit_sqrt_fc': 6.0}})
|
||||
_write_yaml(repo.job_path, {}) # no job.plant at all
|
||||
know, layers = repo.build()
|
||||
assert layers.plant_id is None
|
||||
# Falls back to the MACHINE-WIDE file (<plant_root>/knowledge.yaml), not
|
||||
# the per-plant one this fixture wrote -- which was never created, so
|
||||
# this is the ordinary "no plant file yet" case and stays silent.
|
||||
assert layers.plant_status == 'absent'
|
||||
assert knowledge.get(know, 'handling.tension_limit_sqrt_fc') == 5.0 # DEFAULTS
|
||||
|
||||
|
||||
def test_named_plant_with_no_folder_is_missing_not_silently_skipped(repo):
|
||||
_write_yaml(repo.job_path, {'job': {'plant': 'nobody_configured_this'}})
|
||||
know, layers = repo.build()
|
||||
assert layers.plant_status == 'missing'
|
||||
assert layers.plant_id == 'nobody_configured_this'
|
||||
|
||||
|
||||
def test_unreadable_yaml_layer_is_recorded_and_skipped_whole(repo, tmp_path):
|
||||
bad = tmp_path / 'bad.yaml'
|
||||
bad.write_text('a: [1, 2\n') # malformed
|
||||
status = D._merge_layer({}, str(bad))
|
||||
assert status == 'unreadable'
|
||||
|
||||
|
||||
def test_absent_layer_is_silent(repo, tmp_path):
|
||||
status = D._merge_layer({}, str(tmp_path / 'does-not-exist.yaml'))
|
||||
assert status == 'absent'
|
||||
|
||||
|
||||
# ------------------------------------------------------------- _guess_job_root
|
||||
|
||||
def test_guess_job_root_finds_the_folder_holding_knowledge_yaml(repo):
|
||||
guessed = D._guess_job_root(repo.jobs_dir)
|
||||
assert os.path.normcase(guessed) == os.path.normcase(repo.job_root)
|
||||
|
||||
|
||||
# -------------------------------------------------------------- _num_eq / _verdict
|
||||
|
||||
def test_num_eq_treats_both_none_as_equal():
|
||||
assert D._num_eq(None, None) is True
|
||||
|
||||
|
||||
def test_num_eq_one_none_is_not_equal():
|
||||
assert D._num_eq(None, 5) is False
|
||||
assert D._num_eq(5, None) is False
|
||||
|
||||
|
||||
def test_verdict_both_none_is_current():
|
||||
assert D._verdict(None, None) == D.CURRENT
|
||||
|
||||
|
||||
def test_verdict_current_none_frozen_value_is_unjudgeable():
|
||||
assert D._verdict(None, 7690.0) == D.UNJUDGEABLE
|
||||
|
||||
|
||||
def test_verdict_frozen_none_current_value_is_stale():
|
||||
assert D._verdict(16000.0, None) == D.STALE
|
||||
|
||||
|
||||
def test_verdict_matching_numbers_is_current():
|
||||
assert D._verdict(16000.0, 16000.0) == D.CURRENT
|
||||
|
||||
|
||||
def test_verdict_mismatched_numbers_is_stale():
|
||||
assert D._verdict(16000.0, 7690.0) == D.STALE
|
||||
|
||||
|
||||
# ---------------------------------------------------- check_capacity: current
|
||||
|
||||
def test_capacity_current_for_a_plain_ungated_family(repo):
|
||||
repo.set_catalogue({'PLAIN01': {'working_load_lb': 8000,
|
||||
'shear_load_lb': 8000}})
|
||||
repo.set_plant({'lifters': {'catalogue_alias': {'PLAINLIFT': 'PLAIN01'}}})
|
||||
know, _ = repo.build()
|
||||
status, detail = D.check_capacity(know, 'PLAINLIFT', 'tension', 8000.0, [])
|
||||
assert status == D.CURRENT
|
||||
assert detail['current'] == 8000.0
|
||||
|
||||
|
||||
# ----------------------------------------------- check_capacity: the regression
|
||||
|
||||
def test_regression_catalogue_value_changed_after_export_is_named_stale(repo):
|
||||
"""Incident 1, reproduced: a deck was frozen against a published rating
|
||||
that the catalogue later corrected. The mismatch must be named --
|
||||
mark, field, frozen and current -- not merged away or missed.
|
||||
"""
|
||||
repo.set_catalogue({'P76ALIAS': {'working_load_lb': 7690}})
|
||||
repo.set_plant({'lifters': {'catalogue_alias': {'LIFTFAM_P76': 'P76ALIAS'}}})
|
||||
know_old, _ = repo.build()
|
||||
status_old, detail_old = D.check_capacity(know_old, 'LIFTFAM_P76',
|
||||
'tension', 7690.0, [])
|
||||
assert status_old == D.CURRENT # the deck agreed with knowledge AT EXPORT
|
||||
|
||||
# The correction: 7690 -> 16000, same shape as the real P76 fix.
|
||||
repo.set_catalogue({'P76ALIAS': {'working_load_lb': 16000}})
|
||||
know_new, _ = repo.build()
|
||||
status_new, detail_new = D.check_capacity(know_new, 'LIFTFAM_P76',
|
||||
'tension', 7690.0, [])
|
||||
assert status_new == D.STALE
|
||||
assert detail_new['frozen'] == 7690.0
|
||||
assert detail_new['current'] == 16000.0
|
||||
|
||||
# And end-to-end through check_deck(), which is what names the mark.
|
||||
deck_path = repo.deck_path('WP900.job.json')
|
||||
_write_json(deck_path, _deck('WP900', lifters=[
|
||||
_lifter('L1', 'LIFTFAM_P76', capacity_lb=7690.0)]))
|
||||
findings = D.check_deck(deck_path, know_new)
|
||||
hit = [f for f in findings if f['field'] == 'capacity_lb']
|
||||
assert len(hit) == 1
|
||||
assert hit[0]['status'] == D.STALE
|
||||
assert hit[0]['mark'] == 'WP900'
|
||||
assert hit[0]['detail']['frozen'] == 7690.0
|
||||
assert hit[0]['detail']['current'] == 16000.0
|
||||
|
||||
|
||||
# --------------------------------------------------------- check_capacity: gated
|
||||
|
||||
def test_capacity_conditional_when_tension_bar_gate_unresolvable_offline(repo):
|
||||
repo.set_catalogue({'GATED01': {
|
||||
'working_load_lb': 16000, 'shear_load_lb': 4010,
|
||||
'requires_tension_bar_for_rating': True,
|
||||
'tension_without_tension_bar_lb': 7690,
|
||||
}})
|
||||
repo.set_plant({'lifters': {
|
||||
'catalogue_alias': {'GATEDFAM_P76': 'GATED01'},
|
||||
'tension_bar_families': ['SR5602'],
|
||||
'parts_shipped_with_tension_bar': ['P76'],
|
||||
}})
|
||||
know, _ = repo.build()
|
||||
bar_families = knowledge.get(know, 'lifters.tension_bar_families', [])
|
||||
|
||||
# frozen matches the WITHOUT-bar figure -- the case that must NOT read
|
||||
# as a quiet 'current' match, because the true answer depends on
|
||||
# assembly context this call was never given.
|
||||
status, detail = D.check_capacity(know, 'GATEDFAM_P76', 'tension',
|
||||
7690.0, bar_families)
|
||||
assert status == D.CONDITIONAL
|
||||
assert detail['without_bar'] == 7690.0
|
||||
assert detail['with_bar'] == 16000.0
|
||||
assert detail['frozen_matches'] == ['without_bar']
|
||||
|
||||
# frozen matches the WITH-bar figure -- still conditional, never
|
||||
# silently 'current' either, and frozen_matches names the other side.
|
||||
status2, detail2 = D.check_capacity(know, 'GATEDFAM_P76', 'tension',
|
||||
16000.0, bar_families)
|
||||
assert status2 == D.CONDITIONAL
|
||||
assert detail2['frozen_matches'] == ['with_bar']
|
||||
|
||||
|
||||
def test_capacity_shear_is_never_gated(repo):
|
||||
# The gate in `knowledge.lifter_capacity` only applies to kind='tension'
|
||||
# -- shear must resolve deterministically even for a gated family.
|
||||
repo.set_catalogue({'GATED01': {
|
||||
'working_load_lb': 16000, 'shear_load_lb': 4010,
|
||||
'requires_tension_bar_for_rating': True,
|
||||
'tension_without_tension_bar_lb': 7690,
|
||||
}})
|
||||
repo.set_plant({'lifters': {
|
||||
'catalogue_alias': {'GATEDFAM_P76': 'GATED01'},
|
||||
'tension_bar_families': ['SR5602'],
|
||||
'parts_shipped_with_tension_bar': ['P76'],
|
||||
}})
|
||||
know, _ = repo.build()
|
||||
status, detail = D.check_capacity(know, 'GATEDFAM_P76', 'shear', 4010.0, [])
|
||||
assert status == D.CURRENT
|
||||
assert detail['current'] == 4010.0
|
||||
|
||||
|
||||
def test_capacity_unjudgeable_when_no_catalogue_alias(repo):
|
||||
know, _ = repo.build()
|
||||
status, detail = D.check_capacity(know, 'NOBODY_KNOWS_THIS', 'tension',
|
||||
7690.0, [])
|
||||
assert status == D.UNJUDGEABLE
|
||||
assert 'reason' not in detail or detail.get('current') is None
|
||||
|
||||
|
||||
# ------------------------------------------------------------- stage checks
|
||||
|
||||
def test_stage_tension_limit_current_when_job_wide_matches(repo):
|
||||
repo.set_plant({'handling': {'tension_limit_sqrt_fc': 7.5}})
|
||||
know, _ = repo.build()
|
||||
stage = {'name': 'stripping', 'pci_case': 'stripping',
|
||||
'tension_limit_sqrt_fc': 7.5, 'tension_limit_source': 'job'}
|
||||
status, detail = D.check_stage_tension_limit(
|
||||
know, stage, knowledge.get(know, 'handling.tension_limit_sqrt_fc', 5.0))
|
||||
assert status == D.CURRENT
|
||||
|
||||
|
||||
def test_stage_tension_limit_stale_when_job_wide_moved(repo):
|
||||
repo.set_plant({'handling': {'tension_limit_sqrt_fc': 5.0}})
|
||||
know, _ = repo.build()
|
||||
stage = {'name': 'stripping', 'pci_case': 'stripping',
|
||||
'tension_limit_sqrt_fc': 7.5, 'tension_limit_source': 'job'}
|
||||
status, detail = D.check_stage_tension_limit(
|
||||
know, stage, knowledge.get(know, 'handling.tension_limit_sqrt_fc', 5.0))
|
||||
assert status == D.STALE
|
||||
assert detail['frozen'] == 7.5
|
||||
assert detail['current'] == 5.0
|
||||
|
||||
|
||||
def test_stage_tension_limit_unjudgeable_when_no_current_spec_matches(repo):
|
||||
repo.set_plant({'handling': {'stages': []}}) # the stage was removed
|
||||
know, _ = repo.build()
|
||||
stage = {'name': 'transportation', 'pci_case': 'transportation',
|
||||
'tension_limit_sqrt_fc': 9.0, 'tension_limit_source': 'stage'}
|
||||
status, detail = D.check_stage_tension_limit(know, stage, 5.0)
|
||||
assert status == D.UNJUDGEABLE
|
||||
|
||||
|
||||
# --------------------------------------------------------------- check_deck
|
||||
|
||||
def test_check_deck_skips_fields_the_deck_never_recorded(repo):
|
||||
repo.set_catalogue({'PLAIN01': {'working_load_lb': 8000}})
|
||||
repo.set_plant({'lifters': {'catalogue_alias': {'PLAINLIFT': 'PLAIN01'}}})
|
||||
know, _ = repo.build()
|
||||
# A minimal, panellift/1-shaped lifter with only capacity_lb recorded --
|
||||
# no basis_fc_psi, no fos keys. Those fields must not appear as findings
|
||||
# at all: absent is not the same as a mismatch.
|
||||
deck_path = repo.deck_path('WP1.job.json')
|
||||
_write_json(deck_path, _deck('WP1', lifters=[
|
||||
{'mark': 'L1', 'family': 'PLAINLIFT', 'capacity_lb': 8000.0}]))
|
||||
findings = D.check_deck(deck_path, know)
|
||||
fields = set(f['field'] for f in findings)
|
||||
assert fields == {'capacity_lb'}
|
||||
|
||||
|
||||
def test_check_deck_lifter_with_no_family_is_unjudgeable(repo):
|
||||
know, _ = repo.build()
|
||||
deck_path = repo.deck_path('WP2.job.json')
|
||||
_write_json(deck_path, _deck('WP2', lifters=[
|
||||
{'mark': 'L1', 'family': None, 'capacity_lb': 100.0}]))
|
||||
findings = D.check_deck(deck_path, know)
|
||||
assert len(findings) == 1
|
||||
assert findings[0]['status'] == D.UNJUDGEABLE
|
||||
assert findings[0]['field'] == 'family'
|
||||
|
||||
|
||||
def test_deck_status_is_the_worst_finding():
|
||||
findings = [
|
||||
{'status': D.CURRENT}, {'status': D.UNJUDGEABLE},
|
||||
{'status': D.CONDITIONAL},
|
||||
]
|
||||
assert D.deck_status(findings) == D.CONDITIONAL
|
||||
findings.append({'status': D.STALE})
|
||||
assert D.deck_status(findings) == D.STALE
|
||||
assert D.deck_status([]) == D.CURRENT
|
||||
|
||||
|
||||
# -------------------------------------------------------------- _deck_lines
|
||||
|
||||
def test_deck_lines_group_identical_findings_across_lifters(repo):
|
||||
findings = [
|
||||
{'deck': 'x', 'mark': 'WP1', 'lifter_mark': 'L1', 'family': 'F',
|
||||
'field': 'capacity_lb', 'status': D.STALE,
|
||||
'detail': {'frozen': 7690.0, 'current': 16000.0}},
|
||||
{'deck': 'x', 'mark': 'WP1', 'lifter_mark': 'L2', 'family': 'F',
|
||||
'field': 'capacity_lb', 'status': D.STALE,
|
||||
'detail': {'frozen': 7690.0, 'current': 16000.0}},
|
||||
]
|
||||
lines = D._deck_lines('WP1', findings)
|
||||
assert len(lines) == 1
|
||||
assert '2 lifters' in lines[0]
|
||||
assert 'WP1' in lines[0]
|
||||
|
||||
|
||||
def test_deck_lines_keep_genuinely_different_values_separate():
|
||||
findings = [
|
||||
{'deck': 'x', 'mark': 'WP1', 'lifter_mark': 'L1', 'family': 'F',
|
||||
'field': 'capacity_lb', 'status': D.STALE,
|
||||
'detail': {'frozen': 7690.0, 'current': 16000.0}},
|
||||
{'deck': 'x', 'mark': 'WP1', 'lifter_mark': 'L2', 'family': 'F',
|
||||
'field': 'capacity_lb', 'status': D.STALE,
|
||||
'detail': {'frozen': 8000.0, 'current': 9000.0}},
|
||||
]
|
||||
lines = D._deck_lines('WP1', findings)
|
||||
assert len(lines) == 2
|
||||
|
||||
|
||||
def test_deck_lines_omit_current_findings():
|
||||
findings = [{'deck': 'x', 'mark': 'WP1', 'lifter_mark': 'L1',
|
||||
'family': 'F', 'field': 'capacity_lb', 'status': D.CURRENT,
|
||||
'detail': {'current': 1.0, 'frozen': 1.0}}]
|
||||
assert D._deck_lines('WP1', findings) == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- CLI
|
||||
|
||||
def test_main_exits_1_when_a_deck_is_stale(repo, capsys):
|
||||
repo.set_catalogue({'P76ALIAS': {'working_load_lb': 16000}})
|
||||
repo.set_plant({'lifters': {'catalogue_alias': {'LIFTFAM_P76': 'P76ALIAS'}}})
|
||||
_write_json(repo.deck_path('WP1.job.json'), _deck('WP1', lifters=[
|
||||
_lifter('L1', 'LIFTFAM_P76', capacity_lb=7690.0)]))
|
||||
rc = D.main([repo.jobs_dir])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 1
|
||||
assert 'STALE' in out
|
||||
assert 'WP1' in out
|
||||
assert 'capacity_lb' in out
|
||||
|
||||
|
||||
def test_main_exits_0_when_everything_is_current(repo, capsys):
|
||||
repo.set_catalogue({'PLAIN01': {'working_load_lb': 8000}})
|
||||
repo.set_plant({'lifters': {'catalogue_alias': {'PLAINLIFT': 'PLAIN01'}}})
|
||||
_write_json(repo.deck_path('WP1.job.json'), _deck('WP1', lifters=[
|
||||
_lifter('L1', 'PLAINLIFT', capacity_lb=8000.0)]))
|
||||
rc = D.main([repo.jobs_dir])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 0
|
||||
assert '1 deck(s) checked, 1 current, 0 stale, 0 conditional, 0 unjudgeable' in out
|
||||
|
||||
|
||||
def test_main_exits_1_on_conditional_only(repo, capsys):
|
||||
repo.set_catalogue({'GATED01': {
|
||||
'working_load_lb': 16000, 'shear_load_lb': 4010,
|
||||
'requires_tension_bar_for_rating': True,
|
||||
'tension_without_tension_bar_lb': 7690,
|
||||
}})
|
||||
repo.set_plant({'lifters': {
|
||||
'catalogue_alias': {'GATEDFAM_P76': 'GATED01'},
|
||||
'tension_bar_families': ['SR5602'],
|
||||
'parts_shipped_with_tension_bar': ['P76'],
|
||||
}})
|
||||
_write_json(repo.deck_path('WP1.job.json'), _deck('WP1', lifters=[
|
||||
_lifter('L1', 'GATEDFAM_P76', capacity_lb=7690.0,
|
||||
shear_capacity_lb=4010.0)]))
|
||||
rc = D.main([repo.jobs_dir])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 1
|
||||
assert 'CONDITIONAL' in out
|
||||
|
||||
|
||||
def test_main_exits_2_when_jobs_dir_missing(tmp_path):
|
||||
rc = D.main([str(tmp_path / 'nope')])
|
||||
assert rc == 2
|
||||
|
||||
|
||||
def test_main_exits_2_when_jobs_dir_has_no_decks(repo):
|
||||
rc = D.main([repo.jobs_dir])
|
||||
assert rc == 2
|
||||
|
||||
|
||||
def test_main_prints_the_model_caveat_once(repo, capsys):
|
||||
repo.set_catalogue({'PLAIN01': {'working_load_lb': 8000}})
|
||||
repo.set_plant({'lifters': {'catalogue_alias': {'PLAINLIFT': 'PLAIN01'}}})
|
||||
_write_json(repo.deck_path('WP1.job.json'), _deck('WP1', lifters=[
|
||||
_lifter('L1', 'PLAINLIFT', capacity_lb=8000.0)]))
|
||||
D.main([repo.jobs_dir])
|
||||
out = capsys.readouterr().out
|
||||
assert out.count('NOT against the live') == 1 or 'MODEL' in out
|
||||
|
||||
|
||||
def test_main_writes_json_output(repo, tmp_path):
|
||||
repo.set_catalogue({'PLAIN01': {'working_load_lb': 8000}})
|
||||
repo.set_plant({'lifters': {'catalogue_alias': {'PLAINLIFT': 'PLAIN01'}}})
|
||||
_write_json(repo.deck_path('WP1.job.json'), _deck('WP1', lifters=[
|
||||
_lifter('L1', 'PLAINLIFT', capacity_lb=8000.0)]))
|
||||
out_path = str(tmp_path / 'out.json')
|
||||
D.main([repo.jobs_dir, '--json', out_path])
|
||||
with io.open(out_path, encoding='utf-8') as fh:
|
||||
payload = json.load(fh)
|
||||
assert payload['decks_checked'] == 1
|
||||
assert payload['current'] == 1
|
||||
Reference in New Issue
Block a user