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);