Files
Aislo/resources/tester/test_est_native_export.py

242 lines
15 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:13,PrintLeft:13,PrintRight:13,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": 13,
"left": 13,
"right": 13,
"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_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_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.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,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 "\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/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),q=s.calculate(p);
const ids=['HeadTitleDat','CostTitleDat','MachTitleDat'].every(key=>p.tables[key].every((r,i)=>r.PkID===q.tables[key][i].PkID));
const row=q.tables.EstMainDat.find(r=>r.GN&&r.TC);row.GN+=1;
const edited=s.calculate(q),out=e.exportEstx(edited),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)));
process.stdout.write(JSON.stringify({ids,indexes,gn:again.tables.EstMainDat.find(r=>r.PkID===row.PkID).GN,expected:row.GN}));"""
result = json.loads(
subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8")
)
assert result["ids"] and result["indexes"]
assert result["gn"] == result["expected"]
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