Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
unit:
name: Unit tests
runs-on: ubuntu-latest
needs: build-wabt
needs: [build-wabt, build-wdcli]
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
Expand All @@ -91,15 +91,30 @@ jobs:
name: wabt-build-${{ github.run_id }}
path: .tools

- name: Download WARDuino CLI
uses: actions/download-artifact@v4
with:
name: warduino-build-${{ github.run_id }}
path: .warduino

- name: Verify tools
run: |
chmod u+x $GITHUB_WORKSPACE/.tools/*
$GITHUB_WORKSPACE/.tools/wasm-objdump --version
chmod u+x "$GITHUB_WORKSPACE"/.tools/*
"$GITHUB_WORKSPACE"/.tools/wasm-objdump --version
find "$GITHUB_WORKSPACE"/.warduino -type f -name wdcli -exec chmod u+x {} \;
EMULATOR_PATH="$(find "$GITHUB_WORKSPACE"/.warduino -type f -name wdcli -print -quit)"
test -n "$EMULATOR_PATH"
echo "EMULATOR=$EMULATOR_PATH" >> $GITHUB_ENV

- name: Run ava unit tests
run: npm run test:ava
env:
WABT: ${GITHUB_WORKSPACE}/.tools
WABT: ${{ github.workspace }}/.tools

- name: Run example tests
run: npm run test:example
env:
WABT: ${{ github.workspace }}/.tools

coverage:
name: Code coverage
Expand Down Expand Up @@ -146,4 +161,3 @@ jobs:
valColorRange: ${{ env.LINE_COVERAGE }}
maxColorRange: 100
minColorRange: 0

42 changes: 27 additions & 15 deletions src/framework/Verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,33 @@ export class Verifier {
let result = new StepOutcome(this.step).update(Outcome.succeeded);
for (const expectation of this.step.expected ?? []) {
for (const [field, entry] of Object.entries(expectation)) {
let value;
try {
const value = getValue(actual, field);

if (entry.kind === 'primitive') {
result = this.expectPrimitive(value, entry.value);
} else if (entry.kind === 'description') {
result = this.expectDescription(value, entry.value);
} else if (entry.kind === 'comparison') {
result = this.expectComparison(actual, value, entry.value, entry.message);
} else if (entry.kind === 'behaviour') {
if (previous === undefined) {
return this.error('Invalid test: no [previous] to compare behaviour to.');
}
result = this.expectBehaviour(value, getValue(previous, field), entry.value);
}
value = getValue(actual, field);
} catch {
return this.error(`Failure: ${JSONStringify(actual)} state does not contain '${field}'.`);
}

if (entry.kind === 'primitive') {
result = this.expectPrimitive(value, entry.value);
} else if (entry.kind === 'description') {
result = this.expectDescription(value, entry.value);
} else if (entry.kind === 'comparison') {
result = this.expectComparison(actual, value, entry.value, entry.message);
} else if (entry.kind === 'behaviour') {
if (previous === undefined) {
return this.error('Invalid test: no [previous] to compare behaviour to.');
}

let previousValue;
try {
previousValue = getValue(previous, field);
} catch {
return this.error(`Failure: ${JSONStringify(previous)} previous state does not contain '${field}'.`);
}
result = this.expectBehaviour(value, previousValue, entry.value);
}

if (result.outcome !== Outcome.succeeded) {
return result;
}
Expand Down Expand Up @@ -123,5 +131,9 @@ export class Verifier {

/* eslint @typescript-eslint/no-explicit-any: off */
function deepEqual(a: any, b: any): boolean {
return a === b || (isNaN(Number(a)) && isNaN(Number(b))) || a.equals(b);
if (typeof a === 'number' && typeof b === 'number') {
return Object.is(a, b);
}

return a === b || (a !== undefined && a !== null && typeof a.equals === 'function' && a.equals(b));
}
20 changes: 16 additions & 4 deletions src/messaging/Parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function stacking(objects: { value: string, type: any }[]): WASM.Value<Type>[] {
const stacked: WASM.Value<Type>[] = [];
for (const object of objects) {
const type: WASM.Type = extractType(object);
let buff;
let buff: Buffer;
switch (type) {
case WASM.Integer.u32:
case WASM.Integer.u64:
Expand All @@ -96,11 +96,11 @@ function stacking(objects: { value: string, type: any }[]): WASM.Value<Type>[] {
stacked.push({value: WasmInt.finite(signed(BigInt(object.value), 64)), type: type});
break;
case WASM.Float.f32:
buff = Buffer.from(Number(object.value).toString(16), 'hex');
buff = floatBitsBuffer(object.value, 4);
stacked.push({value: ieee754.read(buff, 0, false, 23, buff.length), type: type});
break;
case WASM.Float.f64:
buff = Buffer.from(BigInt(object.value).toString(16), 'hex');
buff = floatBitsBuffer(object.value, 8);
stacked.push({value: ieee754.read(buff, 0, false, 52, buff.length), type: type});
break;
case WASM.Special.unknown:
Expand All @@ -110,8 +110,20 @@ function stacking(objects: { value: string, type: any }[]): WASM.Value<Type>[] {
return stacked;
}

function floatBitsBuffer(value: string | number | bigint, bytes: number): Buffer {
const raw = value.toString().trim();
const hex = /^[0-9]+$/.test(raw)
? BigInt(raw).toString(16)
: raw.replace(/^0x/i, '');
const length = bytes * 2;
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length > length) {
throw Error(`Invalid ${bytes * 8}-bit float bit pattern: ${value}`);
}
return Buffer.from(hex.padStart(length, '0'), 'hex');
}


// Strips all trailing newlines
function stripEnd(text: string): string {
return text.replace(/\s+$/g, '');
}
}
2 changes: 1 addition & 1 deletion tests/examples/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,5 @@ reverse.test({
new Invoker('read', [WASM.i32(15n)], WASM.i32(1n))]
})

framework.reporter.verbosity(Verbosity.debug);
framework.reporter.verbosity(Verbosity.normal);
framework.analyse([spec, debug, threethree, copysign]);
59 changes: 57 additions & 2 deletions tests/unit/parsing.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import test from 'ava';
import {invokeParser, signed, stateParser} from "../../src/messaging/Parsers";
import {invokeParser, signed} from "../../src/messaging/Parsers";
import {Exception, WARDuino} from "../../src";
import {WASM} from "../../src/sourcemap/Wasm";
import State = WARDuino.State;
import {Verifier} from "../../src/framework/Verifier";
import {Expected, Kind, Step} from "../../src/framework/scenario/Step";
import {Outcome} from "../../src/reporter/describers/Describer";
import Type = WASM.Type;
import WasmInt = WASM.WasmInt;

Expand Down Expand Up @@ -75,3 +77,56 @@ test('[invoke parser] : 64-bit float', t => {
t.true(isNaN(<number>result.value));
}
});

