Files
Aislo/resources/tester/test_est_native_export.py

405 lines
26 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import struct
import subprocess
import zipfile
import pytest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_existing_quantity_links_respect_native_mode_and_avoid_ambiguous_targets():
script = """const {existingTargets}=require('./tools/est-test/native-links.cjs');
const sheet={Code:'N1',items:[{Code:'r1'},{Code:'r2'},{Code:'r3'},{Code:'r4'}]};
const blob=(v,enc='utf8')=>({binary:Buffer.from(v,enc).toString('base64')});
const rows=[{Code:'E1',MullangIndex:1,MulCode:blob('N1\\tr1')},
{Code:'E2',MullangIndex:1,MulCode:blob('N1\\tr2','utf16le')},
{Code:'E3',MullangIndex:2,MulCode:blob('N1\\tr3')},
{Code:'E4',MullangIndex:1,MulCode:blob('N1\\tr4')},{Code:'E4'}];
const warnings=new Set();existingTargets({EstMainDat:rows},[sheet],warnings);
process.stdout.write(JSON.stringify({items:sheet.items,warnings:[...warnings]}));"""
result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT).decode("utf-8"))
assert [item.get("TargetCode") for item in result["items"]] == ["E1", "E2", None, None]
assert len(result["warnings"]) == 2
def test_estx_print_catalog_uses_project_options_when_sample_exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/sample.estx")
if not sample.exists():
pytest.skip("installed EST sample unavailable")
script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),p=require('./tools/est-test/import-print.cjs'),s=require('./tools/est-test/server.cjs');
const bytes=fs.readFileSync(process.argv[1]),project=m.importEstx(bytes),form=project.printCatalog.find(r=>r.Index===1);
let invalid=false;try{p.readPrintOptions(Buffer.from([1,2,3]))}catch{invalid=true}
process.stdout.write(JSON.stringify({count:project.printCatalog.length,name:form.Name,project:form.ProjectName,
lines:form.PrintLineCount,left:form.PrintLeft,preserved:s.calculate(project).printCatalog.length,invalid}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8")
)
assert result == {
"count": 41,
"name": "내역서",
"project": "연습공사",
"lines": 21,
"left": 20,
"preserved": 41,
"invalid": True,
}
def test_estx_print_edits_patch_only_requested_fields_when_sample_exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/sample.estx")
if not sample.exists():
pytest.skip("installed EST sample unavailable")
script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'),s=require('./tools/est-test/server.cjs');
const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),before=structuredClone(p.printCatalog);
p.nativePrintChanges={'1':{PrintTop:13,PrintBottom:14,PrintLeft:15,PrintRight:16,PrintLineCount:19}};
const retained=s.calculate(structuredClone(p)).nativePrintChanges['1'].PrintLineCount===19;
const out=e.exportEstx(p),q=m.importEstx(out),a=m.unzip(bytes),b=m.unzip(out),form=q.printCatalog.find(r=>r.Index===1);
const other=before.filter(r=>r.Index!==1).every(r=>JSON.stringify(r)===JSON.stringify(q.printCatalog.find(x=>x.Index===r.Index)));
const sameForm=Object.keys(form).filter(k=>!['PrintTop','PrintBottom','PrintLeft','PrintRight','PrintLineCount'].includes(k)).every(k=>JSON.stringify(form[k])===JSON.stringify(before.find(r=>r.Index===1)[k]));
const files=[...a].filter(([k])=>k!=='datprintoption.opt').every(([k,v])=>v.equals(b.get(k)));
p.nativePrintChanges={'1':{PrintLeft:1000}};let invalid=false;try{e.exportEstx(p)}catch{invalid=true}
process.stdout.write(JSON.stringify({top:form.PrintTop,bottom:form.PrintBottom,left:form.PrintLeft,right:form.PrintRight,lines:form.PrintLineCount,other,sameForm,files,invalid,retained}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8")
)
assert result == {
"top": 13,
"bottom": 14,
"left": 15,
"right": 16,
"lines": 19,
"other": True,
"sameForm": True,
"files": True,
"invalid": True,
"retained": True,
}
def test_quantity_document_edits_keep_styles_and_bookmarks_and_block_marker_edits():
script = r"""const q=require('./tools/est-test/native-quantity.cjs'),w=require('./tools/est-test/export-quantity.cjs');
const doc='<cs nr=1/><div><c nr=1/>폭= <begin Tag=2 wpsty=[[Name:&#34;r000001&#34;;]]/>6<end Tag=2/> m</div>';
const old=q.fromWpt('A','문서',doc,[],{}),edited=structuredClone(old);
edited.rows[0].LineData=edited.rows[0].LineData.replace('폭=','높이=').replace('6','12');
const next=w.editDocument(doc,old,edited),again=q.fromWpt('A','문서',next,[],{});
const bad=structuredClone(old);bad.rows[0].LineData='완전히 다른 줄';
let rejected=false;try{w.editDocument(doc,old,bad)}catch{rejected=true}
process.stdout.write(JSON.stringify({same:JSON.stringify(again)===JSON.stringify(edited),tags:next.includes('<begin Tag=2')&&next.includes('<end Tag=2'),rejected}));"""
result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT).decode("utf-8"))
assert result == {"same": True, "tags": True, "rejected": True}
def test_estx_quantity_item_labels_round_trip_when_sample_exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx")
if not sample.exists():
pytest.skip("installed EST sample unavailable")
script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs');
const p=m.importEstx(fs.readFileSync(process.argv[1])),before=structuredClone(p.quantitySheets),sheet=p.quantitySheets[0];
sheet.items[0].Name='수정된 품명';sheet.items[0].Size='수정 규격';sheet.items[0].Unite='m2';
const mismatch=p.quantitySheets.find(s=>s.Code==='N0000000083');mismatch.items[4].Name='수량 보존';
const q=m.importEstx(e.exportEstx(p)),item=q.quantitySheets[0].items[0];
const restored=q.quantitySheets.find(s=>s.Code==='N0000000083');
const other=JSON.stringify(q.quantitySheets[0].items.slice(1))===JSON.stringify(before[0].items.slice(1));
const rest=JSON.stringify(q.quantitySheets.filter(s=>s.Code!==sheet.Code && s.Code!==mismatch.Code))===JSON.stringify(before.filter(s=>s.Code!==sheet.Code && s.Code!==mismatch.Code));
const preserved=restored.items[4].Name==='수량 보존' && restored.items[4].GN===286 && JSON.stringify(restored.rows)===JSON.stringify(before.find(s=>s.Code===mismatch.Code).rows);
p.quantitySheets[0].items[0].TargetCode='new target';let rejected=false;try{e.exportEstx(p)}catch{rejected=true}
console.log(JSON.stringify({name:item.Name,size:item.Size,unite:item.Unite,other,rest,preserved,rejected}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=45).decode(
"utf-8"
)
)
assert result == {
"name": "수정된 품명",
"size": "수정 규격",
"unite": "m2",
"other": True,
"rest": True,
"preserved": True,
"rejected": True,
}
def test_estx_quantity_description_preserves_mismatched_result_when_sample_exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx")
if not sample.exists():
pytest.skip("installed EST sample unavailable")
script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'),w=require('./tools/est-test/export-quantity.cjs');
const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),base=m.importEstx(bytes),sheet=p.quantitySheets.find(s=>s.Code==='N0000000083');
sheet.rows[0].LineData=sheet.rows[0].LineData.replace('석축쌓기','석축공사');
const q=m.importEstx(e.exportEstx(p)),saved=q.quantitySheets.find(s=>s.Code===sheet.Code);
sheet.rows[3].LineData=sheet.rows[3].LineData.replace('220','221');let rejected=false;
try{w.updateQuantities(p,base,e.cp949,e.memory)}catch{rejected=true}
sheet.rows[3].LineData=sheet.rows[3].LineData.replace('221','220');
sheet.rows[5].LineData=sheet.rows[5].LineData.replace('286','287');let markerRejected=false;
try{w.updateQuantities(p,base,e.cp949,e.memory)}catch{markerRejected=true}
console.log(JSON.stringify({text:saved.rows[0].LineData,quantity:saved.items[4].GN,
preserved:JSON.stringify(saved.rows.slice(1))===JSON.stringify(base.quantitySheets.find(s=>s.Code===sheet.Code).rows.slice(1)),rejected,markerRejected}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=45).decode(
"utf-8"
)
)
assert result["text"].startswith("석축공사")
assert result["quantity"] == 286
assert result["preserved"] and result["rejected"] and result["markerRejected"]
def test_quantity_document_edit_across_character_styles_preserves_tags():
script = r"""const q=require('./tools/est-test/native-quantity.cjs'),w=require('./tools/est-test/export-quantity.cjs');
const doc='<cs nr=1/><cs nr=2/><div><c nr=1/>abc<c nr=2/>def<c nr=1/>ghi</div>';
const old=q.fromWpt('A','문서',doc,[],{}),edited=structuredClone(old);
edited.rows[0].LineData='ab변경hi';
const next=w.editDocument(doc,old,edited),again=q.fromWpt('A','문서',next,[],{}),tags=x=>x.match(/<[^>]*>/g);
process.stdout.write(JSON.stringify({same:JSON.stringify(again)===JSON.stringify(edited),tags:JSON.stringify(tags(doc))===JSON.stringify(tags(next))}));"""
result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT).decode("utf-8"))
assert result == {"same": True, "tags": True}
def test_quantity_document_edit_across_styles_and_superscript_preserves_structure():
script = r"""const q=require('./tools/est-test/native-quantity.cjs'),w=require('./tools/est-test/export-quantity.cjs');
const doc='<cs nr=1 CharStyleON:16/><cs nr=2/><div><c nr=2/>abc<c nr=1/>2<c nr=2/>def</div>';
const old=q.fromWpt('A','문서',doc,[],{}),edited=structuredClone(old);
edited.rows[0].LineData='abc^(3)xyz';
const next=w.editDocument(doc,old,edited),again=q.fromWpt('A','문서',next,[],{});
process.stdout.write(JSON.stringify({same:JSON.stringify(again.rows)===JSON.stringify(edited.rows),doc:next}));"""
result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT).decode("utf-8"))
assert result["same"] is True
assert "<c nr=1/>3<c nr=2/>xyz" in result["doc"]
def test_quantity_document_edit_across_styles_and_separate_marker():
script = r"""const q=require('./tools/est-test/native-quantity.cjs'),w=require('./tools/est-test/export-quantity.cjs');
const doc='<cs nr=1/><cs nr=2/><div><c nr=1/>abc<c nr=2/>def<begin Tag=2 wpsty=[[Name:&#34;r000001&#34;;]]/>6<end Tag=2/></div>';
const old=q.fromWpt('A','문서',doc,[],{}),edited=structuredClone(old);
edited.rows[0].LineData=edited.rows[0].LineData.replace('bcde','변경').replace('6','12');
const next=w.editDocument(doc,old,edited),again=q.fromWpt('A','문서',next,[],{}),tags=x=>x.match(/<[^>]*>/g);
process.stdout.write(JSON.stringify({same:JSON.stringify(again)===JSON.stringify(edited),tags:JSON.stringify(tags(doc))===JSON.stringify(tags(next))}));"""
result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT).decode("utf-8"))
assert result == {"same": True, "tags": True}
def test_quantity_document_edit_across_actual_style_boundary_when_sample_exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx")
if not sample.exists():
pytest.skip("installed EST main sample unavailable")
script = """const fs=require('fs'),z=require('zlib'),m=require('./tools/est-test/import-estx.cjs'),w=require('./tools/est-test/export-quantity.cjs'),q=require('./tools/est-test/native-quantity.cjs');
const p=m.importEstx(fs.readFileSync(process.argv[1])),old=p.quantitySheets.find(s=>s.Code==='N0000000113');
const r=p.tables.MulMainDat.find(x=>x.Code===old.Code),b=Buffer.from(r.Editor.binary,'base64');
const text=new TextDecoder('euc-kr').decode(b[0]===120?z.inflateSync(b):b),edited=structuredClone(old);
const i=edited.rows.findIndex(r=>r.LineData.includes('27.22 ='));if(i<0)throw Error('sample line missing');
edited.rows[i].LineData=edited.rows[i].LineData.replace('27.22 =','27.22XY');
const out=w.editDocument(text,old,edited),tags=x=>x.match(/<[^>]*>/g);
process.stdout.write(JSON.stringify({same:JSON.stringify(q.fromWpt(edited.Code,edited.Name,out,edited.items,edited.options))===JSON.stringify(edited),tags:JSON.stringify(tags(text))===JSON.stringify(tags(out))}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8")
)
assert result == {"same": True, "tags": True}
def test_multiple_quantity_text_segments_preserve_original_tags_when_sample_exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx")
if not sample.exists():
pytest.skip("installed EST main sample unavailable")
script = """const fs=require('fs'),z=require('zlib'),m=require('./tools/est-test/import-estx.cjs'),w=require('./tools/est-test/export-quantity.cjs'),q=require('./tools/est-test/native-quantity.cjs');
const p=m.importEstx(fs.readFileSync(process.argv[1])),old=p.quantitySheets[0],r=p.tables.MulMainDat.find(x=>x.Code===old.Code),b=Buffer.from(r.Editor.binary,'base64');
const text=new TextDecoder('euc-kr').decode(b[0]===120?z.inflateSync(b):b),next=structuredClone(old),i=next.rows.findIndex(r=>r.LineData.includes('B ×0.05'));
if(i<0)throw Error('sample line missing');next.rows[i].LineData=next.rows[i].LineData.replace('표 층','표층').replace('B ×0.05','B ×0.06');
const out=w.editDocument(text,old,next),tags=x=>x.match(/<[^>]*>/g);
process.stdout.write(JSON.stringify({same:JSON.stringify(q.fromWpt(next.Code,next.Name,out,next.items,next.options))===JSON.stringify(next),tags:JSON.stringify(tags(text))===JSON.stringify(tags(out))}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8")
)
assert result == {"same": True, "tags": True}
def test_estx_quantity_edit_preserves_other_documents_and_indexes_when_sample_exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx")
if not sample.exists():
pytest.skip("installed EST main sample unavailable")
script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs');
const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),first=p.quantitySheets[0];
first.items[0].Name='changed name';first.items[0].Size='changed size';
first.rows[0].LineData=first.rows[0].LineData.replace('표준','시험 표준');
const index=first.rows.findIndex(r=>r.LineData.includes('B = 9.00'));
if(index<0)throw Error('기준 폭 행 없음');
first.rows[index].LineData=first.rows[index].LineData.replace('9.00','10.00');
const out=e.exportEstx(p),q=m.importEstx(out),a=m.unzip(bytes),b=m.unzip(out);
const unchanged=[...a].filter(([k])=>k.endsWith('.idx')||k.startsWith('cad')).every(([k,v])=>v.equals(b.get(k)));
process.stdout.write(JSON.stringify({line:q.quantitySheets[0].rows[0].LineData,quantity:q.quantitySheets[0].items[0].GN,name:q.quantitySheets[0].items[0].Name,size:q.quantitySheets[0].items[0].Size,result:q.quantitySheets[0].rows.find(r=>r.markers.includes('r000000')).LineData,others:JSON.stringify(q.quantitySheets.slice(1))===JSON.stringify(p.quantitySheets.slice(1)),unchanged}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8")
)
assert "시험 표준" in result["line"]
assert result["quantity"] == 116
assert result["name"] == "changed name" and result["size"] == "changed size"
assert "\x12116\x12" in result["result"]
assert result["others"] and result["unchanged"]
def test_estx_options_and_expanded_blob_preserve_unrelated_data_when_sample_exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/sample.estx")
if not sample.exists():
pytest.skip("installed EST sample unavailable")
script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs');
const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),before=structuredClone(p);
p.name='길이가 달라진 새 공사명';p.options.AutoCalc=!p.options.AutoCalc;
p.options.CostValueTrunc=!p.options.CostValueTrunc;p.options.MidCostDiget=3;
const row=p.tables.EstMainDat.find(r=>Object.values(r).some(v=>v&&v.binary));
const field=Object.keys(row).find(k=>row[k]&&row[k].binary);
const content=Buffer.alloc(12000, 65);row[field]={binary:content.toString('base64')};
const out=e.exportEstx(p),q=m.importEstx(out),a=m.unzip(bytes),b=m.unzip(out);
const unchanged=[...a].filter(([k])=>k.endsWith('.idx')||k.startsWith('cad')).every(([k,v])=>v.equals(b.get(k)));
const other=p.tables.EstMainDat.every((r,i)=>Object.keys(r).every(k=>JSON.stringify(r[k])===JSON.stringify(q.tables.EstMainDat[i][k])));
const options=before.originalOptions.every(x=>{const y=q.originalOptions.find(z=>z.className===x.className);
return Object.entries(x.properties).every(([k,v])=>['ProjectName','CalcAuto','CostValueTrunc','MidCostDiget'].includes(k)||JSON.stringify(v)===JSON.stringify(y.properties[k]));});
process.stdout.write(JSON.stringify({unchanged,other,options,name:q.name,auto:q.options.AutoCalc,cost:q.options.CostValueTrunc,digits:q.options.MidCostDiget,expected:p.options,content:Buffer.from(q.tables.EstMainDat.find(r=>r.PkID===row.PkID)[field].binary,'base64').equals(content)}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8")
)
assert result["unchanged"] and result["other"] and result["options"] and result["content"]
assert result["name"] == "길이가 달라진 새 공사명"
assert result["auto"] == result["expected"]["AutoCalc"]
assert result["cost"] == result["expected"]["CostValueTrunc"]
assert result["digits"] == 3
def test_recalculation_and_native_export_keep_original_title_identities_when_sample_exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx")
if not sample.exists():
sample = Path("resources/knowledge/original/원가계산/다산소프트/2026-07-main.estx")
if not sample.exists():
sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/sample.estx")
if not sample.exists():
pytest.skip("installed EST sample unavailable")
script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'),s=require('./tools/est-test/server.cjs');
const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes);
const hasQuantity=Boolean(p.quantitySheets?.[0]?.items?.[0]);
if (hasQuantity) p.quantitySheets[0].items[0].Unite = 'm2';
const q=s.calculate(p);
const memoKeys=['DifineMemo','SpaceMemo','EscData'];
const preserved=['HeadTitleDat','CostTitleDat','MachTitleDat'].every(key=>
p.tables[key].every((r,i)=>{
const target=q.tables[key][i];
return r.PkID===target.PkID && r.Index===target.Index && r.RecNo===target.RecNo && r.Flag===target.Flag &&
memoKeys.every(k=>JSON.stringify(r[k])===JSON.stringify(target[k]));
})
);
const out=e.exportEstx(q),again=m.importEstx(out),original=m.unzip(bytes),files=m.unzip(out);
const indexes=[...original].filter(([name])=>name.endsWith('.idx')).every(([name,value])=>value.equals(files.get(name)));
const reimportedUnite=hasQuantity ? again.quantitySheets?.[0]?.items?.[0]?.Unite : 'm2';
process.stdout.write(JSON.stringify({preserved,indexes,reimportedUnite}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8")
)
assert result["preserved"] and result["indexes"]
assert result["reimportedUnite"] == "m2"
def fixture(path):
fields = [
("PkID", 2, 4),
("Code", 1, 16),
("Name", 1, 32),
("GN", 7, 8),
("GU", 7, 8),
("TC", 7, 8),
]
stride = 25 + sum(width + 1 for _, _, width in fields)
data = bytearray(512 + 768 * len(fields) + stride)
data[8:24] = bytes.fromhex("068abe8e592364cb403d71d2e3bc64d0")
struct.pack_into("<IHH", data, 41, 1, stride, len(fields))
start = 512 + 768 * len(fields)
offset = 25
for i, (name, kind, width) in enumerate(fields):
at = 512 + i * 768
data[at + 2] = len(name)
data[at + 3 : at + 3 + len(name)] = name.encode("ascii")
data[at + 164] = kind
struct.pack_into("<H", data, at + 169, width)
struct.pack_into("<I", data, at + 172, offset)
data[start + offset] = 1
payload = start + offset + 1
if name == "PkID":
struct.pack_into("<i", data, payload, 1)
elif name in ["Code", "Name"]:
text = ("A" if name == "Code" else "원본").encode("cp949")
data[payload : payload + len(text)] = text
else:
struct.pack_into("<d", data, payload, {"GN": 2, "GU": 100, "TC": 200}[name])
offset += width + 1
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
archive.writestr("EstMainDat.dat", data)
archive.writestr("EstMainDat.idx", b"opaque index must remain unchanged")
archive.writestr("CadMainDat.blb", b"original drawing payload")
def test_estx_patches_values_and_preserves_indexes_drawings_and_rejects_structural_changes(
tmp_path,
):
source = tmp_path / "native.estx"
fixture(source)
script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs');
const b=fs.readFileSync(process.argv[1]),p=m.importEstx(b),same=e.exportEstx(p).equals(b);
p.tables.EstMainDat[0].GN=3;p.tables.EstMainDat[0].TC=300;p.tables.EstMainDat[0].Name='변경';
const next=e.exportEstx(p),q=m.importEstx(next),old=m.unzip(b),now=m.unzip(next);
let blocked=false;p.tables.EstMainDat[0].Code='B';try{e.exportEstx(p)}catch{blocked=true}
process.stdout.write(JSON.stringify({same,row:q.tables.EstMainDat[0],blocked,index:old.get('estmaindat.idx').equals(now.get('estmaindat.idx')),drawing:old.get('cadmaindat.blb').equals(now.get('cadmaindat.blb'))}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(source)], cwd=ROOT).decode("utf-8")
)
assert result["same"] and result["blocked"] and result["index"] and result["drawing"]
assert result["row"]["Name"] == "변경"
assert result["row"]["GN"] == 3 and result["row"]["TC"] == 300
def test_estx_export_schedule_and_documents_safety_guards(tmp_path):
source = tmp_path / "native.estx"
fixture(source)
script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs');
const b=fs.readFileSync(process.argv[1]);
// 1) 원본 그대로(빈 schedule/빈 documents/null 등)는 정상 통과
const p1 = m.importEstx(b);
p1.schedule = [];
p1.documents = {};
const same1 = e.exportEstx(p1).equals(b);
// 2) schedule 항목 추가 시 차단 검증
let blockedSchedule = false, schedErrMsg = '';
const p2 = m.importEstx(b);
p2.schedule = [{id:'1', Code:'A', Name:'새공정', start:'2026-01-01', end:'2026-01-05', weight:100, progress:10}];
try { e.exportEstx(p2); } catch (err) { blockedSchedule = true; schedErrMsg = err.message; }
// 2-1) 빈 문자열 또는 0값만 있는 schedule 항목 추가 시도 차단 검증
let blockedEmptySchedule = false;
const p2Empty = m.importEstx(b);
p2Empty.schedule = [{id:'2', Code:'', Name:'', start:'', end:'', weight:0, progress:0}];
try { e.exportEstx(p2Empty); } catch (err) { blockedEmptySchedule = true; }
// 3) documents 텍스트 입력 시 차단 검증
let blockedDocs = false, docErrMsg = '';
const p3 = m.importEstx(b);
p3.documents = {'173': '심사사유'};
try { e.exportEstx(p3); } catch (err) { blockedDocs = true; docErrMsg = err.message; }
// 3-1) documents 공백(" ") 입력 시도 사용자 데이터 보존을 위해 차단 검증
let blockedSpaceDocs = false;
const p3Space = m.importEstx(b);
p3Space.documents = {'173': ' '};
try { e.exportEstx(p3Space); } catch (err) { blockedSpaceDocs = true; }
process.stdout.write(JSON.stringify({same1, blockedSchedule, schedErrMsg, blockedEmptySchedule, blockedDocs, docErrMsg, blockedSpaceDocs}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(source)], cwd=ROOT).decode("utf-8")
)
assert result["same1"] is True
assert result["blockedSchedule"] is True
assert "예정공정표" in result["schedErrMsg"] and "JSON" in result["schedErrMsg"]
assert result["blockedEmptySchedule"] is True
assert result["blockedDocs"] is True
assert "서식 문서" in result["docErrMsg"] and "JSON" in result["docErrMsg"]
assert result["blockedSpaceDocs"] is True
assert "서식 문서" in result["docErrMsg"] and "JSON" in result["docErrMsg"]