WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
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.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Cross-checks `tools/pa_web/static/units.js` against its CPython twin,
|
||||
`panellift.units` -- the two are meant to be a byte-for-byte port of each
|
||||
other (see units.py's own module docstring), and this is the one place that
|
||||
proves it rather than trusting two hand-written files stayed in sync.
|
||||
|
||||
Node-free on purpose: this whole file SKIPS, never fails, when `node` is not
|
||||
on PATH. A missing dev tool on some machine is not a reason a CPython test
|
||||
run should go red, and the JS's own behaviour is otherwise unverified only
|
||||
by inspection -- an acceptable gap, not a gate anyone has to work around.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
TOOLS = os.path.abspath(os.path.join(HERE, os.pardir))
|
||||
REPO = os.path.abspath(os.path.join(TOOLS, os.pardir))
|
||||
UNITS_JS = os.path.join(TOOLS, 'pa_web', 'static', 'units.js')
|
||||
|
||||
PANELLIFT_SRC = os.path.join(REPO, 'panellift', 'src')
|
||||
if PANELLIFT_SRC not in sys.path:
|
||||
sys.path.insert(0, PANELLIFT_SRC)
|
||||
|
||||
from panellift import units # noqa: E402
|
||||
|
||||
NODE = shutil.which('node')
|
||||
|
||||
# (js function name, python function name, args, output kind)
|
||||
# output kind 'str' compares exactly; 'num' compares within a float epsilon
|
||||
# (JS and CPython float arithmetic can differ in the last bit).
|
||||
CASES = [
|
||||
('fmtFtIn', 'fmt_ft_in', [8.375], 'str'),
|
||||
('fmtFtIn', 'fmt_ft_in', [None], 'str'),
|
||||
('fmtFtIn', 'fmt_ft_in', [-0.5], 'str'),
|
||||
('fmtFtIn', 'fmt_ft_in', [0.53125], 'str'), # 0'-6 3/8"
|
||||
('fmtFtIn', 'fmt_ft_in', [37.583333333333336], 'str'), # carries to 37'-7"
|
||||
('fmtFtIn', 'fmt_ft_in', [0.0], 'str'),
|
||||
('fmtIn', 'fmt_in', [6.375], 'str'),
|
||||
('fmtIn', 'fmt_in', [None], 'str'),
|
||||
('fmtIn', 'fmt_in', [12.0], 'str'),
|
||||
('fmtIn', 'fmt_in', [0.0], 'str'),
|
||||
('parseLength', 'parse_length', ["8'-4 1/2\""], 'num'),
|
||||
('parseLength', 'parse_length', ["8'4\""], 'num'),
|
||||
('parseLength', 'parse_length', ["8' 4-1/2\""], 'num'),
|
||||
('parseLength', 'parse_length', ['4 1/2"'], 'num'),
|
||||
('parseLength', 'parse_length', ['100"'], 'num'),
|
||||
('parseLength', 'parse_length', ["-0'-6\""], 'num'),
|
||||
('parseLength', 'parse_length', ['5'], 'num'),
|
||||
('parseLength', 'parse_length', ['5', 'in'], 'num'),
|
||||
('parseLengthIn', 'parse_length_in', ['6 3/8"'], 'num'),
|
||||
('parseLengthIn', 'parse_length_in', ['5', 'ft'], 'num'),
|
||||
]
|
||||
|
||||
_HARNESS = """
|
||||
'use strict';
|
||||
var fs = require('fs');
|
||||
global.window = global;
|
||||
var src = fs.readFileSync(process.argv[2], 'utf8');
|
||||
eval(src);
|
||||
var Units = global.Units;
|
||||
var cases = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
|
||||
var out = cases.map(function (c) {
|
||||
try {
|
||||
return {ok: true, value: Units[c.fn].apply(null, c.args)};
|
||||
} catch (e) {
|
||||
return {ok: false, error: String(e)};
|
||||
}
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
"""
|
||||
|
||||
|
||||
def _run_js(tmp_path):
|
||||
harness = tmp_path / 'harness.js'
|
||||
harness.write_text(_HARNESS, encoding='utf-8')
|
||||
cases_path = tmp_path / 'cases.json'
|
||||
cases_path.write_text(
|
||||
json.dumps([{'fn': fn, 'args': args} for fn, _p, args, _k in CASES]),
|
||||
encoding='utf-8')
|
||||
proc = subprocess.run(
|
||||
[NODE, str(harness), UNITS_JS, str(cases_path)],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
assert proc.returncode == 0, (
|
||||
'node harness failed: stdout={0!r} stderr={1!r}'.format(
|
||||
proc.stdout, proc.stderr))
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason='node is not on PATH')
|
||||
def test_units_js_matches_units_py(tmp_path):
|
||||
assert os.path.isfile(UNITS_JS), UNITS_JS
|
||||
js_results = _run_js(tmp_path)
|
||||
assert len(js_results) == len(CASES)
|
||||
for (fn, py_name, args, kind), js in zip(CASES, js_results):
|
||||
py_fn = getattr(units, py_name)
|
||||
py_value = py_fn(*args)
|
||||
assert js['ok'], '{0}{1} threw in JS: {2}'.format(fn, args,
|
||||
js.get('error'))
|
||||
js_value = js['value']
|
||||
if kind == 'str':
|
||||
assert js_value == py_value, (fn, args, js_value, py_value)
|
||||
else:
|
||||
assert abs(js_value - py_value) < 1e-9, (
|
||||
fn, args, js_value, py_value)
|
||||
|
||||
|
||||
def test_units_js_file_exists_even_without_node():
|
||||
"""The one assertion that always runs: the file this whole module is
|
||||
about is actually there, node or not."""
|
||||
assert os.path.isfile(UNITS_JS)
|
||||
Reference in New Issue
Block a user