Files
releases/wasm/test.js
T
Stephan Renatus 5240170c68 wasm: deal with importing memory in the compiler (#3763)
This sets the stage for eventually allowing OPA wasm modules that do NOT
import memory.

With this change, we add the necessary segments to the wasm module that
declare that memory is to be imported, in the wasm compiler. As far as
LLVM and our C base is concerned, the memory is NOT imported.

The test runners have been adapted, `make wasm-lib-test` works without
having imported memory now. `make wasm-rego-test` can deal with both: it
will provide memory in its `imports` for instantiation, but if the wasm
module happens to not want that import, it'll be ignored. The memory used
in the other host methods is the exported one. (Whether that is exported
or re-exported imported doesn't make a difference.)

Some first steps have been included to make OPA's Wasm SDK work without
imported memory. There are a few loose ends around enforcing memory
limits, to be taken care of later.

----

This also addresses a problem we've seen in the wild before: when our
additions to the wasm modules' data segments exceed the number of pages
needed for the memory import, a "data segment overflowing memory" issue
could have happened. That was because the minimal memory size for the
imported memory was determined by LLVM, and we'd just squeeze our added
data segments in, without adjusting that limit.

Now, the limit will be set properly; and if a too small memory was
provided, a more descriptive failure will happen at instantiation time.
Wasmtime, for example, raises

    incompatible import type for `env::memory`
    Caused by:
        memory types incompatible

Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
2021-10-18 14:02:08 +02:00

125 lines
3.3 KiB
JavaScript

const { readFileSync } = require('fs');
function stringDecoder(mem) {
return function(addr) {
const i8 = new Int8Array(mem.buffer);
var s = "";
while (i8[addr] != 0) {
s += String.fromCharCode(i8[addr++]);
}
return s;
}
}
function red(text) {
return '\x1b[0m\x1b[31m' + text + '\x1b[0m';
}
function green(text) {
return '\x1b[0m\x1b[32m' + text + '\x1b[0m';
}
function yellow(text) {
return '\x1b[0m\x1b[33m' + text + '\x1b[0m';
}
function namespace(cache, func, note) {
let key = func + note;
if (key in cache) {
cache[key] += 1;
note = note + ' (' + cache[key] + ')'
} else {
cache[key] = 0;
}
return note;
}
function report(passed, error, msg) {
if (passed === true) {
if (process.env.VERBOSE === '1') {
console.log(green('PASS'), msg);
}
} else if (error === undefined) {
console.log(yellow('FAIL'), msg);
} else {
console.log(red('ERROR'), msg, error);
}
}
async function test(executable) {
const memory = new WebAssembly.Memory({ initial: 3 });
let addr2string;
let cache = {};
let failedOrErrored = 0;
let seenFuncs = {};
const module = await WebAssembly.instantiate(readFileSync(executable), {
env: {
memory,
opa_builtin0: () => 0,
opa_builtin1: () => 0,
opa_builtin2: () => 0,
opa_builtin3: () => 0,
opa_builtin4: () => 0,
opa_println: (msg) => {
console.log(addr2string(msg));
},
opa_abort: (msg) => {
throw 'abort: ' + addr2string(msg);
},
opa_test_pass: (note, func) => {
note = addr2string(note);
func = addr2string(func);
note = namespace(cache, func, note);
seenFuncs[func] = true;
let key = func + '/' + note
report(true, undefined, key);
},
opa_test_fail: (note, func, file, line) => {
note = addr2string(note);
func = addr2string(func);
note = namespace(cache, func, note);
seenFuncs[func] = true;
let key = func + '/' + note;
failedOrErrored++;
report(false, undefined, key + ' ' + addr2string(file) + ':' + line);
},
}
});
addr2string = stringDecoder(module.instance.exports.memory);
for (let key in module.instance.exports) {
if (key.startsWith("test_")) {
try {
module.instance.exports[key]();
if (!(key in seenFuncs)) {
report(true, undefined, key);
}
} catch (e) {
report(false, e, key)
}
}
}
// NOTE(sr): seenFuncs will not contain all tests run, but only those that
// actually call opa_test_{pass,fail}. However, if it's empty, something is
// definitely wrong.
if (Object.keys(seenFuncs).length == 0) {
console.log(red('ERROR'), "no tests executed");
process.exit(2);
}
if (failedOrErrored > 0) {
process.exit(1);
}
}
if (process.argv.length != 3) {
console.log(process.argv[1] + " <test executable path>");
process.exit(1);
}
test(process.argv[2]);