Skip to content

Commit 4e0da3f

Browse files
committed
Add differential testing harness
This runs wgsl shaders and rust shaders and compares the output. If the output differs, the test fails. Differential testing is better than snapshot testing or golden file testing as there are no reference files to get outdated.
1 parent 32088c8 commit 4e0da3f

File tree

24 files changed

+3386
-10
lines changed

24 files changed

+3386
-10
lines changed

.cargo/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[alias]
22
compiletest = "run --release -p compiletests --"
3+
difftest = "run --release -p difftests --"
34

45

56
[target.x86_64-pc-windows-msvc]

.github/workflows/ci.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ jobs:
4242
- if: ${{ runner.os == 'Linux' }}
4343
name: Linux - Install native dependencies
4444
run: sudo apt install libwayland-cursor0 libxkbcommon-dev libwayland-dev
45+
- if: ${{ runner.os == 'Linux' }}
46+
name: Install xvfb, llvmpipe and lavapipe
47+
run: |
48+
sudo apt-get update -y -qq
49+
sudo add-apt-repository ppa:kisak/turtle -y
50+
sudo apt-get update
51+
sudo apt install -y xvfb libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers
4552
# cargo version is a random command that forces the installation of rust-toolchain
4653
- name: install rust-toolchain
4754
run: cargo version
@@ -64,6 +71,10 @@ jobs:
6471
if: ${{ matrix.target != 'aarch64-linux-android' }}
6572
run: cargo run -p compiletests --release --no-default-features --features "use-installed-tools" -- --target-env vulkan1.1,vulkan1.2,spv1.3
6673

74+
- name: difftest
75+
if: ${{ matrix.target != 'aarch64-linux-android' }}
76+
run: cargo run -p difftests --release --no-default-features --features "use-installed-tools"
77+
6778
- name: workspace test
6879
if: ${{ matrix.target != 'aarch64-linux-android' }}
6980
run: cargo test --release
@@ -147,6 +158,8 @@ jobs:
147158
run: cargo fmt --all -- --check
148159
- name: Rustfmt compiletests
149160
run: shopt -s globstar && rustfmt --check tests/compiletests/ui/**/*.rs
161+
- name: Rustfmt difftests
162+
run: cargo fmt --check --all --manifest-path tests/difftests/tests/Cargo.toml
150163
- name: Check docs are valid
151164
run: RUSTDOCFLAGS=-Dwarnings cargo doc --no-deps
152165
- name: Check docs for `spirv-std` and `spirv-builder` on stable (for docs.rs)

Cargo.lock

Lines changed: 80 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ members = [
2727

2828
"tests/compiletests",
2929
"tests/compiletests/deps-helper",
30+
"tests/difftests/bin",
31+
"tests/difftests/lib",
3032
]
3133

3234
[workspace.package]

deny.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ skip-tree = [
6060
{ name = "num_cpus", version = "=1.16.0", depth = 2 },
6161
# HACK(LegNeato) `tracing-tree` uses newer dependencies of `tracing`.
6262
{ name = "tracing-tree", version = "=0.3.1" },
63+
# HACK(LegNeato) `thorin` has not yet released the version that bumps this.
64+
{ name = "gimli", version = "=0.30.0" },
6365
]
6466

6567

docs/src/testing.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
# Testing Rust-GPU
22

33
Rust-GPU has a couple of different kinds of tests, most can be ran through
4-
`cargo test`, however Rust-GPU also has end-to-end tests for compiling Rust and
5-
validating its SPIR-V output, which can ran by running `cargo compiletest`.
4+
`cargo test`. Rust-GPU also has end-to-end tests for compiling Rust and
5+
validating its SPIR-V output, which can ran by running `cargo compiletest`. Finally,
6+
Rust-GPU has differential tests, which runs Rust and WGSL shaders and
7+
makes sure they have the same output. These can be run with `cargo difftest`.
68

79
```bash
8-
cargo test && cargo compiletest
10+
cargo test && cargo compiletest && cargo difftest
911
```
1012

11-
## Adding Tests
13+
## Compile Tests
14+
15+
### Adding Tests
1216

1317
Rust-GPU's end-to-end test's use an external version of the [`compiletest`] tool
1418
as a testing framework. Be sure to check out the [repository][`compiletest`] and

