Skip to content

Commit

Permalink
Bug 1766045 - Adding xpcshell tests for our UniFFI fixtures r=teshaq,…
Browse files Browse the repository at this point in the history
…markh

To test the bindings generation, we generate JS bindings for a set of test
Rust crates, then run xpcshell tests against those bindings.

Currently these tests only run when the `--enable-uniffi-fixtures`
option is set, so they need to be run manually be a dev.  I'd love to
create a system where these can optionally run in CI, but I'm not sure
how to do that.

Differential Revision: https://phabricator.services.mozilla.com/D144469
  • Loading branch information
bendk committed Aug 3, 2022
1 parent 86a582c commit bc8ce7e
Show file tree
Hide file tree
Showing 8 changed files with 538 additions and 0 deletions.
2 changes: 2 additions & 0 deletions toolkit/components/uniffi-bindgen-gecko-js/fixtures/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ components = [
EXTRA_JS_MODULES["components-utils"] = [
"generated/{}.jsm".format(component) for component in components
]

XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

const Arithmetic = ChromeUtils.import(
"resource://gre/modules/components-utils/Arithmetic.jsm"
);

add_task(async function() {
Assert.ok(Arithmetic.IntegerOverflow);
Assert.equal(await Arithmetic.add(2, 4), 6);
Assert.equal(await Arithmetic.add(4, 8), 12);
// For other backends we would have this test:
// await Assert.rejects(
// Arithmetic.add(18446744073709551615, 1),
// Arithmetic.IntegerOverflow,
// "add() should throw IntegerOverflow")
//
// However, this doesn't work because JS number values are actually 64-bit
// floats, and that number is greater than the maximum "safe" integer.
//
// Instead, let's test that we reject numbers that are that big
await Assert.rejects(
Arithmetic.add(Number.MAX_SAFE_INTEGER + 1, 0),
/TypeError/,
"add() should throw TypeError when an input is > MAX_SAFE_INTEGER"
);

Assert.equal(await Arithmetic.sub(4, 2), 2);
Assert.equal(await Arithmetic.sub(8, 4), 4);
await Assert.rejects(
Arithmetic.sub(0, 1),
Arithmetic.IntegerOverflow,
"sub() should throw IntegerOverflow"
);

Assert.equal(await Arithmetic.div(8, 4), 2);
// Can't test this, because we don't allow Rust panics in FF
// Assert.rejects(
// Arithmetic.div(8, 0),
// (e) => Assert.equal(e, Arithmetic.UniFFIInternalError),
// "Divide by 0 should throw UniFFIInternalError")
//
Assert.ok(await Arithmetic.equal(2, 2));
Assert.ok(await Arithmetic.equal(4, 4));

Assert.ok(!(await Arithmetic.equal(2, 4)));
Assert.ok(!(await Arithmetic.equal(4, 8)));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

const Geometry = ChromeUtils.import(
"resource://gre/modules/components-utils/Geometry.jsm"
);

add_task(async function() {
const ln1 = new Geometry.Line(
new Geometry.Point(0, 0, "p1"),
new Geometry.Point(1, 2, "p2")
);
const ln2 = new Geometry.Line(
new Geometry.Point(1, 1, "p3"),
new Geometry.Point(2, 2, "p4")
);
const origin = new Geometry.Point(0, 0);
Assert.ok((await Geometry.intersection(ln1, ln2)).equals(origin));
Assert.deepEqual(await Geometry.intersection(ln1, ln2), origin);
Assert.strictEqual(await Geometry.intersection(ln1, ln1), null);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

const Rondpoint = ChromeUtils.import(
"resource://gre/modules/components-utils/Rondpoint.jsm"
);

const {
Dictionnaire,
Enumeration,
copieDictionnaire,
copieEnumeration,
copieEnumerations,
copieCarte,
EnumerationAvecDonnees,
switcheroo,
Retourneur,
DictionnaireNombresSignes,
DictionnaireNombres,
Stringifier,
Optionneur,
OptionneurDictionnaire,
} = Rondpoint;
add_task(async function() {
const dico = new Dictionnaire(Enumeration.DEUX, true, 0, 1235);
const copyDico = await copieDictionnaire(dico);
Assert.deepEqual(dico, copyDico);

Assert.equal(await copieEnumeration(Enumeration.DEUX), Enumeration.DEUX);
Assert.deepEqual(
await copieEnumerations([Enumeration.UN, Enumeration.DEUX]),
[Enumeration.UN, Enumeration.DEUX]
);
const obj = {
"0": new EnumerationAvecDonnees.Zero(),
"1": new EnumerationAvecDonnees.Un(1),
"2": new EnumerationAvecDonnees.Deux(2, "deux"),
};

Assert.deepEqual(await copieCarte(obj), obj);

const zero = new EnumerationAvecDonnees.Zero();
const one = new EnumerationAvecDonnees.Un(1);
const two = new EnumerationAvecDonnees.Deux(2);
Assert.notEqual(zero, one);
Assert.notEqual(one, two);

Assert.deepEqual(zero, new EnumerationAvecDonnees.Zero());
Assert.deepEqual(one, new EnumerationAvecDonnees.Un(1));
Assert.notDeepEqual(one, new EnumerationAvecDonnees.Un(4));

Assert.ok(await switcheroo(false));
// Test the roundtrip across the FFI.
// This shows that the values we send come back in exactly the same state as we sent them.
// i.e. it shows that lowering from JS and lifting into rust is symmetrical with
// lowering from rust and lifting into JS.

const rt = await Retourneur.init();

const affirmAllerRetour = async (arr, fn, equalFn) => {
for (const member of arr) {
if (equalFn) {
equalFn(await fn(member), member);
} else {
Assert.equal(await fn(member), member);
}
}
};

// Booleans
await affirmAllerRetour([true, false], rt.identiqueBoolean.bind(rt));

// Bytes
await affirmAllerRetour([-128, 127], rt.identiqueI8.bind(rt));
await affirmAllerRetour([0, 0xff], rt.identiqueU8.bind(rt));

// Shorts
await affirmAllerRetour([-32768, 32767], rt.identiqueI16.bind(rt));
await affirmAllerRetour([0, 0xffff], rt.identiqueU16.bind(rt));

// Ints
await affirmAllerRetour(
[0, 1, -1, -2147483648, 2147483647],
rt.identiqueI32.bind(rt)
);
await affirmAllerRetour([0, 0xffffffff], rt.identiqueU32.bind(rt));

// Longs
// NOTE: we cannot represent greater than `Number.MAX_SAFE_INTEGER`
await affirmAllerRetour(
[0, 1, -1, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER],
rt.identiqueI64.bind(rt)
);
await affirmAllerRetour(
[0, Number.MAX_SAFE_INTEGER],
rt.identiqueU64.bind(rt)
);

// Floats
const equalFloats = (a, b) => Assert.ok(Math.abs(a - b) <= Number.EPSILON);
await affirmAllerRetour(
[0.0, 0.5, 0.25, 1.5],
rt.identiqueFloat.bind(rt),
equalFloats
);
// Some float value's precision gets messed up, an example is 3.22, 100.223, etc
// await affirmAllerRetour([0.0, 0.5, 0.25, 1.5, 100.223], rt.identiqueFloat.bind(rt), equalFloats);

// Double (although on the JS side doubles are limited since they are also represented by Number)
await affirmAllerRetour(
[0.0, 0.5, 0.25, 1.5],
rt.identiqueDouble.bind(rt),
equalFloats
);

// Strings
await affirmAllerRetour(
[
"",
"abc",
"null\u0000byte",
"été",
"ښي لاس ته لوستلو لوستل",
"😻emoji 👨‍👧‍👦multi-emoji, 🇨🇭a flag, a canal, panama",
],
rt.identiqueString.bind(rt)
);

await affirmAllerRetour(
[-1, 0, 1].map(n => new DictionnaireNombresSignes(n, n, n, n)),
rt.identiqueNombresSignes.bind(rt),
(a, b) => Assert.deepEqual(a, b)
);

await affirmAllerRetour(
[0, 1].map(n => new DictionnaireNombres(n, n, n, n)),
rt.identiqueNombres.bind(rt),
(a, b) => Assert.deepEqual(a, b)
);

// Test one way across the FFI.
//
// We send one representation of a value to lib.rs, and it transforms it into another, a string.
// lib.rs sends the string back, and then we compare here in js.
//
// This shows that the values are transformed into strings the same way in both js and rust.
// i.e. if we assume that the string return works (we test this assumption elsewhere)
// we show that lowering from js and lifting into rust has values that both js and rust
// both stringify in the same way. i.e. the same values.
//
// If we roundtripping proves the symmetry of our lowering/lifting from here to rust, and lowering/lifting from rust to here,
// and this convinces us that lowering/lifting from here to rust is correct, then
// together, we've shown the correctness of the return leg.
const st = await Stringifier.init();

const affirmEnchaine = async (arr, fn) => {
for (const member of arr) {
Assert.equal(await fn(member), String(member));
}
};

// Booleans
await affirmEnchaine([true, false], st.toStringBoolean.bind(st));

// Bytes
await affirmEnchaine([-128, 127], st.toStringI8.bind(st));
await affirmEnchaine([0, 0xff], st.toStringU8.bind(st));

// Shorts
await affirmEnchaine([-32768, 32767], st.toStringI16.bind(st));
await affirmEnchaine([0, 0xffff], st.toStringU16.bind(st));

// Ints
await affirmEnchaine(
[0, 1, -1, -2147483648, 2147483647],
st.toStringI32.bind(st)
);
await affirmEnchaine([0, 0xffffffff], st.toStringU32.bind(st));

// Longs
// NOTE: we cannot represent greater than `Number.MAX_SAFE_INTEGER`
await affirmEnchaine(
[0, 1, -1, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER],
st.toStringI64.bind(st)
);
await affirmEnchaine([0, Number.MAX_SAFE_INTEGER], st.toStringU64.bind(st));

// Floats
await affirmEnchaine([0.0, 0.5, 0.25, 1.5], st.toStringFloat.bind(st));

// Doubles
await affirmEnchaine([0.0, 0.5, 0.25, 1.5], st.toStringDouble.bind(st));

// Prove to ourselves that default arguments are being used.
// Step 1: call the methods without arguments, and check against the UDL.
const op = await Optionneur.init();

Assert.equal(await op.sinonString(), "default");

Assert.ok(!(await op.sinonBoolean()));

Assert.deepEqual(await op.sinonSequence(), []);

Assert.equal(await op.sinonNull(), null);
Assert.equal(await op.sinonZero(), 0);

// decimal integers
Assert.equal(await op.sinonI8Dec(), -42);
Assert.equal(await op.sinonU8Dec(), 42);
Assert.equal(await op.sinonI16Dec(), 42);
Assert.equal(await op.sinonU16Dec(), 42);
Assert.equal(await op.sinonI32Dec(), 42);
Assert.equal(await op.sinonU32Dec(), 42);
Assert.equal(await op.sinonI64Dec(), 42);
Assert.equal(await op.sinonU64Dec(), 42);

// hexadecimal integers
Assert.equal(await op.sinonI8Hex(), -0x7f);
Assert.equal(await op.sinonU8Hex(), 0xff);
Assert.equal(await op.sinonI16Hex(), 0x7f);
Assert.equal(await op.sinonU16Hex(), 0xffff);
Assert.equal(await op.sinonI32Hex(), 0x7fffffff);
Assert.equal(await op.sinonU32Hex(), 0xffffffff);
// The following are too big to be represented by js `Number`
// Assert.equal(await op.sinonI64Hex(), 0x7fffffffffffffff);
// Assert.equal(await op.sinonU64Hex(), 0xffffffffffffffff);

// octal integers
Assert.equal(await op.sinonU32Oct(), 0o755);

// floats
Assert.equal(await op.sinonF32(), 42.0);
Assert.equal(await op.sinonF64(), 42.1);

// enums
Assert.equal(await op.sinonEnum(), Enumeration.TROIS);

// Step 2. Convince ourselves that if we pass something else, then that changes the output.
// We have shown something coming out of the sinon methods, but without eyeballing the Rust
// we can't be sure that the arguments will change the return value.

await affirmAllerRetour(["foo", "bar"], op.sinonString.bind(op));
await affirmAllerRetour([true, false], op.sinonBoolean.bind(op));
await affirmAllerRetour([["a", "b"], []], op.sinonSequence.bind(op), (a, b) =>
Assert.deepEqual(a, b)
);

// Optionals
await affirmAllerRetour(["0", "1"], op.sinonNull.bind(op));
await affirmAllerRetour([0, 1], op.sinonZero.bind(op));

// integers
await affirmAllerRetour([0, 1], op.sinonU8Dec.bind(op));
await affirmAllerRetour([0, 1], op.sinonI8Dec.bind(op));
await affirmAllerRetour([0, 1], op.sinonU16Dec.bind(op));
await affirmAllerRetour([0, 1], op.sinonI16Dec.bind(op));
await affirmAllerRetour([0, 1], op.sinonU32Dec.bind(op));
await affirmAllerRetour([0, 1], op.sinonI32Dec.bind(op));
await affirmAllerRetour([0, 1], op.sinonU64Dec.bind(op));
await affirmAllerRetour([0, 1], op.sinonI64Dec.bind(op));

await affirmAllerRetour([0, 1], op.sinonU8Hex.bind(op));
await affirmAllerRetour([0, 1], op.sinonI8Hex.bind(op));
await affirmAllerRetour([0, 1], op.sinonU16Hex.bind(op));
await affirmAllerRetour([0, 1], op.sinonI16Hex.bind(op));
await affirmAllerRetour([0, 1], op.sinonU32Hex.bind(op));
await affirmAllerRetour([0, 1], op.sinonI32Hex.bind(op));
await affirmAllerRetour([0, 1], op.sinonU64Hex.bind(op));
await affirmAllerRetour([0, 1], op.sinonI64Hex.bind(op));
await affirmAllerRetour([0, 1], op.sinonU32Oct.bind(op));

// Floats
await affirmAllerRetour([0.0, 1.0], op.sinonF32.bind(op));
await affirmAllerRetour([0.0, 1.0], op.sinonF64.bind(op));

// enums
await affirmAllerRetour(
[Enumeration.UN, Enumeration.DEUX, Enumeration.TROIS],
op.sinonEnum.bind(op)
);

// Testing defaulting properties in record types.
const defaultes = new OptionneurDictionnaire();
const explicite = new OptionneurDictionnaire(
-8,
8,
-16,
0x10,
-32,
32,
-64,
64,
4.0,
8.0,
true,
"default",
[],
Enumeration.DEUX,
null
);

Assert.deepEqual(defaultes, explicite);

// …and makes sure they travel across and back the FFI.

await affirmAllerRetour(
[defaultes, explicite],
rt.identiqueOptionneurDictionnaire.bind(rt),
(a, b) => Assert.deepEqual(a, b)
);
});
Loading

0 comments on commit bc8ce7e

Please sign in to comment.