// Small script used to calculate the matrix of tests that are going to be1// performed for a CI run.2//3// This is invoked by the `determine` step and is written in JS because I4// couldn't figure out how to write it in bash.56const fs = require('fs');7const { spawn } = require('node:child_process');89// Number of generic buckets to shard crates into. Note that we additionally add10// single-crate buckets for our biggest crates.11const GENERIC_BUCKETS = 3;1213// Crates which are their own buckets. These are the very slowest to14// compile-and-test crates.15const SINGLE_CRATE_BUCKETS = ["wasmtime", "wasmtime-cli", "wasmtime-wasi"];1617const ubuntu = 'ubuntu-24.04';18const windows = 'windows-2025';19const macos = 'macos-15';2021// This is the small, fast-to-execute matrix we use for PRs before they enter22// the merge queue. Same schema as `FULL_MATRIX`.23const FAST_MATRIX = [24{25"name": "Test Linux x86_64",26"os": ubuntu,27"filter": "linux-x64",28"isa": "x64",29},30];3132// This is the full, unsharded, and unfiltered matrix of what we test on33// CI. This includes a number of platforms and a number of cross-compiled34// targets that are emulated with QEMU. This must be kept tightly in sync with35// the `test` step in `main.yml`.36//37// The supported keys here are:38//39// * `os` - the github-actions name of the runner os40//41// * `name` - the human-readable name of the job42//43// * `filter` - a string which if `prtest:$filter` is in the commit messages44// it'll force running this test suite on PR CI.45//46// * `isa` - changes to `cranelift/codegen/src/$isa` will automatically run this47// test suite.48//49// * `target` - used for cross-compiles if present. Effectively Cargo's50// `--target` option for all its operations.51//52// * `gcc_package`, `gcc`, `qemu`, `qemu_target` - configuration for building53// QEMU and installing cross compilers to execute a cross-compiled test suite54// on CI.55//56// * `sde` - if `true`, indicates this test should use Intel SDE for instruction57// emulation. SDE will be set up and configured as the test runner.58//59// * `rust` - the Rust version to install, and if unset this'll be set to60// `default`61const FULL_MATRIX = [62...FAST_MATRIX,63{64"name": "Test MSRV",65"os": ubuntu,66"filter": "linux-x64",67"isa": "x64",68"rust": "msrv",69},70{71"name": "Test MPK",72"os": ubuntu,73"filter": "linux-x64",74"isa": "x64"75},76{77"name": "Test ASAN",78"os": ubuntu,79"filter": "asan",80"rust": "wasmtime-ci-pinned-nightly",81"target": "x86_64-unknown-linux-gnu",82},83{84"name": "Test Intel SDE",85"os": ubuntu,86"filter": "sde",87"isa": "x64",88"sde": true,89"crates": "cranelift-tools",90},91{92"name": "Test macOS x86_64",93"os": macos,94"filter": "macos-x64",95"target": "x86_64-apple-darwin",96},97{98"name": "Test macOS arm64",99"os": macos,100"filter": "macos-arm64",101"target": "aarch64-apple-darwin",102},103{104"name": "Test MSVC x86_64",105"os": windows,106"filter": "windows-x64",107},108{109"name": "Test MinGW x86_64",110"os": windows,111"target": "x86_64-pc-windows-gnu",112"filter": "mingw-x64"113},114{115"name": "Test Linux arm64",116"os": ubuntu + '-arm',117"target": "aarch64-unknown-linux-gnu",118"filter": "linux-arm64",119"isa": "aarch64",120},121{122"name": "Test Linux s390x",123// "os": 'ubuntu-24.04-s390x',124"os": ubuntu,125"target": "s390x-unknown-linux-gnu",126"filter": "linux-s390x",127"isa": "s390x",128"gcc_package": "gcc-s390x-linux-gnu",129"gcc": "s390x-linux-gnu-gcc",130"qemu": "qemu-s390x -L /usr/s390x-linux-gnu",131"qemu_target": "s390x-linux-user",132},133{134"name": "Test Linux riscv64",135"os": ubuntu,136"target": "riscv64gc-unknown-linux-gnu",137"gcc_package": "gcc-riscv64-linux-gnu",138"gcc": "riscv64-linux-gnu-gcc",139"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",140"qemu_target": "riscv64-linux-user",141"filter": "linux-riscv64",142"isa": "riscv64",143},144{145"name": "Tests Linux i686",146"os": ubuntu,147"target": "i686-unknown-linux-gnu",148"gcc_package": "gcc-i686-linux-gnu",149"gcc": "i686-linux-gnu-gcc",150},151{152"name": "Tests Linux armv7",153"os": ubuntu,154"target": "armv7-unknown-linux-gnueabihf",155"gcc_package": "gcc-arm-linux-gnueabihf",156"gcc": "arm-linux-gnueabihf-gcc",157"qemu": "qemu-arm -L /usr/arm-linux-gnueabihf -E LD_LIBRARY_PATH=/usr/arm-linux-gnueabihf/lib",158"qemu_target": "arm-linux-user",159},160];161162/// Get the workspace's full list of member crates.163async function getWorkspaceMembers() {164// Spawn a `cargo metadata` subprocess, accumulate its JSON output from165// `stdout`, and wait for it to exit.166const child = spawn("cargo", ["metadata"], { encoding: "utf8" });167let data = "";168child.stdout.on("data", chunk => data += chunk);169await new Promise((resolve, reject) => {170child.on("close", resolve);171child.on("error", reject);172});173174// Get the names of the crates in the workspace from the JSON metadata by175// building a package-id to name map and then translating the package-ids176// listed as workspace members.177const metadata = JSON.parse(data);178const id_to_name = {};179for (const pkg of metadata.packages) {180id_to_name[pkg.id] = pkg.name;181}182return metadata.workspace_members.map(m => id_to_name[m]);183}184185/// For each given target configuration, shard the workspace's crates into186/// buckets across that config.187///188/// This is essentially a `flat_map` where each config that logically tests all189/// crates in the workspace is mapped to N sharded configs that each test only a190/// subset of crates in the workspace. Each sharded config's subset of crates to191/// test are disjoint from all its siblings, and the union of all these siblings'192/// crates to test is the full workspace members set.193///194/// With some poetic license around a `crates_to_test` key that doesn't actually195/// exist, logically each element of the input `configs` list gets transformed196/// like this:197///198/// { os: "ubuntu-latest", isa: "x64", ..., crates: "all" }199///200/// ==>201///202/// [203/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime"] },204/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-cli"] },205/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-wasi"] },206/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["cranelift", "cranelift-codegen", ...] },207/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-slab", "cranelift-entity", ...] },208/// { os: "ubuntu-latest", isa: "x64", ..., crates: ["cranelift-environ", "wasmtime-cli-flags", ...] },209/// ...210/// ]211///212/// Note that `crates: "all"` is implicit in the input and omitted. Similarly,213/// `crates: [...]` in each output config is actually implemented via adding a214/// `bucket` key, which contains the CLI flags we must pass to `cargo` to run215/// tests for just this config's subset of crates.216async function shard(configs) {217const members = await getWorkspaceMembers();218219// Divide the workspace crates into N disjoint subsets. Crates that are220// particularly expensive to compile and test form their own singleton subset.221const buckets = Array.from({ length: GENERIC_BUCKETS }, _ => new Set());222let i = 0;223for (const crate of members) {224if (SINGLE_CRATE_BUCKETS.indexOf(crate) != -1) continue;225buckets[i].add(crate);226i = (i + 1) % GENERIC_BUCKETS;227}228for (crate of SINGLE_CRATE_BUCKETS) {229buckets.push(new Set([crate]));230}231232// For each config, expand it into N configs, one for each disjoint set we233// created above.234const sharded = [];235for (const config of configs) {236// If crates is specified, don't shard, just use the specified crates237if (config.crates) {238sharded.push(Object.assign(239{},240config,241{242bucket: members243.map(c => c === config.crates ? `--package ${c}` : `--exclude ${c}`)244.join(" ")245}246));247continue;248}249250let nbucket = 1;251for (const bucket of buckets) {252let bucket_name = `${nbucket}/${buckets.length}`;253if (bucket.size === 1)254bucket_name = Array.from(bucket)[0];255256sharded.push(Object.assign(257{},258config,259{260name: `${config.name} (${bucket_name})`,261// We run tests via `cargo test --workspace`, so exclude crates that262// aren't in this bucket, rather than naming only the crates that are263// in this bucket.264bucket: members265.map(c => bucket.has(c) ? `--package ${c}` : `--exclude ${c}`)266.join(" "),267}268));269nbucket += 1;270}271}272return sharded;273}274275async function main() {276// Our first argument is a file that is a giant json blob which contains at277// least all the messages for all of the commits that were a part of this PR.278// This is used to test if any commit message includes a string.279const commits = fs.readFileSync(process.argv[2]).toString();280281// The second argument is a file that contains the names of all files modified282// for a PR, used for file-based filters.283const names = fs.readFileSync(process.argv[3]).toString();284285for (let config of FULL_MATRIX) {286if (config.rust === undefined) {287config.rust = 'default';288}289}290291// If the optional third argument to this script is `true` then that means all292// tests are being run and no filtering should happen.293if (process.argv[4] == 'true') {294console.log(JSON.stringify(await shard(FULL_MATRIX), undefined, 2));295return;296}297298// When we aren't running the full CI matrix, filter configs down to just the299// relevant bits based on files changed in this commit or if the commit asks300// for a certain config to run.301const filtered = FULL_MATRIX.filter(config => {302// If an ISA-specific test was modified, then include that ISA config.303if (config.isa && names.includes(`cranelift/codegen/src/isa/${config.isa}`)) {304return true;305}306307// If any runtest was modified, include all ISA configs as runtests can308// target any backend.309if (names.includes(`cranelift/filetests/filetests/runtests`)) {310if (config.isa !== undefined)311return true;312}313314// If the commit explicitly asks for this test config, then include it.315if (config.filter && commits.includes(`prtest:${config.filter}`)) {316return true;317}318319return false;320});321322// If at least one test is being run via our filters then run those tests.323if (filtered.length > 0) {324console.log(JSON.stringify(await shard(filtered), undefined, 2));325return;326}327328// Otherwise if nothing else is being run, run the fast subset of the matrix.329console.log(JSON.stringify(await shard(FAST_MATRIX), undefined, 2));330}331332main()333334335