mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-14 08:26:16 -08:00
test(danmuku-mask): [PKG-MASK-01] freeze published contracts and model provenance
This commit is contained in:
1 parent
8ec9fbe83a
commit
7d44bce06f
13 files changed
+1138
-13
No files matched your search
@@ -0,0 +1,107 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import lockfile from '@yarnpkg/lockfile'
|
||||
import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
|
||||
|
||||
const workspace = path.dirname(refactorDir.replace(/[\\/]$/u, ''))
|
||||
const prefix = 'packages/artplayer-plugin-danmuku-mask/'
|
||||
function gitBytes(commit, file) {
|
||||
return execFileSync('git', ['show', `${commit}:${file}`], { cwd: workspace, maxBuffer: 32 * 1024 * 1024 })
|
||||
}
|
||||
function gitText(commit, file) {
|
||||
return gitBytes(commit, file).toString().replaceAll('\r\n', '\n')
|
||||
}
|
||||
|
||||
export async function verifyDanmukuMaskContract() {
|
||||
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/danmuku-mask-release.json'), 'utf8'))
|
||||
const registryBytes = fs.readFileSync(path.join(refactorDir, 'baselines/danmuku-mask-registry.json'))
|
||||
assert.equal(hash(registryBytes), baseline.registrySnapshot.bodySha256)
|
||||
const registry = JSON.parse(registryBytes)
|
||||
assert.deepEqual(registry['dist-tags'], baseline.registrySnapshot.distTags)
|
||||
assert.deepEqual(Object.keys(registry.versions).filter(version => /^\d+\.\d+\.\d+$/u.test(version)), baseline.registrySnapshot.stableVersions)
|
||||
const archives = new Map()
|
||||
for (const release of [baseline.release, ...baseline.previous]) {
|
||||
const metadata = registry.versions[release.version]
|
||||
assert.equal(metadata.dist.integrity, release.integrity)
|
||||
assert.equal(metadata.dist.tarball, release.tarball)
|
||||
assert.equal(metadata.gitHead, release.registryGitHead)
|
||||
const archive = await ensureArchive(release)
|
||||
assert.deepEqual(archiveFiles(archive), Object.keys(release.files).sort())
|
||||
for (const [file, expected] of Object.entries(release.files))
|
||||
assert.equal(hash(readMember(archive, file)), expected, `${release.version}/${file}`)
|
||||
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
|
||||
assert.deepEqual(manifest, release.manifest)
|
||||
assert.equal(manifest.version, release.version)
|
||||
assert.equal(manifest.name, release.name)
|
||||
for (const [field, target] of Object.entries(release.entrypoints)) {
|
||||
assert.equal(manifest[field] ?? null, target)
|
||||
if (target)
|
||||
assert(Object.hasOwn(release.files, `package/${target.replace(/^\.\//u, '')}`), `${release.version}/${field}`)
|
||||
}
|
||||
function targetExists(target) {
|
||||
if (typeof target === 'string')
|
||||
assert(Object.hasOwn(release.files, `package/${target.replace(/^\.\//u, '')}`))
|
||||
else if (target && typeof target === 'object')
|
||||
Object.values(target).forEach(targetExists)
|
||||
}
|
||||
targetExists(manifest.exports)
|
||||
for (const association of [release.registrySourceManifest, release.historicalCore]) {
|
||||
assert.equal(association.commit, release.registryGitHead)
|
||||
const text = gitText(association.commit, association.file)
|
||||
assert.equal(hash(text), association.sha256LF)
|
||||
assert.equal(JSON.parse(text).version, association.version)
|
||||
}
|
||||
assert.equal(release.registrySourceManifest.versionMatchesRelease, release.registrySourceManifest.version === release.version)
|
||||
for (const comparison of release.registrySourceComparison) {
|
||||
const text = gitText(release.registryGitHead, comparison.file)
|
||||
assert.equal(hash(text), comparison.sha256LF)
|
||||
assert.equal(comparison.matchesArchiveBytes, hash(text) === release.files[`package/${comparison.file.slice(prefix.length)}`])
|
||||
}
|
||||
archives.set(release.version, archive)
|
||||
}
|
||||
assert.deepEqual([...archives.keys()].sort(), [...baseline.registrySnapshot.stableVersions].sort())
|
||||
const sources = new Map()
|
||||
for (const [file, expected] of Object.entries(baseline.source)) {
|
||||
const text = gitText(baseline.sourceCommit, file)
|
||||
assert.equal(hash(text), expected, file)
|
||||
sources.set(file, text)
|
||||
}
|
||||
for (const file of baseline.observations.latestArchiveMatchesFrozenSourceFiles)
|
||||
assert.equal(hash(sources.get(file)), baseline.release.files[`package/${file.slice(prefix.length)}`])
|
||||
const locked = gitText(baseline.sdk.lock.commit, baseline.sdk.lock.file)
|
||||
assert.equal(hash(locked), baseline.sdk.lock.sha256LF)
|
||||
const parsed = lockfile.parse(locked)
|
||||
assert.equal(parsed.type, 'success')
|
||||
const manifest = JSON.parse(sources.get(`${prefix}package.json`))
|
||||
for (const dependency of baseline.sdk.dependencies) {
|
||||
assert.equal(manifest.dependencies[dependency.name], dependency.range)
|
||||
const entry = parsed.object[dependency.key]
|
||||
for (const field of ['version', 'resolved', 'integrity'])
|
||||
assert.equal(entry[field], dependency[field])
|
||||
const installed = fs.readFileSync(path.join(workspace, dependency.installedManifest.file))
|
||||
assert.equal(hash(installed), dependency.installedManifest.sha256)
|
||||
assert.equal(JSON.parse(installed).version, dependency.version)
|
||||
}
|
||||
for (const [file, digest] of Object.entries(baseline.sdk.adapterFiles))
|
||||
assert.equal(hash(fs.readFileSync(path.join(workspace, file))), digest, file)
|
||||
for (const asset of baseline.sdk.assets) {
|
||||
const bytes = gitBytes(baseline.sourceCommit, asset.file)
|
||||
assert.equal(bytes.length, asset.bytes)
|
||||
assert.equal(hash(bytes), asset.sha256)
|
||||
const installed = fs.readFileSync(path.join(workspace, asset.installedFile))
|
||||
assert.equal(hash(installed), asset.installedSha256)
|
||||
assert.equal(asset.matchesInstalled, asset.sha256 === asset.installedSha256)
|
||||
}
|
||||
assert(sources.get(`${prefix}src/index.js`).includes(baseline.sdk.defaultSolutionPath))
|
||||
assert(sources.get('docs/assets/example/danmuku.mask.js').includes(baseline.sdk.demoSolutionPath))
|
||||
return { baseline, archives, sources }
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const { baseline, archives, sources } = await verifyDanmukuMaskContract()
|
||||
console.log(JSON.stringify({ sourceCommit: baseline.sourceCommit, sourceFiles: sources.size, releases: [...archives.keys()], dependencies: baseline.sdk.dependencies.map(item => `${item.name}@${item.version}`), existingAssets: baseline.sdk.assets.length, validation: 'Archive SHA512/SHA256, complete entrypoints, Git associations, frozen Yarn versions, installed SDK source and existing asset bytes; no model execution' }, null, 2))
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Frozen package contract verification.
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import { transform } from 'esbuild'
|
||||
import { verifyDanmukuMaskContract } from './danmuku-mask-contract.mjs'
|
||||
import { readMember } from './releases.mjs'
|
||||
|
||||
const contract = await verifyDanmukuMaskContract()
|
||||
const source = contract.sources.get('packages/artplayer-plugin-danmuku-mask/src/index.js')
|
||||
|
||||
async function controlledSource(option) {
|
||||
const frames = new Map()
|
||||
const configs = []
|
||||
const calls = []
|
||||
const listeners = new Map()
|
||||
const style = {}
|
||||
const pixels = new Uint8ClampedArray([255, 255, 255, 255, 250, 255, 255, 255])
|
||||
let id = 0
|
||||
const canvas = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
getContext: () => ({ getImageData: () => ({ data: pixels }), putImageData: value => calls.push(['pixels', [...value.data]]) }),
|
||||
toDataURL: () => 'data:image/png;base64,controlled',
|
||||
}
|
||||
const segmenter = { segmentPeople: async () => [{}] }
|
||||
const body = {
|
||||
SupportedModels: { MediaPipeSelfieSegmentation: 'selfie' },
|
||||
createSegmenter: async (model, config) => {
|
||||
configs.push({ model, config: { ...config } })
|
||||
return segmenter
|
||||
},
|
||||
toBinaryMask: async (...args) => {
|
||||
calls.push(['mask', ...args])
|
||||
return {}
|
||||
},
|
||||
drawMask: async (...args) => { calls.push(['draw', args[3], args[4]]) },
|
||||
}
|
||||
const tf = { setBackend: async value => calls.push(['backend', value]) }
|
||||
const module = { exports: {} }
|
||||
vm.runInNewContext((await transform(source, { format: 'cjs', target: 'es2020' })).code, {
|
||||
module,
|
||||
exports: module.exports,
|
||||
require(name) {
|
||||
if (name === '@tensorflow-models/body-segmentation')
|
||||
return body
|
||||
if (name === '@tensorflow/tfjs-core')
|
||||
return tf
|
||||
assert(['@tensorflow/tfjs-backend-webgl', '@tensorflow/tfjs-backend-cpu'].includes(name))
|
||||
return {}
|
||||
},
|
||||
document: { createElement: (tag) => {
|
||||
assert.equal(tag, 'canvas')
|
||||
return canvas
|
||||
} },
|
||||
requestAnimationFrame: (callback) => {
|
||||
frames.set(++id, callback)
|
||||
return id
|
||||
},
|
||||
cancelAnimationFrame: id => frames.delete(id),
|
||||
console,
|
||||
}, { timeout: 1000 })
|
||||
const art = { template: { $video: { paused: false, ended: false, videoWidth: 2, videoHeight: 1 }, $danmuku: { style } }, on: (name, callback) => listeners.set(name, callback) }
|
||||
const result = module.exports.default(option)(art)
|
||||
return { result, listeners, frames, configs, calls, style }
|
||||
}
|
||||
|
||||
test('Danmuku Mask freezes all published stable archive members and their actual Git associations', () => {
|
||||
assert.deepEqual([...contract.archives.keys()].sort(), ['1.0.0', '1.1.0'])
|
||||
for (const release of [contract.baseline.release, ...contract.baseline.previous]) {
|
||||
assert.equal(Object.keys(release.files).length, 6)
|
||||
assert.deepEqual(release.missingEntrypoints, [])
|
||||
}
|
||||
assert.equal(contract.baseline.release.registrySourceManifest.version, '1.0.1')
|
||||
assert.equal(contract.baseline.release.registrySourceManifest.versionMatchesRelease, false)
|
||||
assert.equal(contract.baseline.observations.latestArchiveMatchesFrozenSourceFiles.length, 6)
|
||||
})
|
||||
|
||||
test('Both published declarations retain optional options, synchronous registration, async start and void stop', () => {
|
||||
const declaration = contract.sources.get('packages/artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask.d.ts')
|
||||
for (const archive of contract.archives.values())
|
||||
assert.equal(readMember(archive, 'package/types/artplayer-plugin-danmuku-mask.d.ts').toString(), declaration)
|
||||
assert(declaration.includes('(option?: Option) => (art: Artplayer) => Result'))
|
||||
assert(declaration.includes('start: () => Promise<void>'))
|
||||
assert(declaration.includes('stop: () => void'))
|
||||
assert(declaration.includes('export default artplayerPluginDanmukuMask'))
|
||||
})
|
||||
|
||||
test('Actual main and legacy exports preserve historical CommonJS and browser global forms without starting a model', () => {
|
||||
for (const [version, archive] of contract.archives) {
|
||||
for (const suffix of ['js', 'legacy.js']) {
|
||||
const bytes = readMember(archive, `package/dist/artplayer-plugin-danmuku-mask.${suffix}`).toString()
|
||||
for (const form of ['cjs', 'global']) {
|
||||
const module = { exports: {} }
|
||||
const environment = {
|
||||
console,
|
||||
fetch() { throw new Error('Network is forbidden in this export probe') },
|
||||
}
|
||||
environment.global = environment
|
||||
environment.self = environment
|
||||
if (form === 'cjs')
|
||||
Object.assign(environment, { module, exports: module.exports })
|
||||
else environment.window = environment
|
||||
vm.runInNewContext(bytes, environment, { timeout: 5000 })
|
||||
const value = form === 'cjs' ? module.exports : environment.artplayerPluginDanmukuMask
|
||||
const oldCommonJs = version === '1.0.0' && form === 'cjs'
|
||||
assert.equal(typeof value, oldCommonJs ? 'object' : 'function')
|
||||
assert.equal(typeof value.default, oldCommonJs ? 'function' : 'undefined')
|
||||
const factory = oldCommonJs ? value.default : value
|
||||
const events = []
|
||||
const style = {}
|
||||
const result = factory()({
|
||||
template: { $video: {}, $danmuku: { style } },
|
||||
on: name => events.push(name),
|
||||
})
|
||||
assert.equal(result.name, 'artplayerPluginDanmukuMask')
|
||||
assert.equal(typeof result.start, 'function')
|
||||
assert.equal(typeof result.stop, 'function')
|
||||
assert.equal(result.stop(), undefined)
|
||||
assert.equal(style.maskImage, 'none')
|
||||
assert.deepEqual(events, ['ready', 'destroy'])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Frozen owned source keeps its defaults, ready/destroy hooks and alpha mask pixel boundary without a real SDK', async () => {
|
||||
const env = await controlledSource()
|
||||
assert.equal(env.result.name, 'artplayerPluginDanmukuMask')
|
||||
assert.deepEqual([...env.listeners.keys()], ['ready', 'destroy'])
|
||||
const pending = env.result.start()
|
||||
assert.equal(typeof pending.then, 'function')
|
||||
await pending
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
assert.deepEqual(env.configs, [{ model: 'selfie', config: { runtime: 'mediapipe', modelType: 'general', solutionPath: contract.baseline.sdk.defaultSolutionPath, modelSelection: 1, smoothSegmentation: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5, selfieMode: false } }])
|
||||
assert.deepEqual(env.style, { maskMode: 'alpha', maskSize: 'contain', maskRepeat: 'no-repeat', backgroundSize: 'contain', backgroundRepeat: 'no-repeat', maskImage: 'url(data:image/png;base64,controlled)' })
|
||||
assert.deepEqual(env.calls.find(call => call[0] === 'draw'), ['draw', 1, 3])
|
||||
assert.deepEqual(env.calls.find(call => call[0] === 'pixels'), ['pixels', [255, 255, 255, 0, 250, 255, 255, 255]])
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.result.stop(), undefined)
|
||||
assert.equal(env.frames.size, 0)
|
||||
assert.equal(env.style.maskImage, 'none')
|
||||
})
|
||||
|
||||
test('Frozen zero numeric options use historical OR defaults while false smoothSegmentation is retained', async () => {
|
||||
const env = await controlledSource({ solutionPath: '/local-model', modelSelection: 0, smoothSegmentation: false, minDetectionConfidence: 0, minTrackingConfidence: 0, foregroundThreshold: 0, opacity: 0, maskBlurAmount: 0, drawContour: true, selfieMode: true })
|
||||
await env.result.start()
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
assert.deepEqual(env.configs[0].config, { runtime: 'mediapipe', modelType: 'general', solutionPath: '/local-model', modelSelection: 1, smoothSegmentation: false, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5, selfieMode: true })
|
||||
assert.deepEqual(env.calls.find(call => call[0] === 'draw'), ['draw', 1, 3])
|
||||
const mask = env.calls.find(call => call[0] === 'mask')
|
||||
assert.equal(mask[4], true)
|
||||
assert.equal(mask[5], 0.5)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test('SDK-08 distinguishes the frozen Yarn adapter and local resource bytes from unversioned CDN resolution', () => {
|
||||
assert.deepEqual(contract.baseline.sdk.dependencies.map(item => item.version), ['0.1.1675465747', '1.0.2', '4.22.0', '4.22.0', '4.22.0', '4.22.0'])
|
||||
assert.equal(contract.baseline.sdk.assets.length, 12)
|
||||
assert(contract.baseline.sdk.assets.every(asset => asset.matchesInstalled))
|
||||
const adapter = fs.readFileSync('node_modules/@tensorflow-models/body-segmentation/dist/selfie_segmentation_mediapipe/segmenter.js', 'utf8')
|
||||
assert.match(adapter, /switch \(config\.modelType\)/u)
|
||||
assert.match(adapter, /case 'general':[\s\S]*?modelSelection = 0;/u)
|
||||
for (const ignored of ['modelSelection', 'smoothSegmentation', 'minDetectionConfidence', 'minTrackingConfidence', 'selfieMode'])
|
||||
assert.equal(adapter.includes(`config.${ignored}`), false, `Do not claim ${ignored} is consumed by this adapter`)
|
||||
assert(adapter.includes('return solutionPath + "/" + path;'))
|
||||
assert(adapter.includes('this.selfieSegmentationSolution.close();'))
|
||||
assert.equal(source.includes('.dispose('), false, 'The historical plugin does not dispose its segmenter')
|
||||
})
|
||||
Reference in new issue
Block a user