"""REA 저장 샘플과 웹앱 엔진의 대조. 원본 프로그램 실행 시험이 아님.""" import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] HELPER = Path(__file__).with_name("helper_est_native.cjs") EXPECTED = json.loads( (ROOT / "resources/tester/fixtures/est_rea_expected.json").read_text(encoding="utf-8") ) SAMPLE = json.loads((ROOT / "tools/est-test/sample.json").read_text(encoding="utf-8")) def run(command, *args): return json.loads( subprocess.check_output(["node", str(HELPER), command, *args], text=True, encoding="utf-8") ) def test_machine_stored_components_heads_and_titles(): result = run("machine") assert result["errors"] == [] count = 0 for expected in EXPECTED["machine"]["rows"]: if "stored" not in expected: continue row = result["rows"][expected["slot"]] if isinstance(expected["stored"], dict): for key, value in expected["stored"].items(): assert abs((row.get(key) or 0) - (value or 0)) < 1e-6, ( expected["slot"], key, row.get(key), value, ) else: assert abs(row[expected["field"]] - expected["stored"]) < 1e-6, expected count += 1 by_code = {row["Code"]: row for row in result["titles"]} for expected in SAMPLE["tables"]["MachTitleDat"]: actual = by_code[expected["Code"]] for key in ["NC", "GC", "JC", "TC"]: assert actual[key] == expected[key] count += 1 assert count == 48 def test_numeric_helpers_distinguish_rounding_modes(): assert run( "math", "[EstMath.even(2.5),EstMath.even(3.5),EstMath.floating(-1.25,1,1),EstMath.fixed(1.23999,2)]", ) == [2, 4, -1.3, 1.24] def test_large_currency_boundary_and_unit_conversion_caption(): assert run("math", "EstMath.floating(172068*0.1*756,1,2)") == 13008340.8 result = run( "math", """EstRows.head([ {Index:1,Code:'H'}, {Index:0,GN:1,NU:14341}, {Index:0,GN:0.1,LinkIndex:68,Size:'소계 / 10단자'} ],{HeadDigits:0})""", ) assert result["rows"][0]["NC"] == 1434 assert result["rows"][2]["NU"] == 14341 assert result["rows"][2]["NC"] == 1434 def test_procurement_zero_place_keeps_integer_total(): row = run( "math", "EstRows.procurement({Index:3,LinkIndex:57,GN:0.54,Gangub:0,OCheck:1,SCheck:3},[{LinkIndex:5,GC:30029967,TC:30029967}])", ) assert row["JC"] == row["TC"] == 162161 def test_blob_reused_capacity_and_incomplete_payload_are_distinct(): result = run("blob-tail") assert result["value"] == "abc" assert "BLOB 총 길이 오류" in result["truncated"] def test_procurement_exclusion_only_changes_industrial_safety_base(): result = run( "math", """EstWonga.calculate([ {Gubun:'J',Name:'관급자재대',Cost:1000,NC:100,VSusic:'원금액:1,000, {조달수수료:100}'}, {Gubun:'S',Name:'산업안전보건관리비',VSusic:'J*0.1'}, {Gubun:'P',Name:'other',VSusic:'J*0.1'} ],{GangubExceptJodalRatio:true})""", ) assert result["errors"] == [] assert [row["Cost"] for row in result["rows"]] == [1000, 90, 100] def test_resource_factor_includes_trailing_multiplier(): row = run( "math", r"""EstText.calculate([ {Index:1,Code:'C',LineData:''}, {Index:0,LinkIndex:5,SCheck:-2,LineData:'\x0eM\x0e : \x03100\x03 * 2 = \x12200\x12 * 0.5 = \x12100\x12'} ],{},()=>({Code:'M',Name:'M',SourceKind:5,GC:100,TC:100})).rows[1]""", ) assert row["ResourceFactor"] == 1 assert row["TC"] == 100 def test_direct_material_scope_is_the_marked_section(): result = run( "math", """(() => { const rows=[{Level:2,Name:'순직접공사비'},{Level:0,LinkIndex:5,GC:100}, {Level:2,Name:'사급자재대*'},{Level:0,LinkIndex:5,GC:200},{LinkIndex:54}]; return [EstNative.materialBase(rows,{GCToWonga:false}),EstNative.materialBase(rows,{GCToWonga:true})]; })()""", ) assert result == [200, 300] def test_practical_fire_projects_keep_stored_values_and_report_bad_source_formula(): files = list((ROOT / "resources/knowledge/original/실무문서").rglob("*.estx")) assert len(files) >= 3 for file in files: output = subprocess.check_output( ["node", str(HELPER.with_name("helper_est_archive.cjs")), str(file)], text=True, encoding="utf-8", timeout=120, ) result = json.loads(output) assert result["differences"] == [], file.name # The original text has an unclosed parenthesis in this transport # calculation; preserve its stored price and show the source error. assert all( error.startswith("D0000000194:") and "괄호" in error for error in result["errors"] ), (file.name, result["errors"]) def test_head_41_stored_checks(): result = run("head") assert result["errors"] == [] rows = result["rows"] for expected in EXPECTED["head_est"]["head"]: actual = rows[expected["source_slot"]] for key, value in expected["stored"].items(): assert abs((actual.get(key) or 0) - value) < 1e-6, (expected, key, actual.get(key)) assert len(EXPECTED["head_est"]["head"]) == 41 def test_estimate_37_stored_checks(): result = run("estimate") assert result["errors"] == [] for expected in EXPECTED["head_est"]["est"]: row = result["rows"][expected["slot"]] for key, value in expected["stored"].items(): assert abs((row.get(key) or 0) - value) < 1e-6, ( expected["slot"], key, row.get(key), value, ) assert len(EXPECTED["head_est"]["est"]) == 37 def test_linedata_118_results_and_8_titles(): result = run("text") assert result["errors"] == [] assert len(result["checks"]) == 118 for item in result["checks"]: assert abs(item["stored"] - item["calculated"]) < 1e-6, item for expected in EXPECTED["linedata"]["rows"]: if expected["kind"] == "title": actual = result["rows"][expected["slot"]] for key, value in expected["stored"].items(): assert actual[key] == value def test_wonga_all_23_formulas_and_total_fix(): result = run("wonga") assert result["errors"] == [] for expected in EXPECTED["wonga"]["results"]: row = result["rows"][expected["row_position"]] assert row["Cost"] == float(expected["stored"]), ( row["Gubun"], row["Cost"], expected["stored"], ) assert len(EXPECTED["wonga"]["results"]) == 23 assert result["total"] == 79603000 assert [step["difference"] for step in result["adjustments"]] == [324, -32, 3, 0] def test_entire_refresh_pipeline_preserves_stored_outputs(): result = run("refresh") assert result["calculation"]["errors"] == [] for table in ["MachMainDat", "HeadMainDat", "EstMainDat"]: for index, (before, after) in enumerate( zip(SAMPLE["tables"][table], result["tables"][table]) ): for key in ["NC", "GC", "JC", "SC", "TC"]: assert abs((before.get(key) or 0) - (after.get(key) or 0)) < 1e-6, ( table, index, key, ) for table in ["CostTitleDat", "HeadTitleDat", "MachTitleDat"]: lookup = {row["Code"]: row for row in result["tables"][table]} for before in SAMPLE["tables"][table]: for key in ["NC", "GC", "JC", "SC", "TC"]: assert abs((before.get(key) or 0) - (lookup[before["Code"]].get(key) or 0)) < 1e-6 def test_recalculation_after_price_edit_is_idempotent(): first, second = run("repeat") assert first["calculation"]["errors"] == second["calculation"]["errors"] == [] assert first["tables"]["CostTitleDat"] == second["tables"]["CostTitleDat"] assert first["tables"]["EstMainDat"] == second["tables"]["EstMainDat"] def test_progress_accepted_rounds_annual_allocation_and_resources(): result = run("business") first, second = result["first"], result["second"] assert first["errors"] == second["errors"] == [] # c914d4 rounds ratio-derived quantities to the contract quantity's digits. assert first["rows"][0]["GumGN"] == 3 assert first["rows"][0]["TotalGN"] == 1.5 assert first["total"] == 330 assert second["rows"][0]["JunGN"] == 1.5 assert second["rows"][0]["GumGN"] == 2.5 assert second["rows"][0]["TotalGN"] == 4 assert second["rows"][0]["TotalTC"] == 800 assert first["rows"][-1]["GumTC"] == 660 assert first["rows"][-1]["SaTC"] == first["rows"][-1]["TotalTC"] == 330 assert second["rows"][-1]["JunTC"] == 330 assert second["rows"][-1]["TotalTC"] == 880 assert result["years"]["errors"] == [] assert [r["project"]["calculation"]["total"] for r in result["years"]["results"]] == [880, 1320] assert result["resources"]["errors"] == [] assert result["resources"]["rows"][0]["quantity"] == 20 escalation = result["escalation"] assert escalation["errors"] == [] assert escalation["rows"][0]["EscExcluded"] == 3 assert escalation["rows"][0]["EscApplied"] == 7 assert escalation["contractTotal"] == 2200 assert escalation["adjustedTotal"] == 2640 assert escalation["excludedTotal"] == 660 assert escalation["applied"] == 1540 # REA 10a8fa8: investigation amount 1,980 is the denominator, not contract 2,200. assert escalation["designTotal"] == 1980 assert escalation["rate"] == 22.22 assert escalation["adjustment"] == 342 assert [ next(row["TC"] for row in item["tables"]["EstMainDat"] if row.get("LinkIndex") == 54) for item in result["originals"] ] == [2120, 2520, 2520] assert result["bid"]["calculation"]["direct"]["TC"] == 1800 assert result["bidAgain"]["tables"]["EstMainDat"] == result["bid"]["tables"]["EstMainDat"] assert result["bidResources"]["errors"] == [] assert result["bidResources"]["rows"][0]["quantity"] == 18 assert result["bidResources"]["rows"][0]["amount"] == 1800 assert result["convertedResources"]["errors"] == [] assert result["convertedResources"]["rows"][0]["quantity"] == 10 assert result["convertedResources"]["rows"][0]["amount"] == 1000 assert result["equipmentResources"]["errors"] == [] assert result["equipmentResources"]["rows"][0]["quantity"] == 20 assert result["equipmentResources"]["rows"][0]["amount"] == 2000 def test_estx_import_reads_original_tables_options_and_templates(tmp_path): import zipfile bundle = next((ROOT / "resources/knowledge/original").glob("*/EST Plus 2/*/05_*/sample_bundle")) archive = tmp_path / "sample.estx" with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as output: for file in bundle.iterdir(): if file.is_file(): output.write(file, file.name) result = run("import", str(archive)) assert result["schema"] == "est-web-2" assert len(result["tables"]["EstMainDat"]) == len(SAMPLE["tables"]["EstMainDat"]) assert len(result["originalOptions"]) == 17 assert len(result["wongaTemplates"]) == 4 assert result["options"]["TotalFix"] is True assert result["options"]["GCToWonga"] is False assert result["options"]["CostValueTrunc"] is False assert result["options"]["MidCostDiget"] == 2 assert result["originalArchive"] def test_source_price_edit_changes_downstream_costs(): first, _ = run("repeat") original = run("refresh") assert first["calculation"]["direct"]["TC"] != original["calculation"]["direct"]["TC"] assert first["tables"]["Wonga"] != original["tables"]["Wonga"] def test_extended_original_rows_cover_conversion_fractions_and_fuel(): fixture = json.loads( (ROOT / "resources/tester/fixtures/est_native_extended.json").read_text(encoding="utf-8") ) result = run("extended") for name, table in [("head", "HeadMainDat"), ("machine", "MachMainDat")]: assert result[name]["errors"] == [] for index, (before, after) in enumerate( zip(fixture["tables"][table], result[name]["rows"]) ): for key in ["NC", "GC", "JC", "SC", "TC"]: assert abs((before.get(key) or 0) - (after.get(key) or 0)) < 1e-6, ( table, index, key, ) assert result["text"]["errors"] == [] for check in result["text"]["checks"]: assert abs(check["stored"] - check["calculated"]) < 1e-6, check def test_installed_main_archive_all_stored_values_when_available(): import os import pytest archive = Path( os.environ.get("EST_REA_MAIN", "C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") ) if not archive.is_file(): pytest.skip("Original EST main.estx is not installed; portable fixtures still run") output = subprocess.check_output( ["node", str(HELPER.with_name("helper_est_archive.cjs")), str(archive)], text=True, encoding="utf-8", timeout=120, ) result = json.loads(output) assert result["errors"] == [] assert result["differences"] == [] assert all(item["checked"] == item["matched"] for item in result["counts"].values()) assert sum(item["checked"] for item in result["counts"].values()) > 100000 def test_original_quantity_sheets_and_three_saved_formula_disagreements(): sheets = run("quantity") assert len(sheets) == 75 # Original editor formulas disagree with MulList saved numbers in three # places. Keep both visible instead of changing the formulas to replay them. expected = { ("N0000000083", "r000004"): (286, 0.35 * 1.3 * 220), ("N0000000086", "r000004"): (0, 1 * 1), ("N0000000106", "r000000"): (138.72, 1.25 * 2 * 68 * 1.02), } differences = set() checked = 0 for sheet in sheets: assert sheet["errors"] == [] for before, after in zip(sheet["original"], sheet["items"]): checked += 1 key = (sheet["Code"], before["Code"]) if key in expected: assert before["GN"] == expected[key][0] assert abs(after["GN"] - expected[key][1]) < 1e-6 differences.add(key) else: assert abs((before.get("GN") or 0) - (after.get("GN") or 0)) < 1e-6, key assert checked == 449 assert differences == set(expected) def test_legacy_project_preserves_running_cost_basis_and_final_total(): result = run( "math", """EstNative.calculate(EstModel.legacy({ 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}] })).calculation""", ) assert result["errors"] == [] assert result["direct"]["TC"] == 60 assert result["total"] == 69 def test_manual_calculation_server_preserves_stored_amounts(): result = run("manual") assert result["tables"]["EstMainDat"][1]["GN"] == SAMPLE["tables"]["EstMainDat"][1]["GN"] + 100 for table in ["EstMainDat", "HeadMainDat", "Wonga"]: for original, saved in zip(SAMPLE["tables"][table], result["tables"][table]): for key in ["TC", "NC", "GC", "JC", "SC", "Cost"]: assert saved.get(key) == original.get(key) def test_text_fraction_spacing_ascii_multiplication_and_tree_units(): result = run( "math", r"""EstText.calculate([ {Index:1,Code:'T'}, {LineData:' n = 6.55 Cms = 37.4 A = 1 N = 1'}, {LineData:' L = 50m 25kg/1회'}, {LineData:' ○ 거리 Z=17m, 7m마다 1인으로 하면'}, {LineData:' n × Cms 6.55 × 37.4'}, {LineData:' t1 = ────── = ─────── = \x1211.67\x12'}, {LineData:' 60×0.35 60×0.35'}, {LineData:' D=0.2xAxN+2 = \x122.2\x12'}, {LineData:' 길이 = L + Z = \x1267\x12'}, {LineData:' * 윤 주 : 0.94m (0.65×0.65×3.14÷4)÷2 = \x120.17\x12㎥'}, {LineData:' 노무비: \x03235204\x03 × 3인 / 125주 = \x125644.8\x12',SCheck:-1} ])""", ) assert result["errors"] == [] assert [row["TC"] for row in result["rows"] if "TC" in row][1:] == [ 11.67, 2.2, 67, 0.17, 5644.8, ] def test_text_material_only_subtotal_does_not_reuse_prior_labor(): result = run( "math", r"""EstText.calculate([ {Index:1,Code:'T'}, {LineData:' \x1a노무비: 100\x1a',SCheck:-1,Level:-1}, {LineData:' \x1a재료비: 20\x1a',SCheck:-2,Level:-1}, {LineData:' \x1a계:\x1a \x12120\x12',SCheck:-5,Level:-1}, {LineData:' 다음 재료 구획',Level:1}, {LineData:' \x1a재료비: 30\x1a',SCheck:-2,Level:-1}, {LineData:' \x1a계:\x1a \x1230\x12',SCheck:-5,Level:-1} ])""", ) assert result["errors"] == [] assert result["rows"][3]["TC"] == 120 assert result["rows"][6]["TC"] == 30 def test_original_text_layouts_and_selected_group_subtotals(): results = run("text-layout") assert len(results) == 4 checked = 0 for result in results: assert result["errors"] == [], result["code"] for check in result["checks"]: assert abs(check["stored"] - check["calculated"]) < 1e-6, (result["code"], check) checked += 1 assert checked > 100 def test_text_preserves_braced_signs_and_comments_inside_parentheses(): result = run( "math", r"""EstText.calculate([ {Index:1,Code:'T'}, {LineData:' hm={-}6.76m {?} H={-}7.5m {계획수심}'}, {LineData:' d = hm-H = \x120.74\x12'}, {LineData:' L = 4인÷(4.8개{?}×8hr) = \x120.1\x12'}, {LineData:' q = 1 × 2조/회 = \x122\x12 t=15분 〔접이안시간〕'}, {LineData:' 전력비 = 0.74hr × 52.1 = \x1238.5\x12 원/본',SCheck:-3} ])""", ) assert result["errors"] == [] assert result["rows"][2]["TC"] == 0.74 assert result["rows"][3]["TC"] == 0.1 assert result["rows"][4]["TC"] == 2 assert result["rows"][5]["TC"] == 38.5 def test_source_code_edit_updates_links_and_preserves_original_contract(): result = run( "math", """(()=>{ const p=EstNative.empty(); p.tables.StuffcDat=[{PkID:1,Code:'M',Name:'material',ValueIndex:4,InputTC:100},{PkID:2,Code:'OTHER',ValueIndex:4,InputTC:10}]; p.tables.HeadMainDat=[{PkID:1,Index:1,Code:'H',Name:'head'},{PkID:2,Index:0,Code:'M',TitleCode:'H',LinkIndex:5,GN:2}]; p.tables.HeadTitleDat=[{Code:'H',Name:'head'}]; p.tables.EstMainDat=[{PkID:1,Code:'H',LinkIndex:103,GN:10},{LinkIndex:54}]; let q=EstBusiness.baseline(EstNative.calculate(p)); const before=JSON.stringify(q.baseline); EstModel.editCode(q,'StuffcDat',0,'M2'); EstModel.editCode(q,'HeadMainDat',0,'H2'); q.tables.StuffcDat[0].InputTC=120; q=EstNative.calculate(q); const original=EstBusiness.original(q,2); let duplicate=false;try{EstModel.editCode(q,'StuffcDat',0,'OTHER')}catch{duplicate=true;} return {q,original,preserved:before===JSON.stringify(q.baseline),duplicate}; })()""", ) project = result["q"] assert project["calculation"]["errors"] == [] assert project["calculation"]["direct"]["TC"] == 2400 assert project["tables"]["HeadMainDat"][1]["Code"] == "M2" assert project["tables"]["HeadMainDat"][1]["TitleCode"] == "H2" assert project["tables"]["EstMainDat"][0]["Code"] == "H2" assert project["tables"]["HeadTitleDat"][0]["Code"] == "H2" assert result["original"]["tables"]["EstMainDat"][-1]["TC"] == 2400 assert result["preserved"] and result["duplicate"] def test_text_fraction_columns_preserve_numbered_variables_and_unmarked_result(): result = run( "math", r"""EstText.calculate([ {Index:1,Code:'T'}, {LineData:' L11=0.35 L21=0.35 V51=15 V52=15 N=2'}, {LineData:' L11 L21'}, {LineData:' t23=(──+──)×60 = \x122.8\x12'}, {LineData:' V51 V52'}, {LineData:' t23 + t23'}, {LineData:' Tc = ────────────'}, {LineData:' 1 · N'}, {LineData:' Q=Tc×2 = \x125.6\x12'}, {LineData:' to1 = 0.6 min/m2 to2 = 3 min/m2'}, {LineData:' T=2×30×3.5×to2/60=\x1210.5\x12'} ])""", ) assert result["errors"] == [] assert result["rows"][3]["TC"] == 2.8 assert result["rows"][8]["TC"] == 5.6 assert result["rows"][10]["TC"] == 10.5 def test_original_cost_truncation_and_intermediate_precision_options(): result = run( "math", r"""(()=>{ const rows=[{Index:1,Code:'T'}, {LineData:' 노무비: \x03100.19\x03 = \x12100.1\x12',SCheck:-1}]; const intermediate=[{Index:1,Code:'I'}, {LineData:' 노무비: \x0359020\x03 / 81.8 = \x12721.51\x12 ×0.5=\x12360.7\x12',SCheck:-1}]; return {normal:EstText.calculate(rows),whole:EstText.calculate(rows,{CostValueTrunc:true}), mid1:EstText.calculate(intermediate,{MidCostDiget:1}),mid2:EstText.calculate(intermediate,{MidCostDiget:2}), override:EstText.calculate([rows[0],{...rows[1],Precision:2}],{CostValueTrunc:true})}; })()""", ) assert all(item["errors"] == [] for item in result.values()) assert result["normal"]["rows"][1]["TC"] == 100.1 assert result["whole"]["rows"][1]["TC"] == 100 assert result["override"]["rows"][1]["TC"] == 100.19 assert result["mid1"]["checks"][-1]["calculated"] == 721.5 assert result["mid2"]["checks"][-1]["calculated"] == 721.51 def test_source_price_refresh_recalculates_text_subtotal_and_percentage_reference(): result = run( "math", r"""(()=>{ const p=EstNative.empty(); p.tables.StuffcDat=[{Code:'M',Name:'material',ValueIndex:4,InputTC:150}]; p.tables.CostMainDat=[{Index:1,Code:'C'}, {Level:1,LineData:' material section'}, {Index:0,Code:'M',LinkIndex:5,SCheck:-2,LineData:' \x0ematerial\x0e: \x03100\x03 ×2 = \x12200\x12'}, {Index:-20,LinkIndex:61,SCheck:-2,LineData:' material subtotal: \x0c200\x0c'}, {LinkIndex:58,SCheck:-3,OCheck:-2,LineData:' \x0c200\x0c ×5% = \x1210\x12'}, {Level:1,LineData:' total'}, {Level:-1,SCheck:-2,LineData:' \x1a재료비:200\x1a'}, {Level:-1,SCheck:-3,LineData:' \x1a경비:10\x1a'}, {Level:-1,SCheck:-5,LineData:' \x1a계:\x1a \x12210\x12'}]; p.tables.CostTitleDat=[{Code:'C'}]; return EstNative.calculate(p); })()""", ) assert result["calculation"]["errors"] == [] rows = result["tables"]["CostMainDat"] assert rows[2]["TC"] == 300 assert rows[3]["TC"] == 300 assert "\x0c300\x0c" in rows[3]["LineData"] assert rows[4]["TC"] == 15 assert rows[0]["TC"] == 315 def test_selected_text_group_uses_distinct_rows_with_equal_original_amounts(): result = run( "math", r"""EstText.calculate([ {Index:1,Code:'C'}, {Code:'M',LinkIndex:5,SCheck:-2,LineData:' \x0ematerial\x0e: \x03100\x03 = \x06A\x06\x12100\x12'}, {Code:'L',LinkIndex:6,SCheck:-1,LineData:' \x0elabor\x0e: \x03100\x03 = \x06A\x06\x12100\x12'}, {LinkIndex:62,SCheck:-3,LineData:' \x07A\x07\x1a100 + 100\x1a = \x12200\x12 ×10% = \x1220\x12'} ],{},row=>row.Code==='M'?{GC:150,TC:150}:{NC:200,TC:200})""", ) assert result["errors"] == [] assert result["rows"][3]["TC"] == 35 assert "150 + 200" in result["rows"][3]["LineData"] def test_text_bid_rate_row_preserves_native_component_walk_and_currency_precision(): result = run( "math", r"""EstText.calculate([ {Index:1,Code:'C'}, {Level:1,LineData:' bid section'}, {SCheck:-1,LineData:' 노무비: 100 = \x12100\x12'}, {SCheck:-1,LineData:' 노무비: 50 = \x1250\x12'}, {SCheck:-2,LineData:' 재료비: 20 = \x1220\x12'}, {SCheck:-2,LineData:' 재료비: 30 = \x1230\x12'}, {SCheck:-3,LineData:' 경비: 12.3456 = \x1212.3456\x12',Precision:4}, {LinkIndex:52,LineData:' 낙찰률 \x1590\x15 %'} ])""", ) assert result["errors"] == [] assert result["rows"][-1]["NC"] == result["rows"][0]["NC"] == 90 assert result["rows"][0]["GC"] == 45 assert result["rows"][0]["JC"] == 11.111 assert result["rows"][0]["TC"] == 146.111 def test_text_group_markers_are_editable_and_missing_group_is_reported(): result = run( "math", r"""(()=>{ const line='\x06A\x06\x12100\x12 + \x07A\x07'; return {shown:EstText.display(line),roundtrip:EstText.encode(EstText.display(line))===line, missing:EstText.calculate([{Index:1,Code:'C'}, {LinkIndex:62,SCheck:-3,LineData:'\x07A\x07\x1a100\x1a = \x12100\x12 ×10% = \x1210\x12'}])}; })()""", ) assert result["roundtrip"] assert "⟦구간지정⟧A⟦구간지정⟧" in result["shown"] assert "⟦구간합계⟧A⟦구간합계⟧" in result["shown"] assert len(result["missing"]["errors"]) == 1 assert "A" in result["missing"]["rows"][1]["Error"] def test_fixed_text_formula_clears_the_previous_row_error(): result = run( "math", r"""(()=>{ const bad=EstText.calculate([{Index:1,Code:'C'}, {LineData:' Q=(1+2 = \x123\x12'}]); bad.rows[1].LineData=' Q=1+2 = \x123\x12'; return {bad:bad.errors,good:EstText.calculate(bad.rows)}; })()""", ) assert len(result["bad"]) == 1 assert result["good"]["errors"] == [] assert "Error" not in result["good"]["rows"][1] def test_text_total_without_result_marker_evaluates_its_expression(): result = run( "math", r"""EstText.calculate([ {Index:1,Code:'C'}, {Level:-1,SCheck:-1,LineData:' \x1a노무비:10 + 10\x1a'}, {Level:-1,SCheck:-2,LineData:' \x1a재료비:\x1a'}, {Level:-1,SCheck:-5,LineData:' \x1a계:\x1a \x1220\x12'} ])""", ) assert result["errors"] == [] assert result["rows"][0]["TC"] == 20 assert result["rows"][1]["TC"] == 20 assert result["rows"][2]["TC"] == 0