8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
510 lines
20 KiB
Python
510 lines
20 KiB
Python
# -*- 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
|