mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-24 14:41:20 -08:00
test(danmuku): [PKG-DANMUKU-02] establish failure browser and load baselines
This commit is contained in:
1 parent
ccf3d6ed14
commit
d3c91af92c
14 files changed
+8508
-15
No files matched your search
@@ -0,0 +1,278 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Historical failures are explicit Node observations.
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { danmukuEnvironment, danmukuHistorical, deferred } from './helpers/danmuku.js'
|
||||
|
||||
const implementations = await danmukuHistorical()
|
||||
const cache = fileURLToPath(new URL('../refactor/.cache/danmuku02-controlled/', import.meta.url))
|
||||
fs.mkdirSync(cache, { recursive: true })
|
||||
const evidenceDirectory = fs.mkdtempSync(path.join(cache, 'run-'))
|
||||
|
||||
function isolated(implementation, scenario) {
|
||||
const program = `
|
||||
import fs from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import { danmukuEnvironment } from './test/helpers/danmuku.js';
|
||||
const implementation = JSON.parse(fs.readFileSync(0, 'utf8'));
|
||||
const scenario = ${JSON.stringify(scenario)};
|
||||
let settled = false;
|
||||
let env;
|
||||
process.on('uncaughtExceptionMonitor', (error, origin) => {
|
||||
process.stdout.write(JSON.stringify({ stage: 'fatal', origin, error: error.message, settled, errors: env?.events.filter(e => e.name === 'artplayerPluginDanmuku:error').length }) + '\\n');
|
||||
});
|
||||
if (scenario === 'heatmap-zero' || scenario === 'heatmap-positive') {
|
||||
env = danmukuEnvironment(implementation, { heatmapWidth: 99 });
|
||||
env.factory({ danmuku: [], heatmap: scenario === 'heatmap-positive' ? { sampling: 1 } : true })(env.art);
|
||||
const sandbox = vm.createContext({ run: () => env.art.emit('ready') });
|
||||
process.stdout.write('heatmap-ready\\n');
|
||||
try {
|
||||
vm.runInContext('run()', sandbox, { timeout: 100 });
|
||||
process.stdout.write(JSON.stringify({ stage: 'complete' }) + '\\n');
|
||||
} catch (error) {
|
||||
if (error.code !== 'ERR_SCRIPT_EXECUTION_TIMEOUT') throw error;
|
||||
process.stdout.write(JSON.stringify({ stage: 'bounded-timeout', code: error.code }) + '\\n');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
if (scenario === 'beforeVisible-reject') {
|
||||
env = danmukuEnvironment(implementation);
|
||||
const plugin = env.factory({ danmuku: [], beforeVisible: () => Promise.reject(new Error('beforeVisible-original')) })(env.art);
|
||||
await plugin.emit({ text: 'visible', time: 10 });
|
||||
env.art.playing = true;
|
||||
env.art.emit('video:play');
|
||||
// Browser rAF ignores a callback's returned Promise. Do not observe it here.
|
||||
env.frame();
|
||||
} else {
|
||||
env = danmukuEnvironment(implementation, { fetch: async () => {
|
||||
if (scenario === 'fetch-reject') throw new Error('fetch-original');
|
||||
return { text: async () => { throw new Error('text-original'); } };
|
||||
} });
|
||||
const plugin = env.factory({ danmuku: [] })(env.art);
|
||||
plugin.load('/controlled.xml').then(() => settled = true, () => settled = true);
|
||||
}
|
||||
setTimeout(() => process.stdout.write('unexpected-survival\\n'), 200);
|
||||
`
|
||||
const child = spawnSync(process.execPath, ['--unhandled-rejections=strict', '--input-type=module', '-e', program], {
|
||||
cwd: fileURLToPath(new URL('../', import.meta.url)),
|
||||
input: JSON.stringify(implementation),
|
||||
encoding: 'utf8',
|
||||
timeout: 3000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
})
|
||||
const evidence = { implementation: implementation.name, scenario, status: child.status, signal: child.signal, errorCode: child.error?.code ?? null, stdout: child.stdout, stderr: child.stderr }
|
||||
fs.writeFileSync(path.join(evidenceDirectory, `${implementation.name}-${scenario}.json`), `${JSON.stringify(evidence, null, 2)}\n`)
|
||||
assert.equal(child.error, undefined, `Isolated process infrastructure failed: ${child.error?.message}`)
|
||||
return evidence
|
||||
}
|
||||
|
||||
for (const implementation of implementations) {
|
||||
for (const scenario of ['fetch-reject', 'text-reject']) {
|
||||
test(`danmuku ${implementation.name}: historical ${scenario} crashes the isolated process while public load stays pending`, () => {
|
||||
const evidence = isolated(implementation, scenario)
|
||||
assert.equal(evidence.status, 1)
|
||||
const record = JSON.parse(evidence.stdout.trim())
|
||||
assert.equal(record.stage, 'fatal')
|
||||
assert.equal(record.origin, 'unhandledRejection')
|
||||
assert.equal(record.settled, false)
|
||||
assert.equal(record.errors, 0)
|
||||
assert.equal(record.error, scenario === 'fetch-reject' ? 'fetch-original' : 'text-original')
|
||||
})
|
||||
}
|
||||
|
||||
test(`danmuku ${implementation.name}: historical beforeVisible rejection is an unobserved animation callback Promise`, () => {
|
||||
const evidence = isolated(implementation, 'beforeVisible-reject')
|
||||
assert.equal(evidence.status, 1)
|
||||
const record = JSON.parse(evidence.stdout.trim())
|
||||
assert.equal(record.error, 'beforeVisible-original')
|
||||
assert.equal(record.origin, 'unhandledRejection')
|
||||
assert.equal(record.errors, 0)
|
||||
})
|
||||
|
||||
test(`danmuku ${implementation.name}: historical heatmap width 99 blocks at default sampling zero but sampling 1 completes`, () => {
|
||||
const zero = isolated(implementation, 'heatmap-zero')
|
||||
const positive = isolated(implementation, 'heatmap-positive')
|
||||
assert.equal(zero.status, 0)
|
||||
assert.equal(positive.status, 0)
|
||||
assert.match(zero.stdout, /heatmap-ready\n\{"stage":"bounded-timeout","code":"ERR_SCRIPT_EXECUTION_TIMEOUT"\}/u)
|
||||
assert.match(positive.stdout, /heatmap-ready\n\{"stage":"complete"\}/u)
|
||||
})
|
||||
|
||||
for (const event of ['error', 'empty-xml']) {
|
||||
test(`danmuku ${implementation.name}: historical Bilibili ${event} leaves loading pending and parser worker alive`, async () => {
|
||||
const env = danmukuEnvironment(implementation, event === 'empty-xml' ? { fetch: async () => ({ text: async () => '' }) } : {})
|
||||
const plugin = env.factory({ danmuku: [] })(env.art)
|
||||
let settled = false
|
||||
plugin.load('/controlled.xml').then(() => settled = true, () => settled = true)
|
||||
await env.flush()
|
||||
assert.equal(env.workers.length, 2)
|
||||
const worker = env.workers[1]
|
||||
assert.equal(worker.messages.length, 1)
|
||||
if (event === 'error') {
|
||||
assert.equal(worker.onerror, undefined)
|
||||
worker.fail(new Error('parser worker failure'))
|
||||
}
|
||||
else {
|
||||
assert.equal(worker.messages[0].message.xml, '')
|
||||
// Execute the actual Blob, not a reimplementation of its empty-input branch.
|
||||
await worker.execute(worker.messages[0].message)
|
||||
}
|
||||
await env.flush()
|
||||
assert.equal(settled, false)
|
||||
env.destroy()
|
||||
assert.equal(worker.terminated, false, 'Parser worker is not owned by the Danmuku destroy path')
|
||||
})
|
||||
}
|
||||
|
||||
test(`danmuku ${implementation.name}: historical successful parser leaves its Blob URL allocated`, async () => {
|
||||
const env = danmukuEnvironment(implementation)
|
||||
const plugin = env.factory({ danmuku: [] })(env.art)
|
||||
const pending = plugin.load('/controlled.xml')
|
||||
await env.flush()
|
||||
const worker = env.workers[1]
|
||||
await worker.execute(worker.messages[0].message)
|
||||
const internal = await pending
|
||||
assert.equal(internal.queue.length, 1)
|
||||
assert.equal(internal.queue[0].text, 'xml')
|
||||
assert.equal(worker.terminated, true)
|
||||
assert.equal(env.urls.has(worker.url), true)
|
||||
assert.equal(env.revoked.includes(worker.url), false)
|
||||
env.destroy()
|
||||
})
|
||||
|
||||
test(`danmuku ${implementation.name}: historical parser worker completion emits new data after player destruction`, async () => {
|
||||
const env = danmukuEnvironment(implementation)
|
||||
const plugin = env.factory({ danmuku: [] })(env.art)
|
||||
const pending = plugin.load('/controlled.xml')
|
||||
await env.flush()
|
||||
const worker = env.workers[1]
|
||||
env.destroy()
|
||||
const count = env.events.length
|
||||
assert.equal(worker.terminated, false)
|
||||
await worker.execute(worker.messages[0].message)
|
||||
const internal = await pending
|
||||
assert.equal(internal.queue[0].text, 'xml')
|
||||
assert.deepEqual(env.events.slice(count).map(event => event.name), ['artplayerPluginDanmuku:loaded'])
|
||||
assert.equal(worker.terminated, true)
|
||||
})
|
||||
|
||||
test(`danmuku ${implementation.name}: historical overlapping replacement loads let the oldest completion overwrite newer data`, async () => {
|
||||
const env = danmukuEnvironment(implementation)
|
||||
const plugin = env.factory({ danmuku: [] })(env.art)
|
||||
const first = deferred()
|
||||
const second = deferred()
|
||||
const jobs = [first, second]
|
||||
// Change an additional scalar because the historical function-only config bug is separate.
|
||||
plugin.config({ danmuku: () => jobs.shift().promise, opacity: 0.9 })
|
||||
const older = plugin.load()
|
||||
const newer = plugin.load()
|
||||
second.resolve([{ text: 'newer', time: 10 }])
|
||||
const internal = await newer
|
||||
assert.deepEqual(Array.from(internal.queue, item => item.text), ['newer'])
|
||||
first.resolve([{ text: 'older', time: 10 }])
|
||||
await older
|
||||
assert.deepEqual(Array.from(internal.queue, item => item.text), ['older'])
|
||||
env.destroy()
|
||||
})
|
||||
|
||||
test(`danmuku ${implementation.name}: historical asynchronous input mutates queue and emits loaded after destroy`, async () => {
|
||||
const env = danmukuEnvironment(implementation)
|
||||
const plugin = env.factory({ danmuku: [] })(env.art)
|
||||
const input = deferred()
|
||||
const pending = plugin.load(() => input.promise)
|
||||
env.destroy()
|
||||
const count = env.events.length
|
||||
input.resolve([{ text: 'late', time: 10 }])
|
||||
const internal = await pending
|
||||
assert.deepEqual(Array.from(internal.queue, item => item.text), ['late'])
|
||||
assert.deepEqual(env.events.slice(count).map(event => event.name), ['artplayerPluginDanmuku:loaded'])
|
||||
})
|
||||
|
||||
test(`danmuku ${implementation.name}: historical pending beforeVisible allocates DOM and posts to a terminated worker after destroy`, async () => {
|
||||
const visible = deferred()
|
||||
const env = danmukuEnvironment(implementation)
|
||||
const plugin = env.factory({ danmuku: [], beforeVisible: () => visible.promise })(env.art)
|
||||
await plugin.emit({ text: 'late visible', time: 10 })
|
||||
env.art.playing = true
|
||||
env.art.emit('video:play')
|
||||
env.frame()
|
||||
await env.flush()
|
||||
assert.equal(env.art.template.$danmuku.children.length, 0)
|
||||
env.destroy()
|
||||
visible.resolve(true)
|
||||
await env.flush()
|
||||
assert.equal(env.art.template.$danmuku.children.length, 1)
|
||||
assert.equal(env.workers[0].messages.length, 1)
|
||||
assert.equal(env.workers[0].messages[0].afterTerminate, true)
|
||||
assert.equal(env.events.filter(event => event.name === 'artplayerPluginDanmuku:visible').length, 0)
|
||||
})
|
||||
|
||||
test(`danmuku ${implementation.name}: historical worker response continuation still mutates state after destroy`, async () => {
|
||||
const env = danmukuEnvironment(implementation)
|
||||
const plugin = env.factory({ danmuku: [] })(env.art)
|
||||
await plugin.emit({ text: 'pending position', time: 10 })
|
||||
env.art.playing = true
|
||||
env.art.emit('video:play')
|
||||
const frame = env.frame()
|
||||
await env.flush()
|
||||
const worker = env.workers[0]
|
||||
const { id } = worker.messages[0].message
|
||||
const internal = plugin.show()
|
||||
// Delivery starts before terminate; its awaiting rAF continuation runs afterward.
|
||||
// This does not require a native Worker to dispatch a new event after terminate.
|
||||
worker.deliver({ id, result: 10 })
|
||||
env.destroy()
|
||||
assert.equal(internal.queue[0].$state, 'wait')
|
||||
await frame
|
||||
assert.equal(internal.queue[0].$state, 'ready')
|
||||
assert.equal(internal.queue[0].$ref, null)
|
||||
assert.equal(env.frames.size, 0)
|
||||
assert.equal(env.events.filter(event => event.name === 'artplayerPluginDanmuku:visible').length, 0)
|
||||
})
|
||||
|
||||
test(`danmuku ${implementation.name}: historical play and playing schedule duplicate loops and overwrite pending worker response handlers`, async () => {
|
||||
const env = danmukuEnvironment(implementation)
|
||||
const plugin = env.factory({ danmuku: [] })(env.art)
|
||||
await plugin.emit({ text: 'two loops', time: 10 })
|
||||
env.art.playing = true
|
||||
env.art.emit('video:play')
|
||||
env.art.emit('video:playing')
|
||||
assert.equal(env.frames.size, 2)
|
||||
let firstSettled = false
|
||||
env.frame().then(() => firstSettled = true)
|
||||
await env.flush()
|
||||
env.tick(1)
|
||||
const second = env.frame()
|
||||
await env.flush()
|
||||
const worker = env.workers[0]
|
||||
assert.equal(worker.messages.length, 2)
|
||||
const firstId = worker.messages[0].message.id
|
||||
const secondId = worker.messages[1].message.id
|
||||
assert.notEqual(firstId, secondId)
|
||||
worker.deliver({ id: firstId, result: 10 })
|
||||
await env.flush()
|
||||
assert.equal(firstSettled, false)
|
||||
worker.deliver({ id: secondId, result: 10 })
|
||||
await second
|
||||
assert.equal(firstSettled, false)
|
||||
assert.equal(env.events.filter(event => event.name === 'artplayerPluginDanmuku:visible').length, 1)
|
||||
env.destroy()
|
||||
})
|
||||
|
||||
test(`danmuku ${implementation.name}: historical duplicate start then pause cancels only the latest scheduled frame`, () => {
|
||||
const env = danmukuEnvironment(implementation)
|
||||
env.factory({ danmuku: [] })(env.art)
|
||||
env.art.emit('video:play')
|
||||
env.art.emit('video:playing')
|
||||
env.art.emit('video:pause')
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.destroy()
|
||||
assert.equal(env.frames.size, 1)
|
||||
})
|
||||
}
|
||||
|
||||
process.stdout.write(`Danmuku controlled subprocess evidence: ${evidenceDirectory}\n`)
|
||||
Reference in new issue
Block a user