// Small script used to calculate the matrix of tests that are going to be // performed for a CI run. // // This is invoked by the `determine` step and is written in JS because I // couldn't figure out how to write it in bash. const fs = require('fs'); const { spawn } = require('node:child_process'); // Number of generic buckets to shard crates into. Note that we additionally add // single-crate buckets for our biggest crates. const GENERIC_BUCKETS = 3; // Crates which are their own buckets. These are the very slowest to // compile-and-test crates. const SINGLE_CRATE_BUCKETS = ["wasmtime", "wasmtime-cli", "wasmtime-wasi"]; // This is the small, fast-to-execute matrix we use for PRs before they enter // the merge queue. Same schema as `FULL_MATRIX`. const FAST_MATRIX = [ { "os": "ubuntu-latest", "name": "Test Linux x86_64", "filter": "linux-x64", "isa": "x64", }, ]; // This is the full, unsharded, and unfiltered matrix of what we test on // CI. This includes a number of platforms and a number of cross-compiled // targets that are emulated with QEMU. This must be kept tightly in sync with // the `test` step in `main.yml`. // // The supported keys here are: // // * `os` - the github-actions name of the runner os // // * `name` - the human-readable name of the job // // * `filter` - a string which if `prtest:$filter` is in the commit messages // it'll force running this test suite on PR CI. // // * `isa` - changes to `cranelift/codegen/src/$isa` will automatically run this // test suite. // // * `target` - used for cross-compiles if present. Effectively Cargo's // `--target` option for all its operations. // // * `gcc_package`, `gcc`, `qemu`, `qemu_target` - configuration for building // QEMU and installing cross compilers to execute a cross-compiled test suite // on CI. // // * `rust` - the Rust version to install, and if unset this'll be set to // `default` const FULL_MATRIX = [ ...FAST_MATRIX, { "os": "ubuntu-latest", "name": "Test MSRV on Linux x86_64", "filter": "linux-x64", "isa": "x64", "rust": "msrv", }, { "os": "ubuntu-latest", "name": "Test Linux x86_64 with MPK", "filter": "linux-x64", "isa": "x64" }, { "os": "macos-13", "name": "Test macOS x86_64", "filter": "macos-x64", }, { "os": "macos-14", "name": "Test macOS arm64", "filter": "macos-arm64", "target": "aarch64-apple-darwin", }, { "os": "windows-latest", "name": "Test Windows MSVC x86_64", "filter": "windows-x64", }, { "os": "windows-latest", "target": "x86_64-pc-windows-gnu", "name": "Test Windows MinGW x86_64", "filter": "mingw-x64" }, { "os": "ubuntu-latest", "target": "aarch64-unknown-linux-gnu", "gcc_package": "gcc-aarch64-linux-gnu", "gcc": "aarch64-linux-gnu-gcc", "qemu": "qemu-aarch64 -L /usr/aarch64-linux-gnu", "qemu_target": "aarch64-linux-user", "name": "Test Linux arm64", "filter": "linux-arm64", "isa": "aarch64", }, { "os": "ubuntu-latest", "target": "s390x-unknown-linux-gnu", "gcc_package": "gcc-s390x-linux-gnu", "gcc": "s390x-linux-gnu-gcc", "qemu": "qemu-s390x -L /usr/s390x-linux-gnu", "qemu_target": "s390x-linux-user", "name": "Test Linux s390x", "filter": "linux-s390x", "isa": "s390x" }, { "os": "ubuntu-latest", "target": "riscv64gc-unknown-linux-gnu", "gcc_package": "gcc-riscv64-linux-gnu", "gcc": "riscv64-linux-gnu-gcc", "qemu": "qemu-riscv64 -cpu rv64,v=true,vlen=256,vext_spec=v1.0,Zfa=true,zba=true,zbb=true,zbc=true,zbs=true,zbkb=true,zcb=true,x-zicond=true -L /usr/riscv64-linux-gnu", "qemu_target": "riscv64-linux-user", "name": "Test Linux riscv64", "filter": "linux-riscv64", "isa": "riscv64", } ]; /// Get the workspace's full list of member crates. async function getWorkspaceMembers() { // Spawn a `cargo metadata` subprocess, accumulate its JSON output from // `stdout`, and wait for it to exit. const child = spawn("cargo", ["metadata"], { encoding: "utf8" }); let data = ""; child.stdout.on("data", chunk => data += chunk); await new Promise((resolve, reject) => { child.on("close", resolve); child.on("error", reject); }); // Get the names of the crates in the workspace from the JSON metadata by // building a package-id to name map and then translating the package-ids // listed as workspace members. const metadata = JSON.parse(data); const id_to_name = {}; for (const pkg of metadata.packages) { id_to_name[pkg.id] = pkg.name; } return metadata.workspace_members.map(m => id_to_name[m]); } /// For each given target configuration, shard the workspace's crates into /// buckets across that config. /// /// This is essentially a `flat_map` where each config that logically tests all /// crates in the workspace is mapped to N sharded configs that each test only a /// subset of crates in the workspace. Each sharded config's subset of crates to /// test are disjoint from all its siblings, and the union of all these siblings' /// crates to test is the full workspace members set. /// /// With some poetic license around a `crates_to_test` key that doesn't actually /// exist, logically each element of the input `configs` list gets transformed /// like this: /// /// { os: "ubuntu-latest", isa: "x64", ..., crates: "all" } /// /// ==> /// /// [ /// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime"] }, /// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-cli"] }, /// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-wasi"] }, /// { os: "ubuntu-latest", isa: "x64", ..., crates: ["cranelift", "cranelift-codegen", ...] }, /// { os: "ubuntu-latest", isa: "x64", ..., crates: ["wasmtime-slab", "cranelift-entity", ...] }, /// { os: "ubuntu-latest", isa: "x64", ..., crates: ["cranelift-environ", "wasmtime-cli-flags", ...] }, /// ... /// ] /// /// Note that `crates: "all"` is implicit in the input and omitted. Similarly, /// `crates: [...]` in each output config is actually implemented via adding a /// `bucket` key, which contains the CLI flags we must pass to `cargo` to run /// tests for just this config's subset of crates. async function shard(configs) { const members = await getWorkspaceMembers(); // Divide the workspace crates into N disjoint subsets. Crates that are // particularly expensive to compile and test form their own singleton subset. const buckets = Array.from({ length: GENERIC_BUCKETS }, _ => new Set()); let i = 0; for (const crate of members) { if (SINGLE_CRATE_BUCKETS.indexOf(crate) != -1) continue; buckets[i].add(crate); i = (i + 1) % GENERIC_BUCKETS; } for (crate of SINGLE_CRATE_BUCKETS) { buckets.push(new Set([crate])); } // For each config, expand it into N configs, one for each disjoint set we // created above. const sharded = []; for (const config of configs) { for (const bucket of buckets) { sharded.push(Object.assign( {}, config, { name: `${config.name} (${Array.from(bucket).join(', ')})`, // We run tests via `cargo test --workspace`, so exclude crates that // aren't in this bucket, rather than naming only the crates that are // in this bucket. bucket: members .map(c => bucket.has(c) ? `--package ${c}` : `--exclude ${c}`) .join(" "), } )); } } return sharded; } async function main() { // Our first argument is a file that is a giant json blob which contains at // least all the messages for all of the commits that were a part of this PR. // This is used to test if any commit message includes a string. const commits = fs.readFileSync(process.argv[2]).toString(); // The second argument is a file that contains the names of all files modified // for a PR, used for file-based filters. const names = fs.readFileSync(process.argv[3]).toString(); for (let config of FULL_MATRIX) { if (config.rust === undefined) { config.rust = 'default'; } } // If the optional third argument to this script is `true` then that means all // tests are being run and no filtering should happen. if (process.argv[4] == 'true') { console.log(JSON.stringify(await shard(FULL_MATRIX), undefined, 2)); return; } // When we aren't running the full CI matrix, filter configs down to just the // relevant bits based on files changed in this commit or if the commit asks // for a certain config to run. const filtered = FULL_MATRIX.filter(config => { // If an ISA-specific test was modified, then include that ISA config. if (config.isa && names.includes(`cranelift/codegen/src/isa/${config.isa}`)) { return true; } // If any runtest was modified, include all ISA configs as runtests can // target any backend. if (names.includes(`cranelift/filetests/filetests/runtests`)) { return config.isa !== undefined; } // If the commit explicitly asks for this test config, then include it. if (config.filter && commits.includes(`prtest:${config.filter}`)) { return true; } return false; }); // If at least one test is being run via our filters then run those tests. if (filtered.length > 0) { console.log(JSON.stringify(await shard(filtered), undefined, 2)); return; } // Otherwise if nothing else is being run, run the fast subset of the matrix. console.log(JSON.stringify(await shard(FAST_MATRIX), undefined, 2)); } main()