test('[invoke parser] : f32 hex bit pattern with alpha digits', t => {
const result: WASM.Value<Type> | Exception = invokeParser(`{\"stack\": [{\"idx\":0,\"type\":\"F32\",\"value\":\"a6800001\"}]}\n`);

if ('text' in result) {
t.fail(`Expected parsed value, got exception: ${result.text}`);
return;
}

t.is(result.type, WASM.Float.f32);
t.is(result.value, -8.881785255792436e-16);
});

test('[invoke parser] : f32 decimal bit pattern whose hex has only digits', t => {
const result: WASM.Value<Type> | Exception = invokeParser(`{\"stack\": [{\"idx\":0,\"type\":\"F32\",\"value\":\"645922818\"}]}\n`);

if ('text' in result) {
t.fail(`Expected parsed value, got exception: ${result.text}`);
return;
}

t.is(result.type, WASM.Float.f32);
t.is(result.value, 8.88178631458362e-16);
});

test('[invoke parser] : f32 decimal bit pattern with alpha hex equivalent', t => {
const result: WASM.Value<Type> | Exception = invokeParser(`{\"stack\": [{\"idx\":0,\"type\":\"F32\",\"value\":\"2793406465\"}]}\n`);

if ('text' in result) {
t.fail(`Expected parsed value, got exception: ${result.text}`);
return;
}

t.is(result.type, WASM.Float.f32);
t.is(result.value, -8.881785255792436e-16);
});

test('[verifier] : numeric mismatch is reported as failure, not missing field', t => {
const step: Step = {
title: 'CHECK: numeric mismatch',
instruction: {
kind: Kind.Request,
value: {type: WARDuino.Interrupt.invoke, payload: () => '', parser: invokeParser}
},
expected: [{'value': {kind: 'primitive', value: -8.881785255792436e-16} as Expected<number>}]
};

const result = new Verifier(step).verify({value: 0, type: 'f32'});

t.is(result.outcome, Outcome.failed);
t.false(result.clarification.includes(`state does not contain 'value'`));
t.true(result.clarification.includes('Expected'));
});
Loading