test(danmuku): [PKG-DANMUKU-08] verify fullscreen lifetimes and old-core loads

This commit is contained in:
Harvey Zhao committed 2026-09-14 21:13:03 +08:00
1 parent b54a890832
commit 30363a5346
10 files changed
+15385 -170

No files matched your search

+10 -1
View File
@@ -219,7 +219,16 @@ row. CPU-block observations remain diagnostic, not no-loss acceptance. Both it a
`danmuku-lifetime.spec.js` accept the same artifact override. The latter verifies
the full visible lifetime after a controlled delay to an actual Worker request.
`danmuku-stability.spec.js` runs three native 14-second media cycles at 2 or 20
`danmuku-fullscreen.spec.js` covers old/current core and plugin combinations in
native document and CSS web fullscreen, with three fresh player lifetimes each.
It checks timestamp-delivered visible text, layout restoration, Worker/DOM cleanup
and instance-local event sequences. Frozen-core post-destroy listeners are an
explicit historical observation, never a waiver for the candidate. Native exit
must deliver its event before teardown; hidden measurement nodes are not visible
rows. `ARTPLAYER_DANMUKU_ARTIFACT` selects a real candidate main/legacy input.
`danmuku-stability.spec.js` covers old/current core and plugin combinations and
runs three native 14-second media cycles at 2 or 20
rows per second, appending through the public load(rows) contract. The candidate
must deliver and recycle every row once, preserve state membership and reuse
actual node identities. Its 90-second per-case deadline budgets the 42 seconds
+167
View File
@@ -0,0 +1,167 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { expect, test } from './fixtures.js'
const implementations = new Map()
test.beforeAll(async () => {
const release = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-release.json', 'utf8')).release
const member = 'package/dist/artplayer-plugin-danmuku.js'
const published = readMember(await ensureArchive(release), member)
assert.equal(hash(published), release.files[member])
implementations.set('published', { input: `npm ${release.version}`, code: published.toString() })
implementations.set('candidate', { input: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', code: process.env.ARTPLAYER_DANMUKU_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-danmuku', 'umd') })
})
for (const core of ['published', 'candidate']) {
for (const plugin of ['published', 'candidate']) {
for (const mode of ['web', 'native']) {
test(`${core} core / ${plugin} Danmuku: ${mode} fullscreen across three player lifetimes`, async ({ page }, testInfo) => {
test.setTimeout(45000) // Three independent players, native entry/exit and timestamp-delivered rows.
await page.goto(`/test/player.html?core=${core}`)
const input = implementations.get(plugin)
const evidence = { core, plugin, mode, input: input.input, sha256: hash(input.code), cycles: [], outcome: 'incomplete' }
await page.evaluate(() => {
window.fullscreenWorkers = []
window.fullscreenLifetimes = []
const NativeWorker = window.Worker
window.Worker = class extends NativeWorker {
constructor(...args) {
super(...args)
this.record = { replies: 0, terminated: 0 }
window.fullscreenWorkers.push(this.record)
this.addEventListener('message', () => this.record.replies++)
}
terminate() {
this.record.terminated++
return super.terminate()
}
}
})
await page.addScriptTag({ content: input.code })
try {
for (let cycle = 0; cycle < 3; cycle++) {
await page.evaluate(({ cycle, mode }) => {
window.fullscreenRows = []
window.fullscreenErrors = []
window.fullscreenModes = []
window.art = new window.Artplayer({
container: '.player',
url: '/assets/sample/video.mp4',
muted: true,
fullscreen: true,
fullscreenWeb: true,
plugins: [window.artplayerPluginDanmuku({ danmuku: [], heatmap: true, emitter: true, speed: 5 })],
})
window.fullscreenPlugin = window.art.plugins.artplayerPluginDanmuku
window.fullscreenOwner = window.fullscreenPlugin.config({})
const { art, fullscreenModes: modes, fullscreenRows: rows, fullscreenErrors: errors } = window
window.fullscreenLifetimes.push({ modes, rows, errors })
art.on(mode === 'native' ? 'fullscreen' : 'fullscreenWeb', active => modes.push(active))
art.on('artplayerPluginDanmuku:visible', row => rows.push({ text: row.text, time: art.currentTime }))
art.on('artplayerPluginDanmuku:error', error => errors.push(String(error)))
const toggle = document.createElement('button')
toggle.id = 'danmuku-fullscreen-toggle'
toggle.textContent = `Fullscreen ${cycle}`
toggle.style.cssText = 'position:absolute;top:10px;left:10px;z-index:9999'
toggle.onclick = () => {
if (mode === 'native')
window.art.fullscreen = !window.art.fullscreen
else
window.art.fullscreenWeb = !window.art.fullscreenWeb
}
window.art.template.$player.append(toggle)
document.querySelector('#play').onclick = () => window.art.play()
}, { cycle, mode })
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
const width = await page.evaluate(() => window.art.template.$player.clientWidth)
await page.click('#danmuku-fullscreen-toggle')
if (mode === 'native')
await expect.poll(() => page.evaluate(() => (document.fullscreenElement || document.webkitFullscreenElement) === window.art.template.$player)).toBe(true)
else
await expect.poll(() => page.evaluate(() => window.art.fullscreenWeb)).toBe(true)
const text = `Fullscreen row ${cycle}`
await page.evaluate(async (text) => {
await window.fullscreenPlugin.load([{ text, time: window.art.currentTime + 0.5, mode: 1 }])
}, text)
await expect.poll(() => page.evaluate(text => window.fullscreenRows.some(row => row.text === text), text)).toBe(true)
// The published renderer retains a hidden measurement node with the same text.
await expect(page.getByText(text, { exact: true }).and(page.locator(':visible'))).toBeVisible()
const entered = await page.evaluate(() => ({
width: window.art.template.$player.clientWidth,
layerWidth: window.art.template.$danmuku.clientWidth,
settings: document.querySelectorAll('.artplayer-plugin-danmuku').length,
heatmaps: document.querySelectorAll('.art-control-heatmap').length,
time: window.art.currentTime,
}))
expect(entered.width).toBeGreaterThan(width)
expect(entered.layerWidth).toBe(entered.width)
expect(entered.settings).toBe(1)
expect(entered.heatmaps).toBe(1)
await page.evaluate(() => window.art.pause())
await expect.poll(() => page.evaluate(() => window.art.video.paused)).toBe(true)
await page.click('#danmuku-fullscreen-toggle')
if (mode === 'native')
await expect.poll(() => page.evaluate(() => Boolean(document.fullscreenElement || document.webkitFullscreenElement))).toBe(false)
else
await expect.poll(() => page.evaluate(() => window.art.fullscreenWeb)).toBe(false)
await expect.poll(() => page.evaluate(() => window.art.template.$player.clientWidth)).toBe(width)
// Native exit can clear fullscreenElement before its change event is delivered.
await expect.poll(() => page.evaluate(() => window.fullscreenModes)).toEqual([true, false])
const after = await page.evaluate(() => {
const art = window.art
const owner = window.fullscreenOwner
const modes = [...window.fullscreenModes]
art.destroy()
return {
modes,
errors: window.fullscreenErrors,
roots: document.querySelectorAll('.art-video-player').length,
settings: document.querySelectorAll('.artplayer-plugin-danmuku').length,
heatmaps: document.querySelectorAll('.art-control-heatmap').length,
styles: document.querySelectorAll('style#artplayer-plugin-danmuku').length,
workers: window.fullscreenWorkers.map(worker => ({ ...worker })),
frame: owner.scheduler?.frame,
operation: owner.scheduler?.operation,
}
})
evidence.cycles.push({ cycle, entered, after })
expect(after.roots + after.settings + after.heatmaps).toBe(0)
expect(after.styles).toBe(1)
expect(after.errors).toEqual([])
expect(after.modes).toEqual([true, false])
expect(after.workers).toHaveLength(cycle + 1)
for (const worker of after.workers) {
expect(worker.replies).toBeGreaterThan(0)
expect(worker.terminated).toBe(1)
}
if (plugin === 'candidate') {
expect(after.frame).toBeNull()
expect(after.operation).toBeNull()
}
}
evidence.lifetimes = await page.evaluate(() => window.fullscreenLifetimes)
for (const [index, lifetime] of evidence.lifetimes.entries()) {
// BASE-LIFE-18: the frozen core keeps native listeners after destroy.
const changes = core === 'published' && mode === 'native' ? 3 - index : 1
expect(lifetime.modes).toEqual(Array.from({ length: changes }, () => [true, false]).flat())
expect(lifetime.errors).toEqual([])
}
evidence.outcome = 'three-lifetimes-fullscreen-playback-cleanup-passed'
}
finally {
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy()
})
await testInfo.attach('danmuku-fullscreen', { contentType: 'application/json', body: JSON.stringify(evidence) })
}
})
}
}
}
+167 -165
View File
@@ -17,178 +17,180 @@ test.beforeAll(async () => {
: await compilePackage('artplayer-plugin-danmuku', 'umd'))
})
for (const implementation of ['published', 'candidate']) {
for (const rate of [2, 20]) {
test(`${implementation}: ${rate} rows per second across three native playback cycles`, async ({ page }, testInfo) => {
// Three 14-second media cycles plus setup/teardown; this is a bounded soak,
// not the ordinary short browser test deadline or a synthetic clock jump.
test.setTimeout(90000)
await page.goto('/test/player.html?core=candidate')
const code = implementations.get(implementation)
await page.addScriptTag({ content: code })
await page.evaluate(() => window.createPlayer('/assets/sample/video.mp4'))
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.click('#pause')
await page.evaluate(() => {
const art = window.art
const evidence = window.stabilityEvidence = { workers: [], frames: [], samples: [], visible: [], recycled: [], callbacks: [], cycles: [], errors: [], allocations: 0, reuses: 0, bypassedPool: 0 }
const NativeWorker = window.Worker
window.Worker = class extends NativeWorker {
constructor(...args) {
super(...args)
this.record = { posts: 0, replies: 0, terminated: 0, errors: [] }
evidence.workers.push(this.record)
this.addEventListener('message', () => this.record.replies++)
this.addEventListener('error', event => this.record.errors.push(event.message))
}
for (const core of ['published', 'candidate']) {
for (const implementation of ['published', 'candidate']) {
for (const rate of [2, 20]) {
test(`${core} core / ${implementation}: ${rate} rows per second across three native playback cycles`, async ({ page }, testInfo) => {
// Three 14-second media cycles plus setup/teardown; this is a bounded soak,
// not the ordinary short browser test deadline or a synthetic clock jump.
test.setTimeout(90000)
await page.goto(`/test/player.html?core=${core}`)
const code = implementations.get(implementation)
await page.addScriptTag({ content: code })
await page.evaluate(() => window.createPlayer('/assets/sample/video.mp4'))
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.click('#pause')
await page.evaluate(() => {
const art = window.art
const evidence = window.stabilityEvidence = { workers: [], frames: [], samples: [], visible: [], recycled: [], callbacks: [], cycles: [], errors: [], allocations: 0, reuses: 0, bypassedPool: 0 }
const NativeWorker = window.Worker
window.Worker = class extends NativeWorker {
constructor(...args) {
super(...args)
this.record = { posts: 0, replies: 0, terminated: 0, errors: [] }
evidence.workers.push(this.record)
this.addEventListener('message', () => this.record.replies++)
this.addEventListener('error', event => this.record.errors.push(event.message))
}
postMessage(...args) {
this.record.posts++
return super.postMessage(...args)
}
postMessage(...args) {
this.record.posts++
return super.postMessage(...args)
}
terminate() {
this.record.terminated++
return super.terminate()
terminate() {
this.record.terminated++
return super.terminate()
}
}
art.on('artplayerPluginDanmuku:error', error => evidence.errors.push(String(error)))
art.on('artplayerPluginDanmuku:visible', row => evidence.visible.push({ id: row.id, time: art.currentTime, wall: performance.now() }))
art.plugins.add(window.artplayerPluginDanmuku({
danmuku: [],
heatmap: false,
emitter: false,
speed: 1,
antiOverlap: false,
beforeVisible(row) {
evidence.callbacks.push({ id: row.id, time: art.currentTime })
return true
},
}))
const owner = window.stabilityOwner = art.plugins.artplayerPluginDanmuku.config({})
const acquire = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), '$ref').get
const acquired = new WeakSet()
Object.defineProperty(owner, '$ref', { configurable: true, get() {
const available = new Set(this.$refs)
const ref = acquire.call(this)
if (available.size && !available.has(ref))
evidence.bypassedPool++
if (acquired.has(ref))
evidence.reuses++
else evidence.allocations++
acquired.add(ref)
return ref
} })
const getter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), 'readys').get
Object.defineProperty(owner, 'readys', { configurable: true, get() {
const rows = getter.call(this)
evidence.samples.push({ time: art.currentTime, wall: performance.now(), ids: rows.map(row => row.id) })
return rows
} })
const makeWait = owner.makeWait.bind(owner)
owner.makeWait = (row) => {
if (row.$state === 'emit')
evidence.recycled.push({ id: row.id, time: art.currentTime })
return makeWait(row)
}
const frame = (wall) => {
if (art.playing)
evidence.frames.push({ wall, time: art.currentTime })
window.stabilityFrame = requestAnimationFrame(frame)
}
}
art.on('artplayerPluginDanmuku:error', error => evidence.errors.push(String(error)))
art.on('artplayerPluginDanmuku:visible', row => evidence.visible.push({ id: row.id, time: art.currentTime, wall: performance.now() }))
art.plugins.add(window.artplayerPluginDanmuku({
danmuku: [],
heatmap: false,
emitter: false,
speed: 1,
antiOverlap: false,
beforeVisible(row) {
evidence.callbacks.push({ id: row.id, time: art.currentTime })
return true
},
}))
const owner = window.stabilityOwner = art.plugins.artplayerPluginDanmuku.config({})
const acquire = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), '$ref').get
const acquired = new WeakSet()
Object.defineProperty(owner, '$ref', { configurable: true, get() {
const available = new Set(this.$refs)
const ref = acquire.call(this)
if (available.size && !available.has(ref))
evidence.bypassedPool++
if (acquired.has(ref))
evidence.reuses++
else evidence.allocations++
acquired.add(ref)
return ref
} })
const getter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), 'readys').get
Object.defineProperty(owner, 'readys', { configurable: true, get() {
const rows = getter.call(this)
evidence.samples.push({ time: art.currentTime, wall: performance.now(), ids: rows.map(row => row.id) })
return rows
} })
const makeWait = owner.makeWait.bind(owner)
owner.makeWait = (row) => {
if (row.$state === 'emit')
evidence.recycled.push({ id: row.id, time: art.currentTime })
return makeWait(row)
}
const frame = (wall) => {
if (art.playing)
evidence.frames.push({ wall, time: art.currentTime })
window.stabilityFrame = requestAnimationFrame(frame)
}
window.stabilityFrame = requestAnimationFrame(frame)
})
try {
for (let cycle = 0; cycle < 3; cycle++) {
await page.evaluate(async ({ cycle, rate }) => {
const art = window.art
const base = art.currentTime
const rows = Array.from({ length: 12 * rate }, (_, index) => ({ id: `${cycle}-${index}`, text: `Row ${index}`, time: base + 0.5 + index / rate, mode: index % 3 }))
await art.plugins.artplayerPluginDanmuku.load(rows)
window.stabilityEnd = base + 14
window.stabilityEvidence.cycles.push({ cycle, base, rows: rows.map(row => ({ id: row.id, time: row.time })), startWall: performance.now() })
}, { cycle, rate })
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.currentTime >= window.stabilityEnd), { timeout: 22000 }).toBe(true)
await page.click('#pause')
const snapshot = await page.evaluate(() => {
const owner = window.stabilityOwner
const record = window.stabilityEvidence.cycles.at(-1)
const ids = new Set(record.rows.map(row => row.id))
const visible = window.stabilityEvidence.visible.filter(row => ids.has(row.id))
const sampled = new Set(window.stabilityEvidence.samples.flatMap(sample => sample.ids).filter(id => ids.has(id)))
const shown = new Set(visible.map(row => row.id))
const refs = owner.queue.map(row => row.$ref).filter(Boolean)
const queue = new Set(owner.queue)
const members = Object.values(owner.states).flat()
const memory = performance.memory
Object.assign(record, {
endWall: performance.now(),
time: window.art.currentTime,
decodedWidth: window.art.video.videoWidth,
visibleCount: visible.length,
uniqueVisible: shown.size,
missedSampling: record.rows.filter(row => !sampled.has(row.id)).map(row => row.id),
sampledNotVisible: record.rows.filter(row => sampled.has(row.id) && !shown.has(row.id)).map(row => row.id),
queue: owner.queue.length,
states: Object.fromEntries(Object.entries(owner.states).map(([state, rows]) => [state, rows.length])),
nodes: owner.$danmuku.children.length,
pooled: owner.$refs.length,
uniquePool: new Set(owner.$refs).size,
referenced: refs.length,
distinctReferences: new Set(refs).size,
uniqueStateMembers: new Set(members).size,
validStateMembers: Object.entries(owner.states).every(([state, rows]) => rows.every(row => queue.has(row) && row.$state === state)),
heap: memory ? { used: memory.usedJSHeapSize, total: memory.totalJSHeapSize } : null,
})
try {
for (let cycle = 0; cycle < 3; cycle++) {
await page.evaluate(async ({ cycle, rate }) => {
const art = window.art
const base = art.currentTime
const rows = Array.from({ length: 12 * rate }, (_, index) => ({ id: `${cycle}-${index}`, text: `Row ${index}`, time: base + 0.5 + index / rate, mode: index % 3 }))
await art.plugins.artplayerPluginDanmuku.load(rows)
window.stabilityEnd = base + 14
window.stabilityEvidence.cycles.push({ cycle, base, rows: rows.map(row => ({ id: row.id, time: row.time })), startWall: performance.now() })
}, { cycle, rate })
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.currentTime >= window.stabilityEnd), { timeout: 22000 }).toBe(true)
await page.click('#pause')
const snapshot = await page.evaluate(() => {
const owner = window.stabilityOwner
const record = window.stabilityEvidence.cycles.at(-1)
const ids = new Set(record.rows.map(row => row.id))
const visible = window.stabilityEvidence.visible.filter(row => ids.has(row.id))
const sampled = new Set(window.stabilityEvidence.samples.flatMap(sample => sample.ids).filter(id => ids.has(id)))
const shown = new Set(visible.map(row => row.id))
const refs = owner.queue.map(row => row.$ref).filter(Boolean)
const queue = new Set(owner.queue)
const members = Object.values(owner.states).flat()
const memory = performance.memory
Object.assign(record, {
endWall: performance.now(),
time: window.art.currentTime,
decodedWidth: window.art.video.videoWidth,
visibleCount: visible.length,
uniqueVisible: shown.size,
missedSampling: record.rows.filter(row => !sampled.has(row.id)).map(row => row.id),
sampledNotVisible: record.rows.filter(row => sampled.has(row.id) && !shown.has(row.id)).map(row => row.id),
queue: owner.queue.length,
states: Object.fromEntries(Object.entries(owner.states).map(([state, rows]) => [state, rows.length])),
nodes: owner.$danmuku.children.length,
pooled: owner.$refs.length,
uniquePool: new Set(owner.$refs).size,
referenced: refs.length,
distinctReferences: new Set(refs).size,
uniqueStateMembers: new Set(members).size,
validStateMembers: Object.entries(owner.states).every(([state, rows]) => rows.every(row => queue.has(row) && row.$state === state)),
heap: memory ? { used: memory.usedJSHeapSize, total: memory.totalJSHeapSize } : null,
})
return record
})
return record
})
expect(snapshot.decodedWidth).toBeGreaterThan(0)
expect(snapshot.time - snapshot.base).toBeGreaterThanOrEqual(14)
// load(rows) appends by public contract; old rows stay in the queue.
expect(snapshot.queue).toBe((cycle + 1) * 12 * rate)
expect(Object.values(snapshot.states).reduce((sum, count) => sum + count, 0)).toBe(snapshot.queue)
if (implementation === 'candidate') {
expect(snapshot.missedSampling).toEqual([])
expect(snapshot.sampledNotVisible).toEqual([])
expect(snapshot.visibleCount).toBe(12 * rate)
expect(snapshot.uniqueVisible).toBe(12 * rate)
expect(snapshot.referenced).toBe(0)
expect(snapshot.uniquePool).toBe(snapshot.pooled)
expect(snapshot.nodes).toBe(snapshot.pooled)
expect(snapshot.uniqueStateMembers).toBe(snapshot.queue)
expect(snapshot.validStateMembers).toBe(true)
expect(snapshot.decodedWidth).toBeGreaterThan(0)
expect(snapshot.time - snapshot.base).toBeGreaterThanOrEqual(14)
// load(rows) appends by public contract; old rows stay in the queue.
expect(snapshot.queue).toBe((cycle + 1) * 12 * rate)
expect(Object.values(snapshot.states).reduce((sum, count) => sum + count, 0)).toBe(snapshot.queue)
if (implementation === 'candidate') {
expect(snapshot.missedSampling).toEqual([])
expect(snapshot.sampledNotVisible).toEqual([])
expect(snapshot.visibleCount).toBe(12 * rate)
expect(snapshot.uniqueVisible).toBe(12 * rate)
expect(snapshot.referenced).toBe(0)
expect(snapshot.uniquePool).toBe(snapshot.pooled)
expect(snapshot.nodes).toBe(snapshot.pooled)
expect(snapshot.uniqueStateMembers).toBe(snapshot.queue)
expect(snapshot.validStateMembers).toBe(true)
}
}
}
}
finally {
const evidence = await page.evaluate(() => {
cancelAnimationFrame(window.stabilityFrame)
window.art.destroy()
return { ...window.stabilityEvidence, destroyed: window.art.isDestroy, retainedPlayerRoots: document.querySelectorAll('.art-video-player').length }
})
await testInfo.attach('danmuku-stability-observation', { contentType: 'application/json', body: JSON.stringify({ implementation, rate, sha256: hash(code), evidence, scope: 'Three bounded real media cycles; published defects are observed, candidate delivery and resource ownership are asserted. Optional unforced heap readings are not leak proof or comparable across engines.' }) })
expect(evidence.destroyed).toBe(true)
expect(evidence.retainedPlayerRoots).toBe(0)
expect(evidence.errors).toEqual([])
expect(evidence.workers).toHaveLength(1)
if (implementation === 'candidate' && evidence.cycles.length === 3) {
const expected = evidence.cycles.flatMap(cycle => cycle.rows.map(row => row.id)).sort()
expect(evidence.visible.map(row => row.id).sort()).toEqual(expected)
expect(evidence.recycled.map(row => row.id).sort()).toEqual(expected)
expect(evidence.bypassedPool).toBe(0)
expect(evidence.reuses).toBeGreaterThan(0)
expect(evidence.allocations + evidence.reuses).toBe(expected.length)
expect(evidence.cycles.at(-1).nodes).toBe(evidence.allocations)
finally {
const evidence = await page.evaluate(() => {
cancelAnimationFrame(window.stabilityFrame)
window.art.destroy()
return { ...window.stabilityEvidence, destroyed: window.art.isDestroy, retainedPlayerRoots: document.querySelectorAll('.art-video-player').length }
})
await testInfo.attach('danmuku-stability-observation', { contentType: 'application/json', body: JSON.stringify({ core, implementation, rate, sha256: hash(code), evidence, scope: 'Three bounded real media cycles; published defects are observed, candidate delivery and resource ownership are asserted. Optional unforced heap readings are not leak proof or comparable across engines.' }) })
expect(evidence.destroyed).toBe(true)
expect(evidence.retainedPlayerRoots).toBe(0)
expect(evidence.errors).toEqual([])
expect(evidence.workers).toHaveLength(1)
if (implementation === 'candidate' && evidence.cycles.length === 3) {
const expected = evidence.cycles.flatMap(cycle => cycle.rows.map(row => row.id)).sort()
expect(evidence.visible.map(row => row.id).sort()).toEqual(expected)
expect(evidence.recycled.map(row => row.id).sort()).toEqual(expected)
expect(evidence.bypassedPool).toBe(0)
expect(evidence.reuses).toBeGreaterThan(0)
expect(evidence.allocations + evidence.reuses).toBe(expected.length)
expect(evidence.cycles.at(-1).nodes).toBe(evidence.allocations)
}
for (const worker of evidence.workers) {
expect(worker.terminated).toBe(1)
expect(worker.errors).toEqual([])
if (implementation === 'candidate')
expect(worker.posts).toBe(worker.replies)
}
}
for (const worker of evidence.workers) {
expect(worker.terminated).toBe(1)
expect(worker.errors).toEqual([])
if (implementation === 'candidate')
expect(worker.posts).toBe(worker.replies)
}
}
})
})
}
}
}