# -*- 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)