405 lines
26 KiB
Python
405 lines
26 KiB
Python
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:"r000001";]]/>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:"r000001";]]/>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"]
|