tricu

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

packages.test.mjs (10935B)


      1 import test from 'node:test';
      2 import assert from 'node:assert/strict';
      3 import {readFile} from 'node:fs/promises';
      4 import {createHash} from 'node:crypto';
      5 import {packageIndex,packageReleases,environmentProfile} from '../dist/packages.js';
      6 import {decodeBundle,loadBundle} from '../dist/bundle.js';
      7 import {Graph} from '../dist/graph.js';
      8 import {text} from '../dist/codecs.js';
      9 import {BROWSER_BUDGET} from '../dist/execution.js';
     10 const encodedText=value=>{const g=new Graph();return g.toTernary(text(g,value));};
     11 const read=name=>readFile(new URL('../'+name,import.meta.url));
     12 
     13 test('named versions and demo environments are validated and pinned',async()=>{
     14  const releases=packageIndex(JSON.parse(await read('packages.json')));
     15  const catalogue=JSON.parse(await read('catalogue.json'));
     16  const environments=JSON.parse(await read('environments.json'));
     17  const compiler=JSON.parse(await read('compiler.json'));
     18  assert.equal(environments.compiler,compiler.hash);
     19  for(const entry of catalogue){
     20   const release=releases.find(r=>r.demo===entry.id&&r.hash===entry.hash);assert.ok(release);
     21   if(entry.environment===undefined){assert.equal(entry.preparation,'native');continue;}
     22   const {profile}=environmentProfile(environments,entry.environment);
     23   if(profile){
     24    const bytes=await read('bundles/'+profile.hash+'.arboricx');
     25    assert.equal(createHash('sha256').update(bytes).digest('hex'),profile.hash);
     26    const values=loadBundle(new Graph(),decodeBundle(bytes));
     27    for(const name of profile.names)assert.ok(values.has(name),name);
     28   }
     29  }
     30 });
     31 test('index and environment boundaries reject malformed paths, pins, duplicates and profiles',()=>{
     32  const r={name:'demo',version:'0.1',title:'A demo',hash:'a'.repeat(64),manifest:'b'.repeat(64)};
     33  assert.equal(packageIndex({schema:1,releases:[r]})[0].name,'demo');
     34  for(const change of [{name:'../x'},{version:'x/y'},{hash:'A'.repeat(64)},{manifest:'x'},{title:[]},{demo:'x/y'}])assert.throws(()=>packageIndex({schema:1,releases:[{...r,...change}]}));
     35  assert.throws(()=>packageIndex({schema:1,releases:[r,r]}),/Duplicate/);
     36  assert.throws(()=>packageIndex({schema:2,releases:[r]}));
     37  const descriptor={schema:1,compiler:'a'.repeat(64),profiles:{empty:null}};
     38  assert.equal(environmentProfile(descriptor,'empty').profile,null);
     39  assert.throws(()=>environmentProfile(descriptor,'toString'),/Unknown/);
     40  for(const names of [['bad name'],['x','x'],Array(129).fill('x'),['../x']])assert.throws(()=>environmentProfile({...descriptor,profiles:{test:{hash:r.hash,names}}},'test'));
     41 });
     42 test('browser execution has no application ceilings and retains GC',()=>{
     43  assert.deepEqual(BROWSER_BUDGET,{nodes:Infinity,fuel:Infinity,stack:Infinity});
     44  const open=new Graph(BROWSER_BUDGET,{threshold:2});let root=open.leaf;
     45  for(let i=0;i<100;i++){root=open.stem(root);open.collect([root]);}
     46  assert.ok(open.stats.nodes>10);assert.ok(open.stats.collections>0);
     47 });
     48 
     49 test('published native libraries have authenticated complete interfaces and no fabricated demo source',async()=>{
     50  const releases=packageIndex(JSON.parse(await read('packages.json')));
     51  const expected={
     52   'tricu-base':['append','matchResult','showNumber'],
     53   'tricu-file-io':['io','bind','readFile','sha256Hex','putStr','abortIO'],
     54   'arboricx-inspection':['inspectBundle','renderSummary','describeError'],
     55  };
     56  for(const [name,names] of Object.entries(expected))for(const version of ['0.1.0','0.1.1']){
     57   const release=releases.find(r=>r.name===name&&r.version===version);assert.ok(release);assert.equal(release.demo,undefined);
     58   const bytes=await read('bundles/'+release.hash+'.arboricx');
     59   assert.equal(createHash('sha256').update(bytes).digest('hex'),release.hash);
     60   assert.deepEqual([...loadBundle(new Graph(),decodeBundle(bytes)).keys()].sort(),names.sort());
     61  }
     62 });
     63 
     64 test('showcase packages retain browser-bounded complete library roots and native-only server identity',async()=>{
     65  const releases=packageIndex(JSON.parse(await read('packages.json')));
     66  for(const [name,required] of Object.entries({
     67   'arboricx-server':['arboricxServer','arboricxRouter','bundleHashValid?'],
     68   'tricu-http':['parseRequestLine','response','httpHandlerIO'],
     69   'tricu-intensional-contracts':['fullTree?','powerOfTwo?','sortedList?'],
     70   'tricu-graphs':['graphSort','graphCycle'],
     71   'tricu-parser':['parserBind','parserCommit','parserMatchReply','parserEof'],
     72   'tricu-formatter':['formatTri'],
     73  })){
     74   const release=releases.find(r=>r.name===name);assert.ok(release);assert.equal(release.demo,undefined);
     75   const bytes=await read('bundles/'+release.hash+'.arboricx');
     76   assert.equal(createHash('sha256').update(bytes).digest('hex'),release.hash);
     77   assert.ok(bytes.length<=16777216);
     78   const bundle=decodeBundle(bytes);assert.ok(bundle.nodes.length<=100000);
     79   const exports=loadBundle(new Graph(),bundle);
     80   for(const name of required)assert.ok(exports.has(name),name);
     81   if(release.name==='arboricx-server')assert.match(release.title,/native sockets and filesystem/);
     82  }
     83 });
     84 
     85 test('native parser demo source and goldens are authenticated with an editable native module environment',async()=>{
     86  const catalogue=JSON.parse(await read('catalogue.json')),demo=catalogue.find(e=>e.id==='parser');
     87  const goldens=JSON.parse(await read('parser-demo.json'));
     88  assert.equal(demo.preparation,'native');assert.equal(demo.environment,'empty');
     89  assert.equal(createHash('sha256').update(demo.source).digest('hex'),demo.sourceSha256);
     90  assert.equal(demo.source,await read('demos/parser.tri').then(b=>b.toString()));
     91  assert.equal(goldens.hash,demo.hash);assert.equal(goldens.sourceSha256,demo.sourceSha256);
     92  assert.ok(goldens.haskellCases.some(c=>c.export==='parseNames'&&c.arguments[0]===encodedText('red,')));
     93  assert.ok(goldens.haskellCases.some(c=>c.export==='parsePrefix'&&c.arguments[0]===encodedText('red green')));
     94 });
     95 
     96 test('both indexed compiler packages retain their exact independently authenticated artifacts',async()=>{
     97  const releases=packageIndex(JSON.parse(await read('packages.json'))),descriptor=JSON.parse(await read('compiler.json'));
     98  for(const [name,path,entrypoint] of [
     99   ['tricu-compiler','test/fixtures/compiler-library.arboricx','source_unit.compileStableModuleDefinitions'],
    100   ['tricu-source-builder','bootstrap/build-stable.arboricx','sourceBuildGraph'],
    101  ]){
    102   const reference=await readFile(new URL('../../../'+path,import.meta.url));
    103   const referenceHash=createHash('sha256').update(reference).digest('hex');
    104   const release=releases.find(r=>r.name===name&&r.hash===referenceHash);assert.ok(release);assert.equal(release.demo,undefined);
    105   const bytes=await read('bundles/'+release.hash+'.arboricx');
    106   assert.ok(bytes.equals(reference),'indexed current artifact matches authenticated reference');
    107   assert.equal(createHash('sha256').update(bytes).digest('hex'),release.hash);
    108   const {COMPILER_BUNDLE_LIMIT}=await import('../dist/bundle.js');
    109   const bundle=decodeBundle(bytes,COMPILER_BUNDLE_LIMIT);assert.ok(bundle.nodes.length<=100000);
    110   assert.ok(bundle.exports.some(e=>e.name===entrypoint));
    111   if(name==='tricu-compiler')assert.equal(release.hash,descriptor.hash);
    112   else{assert.equal(bundle.exports.length,1);assert.match(release.title,/native application compiler/);}
    113  }
    114 });
    115 
    116 test('new practical libraries expose complete interfaces and lexer reference source is authenticated',async()=>{
    117  const releases=packageIndex(JSON.parse(await read('packages.json')));
    118  for(const [name,required] of Object.entries({
    119   'tricu-math':['add','divMod','inRange','decimal','printable'],
    120   'tricu-lexer':['lexerScanWhile','lexerAdvance','lexerDispatch','lexerToken'],
    121   'tricu-binary':['readU8','readBytes','binaryUtf8?','errInvalidBinaryInput'],
    122   'tricu-list-traversal':['listContinue','listStop','listFoldUntil','listFoldTailUntil'],
    123   'tricu-patterns':['match','otherwise','cond','guard'],
    124  })){
    125   const release=releases.find(r=>r.name===name);assert.ok(release);assert.equal(release.demo,undefined);
    126   const bytes=await read('bundles/'+release.hash+'.arboricx');
    127   assert.equal(createHash('sha256').update(bytes).digest('hex'),release.hash);
    128   const names=decodeBundle(bytes).exports.map(e=>e.name);
    129   for(const name of required)assert.ok(names.includes(name),name);
    130   if(name==='tricu-list-traversal'||name==='tricu-patterns')assert.deepEqual(names.sort(),required.sort());
    131  }
    132  const demo=JSON.parse(await read('catalogue.json')).find(e=>e.id==='lexer'),goldens=JSON.parse(await read('lexer-demo.json'));
    133  assert.equal(demo.preparation,'native');assert.equal(demo.environment,'empty');
    134  assert.equal(demo.source,await read('demos/lexer.tri').then(b=>b.toString()));
    135  assert.equal(createHash('sha256').update(demo.source).digest('hex'),demo.sourceSha256);
    136  assert.equal(goldens.hash,demo.hash);assert.equal(goldens.sourceSha256,demo.sourceSha256);
    137  assert.ok(goldens.haskellCases.some(c=>c.export==='scanWhitespace'&&c.arguments[0]===encodedText(' \n\tword')));
    138  assert.ok(goldens.haskellCases.some(c=>c.export==='scanWord'&&c.arguments[0]===encodedText('abcé')));
    139 });
    140 
    141 test('release choices are newest-first, numeric, independent of publication order and non-mutating',()=>{
    142  const versions=['0.1.0','0.10.0','0.2.0','1.0.0-rc.2','1.0.0-rc.10','1.0.0','1.0.0-alpha','1.0.0-1'];
    143  const releases=versions.map(version=>({name:'demo',version,hash:'a'.repeat(64),manifest:'b'.repeat(64),title:version}));
    144  const expected=['1.0.0','1.0.0-rc.10','1.0.0-rc.2','1.0.0-alpha','1.0.0-1','0.10.0','0.2.0','0.1.0'];
    145  for(const input of [releases,[...releases].reverse()]){
    146   const snapshot=[...input];
    147   assert.deepEqual(packageReleases([...input,{...input[0],name:'other',version:'99.0.0'}],'demo').map(r=>r.version),expected);
    148   assert.deepEqual(input,snapshot);
    149  }
    150  assert.deepEqual(packageReleases(releases,'missing'),[]);
    151  const labels=['build2','build10','0.1.0'].map(version=>({...releases[0],version}));
    152  assert.deepEqual(packageReleases(labels,'demo').map(r=>r.version),['0.1.0','build10','build2']);
    153 });
    154 
    155 test('every catalogue demo is editable, source-authenticated and explicitly imports indexed manifests',async()=>{
    156  const catalogue=JSON.parse(await read('catalogue.json')),index=packageIndex(JSON.parse(await read('packages.json')));
    157  assert.equal(catalogue.length,16);
    158  assert.equal(catalogue[0].id,'names-values');
    159  assert.ok(!catalogue.some(e=>e.id==='intro'));
    160  for(const demo of catalogue){
    161   assert.equal(demo.environment,'empty',demo.id);
    162   assert.equal(demo.source,(await read('demos/'+demo.id+'.tri')).toString());
    163   assert.equal(createHash('sha256').update(demo.source).digest('hex'),demo.sourceSha256);
    164   assert.match(demo.source,/^!export /m);
    165   for(const line of demo.source.split('\n').filter(s=>s.startsWith('!import '))){
    166    const match=line.match(/^!import ([0-9a-f]{64}) (?:\*|as [A-Za-z_][A-Za-z0-9_]*)$/);assert.ok(match,line);
    167    assert.ok(index.some(r=>r.manifest===match[1]),line);
    168   }
    169   assert.doesNotMatch(demo.source,/Tree graph|copy it into Compiler|source is read-only|input box uses JSON/);
    170  }
    171 });