feat: establish reproducible FreeCAD web compatibility baseline
This commit is contained in:
158
public/chrome-fault-injection-harness.html
Normal file
158
public/chrome-fault-injection-harness.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Chrome fault injection harness</title></head>
|
||||
<body>
|
||||
<pre id="status">loading</pre>
|
||||
<script type="module">
|
||||
import { createSketch } from '/src/facade/sketcher.ts'
|
||||
import { PlanegcsWorkerProvider } from '/src/facade/planegcsWorkerClient.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
import { SketchSolverCoordinator } from '/src/facade/sketchSolverProtocol.ts'
|
||||
|
||||
const browserId = new URL(location.href).searchParams.get('browser') || 'chrome'
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
browserId,
|
||||
executionContext: 'dedicated-worker',
|
||||
crossOriginIsolated,
|
||||
sharedArrayBuffer: typeof SharedArrayBuffer === 'function',
|
||||
status: 'failed',
|
||||
}
|
||||
|
||||
const wrappers = []
|
||||
let activeWrapper = null
|
||||
const workerFactory = () => {
|
||||
const real = new Worker(new URL('/src/facade/planegcsWorkerEntry.ts', location.href), { type: 'module', name: 'qa-06-planegcs' })
|
||||
const listeners = { message: new Set(), error: new Set() }
|
||||
let terminated = false
|
||||
const wrapper = {
|
||||
postMessage: (message) => real.postMessage(message),
|
||||
terminate: () => { terminated = true; real.terminate() },
|
||||
get terminated() { return terminated },
|
||||
addEventListener: (type, listener) => { listeners[type].add(listener) },
|
||||
removeEventListener: (type, listener) => { listeners[type].delete(listener) },
|
||||
crash: () => {
|
||||
terminated = true
|
||||
real.terminate()
|
||||
const event = { message: 'QA-06 injected Worker crash', error: new Error('QA-06 injected Worker crash') }
|
||||
for (const listener of listeners.error) listener(event)
|
||||
},
|
||||
}
|
||||
real.addEventListener('message', (event) => { for (const listener of listeners.message) listener(event) })
|
||||
real.addEventListener('error', (event) => { for (const listener of listeners.error) listener(event) })
|
||||
wrappers.push(wrapper)
|
||||
activeWrapper = wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const nativeWrappers = []
|
||||
let activeNativeWrapper = null
|
||||
const nativeWorkerFactory = () => {
|
||||
const real = new Worker(new URL('/src/facade/nativeHistoryWorkerEntry.ts', location.href), { type: 'module', name: 'qa-06-native-history' })
|
||||
const listeners = { message: new Set(), error: new Set() }
|
||||
let terminated = false
|
||||
const wrapper = {
|
||||
postMessage: (message) => real.postMessage(message),
|
||||
terminate: () => { terminated = true; real.terminate() },
|
||||
get terminated() { return terminated },
|
||||
addEventListener: (type, listener) => { listeners[type].add(listener) },
|
||||
removeEventListener: (type, listener) => { listeners[type].delete(listener) },
|
||||
crash: () => {
|
||||
terminated = true
|
||||
real.terminate()
|
||||
const event = { message: 'QA-06 injected native history Worker crash', error: new Error('QA-06 injected native history Worker crash') }
|
||||
for (const listener of listeners.error) listener(event)
|
||||
},
|
||||
}
|
||||
real.addEventListener('message', (event) => { for (const listener of listeners.message) listener(event) })
|
||||
real.addEventListener('error', (event) => { for (const listener of listeners.error) listener(event) })
|
||||
nativeWrappers.push(wrapper)
|
||||
activeNativeWrapper = wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const snapshot = createSketch('qa-06-sketch', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 3, y: 4 } }], [
|
||||
{ id: 'horizontal', type: 'horizontal', geometryId: 'line' },
|
||||
{ id: 'length', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 5 },
|
||||
])
|
||||
const request = (requestId) => ({ requestId, documentId: 'qa-06-doc', documentVersion: 1, snapshot })
|
||||
const settle = (promise) => promise.then(() => 'resolved', (error) => ({ name: error?.name || 'Error', message: String(error?.message || error) }))
|
||||
|
||||
let provider
|
||||
let nativeProvider
|
||||
let markerName
|
||||
try {
|
||||
if (!crossOriginIsolated || typeof SharedArrayBuffer !== 'function') throw new Error('Chrome fault harness requires cross-origin isolation and SharedArrayBuffer.')
|
||||
if (!navigator.storage?.getDirectory) throw new Error('Chrome OPFS is unavailable.')
|
||||
|
||||
const root = await navigator.storage.getDirectory()
|
||||
markerName = `qa-06-fault-${crypto.randomUUID()}.marker`
|
||||
const marker = await root.getFileHandle(markerName, { create: true })
|
||||
await (await marker.createWritable()).close()
|
||||
const markerBeforeCleanup = await root.getFileHandle(markerName)
|
||||
const markerBytes = (await (await markerBeforeCleanup.getFile()).arrayBuffer()).byteLength
|
||||
|
||||
provider = new PlanegcsWorkerProvider({ workerFactory, initializationTimeoutMs: 30_000 })
|
||||
const initial = await provider.initialize()
|
||||
const completed = await new SketchSolverCoordinator(() => 1).solve(provider, request('qa-06-completed'))
|
||||
|
||||
const cancellationCoordinator = new SketchSolverCoordinator(() => 1)
|
||||
const cancellation = cancellationCoordinator.solve(provider, request('qa-06-cancelled'))
|
||||
cancellationCoordinator.cancel()
|
||||
const cancelled = await cancellation
|
||||
|
||||
const wrapperBeforeCrash = activeWrapper
|
||||
wrapperBeforeCrash?.crash()
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
const unavailableAfterCrash = provider.capabilities().availability === 'unavailable'
|
||||
const recovered = await new SketchSolverCoordinator(() => 1).solve(provider, request('qa-06-recovered'))
|
||||
const stale = await new SketchSolverCoordinator(() => 2).solve(provider, request('qa-06-stale'))
|
||||
const recoveredLine = recovered.response?.result.snapshot.geometry[0]
|
||||
|
||||
nativeProvider = new NativeOcctHistoryWorkerProvider({ workerFactory: nativeWorkerFactory, moduleUrl: '/native/occt-history/bitbybit-occt-history.js', initializationTimeoutMs: 30_000 })
|
||||
const nativeInitial = await nativeProvider.initialize()
|
||||
const nativeWrapperBeforeCrash = activeNativeWrapper
|
||||
nativeWrapperBeforeCrash?.crash()
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
const nativeUnavailableAfterCrash = nativeProvider.capabilities().availability === 'unavailable'
|
||||
const nativeRecovered = await nativeProvider.initialize()
|
||||
|
||||
await root.removeEntry(markerName)
|
||||
let markerRemoved = false
|
||||
try { await root.getFileHandle(markerName); } catch (error) { markerRemoved = error?.name === 'NotFoundError' }
|
||||
markerName = undefined
|
||||
|
||||
const lifecycle = {
|
||||
initialAvailable: initial.availability === 'available',
|
||||
completed: completed.status,
|
||||
cancelled: cancelled.status,
|
||||
crashInjected: true,
|
||||
unavailableAfterCrash,
|
||||
workerFactoryCount: wrappers.length,
|
||||
recovered: recovered.status,
|
||||
stale: stale.status,
|
||||
recoveredEnd: recoveredLine?.type === 'line' ? recoveredLine.end : null,
|
||||
nativeInitialAvailable: nativeInitial.availability === 'available',
|
||||
nativeCrashInjected: true,
|
||||
nativeUnavailableAfterCrash,
|
||||
nativeWorkerFactoryCount: nativeWrappers.length,
|
||||
nativeRecoveredAvailable: nativeRecovered.availability === 'available',
|
||||
}
|
||||
const opfs = { markerBytes, markerRemoved }
|
||||
const passed = lifecycle.initialAvailable && lifecycle.completed === 'completed' && lifecycle.cancelled === 'cancelled' && lifecycle.crashInjected && lifecycle.unavailableAfterCrash && lifecycle.workerFactoryCount >= 3 && lifecycle.recovered === 'completed' && lifecycle.stale === 'stale' && Math.abs(lifecycle.recoveredEnd?.x - 5) <= 1e-7 && Math.abs(lifecycle.recoveredEnd?.y) <= 1e-7 && lifecycle.nativeInitialAvailable && lifecycle.nativeCrashInjected && lifecycle.nativeUnavailableAfterCrash && lifecycle.nativeWorkerFactoryCount >= 2 && lifecycle.nativeRecoveredAvailable && opfs.markerBytes === 0 && opfs.markerRemoved
|
||||
Object.assign(report, { status: passed ? 'pass' : 'failed', lifecycle, opfs, activeWorkersBeforeDispose: wrappers.length, activeNativeWorkersBeforeDispose: nativeWrappers.length })
|
||||
} catch (error) {
|
||||
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
if (markerName) {
|
||||
try { await (await navigator.storage.getDirectory()).removeEntry(markerName) } catch {}
|
||||
}
|
||||
} finally {
|
||||
provider?.dispose()
|
||||
nativeProvider?.dispose()
|
||||
Object.assign(report, { activeWorkersAfterDispose: wrappers.filter((wrapper) => wrapper.terminated).length, activeNativeWorkersAfterDispose: nativeWrappers.filter((wrapper) => wrapper.terminated).length })
|
||||
}
|
||||
document.querySelector('#status').textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__chrome-fault-injection-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user