mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
260 lines
12 KiB
JavaScript
260 lines
12 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { spawnSync } from 'node:child_process'
|
|
import fs from 'node:fs'
|
|
import process from 'node:process'
|
|
// eslint-disable-next-line test/no-import-node-test -- Historical runner includes fatal isolated Node rejection probes.
|
|
import test from 'node:test'
|
|
import { danmukuMaskEnvironment, danmukuMaskHistorical, flushMask } from './helpers/danmuku-mask.js'
|
|
|
|
// These assertions describe frozen historical defects, never candidate acceptance.
|
|
const implementations = await danmukuMaskHistorical()
|
|
const detailed = implementations.filter(item => ['frozen-source', 'published-1.1.0-main'].includes(item.name))
|
|
|
|
test('Historical SDK probes replace exactly five call sites and retain verified input identities', () => {
|
|
const expected = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-mask-failures-validation.json', 'utf8'))
|
|
const actual = implementations.map(({ code: _code, ...identity }) => identity)
|
|
assert.deepEqual(actual, expected.inputs, 'SDK seam transformations must remain exactly reproducible')
|
|
assert.equal(implementations.length, 7)
|
|
for (const item of implementations) {
|
|
assert.match(item.inputSha256, /^[a-f0-9]{64}$/u)
|
|
assert.match(item.transformedSha256, /^[a-f0-9]{64}$/u)
|
|
if (!item.source) {
|
|
assert.match(item.archiveSha256, /^[a-f0-9]{64}$/u)
|
|
assert.match(item.probeSha256, /^[a-f0-9]{64}$/u)
|
|
assert.deepEqual(item.counts, { setBackend: 2, createSegmenter: 1, toBinaryMask: 1, drawMask: 1 })
|
|
assert.equal(item.replacements.length, 5)
|
|
}
|
|
}
|
|
})
|
|
|
|
for (const implementation of implementations) {
|
|
test(`${implementation.name}: historical stop during initialization is undone by late initialization`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
|
|
const pending = env.result.start()
|
|
await flushMask()
|
|
assert.equal(env.initGates.length, 1)
|
|
env.result.stop()
|
|
assert.equal(env.style.maskImage, 'none')
|
|
env.initGates[0].resolve()
|
|
await pending
|
|
await flushMask()
|
|
assert.equal(env.draws.length, 1)
|
|
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
|
|
assert.equal(env.frames.size, 1)
|
|
env.result.stop()
|
|
})
|
|
|
|
test(`${implementation.name}: historical inference completion after destroy writes a mask and revives RAF`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation, { pendingSegment: true })
|
|
await env.result.start()
|
|
await flushMask()
|
|
assert.equal(env.segmentGates.length, 1)
|
|
env.destroy()
|
|
env.segmentGates[0].resolve([{}])
|
|
await flushMask()
|
|
assert.equal(env.draws.length, 1)
|
|
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
|
|
assert.equal(env.frames.size, 1)
|
|
assert.equal(env.created[0].disposed, 0)
|
|
assert.deepEqual([...env.listeners].map(([name, callbacks]) => [name, callbacks.size]), [['ready', 1], ['destroy', 1]])
|
|
env.result.stop()
|
|
})
|
|
|
|
test(`${implementation.name}: historical repeated start leaves a second RAF chain after stop`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation)
|
|
await env.result.start()
|
|
await flushMask()
|
|
await env.result.start()
|
|
await flushMask()
|
|
assert.equal(env.created.length, 1)
|
|
assert.equal(env.frames.size, 2)
|
|
env.result.stop()
|
|
assert.equal(env.frames.size, 1)
|
|
await env.frame()
|
|
assert.equal(env.frames.size, 1)
|
|
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
|
|
env.result.stop()
|
|
})
|
|
}
|
|
|
|
for (const implementation of detailed) {
|
|
for (const action of ['stop', 'destroy']) {
|
|
test(`${implementation.name}: historical ${action} during initialization does not dispose the late model`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
|
|
const pending = env.result.start()
|
|
await flushMask()
|
|
if (action === 'stop')
|
|
env.result.stop()
|
|
else env.destroy()
|
|
env.initGates[0].resolve()
|
|
await pending
|
|
await flushMask()
|
|
assert.equal(env.created[0].disposed, 0)
|
|
assert.equal(env.frames.size, 1)
|
|
assert.equal(env.style.maskMode, 'alpha')
|
|
env.result.stop()
|
|
})
|
|
}
|
|
|
|
test(`${implementation.name}: historical stop during inference cannot prevent its pending write`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation, { pendingSegment: true })
|
|
await env.result.start()
|
|
env.result.stop()
|
|
assert.deepEqual(env.writes, ['none'])
|
|
env.segmentGates[0].resolve([{}])
|
|
await flushMask()
|
|
assert.deepEqual(env.writes, ['none', 'url(data:image/png;base64,historical-mask)'])
|
|
assert.equal(env.frames.size, 1)
|
|
env.result.stop()
|
|
})
|
|
|
|
test(`${implementation.name}: historical concurrent starts initialize two models and lose disposal ownership`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
|
|
const first = env.result.start()
|
|
const second = env.result.start()
|
|
await flushMask()
|
|
assert.equal(env.created.length, 2)
|
|
for (const gate of env.initGates) gate.resolve()
|
|
await Promise.all([first, second])
|
|
await flushMask()
|
|
assert.equal(env.frames.size, 2)
|
|
env.destroy()
|
|
assert.equal(env.frames.size, 1)
|
|
assert.deepEqual(env.created.map(instance => instance.disposed), [0, 0])
|
|
})
|
|
|
|
test(`${implementation.name}: historical inference rejection logs once per frame and keeps retrying`, async () => {
|
|
const error = new Error('controlled inference failure')
|
|
const env = danmukuMaskEnvironment(implementation, { segmentError: error })
|
|
await env.result.start()
|
|
await flushMask()
|
|
assert.equal(env.frames.size, 1)
|
|
assert.equal(env.logs.length, 1)
|
|
assert.deepEqual(env.logs[0], ['error', 'Error in segmentBody:', error])
|
|
assert.equal(env.draws.length, 0)
|
|
await env.frame()
|
|
assert.equal(env.logs.length, 2)
|
|
assert.equal(env.frames.size, 1)
|
|
env.settings.segmentError = null
|
|
await env.frame()
|
|
assert.equal(env.draws.length, 1)
|
|
env.result.stop()
|
|
})
|
|
|
|
test(`${implementation.name}: historical fulfilled false WebGL selection does not trigger CPU fallback`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation, { backendResult: { webgl: false } })
|
|
await env.result.start()
|
|
await flushMask()
|
|
assert.deepEqual(env.backends, ['webgl'])
|
|
assert.equal(env.created.length, 1)
|
|
assert.equal(env.logs.length, 0)
|
|
env.result.stop()
|
|
})
|
|
|
|
test(`${implementation.name}: historical rejected WebGL selection tries CPU and ignores its fulfilled boolean`, async () => {
|
|
for (const cpu of [true, false]) {
|
|
const env = danmukuMaskEnvironment(implementation, { backendError: { webgl: new Error('no WebGL') }, backendResult: { cpu } })
|
|
await env.result.start()
|
|
await flushMask()
|
|
assert.deepEqual(env.backends, ['webgl', 'cpu'])
|
|
assert.deepEqual(env.logs, [['warn', 'WebGL backend not available, falling back to CPU', 'no WebGL']])
|
|
assert.equal(env.created.length, 1)
|
|
env.result.stop()
|
|
}
|
|
})
|
|
|
|
test(`${implementation.name}: historical CPU rejection rejects explicit start before creating a model`, async () => {
|
|
const error = new Error('controlled CPU failure')
|
|
const env = danmukuMaskEnvironment(implementation, { backendError: { webgl: new Error('no WebGL'), cpu: error } })
|
|
await assert.rejects(env.result.start(), failure => failure === error)
|
|
assert.deepEqual(env.backends, ['webgl', 'cpu'])
|
|
assert.equal(env.created.length, 0)
|
|
assert.equal(env.frames.size, 0)
|
|
})
|
|
|
|
test(`${implementation.name}: historical model initialization rejection resolves start and polls without a model`, async () => {
|
|
const error = new Error('controlled model init failure')
|
|
const env = danmukuMaskEnvironment(implementation, { initError: error })
|
|
assert.equal(await env.result.start(), undefined)
|
|
assert.deepEqual(env.logs, [['error', 'Error initializing segmenter:', error]])
|
|
assert.equal(env.frames.size, 1)
|
|
await env.frame()
|
|
assert.equal(env.created.length, 1)
|
|
assert.equal(env.inferences.length, 0)
|
|
env.result.stop()
|
|
})
|
|
|
|
test(`${implementation.name}: historical missing danmuku rejects start after allocating the model and makes stop throw`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation, { noDanmuku: true })
|
|
await assert.rejects(env.result.start(), error => error.name === 'TypeError' && error.message.includes('style'))
|
|
assert.throws(() => env.result.stop(), error => error.name === 'TypeError' && error.message.includes('style'))
|
|
assert.equal(env.created.length, 1)
|
|
assert.equal(env.created[0].disposed, 0)
|
|
})
|
|
|
|
test(`${implementation.name}: historical unavailable 2D context logs read failures and keeps the frame loop`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation, { noContext: true })
|
|
await env.result.start()
|
|
await flushMask()
|
|
assert.equal(env.logs.length, 1)
|
|
assert.equal(env.logs[0][2].name, 'TypeError')
|
|
assert.match(env.logs[0][2].message, /getImageData/u)
|
|
assert.equal(env.frames.size, 1)
|
|
assert.equal(env.writes.length, 0)
|
|
env.result.stop()
|
|
})
|
|
|
|
test(`${implementation.name}: historical canvas allocation failure rejects start but leaves the initialized model owned`, async () => {
|
|
const error = new Error('controlled canvas allocation failure')
|
|
const env = danmukuMaskEnvironment(implementation, { canvasError: error })
|
|
await assert.rejects(env.result.start(), failure => failure === error)
|
|
assert.equal(env.created.length, 1)
|
|
assert.equal(env.created[0].disposed, 0)
|
|
assert.equal(env.frames.size, 0)
|
|
env.destroy()
|
|
assert.equal(env.created[0].disposed, 0)
|
|
})
|
|
|
|
test(`${implementation.name}: historical canvas read security failure is logged and retried without clearing the previous mask`, async () => {
|
|
const error = new Error('controlled canvas taint')
|
|
error.name = 'SecurityError'
|
|
const env = danmukuMaskEnvironment(implementation, { readError: error })
|
|
await env.result.start()
|
|
await flushMask()
|
|
assert.equal(env.logs[0][2], error)
|
|
assert.equal(env.style.maskImage, 'url(prior-mask)')
|
|
await env.frame()
|
|
assert.equal(env.logs.length, 2)
|
|
env.result.stop()
|
|
})
|
|
|
|
test(`${implementation.name}: historical zero RAF id survives stop and retained ready listeners restart after destroy`, async () => {
|
|
const env = danmukuMaskEnvironment(implementation, { firstFrame: 0, paused: true })
|
|
await env.result.start()
|
|
assert.deepEqual([...env.frames.keys()], [0])
|
|
env.destroy()
|
|
assert.deepEqual([...env.frames.keys()], [0])
|
|
assert.deepEqual(env.cancelled, [])
|
|
env.emit('ready')
|
|
await flushMask()
|
|
assert.equal(env.frames.size, 2)
|
|
assert.equal(env.created[0].disposed, 0)
|
|
})
|
|
|
|
for (const scenario of ['ready-backend', 'missing-video']) {
|
|
test(`${implementation.name}: historical ${scenario} unhandled rejection is observed only in a fatal isolated child`, () => {
|
|
const script = `import { danmukuMaskHistorical, danmukuMaskEnvironment, flushMask } from './test/helpers/danmuku-mask.js';
|
|
const implementation = (await danmukuMaskHistorical()).find(item => item.name === ${JSON.stringify(implementation.name)});
|
|
const settings = ${scenario === 'ready-backend' ? '{backendError: {webgl: new Error("no WebGL"), cpu: new Error("MASK02_READY_BACKEND_REJECTION")}}' : '{noVideo: true}'};
|
|
const env = danmukuMaskEnvironment(implementation, settings);
|
|
${scenario === 'ready-backend' ? 'env.emit("ready");' : 'await env.result.start();'}
|
|
await flushMask();`
|
|
const child = spawnSync(process.execPath, ['--unhandled-rejections=strict', '--input-type=module', '-e', script], { encoding: 'utf8', timeout: 15000 })
|
|
assert.equal(child.error, undefined)
|
|
assert.equal(child.status, 1)
|
|
assert.match(child.stderr, scenario === 'ready-backend' ? /MASK02_READY_BACKEND_REJECTION/u : /Cannot read properties of undefined \(reading 'paused'\)/u)
|
|
assert.doesNotMatch(child.stderr, /AssertionError|Model network access is forbidden/u)
|
|
})
|
|
}
|
|
}
|