tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

modules.test.mjs (6405B)


      1 import test from 'node:test';
      2 import assert from 'node:assert/strict';
      3 import {readFile, readdir} from 'node:fs/promises';
      4 import {decodeBundle, sha256} from '../dist/bundle.js';
      5 import {indexedObject, moduleIdentity, moduleHash} from '../dist/module-identity.js';
      6 import {fetchModule} from '../dist/module-resolver.js';
      7 import {packageIndex, packageReleases} from '../dist/packages.js';
      8 const site = new URL('../', import.meta.url), root = new URL('../../', site);
      9 const releases = packageIndex(JSON.parse(await readFile(new URL('packages.json', site))));
     10 const bytes = r => readFile(new URL(`bundles/${r.hash}.arboricx`, site));
     11 const libraries = [...new Set(releases.filter(r => r.name.startsWith('tricu-lib-')).map(r => r.name))]
     12   .map(name => packageReleases(releases, name)[0]);
     13 
     14 test('every library source has a complete-interface package; no module allowlist', async () => {
     15   const paths = [];
     16   async function walk(directory) {
     17     for (const entry of await readdir(new URL(directory, root), {withFileTypes:true})) {
     18       const path = directory + entry.name;
     19       if (entry.isDirectory()) await walk(path + '/');
     20       else if (entry.name.endsWith('.tri')) paths.push(path);
     21     }
     22   }
     23   await walk('lib/');
     24   assert.deepEqual(libraries.map(r => r.title).sort(), paths.sort());
     25   for (const cohort of ['compiler-library', 'support-library']) {
     26     const fixture = decodeBundle(await readFile(new URL(`test/fixtures/${cohort}.arboricx`, root)));
     27     const metadata = await readFile(new URL(`test/fixtures/${cohort}.v1`, root), 'utf8');
     28     for (const row of metadata.split('\n').filter(s => s.startsWith('module\t'))) {
     29       const [, , alias, path] = row.split('\t');
     30       if (!path.startsWith('lib/')) continue;
     31       const release = libraries.find(r => r.title === path);
     32       assert.ok(release, path);
     33       const bundle = decodeBundle(await bytes(release));
     34       assert.deepEqual(bundle.exports.map(e => e.name), fixture.exports.filter(e => e.name.startsWith(alias + '.')).map(e => e.name.slice(alias.length + 1)), path);
     35     }
     36   }
     37 });
     38 
     39 test('browser manifest identities match every independently native-published library interface', async () => {
     40   assert.ok(libraries.length > 0);
     41   for (const release of libraries) {
     42     const data = await bytes(release);
     43     assert.equal(await sha256(data), release.hash, release.name);
     44     assert.equal(await moduleIdentity(decodeBundle(data)), release.manifest, release.name);
     45   }
     46 });
     47 
     48 test('indexed extraction is name-neutral, ignores unrelated roots/metadata, preserves physical sharing', async () => {
     49   const b = decodeBundle(await readFile(new URL('test/fixtures/id.arboricx', root)));
     50   const expected = await sha256(indexedObject(b, b.exports[0].root));
     51   b.metadata = {description:'not executable identity'};
     52   b.exports[0].name = 'renamed';
     53   b.nodes.push({kind:'fork',left:0,right:0});
     54   assert.equal(await sha256(indexedObject(b, b.exports[0].root)), expected);
     55   const shared = {nodes:[{kind:'leaf'}],roots:[31],exports:[{name:'tree',root:31,kind:'term',abi:'arboricx.abi.tree.v1'}],metadata:{}};
     56   for(let i=1;i<=31;i++) shared.nodes.push({kind:'fork',left:i-1,right:i-1});
     57   const object = indexedObject(shared,31);
     58   assert.ok(object.length < 2048);
     59   assert.equal(decodeBundle(object).nodes.length,32);
     60   assert.equal((await moduleIdentity(shared)).length,64);
     61   await assert.rejects(moduleIdentity({...shared,exports:[]}), /nonempty native/);
     62   await assert.rejects(moduleIdentity({...shared,exports:[{...shared.exports[0],name:'bad name'}]}), /native module/);
     63   assert.throws(()=>moduleHash('../path'), /hash/);
     64 });
     65 
     66 test('manifest transport rejects unknown refs, wrong manifest, corrupt bytes and HTTP failure', async () => {
     67   const release = libraries.find(r => r.title === 'lib/packages/io.tri');
     68   const data = await bytes(release), calls = [], original = globalThis.fetch;
     69   let response = () => new Response(data);
     70   globalThis.fetch = async (url, options) => { calls.push({url:String(url),options}); return response(); };
     71   const base = new URL('https://example.invalid/nested/site/');
     72   try {
     73     const loaded = await fetchModule(base, release.manifest, releases);
     74     assert.equal(loaded.exports.length,6);
     75     assert.equal(calls[0].url, `https://example.invalid/_arboricx/bundle/hash/${release.hash}`);
     76     assert.deepEqual(calls[0].options, {redirect:'error',credentials:'omit'});
     77     const count = calls.length;
     78     await assert.rejects(fetchModule(base, '0'.repeat(64), releases), /Unknown module manifest/);
     79     await assert.rejects(fetchModule(base, release.hash, releases), /Unknown module manifest/);
     80     assert.equal(calls.length,count);
     81     await assert.rejects(fetchModule(base, '0'.repeat(64), [{...release,manifest:'0'.repeat(64)}]), /Module manifest mismatch/);
     82     response = () => new Response(new Uint8Array([1,2,3]));
     83     await assert.rejects(fetchModule(base, release.manifest, releases), /SHA-256 mismatch/);
     84     response = () => new Response('missing',{status:404});
     85     await assert.rejects(fetchModule(base, release.manifest, releases), /HTTP 404/);
     86     response = () => new Response(data);
     87     assert.equal((await fetchModule(base, release.manifest, releases)).exports.length,6);
     88   } finally { globalThis.fetch = original; }
     89 });
     90 
     91 test('native-parsed export projection retains opaque trees without evaluating them', async () => {
     92   const {selectModuleExports} = await import('../dist/module-stage.js');
     93   const value = new Proxy({}, {get(){throw Error('Executable tree must remain opaque');}});
     94   const locals = new Map([['private', value], ['answer', value]]);
     95   const dependencies = new Map([['pin', new Map([['id', value]])]]);
     96   const stage = exports => ({body:'',imports:[],exports,native:true});
     97   assert.deepEqual([...selectModuleExports(stage([['answer','']]),locals,dependencies).keys()],['answer']);
     98   assert.deepEqual([...selectModuleExports(stage([['*',''],['from','pin']]),locals,dependencies).keys()],['private','answer','id']);
     99   for(const [exports,pattern] of [[[],/requires an export/],[[['absent','']],/undeclared local/],[[['answer',''],['*','']],/duplicate export/],[[['from','missing']],/Missing re-export/]]){
    100     assert.throws(()=>selectModuleExports(stage(exports),locals,dependencies),pattern);
    101   }
    102   const huge = new Map(Array.from({length:4097},(_,i)=>['n'+i,value]));
    103   assert.throws(()=>selectModuleExports(stage([['*','']]),huge,dependencies),/export limit/);
    104 });