195 lines
7.1 KiB
TypeScript
195 lines
7.1 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { useTransactionalFileWorker } from '../src/composables/useTransactionalFileWorker'
|
|
|
|
type TestResponse = {
|
|
type: 'parsed'
|
|
id: number
|
|
value: string
|
|
} | {
|
|
type: 'calculated'
|
|
id: number
|
|
value: string
|
|
} | {
|
|
type: 'error'
|
|
id: number
|
|
operation: 'parse' | 'calculate'
|
|
error: string
|
|
}
|
|
|
|
class FakeWorker {
|
|
onmessage: ((event: MessageEvent<TestResponse>) => void) | null = null
|
|
onerror: ((event: ErrorEvent) => void) | null = null
|
|
onmessageerror: ((event: MessageEvent) => void) | null = null
|
|
messages: Array<Record<string, unknown>> = []
|
|
terminated = false
|
|
|
|
postMessage(message: Record<string, unknown>) {
|
|
this.messages.push(message)
|
|
}
|
|
|
|
terminate() {
|
|
this.terminated = true
|
|
}
|
|
|
|
respond(response: TestResponse) {
|
|
this.onmessage?.({ data: response } as MessageEvent<TestResponse>)
|
|
}
|
|
|
|
fail() {
|
|
this.onerror?.({ preventDefault: vi.fn() } as unknown as ErrorEvent)
|
|
}
|
|
}
|
|
|
|
function file(name: string): File {
|
|
return { name, size: 1 } as File
|
|
}
|
|
|
|
function createHarness() {
|
|
const workers: FakeWorker[] = []
|
|
const parsed: string[] = []
|
|
const calculated: string[] = []
|
|
const parseErrors: string[] = []
|
|
const calculationErrors: string[] = []
|
|
const pendingWorkerErrors: boolean[] = []
|
|
const activeWorkerError = vi.fn()
|
|
const manager = useTransactionalFileWorker<Record<string, unknown>, TestResponse>({
|
|
createWorker: () => {
|
|
const worker = new FakeWorker()
|
|
workers.push(worker)
|
|
return worker as unknown as Worker
|
|
},
|
|
onParsed: (response, selected) => parsed.push(`${selected.name}:${response.value}`),
|
|
onCalculated: (response) => calculated.push(response.value),
|
|
onParseError: (message) => parseErrors.push(message),
|
|
onCalculationError: (message) => calculationErrors.push(message),
|
|
onPendingWorkerError: (hasCurrentFile) => pendingWorkerErrors.push(hasCurrentFile),
|
|
onActiveWorkerError: activeWorkerError,
|
|
})
|
|
return {
|
|
manager,
|
|
workers,
|
|
parsed,
|
|
calculated,
|
|
parseErrors,
|
|
calculationErrors,
|
|
pendingWorkerErrors,
|
|
activeWorkerError,
|
|
}
|
|
}
|
|
|
|
describe('transactional file Worker', () => {
|
|
it('commits a parsed candidate and calculates with its Worker', () => {
|
|
const harness = createHarness()
|
|
expect(harness.manager.parseFile(file('first.hex'))).toBe(true)
|
|
expect(harness.manager.parsing.value).toBe(true)
|
|
expect(harness.workers[0].messages[0]).toMatchObject({ type: 'parse', id: 1 })
|
|
|
|
harness.workers[0].respond({ type: 'parsed', id: 1, value: 'ready' })
|
|
expect(harness.manager.parsing.value).toBe(false)
|
|
expect(harness.parsed).toEqual(['first.hex:ready'])
|
|
|
|
expect(harness.manager.calculate({ parameters: 'crc-32' })).toBe('started')
|
|
expect(harness.manager.calculating.value).toBe(true)
|
|
expect(harness.workers[0].messages[1]).toEqual({ type: 'calculate', id: 2, parameters: 'crc-32' })
|
|
harness.workers[0].respond({ type: 'calculated', id: 2, value: 'result' })
|
|
expect(harness.calculated).toEqual(['result'])
|
|
expect(harness.manager.calculating.value).toBe(false)
|
|
})
|
|
|
|
it('supports an explicit parse-request payload without changing default callers', () => {
|
|
const workers: FakeWorker[] = []
|
|
const manager = useTransactionalFileWorker<Record<string, unknown>, TestResponse>({
|
|
createWorker: () => {
|
|
const worker = new FakeWorker()
|
|
workers.push(worker)
|
|
return worker as unknown as Worker
|
|
},
|
|
createParseRequest: (id, selected) => ({ type: 'parse', id, file: selected, mode: 'intel-hex' }),
|
|
onParsed: vi.fn(),
|
|
onCalculated: vi.fn(),
|
|
onParseError: vi.fn(),
|
|
onCalculationError: vi.fn(),
|
|
onPendingWorkerError: vi.fn(),
|
|
onActiveWorkerError: vi.fn(),
|
|
})
|
|
|
|
const selected = file('firmware.hex')
|
|
manager.parseFile(selected)
|
|
expect(workers[0].messages[0]).toEqual({ type: 'parse', id: 1, file: selected, mode: 'intel-hex' })
|
|
})
|
|
|
|
it('preserves the active Worker when a replacement parse fails', () => {
|
|
const harness = createHarness()
|
|
harness.manager.parseFile(file('first.hex'))
|
|
harness.workers[0].respond({ type: 'parsed', id: 1, value: 'first' })
|
|
|
|
harness.manager.parseFile(file('invalid.hex'))
|
|
harness.workers[1].respond({ type: 'error', id: 2, operation: 'parse', error: 'bad checksum' })
|
|
expect(harness.parseErrors).toEqual(['bad checksum'])
|
|
expect(harness.workers[0].terminated).toBe(false)
|
|
expect(harness.workers[1].terminated).toBe(true)
|
|
expect(harness.manager.calculate({})).toBe('started')
|
|
})
|
|
|
|
it('atomically replaces the active Worker and ignores its stale result', () => {
|
|
const harness = createHarness()
|
|
harness.manager.parseFile(file('first.hex'))
|
|
harness.workers[0].respond({ type: 'parsed', id: 1, value: 'first' })
|
|
harness.manager.calculate({})
|
|
|
|
harness.manager.parseFile(file('second.hex'))
|
|
harness.workers[1].respond({ type: 'parsed', id: 3, value: 'second' })
|
|
expect(harness.workers[0].terminated).toBe(true)
|
|
expect(harness.parsed).toEqual(['first.hex:first', 'second.hex:second'])
|
|
expect(harness.manager.calculating.value).toBe(false)
|
|
|
|
harness.workers[0].respond({ type: 'calculated', id: 2, value: 'stale' })
|
|
expect(harness.calculated).toEqual([])
|
|
})
|
|
|
|
it('distinguishes pending Worker failures with and without a current file', () => {
|
|
const harness = createHarness()
|
|
harness.manager.parseFile(file('first.hex'))
|
|
harness.workers[0].fail()
|
|
expect(harness.pendingWorkerErrors).toEqual([false])
|
|
|
|
harness.manager.parseFile(file('current.hex'))
|
|
harness.workers[1].respond({ type: 'parsed', id: 2, value: 'current' })
|
|
harness.manager.parseFile(file('replacement.hex'))
|
|
harness.workers[2].fail()
|
|
expect(harness.pendingWorkerErrors).toEqual([false, true])
|
|
expect(harness.workers[1].terminated).toBe(false)
|
|
})
|
|
|
|
it('terminates active and pending Workers and ignores late responses', () => {
|
|
const harness = createHarness()
|
|
harness.manager.parseFile(file('first.hex'))
|
|
harness.workers[0].respond({ type: 'parsed', id: 1, value: 'first' })
|
|
harness.manager.parseFile(file('second.hex'))
|
|
harness.manager.destroyWorkers()
|
|
|
|
expect(harness.workers.every((worker) => worker.terminated)).toBe(true)
|
|
harness.workers[1].respond({ type: 'parsed', id: 2, value: 'late' })
|
|
expect(harness.parsed).toEqual(['first.hex:first'])
|
|
expect(harness.manager.parsing.value).toBe(false)
|
|
expect(harness.manager.calculating.value).toBe(false)
|
|
})
|
|
|
|
it('reports Worker construction failure without entering a busy state', () => {
|
|
const pendingWorkerErrors: boolean[] = []
|
|
const manager = useTransactionalFileWorker<Record<string, unknown>, TestResponse>({
|
|
createWorker: () => { throw new Error('unsupported') },
|
|
onParsed: vi.fn(),
|
|
onCalculated: vi.fn(),
|
|
onParseError: vi.fn(),
|
|
onCalculationError: vi.fn(),
|
|
onPendingWorkerError: (hasCurrentFile) => pendingWorkerErrors.push(hasCurrentFile),
|
|
onActiveWorkerError: vi.fn(),
|
|
})
|
|
|
|
expect(manager.parseFile(file('first.hex'))).toBe(false)
|
|
expect(pendingWorkerErrors).toEqual([false])
|
|
expect(manager.parsing.value).toBe(false)
|
|
})
|
|
})
|