diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/unittests')
21 files changed, 8292 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/README.txt b/dom/webgpu/tests/cts/checkout/src/unittests/README.txt new file mode 100644 index 0000000000..17272c3919 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/README.txt @@ -0,0 +1 @@ +Unit tests for CTS framework. diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts new file mode 100644 index 0000000000..0d7b0b2f56 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts @@ -0,0 +1,167 @@ +export const description = ` +Tests for eventualAsyncExpectation and immediateAsyncExpectation. +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js'; +import { assert, objectEquals, rejectOnTimeout, resolveOnTimeout } from '../common/util/util.js'; + +import { TestGroupTest } from './test_group_test.js'; +import { UnitTest } from './unit_test.js'; + +class FixtureToTest extends UnitTest { + public immediateAsyncExpectation<T>(fn: () => Promise<T>): Promise<T> { + return super.immediateAsyncExpectation(fn); + } + public eventualAsyncExpectation<T>(fn: (niceStack: Error) => Promise<T>): void { + super.eventualAsyncExpectation(fn); + } +} + +export const g = makeTestGroup(TestGroupTest); + +g.test('eventual').fn(async t0 => { + const g = makeTestGroupForUnitTesting(FixtureToTest); + + const runState = [0, 0, 0, 0]; + let runStateIndex = 0; + + // Should pass in state 3 + g.test('noawait,resolve').fn(t => { + const idx = runStateIndex++; + + runState[idx] = 1; + t.eventualAsyncExpectation(async () => { + runState[idx] = 2; + await resolveOnTimeout(50); + runState[idx] = 3; + }); + runState[idx] = 4; + }); + + // Should fail in state 4 + g.test('noawait,reject').fn(t => { + const idx = runStateIndex++; + + runState[idx] = 1; + t.eventualAsyncExpectation(async () => { + runState[idx] = 2; + await rejectOnTimeout(50, 'rejected 1'); + runState[idx] = 3; + }); + runState[idx] = 4; + }); + + // Should fail in state 3 + g.test('nested,2').fn(t => { + const idx = runStateIndex++; + + runState[idx] = 1; + t.eventualAsyncExpectation(async () => { + runState[idx] = 2; + await resolveOnTimeout(50); // Wait a bit before adding a new eventualAsyncExpectation + t.eventualAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 1')); + runState[idx] = 3; + }); + runState[idx] = 4; + }); + + // Should fail in state 3 + g.test('nested,4').fn(t => { + const idx = runStateIndex++; + + runState[idx] = 1; + t.eventualAsyncExpectation(async () => { + t.eventualAsyncExpectation(async () => { + t.eventualAsyncExpectation(async () => { + runState[idx] = 2; + await resolveOnTimeout(50); // Wait a bit before adding a new eventualAsyncExpectation + t.eventualAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 2')); + runState[idx] = 3; + }); + }); + }); + runState[idx] = 4; + }); + + const resultsPromise = t0.run(g); + assert(objectEquals(runState, [0, 0, 0, 0])); + + const statuses = Array.from(await resultsPromise).map(([, v]) => v.status); + assert(objectEquals(runState, [3, 4, 3, 3]), () => runState.toString()); + assert(objectEquals(statuses, ['pass', 'fail', 'fail', 'fail']), () => statuses.toString()); +}); + +g.test('immediate').fn(async t0 => { + const g = makeTestGroupForUnitTesting(FixtureToTest); + + const runState = [0, 0, 0, 0, 0]; + + g.test('noawait,resolve').fn(t => { + runState[0] = 1; + void t.immediateAsyncExpectation(async () => { + runState[0] = 2; + await resolveOnTimeout(50); + runState[0] = 3; + }); + runState[0] = 4; + }); + + // (Can't g.test('noawait,reject') because it causes a top-level Promise + // rejection which crashes Node.) + + g.test('await,resolve').fn(async t => { + runState[1] = 1; + await t.immediateAsyncExpectation(async () => { + runState[1] = 2; + await resolveOnTimeout(50); + runState[1] = 3; + }); + }); + + g.test('await,reject').fn(async t => { + runState[2] = 1; + await t.immediateAsyncExpectation(async () => { + runState[2] = 2; + await rejectOnTimeout(50, 'rejected 3'); + runState[2] = 3; + }); + }); + + // (Similarly can't test 'nested,noawait'.) + + g.test('nested,await,2').fn(t => { + runState[3] = 1; + t.eventualAsyncExpectation(async () => { + runState[3] = 2; + await resolveOnTimeout(50); // Wait a bit before adding a new immediateAsyncExpectation + runState[3] = 3; + await t.immediateAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 3')); + runState[3] = 5; + }); + runState[3] = 4; + }); + + g.test('nested,await,4').fn(t => { + runState[4] = 1; + t.eventualAsyncExpectation(async () => { + t.eventualAsyncExpectation(async () => { + t.eventualAsyncExpectation(async () => { + runState[4] = 2; + await resolveOnTimeout(50); // Wait a bit before adding a new immediateAsyncExpectation + runState[4] = 3; + await t.immediateAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 3')); + runState[4] = 5; + }); + }); + }); + runState[4] = 4; + }); + + const resultsPromise = t0.run(g); + assert(objectEquals(runState, [0, 0, 0, 0, 0])); + + const statuses = Array.from(await resultsPromise).map(([, v]) => v.status); + assert(objectEquals(runState, [3, 3, 2, 3, 3])); + assert(objectEquals(statuses, ['fail', 'pass', 'fail', 'fail', 'fail'])); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts new file mode 100644 index 0000000000..f80444f03b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts @@ -0,0 +1,35 @@ +export const description = ` +Basic unit tests for test framework. +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; + +import { UnitTest } from './unit_test.js'; + +export const g = makeTestGroup(UnitTest); + +g.test('test,sync').fn(t => {}); + +g.test('test,async').fn(async t => {}); + +g.test('test_with_params,sync') + .paramsSimple([{}]) + .fn(t => { + t.debug(JSON.stringify(t.params)); + }); + +g.test('test_with_params,async') + .paramsSimple([{}]) + .fn(async t => { + t.debug(JSON.stringify(t.params)); + }); + +g.test('test_with_params,private_params') + .paramsSimple([ + { a: 1, b: 2, _result: 3 }, // + { a: 4, b: -3, _result: 1 }, + ]) + .fn(t => { + const { a, b, _result } = t.params; + t.expect(a + b === _result); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts new file mode 100644 index 0000000000..1a722a1b86 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts @@ -0,0 +1,71 @@ +export const description = `Unit tests for check_contents`; + +import { Fixture } from '../common/framework/fixture.js'; +import { makeTestGroup } from '../common/internal/test_group.js'; +import { ErrorWithExtra } from '../common/util/util.js'; +import { checkElementsEqual } from '../webgpu/util/check_contents.js'; + +class F extends Fixture { + test(substr: undefined | string, result: undefined | ErrorWithExtra) { + if (substr === undefined) { + this.expect(result === undefined, result?.message); + } else { + this.expect(result !== undefined && result.message.indexOf(substr) !== -1, result?.message); + } + } +} + +export const g = makeTestGroup(F); + +g.test('checkElementsEqual').fn(t => { + t.shouldThrow('Error', () => checkElementsEqual(new Uint8Array(), new Uint16Array())); + t.shouldThrow('Error', () => checkElementsEqual(new Uint32Array(), new Float32Array())); + t.shouldThrow('Error', () => checkElementsEqual(new Uint8Array([]), new Uint8Array([0]))); + t.shouldThrow('Error', () => checkElementsEqual(new Uint8Array([0]), new Uint8Array([]))); + { + t.test(undefined, checkElementsEqual(new Uint8Array([]), new Uint8Array([]))); + t.test(undefined, checkElementsEqual(new Uint8Array([0]), new Uint8Array([0]))); + t.test(undefined, checkElementsEqual(new Uint8Array([1]), new Uint8Array([1]))); + t.test( + ` + Starting at index 0: + actual == 0x: 00 + failed -> xx + expected == 01`, + checkElementsEqual(new Uint8Array([0]), new Uint8Array([1])) + ); + t.test( + 'expected == 01 02 01', + checkElementsEqual(new Uint8Array([1, 1, 1]), new Uint8Array([1, 2, 1])) + ); + } + { + const actual = new Uint8Array(280); + const exp = new Uint8Array(280); + for (let i = 2; i < 20; ++i) actual[i] = i - 4; + t.test( + '00 fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00', + checkElementsEqual(actual, exp) + ); + for (let i = 2; i < 280; ++i) actual[i] = i - 4; + t.test('Starting at index 1:', checkElementsEqual(actual, exp)); + for (let i = 0; i < 2; ++i) actual[i] = i - 4; + t.test('Starting at index 0:', checkElementsEqual(actual, exp)); + } + { + const actual = new Int32Array(30); + const exp = new Int32Array(30); + for (let i = 2; i < 7; ++i) actual[i] = i - 3; + t.test('00000002 00000003 00000000\n', checkElementsEqual(actual, exp)); + for (let i = 2; i < 30; ++i) actual[i] = i - 3; + t.test('00000000 00000000 ...', checkElementsEqual(actual, exp)); + } + { + const actual = new Float64Array(30); + const exp = new Float64Array(30); + for (let i = 2; i < 7; ++i) actual[i] = (i - 4) * 1e100; + t.test('2.000e+100 0.000\n', checkElementsEqual(actual, exp)); + for (let i = 2; i < 280; ++i) actual[i] = (i - 4) * 1e100; + t.test('6.000e+100 7.000e+100 ...', checkElementsEqual(actual, exp)); + } +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts new file mode 100644 index 0000000000..1255b4c548 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts @@ -0,0 +1,408 @@ +export const description = `Unit tests for conversion`; + +import { makeTestGroup } from '../common/internal/test_group.js'; +import { objectEquals } from '../common/util/util.js'; +import { kValue } from '../webgpu/util/constants.js'; +import { + bool, + f16Bits, + f32, + f32Bits, + float16BitsToFloat32, + float32ToFloat16Bits, + float32ToFloatBits, + floatBitsToNormalULPFromZero, + floatBitsToNumber, + i32, + kFloat16Format, + kFloat32Format, + pack2x16float, + pack2x16snorm, + pack2x16unorm, + pack4x8snorm, + pack4x8unorm, + Scalar, + u32, + vec2, + vec3, + vec4, + Vector, +} from '../webgpu/util/conversion.js'; + +import { UnitTest } from './unit_test.js'; + +export const g = makeTestGroup(UnitTest); + +const cases = [ + [0b0_01111_0000000000, 1], + [0b0_00001_0000000000, 0.00006103515625], + [0b0_01101_0101010101, 0.33325195], + [0b0_11110_1111111111, 65504], + [0b0_00000_0000000000, 0], + [0b0_01110_0000000000, 0.5], + [0b0_01100_1001100110, 0.1999512], + [0b0_01111_0000000001, 1.00097656], + [0b0_10101_1001000000, 100], + [0b1_01100_1001100110, -0.1999512], + [0b1_10101_1001000000, -100], +]; + +g.test('float16BitsToFloat32').fn(t => { + for (const [bits, number] of [ + ...cases, + [0b1_00000_0000000000, -0], // (resulting sign is not actually tested) + [0b0_00000_1111111111, 0.00006104], // subnormal f16 input + [0b1_00000_1111111111, -0.00006104], + ]) { + const actual = float16BitsToFloat32(bits); + t.expect( + // some loose check + Math.abs(actual - number) <= 0.00001, + `for ${bits.toString(2)}, expected ${number}, got ${actual}` + ); + } +}); + +g.test('float32ToFloat16Bits').fn(t => { + for (const [bits, number] of [ + ...cases, + [0b0_00000_0000000000, 0.00001], // input that becomes subnormal in f16 is rounded to 0 + [0b1_00000_0000000000, -0.00001], // and sign is preserved + ]) { + // some loose check + const actual = float32ToFloat16Bits(number); + t.expect( + Math.abs(actual - bits) <= 1, + `for ${number}, expected ${bits.toString(2)}, got ${actual.toString(2)}` + ); + } +}); + +g.test('float32ToFloatBits_floatBitsToNumber') + .paramsSubcasesOnly(u => + u + .combine('signed', [0, 1] as const) + .combine('exponentBits', [5, 8]) + .combine('mantissaBits', [10, 23]) + ) + .fn(t => { + const { signed, exponentBits, mantissaBits } = t.params; + const bias = (1 << (exponentBits - 1)) - 1; + + for (const [, value] of cases) { + if (value < 0 && signed === 0) continue; + const bits = float32ToFloatBits(value, signed, exponentBits, mantissaBits, bias); + const reconstituted = floatBitsToNumber(bits, { signed, exponentBits, mantissaBits, bias }); + t.expect(Math.abs(reconstituted - value) <= 0.0000001, `${reconstituted} vs ${value}`); + } + }); + +g.test('floatBitsToULPFromZero,16').fn(t => { + const test = (bits: number, ulpFromZero: number) => + t.expect(floatBitsToNormalULPFromZero(bits, kFloat16Format) === ulpFromZero, bits.toString(2)); + // Zero + test(0b0_00000_0000000000, 0); + // Subnormal + test(0b0_00000_0000000001, 0); + test(0b1_00000_0000000001, 0); + test(0b0_00000_1111111111, 0); + test(0b1_00000_1111111111, 0); + // Normal + test(0b0_00001_0000000000, 1); // 0 + 1ULP + test(0b1_00001_0000000000, -1); // 0 - 1ULP + test(0b0_00001_0000000001, 2); // 0 + 2ULP + test(0b1_00001_0000000001, -2); // 0 - 2ULP + test(0b0_01110_0000000000, 0b01101_0000000001); // 0.5 + test(0b1_01110_0000000000, -0b01101_0000000001); // -0.5 + test(0b0_01110_1111111110, 0b01101_1111111111); // 1.0 - 2ULP + test(0b1_01110_1111111110, -0b01101_1111111111); // -(1.0 - 2ULP) + test(0b0_01110_1111111111, 0b01110_0000000000); // 1.0 - 1ULP + test(0b1_01110_1111111111, -0b01110_0000000000); // -(1.0 - 1ULP) + test(0b0_01111_0000000000, 0b01110_0000000001); // 1.0 + test(0b1_01111_0000000000, -0b01110_0000000001); // -1.0 + test(0b0_01111_0000000001, 0b01110_0000000010); // 1.0 + 1ULP + test(0b1_01111_0000000001, -0b01110_0000000010); // -(1.0 + 1ULP) + test(0b0_10000_0000000000, 0b01111_0000000001); // 2.0 + test(0b1_10000_0000000000, -0b01111_0000000001); // -2.0 + + const testThrows = (b: number) => + t.shouldThrow('Error', () => floatBitsToNormalULPFromZero(b, kFloat16Format)); + // Infinity + testThrows(0b0_11111_0000000000); + testThrows(0b1_11111_0000000000); + // NaN + testThrows(0b0_11111_1111111111); + testThrows(0b1_11111_1111111111); +}); + +g.test('floatBitsToULPFromZero,32').fn(t => { + const test = (bits: number, ulpFromZero: number) => + t.expect(floatBitsToNormalULPFromZero(bits, kFloat32Format) === ulpFromZero, bits.toString(2)); + // Zero + test(0b0_00000000_00000000000000000000000, 0); + // Subnormal + test(0b0_00000000_00000000000000000000001, 0); + test(0b1_00000000_00000000000000000000001, 0); + test(0b0_00000000_11111111111111111111111, 0); + test(0b1_00000000_11111111111111111111111, 0); + // Normal + test(0b0_00000001_00000000000000000000000, 1); // 0 + 1ULP + test(0b1_00000001_00000000000000000000000, -1); // 0 - 1ULP + test(0b0_00000001_00000000000000000000001, 2); // 0 + 2ULP + test(0b1_00000001_00000000000000000000001, -2); // 0 - 2ULP + test(0b0_01111110_00000000000000000000000, 0b01111101_00000000000000000000001); // 0.5 + test(0b1_01111110_00000000000000000000000, -0b01111101_00000000000000000000001); // -0.5 + test(0b0_01111110_11111111111111111111110, 0b01111101_11111111111111111111111); // 1.0 - 2ULP + test(0b1_01111110_11111111111111111111110, -0b01111101_11111111111111111111111); // -(1.0 - 2ULP) + test(0b0_01111110_11111111111111111111111, 0b01111110_00000000000000000000000); // 1.0 - 1ULP + test(0b1_01111110_11111111111111111111111, -0b01111110_00000000000000000000000); // -(1.0 - 1ULP) + test(0b0_01111111_00000000000000000000000, 0b01111110_00000000000000000000001); // 1.0 + test(0b1_01111111_00000000000000000000000, -0b01111110_00000000000000000000001); // -1.0 + test(0b0_01111111_00000000000000000000001, 0b01111110_00000000000000000000010); // 1.0 + 1ULP + test(0b1_01111111_00000000000000000000001, -0b01111110_00000000000000000000010); // -(1.0 + 1ULP) + test(0b0_11110000_00000000000000000000000, 0b11101111_00000000000000000000001); // 2.0 + test(0b1_11110000_00000000000000000000000, -0b11101111_00000000000000000000001); // -2.0 + + const testThrows = (b: number) => + t.shouldThrow('Error', () => floatBitsToNormalULPFromZero(b, kFloat32Format)); + // Infinity + testThrows(0b0_11111111_00000000000000000000000); + testThrows(0b1_11111111_00000000000000000000000); + // NaN + testThrows(0b0_11111111_11111111111111111111111); + testThrows(0b0_11111111_00000000000000000000001); + testThrows(0b1_11111111_11111111111111111111111); + testThrows(0b1_11111111_00000000000000000000001); +}); + +g.test('scalarWGSL').fn(t => { + const cases: Array<[Scalar, string]> = [ + [f32(0.0), '0.0f'], + [f32(1.0), '1.0f'], + [f32(-1.0), '-1.0f'], + [f32Bits(0x70000000), '1.5845632502852868e+29f'], + [f32Bits(0xf0000000), '-1.5845632502852868e+29f'], + [f16Bits(0), '0.0h'], + [f16Bits(0x3c00), '1.0h'], + [f16Bits(0xbc00), '-1.0h'], + [u32(0), '0u'], + [u32(1), '1u'], + [u32(2000000000), '2000000000u'], + [u32(-1), '4294967295u'], + [i32(0), 'i32(0)'], + [i32(1), 'i32(1)'], + [i32(-1), 'i32(-1)'], + [bool(true), 'true'], + [bool(false), 'false'], + ]; + for (const [value, expect] of cases) { + const got = value.wgsl(); + t.expect( + got === expect, + `[value: ${value.value}, type: ${value.type}] +got: ${got} +expect: ${expect}` + ); + } +}); + +g.test('vectorWGSL').fn(t => { + const cases: Array<[Vector, string]> = [ + [vec2(f32(42.0), f32(24.0)), 'vec2(42.0f, 24.0f)'], + [vec2(f16Bits(0x5140), f16Bits(0x4e00)), 'vec2(42.0h, 24.0h)'], + [vec2(u32(42), u32(24)), 'vec2(42u, 24u)'], + [vec2(i32(42), i32(24)), 'vec2(i32(42), i32(24))'], + [vec2(bool(false), bool(true)), 'vec2(false, true)'], + + [vec3(f32(0.0), f32(1.0), f32(-1.0)), 'vec3(0.0f, 1.0f, -1.0f)'], + [vec3(f16Bits(0), f16Bits(0x3c00), f16Bits(0xbc00)), 'vec3(0.0h, 1.0h, -1.0h)'], + [vec3(u32(0), u32(1), u32(-1)), 'vec3(0u, 1u, 4294967295u)'], + [vec3(i32(0), i32(1), i32(-1)), 'vec3(i32(0), i32(1), i32(-1))'], + [vec3(bool(true), bool(false), bool(true)), 'vec3(true, false, true)'], + + [vec4(f32(1.0), f32(-2.0), f32(4.0), f32(-8.0)), 'vec4(1.0f, -2.0f, 4.0f, -8.0f)'], + [ + vec4(f16Bits(0xbc00), f16Bits(0x4000), f16Bits(0xc400), f16Bits(0x4800)), + 'vec4(-1.0h, 2.0h, -4.0h, 8.0h)', + ], + [vec4(u32(1), u32(-2), u32(4), u32(-8)), 'vec4(1u, 4294967294u, 4u, 4294967288u)'], + [vec4(i32(1), i32(-2), i32(4), i32(-8)), 'vec4(i32(1), i32(-2), i32(4), i32(-8))'], + [vec4(bool(false), bool(true), bool(true), bool(false)), 'vec4(false, true, true, false)'], + ]; + for (const [value, expect] of cases) { + const got = value.wgsl(); + t.expect( + got === expect, + `[values: ${value.elements}, type: ${value.type}] +got: ${got} +expect: ${expect}` + ); + } +}); + +g.test('pack2x16float') + .paramsSimple([ + // f16 normals + { inputs: [0, 0], result: [0x00000000, 0x80000000, 0x00008000, 0x80008000] }, + { inputs: [1, 0], result: [0x00003c00, 0x80003c00] }, + { inputs: [1, 1], result: [0x3c003c00] }, + { inputs: [-1, -1], result: [0xbc00bc00] }, + { inputs: [10, 1], result: [0x3c004900] }, + { inputs: [-10, 1], result: [0x3c00c900] }, + + // f32 normal, but not f16 precise + { inputs: [1.00000011920928955078125, 1], result: [0x3c003c00, 0x3c003c01] }, + + // f32 subnormals + // prettier-ignore + { inputs: [kValue.f32.subnormal.positive.max, 1], result: [0x3c000000, 0x3c008000, 0x3c000001] }, + // prettier-ignore + { inputs: [kValue.f32.subnormal.negative.min, 1], result: [0x3c008001, 0x3c000000, 0x3c008000] }, + + // f16 subnormals + // prettier-ignore + { inputs: [kValue.f16.subnormal.positive.max, 1], result: [0x3c0003ff, 0x3c000000, 0x3c008000] }, + // prettier-ignore + { inputs: [kValue.f16.subnormal.negative.min, 1], result: [0x03c0083ff, 0x3c000000, 0x3c008000] }, + + // f16 out of bounds + { inputs: [kValue.f16.positive.max + 1, 1], result: [undefined] }, + { inputs: [kValue.f16.negative.min - 1, 1], result: [undefined] }, + { inputs: [1, kValue.f16.positive.max + 1], result: [undefined] }, + { inputs: [1, kValue.f16.negative.min - 1], result: [undefined] }, + ] as const) + .fn(test => { + const toString = (data: readonly (undefined | number)[]): String[] => { + return data.map(d => (d !== undefined ? u32(d).toString() : 'undefined')); + }; + + const inputs = test.params.inputs; + const got = pack2x16float(inputs[0], inputs[1]); + const expect = test.params.result; + + const got_str = toString(got); + const expect_str = toString(expect); + + // Using strings of the outputs, so they can be easily sorted, since order of the results doesn't matter. + test.expect( + objectEquals(got_str.sort(), expect_str.sort()), + `pack2x16float(${inputs}) returned [${got_str}]. Expected [${expect_str}]` + ); + }); + +g.test('pack2x16snorm') + .paramsSimple([ + // Normals + { inputs: [0, 0], result: 0x00000000 }, + { inputs: [1, 0], result: 0x00007fff }, + { inputs: [0, 1], result: 0x7fff0000 }, + { inputs: [1, 1], result: 0x7fff7fff }, + { inputs: [-1, -1], result: 0x80018001 }, + { inputs: [10, 10], result: 0x7fff7fff }, + { inputs: [-10, -10], result: 0x80018001 }, + { inputs: [0.1, 0.1], result: 0x0ccd0ccd }, + { inputs: [-0.1, -0.1], result: 0xf333f333 }, + { inputs: [0.5, 0.5], result: 0x40004000 }, + { inputs: [-0.5, -0.5], result: 0xc001c001 }, + { inputs: [0.1, 0.5], result: 0x40000ccd }, + { inputs: [-0.1, -0.5], result: 0xc001f333 }, + + // Subnormals + { inputs: [kValue.f32.subnormal.positive.max, 1], result: 0x7fff0000 }, + { inputs: [kValue.f32.subnormal.negative.min, 1], result: 0x7fff0000 }, + ] as const) + .fn(test => { + const inputs = test.params.inputs; + const got = pack2x16snorm(inputs[0], inputs[1]); + const expect = test.params.result; + + test.expect(got === expect, `pack2x16snorm(${inputs}) returned ${got}. Expected ${expect}`); + }); + +g.test('pack2x16unorm') + .paramsSimple([ + // Normals + { inputs: [0, 0], result: 0x00000000 }, + { inputs: [1, 0], result: 0x0000ffff }, + { inputs: [0, 1], result: 0xffff0000 }, + { inputs: [1, 1], result: 0xffffffff }, + { inputs: [-1, -1], result: 0x00000000 }, + { inputs: [0.1, 0.1], result: 0x199a199a }, + { inputs: [0.5, 0.5], result: 0x80008000 }, + { inputs: [0.1, 0.5], result: 0x8000199a }, + { inputs: [10, 10], result: 0xffffffff }, + + // Subnormals + { inputs: [kValue.f32.subnormal.positive.max, 1], result: 0xffff0000 }, + ] as const) + .fn(test => { + const inputs = test.params.inputs; + const got = pack2x16unorm(inputs[0], inputs[1]); + const expect = test.params.result; + + test.expect(got === expect, `pack2x16unorm(${inputs}) returned ${got}. Expected ${expect}`); + }); + +g.test('pack4x8snorm') + .paramsSimple([ + // Normals + { inputs: [0, 0, 0, 0], result: 0x00000000 }, + { inputs: [1, 0, 0, 0], result: 0x0000007f }, + { inputs: [0, 1, 0, 0], result: 0x00007f00 }, + { inputs: [0, 0, 1, 0], result: 0x007f0000 }, + { inputs: [0, 0, 0, 1], result: 0x7f000000 }, + { inputs: [1, 1, 1, 1], result: 0x7f7f7f7f }, + { inputs: [10, 10, 10, 10], result: 0x7f7f7f7f }, + { inputs: [-1, 0, 0, 0], result: 0x00000081 }, + { inputs: [0, -1, 0, 0], result: 0x00008100 }, + { inputs: [0, 0, -1, 0], result: 0x00810000 }, + { inputs: [0, 0, 0, -1], result: 0x81000000 }, + { inputs: [-1, -1, -1, -1], result: 0x81818181 }, + { inputs: [-10, -10, -10, -10], result: 0x81818181 }, + { inputs: [0.1, 0.1, 0.1, 0.1], result: 0x0d0d0d0d }, + { inputs: [-0.1, -0.1, -0.1, -0.1], result: 0xf3f3f3f3 }, + { inputs: [0.1, -0.1, 0.1, -0.1], result: 0xf30df30d }, + { inputs: [0.5, 0.5, 0.5, 0.5], result: 0x40404040 }, + { inputs: [-0.5, -0.5, -0.5, -0.5], result: 0xc1c1c1c1 }, + { inputs: [-0.5, 0.5, -0.5, 0.5], result: 0x40c140c1 }, + { inputs: [0.1, 0.5, 0.1, 0.5], result: 0x400d400d }, + { inputs: [-0.1, -0.5, -0.1, -0.5], result: 0xc1f3c1f3 }, + + // Subnormals + { inputs: [kValue.f32.subnormal.positive.max, 1, 1, 1], result: 0x7f7f7f00 }, + { inputs: [kValue.f32.subnormal.negative.min, 1, 1, 1], result: 0x7f7f7f00 }, + ] as const) + .fn(test => { + const inputs = test.params.inputs; + const got = pack4x8snorm(inputs[0], inputs[1], inputs[2], inputs[3]); + const expect = test.params.result; + + test.expect(got === expect, `pack4x8snorm(${inputs}) returned ${u32(got)}. Expected ${expect}`); + }); + +g.test('pack4x8unorm') + .paramsSimple([ + // Normals + { inputs: [0, 0, 0, 0], result: 0x00000000 }, + { inputs: [1, 0, 0, 0], result: 0x000000ff }, + { inputs: [0, 1, 0, 0], result: 0x0000ff00 }, + { inputs: [0, 0, 1, 0], result: 0x00ff0000 }, + { inputs: [0, 0, 0, 1], result: 0xff000000 }, + { inputs: [1, 1, 1, 1], result: 0xffffffff }, + { inputs: [10, 10, 10, 10], result: 0xffffffff }, + { inputs: [-1, -1, -1, -1], result: 0x00000000 }, + { inputs: [-10, -10, -10, -10], result: 0x00000000 }, + { inputs: [0.1, 0.1, 0.1, 0.1], result: 0x1a1a1a1a }, + { inputs: [0.5, 0.5, 0.5, 0.5], result: 0x80808080 }, + { inputs: [0.1, 0.5, 0.1, 0.5], result: 0x801a801a }, + + // Subnormals + { inputs: [kValue.f32.subnormal.positive.max, 1, 1, 1], result: 0xffffff00 }, + ] as const) + .fn(test => { + const inputs = test.params.inputs; + const got = pack4x8unorm(inputs[0], inputs[1], inputs[2], inputs[3]); + const expect = test.params.result; + + test.expect(got === expect, `pack4x8unorm(${inputs}) returned ${got}. Expected ${expect}`); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/f32_interval.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/f32_interval.spec.ts new file mode 100644 index 0000000000..435d78ecd6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/f32_interval.spec.ts @@ -0,0 +1,3418 @@ +export const description = ` +F32Interval unit tests. +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { objectEquals } from '../common/util/util.js'; +import { kValue } from '../webgpu/util/constants.js'; +import { + absInterval, + absoluteErrorInterval, + acosInterval, + acoshAlternativeInterval, + acoshPrimaryInterval, + additionInterval, + asinInterval, + asinhInterval, + atanInterval, + atan2Interval, + atanhInterval, + ceilInterval, + clampMedianInterval, + clampMinMaxInterval, + correctlyRoundedInterval, + cosInterval, + coshInterval, + crossInterval, + degreesInterval, + distanceInterval, + divisionInterval, + dotInterval, + expInterval, + exp2Interval, + F32Interval, + faceForwardIntervals, + floorInterval, + fmaInterval, + fractInterval, + IntervalBounds, + inverseSqrtInterval, + ldexpInterval, + lengthInterval, + logInterval, + log2Interval, + maxInterval, + minInterval, + mixImpreciseInterval, + mixPreciseInterval, + multiplicationInterval, + negationInterval, + normalizeInterval, + powInterval, + quantizeToF16Interval, + radiansInterval, + reflectInterval, + refractInterval, + remainderInterval, + roundInterval, + saturateInterval, + signInterval, + sinInterval, + sinhInterval, + smoothStepInterval, + sqrtInterval, + stepInterval, + subtractionInterval, + tanInterval, + tanhInterval, + toF32Vector, + truncInterval, + ulpInterval, + unpack2x16floatInterval, + unpack2x16snormInterval, + unpack2x16unormInterval, + unpack4x8snormInterval, + unpack4x8unormInterval, + modfInterval, + toF32Interval, +} from '../webgpu/util/f32_interval.js'; +import { hexToF32, hexToF64, oneULP } from '../webgpu/util/math.js'; + +import { UnitTest } from './unit_test.js'; + +export const g = makeTestGroup(UnitTest); + +/** Bounds indicating an expectation of an interval of all possible values */ +const kAny: IntervalBounds = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY]; + +/** @returns a number N * ULP greater than the provided number */ +function plusNULP(x: number, n: number): number { + return x + n * oneULP(x); +} + +/** @returns a number one ULP greater than the provided number */ +function plusOneULP(x: number): number { + return plusNULP(x, 1); +} + +/** @returns a number N * ULP less than the provided number */ +function minusNULP(x: number, n: number): number { + return x - n * oneULP(x); +} + +/** @returns a number one ULP less than the provided number */ +function minusOneULP(x: number): number { + return minusNULP(x, 1); +} + +/** @returns the expected IntervalBounds adjusted by the given error function + * + * @param expected the bounds to be adjusted + * @param error error function to adjust the bounds via + */ +function applyError(expected: IntervalBounds, error: (n: number) => number): IntervalBounds { + if (expected !== kAny) { + const begin = expected[0]; + const end = expected.length === 2 ? expected[1] : begin; + expected = [begin - error(begin), end + error(end)]; + } + + return expected; +} + +interface ConstructorCase { + input: IntervalBounds; + expected: IntervalBounds; +} + +g.test('constructor') + .paramsSubcasesOnly<ConstructorCase>( + // prettier-ignore + [ + // Common cases + { input: [0, 10], expected: [0, 10]}, + { input: [-5, 0], expected: [-5, 0]}, + { input: [-5, 10], expected: [-5, 10]}, + { input: [0], expected: [0]}, + { input: [10], expected: [10]}, + { input: [-5], expected: [-5]}, + + // Edges + { input: [0, kValue.f32.positive.max], expected: [0, kValue.f32.positive.max]}, + { input: [kValue.f32.negative.min, 0], expected: [kValue.f32.negative.min, 0]}, + { input: [kValue.f32.negative.min, kValue.f32.positive.max], expected: [kValue.f32.negative.min, kValue.f32.positive.max]}, + + // Out of range + { input: [0, 2 * kValue.f32.positive.max], expected: [0, 2 * kValue.f32.positive.max]}, + { input: [2 * kValue.f32.negative.min, 0], expected: [2 * kValue.f32.negative.min, 0]}, + { input: [2 * kValue.f32.negative.min, 2 * kValue.f32.positive.max], expected: [2 * kValue.f32.negative.min, 2 * kValue.f32.positive.max]}, + + // Infinities + { input: [0, kValue.f32.infinity.positive], expected: [0, Number.POSITIVE_INFINITY]}, + { input: [kValue.f32.infinity.negative, 0], expected: [Number.NEGATIVE_INFINITY, 0]}, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny}, + ] + ) + .fn(t => { + const i = new F32Interval(...t.params.input); + t.expect( + objectEquals(i.bounds(), t.params.expected), + `F32Interval([${t.params.input}]) returned ${i}. Expected [${t.params.expected}]` + ); + }); + +interface ContainsNumberCase { + bounds: IntervalBounds; + value: number; + expected: boolean; +} + +g.test('contains_number') + .paramsSubcasesOnly<ContainsNumberCase>( + // prettier-ignore + [ + // Common usage + { bounds: [0, 10], value: 0, expected: true }, + { bounds: [0, 10], value: 10, expected: true }, + { bounds: [0, 10], value: 5, expected: true }, + { bounds: [0, 10], value: -5, expected: false }, + { bounds: [0, 10], value: 50, expected: false }, + { bounds: [0, 10], value: Number.NaN, expected: false }, + { bounds: [-5, 10], value: 0, expected: true }, + { bounds: [-5, 10], value: 10, expected: true }, + { bounds: [-5, 10], value: 5, expected: true }, + { bounds: [-5, 10], value: -5, expected: true }, + { bounds: [-5, 10], value: -6, expected: false }, + { bounds: [-5, 10], value: 50, expected: false }, + { bounds: [-5, 10], value: -10, expected: false }, + + // Point + { bounds: [0], value: 0, expected: true }, + { bounds: [0], value: 10, expected: false }, + { bounds: [0], value: -1000, expected: false }, + { bounds: [10], value: 10, expected: true }, + { bounds: [10], value: 0, expected: false }, + { bounds: [10], value: -10, expected: false }, + { bounds: [10], value: 11, expected: false }, + + // Upper infinity + { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.positive.min, expected: true }, + { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.positive.max, expected: true }, + { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.infinity.positive, expected: true }, + { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.negative.min, expected: false }, + { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.negative.max, expected: false }, + { bounds: [0, kValue.f32.infinity.positive], value: kValue.f32.infinity.negative, expected: false }, + + // Lower infinity + { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.positive.min, expected: false }, + { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.positive.max, expected: false }, + { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.infinity.positive, expected: false }, + { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.negative.min, expected: true }, + { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.negative.max, expected: true }, + { bounds: [kValue.f32.infinity.negative, 0], value: kValue.f32.infinity.negative, expected: true }, + + // Full infinity + { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.positive.min, expected: true }, + { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.positive.max, expected: true }, + { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.infinity.positive, expected: true }, + { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.negative.min, expected: true }, + { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.negative.max, expected: true }, + { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: kValue.f32.infinity.negative, expected: true }, + { bounds: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], value: Number.NaN, expected: true }, + + // Maximum f32 boundary + { bounds: [0, kValue.f32.positive.max], value: kValue.f32.positive.min, expected: true }, + { bounds: [0, kValue.f32.positive.max], value: kValue.f32.positive.max, expected: true }, + { bounds: [0, kValue.f32.positive.max], value: kValue.f32.infinity.positive, expected: false }, + { bounds: [0, kValue.f32.positive.max], value: kValue.f32.negative.min, expected: false }, + { bounds: [0, kValue.f32.positive.max], value: kValue.f32.negative.max, expected: false }, + { bounds: [0, kValue.f32.positive.max], value: kValue.f32.infinity.negative, expected: false }, + + // Minimum f32 boundary + { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.positive.min, expected: false }, + { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.positive.max, expected: false }, + { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.infinity.positive, expected: false }, + { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.negative.min, expected: true }, + { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.negative.max, expected: true }, + { bounds: [kValue.f32.negative.min, 0], value: kValue.f32.infinity.negative, expected: false }, + + // Out of range high + { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.positive.min, expected: true }, + { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.positive.max, expected: true }, + { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.infinity.positive, expected: false }, + { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.negative.min, expected: false }, + { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.negative.max, expected: false }, + { bounds: [0, 2 * kValue.f32.positive.max], value: kValue.f32.infinity.negative, expected: false }, + + // Out of range low + { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.positive.min, expected: false }, + { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.positive.max, expected: false }, + { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.infinity.positive, expected: false }, + { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.negative.min, expected: true }, + { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.negative.max, expected: true }, + { bounds: [2 * kValue.f32.negative.min, 0], value: kValue.f32.infinity.negative, expected: false }, + + // Subnormals + { bounds: [0, kValue.f32.positive.min], value: kValue.f32.subnormal.positive.min, expected: true }, + { bounds: [0, kValue.f32.positive.min], value: kValue.f32.subnormal.positive.max, expected: true }, + { bounds: [0, kValue.f32.positive.min], value: kValue.f32.subnormal.negative.min, expected: false }, + { bounds: [0, kValue.f32.positive.min], value: kValue.f32.subnormal.negative.max, expected: false }, + { bounds: [kValue.f32.negative.max, 0], value: kValue.f32.subnormal.positive.min, expected: false }, + { bounds: [kValue.f32.negative.max, 0], value: kValue.f32.subnormal.positive.max, expected: false }, + { bounds: [kValue.f32.negative.max, 0], value: kValue.f32.subnormal.negative.min, expected: true }, + { bounds: [kValue.f32.negative.max, 0], value: kValue.f32.subnormal.negative.max, expected: true }, + { bounds: [0, kValue.f32.subnormal.positive.min], value: kValue.f32.subnormal.positive.min, expected: true }, + { bounds: [0, kValue.f32.subnormal.positive.min], value: kValue.f32.subnormal.positive.max, expected: false }, + { bounds: [0, kValue.f32.subnormal.positive.min], value: kValue.f32.subnormal.negative.min, expected: false }, + { bounds: [0, kValue.f32.subnormal.positive.min], value: kValue.f32.subnormal.negative.max, expected: false }, + { bounds: [kValue.f32.subnormal.negative.max, 0], value: kValue.f32.subnormal.positive.min, expected: false }, + { bounds: [kValue.f32.subnormal.negative.max, 0], value: kValue.f32.subnormal.positive.max, expected: false }, + { bounds: [kValue.f32.subnormal.negative.max, 0], value: kValue.f32.subnormal.negative.min, expected: false }, + { bounds: [kValue.f32.subnormal.negative.max, 0], value: kValue.f32.subnormal.negative.max, expected: true }, + ] + ) + .fn(t => { + const i = new F32Interval(...t.params.bounds); + const value = t.params.value; + const expected = t.params.expected; + + const got = i.contains(value); + t.expect(expected === got, `${i}.contains(${value}) returned ${got}. Expected ${expected}`); + }); + +interface ContainsIntervalCase { + lhs: IntervalBounds; + rhs: IntervalBounds; + expected: boolean; +} + +g.test('contains_interval') + .paramsSubcasesOnly<ContainsIntervalCase>( + // prettier-ignore + [ + // Common usage + { lhs: [-10, 10], rhs: [0], expected: true}, + { lhs: [-10, 10], rhs: [-1, 0], expected: true}, + { lhs: [-10, 10], rhs: [0, 2], expected: true}, + { lhs: [-10, 10], rhs: [-1, 2], expected: true}, + { lhs: [-10, 10], rhs: [0, 10], expected: true}, + { lhs: [-10, 10], rhs: [-10, 2], expected: true}, + { lhs: [-10, 10], rhs: [-10, 10], expected: true}, + { lhs: [-10, 10], rhs: [-100, 10], expected: false}, + + // Upper infinity + { lhs: [0, kValue.f32.infinity.positive], rhs: [0], expected: true}, + { lhs: [0, kValue.f32.infinity.positive], rhs: [-1, 0], expected: false}, + { lhs: [0, kValue.f32.infinity.positive], rhs: [0, 1], expected: true}, + { lhs: [0, kValue.f32.infinity.positive], rhs: [0, kValue.f32.positive.max], expected: true}, + { lhs: [0, kValue.f32.infinity.positive], rhs: [0, kValue.f32.infinity.positive], expected: true}, + { lhs: [0, kValue.f32.infinity.positive], rhs: [100, kValue.f32.infinity.positive], expected: true}, + { lhs: [0, kValue.f32.infinity.positive], rhs: [Number.NEGATIVE_INFINITY, kValue.f32.infinity.positive], expected: false}, + + // Lower infinity + { lhs: [kValue.f32.infinity.negative, 0], rhs: [0], expected: true}, + { lhs: [kValue.f32.infinity.negative, 0], rhs: [-1, 0], expected: true}, + { lhs: [kValue.f32.infinity.negative, 0], rhs: [kValue.f32.negative.min, 0], expected: true}, + { lhs: [kValue.f32.infinity.negative, 0], rhs: [0, 1], expected: false}, + { lhs: [kValue.f32.infinity.negative, 0], rhs: [kValue.f32.infinity.negative, 0], expected: true}, + { lhs: [kValue.f32.infinity.negative, 0], rhs: [kValue.f32.infinity.negative, -100 ], expected: true}, + { lhs: [kValue.f32.infinity.negative, 0], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false}, + + // Full infinity + { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [0], expected: true}, + { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [-1, 0], expected: true}, + { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [0, 1], expected: true}, + { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [0, kValue.f32.infinity.positive], expected: true}, + { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [100, kValue.f32.infinity.positive], expected: true}, + { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [kValue.f32.infinity.negative, 0], expected: true}, + { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [kValue.f32.infinity.negative, -100 ], expected: true}, + { lhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: true}, + + // Maximum f32 boundary + { lhs: [0, kValue.f32.positive.max], rhs: [0], expected: true}, + { lhs: [0, kValue.f32.positive.max], rhs: [-1, 0], expected: false}, + { lhs: [0, kValue.f32.positive.max], rhs: [0, 1], expected: true}, + { lhs: [0, kValue.f32.positive.max], rhs: [0, kValue.f32.positive.max], expected: true}, + { lhs: [0, kValue.f32.positive.max], rhs: [0, kValue.f32.infinity.positive], expected: false}, + { lhs: [0, kValue.f32.positive.max], rhs: [100, kValue.f32.infinity.positive], expected: false}, + { lhs: [0, kValue.f32.positive.max], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false}, + + // Minimum f32 boundary + { lhs: [kValue.f32.negative.min, 0], rhs: [0, 0], expected: true}, + { lhs: [kValue.f32.negative.min, 0], rhs: [-1, 0], expected: true}, + { lhs: [kValue.f32.negative.min, 0], rhs: [kValue.f32.negative.min, 0], expected: true}, + { lhs: [kValue.f32.negative.min, 0], rhs: [0, 1], expected: false}, + { lhs: [kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, 0], expected: false}, + { lhs: [kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, -100 ], expected: false}, + { lhs: [kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false}, + + // Out of range high + { lhs: [0, 2 * kValue.f32.positive.max], rhs: [0], expected: true}, + { lhs: [0, 2 * kValue.f32.positive.max], rhs: [-1, 0], expected: false}, + { lhs: [0, 2 * kValue.f32.positive.max], rhs: [0, 1], expected: true}, + { lhs: [0, 2 * kValue.f32.positive.max], rhs: [0, kValue.f32.positive.max], expected: true}, + { lhs: [0, 2 * kValue.f32.positive.max], rhs: [0, kValue.f32.infinity.positive], expected: false}, + { lhs: [0, 2 * kValue.f32.positive.max], rhs: [100, kValue.f32.infinity.positive], expected: false}, + { lhs: [0, 2 * kValue.f32.positive.max], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false}, + + // Out of range low + { lhs: [2 * kValue.f32.negative.min, 0], rhs: [0], expected: true}, + { lhs: [2 * kValue.f32.negative.min, 0], rhs: [-1, 0], expected: true}, + { lhs: [2 * kValue.f32.negative.min, 0], rhs: [kValue.f32.negative.min, 0], expected: true}, + { lhs: [2 * kValue.f32.negative.min, 0], rhs: [0, 1], expected: false}, + { lhs: [2 * kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, 0], expected: false}, + { lhs: [2 * kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, -100 ], expected: false}, + { lhs: [2 * kValue.f32.negative.min, 0], rhs: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: false}, + ] + ) + .fn(t => { + const lhs = new F32Interval(...t.params.lhs); + const rhs = new F32Interval(...t.params.rhs); + const expected = t.params.expected; + + const got = lhs.contains(rhs); + t.expect(expected === got, `${lhs}.contains(${rhs}) returned ${got}. Expected ${expected}`); + }); + +interface SpanCase { + intervals: IntervalBounds[]; + expected: IntervalBounds; +} + +g.test('span') + .paramsSubcasesOnly<SpanCase>( + // prettier-ignore + [ + // Single Intervals + { intervals: [[0, 10]], expected: [0, 10]}, + { intervals: [[0, kValue.f32.positive.max]], expected: [0, kValue.f32.positive.max]}, + { intervals: [[0, kValue.f32.positive.nearest_max]], expected: [0, kValue.f32.positive.nearest_max]}, + { intervals: [[0, kValue.f32.infinity.positive]], expected: [0, Number.POSITIVE_INFINITY]}, + { intervals: [[kValue.f32.negative.min, 0]], expected: [kValue.f32.negative.min, 0]}, + { intervals: [[kValue.f32.negative.nearest_min, 0]], expected: [kValue.f32.negative.nearest_min, 0]}, + { intervals: [[kValue.f32.infinity.negative, 0]], expected: [Number.NEGATIVE_INFINITY, 0]}, + + // Double Intervals + { intervals: [[0, 1], [2, 5]], expected: [0, 5]}, + { intervals: [[2, 5], [0, 1]], expected: [0, 5]}, + { intervals: [[0, 2], [1, 5]], expected: [0, 5]}, + { intervals: [[0, 5], [1, 2]], expected: [0, 5]}, + { intervals: [[kValue.f32.infinity.negative, 0], [0, kValue.f32.infinity.positive]], expected: kAny}, + + // Multiple Intervals + { intervals: [[0, 1], [2, 3], [4, 5]], expected: [0, 5]}, + { intervals: [[0, 1], [4, 5], [2, 3]], expected: [0, 5]}, + { intervals: [[0, 1], [0, 1], [0, 1]], expected: [0, 1]}, + ] + ) + .fn(t => { + const intervals = t.params.intervals.map(toF32Interval); + const expected = toF32Interval(t.params.expected); + + const got = F32Interval.span(...intervals); + t.expect( + objectEquals(got, expected), + `span({${intervals}}) returned ${got}. Expected ${expected}` + ); + }); + +interface CorrectlyRoundedCase { + value: number; + expected: IntervalBounds; +} + +g.test('correctlyRoundedInterval') + .paramsSubcasesOnly<CorrectlyRoundedCase>( + // prettier-ignore + [ + // Edge Cases + { value: kValue.f32.infinity.positive, expected: kAny }, + { value: kValue.f32.infinity.negative, expected: kAny }, + { value: kValue.f32.positive.max, expected: [kValue.f32.positive.max] }, + { value: kValue.f32.negative.min, expected: [kValue.f32.negative.min] }, + { value: kValue.f32.positive.min, expected: [kValue.f32.positive.min] }, + { value: kValue.f32.negative.max, expected: [kValue.f32.negative.max] }, + + // 32-bit subnormals + { value: kValue.f32.subnormal.positive.min, expected: [0, kValue.f32.subnormal.positive.min] }, + { value: kValue.f32.subnormal.positive.max, expected: [0, kValue.f32.subnormal.positive.max] }, + { value: kValue.f32.subnormal.negative.min, expected: [kValue.f32.subnormal.negative.min, 0] }, + { value: kValue.f32.subnormal.negative.max, expected: [kValue.f32.subnormal.negative.max, 0] }, + + // 64-bit subnormals + { value: hexToF64(0x00000000, 0x00000001), expected: [0, kValue.f32.subnormal.positive.min] }, + { value: hexToF64(0x00000000, 0x00000002), expected: [0, kValue.f32.subnormal.positive.min] }, + { value: hexToF64(0x800fffff, 0xffffffff), expected: [kValue.f32.subnormal.negative.max, 0] }, + { value: hexToF64(0x800fffff, 0xfffffffe), expected: [kValue.f32.subnormal.negative.max, 0] }, + + // 32-bit normals + { value: 0, expected: [0, 0] }, + { value: hexToF32(0x03800000), expected: [hexToF32(0x03800000)] }, + { value: hexToF32(0x03800001), expected: [hexToF32(0x03800001)] }, + { value: hexToF32(0x83800000), expected: [hexToF32(0x83800000)] }, + { value: hexToF32(0x83800001), expected: [hexToF32(0x83800001)] }, + + // 64-bit normals + { value: hexToF64(0x3ff00000, 0x00000001), expected: [hexToF32(0x3f800000), hexToF32(0x3f800001)] }, + { value: hexToF64(0x3ff00000, 0x00000002), expected: [hexToF32(0x3f800000), hexToF32(0x3f800001)] }, + { value: hexToF64(0x3ff00010, 0x00000010), expected: [hexToF32(0x3f800080), hexToF32(0x3f800081)] }, + { value: hexToF64(0x3ff00020, 0x00000020), expected: [hexToF32(0x3f800100), hexToF32(0x3f800101)] }, + { value: hexToF64(0xbff00000, 0x00000001), expected: [hexToF32(0xbf800001), hexToF32(0xbf800000)] }, + { value: hexToF64(0xbff00000, 0x00000002), expected: [hexToF32(0xbf800001), hexToF32(0xbf800000)] }, + { value: hexToF64(0xbff00010, 0x00000010), expected: [hexToF32(0xbf800081), hexToF32(0xbf800080)] }, + { value: hexToF64(0xbff00020, 0x00000020), expected: [hexToF32(0xbf800101), hexToF32(0xbf800100)] }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = correctlyRoundedInterval(t.params.value); + t.expect( + objectEquals(expected, got), + `correctlyRoundedInterval(${t.params.value}) returned ${got}. Expected ${expected}` + ); + }); + +interface AbsoluteErrorCase { + value: number; + error: number; + expected: IntervalBounds; +} + +g.test('absoluteErrorInterval') + .paramsSubcasesOnly<AbsoluteErrorCase>( + // prettier-ignore + [ + // Edge Cases + { value: kValue.f32.infinity.positive, error: 0, expected: kAny }, + { value: kValue.f32.infinity.positive, error: 2 ** -11, expected: kAny }, + { value: kValue.f32.infinity.positive, error: 1, expected: kAny }, + { value: kValue.f32.infinity.negative, error: 0, expected: kAny }, + { value: kValue.f32.infinity.negative, error: 2 ** -11, expected: kAny }, + { value: kValue.f32.infinity.negative, error: 1, expected: kAny }, + { value: kValue.f32.positive.max, error: 0, expected: [kValue.f32.positive.max] }, + { value: kValue.f32.positive.max, error: 2 ** -11, expected: [kValue.f32.positive.max] }, + { value: kValue.f32.positive.max, error: kValue.f32.positive.max, expected: kAny }, + { value: kValue.f32.positive.min, error: 0, expected: [kValue.f32.positive.min] }, + { value: kValue.f32.positive.min, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: kValue.f32.positive.min, error: 1, expected: [-1, 1] }, + { value: kValue.f32.negative.min, error: 0, expected: [kValue.f32.negative.min] }, + { value: kValue.f32.negative.min, error: 2 ** -11, expected: [kValue.f32.negative.min] }, + { value: kValue.f32.negative.min, error: kValue.f32.positive.max, expected: kAny }, + { value: kValue.f32.negative.max, error: 0, expected: [kValue.f32.negative.max] }, + { value: kValue.f32.negative.max, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: kValue.f32.negative.max, error: 1, expected: [-1, 1] }, + + // 32-bit subnormals + { value: kValue.f32.subnormal.positive.max, error: 0, expected: [0, kValue.f32.subnormal.positive.max] }, + { value: kValue.f32.subnormal.positive.max, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: kValue.f32.subnormal.positive.max, error: 1, expected: [-1, 1] }, + { value: kValue.f32.subnormal.positive.min, error: 0, expected: [0, kValue.f32.subnormal.positive.min] }, + { value: kValue.f32.subnormal.positive.min, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: kValue.f32.subnormal.positive.min, error: 1, expected: [-1, 1] }, + { value: kValue.f32.subnormal.negative.min, error: 0, expected: [kValue.f32.subnormal.negative.min, 0] }, + { value: kValue.f32.subnormal.negative.min, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: kValue.f32.subnormal.negative.min, error: 1, expected: [-1, 1] }, + { value: kValue.f32.subnormal.negative.max, error: 0, expected: [kValue.f32.subnormal.negative.max, 0] }, + { value: kValue.f32.subnormal.negative.max, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: kValue.f32.subnormal.negative.max, error: 1, expected: [-1, 1] }, + + // 64-bit subnormals + { value: hexToF64(0x00000000, 0x00000001), error: 0, expected: [0, kValue.f32.subnormal.positive.min] }, + { value: hexToF64(0x00000000, 0x00000001), error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: hexToF64(0x00000000, 0x00000001), error: 1, expected: [-1, 1] }, + { value: hexToF64(0x00000000, 0x00000002), error: 0, expected: [0, kValue.f32.subnormal.positive.min] }, + { value: hexToF64(0x00000000, 0x00000002), error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: hexToF64(0x00000000, 0x00000002), error: 1, expected: [-1, 1] }, + { value: hexToF64(0x800fffff, 0xffffffff), error: 0, expected: [kValue.f32.subnormal.negative.max, 0] }, + { value: hexToF64(0x800fffff, 0xffffffff), error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: hexToF64(0x800fffff, 0xffffffff), error: 1, expected: [-1, 1] }, + { value: hexToF64(0x800fffff, 0xfffffffe), error: 0, expected: [kValue.f32.subnormal.negative.max, 0] }, + { value: hexToF64(0x800fffff, 0xfffffffe), error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: hexToF64(0x800fffff, 0xfffffffe), error: 1, expected: [-1, 1] }, + + // Zero + { value: 0, error: 0, expected: [0] }, + { value: 0, error: 2 ** -11, expected: [-(2 ** -11), 2 ** -11] }, + { value: 0, error: 1, expected: [-1, 1] }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = absoluteErrorInterval(t.params.value, t.params.error); + t.expect( + objectEquals(expected, got), + `absoluteErrorInterval(${t.params.value}, ${t.params.error}) returned ${got}. Expected ${expected}` + ); + }); + +interface ULPCase { + value: number; + num_ulp: number; + expected: IntervalBounds; +} + +g.test('ulpInterval') + .paramsSubcasesOnly<ULPCase>( + // prettier-ignore + [ + // Edge Cases + { value: kValue.f32.infinity.positive, num_ulp: 0, expected: kAny }, + { value: kValue.f32.infinity.positive, num_ulp: 1, expected: kAny }, + { value: kValue.f32.infinity.positive, num_ulp: 4096, expected: kAny }, + { value: kValue.f32.infinity.negative, num_ulp: 0, expected: kAny }, + { value: kValue.f32.infinity.negative, num_ulp: 1, expected: kAny }, + { value: kValue.f32.infinity.negative, num_ulp: 4096, expected: kAny }, + { value: kValue.f32.positive.max, num_ulp: 0, expected: [kValue.f32.positive.max] }, + { value: kValue.f32.positive.max, num_ulp: 1, expected: kAny }, + { value: kValue.f32.positive.max, num_ulp: 4096, expected: kAny }, + { value: kValue.f32.positive.min, num_ulp: 0, expected: [kValue.f32.positive.min] }, + { value: kValue.f32.positive.min, num_ulp: 1, expected: [0, plusOneULP(kValue.f32.positive.min)] }, + { value: kValue.f32.positive.min, num_ulp: 4096, expected: [0, plusNULP(kValue.f32.positive.min, 4096)] }, + { value: kValue.f32.negative.min, num_ulp: 0, expected: [kValue.f32.negative.min] }, + { value: kValue.f32.negative.min, num_ulp: 1, expected: kAny }, + { value: kValue.f32.negative.min, num_ulp: 4096, expected: kAny }, + { value: kValue.f32.negative.max, num_ulp: 0, expected: [kValue.f32.negative.max] }, + { value: kValue.f32.negative.max, num_ulp: 1, expected: [minusOneULP(kValue.f32.negative.max), 0] }, + { value: kValue.f32.negative.max, num_ulp: 4096, expected: [minusNULP(kValue.f32.negative.max, 4096), 0] }, + + // 32-bit subnormals + { value: kValue.f32.subnormal.positive.max, num_ulp: 0, expected: [0, kValue.f32.subnormal.positive.max] }, + { value: kValue.f32.subnormal.positive.max, num_ulp: 1, expected: [minusOneULP(0), plusOneULP(kValue.f32.subnormal.positive.max)] }, + { value: kValue.f32.subnormal.positive.max, num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(kValue.f32.subnormal.positive.max, 4096)] }, + { value: kValue.f32.subnormal.positive.min, num_ulp: 0, expected: [0, kValue.f32.subnormal.positive.min] }, + { value: kValue.f32.subnormal.positive.min, num_ulp: 1, expected: [minusOneULP(0), plusOneULP(kValue.f32.subnormal.positive.min)] }, + { value: kValue.f32.subnormal.positive.min, num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(kValue.f32.subnormal.positive.min, 4096)] }, + { value: kValue.f32.subnormal.negative.min, num_ulp: 0, expected: [kValue.f32.subnormal.negative.min, 0] }, + { value: kValue.f32.subnormal.negative.min, num_ulp: 1, expected: [minusOneULP(kValue.f32.subnormal.negative.min), plusOneULP(0)] }, + { value: kValue.f32.subnormal.negative.min, num_ulp: 4096, expected: [minusNULP(kValue.f32.subnormal.negative.min, 4096), plusNULP(0, 4096)] }, + { value: kValue.f32.subnormal.negative.max, num_ulp: 0, expected: [kValue.f32.subnormal.negative.max, 0] }, + { value: kValue.f32.subnormal.negative.max, num_ulp: 1, expected: [minusOneULP(kValue.f32.subnormal.negative.max), plusOneULP(0)] }, + { value: kValue.f32.subnormal.negative.max, num_ulp: 4096, expected: [minusNULP(kValue.f32.subnormal.negative.max, 4096), plusNULP(0, 4096)] }, + + // 64-bit subnormals + { value: hexToF64(0x00000000, 0x00000001), num_ulp: 0, expected: [0, kValue.f32.subnormal.positive.min] }, + { value: hexToF64(0x00000000, 0x00000001), num_ulp: 1, expected: [minusOneULP(0), plusOneULP(kValue.f32.subnormal.positive.min)] }, + { value: hexToF64(0x00000000, 0x00000001), num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(kValue.f32.subnormal.positive.min, 4096)] }, + { value: hexToF64(0x00000000, 0x00000002), num_ulp: 0, expected: [0, kValue.f32.subnormal.positive.min] }, + { value: hexToF64(0x00000000, 0x00000002), num_ulp: 1, expected: [minusOneULP(0), plusOneULP(kValue.f32.subnormal.positive.min)] }, + { value: hexToF64(0x00000000, 0x00000002), num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(kValue.f32.subnormal.positive.min, 4096)] }, + { value: hexToF64(0x800fffff, 0xffffffff), num_ulp: 0, expected: [kValue.f32.subnormal.negative.max, 0] }, + { value: hexToF64(0x800fffff, 0xffffffff), num_ulp: 1, expected: [minusOneULP(kValue.f32.subnormal.negative.max), plusOneULP(0)] }, + { value: hexToF64(0x800fffff, 0xffffffff), num_ulp: 4096, expected: [minusNULP(kValue.f32.subnormal.negative.max, 4096), plusNULP(0, 4096)] }, + { value: hexToF64(0x800fffff, 0xfffffffe), num_ulp: 0, expected: [kValue.f32.subnormal.negative.max, 0] }, + { value: hexToF64(0x800fffff, 0xfffffffe), num_ulp: 1, expected: [minusOneULP(kValue.f32.subnormal.negative.max), plusOneULP(0)] }, + { value: hexToF64(0x800fffff, 0xfffffffe), num_ulp: 4096, expected: [minusNULP(kValue.f32.subnormal.negative.max, 4096), plusNULP(0, 4096)] }, + + // Zero + { value: 0, num_ulp: 0, expected: [0] }, + { value: 0, num_ulp: 1, expected: [minusOneULP(0), plusOneULP(0)] }, + { value: 0, num_ulp: 4096, expected: [minusNULP(0, 4096), plusNULP(0, 4096)] }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = ulpInterval(t.params.value, t.params.num_ulp); + t.expect( + objectEquals(expected, got), + `ulpInterval(${t.params.value}, ${t.params.num_ulp}) returned ${got}. Expected ${expected}` + ); + }); + +interface PointToIntervalCase { + input: number; + expected: IntervalBounds; +} + +g.test('absInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Common usages + { input: 1, expected: [1] }, + { input: -1, expected: [1] }, + { input: 0.1, expected: [hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)] }, + { input: -0.1, expected: [hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)] }, + + // Edge cases + { input: kValue.f32.infinity.positive, expected: kAny }, + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] }, + { input: kValue.f32.positive.min, expected: [kValue.f32.positive.min] }, + { input: kValue.f32.negative.min, expected: [kValue.f32.positive.max] }, + { input: kValue.f32.negative.max, expected: [kValue.f32.positive.min] }, + + // 32-bit subnormals + { input: kValue.f32.subnormal.positive.max, expected: [0, kValue.f32.subnormal.positive.max] }, + { input: kValue.f32.subnormal.positive.min, expected: [0, kValue.f32.subnormal.positive.min] }, + { input: kValue.f32.subnormal.negative.min, expected: [0, kValue.f32.subnormal.positive.max] }, + { input: kValue.f32.subnormal.negative.max, expected: [0, kValue.f32.subnormal.positive.min] }, + + // 64-bit subnormals + { input: hexToF64(0x00000000, 0x00000001), expected: [0, kValue.f32.subnormal.positive.min] }, + { input: hexToF64(0x800fffff, 0xffffffff), expected: [0, kValue.f32.subnormal.positive.min] }, + + // Zero + { input: 0, expected: [0]}, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = absInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `absInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('acosInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + // + // The acceptance interval @ x = -1 and 1 is kAny, because sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of inverseqrt + // The acceptance interval @ x = 0 is kAny, because atan2 is not well defined/implemented at 0. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: -1, expected: kAny }, + { input: -1/2, expected: [hexToF32(0x4005fa91), hexToF32(0x40061a94)] }, // ~2π/3 + { input: 0, expected: kAny }, + { input: 1/2, expected: [hexToF32(0x3f85fa8f), hexToF32(0x3f861a94)] }, // ~π/3 + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = acosInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `acosInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('acoshAlternativeInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: -1, expected: kAny }, + { input: 0, expected: kAny }, + { input: 1, expected: kAny }, // 1/0 occurs in inverseSqrt in this formulation + { input: 1.1, expected: [hexToF64(0x3fdc6368, 0x80000000), hexToF64(0x3fdc636f, 0x20000000)] }, // ~0.443..., differs from the primary in the later digits + { input: 10, expected: [hexToF64(0x4007f21e, 0x40000000), hexToF64(0x4007f21f, 0x60000000)] }, // ~2.993... + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = acoshAlternativeInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `acoshInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('acoshPrimaryInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: -1, expected: kAny }, + { input: 0, expected: kAny }, + { input: 1, expected: kAny }, // 1/0 occurs in inverseSqrt in this formulation + { input: 1.1, expected: [hexToF64(0x3fdc6368, 0x20000000), hexToF64(0x3fdc636f, 0x80000000)] }, // ~0.443..., differs from the alternative in the later digits + { input: 10, expected: [hexToF64(0x4007f21e, 0x40000000), hexToF64(0x4007f21f, 0x60000000)] }, // ~2.993... + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = acoshPrimaryInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `acoshInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('asinInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + // + // The acceptance interval @ x = -1 and 1 is kAny, because sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of inversqrt + // The acceptance interval @ x = 0 is kAny, because atan2 is not well defined/implemented at 0. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: -1, expected: kAny }, + { input: -1/2, expected: [hexToF32(0xbf061a96), hexToF32(0xbf05fa8e)] }, // ~-π/6 + { input: 0, expected: kAny }, + { input: 1/2, expected: [hexToF32(0x3f05fa8e), hexToF32(0x3f061a96)] }, // ~π/6 + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = asinInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `asinInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('asinhInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: -1, expected: [hexToF64(0xbfec343a, 0x80000000), hexToF64(0xbfec3432, 0x80000000)] }, // ~-0.88137... + { input: 0, expected: [hexToF64(0xbeaa0000, 0x20000000), hexToF64(0x3eb1ffff, 0xd0000000)] }, // ~0 + { input: 1, expected: [hexToF64(0x3fec3435, 0x40000000), hexToF64(0x3fec3437, 0x80000000)] }, // ~0.88137... + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = asinhInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `asinhInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('atanInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: hexToF32(0xbfddb3d7), expected: [kValue.f32.negative.pi.third, plusOneULP(kValue.f32.negative.pi.third)] }, // x = -√3 + { input: -1, expected: [kValue.f32.negative.pi.quarter, plusOneULP(kValue.f32.negative.pi.quarter)] }, + { input: hexToF32(0xbf13cd3a), expected: [kValue.f32.negative.pi.sixth, plusOneULP(kValue.f32.negative.pi.sixth)] }, // x = -1/√3 + { input: 0, expected: [0] }, + { input: hexToF32(0x3f13cd3a), expected: [minusOneULP(kValue.f32.positive.pi.sixth), kValue.f32.positive.pi.sixth] }, // x = 1/√3 + { input: 1, expected: [minusOneULP(kValue.f32.positive.pi.quarter), kValue.f32.positive.pi.quarter] }, + { input: hexToF32(0x3fddb3d7), expected: [minusOneULP(kValue.f32.positive.pi.third), kValue.f32.positive.pi.third] }, // x = √3 + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const error = (n: number): number => { + return 4096 * oneULP(n); + }; + + t.params.expected = applyError(t.params.expected, error); + const expected = toF32Interval(t.params.expected); + + const got = atanInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `atanInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('atanhInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: -1, expected: kAny }, + { input: -0.1, expected: [hexToF64(0xbfb9af9a, 0x60000000), hexToF64(0xbfb9af8c, 0xc0000000)] }, // ~-0.1003... + { input: 0, expected: [hexToF64(0xbe960000, 0x20000000), hexToF64(0x3e980000, 0x00000000)] }, // ~0 + { input: 0.1, expected: [hexToF64(0x3fb9af8b, 0x80000000), hexToF64(0x3fb9af9b, 0x00000000)] }, // ~0.1003... + { input: 1, expected: kAny }, + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = atanhInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `atanhInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('ceilInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: 0, expected: [0] }, + { input: 0.1, expected: [1] }, + { input: 0.9, expected: [1] }, + { input: 1.0, expected: [1] }, + { input: 1.1, expected: [2] }, + { input: 1.9, expected: [2] }, + { input: -0.1, expected: [0] }, + { input: -0.9, expected: [0] }, + { input: -1.0, expected: [-1] }, + { input: -1.1, expected: [-1] }, + { input: -1.9, expected: [-1] }, + + // Edge cases + { input: kValue.f32.infinity.positive, expected: kAny }, + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] }, + { input: kValue.f32.positive.min, expected: [1] }, + { input: kValue.f32.negative.min, expected: [kValue.f32.negative.min] }, + { input: kValue.f32.negative.max, expected: [0] }, + { input: kValue.powTwo.to30, expected: [kValue.powTwo.to30] }, + { input: -kValue.powTwo.to30, expected: [-kValue.powTwo.to30] }, + + // 32-bit subnormals + { input: kValue.f32.subnormal.positive.max, expected: [0, 1] }, + { input: kValue.f32.subnormal.positive.min, expected: [0, 1] }, + { input: kValue.f32.subnormal.negative.min, expected: [0] }, + { input: kValue.f32.subnormal.negative.max, expected: [0] }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = ceilInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `ceilInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('cosInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // This test does not include some common cases. i.e. f(x = π/2) = 0, because the difference between true x + // and x as a f32 is sufficiently large, such that the high slope of f @ x causes the results to be substantially + // different, so instead of getting 0 you get a value on the order of 10^-8 away from 0, thus difficult to express + // in a human readable manner. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: kValue.f32.negative.pi.whole, expected: [-1, plusOneULP(-1)] }, + { input: kValue.f32.negative.pi.third, expected: [minusOneULP(1/2), 1/2] }, + { input: 0, expected: [1, 1] }, + { input: kValue.f32.positive.pi.third, expected: [minusOneULP(1/2), 1/2] }, + { input: kValue.f32.positive.pi.whole, expected: [-1, plusOneULP(-1)] }, + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const error = (_: number): number => { + return 2 ** -11; + }; + + t.params.expected = applyError(t.params.expected, error); + const expected = toF32Interval(t.params.expected); + + const got = cosInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `cosInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('coshInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: -1, expected: [ hexToF32(0x3fc583a4), hexToF32(0x3fc583b1)] }, // ~1.1543... + { input: 0, expected: [hexToF32(0x3f7ffffd), hexToF32(0x3f800002)] }, // ~1 + { input: 1, expected: [ hexToF32(0x3fc583a4), hexToF32(0x3fc583b1)] }, // ~1.1543... + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = coshInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `coshInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('degreesInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: kValue.f32.negative.pi.whole, expected: [minusOneULP(-180), plusOneULP(-180)] }, + { input: kValue.f32.negative.pi.three_quarters, expected: [minusOneULP(-135), plusOneULP(-135)] }, + { input: kValue.f32.negative.pi.half, expected: [minusOneULP(-90), plusOneULP(-90)] }, + { input: kValue.f32.negative.pi.third, expected: [minusOneULP(-60), plusOneULP(-60)] }, + { input: kValue.f32.negative.pi.quarter, expected: [minusOneULP(-45), plusOneULP(-45)] }, + { input: kValue.f32.negative.pi.sixth, expected: [minusOneULP(-30), plusOneULP(-30)] }, + { input: 0, expected: [0] }, + { input: kValue.f32.positive.pi.sixth, expected: [minusOneULP(30), plusOneULP(30)] }, + { input: kValue.f32.positive.pi.quarter, expected: [minusOneULP(45), plusOneULP(45)] }, + { input: kValue.f32.positive.pi.third, expected: [minusOneULP(60), plusOneULP(60)] }, + { input: kValue.f32.positive.pi.half, expected: [minusOneULP(90), plusOneULP(90)] }, + { input: kValue.f32.positive.pi.three_quarters, expected: [minusOneULP(135), plusOneULP(135)] }, + { input: kValue.f32.positive.pi.whole, expected: [minusOneULP(180), plusOneULP(180)] }, + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = degreesInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `degreesInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('expInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: 0, expected: [1] }, + { input: 1, expected: [kValue.f32.positive.e, plusOneULP(kValue.f32.positive.e)] }, + { input: 89, expected: kAny }, + ] + ) + .fn(t => { + const error = (x: number): number => { + const n = 3 + 2 * Math.abs(t.params.input); + return n * oneULP(x); + }; + + t.params.expected = applyError(t.params.expected, error); + const expected = toF32Interval(t.params.expected); + + const got = expInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `expInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('exp2Interval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: 0, expected: [1] }, + { input: 1, expected: [2] }, + { input: 128, expected: kAny }, + ] + ) + .fn(t => { + const error = (x: number): number => { + const n = 3 + 2 * Math.abs(t.params.input); + return n * oneULP(x); + }; + + t.params.expected = applyError(t.params.expected, error); + const expected = toF32Interval(t.params.expected); + + const got = exp2Interval(t.params.input); + t.expect( + objectEquals(expected, got), + `exp2Interval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('floorInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: 0, expected: [0] }, + { input: 0.1, expected: [0] }, + { input: 0.9, expected: [0] }, + { input: 1.0, expected: [1] }, + { input: 1.1, expected: [1] }, + { input: 1.9, expected: [1] }, + { input: -0.1, expected: [-1] }, + { input: -0.9, expected: [-1] }, + { input: -1.0, expected: [-1] }, + { input: -1.1, expected: [-2] }, + { input: -1.9, expected: [-2] }, + + // Edge cases + { input: kValue.f32.infinity.positive, expected: kAny }, + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] }, + { input: kValue.f32.positive.min, expected: [0] }, + { input: kValue.f32.negative.min, expected: [kValue.f32.negative.min] }, + { input: kValue.f32.negative.max, expected: [-1] }, + { input: kValue.powTwo.to30, expected: [kValue.powTwo.to30] }, + { input: -kValue.powTwo.to30, expected: [-kValue.powTwo.to30] }, + + // 32-bit subnormals + { input: kValue.f32.subnormal.positive.max, expected: [0] }, + { input: kValue.f32.subnormal.positive.min, expected: [0] }, + { input: kValue.f32.subnormal.negative.min, expected: [-1, 0] }, + { input: kValue.f32.subnormal.negative.max, expected: [-1, 0] }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = floorInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `floorInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('fractInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: 0, expected: [0] }, + { input: 0.1, expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: 0.9, expected: [hexToF32(0x3f666666), plusOneULP(hexToF32(0x3f666666))] }, // ~0.9 + { input: 1.0, expected: [0] }, + { input: 1.1, expected: [hexToF64(0x3fb99998, 0x00000000), hexToF64(0x3fb9999a, 0x00000000)] }, // ~0.1 + { input: -0.1, expected: [hexToF32(0x3f666666), plusOneULP(hexToF32(0x3f666666))] }, // ~0.9 + { input: -0.9, expected: [hexToF64(0x3fb99999, 0x00000000), hexToF64(0x3fb9999a, 0x00000000)] }, // ~0.1 + { input: -1.0, expected: [0] }, + { input: -1.1, expected: [hexToF64(0x3feccccc, 0xc0000000), hexToF64(0x3feccccd, 0x00000000), ] }, // ~0.9 + + // Edge cases + { input: kValue.f32.infinity.positive, expected: kAny }, + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.positive.max, expected: [0] }, + { input: kValue.f32.positive.min, expected: [kValue.f32.positive.min, kValue.f32.positive.min] }, + { input: kValue.f32.negative.min, expected: [0] }, + { input: kValue.f32.negative.max, expected: [kValue.f32.positive.less_than_one, 1.0] }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = fractInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `fractInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('inverseSqrtInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: -1, expected: kAny }, + { input: 0, expected: kAny }, + { input: 0.04, expected: [minusOneULP(5), plusOneULP(5)] }, + { input: 1, expected: [1] }, + { input: 100, expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: kValue.f32.positive.max, expected: [hexToF32(0x1f800000), plusNULP(hexToF32(0x1f800000), 2)] }, // ~5.421...e-20, i.e. 1/√max f32 + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const error = (n: number): number => { + return 2 * oneULP(n); + }; + + t.params.expected = applyError(t.params.expected, error); + const expected = toF32Interval(t.params.expected); + + const got = inverseSqrtInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `inverseSqrtInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('lengthIntervalScalar') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + // + // length(0) = kAny, because length uses sqrt, which is defined as 1/inversesqrt + {input: 0, expected: kAny }, + {input: 1.0, expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: -1.0, expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: 0.1, expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + {input: -0.1, expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + {input: 10.0, expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10 + {input: -10.0, expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10 + + // Subnormal Cases + { input: kValue.f32.subnormal.negative.min, expected: kAny }, + { input: kValue.f32.subnormal.negative.max, expected: kAny }, + { input: kValue.f32.subnormal.positive.min, expected: kAny }, + { input: kValue.f32.subnormal.positive.max, expected: kAny }, + + // Edge cases + { input: kValue.f32.infinity.positive, expected: kAny }, + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: kValue.f32.negative.max, expected: kAny }, + { input: kValue.f32.positive.min, expected: kAny }, + { input: kValue.f32.positive.max, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = lengthInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `lengthInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('logInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: -1, expected: kAny }, + { input: 0, expected: kAny }, + { input: 1, expected: [0] }, + { input: kValue.f32.positive.e, expected: [minusOneULP(1), 1] }, + { input: kValue.f32.positive.max, expected: [minusOneULP(hexToF32(0x42b17218)), hexToF32(0x42b17218)] }, // ~88.72... + ] + ) + .fn(t => { + const error = (n: number): number => { + if (t.params.input >= 0.5 && t.params.input <= 2.0) { + return 2 ** -21; + } + return 3 * oneULP(n); + }; + + t.params.expected = applyError(t.params.expected, error); + const expected = toF32Interval(t.params.expected); + + const got = logInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `logInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('log2Interval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: -1, expected: kAny }, + { input: 0, expected: kAny }, + { input: 1, expected: [0] }, + { input: 2, expected: [1] }, + { input: kValue.f32.positive.max, expected: [minusOneULP(128), 128] }, + ] + ) + .fn(t => { + const error = (n: number): number => { + if (t.params.input >= 0.5 && t.params.input <= 2.0) { + return 2 ** -21; + } + return 3 * oneULP(n); + }; + + t.params.expected = applyError(t.params.expected, error); + const expected = toF32Interval(t.params.expected); + + const got = log2Interval(t.params.input); + t.expect( + objectEquals(expected, got), + `log2Interval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('negationInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: 0, expected: [0] }, + { input: 0.1, expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + { input: 1.0, expected: [-1.0] }, + { input: 1.9, expected: [hexToF32(0xbff33334), plusOneULP(hexToF32(0xbff33334))] }, // ~-1.9 + { input: -0.1, expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: -1.0, expected: [1] }, + { input: -1.9, expected: [minusOneULP(hexToF32(0x3ff33334)), hexToF32(0x3ff33334)] }, // ~1.9 + + // Edge cases + { input: kValue.f32.infinity.positive, expected: kAny }, + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.positive.max, expected: [kValue.f32.negative.min] }, + { input: kValue.f32.positive.min, expected: [kValue.f32.negative.max] }, + { input: kValue.f32.negative.min, expected: [kValue.f32.positive.max] }, + { input: kValue.f32.negative.max, expected: [kValue.f32.positive.min] }, + + // 32-bit subnormals + { input: kValue.f32.subnormal.positive.max, expected: [kValue.f32.subnormal.negative.min, 0] }, + { input: kValue.f32.subnormal.positive.min, expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: kValue.f32.subnormal.negative.min, expected: [0, kValue.f32.subnormal.positive.max] }, + { input: kValue.f32.subnormal.negative.max, expected: [0, kValue.f32.subnormal.positive.min] }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = negationInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `negationInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('quantizeToF16Interval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: kValue.f16.negative.min, expected: [kValue.f16.negative.min] }, + { input: -1, expected: [-1] }, + { input: -0.1, expected: [hexToF32(0xbdcce000), hexToF32(0xbdccc000)] }, // ~-0.1 + { input: kValue.f16.negative.max, expected: [kValue.f16.negative.max] }, + { input: kValue.f16.subnormal.negative.min, expected: [kValue.f16.subnormal.negative.min, 0] }, + { input: kValue.f16.subnormal.negative.max, expected: [kValue.f16.subnormal.negative.max, 0] }, + { input: kValue.f32.subnormal.negative.max, expected: [kValue.f16.subnormal.negative.max, 0] }, + { input: 0, expected: [0] }, + { input: kValue.f32.subnormal.positive.min, expected: [0, kValue.f16.subnormal.positive.min] }, + { input: kValue.f16.subnormal.positive.min, expected: [0, kValue.f16.subnormal.positive.min] }, + { input: kValue.f16.subnormal.positive.max, expected: [0, kValue.f16.subnormal.positive.max] }, + { input: kValue.f16.positive.min, expected: [kValue.f16.positive.min] }, + { input: 0.1, expected: [hexToF32(0x3dccc000), hexToF32(0x3dcce000)] }, // ~0.1 + { input: 1, expected: [1] }, + { input: kValue.f16.positive.max, expected: [kValue.f16.positive.max] }, + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = quantizeToF16Interval(t.params.input); + t.expect( + objectEquals(expected, got), + `quantizeToF16Interval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('radiansInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: -180, expected: [minusOneULP(kValue.f32.negative.pi.whole), plusOneULP(kValue.f32.negative.pi.whole)] }, + { input: -135, expected: [minusOneULP(kValue.f32.negative.pi.three_quarters), plusOneULP(kValue.f32.negative.pi.three_quarters)] }, + { input: -90, expected: [minusOneULP(kValue.f32.negative.pi.half), plusOneULP(kValue.f32.negative.pi.half)] }, + { input: -60, expected: [minusOneULP(kValue.f32.negative.pi.third), plusOneULP(kValue.f32.negative.pi.third)] }, + { input: -45, expected: [minusOneULP(kValue.f32.negative.pi.quarter), plusOneULP(kValue.f32.negative.pi.quarter)] }, + { input: -30, expected: [minusOneULP(kValue.f32.negative.pi.sixth), plusOneULP(kValue.f32.negative.pi.sixth)] }, + { input: 0, expected: [0] }, + { input: 30, expected: [minusOneULP(kValue.f32.positive.pi.sixth), plusOneULP(kValue.f32.positive.pi.sixth)] }, + { input: 45, expected: [minusOneULP(kValue.f32.positive.pi.quarter), plusOneULP(kValue.f32.positive.pi.quarter)] }, + { input: 60, expected: [minusOneULP(kValue.f32.positive.pi.third), plusOneULP(kValue.f32.positive.pi.third)] }, + { input: 90, expected: [minusOneULP(kValue.f32.positive.pi.half), plusOneULP(kValue.f32.positive.pi.half)] }, + { input: 135, expected: [minusOneULP(kValue.f32.positive.pi.three_quarters), plusOneULP(kValue.f32.positive.pi.three_quarters)] }, + { input: 180, expected: [minusOneULP(kValue.f32.positive.pi.whole), plusOneULP(kValue.f32.positive.pi.whole)] }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = radiansInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `radiansInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('roundInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: 0, expected: [0] }, + { input: 0.1, expected: [0] }, + { input: 0.5, expected: [0] }, // Testing tie breaking + { input: 0.9, expected: [1] }, + { input: 1.0, expected: [1] }, + { input: 1.1, expected: [1] }, + { input: 1.5, expected: [2] }, // Testing tie breaking + { input: 1.9, expected: [2] }, + { input: -0.1, expected: [0] }, + { input: -0.5, expected: [0] }, // Testing tie breaking + { input: -0.9, expected: [-1] }, + { input: -1.0, expected: [-1] }, + { input: -1.1, expected: [-1] }, + { input: -1.5, expected: [-2] }, // Testing tie breaking + { input: -1.9, expected: [-2] }, + + // Edge cases + { input: kValue.f32.infinity.positive, expected: kAny }, + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] }, + { input: kValue.f32.positive.min, expected: [0] }, + { input: kValue.f32.negative.min, expected: [kValue.f32.negative.min] }, + { input: kValue.f32.negative.max, expected: [0] }, + { input: kValue.powTwo.to30, expected: [kValue.powTwo.to30] }, + { input: -kValue.powTwo.to30, expected: [-kValue.powTwo.to30] }, + + // 32-bit subnormals + { input: kValue.f32.subnormal.positive.max, expected: [0] }, + { input: kValue.f32.subnormal.positive.min, expected: [0] }, + { input: kValue.f32.subnormal.negative.min, expected: [0] }, + { input: kValue.f32.subnormal.negative.max, expected: [0] }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = roundInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `roundInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('saturateInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Normals + { input: 0, expected: [0] }, + { input: 1, expected: [1.0] }, + { input: -0.1, expected: [0] }, + { input: -1, expected: [0] }, + { input: -10, expected: [0] }, + { input: 0.1, expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: 10, expected: [1.0] }, + { input: 11.1, expected: [1.0] }, + { input: kValue.f32.positive.max, expected: [1.0] }, + { input: kValue.f32.positive.min, expected: [kValue.f32.positive.min] }, + { input: kValue.f32.negative.max, expected: [0.0] }, + { input: kValue.f32.negative.min, expected: [0.0] }, + + // Subnormals + { input: kValue.f32.subnormal.positive.max, expected: [0.0, kValue.f32.subnormal.positive.max] }, + { input: kValue.f32.subnormal.positive.min, expected: [0.0, kValue.f32.subnormal.positive.min] }, + { input: kValue.f32.subnormal.negative.min, expected: [0.0] }, + { input: kValue.f32.subnormal.negative.max, expected: [0.0] }, + + // Infinities + { input: kValue.f32.infinity.positive, expected: kAny }, + { input: kValue.f32.infinity.negative, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = saturateInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `saturationInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('signInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: [-1] }, + { input: -10, expected: [-1] }, + { input: -1, expected: [-1] }, + { input: -0.1, expected: [-1] }, + { input: kValue.f32.negative.max, expected: [-1] }, + { input: kValue.f32.subnormal.negative.min, expected: [-1, 0] }, + { input: kValue.f32.subnormal.negative.max, expected: [-1, 0] }, + { input: 0, expected: [0] }, + { input: kValue.f32.subnormal.positive.max, expected: [0, 1] }, + { input: kValue.f32.subnormal.positive.min, expected: [0, 1] }, + { input: kValue.f32.positive.min, expected: [1] }, + { input: 0.1, expected: [1] }, + { input: 1, expected: [1] }, + { input: 10, expected: [1] }, + { input: kValue.f32.positive.max, expected: [1] }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = signInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `signInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('sinInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // This test does not include some common cases, i.e. f(x = -π|π) = 0, because the difference between true x and x + // as a f32 is sufficiently large, such that the high slope of f @ x causes the results to be substantially + // different, so instead of getting 0 you get a value on the order of 10^-8 away from it, thus difficult to + // express in a human readable manner. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: kValue.f32.negative.pi.half, expected: [-1, plusOneULP(-1)] }, + { input: 0, expected: [0] }, + { input: kValue.f32.positive.pi.half, expected: [minusOneULP(1), 1] }, + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const error = (_: number): number => { + return 2 ** -11; + }; + + t.params.expected = applyError(t.params.expected, error); + const expected = toF32Interval(t.params.expected); + + const got = sinInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `sinInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('sinhInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: -1, expected: [ hexToF32(0xbf966d05), hexToF32(0xbf966cf8)] }, // ~-1.175... + { input: 0, expected: [hexToF32(0xb4600000), hexToF32(0x34600000)] }, // ~0 + { input: 1, expected: [ hexToF32(0x3f966cf8), hexToF32(0x3f966d05)] }, // ~1.175... + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = sinhInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `sinhInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('sqrtInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: -1, expected: kAny }, + { input: 0, expected: kAny }, + { input: 0.01, expected: [hexToF64(0x3fb99998, 0xb0000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + { input: 1, expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: 4, expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2 + { input: 100, expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10 + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = sqrtInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `sqrtInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('tanInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // All of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form. Some easy looking cases like f(x = -π|π) = 0 are actually quite difficult. This is because the interval + // is calculated from the results of sin(x)/cos(x), which becomes very messy at x = -π|π, since π is irrational, + // thus does not have an exact representation as a f32. + // Even at 0, which has a precise f32 value, there is still the problem that result of sin(0) and cos(0) will be + // intervals due to the inherited nature of errors, so the proper interval will be an interval calculated from + // dividing an interval by another interval and applying an error function to that. This complexity is why the + // entire interval framework was developed. + // The examples here have been manually traced to confirm the expectation values are correct. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: kValue.f32.negative.pi.whole, expected: [hexToF64(0xbf4002bc, 0x90000000), hexToF64(0x3f400144, 0xf0000000)] }, // ~0.0 + { input: kValue.f32.negative.pi.half, expected: kAny }, + { input: 0, expected: [hexToF64(0xbf400200, 0xb0000000), hexToF64(0x3f400200, 0xb0000000)] }, // ~0.0 + { input: kValue.f32.positive.pi.half, expected: kAny }, + { input: kValue.f32.positive.pi.whole, expected: [hexToF64(0xbf400144, 0xf0000000), hexToF64(0x3f4002bc, 0x90000000)] }, // ~0.0 + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = tanInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `tanInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('tanhInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: -1, expected: [hexToF64(0xbfe85efd, 0x10000000), hexToF64(0xbfe85ef8, 0x90000000)] }, // ~-0.7615... + { input: 0, expected: [hexToF64(0xbe8c0000, 0xb0000000), hexToF64(0x3e8c0000, 0xb0000000)] }, // ~0 + { input: 1, expected: [hexToF64(0x3fe85ef8, 0x90000000), hexToF64(0x3fe85efd, 0x10000000)] }, // ~0.7615... + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = tanhInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `tanhInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('truncInterval') + .paramsSubcasesOnly<PointToIntervalCase>( + // prettier-ignore + [ + { input: 0, expected: [0] }, + { input: 0.1, expected: [0] }, + { input: 0.9, expected: [0] }, + { input: 1.0, expected: [1] }, + { input: 1.1, expected: [1] }, + { input: 1.9, expected: [1] }, + { input: -0.1, expected: [0] }, + { input: -0.9, expected: [0] }, + { input: -1.0, expected: [-1] }, + { input: -1.1, expected: [-1] }, + { input: -1.9, expected: [-1] }, + + // Edge cases + { input: kValue.f32.infinity.positive, expected: kAny }, + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.positive.max, expected: [kValue.f32.positive.max] }, + { input: kValue.f32.positive.min, expected: [0] }, + { input: kValue.f32.negative.min, expected: [kValue.f32.negative.min] }, + { input: kValue.f32.negative.max, expected: [0] }, + + // 32-bit subnormals + { input: kValue.f32.subnormal.positive.max, expected: [0] }, + { input: kValue.f32.subnormal.positive.min, expected: [0] }, + { input: kValue.f32.subnormal.negative.min, expected: [0] }, + { input: kValue.f32.subnormal.negative.max, expected: [0] }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = truncInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `truncInterval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + +interface BinaryToIntervalCase { + // input is a pair of independent values, not an range, so should not be + // converted to a F32Interval. + input: [number, number]; + expected: IntervalBounds; +} + +g.test('additionInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // 32-bit normals + { input: [0, 0], expected: [0] }, + { input: [1, 0], expected: [1] }, + { input: [0, 1], expected: [1] }, + { input: [-1, 0], expected: [-1] }, + { input: [0, -1], expected: [-1] }, + { input: [1, 1], expected: [2] }, + { input: [1, -1], expected: [0] }, + { input: [-1, 1], expected: [0] }, + { input: [-1, -1], expected: [-2] }, + + // 64-bit normals + { input: [0.1, 0], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [0, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [-0.1, 0], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + { input: [0, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + { input: [0.1, 0.1], expected: [minusOneULP(hexToF32(0x3e4ccccd)), hexToF32(0x3e4ccccd)] }, // ~0.2 + { input: [0.1, -0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)) - hexToF32(0x3dcccccd), hexToF32(0x3dcccccd) - minusOneULP(hexToF32(0x3dcccccd))] }, // ~0 + { input: [-0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)) - hexToF32(0x3dcccccd), hexToF32(0x3dcccccd) - minusOneULP(hexToF32(0x3dcccccd))] }, // ~0 + { input: [-0.1, -0.1], expected: [hexToF32(0xbe4ccccd), plusOneULP(hexToF32(0xbe4ccccd))] }, // ~-0.2 + + // 32-bit subnormals + { input: [kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.min, 0], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [0, kValue.f32.subnormal.positive.min], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [kValue.f32.subnormal.negative.max, 0], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [0, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [kValue.f32.subnormal.negative.min, 0], expected: [kValue.f32.subnormal.negative.min, 0] }, + { input: [0, kValue.f32.subnormal.negative.min], expected: [kValue.f32.subnormal.negative.min, 0] }, + + // Infinities + { input: [0, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, 0], expected: kAny}, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, 0], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = additionInterval(x, y); + t.expect( + objectEquals(expected, got), + `additionInterval(${x}, ${y}) returned ${got}. Expected ${expected}` + ); + }); + +// Note: atan2's parameters are labelled (y, x) instead of (x, y) +g.test('atan2Interval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + + // The positive x & y quadrant is tested in more detail, and the other quadrants are spot checked that values are + // pointing in the right direction. + + // Some of the intervals appear slightly asymmetric, i.e. [π/4 - 4097 * ULP(π/4), π/4 + 4096 * ULP(π/4)], this is + // because π/4 is not precisely expressible as a f32, so the higher precision value can be rounded up or down when + // converting to f32. Thus one option will be 1 ULP off of the constant value being used. + + // positive y, positive x + { input: [1, hexToF32(0x3fddb3d7)], expected: [minusNULP(kValue.f32.positive.pi.sixth, 4097), plusNULP(kValue.f32.positive.pi.sixth, 4096)] }, // x = √3 + { input: [1, 1], expected: [minusNULP(kValue.f32.positive.pi.quarter, 4097), plusNULP(kValue.f32.positive.pi.quarter, 4096)] }, + // { input: [hexToF32(0x3fddb3d7), 1], expected: [hexToF64(0x3ff0bf52, 0x00000000), hexToF64(0x3ff0c352, 0x60000000)] }, // y = √3 + { input: [Number.POSITIVE_INFINITY, 1], expected: kAny }, + + // positive y, negative x + { input: [1, -1], expected: [minusNULP(kValue.f32.positive.pi.three_quarters, 4096), plusNULP(kValue.f32.positive.pi.three_quarters, 4097)] }, + { input: [Number.POSITIVE_INFINITY, -1], expected: kAny }, + + // negative y, negative x + { input: [-1, -1], expected: [minusNULP(kValue.f32.negative.pi.three_quarters, 4097), plusNULP(kValue.f32.negative.pi.three_quarters, 4096)] }, + { input: [Number.NEGATIVE_INFINITY, -1], expected: kAny }, + + // negative y, positive x + { input: [-1, 1], expected: [minusNULP(kValue.f32.negative.pi.quarter, 4096), plusNULP(kValue.f32.negative.pi.quarter, 4097)] }, + { input: [Number.NEGATIVE_INFINITY, 1], expected: kAny }, + + // Discontinuity @ origin (0,0) + { input: [0, 0], expected: kAny }, + { input: [0, kValue.f32.subnormal.positive.max], expected: kAny }, + { input: [0, kValue.f32.subnormal.negative.min], expected: kAny }, + { input: [0, kValue.f32.positive.min], expected: kAny }, + { input: [0, kValue.f32.negative.max], expected: kAny }, + { input: [0, kValue.f32.positive.max], expected: kAny }, + { input: [0, kValue.f32.negative.min], expected: kAny }, + { input: [0, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.negative], expected: kAny }, + { input: [0, 1], expected: kAny }, + { input: [kValue.f32.subnormal.positive.max, 1], expected: kAny }, + { input: [kValue.f32.subnormal.negative.min, 1], expected: kAny }, + + // When atan(y/x) ~ 0, test that ULP applied to result of atan2, not the intermediate atan(y/x) value + {input: [hexToF32(0x80800000), hexToF32(0xbf800000)], expected: [minusNULP(kValue.f32.negative.pi.whole, 4096), plusNULP(kValue.f32.negative.pi.whole, 4096)] }, + {input: [hexToF32(0x00800000), hexToF32(0xbf800000)], expected: [minusNULP(kValue.f32.positive.pi.whole, 4096), plusNULP(kValue.f32.positive.pi.whole, 4096)] }, + + // Very large |x| values should cause kAny to be returned, due to the restrictions on division + { input: [1, kValue.f32.positive.max], expected: kAny }, + { input: [1, kValue.f32.positive.nearest_max], expected: kAny }, + { input: [1, kValue.f32.negative.min], expected: kAny }, + { input: [1, kValue.f32.negative.nearest_min], expected: kAny }, + ] + ) + .fn(t => { + const [y, x] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = atan2Interval(y, x); + t.expect( + objectEquals(expected, got), + `atan2Interval(${y}, ${x}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('distanceIntervalScalar') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult + // to express in a closed human readable form due to the inherited nature + // of the errors. + // + // distance(x, y), where x - y = 0 has an acceptance interval of kAny, + // because distance(x, y) = length(x - y), and length(0) = kAny + { input: [0, 0], expected: kAny }, + { input: [1.0, 0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [0.0, 1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [1.0, 1.0], expected: kAny }, + { input: [-0.0, -1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [0.0, -1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [-1.0, -1.0], expected: kAny }, + { input: [0.1, 0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + { input: [0, 0.1], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + { input: [-0.1, 0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + { input: [0, -0.1], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + { input: [10.0, 0], expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10 + { input: [0, 10.0], expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10 + { input: [-10.0, 0], expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10 + { input: [0, -10.0], expected: [hexToF64(0x4023ffff, 0x70000000), hexToF64(0x40240000, 0xb0000000)] }, // ~10 + + // Subnormal Cases + { input: [kValue.f32.subnormal.negative.min, 0], expected: kAny }, + { input: [kValue.f32.subnormal.negative.max, 0], expected: kAny }, + { input: [kValue.f32.subnormal.positive.min, 0], expected: kAny }, + { input: [kValue.f32.subnormal.positive.max, 0], expected: kAny }, + + // Edge cases + { input: [kValue.f32.infinity.positive, 0], expected: kAny }, + { input: [kValue.f32.infinity.negative, 0], expected: kAny }, + { input: [kValue.f32.negative.min, 0], expected: kAny }, + { input: [kValue.f32.negative.max, 0], expected: kAny }, + { input: [kValue.f32.positive.min, 0], expected: kAny }, + { input: [kValue.f32.positive.max, 0], expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = distanceInterval(...t.params.input); + t.expect( + objectEquals(expected, got), + `distanceInterval(${t.params.input[0]}, ${t.params.input[1]}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('divisionInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // 32-bit normals + { input: [0, 1], expected: [0] }, + { input: [0, -1], expected: [0] }, + { input: [1, 1], expected: [1] }, + { input: [1, -1], expected: [-1] }, + { input: [-1, 1], expected: [-1] }, + { input: [-1, -1], expected: [1] }, + { input: [4, 2], expected: [2] }, + { input: [-4, 2], expected: [-2] }, + { input: [4, -2], expected: [-2] }, + { input: [-4, -2], expected: [2] }, + + // 64-bit normals + { input: [0, 0.1], expected: [0] }, + { input: [0, -0.1], expected: [0] }, + { input: [1, 0.1], expected: [minusOneULP(10), plusOneULP(10)] }, + { input: [-1, 0.1], expected: [minusOneULP(-10), plusOneULP(-10)] }, + { input: [1, -0.1], expected: [minusOneULP(-10), plusOneULP(-10)] }, + { input: [-1, -0.1], expected: [minusOneULP(10), plusOneULP(10)] }, + + // Denominator out of range + { input: [1, kValue.f32.infinity.positive], expected: kAny }, + { input: [1, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + { input: [1, kValue.f32.positive.max], expected: kAny }, + { input: [1, kValue.f32.negative.min], expected: kAny }, + { input: [1, 0], expected: kAny }, + { input: [1, kValue.f32.subnormal.positive.max], expected: kAny }, + ] + ) + .fn(t => { + const error = (n: number): number => { + return 2.5 * oneULP(n); + }; + + const [x, y] = t.params.input; + t.params.expected = applyError(t.params.expected, error); + const expected = toF32Interval(t.params.expected); + + const got = divisionInterval(x, y); + t.expect( + objectEquals(expected, got), + `divisionInterval(${x}, ${y}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('ldexpInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // 32-bit normals + { input: [0, 0], expected: [0] }, + { input: [0, 1], expected: [0] }, + { input: [0, -1], expected: [0] }, + { input: [1, 1], expected: [2] }, + { input: [1, -1], expected: [0.5] }, + { input: [-1, 1], expected: [-2] }, + { input: [-1, -1], expected: [-0.5] }, + + // 64-bit normals + { input: [0, 0.1], expected: [0] }, + { input: [0, -0.1], expected: [0] }, + { input: [1.0000000001, 1], expected: [2, plusNULP(2, 2)] }, // ~2, additional ULP error due to first param not being f32 precise + { input: [-1.0000000001, 1], expected: [minusNULP(-2, 2), -2] }, // ~-2, additional ULP error due to first param not being f32 precise + + // Edge Cases + { input: [1.9999998807907104, 127], expected: [kValue.f32.positive.max] }, + { input: [1, -126], expected: [kValue.f32.positive.min] }, + { input: [0.9999998807907104, -126], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [1.1920928955078125e-07, -126], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [-1.1920928955078125e-07, -126], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [-0.9999998807907104, -126], expected: [kValue.f32.subnormal.negative.min, 0] }, + { input: [-1, -126], expected: [kValue.f32.negative.max] }, + { input: [-1.9999998807907104, 127], expected: [kValue.f32.negative.min] }, + + // Out of Bounds + { input: [1, 128], expected: kAny }, + { input: [-1, 128], expected: kAny }, + { input: [100, 126], expected: kAny }, + { input: [-100, 126], expected: kAny }, + { input: [kValue.f32.positive.max, kValue.i32.positive.max], expected: kAny }, + { input: [kValue.f32.negative.min, kValue.i32.positive.max], expected: kAny }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = ldexpInterval(x, y); + t.expect( + objectEquals(expected, got), + `divisionInterval(${x}, ${y}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('maxInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // 32-bit normals + { input: [0, 0], expected: [0] }, + { input: [1, 0], expected: [1] }, + { input: [0, 1], expected: [1] }, + { input: [-1, 0], expected: [0] }, + { input: [0, -1], expected: [0] }, + { input: [1, 1], expected: [1] }, + { input: [1, -1], expected: [1] }, + { input: [-1, 1], expected: [1] }, + { input: [-1, -1], expected: [-1] }, + + // 64-bit normals + { input: [0.1, 0], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [0, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [-0.1, 0], expected: [0] }, + { input: [0, -0.1], expected: [0] }, + { input: [0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [0.1, -0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [-0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [-0.1, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + + // 32-bit normals + { input: [kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.min, 0], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [0, kValue.f32.subnormal.positive.min], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [kValue.f32.subnormal.negative.max, 0], expected: [0] }, + { input: [0, kValue.f32.subnormal.negative.max], expected: [0] }, + { input: [kValue.f32.subnormal.negative.min, 0], expected: [0] }, + { input: [0, kValue.f32.subnormal.negative.min], expected: [0] }, + + // Infinities + { input: [0, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, 0], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, 0], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = maxInterval(x, y); + t.expect( + objectEquals(expected, got), + `maxInterval(${x}, ${y}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('minInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // 32-bit normals + { input: [0, 0], expected: [0] }, + { input: [1, 0], expected: [0] }, + { input: [0, 1], expected: [0] }, + { input: [-1, 0], expected: [-1] }, + { input: [0, -1], expected: [-1] }, + { input: [1, 1], expected: [1] }, + { input: [1, -1], expected: [-1] }, + { input: [-1, 1], expected: [-1] }, + { input: [-1, -1], expected: [-1] }, + + // 64-bit normals + { input: [0.1, 0], expected: [0] }, + { input: [0, 0.1], expected: [0] }, + { input: [-0.1, 0], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + { input: [0, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + { input: [0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [0.1, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + { input: [-0.1, 0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + { input: [-0.1, -0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + + // 32-bit normals + { input: [kValue.f32.subnormal.positive.max, 0], expected: [0] }, + { input: [0, kValue.f32.subnormal.positive.max], expected: [0] }, + { input: [kValue.f32.subnormal.positive.min, 0], expected: [0] }, + { input: [0, kValue.f32.subnormal.positive.min], expected: [0] }, + { input: [kValue.f32.subnormal.negative.max, 0], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [0, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [kValue.f32.subnormal.negative.min, 0], expected: [kValue.f32.subnormal.negative.min, 0] }, + { input: [0, kValue.f32.subnormal.negative.min], expected: [kValue.f32.subnormal.negative.min, 0] }, + + // Infinities + { input: [0, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, 0], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, 0], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = minInterval(x, y); + t.expect( + objectEquals(expected, got), + `minInterval(${x}, ${y}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('multiplicationInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // 32-bit normals + { input: [0, 0], expected: [0] }, + { input: [1, 0], expected: [0] }, + { input: [0, 1], expected: [0] }, + { input: [-1, 0], expected: [0] }, + { input: [0, -1], expected: [0] }, + { input: [1, 1], expected: [1] }, + { input: [1, -1], expected: [-1] }, + { input: [-1, 1], expected: [-1] }, + { input: [-1, -1], expected: [1] }, + { input: [2, 1], expected: [2] }, + { input: [1, -2], expected: [-2] }, + { input: [-2, 1], expected: [-2] }, + { input: [-2, -1], expected: [2] }, + { input: [2, 2], expected: [4] }, + { input: [2, -2], expected: [-4] }, + { input: [-2, 2], expected: [-4] }, + { input: [-2, -2], expected: [4] }, + + // 64-bit normals + { input: [0.1, 0], expected: [0] }, + { input: [0, 0.1], expected: [0] }, + { input: [-0.1, 0], expected: [0] }, + { input: [0, -0.1], expected: [0] }, + { input: [0.1, 0.1], expected: [minusNULP(hexToF32(0x3c23d70a), 2), plusOneULP(hexToF32(0x3c23d70a))] }, // ~0.01 + { input: [0.1, -0.1], expected: [minusOneULP(hexToF32(0xbc23d70a)), plusNULP(hexToF32(0xbc23d70a), 2)] }, // ~-0.01 + { input: [-0.1, 0.1], expected: [minusOneULP(hexToF32(0xbc23d70a)), plusNULP(hexToF32(0xbc23d70a), 2)] }, // ~-0.01 + { input: [-0.1, -0.1], expected: [minusNULP(hexToF32(0x3c23d70a), 2), plusOneULP(hexToF32(0x3c23d70a))] }, // ~0.01 + + // Infinities + { input: [0, kValue.f32.infinity.positive], expected: kAny }, + { input: [1, kValue.f32.infinity.positive], expected: kAny }, + { input: [-1, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.negative], expected: kAny }, + { input: [1, kValue.f32.infinity.negative], expected: kAny }, + { input: [-1, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny }, + + // Edge of f32 + { input: [kValue.f32.positive.max, kValue.f32.positive.max], expected: kAny }, + { input: [kValue.f32.negative.min, kValue.f32.negative.min], expected: kAny }, + { input: [kValue.f32.positive.max, kValue.f32.negative.min], expected: kAny }, + { input: [kValue.f32.negative.min, kValue.f32.positive.max], expected: kAny }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = multiplicationInterval(x, y); + t.expect( + objectEquals(expected, got), + `multiplicationInterval(${x}, ${y}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('remainderInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // 32-bit normals + { input: [0, 1], expected: [0, 0] }, + { input: [0, -1], expected: [0, 0] }, + { input: [1, 1], expected: [0, 1] }, + { input: [1, -1], expected: [0, 1] }, + { input: [-1, 1], expected: [-1, 0] }, + { input: [-1, -1], expected: [-1, 0] }, + { input: [4, 2], expected: [0, 2] }, + { input: [-4, 2], expected: [-2, 0] }, + { input: [4, -2], expected: [0, 2] }, + { input: [-4, -2], expected: [-2, 0] }, + { input: [2, 4], expected: [2, 2] }, + { input: [-2, 4], expected: [-2, -2] }, + { input: [2, -4], expected: [2, 2] }, + { input: [-2, -4], expected: [-2, -2] }, + + // 64-bit normals + { input: [0, 0.1], expected: [0, 0] }, + { input: [0, -0.1], expected: [0, 0] }, + { input: [1, 0.1], expected: [hexToF32(0xb4000000), hexToF32(0x3dccccd8)] }, // ~[0, 0.1] + { input: [-1, 0.1], expected: [hexToF32(0xbdccccd8), hexToF32(0x34000000)] }, // ~[-0.1, 0] + { input: [1, -0.1], expected: [hexToF32(0xb4000000), hexToF32(0x3dccccd8)] }, // ~[0, 0.1] + { input: [-1, -0.1], expected: [hexToF32(0xbdccccd8), hexToF32(0x34000000)] }, // ~[-0.1, 0] + + // Denominator out of range + { input: [1, kValue.f32.infinity.positive], expected: kAny }, + { input: [1, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + { input: [1, kValue.f32.positive.max], expected: kAny }, + { input: [1, kValue.f32.negative.min], expected: kAny }, + { input: [1, 0], expected: kAny }, + { input: [1, kValue.f32.subnormal.positive.max], expected: kAny }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = remainderInterval(x, y); + t.expect( + objectEquals(expected, got), + `remainderInterval(${x}, ${y}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('powInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: [-1, 0], expected: kAny }, + { input: [0, 0], expected: kAny }, + { input: [1, 0], expected: [minusNULP(1, 3), hexToF64(0x3ff00000, 0x30000000)] }, // ~1 + { input: [2, 0], expected: [minusNULP(1, 3), hexToF64(0x3ff00000, 0x30000000)] }, // ~1 + { input: [kValue.f32.positive.max, 0], expected: [minusNULP(1, 3), hexToF64(0x3ff00000, 0x30000000)] }, // ~1 + { input: [0, 1], expected: kAny }, + { input: [1, 1], expected: [hexToF64(0x3feffffe, 0xdffffe00), hexToF64(0x3ff00000, 0xc0000200)] }, // ~1 + { input: [1, 100], expected: [hexToF64(0x3fefffba, 0x3fff3800), hexToF64(0x3ff00023, 0x2000c800)] }, // ~1 + { input: [1, kValue.f32.positive.max], expected: kAny }, + { input: [2, 1], expected: [hexToF64(0x3ffffffe, 0xa0000200), hexToF64(0x40000001, 0x00000200)] }, // ~2 + { input: [2, 2], expected: [hexToF64(0x400ffffd, 0xa0000400), hexToF64(0x40100001, 0xa0000400)] }, // ~4 + { input: [10, 10], expected: [hexToF64(0x4202a04f, 0x51f77000), hexToF64(0x4202a070, 0xee08e000)] }, // ~10000000000 + { input: [10, 1], expected: [hexToF64(0x4023fffe, 0x0b658b00), hexToF64(0x40240002, 0x149a7c00)] }, // ~10 + { input: [kValue.f32.positive.max, 1], expected: kAny }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = powInterval(x, y); + t.expect( + objectEquals(expected, got), + `powInterval(${x}, ${y}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('stepInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // 32-bit normals + { input: [0, 0], expected: [1] }, + { input: [1, 1], expected: [1] }, + { input: [0, 1], expected: [1] }, + { input: [1, 0], expected: [0] }, + { input: [-1, -1], expected: [1] }, + { input: [0, -1], expected: [0] }, + { input: [-1, 0], expected: [1] }, + { input: [-1, 1], expected: [1] }, + { input: [1, -1], expected: [0] }, + + // 64-bit normals + { input: [0.1, 0.1], expected: [0, 1] }, + { input: [0, 0.1], expected: [1] }, + { input: [0.1, 0], expected: [0] }, + { input: [0.1, 1], expected: [1] }, + { input: [1, 0.1], expected: [0] }, + { input: [-0.1, -0.1], expected: [0, 1] }, + { input: [0, -0.1], expected: [0] }, + { input: [-0.1, 0], expected: [1] }, + { input: [-0.1, -1], expected: [0] }, + { input: [-1, -0.1], expected: [1] }, + + // Subnormals + { input: [0, kValue.f32.subnormal.positive.max], expected: [1] }, + { input: [0, kValue.f32.subnormal.positive.min], expected: [1] }, + { input: [0, kValue.f32.subnormal.negative.max], expected: [0, 1] }, + { input: [0, kValue.f32.subnormal.negative.min], expected: [0, 1] }, + { input: [1, kValue.f32.subnormal.positive.max], expected: [0] }, + { input: [1, kValue.f32.subnormal.positive.min], expected: [0] }, + { input: [1, kValue.f32.subnormal.negative.max], expected: [0] }, + { input: [1, kValue.f32.subnormal.negative.min], expected: [0] }, + { input: [-1, kValue.f32.subnormal.positive.max], expected: [1] }, + { input: [-1, kValue.f32.subnormal.positive.min], expected: [1] }, + { input: [-1, kValue.f32.subnormal.negative.max], expected: [1] }, + { input: [-1, kValue.f32.subnormal.negative.min], expected: [1] }, + { input: [kValue.f32.subnormal.positive.max, 0], expected: [0, 1] }, + { input: [kValue.f32.subnormal.positive.min, 0], expected: [0, 1] }, + { input: [kValue.f32.subnormal.negative.max, 0], expected: [1] }, + { input: [kValue.f32.subnormal.negative.min, 0], expected: [1] }, + { input: [kValue.f32.subnormal.positive.max, 1], expected: [1] }, + { input: [kValue.f32.subnormal.positive.min, 1], expected: [1] }, + { input: [kValue.f32.subnormal.negative.max, 1], expected: [1] }, + { input: [kValue.f32.subnormal.negative.min, 1], expected: [1] }, + { input: [kValue.f32.subnormal.positive.max, -1], expected: [0] }, + { input: [kValue.f32.subnormal.positive.min, -1], expected: [0] }, + { input: [kValue.f32.subnormal.negative.max, -1], expected: [0] }, + { input: [kValue.f32.subnormal.negative.min, -1], expected: [0] }, + { input: [kValue.f32.subnormal.negative.min, kValue.f32.subnormal.positive.max], expected: [1] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.min], expected: [0, 1] }, + + // Infinities + { input: [0, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, 0], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, 0], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + ] + ) + .fn(t => { + const [edge, x] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = stepInterval(edge, x); + t.expect( + objectEquals(expected, got), + `stepInterval(${edge}, ${x}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('subtractionInterval') + .paramsSubcasesOnly<BinaryToIntervalCase>( + // prettier-ignore + [ + // 32-bit normals + { input: [0, 0], expected: [0] }, + { input: [1, 0], expected: [1] }, + { input: [0, 1], expected: [-1] }, + { input: [-1, 0], expected: [-1] }, + { input: [0, -1], expected: [1] }, + { input: [1, 1], expected: [0] }, + { input: [1, -1], expected: [2] }, + { input: [-1, 1], expected: [-2] }, + { input: [-1, -1], expected: [0] }, + + // 64-bit normals + { input: [0.1, 0], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [0, 0.1], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + { input: [-0.1, 0], expected: [hexToF32(0xbdcccccd), plusOneULP(hexToF32(0xbdcccccd))] }, // ~-0.1 + { input: [0, -0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)), hexToF32(0x3dcccccd)] }, // ~0.1 + { input: [0.1, 0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)) - hexToF32(0x3dcccccd), hexToF32(0x3dcccccd) - minusOneULP(hexToF32(0x3dcccccd))] }, // ~0.0 + { input: [0.1, -0.1], expected: [minusOneULP(hexToF32(0x3e4ccccd)), hexToF32(0x3e4ccccd)] }, // ~0.2 + { input: [-0.1, 0.1], expected: [hexToF32(0xbe4ccccd), plusOneULP(hexToF32(0xbe4ccccd))] }, // ~-0.2 + { input: [-0.1, -0.1], expected: [minusOneULP(hexToF32(0x3dcccccd)) - hexToF32(0x3dcccccd), hexToF32(0x3dcccccd) - minusOneULP(hexToF32(0x3dcccccd))] }, // ~0 + + // // 32-bit normals + { input: [kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [0, kValue.f32.subnormal.positive.max], expected: [kValue.f32.subnormal.negative.min, 0] }, + { input: [kValue.f32.subnormal.positive.min, 0], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [0, kValue.f32.subnormal.positive.min], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [kValue.f32.subnormal.negative.max, 0], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [0, kValue.f32.subnormal.negative.max], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [kValue.f32.subnormal.negative.min, 0], expected: [kValue.f32.subnormal.negative.min, 0] }, + { input: [0, kValue.f32.subnormal.negative.min], expected: [0, kValue.f32.subnormal.positive.max] }, + + // Infinities + { input: [0, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, 0], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, 0], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = subtractionInterval(x, y); + t.expect( + objectEquals(expected, got), + `subtractionInterval(${x}, ${y}) returned ${got}. Expected ${expected}` + ); + }); + +interface TernaryToIntervalCase { + input: [number, number, number]; + expected: IntervalBounds; +} + +g.test('clampMedianInterval') + .paramsSubcasesOnly<TernaryToIntervalCase>( + // prettier-ignore + [ + // Normals + { input: [0, 0, 0], expected: [0] }, + { input: [1, 0, 0], expected: [0] }, + { input: [0, 1, 0], expected: [0] }, + { input: [0, 0, 1], expected: [0] }, + { input: [1, 0, 1], expected: [1] }, + { input: [1, 1, 0], expected: [1] }, + { input: [0, 1, 1], expected: [1] }, + { input: [1, 1, 1], expected: [1] }, + { input: [1, 10, 100], expected: [10] }, + { input: [10, 1, 100], expected: [10] }, + { input: [100, 1, 10], expected: [10] }, + { input: [-10, 1, 100], expected: [1] }, + { input: [10, 1, -100], expected: [1] }, + { input: [-10, 1, -100], expected: [-10] }, + { input: [-10, -10, -10], expected: [-10] }, + + // Subnormals + { input: [kValue.f32.subnormal.positive.max, 0, 0], expected: [0] }, + { input: [0, kValue.f32.subnormal.positive.max, 0], expected: [0] }, + { input: [0, 0, kValue.f32.subnormal.positive.max], expected: [0] }, + { input: [kValue.f32.subnormal.positive.max, 0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [0, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.negative.max], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [kValue.f32.positive.max, kValue.f32.positive.max, kValue.f32.subnormal.positive.min], expected: [kValue.f32.positive.max] }, + + // Infinities + { input: [0, 1, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + ] + ) + .fn(t => { + const [x, y, z] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = clampMedianInterval(x, y, z); + t.expect( + objectEquals(expected, got), + `clampMedianInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('clampMinMaxInterval') + .paramsSubcasesOnly<TernaryToIntervalCase>( + // prettier-ignore + [ + // Normals + { input: [0, 0, 0], expected: [0] }, + { input: [1, 0, 0], expected: [0] }, + { input: [0, 1, 0], expected: [0] }, + { input: [0, 0, 1], expected: [0] }, + { input: [1, 0, 1], expected: [1] }, + { input: [1, 1, 0], expected: [0] }, + { input: [0, 1, 1], expected: [1] }, + { input: [1, 1, 1], expected: [1] }, + { input: [1, 10, 100], expected: [10] }, + { input: [10, 1, 100], expected: [10] }, + { input: [100, 1, 10], expected: [10] }, + { input: [-10, 1, 100], expected: [1] }, + { input: [10, 1, -100], expected: [-100] }, + { input: [-10, 1, -100], expected: [-100] }, + { input: [-10, -10, -10], expected: [-10] }, + + // Subnormals + { input: [kValue.f32.subnormal.positive.max, 0, 0], expected: [0] }, + { input: [0, kValue.f32.subnormal.positive.max, 0], expected: [0] }, + { input: [0, 0, kValue.f32.subnormal.positive.max], expected: [0] }, + { input: [kValue.f32.subnormal.positive.max, 0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, 0], expected: [0] }, + { input: [0, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, 0] }, + { input: [kValue.f32.positive.max, kValue.f32.positive.max, kValue.f32.subnormal.positive.min], expected: [0, kValue.f32.subnormal.positive.min] }, + + // Infinities + { input: [0, 1, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + ] + ) + .fn(t => { + const [x, y, z] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = clampMinMaxInterval(x, y, z); + t.expect( + objectEquals(expected, got), + `clampMinMaxInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('fmaInterval') + .paramsSubcasesOnly<TernaryToIntervalCase>( + // prettier-ignore + [ + // Normals + { input: [0, 0, 0], expected: [0] }, + { input: [1, 0, 0], expected: [0] }, + { input: [0, 1, 0], expected: [0] }, + { input: [0, 0, 1], expected: [1] }, + { input: [1, 0, 1], expected: [1] }, + { input: [1, 1, 0], expected: [1] }, + { input: [0, 1, 1], expected: [1] }, + { input: [1, 1, 1], expected: [2] }, + { input: [1, 10, 100], expected: [110] }, + { input: [10, 1, 100], expected: [110] }, + { input: [100, 1, 10], expected: [110] }, + { input: [-10, 1, 100], expected: [90] }, + { input: [10, 1, -100], expected: [-90] }, + { input: [-10, 1, -100], expected: [-110] }, + { input: [-10, -10, -10], expected: [90] }, + + // Subnormals + { input: [kValue.f32.subnormal.positive.max, 0, 0], expected: [0] }, + { input: [0, kValue.f32.subnormal.positive.max, 0], expected: [0] }, + { input: [0, 0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.max, 0, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, 0], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [0, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.max] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.positive.min] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.negative.max], expected: [kValue.f32.subnormal.negative.max, kValue.f32.subnormal.positive.min] }, + { input: [kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max], expected: [hexToF32(0x80000002), 0] }, + + // Infinities + { input: [0, 1, kValue.f32.infinity.positive], expected: kAny }, + { input: [0, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.positive], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, kValue.f32.infinity.negative], expected: kAny }, + { input: [kValue.f32.positive.max, kValue.f32.positive.max, kValue.f32.subnormal.positive.min], expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = fmaInterval(...t.params.input); + t.expect( + objectEquals(expected, got), + `fmaInterval(${t.params.input.join(',')}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('mixImpreciseInterval') + .paramsSubcasesOnly<TernaryToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + // [0.0, 1.0] cases + { input: [0.0, 1.0, -1.0], expected: [-1.0] }, + { input: [0.0, 1.0, 0.0], expected: [0.0] }, + { input: [0.0, 1.0, 0.1], expected: [hexToF64(0x3fb99999,0x80000000), hexToF64(0x3fb99999,0xa0000000)] }, // ~0.1 + { input: [0.0, 1.0, 0.5], expected: [0.5] }, + { input: [0.0, 1.0, 0.9], expected: [hexToF64(0x3feccccc,0xc0000000), hexToF64(0x3feccccc,0xe0000000)] }, // ~0.9 + { input: [0.0, 1.0, 1.0], expected: [1.0] }, + { input: [0.0, 1.0, 2.0], expected: [2.0] }, + + // [1.0, 0.0] cases + { input: [1.0, 0.0, -1.0], expected: [2.0] }, + { input: [1.0, 0.0, 0.0], expected: [1.0] }, + { input: [1.0, 0.0, 0.1], expected: [hexToF64(0x3feccccc,0xc0000000), hexToF64(0x3feccccc,0xe0000000)] }, // ~0.9 + { input: [1.0, 0.0, 0.5], expected: [0.5] }, + { input: [1.0, 0.0, 0.9], expected: [hexToF64(0x3fb99999,0x00000000), hexToF64(0x3fb9999a,0x00000000)] }, // ~0.1 + { input: [1.0, 0.0, 1.0], expected: [0.0] }, + { input: [1.0, 0.0, 2.0], expected: [-1.0] }, + + // [0.0, 10.0] cases + { input: [0.0, 10.0, -1.0], expected: [-10.0] }, + { input: [0.0, 10.0, 0.0], expected: [0.0] }, + { input: [0.0, 10.0, 0.1], expected: [hexToF64(0x3fefffff,0xe0000000), hexToF64(0x3ff00000,0x20000000)] }, // ~1 + { input: [0.0, 10.0, 0.5], expected: [5.0] }, + { input: [0.0, 10.0, 0.9], expected: [hexToF64(0x4021ffff,0xe0000000), hexToF64(0x40220000,0x20000000)] }, // ~9 + { input: [0.0, 10.0, 1.0], expected: [10.0] }, + { input: [0.0, 10.0, 2.0], expected: [20.0] }, + + // [2.0, 10.0] cases + { input: [2.0, 10.0, -1.0], expected: [-6.0] }, + { input: [2.0, 10.0, 0.0], expected: [2.0] }, + { input: [2.0, 10.0, 0.1], expected: [hexToF64(0x40066666,0x60000000), hexToF64(0x40066666,0x80000000)] }, // ~2.8 + { input: [2.0, 10.0, 0.5], expected: [6.0] }, + { input: [2.0, 10.0, 0.9], expected: [hexToF64(0x40226666,0x60000000), hexToF64(0x40226666,0x80000000)] }, // ~9.2 + { input: [2.0, 10.0, 1.0], expected: [10.0] }, + { input: [2.0, 10.0, 2.0], expected: [18.0] }, + + // [-1.0, 1.0] cases + { input: [-1.0, 1.0, -2.0], expected: [-5.0] }, + { input: [-1.0, 1.0, 0.0], expected: [-1.0] }, + { input: [-1.0, 1.0, 0.1], expected: [hexToF64(0xbfe99999,0xa0000000), hexToF64(0xbfe99999,0x80000000)] }, // ~-0.8 + { input: [-1.0, 1.0, 0.5], expected: [0.0] }, + { input: [-1.0, 1.0, 0.9], expected: [hexToF64(0x3fe99999,0x80000000), hexToF64(0x3fe99999,0xc0000000)] }, // ~0.8 + { input: [-1.0, 1.0, 1.0], expected: [1.0] }, + { input: [-1.0, 1.0, 2.0], expected: [3.0] }, + + // Infinities + { input: [0.0, kValue.f32.infinity.positive, 0.5], expected: kAny }, + { input: [kValue.f32.infinity.positive, 0.0, 0.5], expected: kAny }, + { input: [kValue.f32.infinity.negative, 1.0, 0.5], expected: kAny }, + { input: [1.0, kValue.f32.infinity.negative, 0.5], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, 0.5], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative, 0.5], expected: kAny }, + { input: [0.0, 1.0, kValue.f32.infinity.negative], expected: kAny }, + { input: [1.0, 0.0, kValue.f32.infinity.negative], expected: kAny }, + { input: [0.0, 1.0, kValue.f32.infinity.positive], expected: kAny }, + { input: [1.0, 0.0, kValue.f32.infinity.positive], expected: kAny }, + + // Showing how precise and imprecise versions diff + { input: [kValue.f32.negative.min, 10.0, 1.0], expected: [0.0]}, + ] + ) + .fn(t => { + const [x, y, z] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = mixImpreciseInterval(x, y, z); + t.expect( + objectEquals(expected, got), + `mixImpreciseInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('mixPreciseInterval') + .paramsSubcasesOnly<TernaryToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + // [0.0, 1.0] cases + { input: [0.0, 1.0, -1.0], expected: [-1.0] }, + { input: [0.0, 1.0, 0.0], expected: [0.0] }, + { input: [0.0, 1.0, 0.1], expected: [hexToF64(0x3fb99999,0x80000000), hexToF64(0x3fb99999,0xa0000000)] }, // ~0.1 + { input: [0.0, 1.0, 0.5], expected: [0.5] }, + { input: [0.0, 1.0, 0.9], expected: [hexToF64(0x3feccccc,0xc0000000), hexToF64(0x3feccccc,0xe0000000)] }, // ~0.9 + { input: [0.0, 1.0, 1.0], expected: [1.0] }, + { input: [0.0, 1.0, 2.0], expected: [2.0] }, + + // [1.0, 0.0] cases + { input: [1.0, 0.0, -1.0], expected: [2.0] }, + { input: [1.0, 0.0, 0.0], expected: [1.0] }, + { input: [1.0, 0.0, 0.1], expected: [hexToF64(0x3feccccc,0xc0000000), hexToF64(0x3feccccc,0xe0000000)] }, // ~0.9 + { input: [1.0, 0.0, 0.5], expected: [0.5] }, + { input: [1.0, 0.0, 0.9], expected: [hexToF64(0x3fb99999,0x00000000), hexToF64(0x3fb9999a,0x00000000)] }, // ~0.1 + { input: [1.0, 0.0, 1.0], expected: [0.0] }, + { input: [1.0, 0.0, 2.0], expected: [-1.0] }, + + // [0.0, 10.0] cases + { input: [0.0, 10.0, -1.0], expected: [-10.0] }, + { input: [0.0, 10.0, 0.0], expected: [0.0] }, + { input: [0.0, 10.0, 0.1], expected: [hexToF64(0x3fefffff,0xe0000000), hexToF64(0x3ff00000,0x20000000)] }, // ~1 + { input: [0.0, 10.0, 0.5], expected: [5.0] }, + { input: [0.0, 10.0, 0.9], expected: [hexToF64(0x4021ffff,0xe0000000), hexToF64(0x40220000,0x20000000)] }, // ~9 + { input: [0.0, 10.0, 1.0], expected: [10.0] }, + { input: [0.0, 10.0, 2.0], expected: [20.0] }, + + // [2.0, 10.0] cases + { input: [2.0, 10.0, -1.0], expected: [-6.0] }, + { input: [2.0, 10.0, 0.0], expected: [2.0] }, + { input: [2.0, 10.0, 0.1], expected: [hexToF64(0x40066666,0x40000000), hexToF64(0x40066666,0x80000000)] }, // ~2.8 + { input: [2.0, 10.0, 0.5], expected: [6.0] }, + { input: [2.0, 10.0, 0.9], expected: [hexToF64(0x40226666,0x40000000), hexToF64(0x40226666,0xa0000000)] }, // ~9.2 + { input: [2.0, 10.0, 1.0], expected: [10.0] }, + { input: [2.0, 10.0, 2.0], expected: [18.0] }, + + // [-1.0, 1.0] cases + { input: [-1.0, 1.0, -2.0], expected: [-5.0] }, + { input: [-1.0, 1.0, 0.0], expected: [-1.0] }, + { input: [-1.0, 1.0, 0.1], expected: [hexToF64(0xbfe99999,0xc0000000), hexToF64(0xbfe99999,0x80000000)] }, // ~-0.8 + { input: [-1.0, 1.0, 0.5], expected: [0.0] }, + { input: [-1.0, 1.0, 0.9], expected: [hexToF64(0x3fe99999,0x80000000), hexToF64(0x3fe99999,0xc0000000)] }, // ~0.8 + { input: [-1.0, 1.0, 1.0], expected: [1.0] }, + { input: [-1.0, 1.0, 2.0], expected: [3.0] }, + + // Infinities + { input: [0.0, kValue.f32.infinity.positive, 0.5], expected: kAny }, + { input: [kValue.f32.infinity.positive, 0.0, 0.5], expected: kAny }, + { input: [kValue.f32.infinity.negative, 1.0, 0.5], expected: kAny }, + { input: [1.0, kValue.f32.infinity.negative, 0.5], expected: kAny }, + { input: [kValue.f32.infinity.negative, kValue.f32.infinity.positive, 0.5], expected: kAny }, + { input: [kValue.f32.infinity.positive, kValue.f32.infinity.negative, 0.5], expected: kAny }, + { input: [0.0, 1.0, kValue.f32.infinity.negative], expected: kAny }, + { input: [1.0, 0.0, kValue.f32.infinity.negative], expected: kAny }, + { input: [0.0, 1.0, kValue.f32.infinity.positive], expected: kAny }, + { input: [1.0, 0.0, kValue.f32.infinity.positive], expected: kAny }, + + // Showing how precise and imprecise versions diff + { input: [kValue.f32.negative.min, 10.0, 1.0], expected: [10.0]}, + ] + ) + .fn(t => { + const [x, y, z] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = mixPreciseInterval(x, y, z); + t.expect( + objectEquals(expected, got), + `mixPreciseInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}` + ); + }); + +g.test('smoothStepInterval') + .paramsSubcasesOnly<TernaryToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + // Normals + { input: [0, 1, 0], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [0, 1, 1], expected: [hexToF32(0x3f7ffffa), hexToF32(0x3f800003)] }, // ~1 + { input: [0, 1, 10], expected: [1] }, + { input: [0, 1, -10], expected: [0] }, + { input: [0, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5 + { input: [0, 2, 0.5], expected: [hexToF32(0x3e1ffffb), hexToF32(0x3e200007)] }, // ~0.15625... + { input: [2, 0, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5 + { input: [2, 0, 1.5], expected: [hexToF32(0x3e1ffffb), hexToF32(0x3e200007)] }, // ~0.15625... + { input: [0, 100, 50], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5 + { input: [0, 100, 25], expected: [hexToF32(0x3e1ffffb), hexToF32(0x3e200007)] }, // ~0.15625... + { input: [0, -2, -1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5 + { input: [0, -2, -0.5], expected: [hexToF32(0x3e1ffffb), hexToF32(0x3e200007)] }, // ~0.15625... + + // Subnormals + { input: [0, 2, kValue.f32.subnormal.positive.max], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [0, 2, kValue.f32.subnormal.positive.min], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [0, 2, kValue.f32.subnormal.negative.max], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [0, 2, kValue.f32.subnormal.negative.min], expected: [0, kValue.f32.subnormal.positive.min] }, + { input: [kValue.f32.subnormal.positive.max, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5 + { input: [kValue.f32.subnormal.positive.min, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5 + { input: [kValue.f32.subnormal.negative.max, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5 + { input: [kValue.f32.subnormal.negative.min, 2, 1], expected: [hexToF32(0x3efffff8), hexToF32(0x3f000007)] }, // ~0.5 + { input: [0, kValue.f32.subnormal.positive.max, 1], expected: kAny }, + { input: [0, kValue.f32.subnormal.positive.min, 1], expected: kAny }, + { input: [0, kValue.f32.subnormal.negative.max, 1], expected: kAny }, + { input: [0, kValue.f32.subnormal.negative.min, 1], expected: kAny }, + + // Infinities + { input: [0, 2, Number.POSITIVE_INFINITY], expected: kAny }, + { input: [0, 2, Number.NEGATIVE_INFINITY], expected: kAny }, + { input: [Number.POSITIVE_INFINITY, 2, 1], expected: kAny }, + { input: [Number.NEGATIVE_INFINITY, 2, 1], expected: kAny }, + { input: [0, Number.POSITIVE_INFINITY, 1], expected: kAny }, + { input: [0, Number.NEGATIVE_INFINITY, 1], expected: kAny }, + ] + ) + .fn(t => { + const [low, high, x] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = smoothStepInterval(low, high, x); + t.expect( + objectEquals(expected, got), + `smoothStepInterval(${low}, ${high}, ${x}) returned ${got}. Expected ${expected}` + ); + }); + +interface PointToVectorCase { + input: number; + expected: IntervalBounds[]; +} + +// Scope for unpack* tests so that they can have constants for magic numbers +// that don't pollute the global namespace or have unwieldy long names. +{ + const kZeroBounds: IntervalBounds = [hexToF32(0x81200000), hexToF32(0x01200000)]; + const kOneBoundsSnorm: IntervalBounds = [ + hexToF64(0x3fefffff, 0xa0000000), + hexToF64(0x3ff00000, 0x40000000), + ]; + const kOneBoundsUnorm: IntervalBounds = [ + hexToF64(0x3fefffff, 0xb0000000), + hexToF64(0x3ff00000, 0x28000000), + ]; + const kNegOneBoundsSnorm: IntervalBounds = [ + hexToF64(0xbff00000, 0x00000000), + hexToF64(0xbfefffff, 0xa0000000), + ]; + + const kHalfBounds2x16snorm: IntervalBounds = [ + hexToF64(0x3fe0001f, 0xa0000000), + hexToF64(0x3fe00020, 0x80000000), + ]; // ~0.5..., due to lack of precision in i16 + const kNegHalfBounds2x16snorm: IntervalBounds = [ + hexToF64(0xbfdfffc0, 0x60000000), + hexToF64(0xbfdfffbf, 0x80000000), + ]; // ~-0.5..., due to lack of precision in i16 + + g.test('unpack2x16snormInterval') + .paramsSubcasesOnly<PointToVectorCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: 0x00000000, expected: [kZeroBounds, kZeroBounds] }, + { input: 0x00007fff, expected: [kOneBoundsSnorm, kZeroBounds] }, + { input: 0x7fff0000, expected: [kZeroBounds, kOneBoundsSnorm] }, + { input: 0x7fff7fff, expected: [kOneBoundsSnorm, kOneBoundsSnorm] }, + { input: 0x80018001, expected: [kNegOneBoundsSnorm, kNegOneBoundsSnorm] }, + { input: 0x40004000, expected: [kHalfBounds2x16snorm, kHalfBounds2x16snorm] }, + { input: 0xc001c001, expected: [kNegHalfBounds2x16snorm, kNegHalfBounds2x16snorm] }, + ] + ) + .fn(t => { + const expected = toF32Vector(t.params.expected); + + const got = unpack2x16snormInterval(t.params.input); + + t.expect( + objectEquals(expected, got), + `unpack2x16snormInterval(${t.params.input}) returned [${got}]. Expected [${expected}]` + ); + }); + + g.test('unpack2x16floatInterval') + .paramsSubcasesOnly<PointToVectorCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + // f16 normals + { input: 0x00000000, expected: [[0], [0]] }, + { input: 0x80000000, expected: [[0], [0]] }, + { input: 0x00008000, expected: [[0], [0]] }, + { input: 0x80008000, expected: [[0], [0]] }, + { input: 0x00003c00, expected: [[1], [0]] }, + { input: 0x3c000000, expected: [[0], [1]] }, + { input: 0x3c003c00, expected: [[1], [1]] }, + { input: 0xbc00bc00, expected: [[-1], [-1]] }, + { input: 0x49004900, expected: [[10], [10]] }, + { input: 0xc900c900, expected: [[-10], [-10]] }, + + // f16 subnormals + { input: 0x000003ff, expected: [[0, kValue.f16.subnormal.positive.max], [0]] }, + { input: 0x000083ff, expected: [[kValue.f16.subnormal.negative.min, 0], [0]] }, + + // f16 out of bounds + { input: 0x7c000000, expected: [kAny, kAny] }, + { input: 0xffff0000, expected: [kAny, kAny] }, + ] + ) + .fn(t => { + const expected = toF32Vector(t.params.expected); + + const got = unpack2x16floatInterval(t.params.input); + + t.expect( + objectEquals(expected, got), + `unpack2x16floatInterval(${t.params.input}) returned [${got}]. Expected [${expected}]` + ); + }); + + const kHalfBounds2x16unorm: IntervalBounds = [ + hexToF64(0x3fe0000f, 0xb0000000), + hexToF64(0x3fe00010, 0x70000000), + ]; // ~0.5..., due to lack of precision in u16 + + g.test('unpack2x16unormInterval') + .paramsSubcasesOnly<PointToVectorCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: 0x00000000, expected: [kZeroBounds, kZeroBounds] }, + { input: 0x0000ffff, expected: [kOneBoundsUnorm, kZeroBounds] }, + { input: 0xffff0000, expected: [kZeroBounds, kOneBoundsUnorm] }, + { input: 0xffffffff, expected: [kOneBoundsUnorm, kOneBoundsUnorm] }, + { input: 0x80008000, expected: [kHalfBounds2x16unorm, kHalfBounds2x16unorm] }, + ] + ) + .fn(t => { + const expected = toF32Vector(t.params.expected); + + const got = unpack2x16unormInterval(t.params.input); + + t.expect( + objectEquals(expected, got), + `unpack2x16unormInterval(${t.params.input}) returned [${got}]. Expected [${expected}]` + ); + }); + + const kHalfBounds4x8snorm: IntervalBounds = [ + hexToF64(0x3fe02040, 0x20000000), + hexToF64(0x3fe02041, 0x00000000), + ]; // ~0.50196..., due to lack of precision in i8 + const kNegHalfBounds4x8snorm: IntervalBounds = [ + hexToF64(0xbfdfbf7f, 0x60000000), + hexToF64(0xbfdfbf7e, 0x80000000), + ]; // ~-0.49606..., due to lack of precision in i8 + + g.test('unpack4x8snormInterval') + .paramsSubcasesOnly<PointToVectorCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: 0x00000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kZeroBounds] }, + { input: 0x0000007f, expected: [kOneBoundsSnorm, kZeroBounds, kZeroBounds, kZeroBounds] }, + { input: 0x00007f00, expected: [kZeroBounds, kOneBoundsSnorm, kZeroBounds, kZeroBounds] }, + { input: 0x007f0000, expected: [kZeroBounds, kZeroBounds, kOneBoundsSnorm, kZeroBounds] }, + { input: 0x7f000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kOneBoundsSnorm] }, + { input: 0x00007f7f, expected: [kOneBoundsSnorm, kOneBoundsSnorm, kZeroBounds, kZeroBounds] }, + { input: 0x7f7f0000, expected: [kZeroBounds, kZeroBounds, kOneBoundsSnorm, kOneBoundsSnorm] }, + { input: 0x7f007f00, expected: [kZeroBounds, kOneBoundsSnorm, kZeroBounds, kOneBoundsSnorm] }, + { input: 0x007f007f, expected: [kOneBoundsSnorm, kZeroBounds, kOneBoundsSnorm, kZeroBounds] }, + { input: 0x7f7f7f7f, expected: [kOneBoundsSnorm, kOneBoundsSnorm, kOneBoundsSnorm, kOneBoundsSnorm] }, + { input: 0x81818181, expected: [kNegOneBoundsSnorm, kNegOneBoundsSnorm, kNegOneBoundsSnorm, kNegOneBoundsSnorm] }, + { input: 0x40404040, expected: [kHalfBounds4x8snorm, kHalfBounds4x8snorm, kHalfBounds4x8snorm, kHalfBounds4x8snorm] }, + { input: 0xc1c1c1c1, expected: [kNegHalfBounds4x8snorm, kNegHalfBounds4x8snorm, kNegHalfBounds4x8snorm, kNegHalfBounds4x8snorm] }, + ] + ) + .fn(t => { + const expected = toF32Vector(t.params.expected); + + const got = unpack4x8snormInterval(t.params.input); + + t.expect( + objectEquals(expected, got), + `unpack4x8snormInterval(${t.params.input}) returned [${got}]. Expected [${expected}]` + ); + }); + + const kHalfBounds4x8unorm: IntervalBounds = [ + hexToF64(0x3fe0100f, 0xb0000000), + hexToF64(0x3fe01010, 0x70000000), + ]; // ~0.50196..., due to lack of precision in u8 + + g.test('unpack4x8unormInterval') + .paramsSubcasesOnly<PointToVectorCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + { input: 0x00000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kZeroBounds] }, + { input: 0x000000ff, expected: [kOneBoundsUnorm, kZeroBounds, kZeroBounds, kZeroBounds] }, + { input: 0x0000ff00, expected: [kZeroBounds, kOneBoundsUnorm, kZeroBounds, kZeroBounds] }, + { input: 0x00ff0000, expected: [kZeroBounds, kZeroBounds, kOneBoundsUnorm, kZeroBounds] }, + { input: 0xff000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kOneBoundsUnorm] }, + { input: 0x0000ffff, expected: [kOneBoundsUnorm, kOneBoundsUnorm, kZeroBounds, kZeroBounds] }, + { input: 0xffff0000, expected: [kZeroBounds, kZeroBounds, kOneBoundsUnorm, kOneBoundsUnorm] }, + { input: 0xff00ff00, expected: [kZeroBounds, kOneBoundsUnorm, kZeroBounds, kOneBoundsUnorm] }, + { input: 0x00ff00ff, expected: [kOneBoundsUnorm, kZeroBounds, kOneBoundsUnorm, kZeroBounds] }, + { input: 0xffffffff, expected: [kOneBoundsUnorm, kOneBoundsUnorm, kOneBoundsUnorm, kOneBoundsUnorm] }, + { input: 0x80808080, expected: [kHalfBounds4x8unorm, kHalfBounds4x8unorm, kHalfBounds4x8unorm, kHalfBounds4x8unorm] }, + ] + ) + .fn(t => { + const expected = toF32Vector(t.params.expected); + + const got = unpack4x8unormInterval(t.params.input); + + t.expect( + objectEquals(expected, got), + `unpack4x8unormInterval(${t.params.input}) returned [${got}]. Expected [${expected}]` + ); + }); +} + +interface VectorToIntervalCase { + input: number[]; + expected: IntervalBounds; +} + +g.test('lengthIntervalVector') + .paramsSubcasesOnly<VectorToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult to express in a closed human readable + // form due to the inherited nature of the errors. + // vec2 + {input: [1.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: [0.0, 1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: [1.0, 1.0], expected: [hexToF64(0x3ff6a09d, 0xb0000000), hexToF64(0x3ff6a09f, 0x10000000)] }, // ~√2 + {input: [-1.0, -1.0], expected: [hexToF64(0x3ff6a09d, 0xb0000000), hexToF64(0x3ff6a09f, 0x10000000)] }, // ~√2 + {input: [-1.0, 1.0], expected: [hexToF64(0x3ff6a09d, 0xb0000000), hexToF64(0x3ff6a09f, 0x10000000)] }, // ~√2 + {input: [0.1, 0.0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + + // vec3 + {input: [1.0, 0.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: [0.0, 1.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: [0.0, 0.0, 1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: [1.0, 1.0, 1.0], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3 + {input: [-1.0, -1.0, -1.0], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3 + {input: [1.0, -1.0, -1.0], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3 + {input: [0.1, 0.0, 0.0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + + // vec4 + {input: [1.0, 0.0, 0.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: [0.0, 1.0, 0.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: [0.0, 0.0, 1.0, 0.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: [0.0, 0.0, 0.0, 1.0], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + {input: [1.0, 1.0, 1.0, 1.0], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2 + {input: [-1.0, -1.0, -1.0, -1.0], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2 + {input: [-1.0, 1.0, -1.0, 1.0], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2 + {input: [0.1, 0.0, 0.0, 0.0], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + + // Test that dot going OOB bounds in the intermediate calculations propagates + { input: [kValue.f32.positive.nearest_max, kValue.f32.positive.max, kValue.f32.negative.min], expected: kAny }, + { input: [kValue.f32.positive.max, kValue.f32.positive.nearest_max, kValue.f32.negative.min], expected: kAny }, + { input: [kValue.f32.negative.min, kValue.f32.positive.max, kValue.f32.positive.nearest_max], expected: kAny }, + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = lengthInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `lengthInterval([${t.params.input}]) returned ${got}. Expected ${expected}` + ); + }); + +interface VectorPairToIntervalCase { + input: [number[], number[]]; + expected: IntervalBounds; +} + +g.test('distanceIntervalVector') + .paramsSubcasesOnly<VectorPairToIntervalCase>( + // prettier-ignore + [ + // Some of these are hard coded, since the error intervals are difficult + // to express in a closed human readable form due to the inherited nature + // of the errors. + // + // distance(x, y), where x - y = 0 has an acceptance interval of kAny, + // because distance(x, y) = length(x - y), and length(0) = kAny + + // vec2 + { input: [[1.0, 0.0], [1.0, 0.0]], expected: kAny }, + { input: [[1.0, 0.0], [0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0], [1.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[-1.0, 0.0], [0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0], [-1.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 1.0], [-1.0, 0.0]], expected: [hexToF64(0x3ff6a09d, 0xb0000000), hexToF64(0x3ff6a09f, 0x10000000)] }, // ~√2 + { input: [[0.1, 0.0], [0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + + // vec3 + { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: kAny }, + { input: [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 1.0, 0.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 0.0], [0.0, 1.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[1.0, 1.0, 1.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3 + { input: [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3 + { input: [[-1.0, -1.0, -1.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3 + { input: [[0.0, 0.0, 0.0], [-1.0, -1.0, -1.0]], expected: [hexToF64(0x3ffbb67a, 0x10000000), hexToF64(0x3ffbb67b, 0xb0000000)] }, // ~√3 + { input: [[0.1, 0.0, 0.0], [0.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + { input: [[0.0, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + + // vec4 + { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: kAny }, + { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]], expected: [hexToF64(0x3fefffff, 0x70000000), hexToF64(0x3ff00000, 0x90000000)] }, // ~1 + { input: [[1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2 + { input: [[0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0]], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2 + { input: [[-1.0, 1.0, -1.0, 1.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2 + { input: [[0.0, 0.0, 0.0, 0.0], [1.0, -1.0, 1.0, -1.0]], expected: [hexToF64(0x3fffffff, 0x70000000), hexToF64(0x40000000, 0x90000000)] }, // ~2 + { input: [[0.1, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + { input: [[0.0, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99998, 0x90000000), hexToF64(0x3fb9999a, 0x70000000)] }, // ~0.1 + ] + ) + .fn(t => { + const expected = toF32Interval(t.params.expected); + + const got = distanceInterval(...t.params.input); + t.expect( + objectEquals(expected, got), + `distanceInterval([${t.params.input[0]}, ${t.params.input[1]}]) returned ${got}. Expected ${expected}` + ); + }); + +g.test('dotInterval') + .paramsSubcasesOnly<VectorPairToIntervalCase>( + // prettier-ignore + [ + // vec2 + { input: [[1.0, 0.0], [1.0, 0.0]], expected: [1.0] }, + { input: [[0.0, 1.0], [0.0, 1.0]], expected: [1.0] }, + { input: [[1.0, 1.0], [1.0, 1.0]], expected: [2.0] }, + { input: [[-1.0, -1.0], [-1.0, -1.0]], expected: [2.0] }, + { input: [[-1.0, 1.0], [1.0, -1.0]], expected: [-2.0] }, + { input: [[0.1, 0.0], [1.0, 0.0]], expected: [hexToF64(0x3fb99999, 0x80000000), hexToF64(0x3fb99999, 0xa0000000)]}, // ~0.1 + + // vec3 + { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [1.0] }, + { input: [[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]], expected: [1.0] }, + { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]], expected: [1.0] }, + { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: [3.0] }, + { input: [[-1.0, -1.0, -1.0], [-1.0, -1.0, -1.0]], expected: [3.0] }, + { input: [[1.0, -1.0, -1.0], [-1.0, 1.0, -1.0]], expected: [-1.0] }, + { input: [[0.1, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99999, 0x80000000), hexToF64(0x3fb99999, 0xa0000000)]}, // ~0.1 + + // vec4 + { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [1.0] }, + { input: [[0.0, 1.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]], expected: [1.0] }, + { input: [[0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: [1.0] }, + { input: [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0]], expected: [1.0] }, + { input: [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], expected: [4.0] }, + { input: [[-1.0, -1.0, -1.0, -1.0], [-1.0, -1.0, -1.0, -1.0]], expected: [4.0] }, + { input: [[-1.0, 1.0, -1.0, 1.0], [1.0, -1.0, 1.0, -1.0]], expected: [-4.0] }, + { input: [[0.1, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [hexToF64(0x3fb99999, 0x80000000), hexToF64(0x3fb99999, 0xa0000000)]}, // ~0.1 + + // Test that going out of bounds in the intermediate calculations is caught correctly. + { input: [[kValue.f32.positive.nearest_max, kValue.f32.positive.max, kValue.f32.negative.min], [1.0, 1.0, 1.0]], expected: kAny }, + { input: [[kValue.f32.positive.nearest_max, kValue.f32.negative.min, kValue.f32.positive.max], [1.0, 1.0, 1.0]], expected: kAny }, + { input: [[kValue.f32.positive.max, kValue.f32.positive.nearest_max, kValue.f32.negative.min], [1.0, 1.0, 1.0]], expected: kAny }, + { input: [[kValue.f32.negative.min, kValue.f32.positive.nearest_max, kValue.f32.positive.max], [1.0, 1.0, 1.0]], expected: kAny }, + { input: [[kValue.f32.positive.max, kValue.f32.negative.min, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kAny }, + { input: [[kValue.f32.negative.min, kValue.f32.positive.max, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kAny }, + + // https://github.com/gpuweb/cts/issues/2155 + { input: [[kValue.f32.positive.max, 1.0, 2.0, 3.0], [-1.0, kValue.f32.positive.max, -2.0, -3.0]], expected: [-13, 0] }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Interval(t.params.expected); + + const got = dotInterval(x, y); + t.expect( + objectEquals(expected, got), + `dotInterval([${x}], [${y}]) returned ${got}. Expected ${expected}` + ); + }); + +interface VectorToVectorCase { + input: number[]; + expected: IntervalBounds[]; +} + +g.test('normalizeInterval') + .paramsSubcasesOnly<VectorToVectorCase>( + // prettier-ignore + [ + // vec2 + {input: [1.0, 0.0], expected: [[hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0] + {input: [0.0, 1.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)]] }, // [ ~0.0, ~1.0] + {input: [-1.0, 0.0], expected: [[hexToF64(0xbff00000, 0xb0000000), hexToF64(0xbfeffffe, 0x70000000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0] + {input: [1.0, 1.0], expected: [[hexToF64(0x3fe6a09d, 0x50000000), hexToF64(0x3fe6a09f, 0x90000000)], [hexToF64(0x3fe6a09d, 0x50000000), hexToF64(0x3fe6a09f, 0x90000000)]] }, // [ ~1/√2, ~1/√2] + + // vec3 + {input: [1.0, 0.0, 0.0], expected: [[hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0] + {input: [0.0, 1.0, 0.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0] + {input: [0.0, 0.0, 1.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)]] }, // [ ~0.0, ~0.0, ~1.0] + {input: [-1.0, 0.0, 0.0], expected: [[hexToF64(0xbff00000, 0xb0000000), hexToF64(0xbfeffffe, 0x70000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0] + {input: [1.0, 1.0, 1.0], expected: [[hexToF64(0x3fe279a6, 0x50000000), hexToF64(0x3fe279a8, 0x50000000)], [hexToF64(0x3fe279a6, 0x50000000), hexToF64(0x3fe279a8, 0x50000000)], [hexToF64(0x3fe279a6, 0x50000000), hexToF64(0x3fe279a8, 0x50000000)]] }, // [ ~1/√3, ~1/√3, ~1/√3] + + // vec4 + {input: [1.0, 0.0, 0.0, 0.0], expected: [[hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] + {input: [0.0, 1.0, 0.0, 0.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0, ~0.0] + {input: [0.0, 0.0, 1.0, 0.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~0.0, ~0.0, ~1.0, ~0.0] + {input: [0.0, 0.0, 0.0, 1.0], expected: [[hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF64(0x3feffffe, 0x70000000), hexToF64(0x3ff00000, 0xb0000000)]] }, // [ ~0.0, ~0.0, ~0.0, ~1.0] + {input: [-1.0, 0.0, 0.0, 0.0], expected: [[hexToF64(0xbff00000, 0xb0000000), hexToF64(0xbfeffffe, 0x70000000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)], [hexToF32(0x81200000), hexToF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0] + {input: [1.0, 1.0, 1.0, 1.0], expected: [[hexToF64(0x3fdffffe, 0x70000000), hexToF64(0x3fe00000, 0xb0000000)], [hexToF64(0x3fdffffe, 0x70000000), hexToF64(0x3fe00000, 0xb0000000)], [hexToF64(0x3fdffffe, 0x70000000), hexToF64(0x3fe00000, 0xb0000000)], [hexToF64(0x3fdffffe, 0x70000000), hexToF64(0x3fe00000, 0xb0000000)]] }, // [ ~1/√4, ~1/√4, ~1/√4] + ] + ) + .fn(t => { + const x = t.params.input; + const expected = toF32Vector(t.params.expected); + + const got = normalizeInterval(x); + t.expect( + objectEquals(expected, got), + `normalizeInterval([${x}]) returned ${got}. Expected ${expected}` + ); + }); + +interface VectorPairToVectorCase { + input: [number[], number[]]; + expected: IntervalBounds[]; +} + +g.test('crossInterval') + .paramsSubcasesOnly<VectorPairToVectorCase>( + // prettier-ignore + [ + // parallel vectors, AXB == 0 + { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[0.0], [0.0], [0.0]] }, + { input: [[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]], expected: [[0.0], [0.0], [0.0]] }, + { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]], expected: [[0.0], [0.0], [0.0]] }, + { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: [[0.0], [0.0], [0.0]] }, + { input: [[-1.0, -1.0, -1.0], [-1.0, -1.0, -1.0]], expected: [[0.0], [0.0], [0.0]] }, + { input: [[0.1, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[0.0], [0.0], [0.0]] }, + { input: [[kValue.f32.subnormal.positive.max, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[0.0], [0.0], [0.0]] }, + + // non-parallel vectors, AXB != 0 + // f32 normals + { input: [[1.0, -1.0, -1.0], [-1.0, 1.0, -1.0]], expected: [[2.0], [2.0], [0.0]] }, + { input: [[1.0, 2, 3], [1.0, 5.0, 7.0]], expected: [[-1], [-4], [3]] }, + + // f64 normals + { input: [[0.1, -0.1, -0.1], [-0.1, 0.1, -0.1]], + expected: [[hexToF32(0x3ca3d708), hexToF32(0x3ca3d70b)], // ~0.02 + [hexToF32(0x3ca3d708), hexToF32(0x3ca3d70b)], // ~0.02 + [hexToF32(0xb1400000), hexToF32(0x31400000)]] }, // ~0 + + // f32 subnormals + { input: [[kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.max, kValue.f32.subnormal.negative.min], + [kValue.f32.subnormal.negative.min, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.negative.max]], + expected: [[0.0, hexToF32(0x00000002)], // ~0 + [0.0, hexToF32(0x00000002)], // ~0 + [hexToF32(0x80000001), hexToF32(0x00000001)]] }, // ~0 + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Vector(t.params.expected); + + const got = crossInterval(x, y); + t.expect( + objectEquals(expected, got), + `crossInterval([${x}], [${y}]) returned ${got}. Expected ${expected}` + ); + }); + +g.test('reflectInterval') + .paramsSubcasesOnly<VectorPairToVectorCase>( + // prettier-ignore + [ + // vec2s + { input: [[1.0, 0.0], [1.0, 0.0]], expected: [[-1.0], [0.0]] }, + { input: [[1.0, 0.0], [0.0, 1.0]], expected: [[1.0], [0.0]] }, + { input: [[0.0, 1.0], [0.0, 1.0]], expected: [[0.0], [-1.0]] }, + { input: [[0.0, 1.0], [1.0, 0.0]], expected: [[0.0], [1.0]] }, + { input: [[1.0, 1.0], [1.0, 1.0]], expected: [[-3.0], [-3.0]] }, + { input: [[-1.0, -1.0], [1.0, 1.0]], expected: [[3.0], [3.0]] }, + { input: [[0.1, 0.1], [1.0, 1.0]], expected: [[hexToF32(0xbe99999a), hexToF32(0xbe999998)], [hexToF32(0xbe99999a), hexToF32(0xbe999998)]] }, // [~-0.3, ~-0.3] + { input: [[kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.max], [1.0, 1.0]], expected: [[hexToF32(0x80fffffe), hexToF32(0x00800001)], [hexToF32(0x80ffffff), hexToF32(0x00000002)]] }, // [~0.0, ~0.0] + + // vec3s + { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[-1.0], [0.0], [0.0]] }, + { input: [[0.0, 1.0, 0.0], [1.0, 0.0, 0.0]], expected: [[0.0], [1.0], [0.0]] }, + { input: [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]], expected: [[0.0], [0.0], [1.0]] }, + { input: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], expected: [[1.0], [0.0], [0.0]] }, + { input: [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], expected: [[1.0], [0.0], [0.0]] }, + { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: [[-5.0], [-5.0], [-5.0]] }, + { input: [[-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]], expected: [[5.0], [5.0], [5.0]] }, + { input: [[0.1, 0.1, 0.1], [1.0, 1.0, 1.0]], expected: [[hexToF32(0xbf000001), hexToF32(0xbefffffe)], [hexToF32(0xbf000001), hexToF32(0xbefffffe)], [hexToF32(0xbf000001), hexToF32(0xbefffffe)]] }, // [~-0.5, ~-0.5, ~-0.5] + { input: [[kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.max, 0.0], [1.0, 1.0, 1.0]], expected: [[hexToF32(0x80fffffe), hexToF32(0x00800001)], [hexToF32(0x80ffffff), hexToF32(0x00000002)], [hexToF32(0x80fffffe), hexToF32(0x00000002)]] }, // [~0.0, ~0.0, ~0.0] + + // vec4s + { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[-1.0], [0.0], [0.0], [0.0]] }, + { input: [[0.0, 1.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[0.0], [1.0], [0.0], [0.0]] }, + { input: [[0.0, 0.0, 1.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[0.0], [0.0], [1.0], [0.0]] }, + { input: [[0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 0.0]], expected: [[0.0], [0.0], [0.0], [1.0]] }, + { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]], expected: [[1.0], [0.0], [0.0], [0.0]] }, + { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: [[1.0], [0.0], [0.0], [0.0]] }, + { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]], expected: [[1.0], [0.0], [0.0], [0.0]] }, + { input: [[-1.0, -1.0, -1.0, -1.0], [1.0, 1.0, 1.0, 1.0]], expected: [[7.0], [7.0], [7.0], [7.0]] }, + { input: [[0.1, 0.1, 0.1, 0.1], [1.0, 1.0, 1.0, 1.0]], expected: [[hexToF32(0xbf333335), hexToF32(0xbf333332)], [hexToF32(0xbf333335), hexToF32(0xbf333332)], [hexToF32(0xbf333335), hexToF32(0xbf333332)], [hexToF32(0xbf333335), hexToF32(0xbf333332)]] }, // [~-0.7, ~-0.7, ~-0.7, ~-0.7] + { input: [[kValue.f32.subnormal.positive.max, kValue.f32.subnormal.negative.max, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0]], expected: [[hexToF32(0x80fffffe), hexToF32(0x00800001)], [hexToF32(0x80ffffff), hexToF32(0x00000002)], [hexToF32(0x80fffffe), hexToF32(0x00000002)], [hexToF32(0x80fffffe), hexToF32(0x00000002)]] }, // [~0.0, ~0.0, ~0.0, ~0.0] + + // Test that dot going OOB bounds in the intermediate calculations propagates + { input: [[kValue.f32.positive.nearest_max, kValue.f32.positive.max, kValue.f32.negative.min], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.positive.nearest_max, kValue.f32.negative.min, kValue.f32.positive.max], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.positive.max, kValue.f32.positive.nearest_max, kValue.f32.negative.min], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.negative.min, kValue.f32.positive.nearest_max, kValue.f32.positive.max], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.positive.max, kValue.f32.negative.min, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.negative.min, kValue.f32.positive.max, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kAny, kAny, kAny] }, + + // Test that post-dot going OOB propagates + { input: [[kValue.f32.positive.max, 1.0, 2.0, 3.0], [-1.0, kValue.f32.positive.max, -2.0, -3.0]], expected: [kAny, kAny, kAny, kAny] }, + ] + ) + .fn(t => { + const [x, y] = t.params.input; + const expected = toF32Vector(t.params.expected); + + const got = reflectInterval(x, y); + t.expect( + objectEquals(expected, got), + `reflectInterval([${x}], [${y}]) returned ${got}. Expected ${expected}` + ); + }); + +interface FaceForwardCase { + input: [number[], number[], number[]]; + expected: (IntervalBounds[] | undefined)[]; +} + +g.test('faceForwardIntervals') + .paramsSubcasesOnly<FaceForwardCase>( + // prettier-ignore + [ + // vec2 + { input: [[1.0, 0.0], [1.0, 0.0], [1.0, 0.0]], expected: [[[-1.0], [0.0]]] }, + { input: [[-1.0, 0.0], [1.0, 0.0], [1.0, 0.0]], expected: [[[1.0], [0.0]]] }, + { input: [[1.0, 0.0], [-1.0, 1.0], [1.0, -1.0]], expected: [[[1.0], [0.0]]] }, + { input: [[-1.0, 0.0], [-1.0, 1.0], [1.0, -1.0]], expected: [[[-1.0], [0.0]]] }, + { input: [[10.0, 0.0], [10.0, 0.0], [10.0, 0.0]], expected: [[[-10.0], [0.0]]] }, + { input: [[-10.0, 0.0], [10.0, 0.0], [10.0, 0.0]], expected: [[[10.0], [0.0]]] }, + { input: [[10.0, 0.0], [-10.0, 10.0], [10.0, -10.0]], expected: [[[10.0], [0.0]]] }, + { input: [[-10.0, 0.0], [-10.0, 10.0], [10.0, -10.0]], expected: [[[-10.0], [0.0]]] }, + { input: [[0.1, 0.0], [0.1, 0.0], [0.1, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0]]] }, + { input: [[-0.1, 0.0], [0.1, 0.0], [0.1, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0]]] }, + { input: [[0.1, 0.0], [-0.1, 0.1], [0.1, -0.1]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0]]] }, + { input: [[-0.1, 0.0], [-0.1, 0.1], [0.1, -0.1]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0]]] }, + + // vec3 + { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[[-1.0], [0.0], [0.0]]] }, + { input: [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[[1.0], [0.0], [0.0]]] }, + { input: [[1.0, 0.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0]], expected: [[[1.0], [0.0], [0.0]]] }, + { input: [[-1.0, 0.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0]], expected: [[[-1.0], [0.0], [0.0]]] }, + { input: [[10.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 0.0, 0.0]], expected: [[[-10.0], [0.0], [0.0]]] }, + { input: [[-10.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 0.0, 0.0]], expected: [[[10.0], [0.0], [0.0]]] }, + { input: [[10.0, 0.0, 0.0], [-10.0, 10.0, 0.0], [10.0, -10.0, 0.0]], expected: [[[10.0], [0.0], [0.0]]] }, + { input: [[-10.0, 0.0, 0.0], [-10.0, 10.0, 0.0], [10.0, -10.0, 0.0]], expected: [[[-10.0], [0.0], [0.0]]] }, + { input: [[0.1, 0.0, 0.0], [0.1, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0], [0.0]]] }, + { input: [[-0.1, 0.0, 0.0], [0.1, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0], [0.0]]] }, + { input: [[0.1, 0.0, 0.0], [-0.1, 0.0, 0.0], [0.1, -0.0, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0], [0.0]]] }, + { input: [[-0.1, 0.0, 0.0], [-0.1, 0.0, 0.0], [0.1, -0.0, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0], [0.0]]] }, + + // vec4 + { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[[-1.0], [0.0], [0.0], [0.0]]] }, + { input: [[-1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[[1.0], [0.0], [0.0], [0.0]]] }, + { input: [[1.0, 0.0, 0.0, 0.0], [-1.0, 1.0, 0.0, 0.0], [1.0, -1.0, 0.0, 0.0]], expected: [[[1.0], [0.0], [0.0], [0.0]]] }, + { input: [[-1.0, 0.0, 0.0, 0.0], [-1.0, 1.0, 0.0, 0.0], [1.0, -1.0, 0.0, 0.0]], expected: [[[-1.0], [0.0], [0.0], [0.0]]] }, + { input: [[10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0]], expected: [[[-10.0], [0.0], [0.0], [0.0]]] }, + { input: [[-10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0]], expected: [[[10.0], [0.0], [0.0], [0.0]]] }, + { input: [[10.0, 0.0, 0.0, 0.0], [-10.0, 10.0, 0.0, 0.0], [10.0, -10.0, 0.0, 0.0]], expected: [[[10.0], [0.0], [0.0], [0.0]]] }, + { input: [[-10.0, 0.0, 0.0, 0.0], [-10.0, 10.0, 0.0, 0.0], [10.0, -10.0, 0.0, 0.0]], expected: [[[-10.0], [0.0], [0.0], [0.0]]] }, + { input: [[0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0], [0.0], [0.0]]] }, + { input: [[-0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0], [0.0], [0.0]]] }, + { input: [[0.1, 0.0, 0.0, 0.0], [-0.1, 0.0, 0.0, 0.0], [0.1, -0.0, 0.0, 0.0]], expected: [[[hexToF32(0x3dcccccc), hexToF32(0x3dcccccd)], [0.0], [0.0], [0.0]]] }, + { input: [[-0.1, 0.0, 0.0, 0.0], [-0.1, 0.0, 0.0, 0.0], [0.1, -0.0, 0.0, 0.0]], expected: [[[hexToF32(0xbdcccccd), hexToF32(0xbdcccccc)], [0.0], [0.0], [0.0]]] }, + + // dot(y, z) === 0 + { input: [[1.0, 1.0], [1.0, 0.0], [0.0, 1.0]], expected: [[[-1.0], [-1.0]]]}, + + // subnormals, also dot(y, z) spans 0 + { input: [[kValue.f32.subnormal.positive.max, 0.0], [kValue.f32.subnormal.positive.min, 0.0], [kValue.f32.subnormal.negative.min, 0.0]], expected: [[[0.0, kValue.f32.subnormal.positive.max], [0.0]], [[kValue.f32.subnormal.negative.min, 0], [0.0]]] }, + + // dot going OOB returns [undefined, x, -x] + { input: [[1.0, 1.0], [kValue.f32.positive.max, kValue.f32.positive.max], [kValue.f32.positive.max, kValue.f32.positive.max]], expected: [undefined, [[1], [1]], [[-1], [-1]]] }, + + ] + ) + .fn(t => { + const [x, y, z] = t.params.input; + const expected = t.params.expected.map(e => (e !== undefined ? toF32Vector(e) : undefined)); + + const got = faceForwardIntervals(x, y, z); + t.expect( + objectEquals(expected, got), + `faceForwardInterval([${x}], [${y}], [${z}]) returned [${got}]. Expected [${expected}]` + ); + }); + +interface RefractCase { + input: [number[], number[], number]; + expected: IntervalBounds[]; +} + +// Scope for refractInterval tests so that they can have constants for magic +// numbers that don't pollute the global namespace or have unwieldy long names. +{ + const kNegativeOneBounds: IntervalBounds = [ + hexToF64(0xbff00000, 0xc0000000), + hexToF64(0xbfefffff, 0x40000000), + ]; + + g.test('refractInterval') + .paramsSubcasesOnly<RefractCase>( + // Some of these are hard coded, since the error intervals are difficult + // to express in a closed human readable form due to the inherited nature + // of the errors. + + // prettier-ignore + [ + // k < 0 + { input: [[1, 1], [0.1, 0], 10], expected: [[0], [0]] }, + + // k contains 0 + { input: [[1, 1], [0.1, 0], 1.005038], expected: [kAny, kAny] }, + + // k > 0 + // vec2 + { input: [[1, 1], [1, 0], 1], expected: [kNegativeOneBounds, [1]] }, + { input: [[1, -2], [3, 4], 5], expected: [[hexToF32(0x40ce87a4), hexToF32(0x40ce8840)], // ~6.454... + [hexToF32(0xc100fae8), hexToF32(0xc100fa80)]] }, // ~-8.061... + + // vec3 + { input: [[1, 1, 1], [1, 0, 0], 1], expected: [kNegativeOneBounds, [1], [1]] }, + { input: [[1, -2, 3], [-4, 5, -6], 7], expected: [[hexToF32(0x40d24480), hexToF32(0x40d24c00)], // ~6.571... + [hexToF32(0xc1576f80), hexToF32(0xc1576ad0)], // ~-13.464... + [hexToF32(0x41a2d9b0), hexToF32(0x41a2dc80)]] }, // ~20.356... + + // vec4 + { input: [[1, 1, 1, 1], [1, 0, 0, 0], 1], expected: [kNegativeOneBounds, [1], [1], [1]] }, + { input: [[1, -2, 3,-4], [-5, 6, -7, 8], 9], expected: [[hexToF32(0x410ae480), hexToF32(0x410af240)], // ~8.680... + [hexToF32(0xc18cf7c0), hexToF32(0xc18cef80)], // ~-17.620... + [hexToF32(0x41d46cc0), hexToF32(0x41d47660)], // ~26.553... + [hexToF32(0xc20dfa80), hexToF32(0xc20df500)]] }, // ~-35.494... + + // Test that dot going OOB bounds in the intermediate calculations propagates + { input: [[kValue.f32.positive.nearest_max, kValue.f32.positive.max, kValue.f32.negative.min], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.positive.nearest_max, kValue.f32.negative.min, kValue.f32.positive.max], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.positive.max, kValue.f32.positive.nearest_max, kValue.f32.negative.min], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.negative.min, kValue.f32.positive.nearest_max, kValue.f32.positive.max], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.positive.max, kValue.f32.negative.min, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] }, + { input: [[kValue.f32.negative.min, kValue.f32.positive.max, kValue.f32.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kAny, kAny, kAny] }, + ] + ) + .fn(t => { + const [i, s, r] = t.params.input; + const expected = toF32Vector(t.params.expected); + + const got = refractInterval(i, s, r); + t.expect( + objectEquals(expected, got), + `refractIntervals([${i}], [${s}], ${r}) returned [${got}]. Expected [${expected}]` + ); + }); +} + +interface ModfCase { + input: number; + fract: IntervalBounds; + whole: IntervalBounds; +} + +g.test('modfInterval') + .paramsSubcasesOnly<ModfCase>( + // prettier-ignore + [ + // Normals + { input: 0, fract: [0], whole: [0] }, + { input: 1, fract: [0], whole: [1] }, + { input: -1, fract: [0], whole: [-1] }, + { input: 0.5, fract: [0.5], whole: [0] }, + { input: -0.5, fract: [-0.5], whole: [0] }, + { input: 2.5, fract: [0.5], whole: [2] }, + { input: -2.5, fract: [-0.5], whole: [-2] }, + { input: 10.0, fract: [0], whole: [10] }, + { input: -10.0, fract: [0], whole: [-10] }, + + // Subnormals + { input: kValue.f32.subnormal.negative.min, fract: [kValue.f32.subnormal.negative.min, 0], whole: [0] }, + { input: kValue.f32.subnormal.negative.max, fract: [kValue.f32.subnormal.negative.max, 0], whole: [0] }, + { input: kValue.f32.subnormal.positive.min, fract: [0, kValue.f32.subnormal.positive.min], whole: [0] }, + { input: kValue.f32.subnormal.positive.max, fract: [0, kValue.f32.subnormal.positive.max], whole: [0] }, + + // Boundaries + { input: kValue.f32.negative.min, fract: [0], whole: [kValue.f32.negative.min] }, + { input: kValue.f32.negative.max, fract: [kValue.f32.negative.max], whole: [0] }, + { input: kValue.f32.positive.min, fract: [kValue.f32.positive.min], whole: [0] }, + { input: kValue.f32.positive.max, fract: [0], whole: [kValue.f32.positive.max] }, + ] + ) + .fn(t => { + const expected = { + fract: toF32Interval(t.params.fract), + whole: toF32Interval(t.params.whole), + }; + + const got = modfInterval(t.params.input); + t.expect( + objectEquals(expected, got), + `modfInterval([${t.params.input}) returned { fract: [${got.fract}], whole: [${got.whole}] }. Expected { fract: [${expected.fract}], whole: [${expected.whole}] }` + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts new file mode 100644 index 0000000000..5090fe3f9d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts @@ -0,0 +1,138 @@ +export const description = ` +Tests for getStackTrace. +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { extractImportantStackTrace } from '../common/internal/stack.js'; + +import { UnitTest } from './unit_test.js'; + +export const g = makeTestGroup(UnitTest); + +g.test('stacks') + .paramsSimple([ + { + case: 'node_fail', + _expectedLines: 3, + _stack: `Error: + at CaseRecorder.fail (/Users/kainino/src/cts/src/common/framework/logger.ts:99:30) + at RunCaseSpecific.exports.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/logger.spec.ts:80:7) + at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18) + at processTicksAndRejections (internal/process/task_queues.js:86:5)`, + }, + { + // MAINTENANCE_TODO: make sure this test case actually matches what happens on windows + case: 'node_fail_backslash', + _expectedLines: 3, + _stack: `Error: + at CaseRecorder.fail (C:\\Users\\kainino\\src\\cts\\src\\common\\framework\\logger.ts:99:30) + at RunCaseSpecific.exports.g.test.t [as fn] (C:\\Users\\kainino\\src\\cts\\src\\unittests\\logger.spec.ts:80:7) + at RunCaseSpecific.run (C:\\Users\\kainino\\src\\cts\\src\\common\\framework\\test_group.ts:121:18) + at processTicksAndRejections (internal\\process\\task_queues.js:86:5)`, + }, + { + case: 'node_fail_processTicksAndRejections', + _expectedLines: 5, + _stack: `Error: expectation had no effect: suite1:foo: + at Object.generateMinimalQueryList (/Users/kainino/src/cts/src/common/framework/generate_minimal_query_list.ts:72:24) + at testGenerateMinimalQueryList (/Users/kainino/src/cts/src/unittests/loading.spec.ts:289:25) + at processTicksAndRejections (internal/process/task_queues.js:93:5) + at RunCaseSpecific.fn (/Users/kainino/src/cts/src/unittests/loading.spec.ts:300:3) + at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:144:9) + at /Users/kainino/src/cts/src/common/runtime/cmdline.ts:62:25 + at async Promise.all (index 29) + at /Users/kainino/src/cts/src/common/runtime/cmdline.ts:78:5`, + }, + { + case: 'node_throw', + _expectedLines: 2, + _stack: `Error: hello + at RunCaseSpecific.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/test_group.spec.ts:51:11) + at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18) + at processTicksAndRejections (internal/process/task_queues.js:86:5)`, + }, + { + case: 'firefox_fail', + _expectedLines: 3, + _stack: `fail@http://localhost:8080/out/common/framework/logger.js:104:30 +expect@http://localhost:8080/out/common/framework/default_fixture.js:59:16 +@http://localhost:8080/out/unittests/util.spec.js:35:5 +run@http://localhost:8080/out/common/framework/test_group.js:119:18`, + }, + { + case: 'firefox_throw', + _expectedLines: 1, + _stack: `@http://localhost:8080/out/unittests/test_group.spec.js:48:11 +run@http://localhost:8080/out/common/framework/test_group.js:119:18`, + }, + { + case: 'safari_fail', + _expectedLines: 3, + _stack: `fail@http://localhost:8080/out/common/framework/logger.js:104:39 +expect@http://localhost:8080/out/common/framework/default_fixture.js:59:20 +http://localhost:8080/out/unittests/util.spec.js:35:11 +http://localhost:8080/out/common/framework/test_group.js:119:20 +asyncFunctionResume@[native code] +[native code] +promiseReactionJob@[native code]`, + }, + { + case: 'safari_throw', + _expectedLines: 1, + _stack: `http://localhost:8080/out/unittests/test_group.spec.js:48:20 +http://localhost:8080/out/common/framework/test_group.js:119:20 +asyncFunctionResume@[native code] +[native code] +promiseReactionJob@[native code]`, + }, + { + case: 'chrome_fail', + _expectedLines: 4, + _stack: `Error + at CaseRecorder.fail (http://localhost:8080/out/common/framework/logger.js:104:30) + at DefaultFixture.expect (http://localhost:8080/out/common/framework/default_fixture.js:59:16) + at RunCaseSpecific.fn (http://localhost:8080/out/unittests/util.spec.js:35:5) + at RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:18) + at async runCase (http://localhost:8080/out/common/runtime/standalone.js:37:17) + at async http://localhost:8080/out/common/runtime/standalone.js:102:7`, + }, + { + case: 'chrome_throw', + _expectedLines: 6, + _stack: `Error: hello + at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11) + at RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:18)" + at async Promise.all (index 0) + at async TestGroupTest.run (http://localhost:8080/out/unittests/test_group_test.js:6:5) + at async RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:53:15) + at async RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:7) + at async runCase (http://localhost:8080/out/common/runtime/standalone.js:37:17) + at async http://localhost:8080/out/common/runtime/standalone.js:102:7`, + }, + { + case: 'multiple_lines', + _expectedLines: 8, + _stack: `Error: hello + at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11) + at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11) + at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11) + at RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:18)" + at async Promise.all (index 0) + at async TestGroupTest.run (http://localhost:8080/out/unittests/test_group_test.js:6:5) + at async RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:53:15) + at async RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:7) + at async runCase (http://localhost:8080/out/common/runtime/standalone.js:37:17) + at async http://localhost:8080/out/common/runtime/standalone.js:102:7`, + }, + ]) + .fn(t => { + const ex = new Error(); + ex.stack = t.params._stack; + t.expect(ex.stack === t.params._stack); + const stringified = extractImportantStackTrace(ex); + const parts = stringified.split('\n'); + + t.expect(parts.length === t.params._expectedLines); + const last = parts[parts.length - 1]; + t.expect(last.indexOf('/unittests/') !== -1 || last.indexOf('\\unittests\\') !== -1); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/listing.ts b/dom/webgpu/tests/cts/checkout/src/unittests/listing.ts new file mode 100644 index 0000000000..823639c692 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/listing.ts @@ -0,0 +1,5 @@ +/* eslint-disable import/no-restricted-paths */ +import { TestSuiteListing } from '../common/internal/test_suite_listing.js'; +import { makeListing } from '../common/tools/crawl.js'; + +export const listing: Promise<TestSuiteListing> = makeListing(__filename); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts new file mode 100644 index 0000000000..29d0b7442c --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts @@ -0,0 +1,931 @@ +export const description = ` +Tests for queries/filtering, loading, and running. +`; + +import { Fixture } from '../common/framework/fixture.js'; +import { makeTestGroup } from '../common/framework/test_group.js'; +import { TestFileLoader, SpecFile } from '../common/internal/file_loader.js'; +import { Logger } from '../common/internal/logging/logger.js'; +import { Status } from '../common/internal/logging/result.js'; +import { parseQuery } from '../common/internal/query/parseQuery.js'; +import { + TestQuery, + TestQuerySingleCase, + TestQueryMultiCase, + TestQueryMultiTest, + TestQueryMultiFile, + TestQueryWithExpectation, +} from '../common/internal/query/query.js'; +import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js'; +import { TestSuiteListing, TestSuiteListingEntry } from '../common/internal/test_suite_listing.js'; +import { ExpandThroughLevel, TestTreeLeaf } from '../common/internal/tree.js'; +import { assert, objectEquals } from '../common/util/util.js'; + +import { UnitTest } from './unit_test.js'; + +const listingData: { [k: string]: TestSuiteListingEntry[] } = { + suite1: [ + { file: [], readme: 'desc 1a' }, + { file: ['foo'] }, + { file: ['bar'], readme: 'desc 1h' }, + { file: ['bar', 'biz'] }, + { file: ['bar', 'buzz', 'buzz'] }, + { file: ['baz'] }, + { file: ['empty'], readme: 'desc 1z' }, // directory with no files + ], + suite2: [{ file: [], readme: 'desc 2a' }, { file: ['foof'] }], +}; + +const specsData: { [k: string]: SpecFile } = { + 'suite1/foo.spec.js': { + description: 'desc 1b', + g: (() => { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('hello').fn(() => {}); + g.test('bonjour').fn(() => {}); + g.test('hola') + .desc('TODO TODO') + .fn(() => {}); + return g; + })(), + }, + 'suite1/bar/biz.spec.js': { + description: 'desc 1f TODO TODO', + g: makeTestGroupForUnitTesting(UnitTest), // file with no tests + }, + 'suite1/bar/buzz/buzz.spec.js': { + description: 'desc 1d TODO', + g: (() => { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('zap').fn(() => {}); + return g; + })(), + }, + 'suite1/baz.spec.js': { + description: 'desc 1e', + g: (() => { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('wye') + .paramsSimple([{}, { x: 1 }]) + .fn(() => {}); + g.test('zed') + .paramsSimple([ + { a: 1, b: 2, _c: 0 }, + { b: 3, a: 1, _c: 0 }, + ]) + .fn(() => {}); + return g; + })(), + }, + 'suite2/foof.spec.js': { + description: 'desc 2b', + g: (() => { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('blah').fn(t => { + t.debug('OK'); + }); + g.test('bleh') + .paramsSimple([{ a: 1 }]) + .fn(t => { + t.debug('OK'); + t.debug('OK'); + }); + g.test('bluh,a').fn(t => { + t.fail('goodbye'); + }); + return g; + })(), + }, +}; + +class FakeTestFileLoader extends TestFileLoader { + async listing(suite: string): Promise<TestSuiteListing> { + return listingData[suite]; + } + + async import(path: string): Promise<SpecFile> { + assert(path in specsData, '[test] mock file ' + path + ' does not exist'); + return specsData[path]; + } +} + +class LoadingTest extends UnitTest { + loader: FakeTestFileLoader = new FakeTestFileLoader(); + events: (string | null)[] = []; + private isListenersAdded = false; + + collectEvents(): void { + this.events = []; + if (!this.isListenersAdded) { + this.isListenersAdded = true; + this.loader.addEventListener('import', ev => this.events.push(ev.data.url)); + this.loader.addEventListener('finish', ev => this.events.push(null)); + } + } + + async load(query: string): Promise<TestTreeLeaf[]> { + return Array.from(await this.loader.loadCases(parseQuery(query))); + } + + async loadNames(query: string): Promise<string[]> { + return (await this.load(query)).map(c => c.query.toString()); + } +} + +export const g = makeTestGroup(LoadingTest); + +g.test('suite').fn(async t => { + t.shouldReject('Error', t.load('suite1')); + t.shouldReject('Error', t.load('suite1:')); +}); + +g.test('group').fn(async t => { + t.collectEvents(); + t.expect((await t.load('suite1:*')).length === 8); + t.expect( + objectEquals(t.events, [ + 'suite1/foo.spec.js', + 'suite1/bar/biz.spec.js', + 'suite1/bar/buzz/buzz.spec.js', + 'suite1/baz.spec.js', + null, + ]) + ); + + t.collectEvents(); + t.expect((await t.load('suite1:foo,*')).length === 3); // x:foo,* matches x:foo: + t.expect(objectEquals(t.events, ['suite1/foo.spec.js', null])); + + t.collectEvents(); + t.expect((await t.load('suite1:bar,*')).length === 1); + t.expect( + objectEquals(t.events, ['suite1/bar/biz.spec.js', 'suite1/bar/buzz/buzz.spec.js', null]) + ); + + t.collectEvents(); + t.expect((await t.load('suite1:bar,buzz,buzz,*')).length === 1); + t.expect(objectEquals(t.events, ['suite1/bar/buzz/buzz.spec.js', null])); + + t.shouldReject('Error', t.load('suite1:f*')); + + { + const s = new TestQueryMultiFile('suite1', ['bar', 'buzz']).toString(); + t.collectEvents(); + t.expect((await t.load(s)).length === 1); + t.expect(objectEquals(t.events, ['suite1/bar/buzz/buzz.spec.js', null])); + } +}); + +g.test('test').fn(async t => { + t.shouldReject('Error', t.load('suite1::')); + t.shouldReject('Error', t.load('suite1:bar:')); + t.shouldReject('Error', t.load('suite1:bar,:')); + + t.shouldReject('Error', t.load('suite1::*')); + t.shouldReject('Error', t.load('suite1:bar,:*')); + t.shouldReject('Error', t.load('suite1:bar:*')); + + t.expect((await t.load('suite1:foo:*')).length === 3); + t.expect((await t.load('suite1:bar,buzz,buzz:*')).length === 1); + t.expect((await t.load('suite1:baz:*')).length === 4); + + t.expect((await t.load('suite2:foof:bluh,*')).length === 1); + t.expect((await t.load('suite2:foof:bluh,a,*')).length === 1); + + { + const s = new TestQueryMultiTest('suite2', ['foof'], ['bluh']).toString(); + t.expect((await t.load(s)).length === 1); + } +}); + +g.test('case').fn(async t => { + t.shouldReject('Error', t.load('suite1:foo::')); + t.shouldReject('Error', t.load('suite1:bar:zed,:')); + + t.shouldReject('Error', t.load('suite1:foo:h*')); + + t.shouldReject('Error', t.load('suite1:foo::*')); + t.shouldReject('Error', t.load('suite1:baz::*')); + t.shouldReject('Error', t.load('suite1:baz:zed,:*')); + + t.shouldReject('Error', t.load('suite1:baz:zed:')); + t.shouldReject('Error', t.load('suite1:baz:zed:a=1;b=2*')); + t.shouldReject('Error', t.load('suite1:baz:zed:a=1;b=2;')); + t.shouldReject('SyntaxError', t.load('suite1:baz:zed:a=1;b=2,')); // tries to parse '2,' as JSON + t.shouldReject('Error', t.load('suite1:baz:zed:a=1,b=2')); // '=' not allowed in value '1,b=2' + t.shouldReject('Error', t.load('suite1:baz:zed:b=2*')); + t.shouldReject('Error', t.load('suite1:baz:zed:b=2;a=1;_c=0')); + t.shouldReject('Error', t.load('suite1:baz:zed:a=1,*')); + + t.expect((await t.load('suite1:baz:zed:*')).length === 2); + t.expect((await t.load('suite1:baz:zed:a=1;*')).length === 2); + t.expect((await t.load('suite1:baz:zed:a=1;b=2')).length === 1); + t.expect((await t.load('suite1:baz:zed:a=1;b=2;*')).length === 1); + t.expect((await t.load('suite1:baz:zed:b=2;*')).length === 1); + t.expect((await t.load('suite1:baz:zed:b=2;a=1')).length === 1); + t.expect((await t.load('suite1:baz:zed:b=2;a=1;*')).length === 1); + t.expect((await t.load('suite1:baz:zed:b=3;a=1')).length === 1); + t.expect((await t.load('suite1:baz:zed:a=1;b=3')).length === 1); + t.expect((await t.load('suite1:foo:hello:')).length === 1); + + { + const s = new TestQueryMultiCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }).toString(); + t.expect((await t.load(s)).length === 1); + } + { + const s = new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }).toString(); + t.expect((await t.load(s)).length === 1); + } +}); + +async function runTestcase( + t: Fixture, + log: Logger, + testcases: TestTreeLeaf[], + i: number, + query: TestQuery, + expectations: TestQueryWithExpectation[], + status: Status, + logs: (s: string[]) => boolean +) { + t.expect(objectEquals(testcases[i].query, query)); + const name = testcases[i].query.toString(); + const [rec, res] = log.record(name); + await testcases[i].run(rec, expectations); + + t.expect(log.results.get(name) === res); + t.expect(res.status === status); + t.expect(res.timems >= 0); + assert(res.logs !== undefined); // only undefined while pending + t.expect(logs(res.logs.map(l => JSON.stringify(l)))); +} + +g.test('end2end').fn(async t => { + const l = await t.load('suite2:foof:*'); + assert(l.length === 3, 'listing length'); + + const log = new Logger({ overrideDebugMode: true }); + + await runTestcase( + t, + log, + l, + 0, + new TestQuerySingleCase('suite2', ['foof'], ['blah'], {}), + [], + 'pass', + logs => objectEquals(logs, ['"DEBUG: OK"']) + ); + await runTestcase( + t, + log, + l, + 1, + new TestQuerySingleCase('suite2', ['foof'], ['bleh'], { a: 1 }), + [], + 'pass', + logs => objectEquals(logs, ['"DEBUG: OK"', '"DEBUG: OK"']) + ); + await runTestcase( + t, + log, + l, + 2, + new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}), + [], + 'fail', + logs => + logs.length === 1 && + logs[0].startsWith('"EXPECTATION FAILED: goodbye\\n') && + logs[0].indexOf('loaders_and_trees.spec.') !== -1 + ); +}); + +g.test('expectations,single_case').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const zedCases = await t.load('suite1:baz:zed:*'); + + // Single-case. Covers one case. + const zedExpectationsSkipA1B2 = [ + { + query: new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + zedCases, + 0, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }), + zedExpectationsSkipA1B2, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); + + await runTestcase( + t, + log, + zedCases, + 1, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }), + zedExpectationsSkipA1B2, + 'pass', + logs => logs.length === 0 + ); +}); + +g.test('expectations,single_case,none').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const zedCases = await t.load('suite1:baz:zed:*'); + // Single-case. Doesn't cover any cases. + const zedExpectationsSkipA1B0 = [ + { + query: new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 0 }), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + zedCases, + 0, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }), + zedExpectationsSkipA1B0, + 'pass', + logs => logs.length === 0 + ); + + await runTestcase( + t, + log, + zedCases, + 1, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }), + zedExpectationsSkipA1B0, + 'pass', + logs => logs.length === 0 + ); +}); + +g.test('expectations,multi_case').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const zedCases = await t.load('suite1:baz:zed:*'); + // Multi-case, not all cases covered. + const zedExpectationsSkipB3 = [ + { + query: new TestQueryMultiCase('suite1', ['baz'], ['zed'], { b: 3 }), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + zedCases, + 0, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }), + zedExpectationsSkipB3, + 'pass', + logs => logs.length === 0 + ); + + await runTestcase( + t, + log, + zedCases, + 1, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }), + zedExpectationsSkipB3, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); +}); + +g.test('expectations,multi_case_all').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const zedCases = await t.load('suite1:baz:zed:*'); + // Multi-case, all cases covered. + const zedExpectationsSkipA1 = [ + { + query: new TestQueryMultiCase('suite1', ['baz'], ['zed'], { a: 1 }), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + zedCases, + 0, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }), + zedExpectationsSkipA1, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); + + await runTestcase( + t, + log, + zedCases, + 1, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }), + zedExpectationsSkipA1, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); +}); + +g.test('expectations,multi_case_none').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const zedCases = await t.load('suite1:baz:zed:*'); + // Multi-case, no params, all cases covered. + const zedExpectationsSkipZed = [ + { + query: new TestQueryMultiCase('suite1', ['baz'], ['zed'], {}), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + zedCases, + 0, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }), + zedExpectationsSkipZed, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); + + await runTestcase( + t, + log, + zedCases, + 1, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }), + zedExpectationsSkipZed, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); +}); + +g.test('expectations,multi_test').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const suite1Cases = await t.load('suite1:*'); + + // Multi-test, all cases covered. + const expectationsSkipAllInBaz = [ + { + query: new TestQueryMultiTest('suite1', ['baz'], []), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + suite1Cases, + 4, + new TestQuerySingleCase('suite1', ['baz'], ['wye'], {}), + expectationsSkipAllInBaz, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); + + await runTestcase( + t, + log, + suite1Cases, + 6, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }), + expectationsSkipAllInBaz, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); +}); + +g.test('expectations,multi_test,none').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const suite1Cases = await t.load('suite1:*'); + + // Multi-test, no cases covered. + const expectationsSkipAllInFoo = [ + { + query: new TestQueryMultiTest('suite1', ['foo'], []), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + suite1Cases, + 4, + new TestQuerySingleCase('suite1', ['baz'], ['wye'], {}), + expectationsSkipAllInFoo, + 'pass', + logs => logs.length === 0 + ); + + await runTestcase( + t, + log, + suite1Cases, + 6, + new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }), + expectationsSkipAllInFoo, + 'pass', + logs => logs.length === 0 + ); +}); + +g.test('expectations,multi_file').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const suite1Cases = await t.load('suite1:*'); + + // Multi-file + const expectationsSkipAll = [ + { + query: new TestQueryMultiFile('suite1', []), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + suite1Cases, + 0, + new TestQuerySingleCase('suite1', ['foo'], ['hello'], {}), + expectationsSkipAll, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); + + await runTestcase( + t, + log, + suite1Cases, + 3, + new TestQuerySingleCase('suite1', ['bar', 'buzz', 'buzz'], ['zap'], {}), + expectationsSkipAll, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); +}); + +g.test('expectations,catches_failure').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const suite2Cases = await t.load('suite2:*'); + + // Catches failure + const expectedFailures = [ + { + query: new TestQueryMultiCase('suite2', ['foof'], ['bluh', 'a'], {}), + expectation: 'fail' as const, + }, + ]; + + await runTestcase( + t, + log, + suite2Cases, + 0, + new TestQuerySingleCase('suite2', ['foof'], ['blah'], {}), + expectedFailures, + 'pass', + logs => objectEquals(logs, ['"DEBUG: OK"']) + ); + + // Status is passed, but failure is logged. + await runTestcase( + t, + log, + suite2Cases, + 2, + new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}), + expectedFailures, + 'pass', + logs => logs.length === 1 && logs[0].startsWith('"EXPECTATION FAILED: goodbye\\n') + ); +}); + +g.test('expectations,skip_dominates_failure').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const suite2Cases = await t.load('suite2:*'); + + const expectedFailures = [ + { + query: new TestQueryMultiCase('suite2', ['foof'], ['bluh', 'a'], {}), + expectation: 'fail' as const, + }, + { + query: new TestQueryMultiCase('suite2', ['foof'], ['bluh', 'a'], {}), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + suite2Cases, + 2, + new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}), + expectedFailures, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); +}); + +g.test('expectations,skip_inside_failure').fn(async t => { + const log = new Logger({ overrideDebugMode: true }); + const suite2Cases = await t.load('suite2:*'); + + const expectedFailures = [ + { + query: new TestQueryMultiFile('suite2', []), + expectation: 'fail' as const, + }, + { + query: new TestQueryMultiCase('suite2', ['foof'], ['blah'], {}), + expectation: 'skip' as const, + }, + ]; + + await runTestcase( + t, + log, + suite2Cases, + 0, + new TestQuerySingleCase('suite2', ['foof'], ['blah'], {}), + expectedFailures, + 'skip', + logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"') + ); + + await runTestcase( + t, + log, + suite2Cases, + 2, + new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}), + expectedFailures, + 'pass', + logs => logs.length === 1 && logs[0].startsWith('"EXPECTATION FAILED: goodbye\\n') + ); +}); + +async function testIterateCollapsed( + t: LoadingTest, + alwaysExpandThroughLevel: ExpandThroughLevel, + expectations: string[], + expectedResult: 'throws' | string[] | [string, number | undefined][], + includeEmptySubtrees = false +) { + t.debug(`expandThrough=${alwaysExpandThroughLevel} expectations=${expectations}`); + const treePromise = t.loader.loadTree(new TestQueryMultiFile('suite1', []), expectations); + if (expectedResult === 'throws') { + t.shouldReject('Error', treePromise, 'loadTree should have thrown Error'); + return; + } + const tree = await treePromise; + const actualIter = tree.iterateCollapsedNodes({ + includeEmptySubtrees, + alwaysExpandThroughLevel, + }); + const testingTODOs = expectedResult.length > 0 && expectedResult[0] instanceof Array; + const actual = Array.from(actualIter, ({ query, subtreeCounts }) => + testingTODOs ? [query.toString(), subtreeCounts?.nodesWithTODO] : query.toString() + ); + if (!objectEquals(actual, expectedResult)) { + t.fail( + `iterateCollapsed failed: + got ${JSON.stringify(actual)} + exp ${JSON.stringify(expectedResult)} +${tree.toString()}` + ); + } +} + +g.test('print').fn(async t => { + const tree = await t.loader.loadTree(new TestQueryMultiFile('suite1', [])); + tree.toString(); +}); + +g.test('iterateCollapsed').fn(async t => { + await testIterateCollapsed( + t, + 1, + [], + [ + ['suite1:foo:*', 1], // to-do propagated up from foo:hola + ['suite1:bar,buzz,buzz:*', 1], // to-do in file description + ['suite1:baz:*', 0], + ] + ); + await testIterateCollapsed( + t, + 2, + [], + [ + ['suite1:foo:hello:*', 0], + ['suite1:foo:bonjour:*', 0], + ['suite1:foo:hola:*', 1], // to-do in test description + ['suite1:bar,buzz,buzz:zap:*', 0], + ['suite1:baz:wye:*', 0], + ['suite1:baz:zed:*', 0], + ] + ); + await testIterateCollapsed( + t, + 3, + [], + [ + ['suite1:foo:hello:', undefined], + ['suite1:foo:bonjour:', undefined], + ['suite1:foo:hola:', undefined], + ['suite1:bar,buzz,buzz:zap:', undefined], + ['suite1:baz:wye:', undefined], + ['suite1:baz:wye:x=1', undefined], + ['suite1:baz:zed:a=1;b=2', undefined], + ['suite1:baz:zed:b=3;a=1', undefined], + ] + ); + + // Expectations lists that have no effect + await testIterateCollapsed( + t, + 1, + ['suite1:foo:*'], + ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:*'] + ); + await testIterateCollapsed( + t, + 1, + ['suite1:bar,buzz,buzz:*'], + ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:*'] + ); + await testIterateCollapsed( + t, + 2, + ['suite1:baz:wye:*'], + [ + 'suite1:foo:hello:*', + 'suite1:foo:bonjour:*', + 'suite1:foo:hola:*', + 'suite1:bar,buzz,buzz:zap:*', + 'suite1:baz:wye:*', + 'suite1:baz:zed:*', + ] + ); + // Test with includeEmptySubtrees=true + await testIterateCollapsed( + t, + 1, + [], + [ + 'suite1:foo:*', + 'suite1:bar,biz:*', + 'suite1:bar,buzz,buzz:*', + 'suite1:baz:*', + 'suite1:empty,*', + ], + true + ); + await testIterateCollapsed( + t, + 2, + [], + [ + 'suite1:foo:hello:*', + 'suite1:foo:bonjour:*', + 'suite1:foo:hola:*', + 'suite1:bar,biz:*', + 'suite1:bar,buzz,buzz:zap:*', + 'suite1:baz:wye:*', + 'suite1:baz:zed:*', + 'suite1:empty,*', + ], + true + ); + + // Expectations lists that have some effect + await testIterateCollapsed( + t, + 1, + ['suite1:baz:wye:*'], + ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:wye:*', 'suite1:baz:zed,*'] + ); + await testIterateCollapsed( + t, + 1, + ['suite1:baz:zed:*'], + ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:wye,*', 'suite1:baz:zed:*'] + ); + await testIterateCollapsed( + t, + 1, + ['suite1:baz:wye:*', 'suite1:baz:zed:*'], + ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:wye:*', 'suite1:baz:zed:*'] + ); + await testIterateCollapsed( + t, + 1, + ['suite1:baz:wye:'], + [ + 'suite1:foo:*', + 'suite1:bar,buzz,buzz:*', + 'suite1:baz:wye:', + 'suite1:baz:wye:x=1;*', + 'suite1:baz:zed,*', + ] + ); + await testIterateCollapsed( + t, + 1, + ['suite1:baz:wye:x=1'], + [ + 'suite1:foo:*', + 'suite1:bar,buzz,buzz:*', + 'suite1:baz:wye:', + 'suite1:baz:wye:x=1', + 'suite1:baz:zed,*', + ] + ); + await testIterateCollapsed( + t, + 1, + ['suite1:foo:*', 'suite1:baz:wye:'], + [ + 'suite1:foo:*', + 'suite1:bar,buzz,buzz:*', + 'suite1:baz:wye:', + 'suite1:baz:wye:x=1;*', + 'suite1:baz:zed,*', + ] + ); + await testIterateCollapsed( + t, + 2, + ['suite1:baz:wye:'], + [ + 'suite1:foo:hello:*', + 'suite1:foo:bonjour:*', + 'suite1:foo:hola:*', + 'suite1:bar,buzz,buzz:zap:*', + 'suite1:baz:wye:', + 'suite1:baz:wye:x=1;*', + 'suite1:baz:zed:*', + ] + ); + await testIterateCollapsed( + t, + 2, + ['suite1:baz:wye:x=1'], + [ + 'suite1:foo:hello:*', + 'suite1:foo:bonjour:*', + 'suite1:foo:hola:*', + 'suite1:bar,buzz,buzz:zap:*', + 'suite1:baz:wye:', + 'suite1:baz:wye:x=1', + 'suite1:baz:zed:*', + ] + ); + await testIterateCollapsed( + t, + 2, + ['suite1:foo:hello:*', 'suite1:baz:wye:'], + [ + 'suite1:foo:hello:*', + 'suite1:foo:bonjour:*', + 'suite1:foo:hola:*', + 'suite1:bar,buzz,buzz:zap:*', + 'suite1:baz:wye:', + 'suite1:baz:wye:x=1;*', + 'suite1:baz:zed:*', + ] + ); + + // Invalid expectation queries + await testIterateCollapsed(t, 1, ['*'], 'throws'); + await testIterateCollapsed(t, 1, ['garbage'], 'throws'); + await testIterateCollapsed(t, 1, ['garbage*'], 'throws'); + await testIterateCollapsed(t, 1, ['suite1*'], 'throws'); + await testIterateCollapsed(t, 1, ['suite1:foo*'], 'throws'); + await testIterateCollapsed(t, 1, ['suite1:foo:he*'], 'throws'); + + // Valid expectation queries but they don't match anything + await testIterateCollapsed(t, 1, ['garbage:*'], 'throws'); + await testIterateCollapsed(t, 1, ['suite1:doesntexist:*'], 'throws'); + await testIterateCollapsed(t, 1, ['suite2:foo:*'], 'throws'); + // Can't expand subqueries bigger than one file. + await testIterateCollapsed(t, 1, ['suite1:*'], 'throws'); + await testIterateCollapsed(t, 1, ['suite1:bar,*'], 'throws'); + await testIterateCollapsed(t, 1, ['suite1:*'], 'throws'); + await testIterateCollapsed(t, 1, ['suite1:bar:hello,*'], 'throws'); + await testIterateCollapsed(t, 1, ['suite1:baz,*'], 'throws'); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts new file mode 100644 index 0000000000..18aa0a02fe --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts @@ -0,0 +1,147 @@ +export const description = ` +Unit tests for namespaced logging system. + +Also serves as a larger test of async test functions, and of the logging system. +`; + +import { SkipTestCase } from '../common/framework/fixture.js'; +import { makeTestGroup } from '../common/framework/test_group.js'; +import { Logger } from '../common/internal/logging/logger.js'; +import { assert } from '../common/util/util.js'; + +import { UnitTest } from './unit_test.js'; + +export const g = makeTestGroup(UnitTest); + +g.test('construct').fn(t => { + const mylog = new Logger({ overrideDebugMode: true }); + const [, res1] = mylog.record('one'); + const [, res2] = mylog.record('two'); + + t.expect(mylog.results.get('one') === res1); + t.expect(mylog.results.get('two') === res2); + t.expect(res1.logs === undefined); + t.expect(res1.status === 'running'); + t.expect(res1.timems < 0); + t.expect(res2.logs === undefined); + t.expect(res2.status === 'running'); + t.expect(res2.timems < 0); +}); + +g.test('empty').fn(t => { + const mylog = new Logger({ overrideDebugMode: true }); + const [rec, res] = mylog.record('one'); + + rec.start(); + t.expect(res.status === 'running'); + rec.finish(); + + t.expect(res.status === 'pass'); + t.expect(res.timems >= 0); +}); + +g.test('pass').fn(t => { + const mylog = new Logger({ overrideDebugMode: true }); + const [rec, res] = mylog.record('one'); + + rec.start(); + rec.debug(new Error('hello')); + t.expect(res.status === 'running'); + rec.finish(); + + t.expect(res.status === 'pass'); + t.expect(res.timems >= 0); +}); + +g.test('skip').fn(t => { + const mylog = new Logger({ overrideDebugMode: true }); + const [rec, res] = mylog.record('one'); + + rec.start(); + rec.skipped(new SkipTestCase()); + rec.debug(new Error('hello')); + rec.finish(); + + t.expect(res.status === 'skip'); + t.expect(res.timems >= 0); +}); + +g.test('warn').fn(t => { + const mylog = new Logger({ overrideDebugMode: true }); + const [rec, res] = mylog.record('one'); + + rec.start(); + rec.warn(new Error('hello')); + rec.skipped(new SkipTestCase()); + rec.finish(); + + t.expect(res.status === 'warn'); + t.expect(res.timems >= 0); +}); + +g.test('fail,expectationFailed').fn(t => { + const mylog = new Logger({ overrideDebugMode: true }); + const [rec, res] = mylog.record('one'); + + rec.start(); + rec.expectationFailed(new Error('bye')); + rec.warn(new Error()); + rec.skipped(new SkipTestCase()); + rec.finish(); + + t.expect(res.status === 'fail'); + t.expect(res.timems >= 0); +}); + +g.test('fail,validationFailed').fn(t => { + const mylog = new Logger({ overrideDebugMode: true }); + const [rec, res] = mylog.record('one'); + + rec.start(); + rec.validationFailed(new Error('bye')); + rec.warn(new Error()); + rec.skipped(new SkipTestCase()); + rec.finish(); + + t.expect(res.status === 'fail'); + t.expect(res.timems >= 0); +}); + +g.test('fail,threw').fn(t => { + const mylog = new Logger({ overrideDebugMode: true }); + const [rec, res] = mylog.record('one'); + + rec.start(); + rec.threw(new Error('bye')); + rec.warn(new Error()); + rec.skipped(new SkipTestCase()); + rec.finish(); + + t.expect(res.status === 'fail'); + t.expect(res.timems >= 0); +}); + +g.test('debug') + .paramsSimple([ + { debug: true, _logsCount: 5 }, // + { debug: false, _logsCount: 3 }, + ]) + .fn(t => { + const { debug, _logsCount } = t.params; + + const mylog = new Logger({ overrideDebugMode: debug }); + const [rec, res] = mylog.record('one'); + + rec.start(); + rec.debug(new Error('hello')); + rec.expectationFailed(new Error('bye')); + rec.warn(new Error()); + rec.skipped(new SkipTestCase()); + rec.debug(new Error('foo')); + rec.finish(); + + t.expect(res.status === 'fail'); + t.expect(res.timems >= 0); + assert(res.logs !== undefined); + t.expect(res.logs.length === _logsCount); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts new file mode 100644 index 0000000000..4f3181855b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts @@ -0,0 +1,1021 @@ +export const description = ` +Util math unit tests. +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { objectEquals } from '../common/util/util.js'; +import { kBit, kValue } from '../webgpu/util/constants.js'; +import { + f32, + f32Bits, + float16ToUint16, + float32ToUint32, + Scalar, + uint16ToFloat16, + uint32ToFloat32, +} from '../webgpu/util/conversion.js'; +import { + biasedRange, + calculatePermutations, + cartesianProduct, + correctlyRoundedF32, + FlushMode, + fullF16Range, + fullF32Range, + fullI32Range, + hexToF32, + hexToF64, + lerp, + linearRange, + nextAfterF32, + oneULP, +} from '../webgpu/util/math.js'; + +import { UnitTest } from './unit_test.js'; + +export const g = makeTestGroup(UnitTest); + +/** + * Utility wrapper around oneULP to test if a value is within 1 ULP(x) + * + * @param got number to test + * @param expected number to be within 1 ULP of + * @param mode should oneULP FTZ + * @returns if got is within 1 ULP of expected + */ +function withinOneULP(got: number, expected: number, mode: FlushMode): boolean { + const ulp = oneULP(expected, mode); + return got >= expected - ulp && got <= expected + ulp; +} + +/** + * @returns true if arrays are equal within 1ULP, doing element-wise comparison + * as needed, and considering NaNs to be equal. + * + * Depends on the correctness of oneULP, which is tested in this file. + ** + * @param got array of numbers to compare for equality + * @param expect array of numbers to compare against + * @param mode should different subnormals be considered the same, i.e. should + * FTZ occur during comparison + **/ +function compareArrayOfNumbers( + got: Array<number>, + expect: Array<number>, + mode: FlushMode = 'flush' +): boolean { + return ( + got.length === expect.length && + got.every((value, index) => { + const expected = expect[index]; + return (Number.isNaN(value) && Number.isNaN(expected)) || withinOneULP(value, expected, mode); + }) + ); +} + +interface nextAfterCase { + val: number; + dir: boolean; + result: Scalar; +} + +g.test('nextAfterFlushToZero') + .paramsSubcasesOnly<nextAfterCase>( + // prettier-ignore + [ + // Edge Cases + { val: Number.NaN, dir: true, result: f32Bits(0x7fffffff) }, + { val: Number.NaN, dir: false, result: f32Bits(0x7fffffff) }, + { val: Number.POSITIVE_INFINITY, dir: true, result: f32Bits(kBit.f32.infinity.positive) }, + { val: Number.POSITIVE_INFINITY, dir: false, result: f32Bits(kBit.f32.infinity.positive) }, + { val: Number.NEGATIVE_INFINITY, dir: true, result: f32Bits(kBit.f32.infinity.negative) }, + { val: Number.NEGATIVE_INFINITY, dir: false, result: f32Bits(kBit.f32.infinity.negative) }, + + // Zeroes + { val: +0, dir: true, result: f32Bits(kBit.f32.positive.min) }, + { val: +0, dir: false, result: f32Bits(kBit.f32.negative.max) }, + { val: -0, dir: true, result: f32Bits(kBit.f32.positive.min) }, + { val: -0, dir: false, result: f32Bits(kBit.f32.negative.max) }, + + // Subnormals + { val: hexToF32(kBit.f32.subnormal.positive.min), dir: true, result: f32Bits(kBit.f32.positive.min) }, + { val: hexToF32(kBit.f32.subnormal.positive.min), dir: false, result: f32Bits(kBit.f32.negative.max) }, + { val: hexToF32(kBit.f32.subnormal.positive.max), dir: true, result: f32Bits(kBit.f32.positive.min) }, + { val: hexToF32(kBit.f32.subnormal.positive.max), dir: false, result: f32Bits(kBit.f32.negative.max) }, + { val: hexToF32(kBit.f32.subnormal.negative.min), dir: true, result: f32Bits(kBit.f32.positive.min) }, + { val: hexToF32(kBit.f32.subnormal.negative.min), dir: false, result: f32Bits(kBit.f32.negative.max) }, + { val: hexToF32(kBit.f32.subnormal.negative.max), dir: true, result: f32Bits(kBit.f32.positive.min) }, + { val: hexToF32(kBit.f32.subnormal.negative.max), dir: false, result: f32Bits(kBit.f32.negative.max) }, + + // Normals + { val: hexToF32(kBit.f32.positive.max), dir: true, result: f32Bits(kBit.f32.infinity.positive) }, + { val: hexToF32(kBit.f32.positive.max), dir: false, result: f32Bits(0x7f7ffffe) }, + { val: hexToF32(kBit.f32.positive.min), dir: true, result: f32Bits(0x00800001) }, + { val: hexToF32(kBit.f32.positive.min), dir: false, result: f32(0) }, + { val: hexToF32(kBit.f32.negative.max), dir: true, result: f32(0) }, + { val: hexToF32(kBit.f32.negative.max), dir: false, result: f32Bits(0x80800001) }, + { val: hexToF32(kBit.f32.negative.min), dir: true, result: f32Bits(0xff7ffffe) }, + { val: hexToF32(kBit.f32.negative.min), dir: false, result: f32Bits(kBit.f32.infinity.negative) }, + { val: hexToF32(0x03800000), dir: true, result: f32Bits(0x03800001) }, + { val: hexToF32(0x03800000), dir: false, result: f32Bits(0x037fffff) }, + { val: hexToF32(0x83800000), dir: true, result: f32Bits(0x837fffff) }, + { val: hexToF32(0x83800000), dir: false, result: f32Bits(0x83800001) }, + + // Not precisely expressible as float32 + { val: 0.001, dir: true, result: f32Bits(0x3a83126f) }, // positive normal + { val: 0.001, dir: false, result: f32Bits(0x3a83126e) }, // positive normal + { val: -0.001, dir: true, result: f32Bits(0xba83126e) }, // negative normal + { val: -0.001, dir: false, result: f32Bits(0xba83126f) }, // negative normal + { val: 2.82E-40, dir: true, result: f32Bits(kBit.f32.positive.min) }, // positive subnormal + { val: 2.82E-40, dir: false, result: f32Bits(kBit.f32.negative.max) }, // positive subnormal + { val: -2.82E-40, dir: true, result: f32Bits(kBit.f32.positive.min) }, // negative subnormal + { val: -2.82E-40, dir: false, result: f32Bits(kBit.f32.negative.max) }, // negative subnormal + ] + ) + .fn(t => { + const val = t.params.val; + const dir = t.params.dir; + const expect = t.params.result; + const expect_type = typeof expect; + const got = nextAfterF32(val, dir, 'flush'); + const got_type = typeof got; + t.expect( + got.value === expect.value || (Number.isNaN(got.value) && Number.isNaN(expect.value)), + `nextAfter(${val}, ${dir}, true) returned ${got} (${got_type}). Expected ${expect} (${expect_type})` + ); + }); + +g.test('nextAfterNoFlush') + .paramsSubcasesOnly<nextAfterCase>( + // prettier-ignore + [ + // Edge Cases + { val: Number.NaN, dir: true, result: f32Bits(0x7fffffff) }, + { val: Number.NaN, dir: false, result: f32Bits(0x7fffffff) }, + { val: Number.POSITIVE_INFINITY, dir: true, result: f32Bits(kBit.f32.infinity.positive) }, + { val: Number.POSITIVE_INFINITY, dir: false, result: f32Bits(kBit.f32.infinity.positive) }, + { val: Number.NEGATIVE_INFINITY, dir: true, result: f32Bits(kBit.f32.infinity.negative) }, + { val: Number.NEGATIVE_INFINITY, dir: false, result: f32Bits(kBit.f32.infinity.negative) }, + + // Zeroes + { val: +0, dir: true, result: f32Bits(kBit.f32.subnormal.positive.min) }, + { val: +0, dir: false, result: f32Bits(kBit.f32.subnormal.negative.max) }, + { val: -0, dir: true, result: f32Bits(kBit.f32.subnormal.positive.min) }, + { val: -0, dir: false, result: f32Bits(kBit.f32.subnormal.negative.max) }, + + // Subnormals + { val: hexToF32(kBit.f32.subnormal.positive.min), dir: true, result: f32Bits(0x00000002) }, + { val: hexToF32(kBit.f32.subnormal.positive.min), dir: false, result: f32(0) }, + { val: hexToF32(kBit.f32.subnormal.positive.max), dir: true, result: f32Bits(kBit.f32.positive.min) }, + { val: hexToF32(kBit.f32.subnormal.positive.max), dir: false, result: f32Bits(0x007ffffe) }, + { val: hexToF32(kBit.f32.subnormal.negative.min), dir: true, result: f32Bits(0x807ffffe) }, + { val: hexToF32(kBit.f32.subnormal.negative.min), dir: false, result: f32Bits(kBit.f32.negative.max) }, + { val: hexToF32(kBit.f32.subnormal.negative.max), dir: true, result: f32(0) }, + { val: hexToF32(kBit.f32.subnormal.negative.max), dir: false, result: f32Bits(0x80000002) }, + + // Normals + { val: hexToF32(kBit.f32.positive.max), dir: true, result: f32Bits(kBit.f32.infinity.positive) }, + { val: hexToF32(kBit.f32.positive.max), dir: false, result: f32Bits(0x7f7ffffe) }, + { val: hexToF32(kBit.f32.positive.min), dir: true, result: f32Bits(0x00800001) }, + { val: hexToF32(kBit.f32.positive.min), dir: false, result: f32Bits(kBit.f32.subnormal.positive.max) }, + { val: hexToF32(kBit.f32.negative.max), dir: true, result: f32Bits(kBit.f32.subnormal.negative.min) }, + { val: hexToF32(kBit.f32.negative.max), dir: false, result: f32Bits(0x80800001) }, + { val: hexToF32(kBit.f32.negative.min), dir: true, result: f32Bits(0xff7ffffe) }, + { val: hexToF32(kBit.f32.negative.min), dir: false, result: f32Bits(kBit.f32.infinity.negative) }, + { val: hexToF32(0x03800000), dir: true, result: f32Bits(0x03800001) }, + { val: hexToF32(0x03800000), dir: false, result: f32Bits(0x037fffff) }, + { val: hexToF32(0x83800000), dir: true, result: f32Bits(0x837fffff) }, + { val: hexToF32(0x83800000), dir: false, result: f32Bits(0x83800001) }, + + // Not precisely expressible as float32 + { val: 0.001, dir: true, result: f32Bits(0x3a83126f) }, // positive normal + { val: 0.001, dir: false, result: f32Bits(0x3a83126e) }, // positive normal + { val: -0.001, dir: true, result: f32Bits(0xba83126e) }, // negative normal + { val: -0.001, dir: false, result: f32Bits(0xba83126f) }, // negative normal + { val: 2.82E-40, dir: true, result: f32Bits(0x0003121a) }, // positive subnormal + { val: 2.82E-40, dir: false, result: f32Bits(0x00031219) }, // positive subnormal + { val: -2.82E-40, dir: true, result: f32Bits(0x80031219) }, // negative subnormal + { val: -2.82E-40, dir: false, result: f32Bits(0x8003121a) }, // negative subnormal + ] + ) + .fn(t => { + const val = t.params.val; + const dir = t.params.dir; + const expect = t.params.result; + const expect_type = typeof expect; + const got = nextAfterF32(val, dir, 'no-flush'); + const got_type = typeof got; + t.expect( + got.value === expect.value || (Number.isNaN(got.value) && Number.isNaN(expect.value)), + `nextAfter(${val}, ${dir}, false) returned ${got} (${got_type}). Expected ${expect} (${expect_type})` + ); + }); + +interface OneULPCase { + target: number; + expect: number; +} + +g.test('oneULPFlushToZero') + .paramsSimple<OneULPCase>([ + // Edge Cases + { target: Number.NaN, expect: Number.NaN }, + { target: Number.POSITIVE_INFINITY, expect: hexToF32(0x73800000) }, + { target: Number.NEGATIVE_INFINITY, expect: hexToF32(0x73800000) }, + + // Zeroes + { target: +0, expect: hexToF32(0x00800000) }, + { target: -0, expect: hexToF32(0x00800000) }, + + // Subnormals + { target: hexToF32(kBit.f32.subnormal.positive.min), expect: hexToF32(0x00800000) }, + { target: 2.82e-40, expect: hexToF32(0x00800000) }, // positive subnormal + { target: hexToF32(kBit.f32.subnormal.positive.max), expect: hexToF32(0x00800000) }, + { target: hexToF32(kBit.f32.subnormal.negative.min), expect: hexToF32(0x00800000) }, + { target: -2.82e-40, expect: hexToF32(0x00800000) }, // negative subnormal + { target: hexToF32(kBit.f32.subnormal.negative.max), expect: hexToF32(0x00800000) }, + + // Normals + { target: hexToF32(kBit.f32.positive.min), expect: hexToF32(0x00000001) }, + { target: 1, expect: hexToF32(0x33800000) }, + { target: 2, expect: hexToF32(0x34000000) }, + { target: 4, expect: hexToF32(0x34800000) }, + { target: 1000000, expect: hexToF32(0x3d800000) }, + { target: hexToF32(kBit.f32.positive.max), expect: hexToF32(0x73800000) }, + { target: hexToF32(kBit.f32.negative.max), expect: hexToF32(0x00000001) }, + { target: -1, expect: hexToF32(0x33800000) }, + { target: -2, expect: hexToF32(0x34000000) }, + { target: -4, expect: hexToF32(0x34800000) }, + { target: -1000000, expect: hexToF32(0x3d800000) }, + { target: hexToF32(kBit.f32.negative.min), expect: hexToF32(0x73800000) }, + + // No precise f32 value + { target: 0.001, expect: hexToF32(0x2f000000) }, // positive normal + { target: -0.001, expect: hexToF32(0x2f000000) }, // negative normal + { target: 1e40, expect: hexToF32(0x73800000) }, // positive out of range + { target: -1e40, expect: hexToF32(0x73800000) }, // negative out of range + ]) + .fn(t => { + const target = t.params.target; + const got = oneULP(target, 'flush'); + const expect = t.params.expect; + t.expect( + got === expect || (Number.isNaN(got) && Number.isNaN(expect)), + `oneULP(${target}, true) returned ${got}. Expected ${expect}` + ); + }); + +g.test('oneULPNoFlush') + .paramsSimple<OneULPCase>([ + // Edge Cases + { target: Number.NaN, expect: Number.NaN }, + { target: Number.POSITIVE_INFINITY, expect: hexToF32(0x73800000) }, + { target: Number.NEGATIVE_INFINITY, expect: hexToF32(0x73800000) }, + + // Zeroes + { target: +0, expect: hexToF32(0x00000001) }, + { target: -0, expect: hexToF32(0x00000001) }, + + // Subnormals + { target: hexToF32(kBit.f32.subnormal.positive.min), expect: hexToF32(0x00000001) }, + { target: -2.82e-40, expect: hexToF32(0x00000001) }, // negative subnormal + { target: hexToF32(kBit.f32.subnormal.positive.max), expect: hexToF32(0x00000001) }, + { target: hexToF32(kBit.f32.subnormal.negative.min), expect: hexToF32(0x00000001) }, + { target: 2.82e-40, expect: hexToF32(0x00000001) }, // positive subnormal + { target: hexToF32(kBit.f32.subnormal.negative.max), expect: hexToF32(0x00000001) }, + + // Normals + { target: hexToF32(kBit.f32.positive.min), expect: hexToF32(0x00000001) }, + { target: 1, expect: hexToF32(0x33800000) }, + { target: 2, expect: hexToF32(0x34000000) }, + { target: 4, expect: hexToF32(0x34800000) }, + { target: 1000000, expect: hexToF32(0x3d800000) }, + { target: hexToF32(kBit.f32.positive.max), expect: hexToF32(0x73800000) }, + { target: hexToF32(kBit.f32.negative.max), expect: hexToF32(0x00000001) }, + { target: -1, expect: hexToF32(0x33800000) }, + { target: -2, expect: hexToF32(0x34000000) }, + { target: -4, expect: hexToF32(0x34800000) }, + { target: -1000000, expect: hexToF32(0x3d800000) }, + { target: hexToF32(kBit.f32.negative.min), expect: hexToF32(0x73800000) }, + + // No precise f32 value + { target: 0.001, expect: hexToF32(0x2f000000) }, // positive normal + { target: -0.001, expect: hexToF32(0x2f000000) }, // negative normal + { target: 1e40, expect: hexToF32(0x73800000) }, // positive out of range + { target: -1e40, expect: hexToF32(0x73800000) }, // negative out of range + ]) + .fn(t => { + const target = t.params.target; + const got = oneULP(target, 'no-flush'); + const expect = t.params.expect; + t.expect( + got === expect || (Number.isNaN(got) && Number.isNaN(expect)), + `oneULPImpl(${target}, false) returned ${got}. Expected ${expect}` + ); + }); + +g.test('oneULP') + .paramsSimple<OneULPCase>([ + // Edge Cases + { target: Number.NaN, expect: Number.NaN }, + { target: Number.NEGATIVE_INFINITY, expect: hexToF32(0x73800000) }, + { target: Number.POSITIVE_INFINITY, expect: hexToF32(0x73800000) }, + + // Zeroes + { target: +0, expect: hexToF32(0x00800000) }, + { target: -0, expect: hexToF32(0x00800000) }, + + // Subnormals + { target: hexToF32(kBit.f32.subnormal.negative.max), expect: hexToF32(0x00800000) }, + { target: -2.82e-40, expect: hexToF32(0x00800000) }, + { target: hexToF32(kBit.f32.subnormal.negative.min), expect: hexToF32(0x00800000) }, + { target: hexToF32(kBit.f32.subnormal.positive.max), expect: hexToF32(0x00800000) }, + { target: 2.82e-40, expect: hexToF32(0x00800000) }, + { target: hexToF32(kBit.f32.subnormal.positive.min), expect: hexToF32(0x00800000) }, + + // Normals + { target: hexToF32(kBit.f32.positive.min), expect: hexToF32(0x00000001) }, + { target: 1, expect: hexToF32(0x33800000) }, + { target: 2, expect: hexToF32(0x34000000) }, + { target: 4, expect: hexToF32(0x34800000) }, + { target: 1000000, expect: hexToF32(0x3d800000) }, + { target: hexToF32(kBit.f32.positive.max), expect: hexToF32(0x73800000) }, + { target: hexToF32(kBit.f32.negative.max), expect: hexToF32(0x000000001) }, + { target: -1, expect: hexToF32(0x33800000) }, + { target: -2, expect: hexToF32(0x34000000) }, + { target: -4, expect: hexToF32(0x34800000) }, + { target: -1000000, expect: hexToF32(0x3d800000) }, + { target: hexToF32(kBit.f32.negative.min), expect: hexToF32(0x73800000) }, + + // No precise f32 value + { target: -0.001, expect: hexToF32(0x2f000000) }, // negative normal + { target: -1e40, expect: hexToF32(0x73800000) }, // negative out of range + { target: 0.001, expect: hexToF32(0x2f000000) }, // positive normal + { target: 1e40, expect: hexToF32(0x73800000) }, // positive out of range + ]) + .fn(t => { + const target = t.params.target; + const got = oneULP(target); + const expect = t.params.expect; + t.expect( + got === expect || (Number.isNaN(got) && Number.isNaN(expect)), + `oneULP(${target}) returned ${got}. Expected ${expect}` + ); + }); + +interface correctlyRoundedF32Case { + value: number; + expected: Array<number>; +} + +g.test('correctlyRoundedF32') + .paramsSubcasesOnly<correctlyRoundedF32Case>( + // prettier-ignore + [ + // Edge Cases + { value: kValue.f32.infinity.positive, expected: [kValue.f32.positive.max, Number.POSITIVE_INFINITY] }, + { value: kValue.f32.infinity.negative, expected: [Number.NEGATIVE_INFINITY, kValue.f32.negative.min] }, + { value: kValue.f32.positive.max, expected: [kValue.f32.positive.max] }, + { value: kValue.f32.negative.min, expected: [kValue.f32.negative.min] }, + + // 32-bit subnormals + { value: kValue.f32.subnormal.positive.min, expected: [kValue.f32.subnormal.positive.min] }, + { value: kValue.f32.subnormal.positive.max, expected: [kValue.f32.subnormal.positive.max] }, + { value: kValue.f32.subnormal.negative.min, expected: [kValue.f32.subnormal.negative.min] }, + { value: kValue.f32.subnormal.negative.max, expected: [kValue.f32.subnormal.negative.max] }, + + // 64-bit subnormals + { value: hexToF64(0x00000000, 0x00000001), expected: [0, kValue.f32.subnormal.positive.min] }, + { value: hexToF64(0x00000000, 0x00000002), expected: [0, kValue.f32.subnormal.positive.min] }, + { value: hexToF64(0x800fffff, 0xffffffff), expected: [kValue.f32.subnormal.negative.max, 0] }, + { value: hexToF64(0x800fffff, 0xfffffffe), expected: [kValue.f32.subnormal.negative.max, 0] }, + + // 32-bit normals + { value: 0, expected: [0] }, + { value: kValue.f32.positive.min, expected: [kValue.f32.positive.min] }, + { value: kValue.f32.negative.max, expected: [kValue.f32.negative.max] }, + { value: hexToF32(0x03800000), expected: [hexToF32(0x03800000)] }, + { value: hexToF32(0x03800001), expected: [hexToF32(0x03800001)] }, + { value: hexToF32(0x83800000), expected: [hexToF32(0x83800000)] }, + { value: hexToF32(0x83800001), expected: [hexToF32(0x83800001)] }, + + // 64-bit normals + { value: hexToF64(0x3ff00000, 0x00000001), expected: [hexToF32(0x3f800000), hexToF32(0x3f800001)] }, + { value: hexToF64(0x3ff00000, 0x00000002), expected: [hexToF32(0x3f800000), hexToF32(0x3f800001)] }, + { value: hexToF64(0x3ff00010, 0x00000010), expected: [hexToF32(0x3f800080), hexToF32(0x3f800081)] }, + { value: hexToF64(0x3ff00020, 0x00000020), expected: [hexToF32(0x3f800100), hexToF32(0x3f800101)] }, + { value: hexToF64(0xbff00000, 0x00000001), expected: [hexToF32(0xbf800001), hexToF32(0xbf800000)] }, + { value: hexToF64(0xbff00000, 0x00000002), expected: [hexToF32(0xbf800001), hexToF32(0xbf800000)] }, + { value: hexToF64(0xbff00010, 0x00000010), expected: [hexToF32(0xbf800081), hexToF32(0xbf800080)] }, + { value: hexToF64(0xbff00020, 0x00000020), expected: [hexToF32(0xbf800101), hexToF32(0xbf800100)] }, + ] + ) + .fn(t => { + const value = t.params.value; + const expected = t.params.expected; + + const got = correctlyRoundedF32(value); + t.expect( + objectEquals(expected, got), + `correctlyRoundedF32(${value}) returned [${got}]. Expected [${expected}]` + ); + }); + +interface lerpCase { + a: number; + b: number; + t: number; + result: number; +} + +g.test('lerp') + .paramsSimple<lerpCase>([ + // Infinite cases + { a: 0.0, b: Number.POSITIVE_INFINITY, t: 0.5, result: Number.NaN }, + { a: Number.POSITIVE_INFINITY, b: 0.0, t: 0.5, result: Number.NaN }, + { a: Number.NEGATIVE_INFINITY, b: 1.0, t: 0.5, result: Number.NaN }, + { a: 1.0, b: Number.NEGATIVE_INFINITY, t: 0.5, result: Number.NaN }, + { a: Number.NEGATIVE_INFINITY, b: Number.POSITIVE_INFINITY, t: 0.5, result: Number.NaN }, + { a: Number.POSITIVE_INFINITY, b: Number.NEGATIVE_INFINITY, t: 0.5, result: Number.NaN }, + { a: 0.0, b: 1.0, t: Number.NEGATIVE_INFINITY, result: Number.NaN }, + { a: 1.0, b: 0.0, t: Number.NEGATIVE_INFINITY, result: Number.NaN }, + { a: 0.0, b: 1.0, t: Number.POSITIVE_INFINITY, result: Number.NaN }, + { a: 1.0, b: 0.0, t: Number.POSITIVE_INFINITY, result: Number.NaN }, + + // [0.0, 1.0] cases + { a: 0.0, b: 1.0, t: -1.0, result: -1.0 }, + { a: 0.0, b: 1.0, t: 0.0, result: 0.0 }, + { a: 0.0, b: 1.0, t: 0.1, result: 0.1 }, + { a: 0.0, b: 1.0, t: 0.01, result: 0.01 }, + { a: 0.0, b: 1.0, t: 0.001, result: 0.001 }, + { a: 0.0, b: 1.0, t: 0.25, result: 0.25 }, + { a: 0.0, b: 1.0, t: 0.5, result: 0.5 }, + { a: 0.0, b: 1.0, t: 0.9, result: 0.9 }, + { a: 0.0, b: 1.0, t: 0.99, result: 0.99 }, + { a: 0.0, b: 1.0, t: 0.999, result: 0.999 }, + { a: 0.0, b: 1.0, t: 1.0, result: 1.0 }, + { a: 0.0, b: 1.0, t: 2.0, result: 2.0 }, + + // [1.0, 0.0] cases + { a: 1.0, b: 0.0, t: -1.0, result: 2.0 }, + { a: 1.0, b: 0.0, t: 0.0, result: 1.0 }, + { a: 1.0, b: 0.0, t: 0.1, result: 0.9 }, + { a: 1.0, b: 0.0, t: 0.01, result: 0.99 }, + { a: 1.0, b: 0.0, t: 0.001, result: 0.999 }, + { a: 1.0, b: 0.0, t: 0.25, result: 0.75 }, + { a: 1.0, b: 0.0, t: 0.5, result: 0.5 }, + { a: 1.0, b: 0.0, t: 0.9, result: 0.1 }, + { a: 1.0, b: 0.0, t: 0.99, result: 0.01 }, + { a: 1.0, b: 0.0, t: 0.999, result: 0.001 }, + { a: 1.0, b: 0.0, t: 1.0, result: 0.0 }, + { a: 1.0, b: 0.0, t: 2.0, result: -1.0 }, + + // [0.0, 10.0] cases + { a: 0.0, b: 10.0, t: -1.0, result: -10.0 }, + { a: 0.0, b: 10.0, t: 0.0, result: 0.0 }, + { a: 0.0, b: 10.0, t: 0.1, result: 1.0 }, + { a: 0.0, b: 10.0, t: 0.01, result: 0.1 }, + { a: 0.0, b: 10.0, t: 0.001, result: 0.01 }, + { a: 0.0, b: 10.0, t: 0.25, result: 2.5 }, + { a: 0.0, b: 10.0, t: 0.5, result: 5.0 }, + { a: 0.0, b: 10.0, t: 0.9, result: 9.0 }, + { a: 0.0, b: 10.0, t: 0.99, result: 9.9 }, + { a: 0.0, b: 10.0, t: 0.999, result: 9.99 }, + { a: 0.0, b: 10.0, t: 1.0, result: 10.0 }, + { a: 0.0, b: 10.0, t: 2.0, result: 20.0 }, + + // [10.0, 0.0] cases + { a: 10.0, b: 0.0, t: -1.0, result: 20.0 }, + { a: 10.0, b: 0.0, t: 0.0, result: 10.0 }, + { a: 10.0, b: 0.0, t: 0.1, result: 9 }, + { a: 10.0, b: 0.0, t: 0.01, result: 9.9 }, + { a: 10.0, b: 0.0, t: 0.001, result: 9.99 }, + { a: 10.0, b: 0.0, t: 0.25, result: 7.5 }, + { a: 10.0, b: 0.0, t: 0.5, result: 5.0 }, + { a: 10.0, b: 0.0, t: 0.9, result: 1.0 }, + { a: 10.0, b: 0.0, t: 0.99, result: 0.1 }, + { a: 10.0, b: 0.0, t: 0.999, result: 0.01 }, + { a: 10.0, b: 0.0, t: 1.0, result: 0.0 }, + { a: 10.0, b: 0.0, t: 2.0, result: -10.0 }, + + // [2.0, 10.0] cases + { a: 2.0, b: 10.0, t: -1.0, result: -6.0 }, + { a: 2.0, b: 10.0, t: 0.0, result: 2.0 }, + { a: 2.0, b: 10.0, t: 0.1, result: 2.8 }, + { a: 2.0, b: 10.0, t: 0.01, result: 2.08 }, + { a: 2.0, b: 10.0, t: 0.001, result: 2.008 }, + { a: 2.0, b: 10.0, t: 0.25, result: 4.0 }, + { a: 2.0, b: 10.0, t: 0.5, result: 6.0 }, + { a: 2.0, b: 10.0, t: 0.9, result: 9.2 }, + { a: 2.0, b: 10.0, t: 0.99, result: 9.92 }, + { a: 2.0, b: 10.0, t: 0.999, result: 9.992 }, + { a: 2.0, b: 10.0, t: 1.0, result: 10.0 }, + { a: 2.0, b: 10.0, t: 2.0, result: 18.0 }, + + // [10.0, 2.0] cases + { a: 10.0, b: 2.0, t: -1.0, result: 18.0 }, + { a: 10.0, b: 2.0, t: 0.0, result: 10.0 }, + { a: 10.0, b: 2.0, t: 0.1, result: 9.2 }, + { a: 10.0, b: 2.0, t: 0.01, result: 9.92 }, + { a: 10.0, b: 2.0, t: 0.001, result: 9.992 }, + { a: 10.0, b: 2.0, t: 0.25, result: 8.0 }, + { a: 10.0, b: 2.0, t: 0.5, result: 6.0 }, + { a: 10.0, b: 2.0, t: 0.9, result: 2.8 }, + { a: 10.0, b: 2.0, t: 0.99, result: 2.08 }, + { a: 10.0, b: 2.0, t: 0.999, result: 2.008 }, + { a: 10.0, b: 2.0, t: 1.0, result: 2.0 }, + { a: 10.0, b: 2.0, t: 2.0, result: -6.0 }, + + // [-1.0, 1.0] cases + { a: -1.0, b: 1.0, t: -2.0, result: -5.0 }, + { a: -1.0, b: 1.0, t: 0.0, result: -1.0 }, + { a: -1.0, b: 1.0, t: 0.1, result: -0.8 }, + { a: -1.0, b: 1.0, t: 0.01, result: -0.98 }, + { a: -1.0, b: 1.0, t: 0.001, result: -0.998 }, + { a: -1.0, b: 1.0, t: 0.25, result: -0.5 }, + { a: -1.0, b: 1.0, t: 0.5, result: 0.0 }, + { a: -1.0, b: 1.0, t: 0.9, result: 0.8 }, + { a: -1.0, b: 1.0, t: 0.99, result: 0.98 }, + { a: -1.0, b: 1.0, t: 0.999, result: 0.998 }, + { a: -1.0, b: 1.0, t: 1.0, result: 1.0 }, + { a: -1.0, b: 1.0, t: 2.0, result: 3.0 }, + + // [1.0, -1.0] cases + { a: 1.0, b: -1.0, t: -2.0, result: 5.0 }, + { a: 1.0, b: -1.0, t: 0.0, result: 1.0 }, + { a: 1.0, b: -1.0, t: 0.1, result: 0.8 }, + { a: 1.0, b: -1.0, t: 0.01, result: 0.98 }, + { a: 1.0, b: -1.0, t: 0.001, result: 0.998 }, + { a: 1.0, b: -1.0, t: 0.25, result: 0.5 }, + { a: 1.0, b: -1.0, t: 0.5, result: 0.0 }, + { a: 1.0, b: -1.0, t: 0.9, result: -0.8 }, + { a: 1.0, b: -1.0, t: 0.99, result: -0.98 }, + { a: 1.0, b: -1.0, t: 0.999, result: -0.998 }, + { a: 1.0, b: -1.0, t: 1.0, result: -1.0 }, + { a: 1.0, b: -1.0, t: 2.0, result: -3.0 }, + + // [-1.0, 0.0] cases + { a: -1.0, b: 0.0, t: -1.0, result: -2.0 }, + { a: -1.0, b: 0.0, t: 0.0, result: -1.0 }, + { a: -1.0, b: 0.0, t: 0.1, result: -0.9 }, + { a: -1.0, b: 0.0, t: 0.01, result: -0.99 }, + { a: -1.0, b: 0.0, t: 0.001, result: -0.999 }, + { a: -1.0, b: 0.0, t: 0.25, result: -0.75 }, + { a: -1.0, b: 0.0, t: 0.5, result: -0.5 }, + { a: -1.0, b: 0.0, t: 0.9, result: -0.1 }, + { a: -1.0, b: 0.0, t: 0.99, result: -0.01 }, + { a: -1.0, b: 0.0, t: 0.999, result: -0.001 }, + { a: -1.0, b: 0.0, t: 1.0, result: 0.0 }, + { a: -1.0, b: 0.0, t: 2.0, result: 1.0 }, + + // [0.0, -1.0] cases + { a: 0.0, b: -1.0, t: -1.0, result: 1.0 }, + { a: 0.0, b: -1.0, t: 0.0, result: 0.0 }, + { a: 0.0, b: -1.0, t: 0.1, result: -0.1 }, + { a: 0.0, b: -1.0, t: 0.01, result: -0.01 }, + { a: 0.0, b: -1.0, t: 0.001, result: -0.001 }, + { a: 0.0, b: -1.0, t: 0.25, result: -0.25 }, + { a: 0.0, b: -1.0, t: 0.5, result: -0.5 }, + { a: 0.0, b: -1.0, t: 0.9, result: -0.9 }, + { a: 0.0, b: -1.0, t: 0.99, result: -0.99 }, + { a: 0.0, b: -1.0, t: 0.999, result: -0.999 }, + { a: 0.0, b: -1.0, t: 1.0, result: -1.0 }, + { a: 0.0, b: -1.0, t: 2.0, result: -2.0 }, + ]) + .fn(test => { + const a = test.params.a; + const b = test.params.b; + const t = test.params.t; + const got = lerp(a, b, t); + const expect = test.params.result; + + test.expect( + (Number.isNaN(got) && Number.isNaN(expect)) || withinOneULP(got, expect, 'flush'), + `lerp(${a}, ${b}, ${t}) returned ${got}. Expected ${expect}` + ); + }); + +interface rangeCase { + a: number; + b: number; + num_steps: number; + result: Array<number>; +} + +g.test('linearRange') + .paramsSimple<rangeCase>( + // prettier-ignore + [ + { a: 0.0, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: Number.POSITIVE_INFINITY, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: Number.NEGATIVE_INFINITY, b: 1.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: 1.0, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: Number.NEGATIVE_INFINITY, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: Number.POSITIVE_INFINITY, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: 0.0, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(0.0) }, + { a: 10.0, b: 10.0, num_steps: 10, result: new Array<number>(10).fill(10.0) }, + { a: 0.0, b: 10.0, num_steps: 1, result: [0.0] }, + { a: 10.0, b: 0.0, num_steps: 1, result: [10] }, + { a: 0.0, b: 10.0, num_steps: 11, result: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] }, + { a: 10.0, b: 0.0, num_steps: 11, result: [10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] }, + { a: 0.0, b: 1000.0, num_steps: 11, result: [0.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0] }, + { a: 1000.0, b: 0.0, num_steps: 11, result: [1000.0, 900.0, 800.0, 700.0, 600.0, 500.0, 400.0, 300.0, 200.0, 100.0, 0.0] }, + { a: 1.0, b: 5.0, num_steps: 5, result: [1.0, 2.0, 3.0, 4.0, 5.0] }, + { a: 5.0, b: 1.0, num_steps: 5, result: [5.0, 4.0, 3.0, 2.0, 1.0] }, + { a: 0.0, b: 1.0, num_steps: 11, result: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] }, + { a: 1.0, b: 0.0, num_steps: 11, result: [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0] }, + { a: 0.0, b: 1.0, num_steps: 5, result: [0.0, 0.25, 0.5, 0.75, 1.0] }, + { a: 1.0, b: 0.0, num_steps: 5, result: [1.0, 0.75, 0.5, 0.25, 0.0] }, + { a: -1.0, b: 1.0, num_steps: 11, result: [-1.0, -0.8, -0.6, -0.4, -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0] }, + { a: 1.0, b: -1.0, num_steps: 11, result: [1.0, 0.8, 0.6, 0.4, 0.2, 0.0, -0.2, -0.4, -0.6, -0.8, -1.0] }, + { a: -1.0, b: 0, num_steps: 11, result: [-1.0, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0.0] }, + { a: 0.0, b: -1.0, num_steps: 11, result: [0.0, -0.1, -0.2, -0.3, -0.4, -0.5, -0.6, -0.7, -0.8, -0.9, -1.0] }, + ] + ) + .fn(test => { + const a = test.params.a; + const b = test.params.b; + const num_steps = test.params.num_steps; + const got = linearRange(a, b, num_steps); + const expect = test.params.result; + + test.expect( + compareArrayOfNumbers(got, expect, 'no-flush'), + `linearRange(${a}, ${b}, ${num_steps}) returned ${got}. Expected ${expect}` + ); + }); + +g.test('biasedRange') + .paramsSimple<rangeCase>( + // prettier-ignore + [ + { a: 0.0, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: Number.POSITIVE_INFINITY, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: Number.NEGATIVE_INFINITY, b: 1.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: 1.0, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: Number.NEGATIVE_INFINITY, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: Number.POSITIVE_INFINITY, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) }, + { a: 0.0, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(0.0) }, + { a: 10.0, b: 10.0, num_steps: 10, result: new Array<number>(10).fill(10.0) }, + { a: 0.0, b: 10.0, num_steps: 1, result: [0.0] }, + { a: 10.0, b: 0.0, num_steps: 1, result: [10.0] }, + { a: 0.0, b: 10.0, num_steps: 11, result: [0.0, 0.1, 0.4, 0.9, 1.6, 2.5, 3.6, 4.9, 6.4, 8.1, 10.0] }, + { a: 10.0, b: 0.0, num_steps: 11, result: [10.0, 9.9, 9.6, 9.1, 8.4, 7.5, 6.4, 5.1, 3.6, 1.9, 0.0] }, + { a: 0.0, b: 1000.0, num_steps: 11, result: [0.0, 10.0, 40.0, 90.0, 160.0, 250.0, 360.0, 490.0, 640.0, 810.0, 1000.0] }, + { a: 1000.0, b: 0.0, num_steps: 11, result: [1000.0, 990.0, 960.0, 910.0, 840.0, 750.0, 640.0, 510.0, 360.0, 190.0, 0.0] }, + { a: 1.0, b: 5.0, num_steps: 5, result: [1.0, 1.25, 2.0, 3.25, 5.0] }, + { a: 5.0, b: 1.0, num_steps: 5, result: [5.0, 4.75, 4.0, 2.75, 1.0] }, + { a: 0.0, b: 1.0, num_steps: 11, result: [0.0, 0.01, 0.04, 0.09, 0.16, 0.25, 0.36, 0.49, 0.64, 0.81, 1.0] }, + { a: 1.0, b: 0.0, num_steps: 11, result: [1.0, 0.99, 0.96, 0.91, 0.84, 0.75, 0.64, 0.51, 0.36, 0.19, 0.0] }, + { a: 0.0, b: 1.0, num_steps: 5, result: [0.0, 0.0625, 0.25, 0.5625, 1.0] }, + { a: 1.0, b: 0.0, num_steps: 5, result: [1.0, 0.9375, 0.75, 0.4375, 0.0] }, + { a: -1.0, b: 1.0, num_steps: 11, result: [-1.0, -0.98, -0.92, -0.82, -0.68, -0.5, -0.28 ,-0.02, 0.28, 0.62, 1.0] }, + { a: 1.0, b: -1.0, num_steps: 11, result: [1.0, 0.98, 0.92, 0.82, 0.68, 0.5, 0.28 ,0.02, -0.28, -0.62, -1.0] }, + { a: -1.0, b: 0, num_steps: 11, result: [-1.0 , -0.99, -0.96, -0.91, -0.84, -0.75, -0.64, -0.51, -0.36, -0.19, 0.0] }, + { a: 0.0, b: -1.0, num_steps: 11, result: [0.0, -0.01, -0.04, -0.09, -0.16, -0.25, -0.36, -0.49, -0.64, -0.81, -1.0] }, + ] + ) + .fn(test => { + const a = test.params.a; + const b = test.params.b; + const num_steps = test.params.num_steps; + const got = biasedRange(a, b, num_steps); + const expect = test.params.result; + + test.expect( + compareArrayOfNumbers(got, expect, 'no-flush'), + `biasedRange(${a}, ${b}, ${num_steps}) returned ${got}. Expected ${expect}` + ); + }); + +interface fullF32RangeCase { + neg_norm: number; + neg_sub: number; + pos_sub: number; + pos_norm: number; + expect: Array<number>; +} + +g.test('fullF32Range') + .paramsSimple<fullF32RangeCase>( + // prettier-ignore + [ + { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ 0.0 ] }, + { neg_norm: 1, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.min, 0.0] }, + { neg_norm: 2, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.min, kValue.f32.negative.max, 0.0 ] }, + { neg_norm: 3, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.min, -1.9999998807907104, kValue.f32.negative.max, 0.0 ] }, + { neg_norm: 0, neg_sub: 1, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.subnormal.negative.min, 0.0 ] }, + { neg_norm: 0, neg_sub: 2, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max, 0.0 ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 1, pos_norm: 0, expect: [ 0.0, kValue.f32.subnormal.positive.min ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 2, pos_norm: 0, expect: [ 0.0, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.positive.max ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 1, expect: [ 0.0, kValue.f32.positive.min ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 2, expect: [ 0.0, kValue.f32.positive.min, kValue.f32.positive.max ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 3, expect: [ 0.0, kValue.f32.positive.min, 1.9999998807907104, kValue.f32.positive.max ] }, + { neg_norm: 1, neg_sub: 1, pos_sub: 1, pos_norm: 1, expect: [ kValue.f32.negative.min, kValue.f32.subnormal.negative.min, 0.0, kValue.f32.subnormal.positive.min, kValue.f32.positive.min ] }, + { neg_norm: 2, neg_sub: 2, pos_sub: 2, pos_norm: 2, expect: [ kValue.f32.negative.min, kValue.f32.negative.max, kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max, 0.0, kValue.f32.subnormal.positive.min, kValue.f32.subnormal.positive.max, kValue.f32.positive.min, kValue.f32.positive.max ] }, + ] + ) + .fn(test => { + const neg_norm = test.params.neg_norm; + const neg_sub = test.params.neg_sub; + const pos_sub = test.params.pos_sub; + const pos_norm = test.params.pos_norm; + const got = fullF32Range({ neg_norm, neg_sub, pos_sub, pos_norm }); + const expect = test.params.expect; + + test.expect( + compareArrayOfNumbers(got, expect, 'no-flush'), + `fullF32Range(${neg_norm}, ${neg_sub}, ${pos_sub}, ${pos_norm}) returned [${got}]. Expected [${expect}]` + ); + }); + +interface fullF16RangeCase { + neg_norm: number; + neg_sub: number; + pos_sub: number; + pos_norm: number; + expect: Array<number>; +} + +g.test('fullF16Range') + .paramsSimple<fullF16RangeCase>( + // prettier-ignore + [ + { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ 0.0 ] }, + { neg_norm: 1, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.min, 0.0] }, + { neg_norm: 2, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.min, kValue.f16.negative.max, 0.0 ] }, + { neg_norm: 3, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.min, -1.9990234375, kValue.f16.negative.max, 0.0 ] }, + { neg_norm: 0, neg_sub: 1, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.subnormal.negative.min, 0.0 ] }, + { neg_norm: 0, neg_sub: 2, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.subnormal.negative.min, kValue.f16.subnormal.negative.max, 0.0 ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 1, pos_norm: 0, expect: [ 0.0, kValue.f16.subnormal.positive.min ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 2, pos_norm: 0, expect: [ 0.0, kValue.f16.subnormal.positive.min, kValue.f16.subnormal.positive.max ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 1, expect: [ 0.0, kValue.f16.positive.min ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 2, expect: [ 0.0, kValue.f16.positive.min, kValue.f16.positive.max ] }, + { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 3, expect: [ 0.0, kValue.f16.positive.min, 1.9990234375, kValue.f16.positive.max ] }, + { neg_norm: 1, neg_sub: 1, pos_sub: 1, pos_norm: 1, expect: [ kValue.f16.negative.min, kValue.f16.subnormal.negative.min, 0.0, kValue.f16.subnormal.positive.min, kValue.f16.positive.min ] }, + { neg_norm: 2, neg_sub: 2, pos_sub: 2, pos_norm: 2, expect: [ kValue.f16.negative.min, kValue.f16.negative.max, kValue.f16.subnormal.negative.min, kValue.f16.subnormal.negative.max, 0.0, kValue.f16.subnormal.positive.min, kValue.f16.subnormal.positive.max, kValue.f16.positive.min, kValue.f16.positive.max ] }, + ] + ) + .fn(test => { + const neg_norm = test.params.neg_norm; + const neg_sub = test.params.neg_sub; + const pos_sub = test.params.pos_sub; + const pos_norm = test.params.pos_norm; + const got = fullF16Range({ neg_norm, neg_sub, pos_sub, pos_norm }); + const expect = test.params.expect; + + test.expect( + compareArrayOfNumbers(got, expect), + `fullF16Range(${neg_norm}, ${neg_sub}, ${pos_sub}, ${pos_norm}) returned [${got}]. Expected [${expect}]` + ); + }); + +interface fullI32RangeCase { + neg_count: number; + pos_count: number; + expect: Array<number>; +} + +g.test('fullI32Range') + .paramsSimple<fullI32RangeCase>( + // prettier-ignore + [ + { neg_count: 0, pos_count: 0, expect: [0] }, + { neg_count: 1, pos_count: 0, expect: [kValue.i32.negative.min, 0] }, + { neg_count: 2, pos_count: 0, expect: [kValue.i32.negative.min, -1, 0] }, + { neg_count: 3, pos_count: 0, expect: [kValue.i32.negative.min, -1610612736, -1, 0] }, + { neg_count: 0, pos_count: 1, expect: [0, 1] }, + { neg_count: 0, pos_count: 2, expect: [0, 1, kValue.i32.positive.max] }, + { neg_count: 0, pos_count: 3, expect: [0, 1, 536870912, kValue.i32.positive.max] }, + { neg_count: 1, pos_count: 1, expect: [kValue.i32.negative.min, 0, 1] }, + { neg_count: 2, pos_count: 2, expect: [kValue.i32.negative.min, -1, 0, 1, kValue.i32.positive.max ] }, + ] + ) + .fn(test => { + const neg_count = test.params.neg_count; + const pos_count = test.params.pos_count; + const got = fullI32Range({ negative: neg_count, positive: pos_count }); + const expect = test.params.expect; + + test.expect( + compareArrayOfNumbers(got, expect), + `fullI32Range(${neg_count}, ${pos_count}) returned [${got}]. Expected [${expect}]` + ); + }); + +interface limitsCase { + bits: number; + value: number; +} + +// Test to confirm kBit and kValue constants are equivalent for f32 +g.test('f32LimitsEquivalency') + .paramsSimple<limitsCase>([ + { bits: kBit.f32.positive.max, value: kValue.f32.positive.max }, + { bits: kBit.f32.positive.min, value: kValue.f32.positive.min }, + { bits: kBit.f32.positive.nearest_max, value: kValue.f32.positive.nearest_max }, + { bits: kBit.f32.positive.less_than_one, value: kValue.f32.positive.less_than_one }, + { bits: kBit.f32.positive.pi.whole, value: kValue.f32.positive.pi.whole }, + { bits: kBit.f32.positive.pi.three_quarters, value: kValue.f32.positive.pi.three_quarters }, + { bits: kBit.f32.positive.pi.half, value: kValue.f32.positive.pi.half }, + { bits: kBit.f32.positive.pi.third, value: kValue.f32.positive.pi.third }, + { bits: kBit.f32.positive.pi.quarter, value: kValue.f32.positive.pi.quarter }, + { bits: kBit.f32.positive.pi.sixth, value: kValue.f32.positive.pi.sixth }, + { bits: kBit.f32.positive.e, value: kValue.f32.positive.e }, + { bits: kBit.f32.negative.max, value: kValue.f32.negative.max }, + { bits: kBit.f32.negative.min, value: kValue.f32.negative.min }, + { bits: kBit.f32.negative.nearest_min, value: kValue.f32.negative.nearest_min }, + { bits: kBit.f32.negative.pi.whole, value: kValue.f32.negative.pi.whole }, + { bits: kBit.f32.negative.pi.three_quarters, value: kValue.f32.negative.pi.three_quarters }, + { bits: kBit.f32.negative.pi.half, value: kValue.f32.negative.pi.half }, + { bits: kBit.f32.negative.pi.third, value: kValue.f32.negative.pi.third }, + { bits: kBit.f32.negative.pi.quarter, value: kValue.f32.negative.pi.quarter }, + { bits: kBit.f32.negative.pi.sixth, value: kValue.f32.negative.pi.sixth }, + { bits: kBit.f32.subnormal.positive.max, value: kValue.f32.subnormal.positive.max }, + { bits: kBit.f32.subnormal.positive.min, value: kValue.f32.subnormal.positive.min }, + { bits: kBit.f32.subnormal.negative.max, value: kValue.f32.subnormal.negative.max }, + { bits: kBit.f32.subnormal.negative.min, value: kValue.f32.subnormal.negative.min }, + { bits: kBit.f32.infinity.positive, value: kValue.f32.infinity.positive }, + { bits: kBit.f32.infinity.negative, value: kValue.f32.infinity.negative }, + ]) + .fn(test => { + const bits = test.params.bits; + const value = test.params.value; + + const val_to_bits = bits === float32ToUint32(value); + const bits_to_val = value === uint32ToFloat32(bits); + test.expect( + val_to_bits && bits_to_val, + `bits = ${bits}, value = ${value}, returned val_to_bits as ${val_to_bits}, and bits_to_val as ${bits_to_val}, they are expected to be equivalent` + ); + }); + +// Test to confirm kBit and kValue constants are equivalent for f16 +g.test('f16LimitsEquivalency') + .paramsSimple<limitsCase>([ + { bits: kBit.f16.positive.max, value: kValue.f16.positive.max }, + { bits: kBit.f16.positive.min, value: kValue.f16.positive.min }, + { bits: kBit.f16.negative.max, value: kValue.f16.negative.max }, + { bits: kBit.f16.negative.min, value: kValue.f16.negative.min }, + { bits: kBit.f16.subnormal.positive.max, value: kValue.f16.subnormal.positive.max }, + { bits: kBit.f16.subnormal.positive.min, value: kValue.f16.subnormal.positive.min }, + { bits: kBit.f16.subnormal.negative.max, value: kValue.f16.subnormal.negative.max }, + { bits: kBit.f16.subnormal.negative.min, value: kValue.f16.subnormal.negative.min }, + { bits: kBit.f16.infinity.positive, value: kValue.f16.infinity.positive }, + { bits: kBit.f16.infinity.negative, value: kValue.f16.infinity.negative }, + ]) + .fn(test => { + const bits = test.params.bits; + const value = test.params.value; + + const val_to_bits = bits === float16ToUint16(value); + const bits_to_val = value === uint16ToFloat16(bits); + test.expect( + val_to_bits && bits_to_val, + `bits = ${bits}, value = ${value}, returned val_to_bits as ${val_to_bits}, and bits_to_val as ${bits_to_val}, they are expected to be equivalent` + ); + }); + +interface cartesianProductCase<T> { + inputs: T[][]; + result: T[][]; +} + +g.test('cartesianProductNumber') + .paramsSimple<cartesianProductCase<number>>( + // prettier-ignore + [ + { inputs: [[0], [1]], result: [[0, 1]] }, + { inputs: [[0, 1], [2]], result: [[0, 2], + [1, 2]] }, + { inputs: [[0], [1, 2]], result: [[0, 1], + [0, 2]] }, + { inputs: [[0, 1], [2, 3]], result: [[0,2], + [1, 2], + [0, 3], + [1, 3]] }, + { inputs: [[0, 1, 2], [3, 4, 5]], result: [[0, 3], + [1, 3], + [2, 3], + [0, 4], + [1, 4], + [2, 4], + [0, 5], + [1, 5], + [2, 5]] }, + { inputs: [[0, 1], [2, 3], [4, 5]], result: [[0, 2, 4], + [1, 2, 4], + [0, 3, 4], + [1, 3, 4], + [0, 2, 5], + [1, 2, 5], + [0, 3, 5], + [1, 3, 5]] }, + + ] + ) + .fn(test => { + const inputs = test.params.inputs; + const got = cartesianProduct(...inputs); + const expect = test.params.result; + + test.expect( + objectEquals(got, expect), + `cartesianProduct(${JSON.stringify(inputs)}) returned ${JSON.stringify( + got + )}. Expected ${JSON.stringify(expect)} ` + ); + }); + +g.test('cartesianProductArray') + .paramsSimple<cartesianProductCase<number[]>>( + // prettier-ignore + [ + { inputs: [[[0, 1], [2, 3]], [[4, 5], [6, 7]]], result: [[[0, 1], [4, 5]], + [[2, 3], [4, 5]], + [[0, 1], [6, 7]], + [[2, 3], [6, 7]]]}, + { inputs: [[[0, 1], [2, 3]], [[4, 5], [6, 7]], [[8, 9]]], result: [[[0, 1], [4, 5], [8, 9]], + [[2, 3], [4, 5], [8, 9]], + [[0, 1], [6, 7], [8, 9]], + [[2, 3], [6, 7], [8, 9]]]}, + { inputs: [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[2, 1, 0], [5, 4, 3], [8, 7, 6]]], result: [[[0, 1, 2], [2, 1, 0]], + [[3, 4, 5], [2, 1, 0]], + [[6, 7, 8], [2, 1, 0]], + [[0, 1, 2], [5, 4, 3]], + [[3, 4, 5], [5, 4, 3]], + [[6, 7, 8], [5, 4, 3]], + [[0, 1, 2], [8, 7, 6]], + [[3, 4, 5], [8, 7, 6]], + [[6, 7, 8], [8, 7, 6]]]} + + ] + ) + .fn(test => { + const inputs = test.params.inputs; + const got = cartesianProduct(...inputs); + const expect = test.params.result; + + test.expect( + objectEquals(got, expect), + `cartesianProduct(${JSON.stringify(inputs)}) returned ${JSON.stringify( + got + )}. Expected ${JSON.stringify(expect)} ` + ); + }); + +interface calculatePermutationsCase<T> { + input: T[]; + result: T[][]; +} + +g.test('calculatePermutations') + .paramsSimple<calculatePermutationsCase<number>>( + // prettier-ignore + [ + { input: [0, 1], result: [[0, 1], + [1, 0]] }, + { input: [0, 1, 2], result: [[0, 1, 2], + [0, 2, 1], + [1, 0, 2], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0]] }, + { input: [0, 1, 2, 3], result: [[0, 1, 2, 3], + [0, 1, 3, 2], + [0, 2, 1, 3], + [0, 2, 3, 1], + [0, 3, 1, 2], + [0, 3, 2, 1], + [1, 0, 2, 3], + [1, 0, 3, 2], + [1, 2, 0, 3], + [1, 2, 3, 0], + [1, 3, 0, 2], + [1, 3, 2, 0], + [2, 0, 1, 3], + [2, 0, 3, 1], + [2, 1, 0, 3], + [2, 1, 3, 0], + [2, 3, 0, 1], + [2, 3, 1, 0], + [3, 0, 1, 2], + [3, 0, 2, 1], + [3, 1, 0, 2], + [3, 1, 2, 0], + [3, 2, 0, 1], + [3, 2, 1, 0]] }, + ] + ) + .fn(test => { + const input = test.params.input; + const got = calculatePermutations(input); + const expect = test.params.result; + + test.expect( + objectEquals(got, expect), + `calculatePermutations(${JSON.stringify(input)}) returned ${JSON.stringify( + got + )}. Expected ${JSON.stringify(expect)} ` + ); + }); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts new file mode 100644 index 0000000000..5c20af355d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts @@ -0,0 +1,440 @@ +export const description = ` +Unit tests for parameterization helpers. +`; + +import { + kUnitCaseParamsBuilder, + CaseSubcaseIterable, + ParamsBuilderBase, + builderIterateCasesWithSubcases, +} from '../common/framework/params_builder.js'; +import { makeTestGroup } from '../common/framework/test_group.js'; +import { mergeParams, publicParamsEquals } from '../common/internal/params_utils.js'; +import { assert, objectEquals } from '../common/util/util.js'; + +import { UnitTest } from './unit_test.js'; + +class ParamsTest extends UnitTest { + expectParams<CaseP, SubcaseP>( + act: ParamsBuilderBase<CaseP, SubcaseP>, + exp: CaseSubcaseIterable<{}, {}> + ): void { + const a = Array.from(builderIterateCasesWithSubcases(act)).map(([caseP, subcases]) => [ + caseP, + subcases ? Array.from(subcases) : undefined, + ]); + const e = Array.from(exp); + this.expect( + objectEquals(a, e), + ` +got ${JSON.stringify(a)} +expected ${JSON.stringify(e)}` + ); + } +} + +export const g = makeTestGroup(ParamsTest); + +const u = kUnitCaseParamsBuilder; + +g.test('combine').fn(t => { + t.expectParams<{ hello: number }, {}>(u.combine('hello', [1, 2, 3]), [ + [{ hello: 1 }, undefined], + [{ hello: 2 }, undefined], + [{ hello: 3 }, undefined], + ]); + t.expectParams<{ hello: 1 | 2 | 3 }, {}>(u.combine('hello', [1, 2, 3] as const), [ + [{ hello: 1 }, undefined], + [{ hello: 2 }, undefined], + [{ hello: 3 }, undefined], + ]); + t.expectParams<{}, { hello: number }>(u.beginSubcases().combine('hello', [1, 2, 3]), [ + [{}, [{ hello: 1 }, { hello: 2 }, { hello: 3 }]], + ]); + t.expectParams<{}, { hello: 1 | 2 | 3 }>(u.beginSubcases().combine('hello', [1, 2, 3] as const), [ + [{}, [{ hello: 1 }, { hello: 2 }, { hello: 3 }]], + ]); +}); + +g.test('empty').fn(t => { + t.expectParams<{}, {}>(u, [ + [{}, undefined], // + ]); + t.expectParams<{}, {}>(u.beginSubcases(), [ + [{}, [{}]], // + ]); +}); + +g.test('combine,zeroes_and_ones').fn(t => { + t.expectParams<{}, {}>(u.combineWithParams([]).combineWithParams([]), []); + t.expectParams<{}, {}>(u.combineWithParams([]).combineWithParams([{}]), []); + t.expectParams<{}, {}>(u.combineWithParams([{}]).combineWithParams([]), []); + t.expectParams<{}, {}>(u.combineWithParams([{}]).combineWithParams([{}]), [ + [{}, undefined], // + ]); + + t.expectParams<{}, {}>(u.combine('x', []).combine('y', []), []); + t.expectParams<{}, {}>(u.combine('x', []).combine('y', [1]), []); + t.expectParams<{}, {}>(u.combine('x', [1]).combine('y', []), []); + t.expectParams<{}, {}>(u.combine('x', [1]).combine('y', [1]), [ + [{ x: 1, y: 1 }, undefined], // + ]); +}); + +g.test('combine,mixed').fn(t => { + t.expectParams<{ x: number; y: string; p: number | undefined; q: number | undefined }, {}>( + u + .combine('x', [1, 2]) + .combine('y', ['a', 'b']) + .combineWithParams([{ p: 4 }, { q: 5 }]) + .combineWithParams([{}]), + [ + [{ x: 1, y: 'a', p: 4 }, undefined], + [{ x: 1, y: 'a', q: 5 }, undefined], + [{ x: 1, y: 'b', p: 4 }, undefined], + [{ x: 1, y: 'b', q: 5 }, undefined], + [{ x: 2, y: 'a', p: 4 }, undefined], + [{ x: 2, y: 'a', q: 5 }, undefined], + [{ x: 2, y: 'b', p: 4 }, undefined], + [{ x: 2, y: 'b', q: 5 }, undefined], + ] + ); +}); + +g.test('filter').fn(t => { + t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>( + u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .filter(p => p.a), + [ + [{ a: true, x: 1 }, undefined], // + ] + ); + + t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>( + u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .beginSubcases() + .filter(p => p.a), + [ + [{ a: true, x: 1 }, [{}]], // + // Case with no subcases is filtered out. + ] + ); + + t.expectParams<{}, { a: boolean; x: number | undefined; y: number | undefined }>( + u + .beginSubcases() + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .filter(p => p.a), + [ + [{}, [{ a: true, x: 1 }]], // + ] + ); +}); + +g.test('unless').fn(t => { + t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>( + u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .unless(p => p.a), + [ + [{ a: false, y: 2 }, undefined], // + ] + ); + + t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>( + u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .beginSubcases() + .unless(p => p.a), + [ + // Case with no subcases is filtered out. + [{ a: false, y: 2 }, [{}]], // + ] + ); + + t.expectParams<{}, { a: boolean; x: number | undefined; y: number | undefined }>( + u + .beginSubcases() + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .unless(p => p.a), + [ + [{}, [{ a: false, y: 2 }]], // + ] + ); +}); + +g.test('expandP').fn(t => { + // simple + t.expectParams<{}, {}>( + u.expandWithParams(function* () {}), + [] + ); + t.expectParams<{}, {}>( + u.expandWithParams(function* () { + yield {}; + }), + [[{}, undefined]] + ); + t.expectParams<{ z: number | undefined; w: number | undefined }, {}>( + u.expandWithParams(function* () { + yield* kUnitCaseParamsBuilder.combine('z', [3, 4]); + yield { w: 5 }; + }), + [ + [{ z: 3 }, undefined], + [{ z: 4 }, undefined], + [{ w: 5 }, undefined], + ] + ); + t.expectParams<{}, { z: number | undefined; w: number | undefined }>( + u.beginSubcases().expandWithParams(function* () { + yield* kUnitCaseParamsBuilder.combine('z', [3, 4]); + yield { w: 5 }; + }), + [[{}, [{ z: 3 }, { z: 4 }, { w: 5 }]]] + ); + + // more complex + t.expectParams< + { + a: boolean; + x: number | undefined; + y: number | undefined; + z: number | undefined; + w: number | undefined; + }, + {} + >( + u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .expandWithParams(function* (p) { + if (p.a) { + yield { z: 3 }; + yield { z: 4 }; + } else { + yield { w: 5 }; + } + }), + [ + [{ a: true, x: 1, z: 3 }, undefined], + [{ a: true, x: 1, z: 4 }, undefined], + [{ a: false, y: 2, w: 5 }, undefined], + ] + ); + t.expectParams< + { a: boolean; x: number | undefined; y: number | undefined }, + { z: number | undefined; w: number | undefined } + >( + u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .beginSubcases() + .expandWithParams(function* (p) { + if (p.a) { + yield { z: 3 }; + yield { z: 4 }; + } else { + yield { w: 5 }; + } + }), + [ + [{ a: true, x: 1 }, [{ z: 3 }, { z: 4 }]], + [{ a: false, y: 2 }, [{ w: 5 }]], + ] + ); +}); + +g.test('expand').fn(t => { + // simple + t.expectParams<{}, {}>( + u.expand('x', function* () {}), + [] + ); + t.expectParams<{ z: number }, {}>( + u.expand('z', function* () { + yield 3; + yield 4; + }), + [ + [{ z: 3 }, undefined], + [{ z: 4 }, undefined], + ] + ); + t.expectParams<{}, { z: number }>( + u.beginSubcases().expand('z', function* () { + yield 3; + yield 4; + }), + [[{}, [{ z: 3 }, { z: 4 }]]] + ); + + // more complex + t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined; z: number }, {}>( + u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .expand('z', function* (p) { + if (p.a) { + yield 3; + } else { + yield 5; + } + }), + [ + [{ a: true, x: 1, z: 3 }, undefined], + [{ a: false, y: 2, z: 5 }, undefined], + ] + ); + t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, { z: number }>( + u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, y: 2 }, + ]) + .beginSubcases() + .expand('z', function* (p) { + if (p.a) { + yield 3; + } else { + yield 5; + } + }), + [ + [{ a: true, x: 1 }, [{ z: 3 }]], + [{ a: false, y: 2 }, [{ z: 5 }]], + ] + ); +}); + +g.test('invalid,shadowing').fn(t => { + // Existing CaseP is shadowed by a new CaseP. + { + const p = u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, x: 2 }, + ]) + .expandWithParams(function* (p) { + if (p.a) { + yield { x: 3 }; + } else { + yield { w: 5 }; + } + }); + // Iterating causes e.g. mergeParams({x:1}, {x:3}), which fails. + t.shouldThrow('Error', () => { + Array.from(p.iterateCasesWithSubcases()); + }); + } + // Existing SubcaseP is shadowed by a new SubcaseP. + { + const p = u + .beginSubcases() + .combineWithParams([ + { a: true, x: 1 }, + { a: false, x: 2 }, + ]) + .expandWithParams(function* (p) { + if (p.a) { + yield { x: 3 }; + } else { + yield { w: 5 }; + } + }); + // Iterating causes e.g. mergeParams({x:1}, {x:3}), which fails. + t.shouldThrow('Error', () => { + Array.from(p.iterateCasesWithSubcases()); + }); + } + // Existing CaseP is shadowed by a new SubcaseP. + { + const p = u + .combineWithParams([ + { a: true, x: 1 }, + { a: false, x: 2 }, + ]) + .beginSubcases() + .expandWithParams(function* (p) { + if (p.a) { + yield { x: 3 }; + } else { + yield { w: 5 }; + } + }); + const cases = Array.from(p.iterateCasesWithSubcases()); + // Iterating cases is fine... + for (const [caseP, subcases] of cases) { + assert(subcases !== undefined); + // Iterating subcases is fine... + for (const subcaseP of subcases) { + if (caseP.a) { + assert(subcases !== undefined); + // Only errors once we try to e.g. mergeParams({x:1}, {x:3}). + t.shouldThrow('Error', () => { + mergeParams(caseP, subcaseP); + }); + } else { + mergeParams(caseP, subcaseP); + } + } + } + } +}); + +g.test('undefined').fn(t => { + t.expect(!publicParamsEquals({ a: undefined }, {})); + t.expect(!publicParamsEquals({}, { a: undefined })); +}); + +g.test('private').fn(t => { + t.expect(publicParamsEquals({ _a: 0 }, {})); + t.expect(publicParamsEquals({}, { _a: 0 })); +}); + +g.test('value,array').fn(t => { + t.expectParams<{ a: number[] }, {}>(u.combineWithParams([{ a: [1, 2] }]), [ + [{ a: [1, 2] }, undefined], // + ]); + t.expectParams<{}, { a: number[] }>(u.beginSubcases().combineWithParams([{ a: [1, 2] }]), [ + [{}, [{ a: [1, 2] }]], // + ]); +}); + +g.test('value,object').fn(t => { + t.expectParams<{ a: { [k: string]: number } }, {}>(u.combineWithParams([{ a: { x: 1 } }]), [ + [{ a: { x: 1 } }, undefined], // + ]); + t.expectParams<{}, { a: { [k: string]: number } }>( + u.beginSubcases().combineWithParams([{ a: { x: 1 } }]), + [ + [{}, [{ a: { x: 1 } }]], // + ] + ); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts new file mode 100644 index 0000000000..08a84b23e7 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts @@ -0,0 +1,112 @@ +export const description = ` +Unit tests for parameterization. +`; + +import { TestParams } from '../common/framework/fixture.js'; +import { kUnitCaseParamsBuilder } from '../common/framework/params_builder.js'; +import { makeTestGroup } from '../common/framework/test_group.js'; +import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js'; + +import { TestGroupTest } from './test_group_test.js'; +import { UnitTest } from './unit_test.js'; + +export const g = makeTestGroup(TestGroupTest); + +g.test('combine_none,arg_unit') + .params(u => u.combineWithParams([])) + .fn(t => { + t.fail("this test shouldn't run"); + }); + +g.test('combine_none,arg_ignored') + .params(() => kUnitCaseParamsBuilder.combineWithParams([])) + .fn(t => { + t.fail("this test shouldn't run"); + }); + +g.test('combine_none,plain_builder') + .params(kUnitCaseParamsBuilder.combineWithParams([])) + .fn(t => { + t.fail("this test shouldn't run"); + }); + +g.test('combine_none,plain_array') + .paramsSimple([]) + .fn(t => { + t.fail("this test shouldn't run"); + }); + +g.test('combine_one,case') + .params(u => + u // + .combineWithParams([{ x: 1 }]) + ) + .fn(t => { + t.expect(t.params.x === 1); + }); + +g.test('combine_one,subcase') + .paramsSubcasesOnly(u => + u // + .combineWithParams([{ x: 1 }]) + ) + .fn(t => { + t.expect(t.params.x === 1); + }); + +g.test('filter') + .params(u => + u + .combineWithParams([ + { a: true, x: 1 }, // + { a: false, y: 2 }, + ]) + .filter(p => p.a) + ) + .fn(t => { + t.expect(t.params.a); + }); + +g.test('unless') + .params(u => + u + .combineWithParams([ + { a: true, x: 1 }, // + { a: false, y: 2 }, + ]) + .unless(p => p.a) + ) + .fn(t => { + t.expect(!t.params.a); + }); + +g.test('generator').fn(t0 => { + const g = makeTestGroupForUnitTesting(UnitTest); + + const ran: TestParams[] = []; + + g.test('generator') + .params(u => + u.combineWithParams({ + *[Symbol.iterator]() { + for (let x = 0; x < 3; ++x) { + for (let y = 0; y < 2; ++y) { + yield { x, y }; + } + } + }, + }) + ) + .fn(t => { + ran.push(t.params); + }); + + t0.expectCases(g, [ + { test: ['generator'], params: { x: 0, y: 0 } }, + { test: ['generator'], params: { x: 0, y: 1 } }, + { test: ['generator'], params: { x: 1, y: 0 } }, + { test: ['generator'], params: { x: 1, y: 1 } }, + { test: ['generator'], params: { x: 2, y: 0 } }, + { test: ['generator'], params: { x: 2, y: 1 } }, + ]); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts new file mode 100644 index 0000000000..040629355d --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts @@ -0,0 +1,207 @@ +export const description = ` +Test for "pp" preprocessor. +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { pp } from '../common/util/preprocessor.js'; + +import { UnitTest } from './unit_test.js'; + +class F extends UnitTest { + test(act: string, exp: string): void { + this.expect(act === exp, 'got: ' + act.replace('\n', '⏎')); + } +} + +export const g = makeTestGroup(F); + +g.test('empty').fn(t => { + t.test(pp``, ''); + t.test(pp`\n`, '\n'); + t.test(pp`\n\n`, '\n\n'); +}); + +g.test('plain').fn(t => { + t.test(pp`a`, 'a'); + t.test(pp`\na`, '\na'); + t.test(pp`\n\na`, '\n\na'); + t.test(pp`\na\n`, '\na\n'); + t.test(pp`a\n\n`, 'a\n\n'); +}); + +g.test('substitutions,1').fn(t => { + const act = pp`a ${3} b`; + const exp = 'a 3 b'; + t.test(act, exp); +}); + +g.test('substitutions,2').fn(t => { + const act = pp`a ${'x'}`; + const exp = 'a x'; + t.test(act, exp); +}); + +g.test('substitutions,3').fn(t => { + const act = pp`a ${'x'} b`; + const exp = 'a x b'; + t.test(act, exp); +}); + +g.test('substitutions,4').fn(t => { + const act = pp` +a +${pp._if(false)} +${'x'} +${pp._endif} +b`; + const exp = '\na\n\nb'; + t.test(act, exp); +}); + +g.test('if,true').fn(t => { + const act = pp` +a +${pp._if(true)}c${pp._endif} +d +`; + const exp = '\na\nc\nd\n'; + t.test(act, exp); +}); + +g.test('if,false').fn(t => { + const act = pp` +a +${pp._if(false)}c${pp._endif} +d +`; + const exp = '\na\n\nd\n'; + t.test(act, exp); +}); + +g.test('else,1').fn(t => { + const act = pp` +a +${pp._if(true)} +b +${pp._else} +c +${pp._endif} +d +`; + const exp = '\na\n\nb\n\nd\n'; + t.test(act, exp); +}); + +g.test('else,2').fn(t => { + const act = pp` +a +${pp._if(false)} +b +${pp._else} +c +${pp._endif} +d +`; + const exp = '\na\n\nc\n\nd\n'; + t.test(act, exp); +}); + +g.test('elif,1').fn(t => { + const act = pp` +a +${pp._if(false)} +b +${pp._elif(true)} +e +${pp._else} +c +${pp._endif} +d +`; + const exp = '\na\n\ne\n\nd\n'; + t.test(act, exp); +}); + +g.test('elif,2').fn(t => { + const act = pp` +a +${pp._if(true)} +b +${pp._elif(true)} +e +${pp._else} +c +${pp._endif} +d +`; + const exp = '\na\n\nb\n\nd\n'; + t.test(act, exp); +}); + +g.test('nested,1').fn(t => { + const act = pp` +a +${pp._if(false)} +b +${pp.__if(true)} +e +${pp.__endif} +c +${pp._endif} +d +`; + const exp = '\na\n\nd\n'; + t.test(act, exp); +}); + +g.test('nested,2').fn(t => { + const act = pp` +a +${pp._if(false)} +b +${pp._else} +h +${pp.__if(false)} +e +${pp.__elif(true)} +f +${pp.__else} +g +${pp.__endif} +c +${pp._endif} +d +`; + const exp = '\na\n\nh\n\nf\n\nc\n\nd\n'; + t.test(act, exp); +}); + +g.test('errors,pass').fn(() => { + pp`${pp._if(true)}${pp._endif}`; + pp`${pp._if(true)}${pp._else}${pp._endif}`; + pp`${pp._if(true)}${pp.__if(true)}${pp.__endif}${pp._endif}`; +}); + +g.test('errors,fail').fn(t => { + const e = (fn: () => void) => t.shouldThrow('Error', fn); + e(() => pp`${pp._if(true)}`); + e(() => pp`${pp._elif(true)}`); + e(() => pp`${pp._else}`); + e(() => pp`${pp._endif}`); + e(() => pp`${pp.__if(true)}`); + e(() => pp`${pp.__elif(true)}`); + e(() => pp`${pp.__else}`); + e(() => pp`${pp.__endif}`); + + e(() => pp`${pp._if(true)}${pp._elif(true)}`); + e(() => pp`${pp._if(true)}${pp._elif(true)}${pp._else}`); + e(() => pp`${pp._if(true)}${pp._else}`); + e(() => pp`${pp._else}${pp._endif}`); + + e(() => pp`${pp._if(true)}${pp.__endif}`); + e(() => pp`${pp.__if(true)}${pp.__endif}`); + e(() => pp`${pp.__if(true)}${pp._endif}`); + + e(() => pp`${pp._if(true)}${pp._else}${pp._else}${pp._endif}`); + e(() => pp`${pp._if(true)}${pp.__if(true)}${pp.__else}${pp.__else}${pp.__endif}${pp._endif}`); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts new file mode 100644 index 0000000000..520af9e663 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts @@ -0,0 +1,133 @@ +export const description = ` +Tests for TestQuery comparison +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { compareQueries, Ordering } from '../common/internal/query/compare.js'; +import { + TestQuery, + TestQuerySingleCase, + TestQueryMultiFile, + TestQueryMultiTest, + TestQueryMultiCase, +} from '../common/internal/query/query.js'; + +import { UnitTest } from './unit_test.js'; + +class F extends UnitTest { + expectQ(a: TestQuery, exp: '<' | '=' | '>' | '!', b: TestQuery) { + const [expOrdering, expInvOrdering] = + exp === '<' + ? [Ordering.StrictSubset, Ordering.StrictSuperset] + : exp === '=' + ? [Ordering.Equal, Ordering.Equal] + : exp === '>' + ? [Ordering.StrictSuperset, Ordering.StrictSubset] + : [Ordering.Unordered, Ordering.Unordered]; + { + const act = compareQueries(a, b); + this.expect(act === expOrdering, `${a} ${b} got ${act}, exp ${expOrdering}`); + } + { + const act = compareQueries(a, b); + this.expect(act === expOrdering, `${b} ${a} got ${act}, exp ${expInvOrdering}`); + } + } + + expectWellOrdered(...qs: TestQuery[]) { + for (let i = 0; i < qs.length; ++i) { + this.expectQ(qs[i], '=', qs[i]); + for (let j = i + 1; j < qs.length; ++j) { + this.expectQ(qs[i], '>', qs[j]); + } + } + } + + expectUnordered(...qs: TestQuery[]) { + for (let i = 0; i < qs.length; ++i) { + this.expectQ(qs[i], '=', qs[i]); + for (let j = i + 1; j < qs.length; ++j) { + this.expectQ(qs[i], '!', qs[j]); + } + } + } +} + +export const g = makeTestGroup(F); + +// suite:* > suite:a,* > suite:a,b,* > suite:a,b:* +// suite:a,b:* > suite:a,b:c,* > suite:a,b:c,d,* > suite:a,b:c,d:* +// suite:a,b:c,d:* > suite:a,b:c,d:x=1;* > suite:a,b:c,d:x=1;y=2;* > suite:a,b:c,d:x=1;y=2 +// suite:a;* (unordered) suite:b;* +g.test('well_ordered').fn(t => { + t.expectWellOrdered( + new TestQueryMultiFile('suite', []), + new TestQueryMultiFile('suite', ['a']), + new TestQueryMultiFile('suite', ['a', 'b']), + new TestQueryMultiTest('suite', ['a', 'b'], []), + new TestQueryMultiTest('suite', ['a', 'b'], ['c']), + new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']), + new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], {}), + new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1 }), + new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }), + new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }) + ); + t.expectWellOrdered( + new TestQueryMultiFile('suite', []), + new TestQueryMultiFile('suite', ['a']), + new TestQueryMultiFile('suite', ['a', 'b']), + new TestQueryMultiTest('suite', ['a', 'b'], []), + new TestQueryMultiTest('suite', ['a', 'b'], ['c']), + new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']), + new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], {}), + new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], {}) + ); +}); + +g.test('unordered').fn(t => { + t.expectUnordered( + new TestQueryMultiFile('suite', ['a']), // + new TestQueryMultiFile('suite', ['x']) + ); + t.expectUnordered( + new TestQueryMultiFile('suite', ['a', 'b']), + new TestQueryMultiFile('suite', ['a', 'x']) + ); + t.expectUnordered( + new TestQueryMultiTest('suite', ['a', 'b'], ['c']), + new TestQueryMultiTest('suite', ['a', 'b'], ['x']), + new TestQueryMultiTest('suite', ['a'], []), + new TestQueryMultiTest('suite', ['a', 'x'], []) + ); + t.expectUnordered( + new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']), + new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'x']), + new TestQueryMultiTest('suite', ['a'], []), + new TestQueryMultiTest('suite', ['a', 'x'], []) + ); + t.expectUnordered( + new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']), + new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'x']), + new TestQueryMultiTest('suite', ['a'], []), + new TestQueryMultiTest('suite', ['a', 'x'], ['c']) + ); + t.expectUnordered( + new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1 }), + new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 9 }), + new TestQueryMultiCase('suite', ['a', 'b'], ['c'], { x: 9 }) + ); + t.expectUnordered( + new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }), + new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 8 }), + new TestQueryMultiCase('suite', ['a', 'b'], ['c'], { x: 1, y: 8 }) + ); + t.expectUnordered( + new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }), + new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 8 }), + new TestQuerySingleCase('suite', ['a', 'b'], ['c'], { x: 1, y: 8 }) + ); + t.expectUnordered( + new TestQuerySingleCase('suite1', ['bar', 'buzz', 'buzz'], ['zap'], {}), + new TestQueryMultiTest('suite1', ['bar'], []) + ); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts new file mode 100644 index 0000000000..040acd1b87 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts @@ -0,0 +1,268 @@ +export const description = ` +Unit tests for TestQuery strings. +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { compareQueries, Ordering } from '../common/internal/query/compare.js'; +import { + TestQuery, + TestQuerySingleCase, + TestQueryMultiCase, + TestQueryMultiTest, + TestQueryMultiFile, + relativeQueryString, +} from '../common/internal/query/query.js'; + +import { UnitTest } from './unit_test.js'; + +class T extends UnitTest { + expectQueryString(q: TestQuery, exp: string): void { + const s = q.toString(); + this.expect(s === exp, `got ${s} expected ${exp}`); + } + + expectRelativeQueryString(parent: TestQuery, child: TestQuery, exp: string): void { + const s = relativeQueryString(parent, child); + this.expect(s === exp, `got ${s} expected ${exp}`); + + if (compareQueries(parent, child) !== Ordering.Equal) { + // Test in reverse + this.shouldThrow('Error', () => { + relativeQueryString(child, parent); + }); + } + } +} + +export const g = makeTestGroup(T); + +g.test('stringifyQuery,single_case').fn(t => { + t.expectQueryString( + new TestQuerySingleCase('a', ['b_1', '2_c'], ['d_3', '4_e'], { + f: 'g', + _pri1: 0, + x: 3, + _pri2: 1, + }), + 'a:b_1,2_c:d_3,4_e:f="g";x=3' + ); +}); + +g.test('stringifyQuery,single_case,json').fn(t => { + t.expectQueryString( + new TestQuerySingleCase('a', ['b_1', '2_c'], ['d_3', '4_e'], { + f: 'g', + x: { p: 2, q: 'Q' }, + }), + 'a:b_1,2_c:d_3,4_e:f="g";x={"p":2,"q":"Q"}' + ); +}); + +g.test('stringifyQuery,multi_case').fn(t => { + t.expectQueryString( + new TestQueryMultiCase('a', ['b_1', '2_c'], ['d_3', '4_e'], { + f: 'g', + _pri1: 0, + a: 3, + _pri2: 1, + }), + 'a:b_1,2_c:d_3,4_e:f="g";a=3;*' + ); + + t.expectQueryString( + new TestQueryMultiCase('a', ['b_1', '2_c'], ['d_3', '4_e'], {}), + 'a:b_1,2_c:d_3,4_e:*' + ); +}); + +g.test('stringifyQuery,multi_test').fn(t => { + t.expectQueryString( + new TestQueryMultiTest('a', ['b_1', '2_c'], ['d_3', '4_e']), + 'a:b_1,2_c:d_3,4_e,*' + ); + + t.expectQueryString( + new TestQueryMultiTest('a', ['b_1', '2_c'], []), // + 'a:b_1,2_c:*' + ); +}); + +g.test('stringifyQuery,multi_file').fn(t => { + t.expectQueryString( + new TestQueryMultiFile('a', ['b_1', '2_c']), // + 'a:b_1,2_c,*' + ); + + t.expectQueryString( + new TestQueryMultiFile('a', []), // + 'a:*' + ); +}); + +g.test('relativeQueryString,equal_or_child').fn(t => { + // Depth difference = 0 + t.expectRelativeQueryString( + new TestQueryMultiFile('a', []), // + new TestQueryMultiFile('a', []), // + '' + ); + t.expectRelativeQueryString( + new TestQueryMultiFile('a', ['b', 'c']), // + new TestQueryMultiFile('a', ['b', 'c']), // + '' + ); + t.expectRelativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), // + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), // + '' + ); + t.expectRelativeQueryString( + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), // + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), // + '' + ); + t.expectRelativeQueryString( + new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + '' + ); + + // Depth difference = 1 + t.expectRelativeQueryString( + new TestQueryMultiFile('a', []), // + new TestQueryMultiFile('a', ['b']), // + ':b,*' + ); + t.expectRelativeQueryString( + new TestQueryMultiFile('a', ['b']), // + new TestQueryMultiFile('a', ['b', 'c']), // + ',c,*' + ); + t.expectRelativeQueryString( + new TestQueryMultiFile('a', ['b', 'c']), // + new TestQueryMultiTest('a', ['b', 'c'], []), // + ':*' + ); + t.expectRelativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], []), // + new TestQueryMultiTest('a', ['b', 'c'], ['d']), // + ':d,*' + ); + t.expectRelativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], ['d']), // + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), // + ',e,*' + ); + t.expectRelativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), // + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), // + ':*' + ); + t.expectRelativeQueryString( + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), // + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), // + ':f=0;*' + ); + t.expectRelativeQueryString( + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), // + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + ';g=1;*' + ); + t.expectRelativeQueryString( + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + '' + ); + + // Depth difference = 2 + t.expectRelativeQueryString( + new TestQueryMultiFile('a', []), // + new TestQueryMultiFile('a', ['b', 'c']), // + ':b,c,*' + ); + t.expectRelativeQueryString( + new TestQueryMultiFile('a', ['b', 'c']), // + new TestQueryMultiTest('a', ['b', 'c'], ['d']), // + ':d,*' + ); + t.expectRelativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], ['d']), // + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), // + ',e:*' + ); + t.expectRelativeQueryString( + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), // + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + ':f=0;g=1;*' + ); + t.expectRelativeQueryString( + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1, h: 2 }), // + ';h=2' + ); + // Depth difference = 2 + t.expectRelativeQueryString( + new TestQueryMultiFile('a', ['b']), // + new TestQueryMultiTest('a', ['b', 'c'], []), // + ',c:*' + ); + t.expectRelativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], []), // + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), // + ':d,e,*' + ); + t.expectRelativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), // + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), // + ':f=0;*' + ); + t.expectRelativeQueryString( + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), // + new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + ';g=1' + ); + + // Depth difference = 4 + t.expectRelativeQueryString( + new TestQueryMultiFile('a', []), // + new TestQueryMultiTest('a', ['b', 'c'], ['d']), // + ':b,c:d,*' + ); + t.expectRelativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], ['d']), // + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + ',e:f=0;g=1;*' + ); + // Depth difference = 4 + t.expectRelativeQueryString( + new TestQueryMultiFile('a', ['b']), // + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), // + ',c:d,e,*' + ); + t.expectRelativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), // + new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), // + ':f=0;g=1' + ); +}); + +g.test('relativeQueryString,unrelated').fn(t => { + t.shouldThrow('Error', () => { + relativeQueryString( + new TestQueryMultiFile('a', ['b', 'x']), // + new TestQueryMultiFile('a', ['b', 'c']) // + ); + }); + t.shouldThrow('Error', () => { + relativeQueryString( + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'x']), // + new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']) // + ); + }); + t.shouldThrow('Error', () => { + relativeQueryString( + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), // + new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 1 }) // + ); + }); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts new file mode 100644 index 0000000000..73bfcbef9f --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts @@ -0,0 +1,259 @@ +export const description = `Unit tests for data cache serialization`; + +import { getIsBuildingDataCache, setIsBuildingDataCache } from '../common/framework/data_cache.js'; +import { makeTestGroup } from '../common/internal/test_group.js'; +import { objectEquals } from '../common/util/util.js'; +import { + deserializeExpectation, + serializeExpectation, +} from '../webgpu/shader/execution/expression/case_cache.js'; +import { + anyOf, + deserializeComparator, + SerializedComparator, + skipUndefined, +} from '../webgpu/util/compare.js'; +import { kValue } from '../webgpu/util/constants.js'; +import { + bool, + deserializeValue, + f16, + f32, + i16, + i32, + i8, + serializeValue, + u16, + u32, + u8, + vec2, + vec3, + vec4, +} from '../webgpu/util/conversion.js'; +import { + deserializeF32Interval, + serializeF32Interval, + toF32Interval, +} from '../webgpu/util/f32_interval.js'; + +import { UnitTest } from './unit_test.js'; + +export const g = makeTestGroup(UnitTest); + +g.test('value').fn(t => { + for (const value of [ + u32(kValue.u32.min + 0), + u32(kValue.u32.min + 1), + u32(kValue.u32.min + 2), + u32(kValue.u32.max - 2), + u32(kValue.u32.max - 1), + u32(kValue.u32.max - 0), + + u16(kValue.u16.min + 0), + u16(kValue.u16.min + 1), + u16(kValue.u16.min + 2), + u16(kValue.u16.max - 2), + u16(kValue.u16.max - 1), + u16(kValue.u16.max - 0), + + u8(kValue.u8.min + 0), + u8(kValue.u8.min + 1), + u8(kValue.u8.min + 2), + u8(kValue.u8.max - 2), + u8(kValue.u8.max - 1), + u8(kValue.u8.max - 0), + + i32(kValue.i32.negative.min + 0), + i32(kValue.i32.negative.min + 1), + i32(kValue.i32.negative.min + 2), + i32(kValue.i32.negative.max - 2), + i32(kValue.i32.negative.max - 1), + i32(kValue.i32.positive.min - 0), + i32(kValue.i32.positive.min + 1), + i32(kValue.i32.positive.min + 2), + i32(kValue.i32.positive.max - 2), + i32(kValue.i32.positive.max - 1), + i32(kValue.i32.positive.max - 0), + + i16(kValue.i16.negative.min + 0), + i16(kValue.i16.negative.min + 1), + i16(kValue.i16.negative.min + 2), + i16(kValue.i16.negative.max - 2), + i16(kValue.i16.negative.max - 1), + i16(kValue.i16.positive.min + 0), + i16(kValue.i16.positive.min + 1), + i16(kValue.i16.positive.min + 2), + i16(kValue.i16.positive.max - 2), + i16(kValue.i16.positive.max - 1), + i16(kValue.i16.positive.max - 0), + + i8(kValue.i8.negative.min + 0), + i8(kValue.i8.negative.min + 1), + i8(kValue.i8.negative.min + 2), + i8(kValue.i8.negative.max - 2), + i8(kValue.i8.negative.max - 1), + i8(kValue.i8.positive.min + 0), + i8(kValue.i8.positive.min + 1), + i8(kValue.i8.positive.min + 2), + i8(kValue.i8.positive.max - 2), + i8(kValue.i8.positive.max - 1), + i8(kValue.i8.positive.max - 0), + + f32(0), + f32(-0), + f32(1), + f32(-1), + f32(0.5), + f32(-0.5), + f32(kValue.f32.positive.max), + f32(kValue.f32.positive.min), + f32(kValue.f32.subnormal.positive.max), + f32(kValue.f32.subnormal.positive.min), + f32(kValue.f32.subnormal.negative.max), + f32(kValue.f32.subnormal.negative.min), + f32(kValue.f32.infinity.positive), + f32(kValue.f32.infinity.negative), + + f16(0), + f16(-0), + f16(1), + f16(-1), + f16(0.5), + f16(-0.5), + f16(kValue.f32.positive.max), + f16(kValue.f32.positive.min), + f16(kValue.f32.subnormal.positive.max), + f16(kValue.f32.subnormal.positive.min), + f16(kValue.f32.subnormal.negative.max), + f16(kValue.f32.subnormal.negative.min), + f16(kValue.f32.infinity.positive), + f16(kValue.f32.infinity.negative), + + bool(true), + bool(false), + + vec2(f32(1), f32(2)), + vec3(u32(1), u32(2), u32(3)), + vec4(bool(false), bool(true), bool(false), bool(true)), + ]) { + const serialized = serializeValue(value); + const deserialized = deserializeValue(serialized); + t.expect( + objectEquals(value, deserialized), + `value ${value} -> serialize -> deserialize -> ${deserialized}` + ); + } +}); + +g.test('f32_interval').fn(t => { + for (const interval of [ + toF32Interval(0), + toF32Interval(-0), + toF32Interval(1), + toF32Interval(-1), + toF32Interval(0.5), + toF32Interval(-0.5), + toF32Interval(kValue.f32.positive.max), + toF32Interval(kValue.f32.positive.min), + toF32Interval(kValue.f32.subnormal.positive.max), + toF32Interval(kValue.f32.subnormal.positive.min), + toF32Interval(kValue.f32.subnormal.negative.max), + toF32Interval(kValue.f32.subnormal.negative.min), + toF32Interval(kValue.f32.infinity.positive), + toF32Interval(kValue.f32.infinity.negative), + + toF32Interval([-0, 0]), + toF32Interval([-1, 1]), + toF32Interval([-0.5, 0.5]), + toF32Interval([kValue.f32.positive.min, kValue.f32.positive.max]), + toF32Interval([kValue.f32.subnormal.positive.min, kValue.f32.subnormal.positive.max]), + toF32Interval([kValue.f32.subnormal.negative.min, kValue.f32.subnormal.negative.max]), + toF32Interval([kValue.f32.infinity.negative, kValue.f32.infinity.positive]), + ]) { + const serialized = serializeF32Interval(interval); + const deserialized = deserializeF32Interval(serialized); + t.expect( + objectEquals(interval, deserialized), + `interval ${interval} -> serialize -> deserialize -> ${deserialized}` + ); + } +}); + +g.test('expression_expectation').fn(t => { + for (const expectation of [ + // Value + f32(123), + vec2(f32(1), f32(2)), + // Interval + toF32Interval([-0.5, 0.5]), + toF32Interval([kValue.f32.positive.min, kValue.f32.positive.max]), + // Intervals + [toF32Interval([-8.0, 0.5]), toF32Interval([2.0, 4.0])], + ]) { + const serialized = serializeExpectation(expectation); + const deserialized = deserializeExpectation(serialized); + t.expect( + objectEquals(expectation, deserialized), + `expectation ${expectation} -> serialize -> deserialize -> ${deserialized}` + ); + } +}); + +/** + * Temporarily enabled building of the data cache. + * Required for Comparators to serialize. + */ +function enableBuildingDataCache(f: () => void) { + const wasBuildingDataCache = getIsBuildingDataCache(); + setIsBuildingDataCache(true); + f(); + setIsBuildingDataCache(wasBuildingDataCache); +} + +g.test('anyOf').fn(t => { + enableBuildingDataCache(() => { + for (const c of [ + { + comparator: anyOf(i32(123)), + testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)], + }, + ]) { + const serialized = c.comparator as SerializedComparator; + const deserialized = deserializeComparator(serialized); + for (const val of c.testCases) { + const got = deserialized(val); + const expect = c.comparator(val); + t.expect( + got.matched === expect.matched, + `comparator(${val}): got: ${expect.matched}, expect: ${got.matched}` + ); + } + } + }); +}); + +g.test('skipUndefined').fn(t => { + enableBuildingDataCache(() => { + for (const c of [ + { + comparator: skipUndefined(i32(123)), + testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)], + }, + { + comparator: skipUndefined(undefined), + testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)], + }, + ]) { + const serialized = c.comparator as SerializedComparator; + const deserialized = deserializeComparator(serialized); + for (const val of c.testCases) { + const got = deserialized(val); + const expect = c.comparator(val); + t.expect( + got.matched === expect.matched, + `comparator(${val}): got: ${expect.matched}, expect: ${got.matched}` + ); + } + } + }); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts new file mode 100644 index 0000000000..cc6dce72fb --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts @@ -0,0 +1,351 @@ +export const description = ` +Unit tests for TestGroup. +`; + +import { Fixture } from '../common/framework/fixture.js'; +import { makeTestGroup } from '../common/framework/test_group.js'; +import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js'; +import { assert } from '../common/util/util.js'; + +import { TestGroupTest } from './test_group_test.js'; +import { UnitTest } from './unit_test.js'; + +export const g = makeTestGroup(TestGroupTest); + +g.test('UnitTest_fixture').fn(async t0 => { + let seen = 0; + function count(t: Fixture): void { + seen++; + } + + const g = makeTestGroupForUnitTesting(UnitTest); + + g.test('test').fn(count); + g.test('testp') + .paramsSimple([{ a: 1 }]) + .fn(count); + + await t0.run(g); + t0.expect(seen === 2); +}); + +g.test('custom_fixture').fn(async t0 => { + let seen = 0; + class Counter extends UnitTest { + count(): void { + seen++; + } + } + + const g = makeTestGroupForUnitTesting(Counter); + + g.test('test').fn(t => { + t.count(); + }); + g.test('testp') + .paramsSimple([{ a: 1 }]) + .fn(t => { + t.count(); + }); + + await t0.run(g); + t0.expect(seen === 2); +}); + +g.test('stack').fn(async t0 => { + const g = makeTestGroupForUnitTesting(UnitTest); + + const doNestedThrow1 = () => { + throw new Error('goodbye'); + }; + + const doNestedThrow2 = () => doNestedThrow1(); + + g.test('fail').fn(t => { + t.fail(); + }); + g.test('throw').fn(t => { + throw new Error('hello'); + }); + g.test('throw_nested').fn(t => { + doNestedThrow2(); + }); + + const res = await t0.run(g); + + const search = /unittests[/\\]test_group\.spec\.[tj]s/; + t0.expect(res.size > 0); + for (const { logs } of res.values()) { + assert(logs !== undefined, 'expected logs'); + t0.expect(logs.some(l => search.test(l.toJSON()))); + t0.expect(search.test(logs[logs.length - 1].toJSON())); + } +}); + +g.test('no_fn').fn(t => { + const g = makeTestGroupForUnitTesting(UnitTest); + + g.test('missing'); + + t.shouldThrow('Error', () => { + g.validate(); + }); +}); + +g.test('duplicate_test_name').fn(t => { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('abc').fn(() => {}); + + t.shouldThrow('Error', () => { + g.test('abc').fn(() => {}); + }); +}); + +g.test('duplicate_test_params,none').fn(() => { + { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('abc') + .paramsSimple([]) + .fn(() => {}); + g.validate(); + } + + { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('abc').fn(() => {}); + g.validate(); + } + + { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('abc') + .paramsSimple([ + { a: 1 }, // + ]) + .fn(() => {}); + g.validate(); + } +}); + +g.test('duplicate_test_params,basic').fn(t => { + { + const g = makeTestGroupForUnitTesting(UnitTest); + const builder = g.test('abc'); + t.shouldThrow('Error', () => { + builder.paramsSimple([ + { a: 1 }, // + { a: 1 }, + ]); + g.validate(); + }); + } + { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('abc') + .params(u => + u.expandWithParams(() => [ + { a: 1 }, // + { a: 1 }, + ]) + ) + .fn(() => {}); + t.shouldThrow('Error', () => { + g.validate(); + }); + } + { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('abc') + .paramsSimple([ + { a: 1, b: 3 }, // + { b: 3, a: 1 }, + ]) + .fn(() => {}); + t.shouldThrow('Error', () => { + g.validate(); + }); + } +}); + +g.test('duplicate_test_params,with_different_private_params').fn(t => { + { + const g = makeTestGroupForUnitTesting(UnitTest); + const builder = g.test('abc'); + t.shouldThrow('Error', () => { + builder.paramsSimple([ + { a: 1, _b: 1 }, // + { a: 1, _b: 2 }, + ]); + }); + } + { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('abc') + .params(u => + u.expandWithParams(() => [ + { a: 1, _b: 1 }, // + { a: 1, _b: 2 }, + ]) + ) + .fn(() => {}); + t.shouldThrow('Error', () => { + g.validate(); + }); + } +}); + +g.test('invalid_test_name').fn(t => { + const g = makeTestGroupForUnitTesting(UnitTest); + + const badChars = Array.from('"`~@#$+=\\|!^&*[]<>{}-\'. '); + for (const char of badChars) { + const name = 'a' + char + 'b'; + t.shouldThrow( + 'Error', + () => { + g.test(name).fn(() => {}); + }, + name + ); + } +}); + +g.test('param_value,valid').fn(() => { + const g = makeTestGroup(UnitTest); + g.test('a').paramsSimple([{ x: JSON.stringify({ a: 1, b: 2 }) }]); +}); + +g.test('param_value,invalid').fn(t => { + for (const badChar of ';=*') { + const g = makeTestGroupForUnitTesting(UnitTest); + const builder = g.test('a'); + t.shouldThrow('Error', () => { + builder.paramsSimple([{ badChar }]); + }); + } +}); + +g.test('subcases').fn(async t0 => { + const g = makeTestGroupForUnitTesting(UnitTest); + g.test('a') + .paramsSubcasesOnly(u => + u // + .combineWithParams([{ a: 1 }]) + ) + .fn(t => { + t.expect(t.params.a === 1, 'a must be 1'); + }); + + function* gen({ a, b }: { a?: number; b?: number }) { + if (b === 2) { + yield { ret: 2 }; + } else if (a === 1) { + yield { ret: 1 }; + } else { + yield { ret: -1 }; + } + } + g.test('b') + .params(u => + u + .combineWithParams([{ a: 1 }, { b: 2 }]) + .beginSubcases() + .expandWithParams(gen) + ) + .fn(t => { + const { a, b, ret } = t.params; + t.expect((a === 1 && ret === 1) || (b === 2 && ret === 2)); + }); + + const result = await t0.run(g); + t0.expect(Array.from(result.values()).every(v => v.status === 'pass')); +}); + +g.test('exceptions') + .params(u => + u + .combine('useSubcases', [false, true]) // + .combine('useDOMException', [false, true]) + ) + .fn(async t0 => { + const { useSubcases, useDOMException } = t0.params; + const g = makeTestGroupForUnitTesting(UnitTest); + + const b1 = g.test('a'); + let b2; + if (useSubcases) { + b2 = b1.paramsSubcasesOnly(u => u); + } else { + b2 = b1.params(u => u); + } + b2.fn(t => { + if (useDOMException) { + throw new DOMException('Message!', 'Name!'); + } else { + throw new Error('Message!'); + } + }); + + const result = await t0.run(g); + const values = Array.from(result.values()); + t0.expect(values.length === 1); + t0.expect(values[0].status === 'fail'); + }); + +g.test('throws').fn(async t0 => { + const g = makeTestGroupForUnitTesting(UnitTest); + + g.test('a').fn(t => { + throw new Error(); + }); + + const result = await t0.run(g); + const values = Array.from(result.values()); + t0.expect(values.length === 1); + t0.expect(values[0].status === 'fail'); +}); + +g.test('shouldThrow').fn(async t0 => { + t0.shouldThrow('TypeError', () => { + throw new TypeError(); + }); + + const g = makeTestGroupForUnitTesting(UnitTest); + + g.test('a').fn(t => { + t.shouldThrow('Error', () => { + throw new TypeError(); + }); + }); + + const result = await t0.run(g); + const values = Array.from(result.values()); + t0.expect(values.length === 1); + t0.expect(values[0].status === 'fail'); +}); + +g.test('shouldReject').fn(async t0 => { + t0.shouldReject( + 'TypeError', + (async () => { + throw new TypeError(); + })() + ); + + const g = makeTestGroupForUnitTesting(UnitTest); + + g.test('a').fn(async t => { + t.shouldReject( + 'Error', + (async () => { + throw new TypeError(); + })() + ); + }); + + const result = await t0.run(g); + // Fails even though shouldReject doesn't fail until after the test function ends + const values = Array.from(result.values()); + t0.expect(values.length === 1); + t0.expect(values[0].status === 'fail'); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts b/dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts new file mode 100644 index 0000000000..2a939ba052 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts @@ -0,0 +1,34 @@ +import { Logger, LogResults } from '../common/internal/logging/logger.js'; +import { TestQuerySingleCase } from '../common/internal/query/query.js'; +import { IterableTestGroup, TestCaseID } from '../common/internal/test_group.js'; +import { objectEquals } from '../common/util/util.js'; + +import { UnitTest } from './unit_test.js'; + +export class TestGroupTest extends UnitTest { + async run(g: IterableTestGroup): Promise<LogResults> { + const logger = new Logger({ overrideDebugMode: true }); + for (const t of g.iterate()) { + for (const rc of t.iterate()) { + const query = new TestQuerySingleCase('xx', ['yy'], rc.id.test, rc.id.params); + const [rec] = logger.record(query.toString()); + await rc.run(rec, query, []); + } + } + return logger.results; + } + + expectCases(g: IterableTestGroup, cases: TestCaseID[]): void { + const gcases = []; + for (const t of g.iterate()) { + gcases.push(...Array.from(t.iterate(), c => c.id)); + } + this.expect( + objectEquals(gcases, cases), + `expected + ${JSON.stringify(cases)} +got + ${JSON.stringify(gcases)}` + ); + } +} diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts new file mode 100644 index 0000000000..4a744c49e9 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts @@ -0,0 +1,143 @@ +export const description = ` +Tests for TestQuery +`; + +import { makeTestGroup } from '../common/framework/test_group.js'; +import { parseQuery } from '../common/internal/query/parseQuery.js'; +import { + TestQueryMultiFile, + TestQueryMultiTest, + TestQueryMultiCase, + TestQuerySingleCase, + TestQuery, +} from '../common/internal/query/query.js'; + +import { UnitTest } from './unit_test.js'; + +class F extends UnitTest { + expectToString(q: TestQuery, exp: string) { + this.expect(q.toString() === exp); + } + + expectQueriesEqual(q1: TestQuery, q2: TestQuery) { + this.expect(q1.level === q2.level); + + if (q1.level >= 1) { + this.expect(q1.isMultiFile === q2.isMultiFile); + this.expect(q1.suite === q2.suite); + this.expect(q1.filePathParts.length === q2.filePathParts.length); + for (let i = 0; i < q1.filePathParts.length; i++) { + this.expect(q1.filePathParts[i] === q2.filePathParts[i]); + } + } + + if (q1.level >= 2) { + const p1 = q1 as TestQueryMultiTest; + const p2 = q2 as TestQueryMultiTest; + + this.expect(p1.isMultiTest === p2.isMultiTest); + this.expect(p1.testPathParts.length === p2.testPathParts.length); + for (let i = 0; i < p1.testPathParts.length; i++) { + this.expect(p1.testPathParts[i] === p2.testPathParts[i]); + } + } + + if (q1.level >= 3) { + const p1 = q1 as TestQueryMultiCase; + const p2 = q2 as TestQueryMultiCase; + + this.expect(p1.isMultiCase === p2.isMultiCase); + this.expect(Object.keys(p1.params).length === Object.keys(p2.params).length); + for (const key of Object.keys(p1.params)) { + this.expect(key in p2.params); + const v1 = p1.params[key]; + const v2 = p2.params[key]; + this.expect( + v1 === v2 || + (typeof v1 === 'number' && isNaN(v1)) === (typeof v2 === 'number' && isNaN(v2)) + ); + this.expect(Object.is(v1, -0) === Object.is(v2, -0)); + } + } + } + + expectQueryParse(s: string, q: TestQuery) { + this.expectQueriesEqual(q, parseQuery(s)); + } +} + +export const g = makeTestGroup(F); + +g.test('constructor').fn(t => { + t.shouldThrow('Error', () => new TestQueryMultiTest('suite', [], [])); + + t.shouldThrow('Error', () => new TestQueryMultiCase('suite', ['a'], [], {})); + t.shouldThrow('Error', () => new TestQueryMultiCase('suite', [], ['c'], {})); + t.shouldThrow('Error', () => new TestQueryMultiCase('suite', [], [], {})); + + t.shouldThrow('Error', () => new TestQuerySingleCase('suite', ['a'], [], {})); + t.shouldThrow('Error', () => new TestQuerySingleCase('suite', [], ['c'], {})); + t.shouldThrow('Error', () => new TestQuerySingleCase('suite', [], [], {})); +}); + +g.test('toString').fn(t => { + t.expectToString(new TestQueryMultiFile('s', []), 's:*'); + t.expectToString(new TestQueryMultiFile('s', ['a']), 's:a,*'); + t.expectToString(new TestQueryMultiFile('s', ['a', 'b']), 's:a,b,*'); + t.expectToString(new TestQueryMultiTest('s', ['a', 'b'], []), 's:a,b:*'); + t.expectToString(new TestQueryMultiTest('s', ['a', 'b'], ['c']), 's:a,b:c,*'); + t.expectToString(new TestQueryMultiTest('s', ['a', 'b'], ['c', 'd']), 's:a,b:c,d,*'); + t.expectToString(new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], {}), 's:a,b:c,d:*'); + t.expectToString( + new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1 }), + 's:a,b:c,d:x=1;*' + ); + t.expectToString( + new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }), + 's:a,b:c,d:x=1;y=2;*' + ); + t.expectToString( + new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }), + 's:a,b:c,d:x=1;y=2' + ); + t.expectToString(new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], {}), 's:a,b:c,d:'); + + // Test handling of magic param value that convert to NaN/undefined/Infinity/etc. + t.expectToString(new TestQuerySingleCase('s', ['a'], ['b'], { c: NaN }), 's:a:b:c="_nan_"'); + t.expectToString( + new TestQuerySingleCase('s', ['a'], ['b'], { c: undefined }), + 's:a:b:c="_undef_"' + ); + t.expectToString(new TestQuerySingleCase('s', ['a'], ['b'], { c: -0 }), 's:a:b:c="_negzero_"'); +}); + +g.test('parseQuery').fn(t => { + t.expectQueryParse('s:*', new TestQueryMultiFile('s', [])); + t.expectQueryParse('s:a,*', new TestQueryMultiFile('s', ['a'])); + t.expectQueryParse('s:a,b,*', new TestQueryMultiFile('s', ['a', 'b'])); + t.expectQueryParse('s:a,b:*', new TestQueryMultiTest('s', ['a', 'b'], [])); + t.expectQueryParse('s:a,b:c,*', new TestQueryMultiTest('s', ['a', 'b'], ['c'])); + t.expectQueryParse('s:a,b:c,d,*', new TestQueryMultiTest('s', ['a', 'b'], ['c', 'd'])); + t.expectQueryParse('s:a,b:c,d:*', new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], {})); + t.expectQueryParse( + 's:a,b:c,d:x=1;*', + new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1 }) + ); + t.expectQueryParse( + 's:a,b:c,d:x=1;y=2;*', + new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }) + ); + t.expectQueryParse( + 's:a,b:c,d:x=1;y=2', + new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }) + ); + t.expectQueryParse('s:a,b:c,d:', new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], {})); + + // Test handling of magic param value that convert to NaN/undefined/Infinity/etc. + t.expectQueryParse('s:a:b:c="_nan_"', new TestQuerySingleCase('s', ['a'], ['b'], { c: NaN })); + t.expectQueryParse( + 's:a:b:c="_undef_"', + new TestQuerySingleCase('s', ['a'], ['b'], { c: undefined }) + ); + t.expectQueryParse('s:a:b:c="_negzero_"', new TestQuerySingleCase('s', ['a'], ['b'], { c: -0 })); +}); diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts b/dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts new file mode 100644 index 0000000000..876780e151 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts @@ -0,0 +1,3 @@ +import { Fixture } from '../common/framework/fixture.js'; + +export class UnitTest extends Fixture {} |