Files
FreeCADWasmDev/tools/test_ifc_identity.mjs

83 lines
5.0 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { loadWasmProbe } from './wasm-probe-runtime.mjs';
const output=path.resolve('docs/current/user-tests-20260915',`ifc-identity-${new Date().toISOString().replace(/[:.]/g,'-')}`);
fs.mkdirSync(output,{recursive:true});
const log=value=>fs.appendFileSync(path.join(output,'native.log'),value+'\n');
let kernel;
try {
({kernel}=await loadWasmProbe('web/public/kernel','FreeCADWebBimRuntimeProbe',{print:log,printErr:log}));
const reply=JSON.parse(kernel.ccall('fc_web_bim_test_python','string',['string'],[`
import FreeCAD as App, Part, IfcOpenShellNative, json, hashlib, os
checks=[]
def check(name, actual, passed):
checks.append({'name':name,'actual':actual,'passed':bool(passed)})
if not passed: raise AssertionError(name+': '+str(actual))
def export_ids(document):
IfcOpenShellNative.exportDocument(document.Name,'/tmp/identity.ifc')
r=App.newDocument('IdentityRead')
try:
IfcOpenShellNative.insert('/tmp/identity.ifc',r.Name)
return {o.Label:o.IfcGlobalId for o in r.Objects if o.isDerivedFrom('Part::Feature')}
finally: App.closeDocument(r.Name)
def preserved_failure(document, name):
before=hashlib.sha256(open('/tmp/identity.ifc','rb').read()).hexdigest()
before_objects=[(o.Name,o.Label,o.Shape.Volume,sorted(o.PropertiesList)) for o in document.Objects]
rejected=False
try: IfcOpenShellNative.exportDocument(document.Name,'/tmp/identity.ifc')
except RuntimeError: rejected=True
after=hashlib.sha256(open('/tmp/identity.ifc','rb').read()).hexdigest()
after_objects=[(o.Name,o.Label,o.Shape.Volume,sorted(o.PropertiesList)) for o in document.Objects]
check(name,{'rejected':rejected,'fileUnchanged':before==after,'objectsUnchanged':before_objects==after_objects},
rejected and before==after and before_objects==after_objects)
d=App.newDocument('IdentitySource'); d.UndoMode=1
for name,dims in [('First',(2,3,4)),('Second',(3,4,5))]:
o=d.addObject('Part::Feature',name); o.Shape=Part.makeBox(*dims)
d.recompute()
before=[(o.Name,sorted(o.PropertiesList)) for o in d.Objects]
first=export_ids(d); repeated=export_ids(d)
check('Repeated exports retain distinct product identities without source property mutation',first,
first==repeated and len(set(first.values()))==2 and before==[(o.Name,sorted(o.PropertiesList)) for o in d.Objects])
d.getObject('First').Label='Renamed wall column'; d.getObject('First').Shape=Part.makeBox(5,6,7); d.recompute()
edited=export_ids(d)
check('Label and geometry edits retain object identity',edited,edited['Renamed wall column']==first['First'] and edited['Second']==first['Second'])
d.saveAs('/tmp/Identity.FCStd'); App.closeDocument(d.Name); d=App.openDocument('/tmp/Identity.FCStd')
restored=export_ids(d)
check('FCStd save close and reopen retains generated GlobalIds',restored,restored==edited)
one=d.getObject('First'); two=d.getObject('Second')
one.addProperty('App::PropertyString','IfcGlobalId'); one.IfcGlobalId='2Kyssv$kPduAY5HpYkdCrw'
explicit=export_ids(d)
check('Imported native IfcGlobalId is preserved',explicit,explicit[one.Label]==one.IfcGlobalId)
two.addProperty('App::PropertyMap','IfcData'); two.IfcData={'IfcUID':'2E7Xcrf_DiKNFkQbRhLvWM'}
legacy=export_ids(d)
check('Desktop IfcData IfcUID is preserved',legacy,legacy[two.Label]=='2E7Xcrf_DiKNFkQbRhLvWM')
two.IfcData={'IfcUID':one.IfcGlobalId}; preserved_failure(d,'Duplicate GlobalIds reject export without modifying file or document')
two.IfcData={'IfcUID':'2E7Xcrf_DiKNFkQbRhLvWM'}
one.addProperty('App::PropertyString','GlobalId'); one.GlobalId='1sLpijh_wqTpS_F_2QKBAo'
preserved_failure(d,'Conflicting native identities reject export atomically')
one.removeProperty('GlobalId')
for invalid in ['too-short','9Kyssv$kPduAY5HpYkdCrw','2Kyssv!kPduAY5HpYkdCrw']:
one.IfcGlobalId=invalid; preserved_failure(d,'Invalid compressed IFC GlobalId is rejected: '+invalid)
one.IfcGlobalId='2Kyssv$kPduAY5HpYkdCrw'
recovered=export_ids(d)
check('Valid export recovers after rejected identities',recovered,recovered==legacy)
independent=App.newDocument('IndependentIdentity')
for name,dims in [('First',(2,3,4)),('Second',(3,4,5))]:
o=independent.addObject('Part::Feature',name); o.Shape=Part.makeBox(*dims)
independent.recompute(); other=export_ids(independent)
check('Separate native documents cannot share generated identities',other,not(set(other.values())&set(first.values())))
for doc in list(App.listDocuments()): App.closeDocument(doc)
for file in ['/tmp/identity.ifc','/tmp/Identity.FCStd']:
if os.path.exists(file): os.remove(file)
result=json.dumps({'status':'passed','checks':checks})
`]));
fs.writeFileSync(path.join(output,'results.json'),JSON.stringify(reply,null,2)+'\n');
if(reply.status!=='passed') throw Error(reply.error??JSON.stringify(reply));
console.log(JSON.stringify({status:'passed',checks:reply.checks.length,output}));
} catch(error) {
fs.writeFileSync(path.join(output,'failure.json'),JSON.stringify({error:String(error)},null,2)+'\n');
console.error(String(error));process.exitCode=1;
} finally {kernel?.PThread?.terminateAllThreads?.();}
process.exit(process.exitCode??0);