Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/ci/build-test-matrix.js
1685 views
1
// Small script used to calculate the matrix of tests that are going to be
2
// performed for a CI run.
3
//
4
// This is invoked by the `determine` step and is written in JS because I
5
// couldn't figure out how to write it in bash.
6
7
const fs = require('fs');
8
const { spawn } = require('node:child_process');
9
10
// Number of generic buckets to shard crates into. Note that we additionally add
11
// single-crate buckets for our biggest crates.
12
const GENERIC_BUCKETS = 3;
13
14
// Crates which are their own buckets. These are the very slowest to
15
// compile-and-test crates.
16
const SINGLE_CRATE_BUCKETS = ["wasmtime", "wasmtime-cli", "wasmtime-wasi"];
17
18
const ubuntu = 'ubuntu-24.04';
19
const windows = 'windows-2025';
20
const macos = 'macos-15';
21
22
// This is the small, fast-to-execute matrix we use for PRs before they enter
23
// the merge queue. Same schema as `FULL_MATRIX`.
24
const FAST_MATRIX = [
25
{
26
"os": ubuntu,
27
"name": "Test Linux x86_64",
28
"filter": "linux-x64",
29
"isa": "x64",
30
},
31
];
32
33
// This is the full, unsharded, and unfiltered matrix of what we test on
34
// CI. This includes a number of platforms and a number of cross-compiled
35
// targets that are emulated with QEMU. This must be kept tightly in sync with
36
// the `test` step in `main.yml`.
37
//
38
// The supported keys here are:
39
//
40
// * `os` - the github-actions name of the runner os
41
//
42
// * `name` - the human-readable name of the job
43
//
44
// * `filter` - a string which if `prtest:$filter` is in the commit messages
45
// it'll force running this test suite on PR CI.
46
//
47
// * `isa` - changes to `cranelift/codegen/src/$isa` will automatically run this
48
// test suite.
49
//
50
// * `target` - used for cross-compiles if present. Effectively Cargo's
51
// `--target` option for all its operations.
52
//
53
// * `gcc_package`, `gcc`, `qemu`, `qemu_target` - configuration for building
54
// QEMU and installing cross compilers to execute a cross-compiled test suite
55
// on CI.
56
//
57
// * `rust` - the Rust version to install, and if unset this'll be set to
58
// `default`
59
const FULL_MATRIX = [
60
...FAST_MATRIX,
61
{
62
"os": ubuntu,
63
"name": "Test MSRV on Linux x86_64",
64
"filter": "linux-x64",
65
"isa": "x64",
66
"rust": "msrv",
67
},
68
{
69
"os": ubuntu,
70
"name": "Test Linux x86_64 with MPK",
71
"filter": "linux-x64",
72
"isa": "x64"
73
},
74
{
75
"os": ubuntu,
76
"name": "Test Linux x86_64 with ASAN",
77
"filter": "asan",
78
"rust": "wasmtime-ci-pinned-nightly",
79
"target": "x86_64-unknown-linux-gnu",
80
},
81
{
82
"os": macos,
83
"name": "Test macOS x86_64",
84
"filter": "macos-x64",
85
"target": "x86_64-apple-darwin",
86
},
87
{
88
"os": macos,
89
"name": "Test macOS arm64",
90
"filter": "macos-arm64",
91
"target": "aarch64-apple-darwin",
92
},
93
{
94
"os": windows,
95
"name": "Test Windows MSVC x86_64",
96
"filter": "windows-x64",
97
},
98
{
99
"os": windows,
100
"target": "x86_64-pc-windows-gnu",
101
"name": "Test Windows MinGW x86_64",
102
"filter": "mingw-x64"
103
},
104
{
105
"os": ubuntu + '-arm',
106
"target": "aarch64-unknown-linux-gnu",
107
"name": "Test Linux arm64",
108
"filter": "linux-arm64",
109
"isa": "aarch64",
110
},
111
{
112
"os": ubuntu,
113
"target": "s390x-unknown-linux-gnu",
114
"gcc_package": "gcc-s390x-linux-gnu",
115
"gcc": "s390x-linux-gnu-gcc",
116
"qemu": "qemu-s390x -L /usr/s390x-linux-gnu",
117
"qemu_target": "s390x-linux-user",
118
"name": "Test Linux s390x",
119
"filter": "linux-s390x",
120
"isa": "s390x"
121
},
122
{
123
"os": ubuntu,
124
"target": "riscv64gc-unknown-linux-gnu",
125
"gcc_package": "gcc-riscv64-linux-gnu",
126
"gcc": "riscv64-linux-gnu-gcc",
127
"qemu": "qemu-riscv64 -cpu rv64,v=true,vlen=256,vext_spec=v1.0,zfa=true,zfh=true,zba=true,zbb=true,zbc=true,zbs=true,zbkb=true,zcb=true,zicond=true,zvfh=true -L /usr/riscv64-linux-gnu",
128
"qemu_target": "riscv64-linux-user",
129
"name": "Test Linux riscv64",
130
"filter": "linux-riscv64",
131
"isa": "riscv64",
132
},
133
{
134
"name": "Tests on i686-unknown-linux-gnu",
135
"os": ubuntu,
136
"target": "i686-unknown-linux-gnu",
137
"gcc_package": "gcc-i686-linux-gnu",
138
"gcc": "i686-linux-gnu-gcc",
139
},
140
{
141
"name": "Tests on armv7-unknown-linux-gnueabihf",
142
"os": ubuntu,
143
"target": "armv7-unknown-linux-gnueabihf",
144
"gcc_package": "gcc-arm-linux-gnueabihf",
145
"gcc": "arm-linux-gnueabihf-gcc",
146
"qemu": "qemu-arm -L /usr/arm-linux-gnueabihf -E LD_LIBRARY_PATH=/usr/arm-linux-gnueabihf/lib",
147
"qemu_target": "arm-linux-user",
148
},
149
];
150
151
/// Get the workspace's full list of member crates.
152
async function getWorkspaceMembers() {
153
// Spawn a `cargo metadata` subprocess, accumulate its JSON output from
154
// `stdout`, and wait for it to exit.
155
const child = spawn("cargo", ["metadata"], { encoding: "utf8" });
156
let data = "";
157
child.stdout.on("data", chunk => data += chunk);
158
await new Promise((resolve, reject) => {
159
child.on("close", resolve);
160
child.on("error", reject);
161
});
162
163
// Get the names of the crates in the workspace from the JSON metadata by
164
// building a package-id to name map and then translating the package-ids
165
// listed as workspace members.
166
const metadata = JSON.parse(data);
167
const id_to_name = {};
168
for (const pkg of metadata.packages) {
169
id_to_name[pkg.id] = pkg.name;
170
}
171
return metadata.workspace_members.map(m => id_to_name[m]);
172
}
173
174
/// For each given target configuration, shard the workspace's crates into
175
/// buckets across that config.
176
///
177
/// This is essentially a `flat_map` where each config that logically tests all
178
/// crates in the workspace is mapped to N sharded configs that each test only a
179
/// subset of crates in the workspace. Each sharded config's subset of crates to
180
/// test are disjoint from all its siblings, and the union of all these siblings'
181
/// crates to test is the full workspace members set.
182
///
183
/// With some poetic license around a `crates_to_test` key that doesn't actually
184
/// exist, logically each element of the input `configs` list gets transformed
185
/// like this:
186
///
187
/// { os: "ubuntu-latest", isa: "x64", ..., crates: "all" }
188
///
189
/// ==>
190
///
191
/// [
192
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime"] },
193
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-cli"] },
194
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-wasi"] },
195
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["cranelift", "cranelift-codegen", ...] },
196
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-slab", "cranelift-entity", ...] },
197
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["cranelift-environ", "wasmtime-cli-flags", ...] },
198
/// ...
199
/// ]
200
///
201
/// Note that `crates: "all"` is implicit in the input and omitted. Similarly,
202
/// `crates: [...]` in each output config is actually implemented via adding a
203
/// `bucket` key, which contains the CLI flags we must pass to `cargo` to run
204
/// tests for just this config's subset of crates.
205
async function shard(configs) {
206
const members = await getWorkspaceMembers();
207
208
// Divide the workspace crates into N disjoint subsets. Crates that are
209
// particularly expensive to compile and test form their own singleton subset.
210
const buckets = Array.from({ length: GENERIC_BUCKETS }, _ => new Set());
211
let i = 0;
212
for (const crate of members) {
213
if (SINGLE_CRATE_BUCKETS.indexOf(crate) != -1) continue;
214
buckets[i].add(crate);
215
i = (i + 1) % GENERIC_BUCKETS;
216
}
217
for (crate of SINGLE_CRATE_BUCKETS) {
218
buckets.push(new Set([crate]));
219
}
220
221
// For each config, expand it into N configs, one for each disjoint set we
222
// created above.
223
const sharded = [];
224
for (const config of configs) {
225
for (const bucket of buckets) {
226
sharded.push(Object.assign(
227
{},
228
config,
229
{
230
name: `${config.name} (${Array.from(bucket).join(', ')})`,
231
// We run tests via `cargo test --workspace`, so exclude crates that
232
// aren't in this bucket, rather than naming only the crates that are
233
// in this bucket.
234
bucket: members
235
.map(c => bucket.has(c) ? `--package ${c}` : `--exclude ${c}`)
236
.join(" "),
237
}
238
));
239
}
240
}
241
return sharded;
242
}
243
244
async function main() {
245
// Our first argument is a file that is a giant json blob which contains at
246
// least all the messages for all of the commits that were a part of this PR.
247
// This is used to test if any commit message includes a string.
248
const commits = fs.readFileSync(process.argv[2]).toString();
249
250
// The second argument is a file that contains the names of all files modified
251
// for a PR, used for file-based filters.
252
const names = fs.readFileSync(process.argv[3]).toString();
253
254
for (let config of FULL_MATRIX) {
255
if (config.rust === undefined) {
256
config.rust = 'default';
257
}
258
}
259
260
// If the optional third argument to this script is `true` then that means all
261
// tests are being run and no filtering should happen.
262
if (process.argv[4] == 'true') {
263
console.log(JSON.stringify(await shard(FULL_MATRIX), undefined, 2));
264
return;
265
}
266
267
// When we aren't running the full CI matrix, filter configs down to just the
268
// relevant bits based on files changed in this commit or if the commit asks
269
// for a certain config to run.
270
const filtered = FULL_MATRIX.filter(config => {
271
// If an ISA-specific test was modified, then include that ISA config.
272
if (config.isa && names.includes(`cranelift/codegen/src/isa/${config.isa}`)) {
273
return true;
274
}
275
276
// If any runtest was modified, include all ISA configs as runtests can
277
// target any backend.
278
if (names.includes(`cranelift/filetests/filetests/runtests`)) {
279
if (config.isa !== undefined)
280
return true;
281
}
282
283
// If the commit explicitly asks for this test config, then include it.
284
if (config.filter && commits.includes(`prtest:${config.filter}`)) {
285
return true;
286
}
287
288
return false;
289
});
290
291
// If at least one test is being run via our filters then run those tests.
292
if (filtered.length > 0) {
293
console.log(JSON.stringify(await shard(filtered), undefined, 2));
294
return;
295
}
296
297
// Otherwise if nothing else is being run, run the fast subset of the matrix.
298
console.log(JSON.stringify(await shard(FAST_MATRIX), undefined, 2));
299
}
300
301
main()
302
303