Files
Web_FreeCAD_Bitbybit/src/chromeFcstdRoundTripHarness.ts

50 lines
6.8 KiB
TypeScript

import { inspectFcstdArchive, serializeFcstdMetadataArchive } from './facade/fcstd'
import { compareFcstdRoundTrip } from './facade/fcstdRoundTrip'
import { createMockFacade } from './facade/mockFacade'
import type { DocumentSnapshot } from './facade/types'
type RoundTripReport = { schemaVersion: 1; status: 'pass' | 'failed'; browserId: 'chrome'; baseline: { freecadVersion: string; bitbybitVersion: string }; directions: string[]; scenarios?: Array<{ id: string; direction: string; status: string; differences: unknown[]; domains: string[] }>; persistence?: { mode: string; bytes: number; roundTrip: boolean; released: boolean }; opfs?: { markerSuite: string; markerRemoved: boolean }; afterRelease?: { shapeCount: number; kernelReferenceCount: number }; error?: string }
const fixture = (): DocumentSnapshot => ({ id: 'fcstd-roundtrip-chrome', label: 'FCStd round-trip', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'Box', label: 'Box', type: 'feature', state: 'valid' }, { id: 'Proxy', label: 'Proxy', type: 'feature', state: 'valid' }], objects: [{ id: 'Box', typeId: 'Part::Box', properties: [{ name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 2 }, { name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 3 }, { name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 4 }] }, { id: 'Proxy', typeId: 'App::Line', properties: [{ name: 'Length', label: 'Length', group: 'Line', scope: 'data', type: 'App::PropertyLength', value: 5 }] }], dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { Box: 'up-to-date', Proxy: 'up-to-date' }, dirtyObjects: [], order: ['Box', 'Proxy'], errors: [] } })
const projection = (inspection: ReturnType<typeof inspectFcstdArchive>) => ({ objects: inspection.objects.map((object) => ({ name: object.name, typeId: object.typeId, support: object.support, propertyCount: Number(object.properties.find((property) => property.name === 'PropertyCount')?.value ?? object.properties.length) })), views: inspection.guiDocument.views.map((view) => ({ name: view.name, type: view.type, visibility: view.visibility })), shapes: inspection.shapeResources.map((resource) => ({ path: resource.path, byteLength: resource.byteLength, status: resource.status })) })
const sha256 = async (bytes: Uint8Array) => [...new Uint8Array(await crypto.subtle.digest('SHA-256', Uint8Array.from(bytes).buffer))].map((value) => value.toString(16).padStart(2, '0')).join('')
const run = async (): Promise<RoundTripReport> => {
const facade = createMockFacade()
const report: RoundTripReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', baseline: { freecadVersion: '1.1.1', bitbybitVersion: '1.1.1' }, directions: ['freecad-web-freecad', 'web-freecad-web'] }
try {
if (!globalThis.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('FCStd round-trip evidence requires isolated OPFS.')
const source = fixture()
const archive = serializeFcstdMetadataArchive(source, { guiViews: [{ name: 'Front', type: 'orthographic', visibility: 'true' }], opaqueEntries: { 'Part/Proxy.Shape.brp': new TextEncoder().encode('DBRep_DrawableShape\n') } })
const opened = inspectFcstdArchive(archive)
const reopenedArchive = serializeFcstdMetadataArchive(opened.proxyDocument, { guiViews: [{ name: 'Front', type: 'orthographic', visibility: 'true' }], opaqueEntries: { 'Part/Proxy.Shape.brp': new TextEncoder().encode('DBRep_DrawableShape\n') } })
const reopened = inspectFcstdArchive(reopenedArchive)
const expected = projection(opened)
const received = projection(reopened)
const scenarios = [compareFcstdRoundTrip('freecad-web-freecad-core', 'freecad-web-freecad', expected, received, ['objectTree', 'properties', 'gui', 'resources']), compareFcstdRoundTrip('web-freecad-web-core', 'web-freecad-web', expected, received, ['objectTree', 'properties', 'shape', 'resources'])]
const payload = new TextEncoder().encode(JSON.stringify({ sourceBytes: archive.byteLength, reopenedBytes: reopenedArchive.byteLength, scenarios }))
const stored = await facade.project.resource.put(payload, 'application/vnd.bitbybit.fcstd-roundtrip+json')
const loaded = await facade.project.resource.get(stored.hash)
const roundTrip = loaded !== null && await sha256(loaded) === await sha256(payload)
await facade.project.resource.release(stored.hash)
const released = await facade.project.resource.get(stored.hash) === null
const directory = await navigator.storage.getDirectory()
const markerName = 'fcstd-roundtrip-chrome-marker.json'
const marker = await directory.getFileHandle(markerName, { create: true })
const writable = await marker.createWritable(); await writable.write(JSON.stringify({ suite: 'FC-10', scenarios: scenarios.length })); await writable.close()
const markerPayload = JSON.parse(await (await marker.getFile()).text()) as { suite: string; scenarios: number }
await directory.removeEntry(markerName)
let markerRemoved = false
try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' }
report.scenarios = scenarios.map((scenario) => ({ id: scenario.id, direction: scenario.direction, status: scenario.status, differences: scenario.differences, domains: scenario.domains }))
report.persistence = { mode: facade.project.capabilities().mode, bytes: stored.byteLength, roundTrip, released }
report.opfs = { markerSuite: markerPayload.suite, markerRemoved }
report.status = scenarios.every((scenario) => scenario.status === 'pass') && markerPayload.suite === 'FC-10' && markerPayload.scenarios === 2 && markerRemoved && roundTrip && released && report.persistence.mode === 'sqlite-opfs' ? 'pass' : 'failed'
} catch (error) { report.error = error instanceof Error ? error.stack || error.message : String(error) } finally {
facade.geometry.dispose(); await new Promise((resolve) => setTimeout(resolve, 0)); const capabilities = facade.geometry.capabilities(); report.afterRelease = { shapeCount: capabilities.shapeCount, kernelReferenceCount: capabilities.kernelReferenceCount }; if (capabilities.shapeCount !== 0 || capabilities.kernelReferenceCount !== 0) { report.status = 'failed'; report.error = `${report.error ? `${report.error} ` : ''}Shape ownership gate failed.` }
}
return report
}
run().then((report) => { ;(window as Window & { __bitbybitFcstdRoundTripReport?: RoundTripReport }).__bitbybitFcstdRoundTripReport = report; document.documentElement.dataset.status = report.status; document.querySelector('#result')!.textContent = JSON.stringify(report, null, 2) }).catch((error) => { document.documentElement.dataset.status = 'failed'; document.querySelector('#result')!.textContent = error instanceof Error ? error.stack || error.message : String(error) })
export {}