Files
Aislo/resources/tester/test_est_exchange.py
T

236 lines
9.1 KiB
Python

import io
import json
import subprocess
import zipfile
from pathlib import Path
import xml.etree.ElementTree as ET
from test_est_rea_parity import run
ROOT = Path(__file__).resolve().parents[2]
def export_workbook(project):
result = subprocess.run(
[
"node",
"-e",
"const fs=require('fs');process.stdout.write(require('./tools/est-test/exchange.cjs').workbook(JSON.parse(fs.readFileSync(0,'utf8'))))",
],
input=json.dumps(project, ensure_ascii=False).encode("utf-8"),
capture_output=True,
cwd=ROOT,
check=True,
)
return result.stdout
def test_excel_is_valid_office_zip_with_numeric_text_types_and_no_executable_formulas():
data = export_workbook(
{
"name": "한글 공사",
"tables": {
"EstMainDat": [
{
"PkID": 1,
"Code": "001",
"Name": '=HYPERLINK("bad")',
"GN": 1.23456789,
"TC": 99,
"NewItem": False,
"Editor": {"binary": "AAAA"},
}
],
"CadMainDat": [{"Code": "excluded"}],
},
}
)
with zipfile.ZipFile(io.BytesIO(data)) as archive:
assert archive.testzip() is None
workbook = archive.read("xl/workbook.xml").decode("utf-8")
assert "EstMainDat" in workbook and "CadMainDat" not in workbook
root = ET.fromstring(archive.read("xl/worksheets/sheet2.xml"))
ns = {"s": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
rows = root.findall(".//s:row", ns)
assert "Editor" not in ET.tostring(rows[0], encoding="unicode")
assert rows[1].find("s:c[@r='B2']", ns).get("t") == "inlineStr"
assert rows[1].find("s:c[@r='D2']/s:v", ns).text == "1.23456789"
assert root.findall(".//s:f", ns) == []
assert "=HYPERLINK" in ET.tostring(root, encoding="unicode")
def test_print_settings_validation_blocks_css_injection_and_bad_pagination():
result = run(
"math",
"""(()=>{
return [{paper:'A4;display:none'}, {rowsPerPage:0}, {rowsPerPage:1.5}, {font:100}, {margin:-1}, {orientation:'portrait',paper:'A4',rowsPerPage:20,font:9}].map(s=>{
const p=EstNative.empty();p.printSettings=s;
try{EstModel.validate(p);return true}catch{return false}
});
})()""",
)
assert result == [False, False, False, False, False, True]
def test_excel_price_edits_refresh_linked_values_and_keep_binary_fields():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.tables.StuffcDat=[{PkID:1,Code:'M',Name:'material',InputTC:100,ValueIndex:4,Editor:{binary:'AAAA'}}];
p.tables.EstMainDat=[{Code:'M',LinkIndex:5,GN:2},{LinkIndex:54}];
const q=EstNative.calculate(p),updated=EstExchange.apply(q,{StuffcDat:[{PkID:1,Code:'M',Name:'material',TC:150,InputTC:100,ValueIndex:4}]});
let rejected=false;try{EstExchange.apply(q,{StuffcDat:[{PkID:1,Code:'',TC:150}]})}catch{rejected=true}
return {next:EstNative.calculate(updated),rejected};
})()""",
)
assert result["rejected"]
assert result["next"]["tables"]["EstMainDat"][-1]["TC"] == 300
assert result["next"]["tables"]["StuffcDat"][0]["Editor"]["binary"] == "AAAA"
def test_excel_exports_manual_quantities_and_schedule():
import openpyxl
data = export_workbook(
{
"name": "project",
"tables": {},
"quantities": [
{
"Code": "Q1",
"Name": "quantity",
"formula": "[A]*3",
"digits": 2,
"TargetKey": "id:5",
}
],
"schedule": [
{
"id": "s1",
"SourceKey": "id:5",
"Code": "",
"Name": "work",
"start": "2026-01-01",
"end": "2026-01-10",
"weight": 100,
"progress": 25,
}
],
}
)
workbook = openpyxl.load_workbook(io.BytesIO(data), data_only=False)
assert list(workbook["Quantities"].values)[1] == ("Q1", "quantity", None, "id:5", "[A]*3", 2)
assert list(workbook["Schedule"].values)[1] == (
"s1",
"id:5",
"",
"work",
"2026-01-01",
"2026-01-10",
100,
25,
)
def test_excel_imports_manual_quantities_and_schedule_and_rejects_invalid():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.tables.EstMainDat=[{PkID:5,Code:'',Name:'work',GN:1}];
const qty={Code:'Q1',Name:'quantity',formula:'2*3',digits:2,TargetKey:'id:5',TargetCode:''};
const sched={id:'s1',SourceKey:'id:5',Code:'',Name:'work',start:'2026-01-01',end:'2026-01-10',weight:100,progress:25};
const q=EstExchange.apply(p,{Quantities:[qty],Schedule:[sched]});
EstQuantity.applyTotals(q,EstQuantity.totals(q,'manual'));
let invalid=0;for(const bad of [{Quantities:[qty,qty]},{Quantities:[{...qty,digits:7}]},
{Schedule:[{...sched,progress:101}]}])try{EstExchange.apply(p,bad)}catch{invalid++}
return {q,invalid,original:p};
})()""",
)
assert result["q"]["tables"]["EstMainDat"][0]["GN"] == 6
assert result["q"]["schedule"][0]["SourceKey"] == "id:5"
assert result["q"]["schedule"][0]["progress"] == 25
assert result["original"]["tables"]["EstMainDat"][0]["GN"] == 1
assert result["invalid"] == 3
def test_excel_writes_quantity_marker_controls_as_valid_xml():
import openpyxl
data = export_workbook(
{
"name": "project",
"tables": {"CostMainDat": [{"PkID": 1, "LineData": "A=\x121.5\x12 _x0012_"}]},
}
)
workbook = openpyxl.load_workbook(io.BytesIO(data))
assert workbook["CostMainDat"]["B2"].value == "A=_x0012_1.5_x0012_ _x005F_x0012_"
assert (
run("math", "EstExchange.decodeExcelText('A=_x0012_1.5_x0012_ _x005F_x0012_')")
== "A=\x121.5\x12 _x0012_"
)
def test_excel_exports_and_imports_quantity_documents_with_markers():
import openpyxl
data = export_workbook(
{
"name": "project",
"tables": {},
"quantitySheets": [
{
"Code": "Q",
"Name": "sheet",
"options": {"digits": 2, "roundMode": 2},
"rows": [{"LineData": "2*3 = \x120\x12", "markers": ["r1"]}],
"items": [{"Code": "r1", "Name": "item", "GN": 0, "TargetKey": "id:5"}],
}
],
}
)
workbook = openpyxl.load_workbook(io.BytesIO(data))
assert workbook["QDocuments"]["A2"].value == "Q"
assert workbook["QLines"]["C2"].value == "2*3 = _x0012_0_x0012_"
assert workbook["QItems"]["I2"].value == "id:5"
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.tables.EstMainDat=[{PkID:5,Code:'',GN:1}];
p.quantitySheets=[{Code:'Q',Name:'old',options:{digits:2,roundMode:2},
rows:[{LineData:'1 = \\x120\\x12',markers:['r1']}],
items:[{Code:'r1',Name:'old',GN:0,Extra:'preserved'}]}];
const tables={QDocuments:[{Code:'Q',Name:'edited',digits:2,roundMode:2}],
QLines:[{DocumentCode:'Q',Position:1,LineData:'2*3 = \\x120\\x12',Markers:'["r1"]'}],
QItems:[{DocumentCode:'Q',Position:1,Code:'r1',Name:'item',GN:0,TargetKey:'id:5'}]};
const q=EstExchange.apply(p,tables),out=EstQuantity.calculate(q.quantitySheets[0]);
EstQuantity.applyTotals(q,EstQuantity.totals(q,'Q'));
let rejected=0;for(const bad of [{QDocuments:tables.QDocuments},
{...tables,QLines:[{...tables.QLines[0],Markers:'not-json'}]},
{...tables,QItems:[...tables.QItems,...tables.QItems]}])
try{EstExchange.apply(p,bad)}catch{rejected++}
return {q,out,rejected,original:p.quantitySheets[0].Name};
})()""",
)
assert result["q"]["quantitySheets"][0]["Name"] == "edited"
assert result["q"]["quantitySheets"][0]["items"][0]["Extra"] == "preserved"
assert result["q"]["tables"]["EstMainDat"][0]["GN"] == 6
assert result["out"]["errors"] == []
assert result["rejected"] == 3
assert result["original"] == "old"
def test_excel_quantity_document_roundtrip_keeps_optional_original_fields():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.quantitySheets=[{Code:'Q',Name:'sheet',options:{digits:2,roundMode:2},
rows:[{LineData:'1 = \\x120\\x12',markers:['r1']}],
items:[{Code:'r1',Name:'item',Size:null,Unite:null,GN:null,LinkCode:null,Extra:'keep'}]}];
const tables={QDocuments:[{Code:'Q',Name:'sheet',digits:2,roundMode:2}],
QLines:[{DocumentCode:'Q',Position:1,LineData:'1 = \\x120\\x12',Markers:'["r1"]'}],
QItems:[{DocumentCode:'Q',Position:1,Code:'r1',Name:'item',Size:null,Unite:null,GN:null,TargetCode:null,TargetKey:null}]};
return {before:p.quantitySheets,after:EstExchange.apply(p,tables).quantitySheets};
})()""",
)
assert result["before"] == result["after"]