/** * 图片解码池的并发、排队、取消与响应校验测试。尺寸元数据必须合法,调用者取消不能提前释放仍在运行的槽位。 * 使用解码执行器及 Worker 替身,不实际解码图片像素或启动浏览器线程。 */ import {test} from 'node:test'; import assert from 'node:assert/strict'; import {createImageDecodePool} from '../../src/lib/canvas-runtime/image-decoder.ts'; import {createBrowserImageDecoder} from '../../src/lib/canvas-runtime/browser-images.ts'; // 手动完成的 Promise 控制解码退出;bytes 的 MIME 只是输入标签,替身执行器直接返回已知尺寸。 const deferred=()=>{let resolve;const promise=new Promise(yes=>resolve=yes);return{promise,resolve};},bytes=new Blob(['image'],{type:'image/png'}); // 一个活动加一个等待名额,第三个拒绝;释放屏障后按 FIFO 完成,执行器计数峰值始终为一。 test('image decoder bounds real execution slots, preserves FIFO and rejects excess work',async()=>{ const gate=deferred(),started=[];let active=0,peak=0;const pool=createImageDecodePool({concurrency:1,maxQueued:1,execute:async file=>{active++;peak=Math.max(peak,active);started.push(await file.text());await gate.promise;active--;return{width:4,height:2};}});try{const first=pool.decode(bytes),second=pool.decode(new Blob(['second'],{type:'image/png'}));await assert.rejects(pool.decode(bytes),/image_decode_capacity/);gate.resolve();assert.deepEqual(await first,{width:4,height:2});assert.deepEqual(await second,{width:4,height:2});assert.deepEqual(started,['image','second']);assert.equal(peak,1);}finally{gate.resolve();pool.close();} }); // 活动调用取消后,底层解码尚未结束时新任务仍排队;直到 gate 完成才启动下一次执行。 test('cancelling active work does not free its slot before the underlying decoder settles',async()=>{ const gate=deferred(),started=deferred(),cancel=new AbortController();let executions=0;const pool=createImageDecodePool({concurrency:1,execute:async()=>{executions++;started.resolve();await gate.promise;return{width:4,height:2};}});try{const first=pool.decode(bytes,cancel.signal),rejected=assert.rejects(first,/image_decode_aborted/);await started.promise;cancel.abort();await rejected;const second=pool.decode(bytes);await Promise.resolve();assert.equal(executions,1);gate.resolve();await second;assert.equal(executions,2);}finally{gate.resolve();pool.close();} }); // 零宽尺寸不能成为可用元数据;Worker 返回错误请求 id 也要拒绝并终止线程替身,finally 恢复全局 Worker。 test('invalid dimensions and failed workers do not produce usable metadata; worker identity is checked',async()=>{ const invalid=createImageDecodePool({execute:async()=>({width:0,height:2})});await assert.rejects(invalid.decode(bytes),/invalid_image_dimensions/);invalid.close();const previous=globalThis.Worker;let terminated=0;globalThis.Worker=class{/* 异步回信的尺寸合法,唯独 id 错误,应被视为其他请求的结果。 */ postMessage(message){queueMicrotask(()=>this.onmessage({data:{version:1,id:message.id+1,byteLength:message.bytes.size,width:1,height:1}}));}/* 错误响应必须清理 worker,不能留在池内继续复用。 */ terminate(){terminated++;}};const pool=createBrowserImageDecoder();try{await assert.rejects(pool.decode(bytes),/invalid_image_response/);assert.equal(terminated,1);}finally{pool.close();if(previous===undefined)delete globalThis.Worker;else globalThis.Worker=previous;} });