tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

io-resources.test.mjs (11357B)


      1 import test from 'node:test';
      2 import assert from 'node:assert/strict';
      3 import {lifecycle} from '../dist/io-resources.js';
      4 import {HostWork} from '../dist/io-host-work.js';
      5 const ok=value=>({kind:'completed',value});
      6 const setup=(limits={})=>lifecycle(1n,'owner-stopped','cycle',['stdin','stdout','stderr'],limits);
      7 const create=(s,owner,backend,flags={},kind='file')=>s.resources.publish(s.resources.beginAcquire(owner),kind,backend,flags);
      8 async function finish(s,scope,release=()=>{},outcome=ok(null)){
      9  s.tasks.bodyDone(scope,outcome);assert(s.tasks.beginRelease(scope));
     10  const errors=await s.resources.releaseScope(scope,release,error=>error,'fallback');return s.tasks.settle(scope,errors);
     11 }
     12 function retired(s){assert.deepEqual(s.tasks.counts,{scopes:0,tasks:0,hosts:0});assert.deepEqual(s.resources.counts,{resources:0,admissions:0,leases:0});}
     13 
     14 test('admission closure preserves admitted opens and existing descendant borrowing',async()=>{
     15  const s=setup(),child=s.tasks.spawn(0,true,false),admission=s.resources.beginAcquire(0);
     16  s.tasks.bodyDone(0,ok(null));assert.equal(s.tasks.beginRelease(0),false);
     17  assert.throws(()=>s.resources.beginAcquire(child.key,0),/closed-scope/);
     18  const file=s.resources.publish(admission,'file','backend'),lease=s.resources.beginLease(child.key,file,'file','read');
     19  assert.equal(lease.backend,'backend');s.tasks.bodyDone(child.key,ok(null));assert.equal(s.tasks.beginRelease(child.key),false);
     20  s.resources.endLease(lease.token);assert(s.tasks.beginRelease(child.key));s.tasks.settle(child.key);
     21  assert(s.tasks.beginRelease(0));assert.throws(()=>s.tasks.settle(0),/busy/);
     22  const released=[];await s.resources.releaseScope(0,r=>released.push(r.backend),e=>e,'fallback');s.tasks.settle(0);
     23  assert.deepEqual(released,['backend']);retired(s);
     24 });
     25 test('pending ancestor acquisition blocks owner even after acquiring child has stopped',async()=>{
     26  const s=setup(),child=s.tasks.spawn(0,true,false),admission=s.resources.beginAcquire(child.key,0);
     27  s.tasks.cancel(0,child,'cancel');await finish(s,child.key);s.tasks.bodyDone(0,ok(null));
     28  assert.equal(s.tasks.beginRelease(0),false);s.resources.abortAcquire(admission);
     29  assert.throws(()=>s.resources.abortAcquire(admission),/invalid-admission/);
     30  assert(s.tasks.beginRelease(0));await s.resources.releaseScope(0,()=>assert.fail('borrowed close'),e=>e,'fallback');s.tasks.settle(0);retired(s);
     31 });
     32 test('transfer validation is atomic; rekeying invalidates aliases and preserves acquisition order',async()=>{
     33  const s=setup(),a=create(s,0,'first'),b=create(s,0,'second');
     34  const before={tasks:s.tasks.counts,resources:s.resources.counts};
     35  for(const [tokens,message] of [[[a,a],/duplicate/],[[a,{...b,run:2n}],/stale/],[[a,{run:1n,key:1}],/borrowed/]]){
     36   assert.throws(()=>s.resources.spawn(0,false,true,tokens),message);
     37   assert.deepEqual({tasks:s.tasks.counts,resources:s.resources.counts},before);assert.equal(s.resources.lookup(0,a).backend,'first');
     38  }
     39  const {task,handles}=s.resources.spawn(0,true,true,[b,a]);
     40  assert.throws(()=>s.resources.lookup(0,a),/stale/);assert.throws(()=>s.resources.lookup(task.key,b),/stale/);
     41  assert.throws(()=>s.resources.lookup(0,handles[0]),/out-of-scope/);
     42  assert.equal(s.resources.lookup(task.key,handles[0]).backend,'second');assert.deepEqual(s.resources.newestOwned(task.key),handles[0]);
     43  const nested=s.tasks.nested(task.key);assert.throws(()=>s.resources.spawn(nested,true,false,[handles[1]]),/not-owner/);
     44  await finish(s,nested);const released=[];await finish(s,task.key,r=>released.push(r.backend));
     45  assert.deepEqual(released,['second','first']);await finish(s,0);retired(s);
     46 });
     47 test('failed task admission cannot move resources, start a child or consume resource IDs',async()=>{
     48  const s=setup({taskIdentities:1}),file=create(s,0,'owner'),only=s.tasks.spawn(0,true,false);await finish(s,only.key);
     49  const before=s.resources.counts;
     50  assert.throws(()=>s.resources.spawn(0,true,true,[file]),/scope-identity-exhausted/);
     51  assert.equal(s.resources.lookup(0,file).owner,0);assert.deepEqual(s.resources.counts,before);
     52  assert.deepEqual(s.tasks.counts,{scopes:1,tasks:0,hosts:0});
     53  const next=create(s,0,'next');assert.equal(next.key,file.key+2,'failed spawn consumed resource identities');
     54  await finish(s,0);retired(s);
     55 });
     56 test('direction leases, wrong-kind rejection and exactly-once close acknowledgements',async()=>{
     57  const s=setup(),file=create(s,0,'duplex',{duplex:true});
     58  assert.throws(()=>s.resources.beginLease(0,file,'reader','read'),/wrong-kind/);assert.equal(s.resources.counts.leases,0);
     59  const r=s.resources.beginLease(0,file,'file','read'),w=s.resources.beginLease(0,file,'file','write');
     60  assert.throws(()=>s.resources.beginLease(0,file,'file','read'),/busy/);assert.throws(()=>s.resources.spawn(0,true,false,[file]),/busy/);
     61  assert.equal(s.resources.claimClose(0,file),null);assert.throws(()=>s.resources.beginLease(0,file,'file','read'),/closed-handle/);
     62  s.resources.endLease(r.token);assert.throws(()=>s.resources.endLease(r.token),/invalid-lease/);s.resources.endLease(w.token);
     63  assert(s.resources.claimClose(0,file));assert.equal(s.resources.claimClose(0,file),null);
     64  s.tasks.bodyDone(0,ok(null));assert(s.tasks.beginRelease(0));assert.throws(()=>s.tasks.settle(0),/busy/);
     65  s.resources.finishClose(file);assert.throws(()=>s.resources.finishClose(file),/stale-handle/);
     66  await s.resources.releaseScope(0,()=>{},e=>e,'fallback');s.tasks.settle(0);retired(s);
     67 });
     68 test('ordinary single-cursor resources, internal resources and borrowed standards stay distinct',async()=>{
     69  const s=setup(),file=create(s,0,'file'),lease=s.resources.beginLease(0,file,'file','read');
     70  assert.throws(()=>s.resources.beginLease(0,file,'file','write'),/busy/);s.resources.endLease(lease.token);
     71  const hidden=create(s,0,'hidden',{internal:true});
     72  assert.throws(()=>s.resources.beginLease(0,hidden,'file','read'),/internal-resource/);
     73  assert.throws(()=>s.resources.claimClose(0,hidden),/internal-resource/);
     74  assert.throws(()=>s.resources.spawn(0,true,false,[hidden]),/internal-resource/);
     75  assert.throws(()=>s.resources.claimClose(0,{run:1n,key:2}),/borrowed-resource/);
     76  const internal=s.resources.beginLease(0,hidden,'file','exclusive',true);s.resources.endLease(internal.token);
     77  const view=s.resources.lookup(0,file);view.owner=999;assert.equal(s.resources.lookup(0,file).owner,0);
     78  assert.throws(()=>{view.flags.borrowed=true;},TypeError);
     79  await finish(s,0);retired(s);
     80 });
     81 test('reverse publication order, not open-start order, determines cleanup',async()=>{
     82  const s=setup(),first=s.resources.beginAcquire(0),second=s.resources.beginAcquire(0);
     83  s.resources.publish(second,'file','published first');s.resources.publish(first,'file','published second');
     84  const order=[];await finish(s,0,r=>order.push(r.backend));assert.deepEqual(order,['published second','published first']);retired(s);
     85 });
     86 test('publication identity exhaustion leaves admission live for raw release/abort; cleanup needs no IDs',async()=>{
     87  const s=setup({resourceIdentities:4}),admission=s.resources.beginAcquire(0);
     88  assert.throws(()=>s.resources.publish(admission,'file','raw backend'),/identity-exhausted/);
     89  assert.deepEqual(s.resources.counts,{resources:3,admissions:1,leases:0});
     90  assert.throws(()=>s.resources.beginAcquire(0),/identity-exhausted/);
     91  assert.throws(()=>s.resources.beginLease(0,{run:1n,key:1},'reader','read'),/identity-exhausted/);
     92  s.resources.abortAcquire(admission);await finish(s,0);retired(s);
     93 });
     94 test('physical cleanup attempts every provider once and diagnostics failure cannot skip releases',async()=>{
     95  const s=setup(),order=[];let classified=0;for(let i=0;i<20;i++)create(s,0,i);
     96  s.tasks.bodyDone(0,{kind:'failed',value:'primary'});assert(s.tasks.beginRelease(0));
     97  const errors=await s.resources.releaseScope(0,r=>{order.push(r.backend);throw 'release failed';},()=>{classified++;throw 'classifier failed';},'fallback');
     98  assert.deepEqual(order,Array.from({length:20},(_,i)=>19-i));assert.equal(errors.items.length,16);assert.equal(errors.omitted,4);assert.equal(classified,16);
     99  const out=s.tasks.settle(0,errors);assert.equal(out.value,'primary');assert.deepEqual(out.cleanup,errors);retired(s);
    100 });
    101 test('asynchronous release stays registered until provider acknowledgement, even on failure',async()=>{
    102  const s=setup(),work=new HostWork(s.tasks),file=create(s,0,'file');let acknowledge;
    103  s.tasks.bodyDone(0,ok(null));assert(s.tasks.beginRelease(0));
    104  const pending=new Promise(resolve=>{acknowledge=resolve;});
    105  work.release(0,()=>s.resources.releaseScope(0,async()=>{await pending;throw 'close failed';},e=>e,'fallback'));
    106  assert(s.resources.lookup(0,file).releasing);assert.throws(()=>s.tasks.settle(0),/invalid-transition/);
    107  const wait=work.wait();acknowledge();await wait;const reply=work.take();assert(reply.outcome.ok);
    108  assert.throws(()=>s.resources.lookup(0,file),/stale-handle/);
    109  const out=s.tasks.settle(0,reply.outcome.value);assert.equal(out.kind,'failed');assert.equal(out.value,'close failed');retired(s);
    110 });
    111 test('repeated scoped resources and leases retain no registry history',async()=>{
    112  const s=setup();for(let i=0;i<1000;i++){
    113   const child=s.resources.spawn(0,true,false,[]).task,file=create(s,child.key,i),lease=s.resources.beginLease(child.key,file,'file','exclusive');
    114   s.resources.endLease(lease.token);await finish(s,child.key);
    115   assert.deepEqual(s.resources.counts,{resources:3,admissions:0,leases:0});assert.deepEqual(s.tasks.counts,{scopes:1,tasks:0,hosts:0});
    116  }
    117  await finish(s,0);retired(s);
    118 });
    119 test('explicit close cancels a leased operation, not its task, and waits for lease acknowledgement',async()=>{
    120  const s=setup(),work=new HostWork(s.tasks),file=create(s,0,'reader',{},'reader'),child=s.tasks.spawn(0,true,false);let acknowledge;
    121  const drained=new Promise(resolve=>{acknowledge=resolve;});
    122  const read=work.start(child.key,async signal=>{
    123   const lease=s.resources.beginLease(child.key,file,'reader','read');
    124   try {await new Promise(resolve=>signal.addEventListener('abort',resolve,{once:true}));await drained;throw 'resource closed';}
    125   finally{s.resources.endLease(lease.token);}
    126  });
    127  assert.equal(s.resources.claimClose(0,file),null);
    128  assert.equal(work.cancelBody({...read,run:2n}),false);assert.equal(work.cancelBody(read),true);assert.equal(work.cancelBody(read),false);
    129  assert.equal(s.resources.claimClose(0,file),null);assert.equal(s.tasks.state(child.key).reason,null);
    130  const waiting=work.wait();acknowledge();await waiting;const reply=work.take();
    131  assert.equal(reply.outcome.ok,false);assert.equal(reply.outcome.error,'resource closed');assert.equal(s.tasks.state(child.key).reason,null);
    132  let physical=0;work.start(0,()=>{assert(s.resources.claimClose(0,file));physical++;s.resources.finishClose(file);});
    133  await work.wait();assert(work.take().outcome.ok);assert.equal(physical,1);
    134  await finish(s,child.key);await finish(s,0);retired(s);
    135 });
    136 
    137 test('resource exhaustion and oversize transfers do not admit tasks or invalidate aliases',async()=>{
    138  const s=setup({resourceIdentities:5}),file=create(s,0,'file');
    139  for(const [tokens,error] of [[[file],/identity-exhausted/],[Array(257).fill(file),/transfer-limit/]]){
    140   assert.throws(()=>s.resources.spawn(0,true,false,tokens),error);
    141   assert.equal(s.resources.lookup(0,file).owner,0);assert.deepEqual(s.tasks.counts,{scopes:1,tasks:0,hosts:0});
    142  }
    143  await finish(s,0);retired(s);
    144 });