tests/difftests/README.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Difftests
2+
3+
Difftests verify correctness by running multiple implementations of the same logic and
4+
comparing their outputs. Instead of relying on fixed reference outputs, they detect
5+
discrepancies across implementations.
6+
7+
## How It Works
8+
9+
1. **Test Discovery**
10+
11+
- The harness scans `tests/` for test cases.
12+
- A test case is a directory containing at least two Rust binary packages / variants
13+
to compare.
14+
15+
2. **Configuration & Execution**
16+
17+
- The harness creates a temporary output file for each variant binary.
18+
- The harness runs each test variant binary with `cargo run --release`.
19+
- A JSON config file with the output path and other settings is passed to the test
20+
binary as `argv[1]`.
21+
- The binary reads the config, runs its computation, and writes to the output file.
22+
- Tests are run serially so they have full control of the GPU.
23+
24+
3. **Output Comparison**
25+
- The harness reads outputs as opaque bytes.
26+
- If outputs differ, the test fails.
27+
28+
Because the difftest harness merely runs Rust binaries in a directory, it supports
29+
testing various setups. For example, you can:
30+
31+
- Compare different CPU host code (`ash`, `wgpu`, etc.) with different GPU backends
32+
(`rust-gpu`, `cuda`, `metal`, `wgsl`, etc.) against each other to make sure the output
33+
is consistent.
34+
- Verify that CPU and GPU implementations produce the same output.
35+
- Ensure the same `rust-gpu` code gives identical results across different dispatch
36+
methods.
37+
38+
## Writing a Test
39+
40+
Create a subdirectory under `tests/` with the test name. For example, `tests/foo/` for a
41+
test named `foo`. In the test directory, create 2 or more Rust binary packages. Add the
42+
packages to the top-level workspace `Cargo.toml` in the `tests/` directory. _Note that
43+
this isn't the top-level workspace for the project._ The test binaries are in their own
44+
workspace rather than the main workspace.
45+
46+
### Test Binary Example
47+
48+
Each test binary must:
49+
50+
1. Have a unique package name in `Cargo.toml`.
51+
2. Read the config file path from `argv[1]`.
52+
3. Load the config using `difftest::Config::from_path`.
53+
4. Write its computed output to `output_path`.
54+
55+
For example:
56+
57+
```rust
58+
use difftest::config::Config;
59+
use std::{env, fs, io::Write};
60+
61+
fn main() {
62+
let config_path = env::args().nth(1).expect("No config path provided");
63+
let config = Config::from_path(&config_path).expect("Invalid config");
64+
65+
// Real work would go here.
66+
let output = compute_test_output();
67+
68+
let mut file = fs::File::create(&config.output_path)
69+
.expect("Failed to create output file");
70+
file.write_all(&output).expect("Failed to write output");
71+
}
72+
```
73+
74+
### Common test types
75+
76+
Of course, many test will have common host and GPU needs. Rather than require every test
77+
binary to reimplement functionality, we have created some common tests with reasonable
78+
defaults in the `difftest` library.
79+
80+
For example, this will handle compiling the current crate as a Rust compute shader,
81+
running it via `wgpu`, and writing the output to the appropriate place:
82+
83+
```rust
84+
fn main() {
85+
// Load the config from the harness.
86+
let config = Config::from_path(std::env::args().nth(1).unwrap()).unwrap();
87+
88+
// Define test parameters, loading the rust shader from the current crate.
89+
let test = WgpuComputeTest::new(RustComputeShader::default(), [1, 1, 1], 1024);
90+
91+
// Run the test and write the output to a file.
92+
test.run_test(&config).unwrap();
93+
}
94+
```
95+
96+
and this will handle loading a shader named `shader.wgsl` or `compute.wgsl` in the root
97+
of the current crate, running it via `wgpu`, and writing the output to the appropriate
98+
place:
99+
100+
```rust
101+
fn main() {
102+
// Load the config from the harness.
103+
let config = Config::from_path(std::env::args().nth(1).unwrap()).unwrap();
104+
105+
// Define test parameters, loading the wgsl shader from the crate directory.
106+
let test = WgpuComputeTest::new(WgslComputeShader::default(), [1, 1, 1], 1024);
107+
108+
// Run the test and write the output to a file.
109+
test.run_test(&config).unwrap();
110+
}
111+
```
112+
113+
## Running Tests
114+
115+
### Run all difftests:
116+
117+
```sh
118+
cargo difftest
119+
```
120+
121+
Note that `cargo difftest` is an alias in `.cargo/config` for `cargo run --release -p
122+
difftest --`.
123+
124+
### Run specific tests by name:
125+
126+
```sh
127+
cargo difftest some_test_name
128+
```
129+
130+
### Show stdout/stderr from tests:
131+
132+
```sh
133+
cargo difftest --nocapture
134+
```
135+
136+
## Debugging Failing Tests
137+
138+
If outputs differ, the error message lists:
139+
140+
- Binary package names
141+
- Their directories
142+
- Output file paths
143+
144+
Inspect the output files with your preferred tools to determine the differences.
145+
146+
If you suspect a bug in the test harness, you can view detailed test harness logs:
147+
148+
```sh
149+
RUST_LOG=trace cargo difftest
150+
```

0 commit comments

Comments
 (0)