"""EST 테스트 앱 낙찰률 적용 범위(원본 TFRMBIMOGBIUL 전체·선택·공종별·적용된 줄 제외).""" import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] FILES = [ "native-math.js", "native-formula.js", "native-machine.js", "native-rows.js", "native-text.js", "native-quantity.cjs", "native-wonga.js", "native-engine.js", "native-row-match.js", "native-escalation.js", "native-progress.js", "native-business.js", "native-review.js", "native-resources.js", "native-schedule.js", "native-model.js", "native-verify.js", "native-bid.js", ] def run_js(code: str): runner = f""" const fs = require('fs'), vm = require('vm'); const ctx = vm.createContext({{ structuredClone, console }}); for (const f of {json.dumps(FILES)}) vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }}); ctx.source = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8')); process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx))); """ out = subprocess.check_output(["node", "-e", runner], cwd=ROOT) return json.loads(out.decode("utf-8")) SETUP = """ const p = EstNative.calculate(EstModel.validate(structuredClone(source))); const rows = p.tables.EstMainDat; const heading = rows.findIndex((r) => Number(r.Level) > 0 && EstBid.subtree(rows, rows.indexOf(r)).length >= 2); const kids = EstBid.subtree(rows, heading); const outside = rows.findIndex((r, i) => i > kids.at(-1) && !Number(r.Level) && Number(r.NU) > 0 && Number(r.LinkIndex) !== 54); const unit = (q, i) => Number(q.tables.EstMainDat[i].NU); """ def test_group_scope_changes_only_rows_under_selected_heading(): result = run_js( "(() => {" + SETUP + """ const only = EstBid.scope(p, 'EstMainDat', new Set([heading]), 'groups', false); const q = EstBusiness.bid(p, EstBid.rates({method:'total', T:80}), only); const inside = kids.find((i) => unit(p, i) > 0); const base = Number(p.tables.EstMainDat[inside].ONU ?? unit(p, inside)); return { inside: Math.abs(unit(q, inside) - base * 0.8) < 1 && q.tables.EstMainDat[inside].ApplyNacChal === 1, outside: unit(q, outside) === unit(p, outside), size: only.EstMainDat.size === kids.length }; })()""" ) assert result == {"inside": True, "outside": True, "size": True} def test_selected_rows_skip_applied_and_errors(): result = run_js( "(() => {" + SETUP + """ const first = kids.find((i) => unit(p, i) > 0); const once = EstBusiness.bid(p, [90, 90, 90, 90], { EstMainDat: new Set([first]) }); const errors = []; const t = (fn) => { try { fn(); errors.push('ok'); } catch (e) { errors.push(e.message.slice(0, 6)); } }; t(() => EstBid.scope(once, 'EstMainDat', new Set([first]), 'rows', true)); t(() => EstBid.scope(once, 'EstMainDat', new Set(), 'rows', false)); t(() => EstBid.scope(once, 'EstMainDat', new Set([first]), 'groups', false)); t(() => EstBid.rates({method:'bimog', N:80, G:-1, J:1, S:1})); const kept = EstBid.scope(once, 'EstMainDat', new Set([first, outside]), 'rows', true); return { errors, kept: [...kept.EstMainDat], all: EstBid.scope(once, 'EstMainDat', new Set(), 'all', false) }; })()""" ) assert [e[:4] for e in result["errors"]] == ["적용할 ", "낙찰률을", "낙찰률을", "낙찰률은"] assert len(result["kept"]) == 1 assert result["all"] is None