import test from 'node:test'; import assert from 'node:assert/strict'; import {readFileSync} from 'node:fs'; import vm from 'node:vm'; function camera() { const workers = []; const context = {setTimeout, clearTimeout, Subprocess: {async call(options) { let finish; const done = new Promise(resolve => { finish = resolve; }); const worker = {closed:false, writes:[], options, stdin:{async close() {worker.closed=true;finish();}, async write(text) {worker.writes.push(text);}}, wait:()=>done, kill:()=>finish()}; workers.push(worker); return worker; }}}; const source = readFileSync(new URL('../host/NativeCamera.sys.mjs', import.meta.url),'utf8') .replace(/^import .*;\n/gm,'').replace('export class NativeCamera','globalThis.NativeCamera = class NativeCamera'); vm.runInNewContext(source,context); const service = new context.NativeCamera('beatles'); service.packet = async () => ({ready:true,format:'nv21',...service.previewSize}); return {service,workers}; } test('a delayed stop or frame from the old camera cannot close its replacement',async()=>{ const {service,workers}=camera(), actor={}; const old=await service.request(actor,{action:'start',camera:'back'}); const next=await service.request(actor,{action:'start',camera:'back'}); assert.notEqual(old.session,next.session);assert.equal(workers[0].closed,true); await service.request(actor,{action:'stop',session:old.session}); assert.equal(workers[1].closed,false); await assert.rejects(service.request(actor,{action:'frame',session:old.session}),/CAMERA_NOT_OPEN/); assert.equal(workers[1].closed,false); await service.request(actor,{action:'frame',session:next.session}); assert.deepEqual(workers[1].writes,['frame\n']); await service.request(actor,{action:'stop',session:next.session}); assert.equal(workers[1].closed,true); }); test('Beatles uses the stock KaiOS 320x240 preview while other devices keep 640x480',async()=>{ const {service,workers}=camera(), actor={}; const metadata=await service.request(actor,{action:'start',camera:'back'}); assert.equal(service.previewSize.width,320); assert.equal(service.previewSize.height,240); assert.equal(metadata.width,320); assert.equal(metadata.height,240); assert.equal(metadata.frameRate,5); assert.deepEqual([...metadata.cameras],['back']); assert.equal(workers[0].options.command,'/usr/bin/python3.11'); assert.equal(workers[0].options.arguments[1],'/opt/vulpes/tundra/runtime/beatles-camera-client.py'); await service.request(actor,{action:'stop',session:metadata.session}); const sargo=new service.constructor('sargo'); assert.equal(sargo.previewSize.width,640); assert.equal(sargo.previewSize.height,480); sargo.packet=async()=>({ready:true,format:'nv21',...sargo.previewSize}); const sargoMetadata=await sargo.request(actor,{action:'start',camera:'back'}); assert.equal(sargoMetadata.frameRate,30); await sargo.request(actor,{action:'stop',session:sargoMetadata.session}); }); test('Beatles reports the missing front sensor instead of opening its rear sensor twice',async()=>{ const {service,workers}=camera(), actor={}; await assert.rejects(service.request(actor,{action:'start',camera:'front'}),/CAMERA_UNAVAILABLE/); assert.equal(workers.length,0); }); test('a different actor cannot stop or use the current camera',async()=>{ const {service,workers}=camera(), owner={}, other={}; const meta=await service.request(owner,{action:'start'}); await service.request(other,{action:'stop',session:meta.session}); await assert.rejects(service.request(other,{action:'frame',session:meta.session}),/CAMERA_NOT_OPEN/); await assert.rejects(service.request(other,{action:'start'}),/CAMERA_BUSY/); assert.equal(workers[0].closed,false); await service.request(owner,{action:'stop',session:meta.session}); });