159 lines
8.6 KiB
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>
|