83 lines
5.0 KiB
JavaScript
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);
|