Files
Web_FreeCAD_Bitbybit/public/chrome-fault-injection-harness.html

159 lines
8.6 KiB
HTML

<!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>