Login
testing-common.js
Login

File testing-common.js from the latest check-in


/*
  2022-07-02

  The author disclaims copyright to this source code.  In place of a
  legal notice, here is a blessing:

  *   May you do good and not evil.
  *   May you find forgiveness for yourself and forgive others.
  *   May you share freely, never taking more than you give.

  ***********************************************************************

*/
'use strict';
self.Jaccwabyt.Tester = (function (self){
  const T = self.WhTest.Tester;
  const log = console.log.bind(console);
  const debug = console.debug.bind(console);

  /** Throws a new Error, the message of which is the concatenation
      all args with a space between each. */
  const toss = function(){
    throw new Error(Array.prototype.join.call(arguments, ' '));
  };

  const eqApprox = function(v1,v2,factor=0.05){
    //debug('eqApprox',v1, v2);
    return v1>=(v2-factor) && v1<=(v2+factor);
  };

  const testIntPtr = function(T,C){
    log("Testing output-pointer argument handling...");
    const w = C.wasm;
    const stack = w.scopedAllocPush();
    let ptrInt;
    const origValue = 512;
    const ptrValType = 'i32';
    try{
      ptrInt = w.scopedAlloc(4);
      w.poke(ptrInt,origValue, ptrValType);
      const cf = w.xGet('jaccwabyt_test_intptr');
      const oldPtrInt = ptrInt;
      //log('ptrInt',ptrInt);
      //log('peek(ptrInt)',w.peek(ptrInt));
      T.assert(origValue === w.peek(ptrInt, ptrValType));
      const rc = cf(ptrInt);
      //log('cf(ptrInt)',rc);
      //log('ptrInt',ptrInt);
      //log('peek(ptrInt)',w.peek(ptrInt,ptrValType));
      T.assert(2*origValue === rc).
        assert(rc === w.peek(ptrInt,ptrValType)).
        assert(oldPtrInt === ptrInt);
      const pi64 = w.scopedAlloc(8)/*ptr to 64-bit integer*/;
      const o64 = 0x010203040506/*>32-bit integer*/;
      const ptrType64 = 'i64';
      if(w.bigIntEnabled){
        log("BigInt support is enabled...");
        w.poke(pi64, o64, ptrType64);
        //log("pi64 =",pi64, "o64 = 0x",o64.toString(16), o64);
        const v64 = ()=>w.peek(pi64,ptrType64)
        //log("peek(pi64)",v64());
        T.assert(v64() == o64);
        //T.assert(o64 === w.peek(pi64, ptrType64));
        const cf64w = w.xGet('jaccwabyt_test_int64ptr');
        cf64w(pi64);
        //log("peek(pi64)",v64());
        T.assert(v64() == BigInt(2 * o64));
        cf64w(pi64);
        T.assert(v64() == BigInt(4 * o64));

        const biTimes2 = w.xGet('jaccwabyt_test_int64_times2');
        T.assert(BigInt(2 * o64) ===
                 biTimes2(BigInt(o64)/*explicit conv. required to avoid TypeError
                                       in the call :/ */));
        const pMin = w.scopedAlloc(16);
        const pMax = pMin + 8;
        const g64 = (p)=>w.peek(p,ptrType64);
        w.poke(pMin, 0, ptrType64);
        w.poke(pMax, 0, ptrType64);
        const minMaxI64 = [
          w.xCall('jaccwabyt_test_int64_min'),
          w.xCall('jaccwabyt_test_int64_max')
        ];
        T.assert(minMaxI64[0] < BigInt(Number.MIN_SAFE_INTEGER)).
          assert(minMaxI64[1] > BigInt(Number.MAX_SAFE_INTEGER));
        //log("int64_min/max() =",minMaxI64, typeof minMaxI64[0]);
        w.xCall('jaccwabyt_test_int64_minmax', pMin, pMax);
        /* Ensure that our int64 handling is doing what we want... */
        T.assert(g64(pMin) === minMaxI64[0], "int64 mismatch").
          assert(g64(pMax) === minMaxI64[1], "int64 mismatch");
        //log("pMin",g64(pMin), "pMax",g64(pMax));
        w.poke(pMin, minMaxI64[0], ptrType64);
        T.assert(g64(pMin) === minMaxI64[0]);
      }else{
        log("No BigInt support.");
        log("\"The problem\" here is that we can manipulate, at the byte level,",
            "heap memory to set 64-bit values, but we can't get those values",
            "back into JS because of the lack of 64-bit number support.");
      }
    }finally{
      const x = w.scopedAlloc(1), y = w.scopedAlloc(1), z = w.scopedAlloc(1);
      //log("x=",x,"y=",y,"z=",z); // just looking at the alignment
      T.assert(stack.length>=3);
      w.scopedAllocPop(stack);
    }
  }/*testIntPtr()*/;
  
  const testStructStuff = function(T,C){
    const W = C.wasm;
    log("Jaccwabyt tests...");
    const MyStructDef = {
      sizeof: 16,
      members: {
        p4: {offset: 0, sizeof: 4, signature: "i"},
        pP: {offset: 4, sizeof: 4, signature: "P"},
        ro: {offset: 8, sizeof: 4, signature: "i", readOnly: true},
        cstr: {offset: 12, sizeof: 4, signature: "s"}
      }
    };
    if(W.bigIntEnabled){
      const m = MyStructDef;
      m.members.p8 = {offset: m.sizeof, sizeof: 8, signature: "j"};
      m.sizeof += m.members.p8.sizeof;
    }
    const StructType = C.StructBinder.StructType;
    const K = C.StructBinder('my_struct',MyStructDef);
    T.mustThrowMatching(()=>K(), /via 'new'/).
      mustThrowMatching(()=>new K('hi'), /^Invalid pointer/);
    const k1 = new K(), k2 = new K();
    try {
      T.assert(k1.constructor === K).
        assert(K.isA(k1)).
        assert(k1 instanceof K).
        assert(K.prototype.lookupMember('p4').key === '$p4').
        assert(K.prototype.lookupMember('$p4').name === 'p4').
        mustThrowMatching(()=>K.prototype.lookupMember('nope'), /not a mapped/).
        assert(undefined === K.prototype.lookupMember('nope',false)).
        assert(k1 instanceof StructType).
        assert(StructType.isA(k1)).
        mustThrowMatching(()=>k1.$ro = 1, /read-only/);
      Object.keys(MyStructDef.members).forEach(function(key){
        key = K.memberKey(key);
        T.assert(0 == k1[key],
                 "Expecting allocation to zero the memory "+
                 "for "+key+" but got: "+k1[key]+
                 " from "+k1.memoryDump());
      });
      T.assert('number' === typeof k1.pointer).
        mustThrowMatching(()=>k1.pointer = 1, /pointer/);
      k1.$p4 = 1; k1.$pP = 2;
      T.assert(1 === k1.$p4).assert(2 === k1.$pP);
      if(MyStructDef.members.$p8){
        k1.$p8 = 1/*must not throw despite not being a BigInt*/;
        k1.$p8 = BigInt(Number.MAX_SAFE_INTEGER * 2);
        T.assert(BigInt(2 * Number.MAX_SAFE_INTEGER) === k1.$p8);
      }
      T.assert(!k1.ondispose);
      k1.setMemberCString('cstr', "A C-string.");
      T.assert(Array.isArray(k1.ondispose)).
        assert(k1.ondispose[0] === k1.$cstr).
        assert('number' === typeof k1.$cstr).
        assert('A C-string.' === k1.memberToJsString('cstr'));
      k1.$pP = k2;
      T.assert(k1.$pP === k2.pointer);
      k1.$pP = null/*null is special-cased to 0.*/;
      T.assert(0===k1.$pP);
      let ptr = k1.pointer;
      k1.dispose();
      T.assert(undefined === k1.pointer).
        mustThrowMatching(()=>{k1.$pP=1}, /disposed instance/);
    }finally{
      k1.dispose();
      k2.dispose();
    }

    if(!W.bigIntEnabled){
      log("Skipping WasmTestStruct tests: BigInt not enabled.");
      return;
    }

    const ctype = W.xCallWrapped('jaccwabyt_test_ctype_json', 'json');
    log("Struct descriptions:",ctype.structs);
    const WTStructDesc =
          ctype.structs.filter((e)=>'WasmTestStruct'===e.name)[0];
    const autoResolvePtr = true /* EXPERIMENTAL */;
    if(autoResolvePtr){
      WTStructDesc.members.ppV.signature = 'P';
    }
    const WTStruct = C.StructBinder(WTStructDesc);
    log(WTStruct.structName, WTStruct.structInfo);
    const wts = new WTStruct();
    log("WTStruct.prototype keys:",Object.keys(WTStruct.prototype));
    try{
      T.assert(wts.constructor === WTStruct).
        assert(WTStruct.memberKeys().indexOf('$ppV')>=0).
        assert(wts.memberKeys().indexOf('$v8')>=0).
        assert(!K.isA(wts)).
        assert(WTStruct.isA(wts)).
        assert(wts instanceof WTStruct).
        assert(wts instanceof StructType).
        assert(StructType.isA(wts));
      T.assert(wts.pointer>0).assert(0===wts.$v4).assert(0n===wts.$v8).
        assert(0===wts.$ppV).assert(0===wts.$xFunc);
      const testFunc =
            W.xGet('jaccwabyt_test_struct'/*name gets mangled in -O3 builds!*/);
      let counter = 0;
      log("wts.pointer =",wts.pointer);
      const wtsFunc = function(arg){
        log("This from a JS function called from C, "+
            "which itself was called from JS. arg =",arg);
        ++counter;
        if(3===counter){
          toss("Testing exception propagation.");
        }
      }
      wts.$v4 = 10; wts.$v8 = 20;
      wts.$xFunc = W.installFunction(wtsFunc, wts.memberSignature('xFunc'))
      /* ^^^ compiles wtsFunc to WASM and returns its new function pointer */;
      T.assert(0===counter).assert(10 === wts.$v4).assert(20n === wts.$v8)
        .assert(0 === wts.$ppV).assert('number' === typeof wts.$xFunc)
        .assert(0 === wts.$cstr)
        .assert(wts.memberIsString('$cstr'))
        .assert(!wts.memberIsString('$v4'))
        .assert(null === wts.memberToJsString('$cstr'))
        .assert(W.functionEntry(wts.$xFunc) instanceof Function);
      /* It might seem silly to assert that the values match
         what we just set, but recall that all of those property
         reads and writes are, via property interceptors,
         actually marshaling their data to/from a raw memory
         buffer, so merely reading them back is actually part of
         testing the struct-wrapping API. */

      testFunc(wts.pointer);
      log("wts.pointer, wts.$ppV",wts.pointer, wts.$ppV);
      T.assert(1===counter).assert(20 === wts.$v4).assert(40n === wts.$v8)
        .assert(wts.$ppV === wts.pointer)
        .assert('string' === typeof wts.memberToJsString('cstr'))
        .assert(wts.memberToJsString('cstr') === wts.memberToJsString('$cstr'))
        .mustThrowMatching(()=>wts.memberToJsString('xFunc'),
                           /Invalid member type signature for C-string/)
      ;
      testFunc(wts.pointer);
      T.assert(2===counter).assert(40 === wts.$v4).assert(80n === wts.$v8)
        .assert(wts.$ppV === wts.pointer);
      /** The 3rd call to wtsFunc throw from JS, which is called
          from C, which is called from JS. Let's ensure that
          that exception propagates back here... */
      T.mustThrowMatching(()=>testFunc(wts.pointer),/^Testing/);
      W.uninstallFunction(wts.$xFunc);
      wts.$xFunc = 0;
      if(autoResolvePtr){
        wts.$ppV = 0;
        T.assert(!wts.$ppV);
        WTStruct.debugFlags(0x03);
        wts.$ppV = wts;
        T.assert(wts.pointer === wts.$ppV)
        WTStruct.debugFlags(0);
      }
      wts.setMemberCString('cstr', "A C-string.");
      T.assert(Array.isArray(wts.ondispose)).
        assert(wts.ondispose[0] === wts.$cstr).
        assert('A C-string.' === wts.memberToJsString('cstr'));
      const ptr = wts.pointer;
      wts.dispose();
      T.assert(ptr).assert(undefined === wts.pointer);
    }finally{
      wts.dispose();
    }

    if(1){ // ondispose of other struct instances
      const s1 = new WTStruct, s2 = new WTStruct, s3 = new WTStruct;
      T.assert(s1.lookupMember instanceof Function)
        .assert(s1.addOnDispose instanceof Function);
      s1.addOnDispose(s2,"testing variadic args");
      T.assert(2===s1.ondispose.length);
      s2.addOnDispose(s3);
      s1.dispose();
      T.assert(!s2.pointer,"Expecting s2 to be ondispose'd by s1.");
      T.assert(!s3.pointer,"Expecting s3 to be ondispose'd by s2.");
    }
  }/*testStructStuff()*/;

  const testWasmUtil = function(T,C){
    const w = C.wasm;
    const chr = (x)=>x.charCodeAt(0);

    log("heap getters...");
    {
      const li = [8, 16, 32];
      if(w.bigIntEnabled) li.push(64);
      for(const n of li){
        const bpe = n/8;
        const s = w.heapForSize(n,false);
        T.assert(bpe===s.BYTES_PER_ELEMENT).
          assert(w.heapForSize(s.constructor) === s);
        const u = w.heapForSize(n,true);
        T.assert(bpe===u.BYTES_PER_ELEMENT).
          assert(s!==u).
          assert(w.heapForSize(u.constructor) === u);
      }
    }

    log("jstrlen()...");
    {
      T.assert(3 === w.jstrlen("abc")).assert(4 === w.jstrlen("äbc"));
    }

    log("jstrcpy()...");
    {
      const fillChar = 10;
      let ua = new Uint8Array(8), rc,
          refill = ()=>ua.fill(fillChar);
      refill();
      rc = w.jstrcpy("hello", ua);
      T.assert(6===rc).assert(0===ua[5]).assert(chr('o')===ua[4]);
      refill();
      ua[5] = chr('!');
      rc = w.jstrcpy("HELLO", ua, 0, -1, false);
      T.assert(5===rc).assert(chr('!')===ua[5]).assert(chr('O')===ua[4]);
      refill();
      rc = w.jstrcpy("the end", ua, 4);
      //log("rc,ua",rc,ua);
      T.assert(4===rc).assert(0===ua[7]).
        assert(chr('e')===ua[6]).assert(chr('t')===ua[4]);
      refill();
      rc = w.jstrcpy("the end", ua, 4, -1, false);
      T.assert(4===rc).assert(chr(' ')===ua[7]).
        assert(chr('e')===ua[6]).assert(chr('t')===ua[4]);
      refill();
      rc = w.jstrcpy("", ua, 0, 1, true);
      //log("rc,ua",rc,ua);
      T.assert(1===rc).assert(0===ua[0]);
      refill();
      rc = w.jstrcpy("x", ua, 0, 1, true);
      //log("rc,ua",rc,ua);
      T.assert(1===rc).assert(0===ua[0]);
      refill();
      rc = w.jstrcpy('äbä', ua, 0, 1, true);
      T.assert(1===rc, 'Must not write partial multi-byte char.')
        .assert(0===ua[0]);
      refill();
      rc = w.jstrcpy('äbä', ua, 0, 2, true);
      T.assert(1===rc, 'Must not write partial multi-byte char.')
        .assert(0===ua[0]);
      refill();
      rc = w.jstrcpy('äbä', ua, 0, 2, false);
      T.assert(2===rc).assert(fillChar!==ua[1]).assert(fillChar===ua[2]);
    }/*jstrcpy()*/

    log("cstrncpy()...");
    {
      w.scopedAllocPush();
      try {
        let cStr = w.scopedAllocCString("hello");
        const n = w.cstrlen(cStr);
        let cpy = w.scopedAlloc(n+10);
        let rc = w.cstrncpy(cpy, cStr, n+10);
        T.assert(n+1 === rc).
          assert("hello" === w.cstrToJs(cpy)).
          assert(chr('o') === w.peek(cpy+n-1)).
          assert(0 === w.peek(cpy+n));
        let cStr2 = w.scopedAllocCString("HI!!!");
        rc = w.cstrncpy(cpy, cStr2, 3);
        T.assert(3===rc).
          assert("HI!lo" === w.cstrToJs(cpy)).
          assert(chr('!') === w.peek(cpy+2)).
          assert(chr('l') === w.peek(cpy+3));
      }finally{
        w.scopedAllocPop();
      }
    }

    log("jstrToUintArray()...");
    {
      let a = w.jstrToUintArray("hello", false);
      T.assert(5===a.byteLength).assert(chr('o')===a[4]);
      a = w.jstrToUintArray("hello", true);
      T.assert(6===a.byteLength).assert(chr('o')===a[4]).assert(0===a[5]);
      a = w.jstrToUintArray("äbä", false);
      T.assert(5===a.byteLength).assert(chr('b')===a[2]);
      a = w.jstrToUintArray("äbä", true);
      T.assert(6===a.byteLength).assert(chr('b')===a[2]).assert(0===a[5]);
    }

    log("allocCString()...");
    {
      const cstr = w.allocCString("hällo, world");
      const n = w.cstrlen(cstr);
      T.assert(13 === n)
        .assert(0===w.peek(cstr+n))
        .assert(chr('d')===w.peek(cstr+n-1));
      w.dealloc(cstr);
    }

    log("scopedAlloc() and friends...");
    {
      const alloc = w.alloc, dealloc = w.dealloc;
      w.alloc = w.dealloc = null;
      T.assert(!w.scopedAlloc.level)
        .mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/)
        .mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/);
      w.alloc = alloc;
      T.mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/);
      w.dealloc = dealloc;
      T.mustThrowMatching(()=>w.scopedAllocPop(), /^Invalid state/)
        .mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/)
        .mustThrowMatching(()=>w.scopedAlloc.level=0, /read-only/);
      const asc = w.scopedAllocPush();
      let asc2;
      try {
        const p1 = w.scopedAlloc(16),
              p2 = w.scopedAlloc(16);
        T.assert(1===w.scopedAlloc.level)
          .assert(Number.isFinite(p1))
          .assert(Number.isFinite(p2))
          .assert(asc[0] === p1)
          .assert(asc[1]===p2);
        asc2 = w.scopedAllocPush();
        const p3 = w.scopedAlloc(16);
        T.assert(2===w.scopedAlloc.level)
          .assert(Number.isFinite(p3))
          .assert(2===asc.length)
          .assert(p3===asc2[0]);

        const [z1, z2, z3] = w.scopedAllocPtr(3);
        T.assert('number'===typeof z1).assert(z2>z1).assert(z3>z2)
          .assert(0===w.peek(z1,'i32'), 'allocPtr() must zero the targets')
          .assert(0===w.peek(z3,'i32'));
      }finally{
        // Pop them in "incorrect" order to make sure they behave:
        w.scopedAllocPop(asc);
        T.assert(0===asc.length);
        T.mustThrowMatching(()=>w.scopedAllocPop(asc),
                            /^Invalid state object/);
        if(asc2){
          T.assert(2===asc2.length,'Should be p3 and z1');
          w.scopedAllocPop(asc2);
          T.assert(0===asc2.length);
          T.mustThrowMatching(()=>w.scopedAllocPop(asc2),
                              /^Invalid state object/);
        }
      }
      T.assert(0===w.scopedAlloc.level);
      w.scopedAllocCall(function(){
        T.assert(1===w.scopedAlloc.level);
        const [cstr, n] = w.scopedAllocCString("hello, world", true);
        T.assert(12 === n)
          .assert(0===w.peek(cstr+n))
          .assert(chr('d')===w.peek(cstr+n-1));
      });
    }/*scopedAlloc()*/

    log("xCall()...");
    {
      const pJson = w.xCall('jaccwabyt_test_ctype_json');
      T.assert(Number.isFinite(pJson)).assert(w.cstrlen(pJson)>300);
    }

    log("xWrap()...");
    {
      //int jaccwabyt_test_intptr(int * p);
      //int64_t jaccwabyt_test_int64_max(void)
      //int64_t jaccwabyt_test_int64_min(void)
      //int64_t jaccwabyt_test_int64_times2(int64_t x)
      //void jaccwabyt_test_int64_minmax(int64_t * min, int64_t *max)
      //int64_t jaccwabyt_test_int64ptr(int64_t * p)
      //const char * jaccwabyt_test_ctype_json(void)
      T.mustThrowMatching(()=>w.xWrap('jaccwabyt_test_ctype_json',null,'i32'),
                          /requires 0 arg/).
        assert(w.xWrap.resultAdapter('i32') instanceof Function).
        assert(w.xWrap.argAdapter('i32') instanceof Function);
      let fw = w.xWrap('jaccwabyt_test_ctype_json','string');
      T.mustThrowMatching(()=>fw(1), /requires 0 arg/);
      let rc = fw();
      T.assert('string'===typeof rc).assert(rc.length>300);
      rc = w.xCallWrapped('jaccwabyt_test_ctype_json','*');
      T.assert(rc>0 && Number.isFinite(rc));
      rc = w.xCallWrapped('jaccwabyt_test_ctype_json','string');
      T.assert('string'===typeof rc).assert(rc.length>300);
      fw = w.xWrap('jaccwabyt_test_str_hello', 'string:dealloc',['i32']);
      rc = fw(0);
      T.assert('hello'===rc);
      rc = fw(1);
      T.assert(null===rc);

      w.xWrap.resultAdapter('thrice', (v)=>3n*BigInt(v));
      w.xWrap.argAdapter('twice', (v)=>2n*BigInt(v));
      fw = w.xWrap('jaccwabyt_test_int64_times2','thrice','twice');
      rc = fw(1);
      T.assert(12n===rc);

      w.scopedAllocCall(function(){
        const pI1 = w.scopedAlloc(8), pI2 = pI1+4;
        w.poke([pI1,pI2], 0,'*');
        const f = w.xWrap('jaccwabyt_test_int64_minmax',undefined,['i64*','i64*']);
        const [r1, r2] = w.peek([pI1, pI2], 'i64');
        T.assert(!Number.isSafeInteger(r1)).assert(!Number.isSafeInteger(r2));
      });
    }
  }/*testWasmUtil()*/;

  const runTests = function(ClientApp){
    log("Starting tests. Client:",ClientApp);
    const T = WhTest.Tester, C = ClientApp;
    //log("Client =",C);
    log("C.wasm.exports",C.wasm.exports);
    //log("C.wasm.exports.__indirect_function_table",C.wasm.exports.__indirect_function_table);
    const startTime = performance.now();
    try {
      [
        testWasmUtil, testIntPtr, testStructStuff
      ].forEach((f)=>{
        const t = T.counter, n = performance.now();
        log("Running",f.name+"()...");
        f(T,C);
        log(f.name+"():",T.counter - t,'tests in',(performance.now() - n),"ms");
      });
    }finally{
    }
    log("Total Test count:",T.counter,"in",(performance.now() - startTime),"ms");
  };

  return runTests;
})(self);