"""EST 테스트 앱 무관자료 삭제(원본 mnuNotLinkDelete·DelSort·MulDelete).""" 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-cleanup.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))); """ return json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8")) def test_unused_rows_removed_and_totals_unchanged(): result = run_js( """(() => { const calc = (p) => EstNative.calculate(EstModel.validate(p)); const p = calc(structuredClone(source)); p.tables.StuffcDat.push({ Code: 'MZZZ', Name: '안 쓰는 자재', Index: 999, Cost1: 1, ValueIndex: 0 }); p.tables.HeadMainDat.push({ Index: 900, Code: 'UZZZ', Name: '안 쓰는 일위', TitleCode: 'UZZZ' }, { Code: 'MZZZ', LinkIndex: 5, GN: 1 }); const before = calc(p), total = before.calculation.total, direct = before.calculation.direct.TC; const counts = Object.fromEntries(['StuffcDat','LaborcDat','HeadMainDat','CostMainDat','MachMainDat'].map((t) => [t, p.tables[t].length])); const removed = EstCleanup.clean(p, false); const after = calc(p); return { total: after.calculation.total === total, direct: after.calculation.direct.TC === direct, errors: after.calculation.errors.length, gone: !p.tables.StuffcDat.some((r) => r.Code === 'MZZZ') && !EstNative.groups(p.tables.HeadMainDat).has('UZZZ'), removedStuff: removed.StuffcDat >= 1, removedHead: removed.HeadMainDat >= 1, estimate: p.tables.EstMainDat.length === before.tables.EstMainDat.length }; })()""" ) assert result == { "total": True, "direct": True, "errors": 0, "gone": True, "removedStuff": True, "removedHead": True, "estimate": True, } def test_sorted_mode_follows_first_use_and_keeps_codes(): result = run_js( """(() => { const calc = (p) => EstNative.calculate(EstModel.validate(p)); const p = calc(structuredClone(source)); const order = EstCleanup.used(p); const total = p.calculation.total; const codes = new Set(p.tables.HeadTitleDat.map((r) => r.Code)); EstCleanup.clean(p, true); const after = calc(p); return { heads: [...EstNative.groups(p.tables.HeadMainDat).keys()].join() === order.get('HeadMainDat').join(), stuff: p.tables.StuffcDat.map((r) => r.Code).join() === order.get('StuffcDat').join(), codes: p.tables.HeadTitleDat.every((r) => codes.has(r.Code)), total: after.calculation.total === total, errors: after.calculation.errors.length }; })()""" ) assert result == {"heads": True, "stuff": True, "codes": True, "total": True, "errors": 0} def test_blank_rows_before_heads_and_quantity_sheets(): result = run_js( """(() => { const rows = [{ Index: 1, Code: 'A' }, { Name: 'x', GN: 1 }, {}, {}, { Index: 2, Code: 'B' }, {}]; const trimmed = EstCleanup.trimBeforeHeads(rows, 'HeadMainDat').map((r) => r.Code || r.Name || '-'); const p = { tables: { EstMainDat: [{ PkID: 7, Code: 'D1' }] }, quantitySheets: [ { Code: 'Q1', items: [{ TargetKey: 'id:7' }] }, { Code: 'Q2', items: [{ TargetCode: 'NONE' }] }, { Code: 'Q3', items: [] }] }; const gone = EstCleanup.cleanQuantity(p); return { trimmed, gone, left: p.quantitySheets.map((s) => s.Code) }; })()""" ) assert result == {"trimmed": ["A", "x", "B", "-"], "gone": 2, "left": ["Q1"]}