wgsl-test 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -16
- package/dist/index.d.ts +72 -9
- package/dist/index.js +258 -64
- package/dist/index.js.map +1 -1
- package/dist/weslBundle.d.ts +20 -0
- package/dist/weslBundle.js +12 -0
- package/package.json +10 -5
- package/src/CompileShader.ts +41 -24
- package/src/TestComputeShader.ts +19 -19
- package/src/TestDiscovery.ts +50 -0
- package/src/TestFragmentShader.ts +13 -10
- package/src/TestVirtualLib.ts +18 -0
- package/src/TestWesl.ts +183 -0
- package/src/VitestImport.ts +23 -0
- package/src/WebGPUTestSetup.ts +28 -13
- package/src/index.ts +3 -0
- package/src/test/BackendCheck.test.ts +12 -0
- package/src/test/TestDiscovery.test.ts +77 -0
- package/src/test/TestWesl.test.ts +111 -0
- package/src/test/fixtures/wesl_test_pkg/package.json +4 -0
- package/src/test/fixtures/wesl_test_pkg/shaders/expect_eq.wesl +9 -0
- package/src/test/fixtures/wesl_test_pkg/shaders/failing_expect.wesl +5 -0
- package/src/test/fixtures/wesl_test_pkg/shaders/failing_near.wesl +5 -0
- package/src/test/fixtures/wesl_test_pkg/shaders/failing_ulp.wesl +8 -0
- package/src/test/fixtures/wesl_test_pkg/shaders/multiple_tests.wesl +7 -0
- package/src/test/fixtures/wesl_test_pkg/shaders/no_tests.wesl +1 -0
- package/src/test/fixtures/wesl_test_pkg/shaders/passing_expect.wesl +5 -0
- package/src/test/fixtures/wesl_test_pkg/shaders/passing_near.wesl +5 -0
- package/src/test/fixtures/wesl_test_pkg/shaders/passing_ulp.wesl +12 -0
- package/src/test/fixtures/wesl_test_pkg/wesl.toml +3 -0
- package/src/test_lib.wesl +89 -0
package/src/WebGPUTestSetup.ts
CHANGED
|
@@ -1,30 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
export const isDeno = !!(globalThis as any).Deno;
|
|
2
2
|
|
|
3
3
|
let sharedGpu: GPU | undefined;
|
|
4
|
+
let sharedAdapter: GPUAdapter | undefined;
|
|
4
5
|
let sharedDevice: GPUDevice | undefined;
|
|
5
6
|
|
|
6
7
|
/** get or create shared GPU device for testing */
|
|
7
8
|
export async function getGPUDevice(): Promise<GPUDevice> {
|
|
8
9
|
if (!sharedDevice) {
|
|
9
|
-
const
|
|
10
|
-
const adapter = await gpu.requestAdapter();
|
|
11
|
-
if (!adapter) throw new Error("Failed to get GPU adapter");
|
|
10
|
+
const adapter = await getGPUAdapter();
|
|
12
11
|
sharedDevice = await adapter.requestDevice();
|
|
13
12
|
}
|
|
14
13
|
return sharedDevice;
|
|
15
14
|
}
|
|
16
15
|
|
|
16
|
+
/** get or create shared GPU object for testing */
|
|
17
|
+
export async function getGPU(): Promise<GPU> {
|
|
18
|
+
if (!sharedGpu) {
|
|
19
|
+
if (isDeno) {
|
|
20
|
+
sharedGpu = navigator.gpu;
|
|
21
|
+
} else {
|
|
22
|
+
const webgpu = await import("webgpu");
|
|
23
|
+
Object.assign(globalThis, webgpu.globals);
|
|
24
|
+
sharedGpu = webgpu.create([]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return sharedGpu;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** get or create shared GPU adapter for testing */
|
|
31
|
+
export async function getGPUAdapter(): Promise<GPUAdapter> {
|
|
32
|
+
if (!sharedAdapter) {
|
|
33
|
+
const gpu = await getGPU();
|
|
34
|
+
const adapter = await gpu.requestAdapter();
|
|
35
|
+
if (!adapter) throw new Error("Failed to get GPU adapter");
|
|
36
|
+
sharedAdapter = adapter;
|
|
37
|
+
}
|
|
38
|
+
return sharedAdapter;
|
|
39
|
+
}
|
|
40
|
+
|
|
17
41
|
/** destroy globally shared GPU test device */
|
|
18
42
|
export function destroySharedDevice(): void {
|
|
19
43
|
sharedDevice?.destroy();
|
|
20
44
|
sharedDevice = undefined;
|
|
21
45
|
}
|
|
22
|
-
|
|
23
|
-
/** initialize WebGPU for testing */
|
|
24
|
-
async function setupWebGPU(): Promise<GPU> {
|
|
25
|
-
if (!sharedGpu) {
|
|
26
|
-
Object.assign(globalThis, webgpu.globals);
|
|
27
|
-
sharedGpu = webgpu.create([]);
|
|
28
|
-
}
|
|
29
|
-
return sharedGpu;
|
|
30
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -26,7 +26,10 @@ export * from "./CompileShader.ts";
|
|
|
26
26
|
export * from "./ExampleImages.ts";
|
|
27
27
|
export * from "./ImageHelpers.ts";
|
|
28
28
|
export * from "./TestComputeShader.ts";
|
|
29
|
+
export * from "./TestDiscovery.ts";
|
|
29
30
|
export * from "./TestFragmentShader.ts";
|
|
31
|
+
export * from "./TestVirtualLib.ts";
|
|
32
|
+
export * from "./TestWesl.ts";
|
|
30
33
|
export * from "./WebGPUTestSetup.ts";
|
|
31
34
|
|
|
32
35
|
// Re-export module augmentation from vitest-image-snapshot for packaged builds
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { getGPUDevice, isDeno } from "../WebGPUTestSetup.ts";
|
|
3
|
+
|
|
4
|
+
test("check WebGPU backend", async () => {
|
|
5
|
+
const device = await getGPUDevice();
|
|
6
|
+
expect(device).toBeTruthy();
|
|
7
|
+
|
|
8
|
+
console.log(
|
|
9
|
+
"==> Backend:",
|
|
10
|
+
isDeno ? "wgpu (Deno native)" : "Dawn (Node webgpu package)",
|
|
11
|
+
);
|
|
12
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { parseSrcModule } from "wesl";
|
|
3
|
+
import { findTestFunctions } from "../TestDiscovery.ts";
|
|
4
|
+
|
|
5
|
+
function parse(src: string) {
|
|
6
|
+
return parseSrcModule({
|
|
7
|
+
modulePath: "test",
|
|
8
|
+
debugFilePath: "test.wesl",
|
|
9
|
+
src,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("finds single @test function", () => {
|
|
14
|
+
const ast = parse(`
|
|
15
|
+
@test fn myTest() { }
|
|
16
|
+
`);
|
|
17
|
+
const tests = findTestFunctions(ast);
|
|
18
|
+
expect(tests).toHaveLength(1);
|
|
19
|
+
expect(tests[0].name).toBe("myTest");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("finds multiple @test functions", () => {
|
|
23
|
+
const ast = parse(`
|
|
24
|
+
@test fn testOne() { }
|
|
25
|
+
fn helper() { }
|
|
26
|
+
@test fn testTwo() { }
|
|
27
|
+
`);
|
|
28
|
+
const tests = findTestFunctions(ast);
|
|
29
|
+
expect(tests).toHaveLength(2);
|
|
30
|
+
expect(tests.map(t => t.name)).toEqual(["testOne", "testTwo"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("ignores non-test functions", () => {
|
|
34
|
+
const ast = parse(`
|
|
35
|
+
fn notATest() { }
|
|
36
|
+
@compute @workgroup_size(1) fn compute() { }
|
|
37
|
+
`);
|
|
38
|
+
const tests = findTestFunctions(ast);
|
|
39
|
+
expect(tests).toHaveLength(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("preserves FnElem for access to returnType", () => {
|
|
43
|
+
const ast = parse(`
|
|
44
|
+
@test fn returnsFloat() -> f32 { return 1.0; }
|
|
45
|
+
`);
|
|
46
|
+
const tests = findTestFunctions(ast);
|
|
47
|
+
expect(tests).toHaveLength(1);
|
|
48
|
+
expect(tests[0].fn.returnType).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("handles @test with other attributes", () => {
|
|
52
|
+
const ast = parse(`
|
|
53
|
+
@test @must_use fn withMustUse() -> bool { return true; }
|
|
54
|
+
`);
|
|
55
|
+
const tests = findTestFunctions(ast);
|
|
56
|
+
expect(tests).toHaveLength(1);
|
|
57
|
+
expect(tests[0].name).toBe("withMustUse");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("extracts description from @test(description)", () => {
|
|
61
|
+
const ast = parse(`
|
|
62
|
+
@test(pythagorean_triple) fn lengthSq3() { }
|
|
63
|
+
`);
|
|
64
|
+
const tests = findTestFunctions(ast);
|
|
65
|
+
expect(tests).toHaveLength(1);
|
|
66
|
+
expect(tests[0].name).toBe("lengthSq3");
|
|
67
|
+
expect(tests[0].description).toBe("pythagorean_triple");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("description is undefined for plain @test", () => {
|
|
71
|
+
const ast = parse(`
|
|
72
|
+
@test fn simpleTest() { }
|
|
73
|
+
`);
|
|
74
|
+
const tests = findTestFunctions(ast);
|
|
75
|
+
expect(tests).toHaveLength(1);
|
|
76
|
+
expect(tests[0].description).toBeUndefined();
|
|
77
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { afterAll, beforeAll, expect, test } from "vitest";
|
|
3
|
+
import { expectWesl, runWesl } from "../TestWesl.ts";
|
|
4
|
+
import { destroySharedDevice, getGPUDevice } from "../WebGPUTestSetup.ts";
|
|
5
|
+
|
|
6
|
+
let device: GPUDevice;
|
|
7
|
+
const fixturesDir = new URL(
|
|
8
|
+
"./fixtures/wesl_test_pkg/shaders/",
|
|
9
|
+
import.meta.url,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
function loadFixture(name: string): string {
|
|
13
|
+
return readFileSync(new URL(name, fixturesDir), "utf-8");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
device = await getGPUDevice();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
destroySharedDevice();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("passing test with expect()", async () => {
|
|
25
|
+
const src = loadFixture("passing_expect.wesl");
|
|
26
|
+
const results = await runWesl({ device, src });
|
|
27
|
+
expect(results).toHaveLength(1);
|
|
28
|
+
expect(results[0].name).toBe("checkTrue");
|
|
29
|
+
expect(results[0].passed).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("failing test with expect()", async () => {
|
|
33
|
+
const src = loadFixture("failing_expect.wesl");
|
|
34
|
+
const results = await runWesl({ device, src });
|
|
35
|
+
expect(results).toHaveLength(1);
|
|
36
|
+
expect(results[0].passed).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("passing test with expectNear()", async () => {
|
|
40
|
+
const src = loadFixture("passing_near.wesl");
|
|
41
|
+
const results = await runWesl({ device, src });
|
|
42
|
+
expect(results).toHaveLength(1);
|
|
43
|
+
expect(results[0].passed).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("failing test with expectNear() reports actual and expected", async () => {
|
|
47
|
+
const src = loadFixture("failing_near.wesl");
|
|
48
|
+
const results = await runWesl({ device, src });
|
|
49
|
+
expect(results).toHaveLength(1);
|
|
50
|
+
expect(results[0].passed).toBe(false);
|
|
51
|
+
expect(results[0].actual?.[0]).toBeCloseTo(1.0);
|
|
52
|
+
expect(results[0].expected?.[0]).toBeCloseTo(2.0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("multiple @test functions in one module", async () => {
|
|
56
|
+
const src = loadFixture("multiple_tests.wesl");
|
|
57
|
+
const results = await runWesl({ device, src });
|
|
58
|
+
expect(results).toHaveLength(3);
|
|
59
|
+
expect(results[0].name).toBe("first");
|
|
60
|
+
expect(results[0].passed).toBe(true);
|
|
61
|
+
expect(results[1].name).toBe("second");
|
|
62
|
+
expect(results[1].passed).toBe(false);
|
|
63
|
+
expect(results[2].name).toBe("third");
|
|
64
|
+
expect(results[2].passed).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("no @test functions returns empty array", async () => {
|
|
68
|
+
const src = loadFixture("no_tests.wesl");
|
|
69
|
+
const results = await runWesl({ device, src });
|
|
70
|
+
expect(results).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("expectEq passes for equal values", async () => {
|
|
74
|
+
const src = loadFixture("expect_eq.wesl");
|
|
75
|
+
const results = await runWesl({ device, src });
|
|
76
|
+
const equalResult = results.find(r => r.name === "equalInts");
|
|
77
|
+
expect(equalResult?.passed).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("expectEq fails for different values", async () => {
|
|
81
|
+
const src = loadFixture("expect_eq.wesl");
|
|
82
|
+
const results = await runWesl({ device, src });
|
|
83
|
+
const unequalResult = results.find(r => r.name === "unequalInts");
|
|
84
|
+
expect(unequalResult?.passed).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("expectWesl passes when all tests pass", async () => {
|
|
88
|
+
const src = loadFixture("passing_expect.wesl");
|
|
89
|
+
await expectWesl({ device, src }); // should not throw
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("expectWesl throws on failure with details", async () => {
|
|
93
|
+
const src = loadFixture("failing_near.wesl");
|
|
94
|
+
await expect(expectWesl({ device, src })).rejects.toThrow(
|
|
95
|
+
"WESL tests failed",
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("passing test with expectUlp()", async () => {
|
|
100
|
+
const src = loadFixture("passing_ulp.wesl");
|
|
101
|
+
const results = await runWesl({ device, src });
|
|
102
|
+
expect(results).toHaveLength(2);
|
|
103
|
+
expect(results.every(r => r.passed)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("failing test with expectUlp()", async () => {
|
|
107
|
+
const src = loadFixture("failing_ulp.wesl");
|
|
108
|
+
const results = await runWesl({ device, src });
|
|
109
|
+
expect(results).toHaveLength(1);
|
|
110
|
+
expect(results[0].passed).toBe(false);
|
|
111
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fn helper() -> f32 { return 1.0; }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import wgsl_test::expectUlp;
|
|
2
|
+
|
|
3
|
+
@test fn withinOneUlp() {
|
|
4
|
+
// 1.0 and the next representable float differ by 1 ULP
|
|
5
|
+
let a = 1.0;
|
|
6
|
+
let b = bitcast<f32>(bitcast<u32>(a) + 1u); // next float after 1.0
|
|
7
|
+
expectUlp(a, b, 1u);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@test fn exactMatch() {
|
|
11
|
+
expectUlp(42.0, 42.0, 0u);
|
|
12
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Test assertion library for @test functions
|
|
2
|
+
|
|
3
|
+
const relTol: f32 = 1e-3;
|
|
4
|
+
const absTol: f32 = 1e-6;
|
|
5
|
+
|
|
6
|
+
struct TestResult {
|
|
7
|
+
passed: u32,
|
|
8
|
+
failCount: u32,
|
|
9
|
+
actual: vec4f,
|
|
10
|
+
expected: vec4f,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@group(0) @binding(0) var<storage, read_write> testResult: TestResult;
|
|
14
|
+
|
|
15
|
+
fn expect(condition: bool) {
|
|
16
|
+
if (!condition && testResult.failCount == 0u) {
|
|
17
|
+
testResult.passed = 0u;
|
|
18
|
+
testResult.failCount = 1u;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fn expectEq(actual: u32, expected: u32) {
|
|
23
|
+
testResult.actual = vec4f(f32(actual), 0.0, 0.0, 0.0);
|
|
24
|
+
testResult.expected = vec4f(f32(expected), 0.0, 0.0, 0.0);
|
|
25
|
+
if (actual != expected && testResult.failCount == 0u) {
|
|
26
|
+
testResult.passed = 0u;
|
|
27
|
+
testResult.failCount = 1u;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn expectWithin(actual: f32, expected: f32, relTol: f32, absTol: f32) {
|
|
32
|
+
testResult.actual = vec4f(actual, 0.0, 0.0, 0.0);
|
|
33
|
+
testResult.expected = vec4f(expected, 0.0, 0.0, 0.0);
|
|
34
|
+
let diff = abs(actual - expected);
|
|
35
|
+
let tolerance = max(absTol, relTol * max(abs(actual), abs(expected)));
|
|
36
|
+
if (diff > tolerance && testResult.failCount == 0u) {
|
|
37
|
+
testResult.passed = 0u;
|
|
38
|
+
testResult.failCount = 1u;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn expectNear(actual: f32, expected: f32) {
|
|
43
|
+
expectWithin(actual, expected, relTol, absTol);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn expectWithinVec2(actual: vec2f, expected: vec2f, relTol: f32, absTol: f32) {
|
|
47
|
+
testResult.actual = vec4f(actual, 0.0, 0.0);
|
|
48
|
+
testResult.expected = vec4f(expected, 0.0, 0.0);
|
|
49
|
+
let diff = abs(actual - expected);
|
|
50
|
+
let tolerance = max(vec2f(absTol), relTol * max(abs(actual), abs(expected)));
|
|
51
|
+
if (any(diff > tolerance) && testResult.failCount == 0u) {
|
|
52
|
+
testResult.passed = 0u;
|
|
53
|
+
testResult.failCount = 1u;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn expectNearVec2(actual: vec2f, expected: vec2f) {
|
|
58
|
+
expectWithinVec2(actual, expected, relTol, absTol);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn expectWithinVec3(actual: vec3f, expected: vec3f, relTol: f32, absTol: f32) {
|
|
62
|
+
testResult.actual = vec4f(actual, 0.0);
|
|
63
|
+
testResult.expected = vec4f(expected, 0.0);
|
|
64
|
+
let diff = abs(actual - expected);
|
|
65
|
+
let tolerance = max(vec3f(absTol), relTol * max(abs(actual), abs(expected)));
|
|
66
|
+
if (any(diff > tolerance) && testResult.failCount == 0u) {
|
|
67
|
+
testResult.passed = 0u;
|
|
68
|
+
testResult.failCount = 1u;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn expectNearVec3(actual: vec3f, expected: vec3f) {
|
|
73
|
+
expectWithinVec3(actual, expected, relTol, absTol);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn expectWithinVec4(actual: vec4f, expected: vec4f, relTol: f32, absTol: f32) {
|
|
77
|
+
testResult.actual = actual;
|
|
78
|
+
testResult.expected = expected;
|
|
79
|
+
let diff = abs(actual - expected);
|
|
80
|
+
let tolerance = max(vec4f(absTol), relTol * max(abs(actual), abs(expected)));
|
|
81
|
+
if (any(diff > tolerance) && testResult.failCount == 0u) {
|
|
82
|
+
testResult.passed = 0u;
|
|
83
|
+
testResult.failCount = 1u;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn expectNearVec4(actual: vec4f, expected: vec4f) {
|
|
88
|
+
expectWithinVec4(actual, expected, relTol, absTol);
|
|
89
|
+
}
|