Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/ci/build-test-matrix.js
3064 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
"name": "Test Linux x86_64",
27
"os": ubuntu,
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
// * `sde` - if `true`, indicates this test should use Intel SDE for instruction
58
// emulation. SDE will be set up and configured as the test runner.
59
//
60
// * `rust` - the Rust version to install, and if unset this'll be set to
61
// `default`
62
const FULL_MATRIX = [
63
...FAST_MATRIX,
64
{
65
"name": "Test MSRV",
66
"os": ubuntu,
67
"filter": "linux-x64",
68
"isa": "x64",
69
"rust": "msrv",
70
},
71
{
72
"name": "Test MPK",
73
"os": ubuntu,
74
"filter": "linux-x64",
75
"isa": "x64"
76
},
77
{
78
"name": "Test ASAN",
79
"os": ubuntu,
80
"filter": "asan",
81
"rust": "wasmtime-ci-pinned-nightly",
82
"target": "x86_64-unknown-linux-gnu",
83
},
84
{
85
"name": "Test Intel SDE",
86
"os": ubuntu,
87
"filter": "sde",
88
"isa": "x64",
89
"sde": true,
90
"crates": "cranelift-tools",
91
},
92
{
93
"name": "Test macOS x86_64",
94
"os": macos,
95
"filter": "macos-x64",
96
"target": "x86_64-apple-darwin",
97
},
98
{
99
"name": "Test macOS arm64",
100
"os": macos,
101
"filter": "macos-arm64",
102
"target": "aarch64-apple-darwin",
103
},
104
{
105
"name": "Test MSVC x86_64",
106
"os": windows,
107
"filter": "windows-x64",
108
},
109
{
110
"name": "Test MinGW x86_64",
111
"os": windows,
112
"target": "x86_64-pc-windows-gnu",
113
"filter": "mingw-x64"
114
},
115
{
116
"name": "Test Linux arm64",
117
"os": ubuntu + '-arm',
118
"target": "aarch64-unknown-linux-gnu",
119
"filter": "linux-arm64",
120
"isa": "aarch64",
121
},
122
{
123
"name": "Test Linux s390x",
124
// "os": 'ubuntu-24.04-s390x',
125
"os": ubuntu,
126
"target": "s390x-unknown-linux-gnu",
127
"filter": "linux-s390x",
128
"isa": "s390x",
129
"gcc_package": "gcc-s390x-linux-gnu",
130
"gcc": "s390x-linux-gnu-gcc",
131
"qemu": "qemu-s390x -L /usr/s390x-linux-gnu",
132
"qemu_target": "s390x-linux-user",
133
},
134
{
135
"name": "Test Linux riscv64",
136
"os": ubuntu,
137
"target": "riscv64gc-unknown-linux-gnu",
138
"gcc_package": "gcc-riscv64-linux-gnu",
139
"gcc": "riscv64-linux-gnu-gcc",
140
"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",
141
"qemu_target": "riscv64-linux-user",
142
"filter": "linux-riscv64",
143
"isa": "riscv64",
144
},
145
{
146
"name": "Tests Linux i686",
147
"os": ubuntu,
148
"target": "i686-unknown-linux-gnu",
149
"gcc_package": "gcc-i686-linux-gnu",
150
"gcc": "i686-linux-gnu-gcc",
151
},
152
{
153
"name": "Tests Linux armv7",
154
"os": ubuntu,
155
"target": "armv7-unknown-linux-gnueabihf",
156
"gcc_package": "gcc-arm-linux-gnueabihf",
157
"gcc": "arm-linux-gnueabihf-gcc",
158
"qemu": "qemu-arm -L /usr/arm-linux-gnueabihf -E LD_LIBRARY_PATH=/usr/arm-linux-gnueabihf/lib",
159
"qemu_target": "arm-linux-user",
160
},
161
];
162
163
/// Get the workspace's full list of member crates.
164
async function getWorkspaceMembers() {
165
// Spawn a `cargo metadata` subprocess, accumulate its JSON output from
166
// `stdout`, and wait for it to exit.
167
const child = spawn("cargo", ["metadata"], { encoding: "utf8" });
168
let data = "";
169
child.stdout.on("data", chunk => data += chunk);
170
await new Promise((resolve, reject) => {
171
child.on("close", resolve);
172
child.on("error", reject);
173
});
174
175
// Get the names of the crates in the workspace from the JSON metadata by
176
// building a package-id to name map and then translating the package-ids
177
// listed as workspace members.
178
const metadata = JSON.parse(data);
179
const id_to_name = {};
180
for (const pkg of metadata.packages) {
181
id_to_name[pkg.id] = pkg.name;
182
}
183
return metadata.workspace_members.map(m => id_to_name[m]);
184
}
185
186
/// For each given target configuration, shard the workspace's crates into
187
/// buckets across that config.
188
///
189
/// This is essentially a `flat_map` where each config that logically tests all
190
/// crates in the workspace is mapped to N sharded configs that each test only a
191
/// subset of crates in the workspace. Each sharded config's subset of crates to
192
/// test are disjoint from all its siblings, and the union of all these siblings'
193
/// crates to test is the full workspace members set.
194
///
195
/// With some poetic license around a `crates_to_test` key that doesn't actually
196
/// exist, logically each element of the input `configs` list gets transformed
197
/// like this:
198
///
199
/// { os: "ubuntu-latest", isa: "x64", ..., crates: "all" }
200
///
201
/// ==>
202
///
203
/// [
204
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime"] },
205
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-cli"] },
206
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-wasi"] },
207
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["cranelift", "cranelift-codegen", ...] },
208
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-slab", "cranelift-entity", ...] },
209
/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["cranelift-environ", "wasmtime-cli-flags", ...] },
210
/// ...
211
/// ]
212
///
213
/// Note that `crates: "all"` is implicit in the input and omitted. Similarly,
214
/// `crates: [...]` in each output config is actually implemented via adding a
215
/// `bucket` key, which contains the CLI flags we must pass to `cargo` to run
216
/// tests for just this config's subset of crates.
217
async function shard(configs) {
218
const members = await getWorkspaceMembers();
219
220
// Divide the workspace crates into N disjoint subsets. Crates that are
221
// particularly expensive to compile and test form their own singleton subset.
222
const buckets = Array.from({ length: GENERIC_BUCKETS }, _ => new Set());
223
let i = 0;
224
for (const crate of members) {
225
if (SINGLE_CRATE_BUCKETS.indexOf(crate) != -1) continue;
226
buckets[i].add(crate);
227
i = (i + 1) % GENERIC_BUCKETS;
228
}
229
for (crate of SINGLE_CRATE_BUCKETS) {
230
buckets.push(new Set([crate]));
231
}
232
233
// For each config, expand it into N configs, one for each disjoint set we
234
// created above.
235
const sharded = [];
236
for (const config of configs) {
237
// If crates is specified, don't shard, just use the specified crates
238
if (config.crates) {
239
sharded.push(Object.assign(
240
{},
241
config,
242
{
243
bucket: members
244
.map(c => c === config.crates ? `--package ${c}` : `--exclude ${c}`)
245
.join(" ")
246
}
247
));
248
continue;
249
}
250
251
let nbucket = 1;
252
for (const bucket of buckets) {
253
let bucket_name = `${nbucket}/${buckets.length}`;
254
if (bucket.size === 1)
255
bucket_name = Array.from(bucket)[0];
256
257
sharded.push(Object.assign(
258
{},
259
config,
260
{
261
name: `${config.name} (${bucket_name})`,
262
// We run tests via `cargo test --workspace`, so exclude crates that
263
// aren't in this bucket, rather than naming only the crates that are
264
// in this bucket.
265
bucket: members
266
.map(c => bucket.has(c) ? `--package ${c}` : `--exclude ${c}`)
267
.join(" "),
268
}
269
));
270
nbucket += 1;
271
}
272
}
273
return sharded;
274
}
275
276
async function main() {
277
// Our first argument is a file that is a giant json blob which contains at
278
// least all the messages for all of the commits that were a part of this PR.
279
// This is used to test if any commit message includes a string.
280
const commits = fs.readFileSync(process.argv[2]).toString();
281
282
// The second argument is a file that contains the names of all files modified
283
// for a PR, used for file-based filters.
284
const names = fs.readFileSync(process.argv[3]).toString();
285
286
for (let config of FULL_MATRIX) {
287
if (config.rust === undefined) {
288
config.rust = 'default';
289
}
290
}
291
292
// If the optional third argument to this script is `true` then that means all
293
// tests are being run and no filtering should happen.
294
if (process.argv[4] == 'true') {
295
console.log(JSON.stringify(await shard(FULL_MATRIX), undefined, 2));
296
return;
297
}
298
299
// When we aren't running the full CI matrix, filter configs down to just the
300
// relevant bits based on files changed in this commit or if the commit asks
301
// for a certain config to run.
302
const filtered = FULL_MATRIX.filter(config => {
303
// If an ISA-specific test was modified, then include that ISA config.
304
if (config.isa && names.includes(`cranelift/codegen/src/isa/${config.isa}`)) {
305
return true;
306
}
307
308
// If any runtest was modified, include all ISA configs as runtests can
309
// target any backend.
310
if (names.includes(`cranelift/filetests/filetests/runtests`)) {
311
if (config.isa !== undefined)
312
return true;
313
}
314
315
// If the commit explicitly asks for this test config, then include it.
316
if (config.filter && commits.includes(`prtest:${config.filter}`)) {
317
return true;
318
}
319
320
return false;
321
});
322
323
// If at least one test is being run via our filters then run those tests.
324
if (filtered.length > 0) {
325
console.log(JSON.stringify(await shard(filtered), undefined, 2));
326
return;
327
}
328
329
// Otherwise if nothing else is being run, run the fast subset of the matrix.
330
console.log(JSON.stringify(await shard(FAST_MATRIX), undefined, 2));
331
}
332
333
main()
334
335