import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] CALC = ROOT / "tools" / "est-test" / "calc.js" def test_work_ids_do_not_require_secure_context_random_uuid(): script = """const fs = require('fs'), vm = require('vm'); const webcrypto = require('crypto').webcrypto; const context = {crypto: {getRandomValues: a => webcrypto.getRandomValues(a)}, document: {addEventListener() {}}}; vm.runInNewContext(fs.readFileSync(process.argv[1], 'utf8') + ';this.ids = [EstApp.uid(), EstApp.uid()]', context); console.log(JSON.stringify(context.ids));""" ids = json.loads( subprocess.check_output( ["node", "-e", script, str(ROOT / "tools" / "est-test" / "native-app.js")], text=True, encoding="utf-8", timeout=10, ) ) import uuid assert ids[0] != ids[1] assert all(str(uuid.UUID(value, version=4)) == value for value in ids) def calculate(data): script = 'const vm=require("vm"), fs=require("fs"); const ctx={}; vm.runInNewContext(fs.readFileSync(process.argv[1],"utf8")+";this.calc=EstCalc",ctx); console.log(JSON.stringify(ctx.calc.calculate(JSON.parse(process.argv[2]))));' return json.loads( subprocess.check_output( ["node", "-e", script, str(CALC), json.dumps(data)], text=True, encoding="utf-8" ) ) def test_est_test_calculation_and_linked_price_change(): data = { "sources": [{"id": "s", "kind": "재료비", "price": 10.129}], "units": [{"id": "u", "lines": [{"sourceId": "s", "quantity": 2, "digits": 2}]}], "estimate": [{"unitId": "u", "quantity": 3}], "costs": [{"base": "직접공사비", "rate": 10}, {"base": "누계", "rate": 5}], } result = calculate(data) assert result["units"]["u"]["lines"][0]["amount"] == 20.25 assert result["directTotal"] == 60 assert [row["amount"] for row in result["costs"]] == [6, 3] assert result["total"] == 69 data["sources"][0]["price"] = 20 assert calculate(data)["total"] == 138 def test_est_test_unlinked_rows_contribute_zero(): data = { "sources": [], "units": [{"id": "u", "lines": [{"sourceId": "", "quantity": 2}]}], "estimate": [{"unitId": "missing", "quantity": 4}], "costs": [], } result = calculate(data) assert result["units"]["u"]["total"] == result["total"] == 0 def test_source_selection_recursive_unit_and_percent(): data = { "sources": [ { "id": "s", "kind": "실적", "price": 900, "prices": [0, 125, 155, 0, 0, 0], "priceMode": "평균", } ], "units": [ {"id": "u1", "name": "기초", "lines": [{"sourceId": "s", "quantity": 2}]}, { "id": "u2", "name": "상위", "lines": [ {"type": "일위대가", "sourceId": "", "unitId": "u1", "quantity": 1}, { "type": "비율", "sourceId": "", "base": "실적", "kind": "경비", "quantity": 10, }, ], }, ], "estimate": [{"unitId": "u2", "quantity": 2}], "costs": [], } result = calculate(data) assert result["units"]["u1"]["parts"]["실적"] == 280 assert result["units"]["u2"]["parts"]["경비"] == 28 assert result["directTotal"] == 616 def test_sample_percentage_row_and_cycle_error(): data = { "sources": [{"id": "labor", "kind": "노무비", "price": 66841}], "units": [ { "id": "u", "name": "호표", "lines": [ {"sourceId": "labor", "quantity": 1}, { "type": "비율", "sourceId": "", "base": "노무비", "kind": "재료비", "quantity": 2, }, ], } ], "estimate": [], "costs": [], } result = calculate(data) assert result["units"]["u"]["parts"]["재료비"] == 1336 data["units"][0]["lines"].append( {"type": "일위대가", "sourceId": "", "unitId": "u", "quantity": 1} ) assert result["errors"] == [] assert calculate(data)["errors"] def test_progress_uses_approved_quantity_only_when_selected(): script = ( 'const vm=require("vm"),fs=require("fs");const c={};' 'vm.runInNewContext(fs.readFileSync(process.argv[1],"utf8")+";this.calc=EstCalc",c);' "console.log(JSON.stringify([c.calc.progress(100,2,3,1,false)," "c.calc.progress(100,2,3,1,true)]));" ) ordinary, approved = json.loads( subprocess.check_output(["node", "-e", script, str(CALC)], text=True, encoding="utf-8") ) assert ordinary["currentAmount"] == 300 assert ordinary["cumulativeAmount"] == 500 assert approved["cumulativeAmount"] == 300 def test_machine_components(): script = ( 'const vm=require("vm"),fs=require("fs");const c={};' 'vm.runInNewContext(fs.readFileSync(process.argv[1],"utf8")+";this.calc=EstCalc",c);' "console.log(JSON.stringify(c.calc.machine({purchase:1000000,rentalCoefficient:100," "fuelPrice:1000,fuelQuantity:2,suppliesRate:5,laborPrice:8000,laborFactor:0.125,laborHours:1})));" ) result = json.loads( subprocess.check_output(["node", "-e", script, str(CALC)], text=True, encoding="utf-8") ) assert result == {"재료비": 2100, "노무비": 1000, "경비": 10, "실적": 0} def test_formula_parser_rejects_invalid_expressions(): formula = ROOT / "tools" / "est-test" / "formula.js" script = ( 'const vm=require("vm"),fs=require("fs");const c={};' 'vm.runInNewContext(fs.readFileSync(process.argv[1],"utf8")+";this.f=EstFormula",c);' 'let failed=false;try{c.f.evaluate("1/0",{})}catch{failed=true};' 'console.log(JSON.stringify([c.f.evaluate("([금액]+2)*3",{"금액":8}),failed]));' ) assert json.loads( subprocess.check_output(["node", "-e", script, str(formula)], text=True, encoding="utf-8") ) == [30, True] def test_escalation_comparison_branches(): workflows = ROOT / "tools" / "est-test" / "workflows.js" script = ( 'const vm=require("vm"),fs=require("fs");const c={};' 'vm.runInNewContext(fs.readFileSync(process.argv[1],"utf8")+";this.w=EstWorkflows",c);' "console.log(JSON.stringify([c.w.escalation(100,80,120,2)," "c.w.escalation(100,110,125,2),c.w.escalation(100,80,90,1)," "c.w.excluded(3,5,4)]));" ) a, b, lower, excluded = json.loads( subprocess.check_output(["node", "-e", script, str(workflows)], text=True, encoding="utf-8") ) assert (a["rate"], a["difference"], a["amount"]) == (20, 16, 192) assert (b["difference"], b["amount"]) == (15, 250) assert lower["difference"] == 0 assert excluded == 5 def test_resource_quantities_follow_nested_units(): workflows = ROOT / "tools" / "est-test" / "workflows.js" script = ( 'const vm=require("vm"),fs=require("fs");const c={};' 'vm.runInNewContext(fs.readFileSync(process.argv[1],"utf8")+";this.w=EstWorkflows",c);' "console.log(JSON.stringify(c.w.resources(JSON.parse(process.argv[2]))));" ) data = { "sources": [{"id": "s", "name": "자재"}], "units": [ {"id": "a", "lines": [{"sourceId": "s", "quantity": 2}]}, {"id": "b", "lines": [{"type": "일위대가", "unitId": "a", "quantity": 3}]}, ], "estimate": [{"unitId": "b", "quantity": 4}], } result = json.loads( subprocess.check_output( ["node", "-e", script, str(workflows), json.dumps(data)], text=True, encoding="utf-8" ) ) assert result["rows"][0]["quantity"] == 24 assert result["errors"] == []