let parse, expandGlobSync, basename: any, relative: any;
try {
const path = await import("stdlib/path");
basename = path.basename;
relative = path.relative;
expandGlobSync = (await import("stdlib/fs")).expandGlobSync;
parse = (await import("stdlib/flags")).parse;
} catch (_e) {
const path = await import("https://deno.land/std/path/mod.ts");
basename = path.basename;
relative = path.relative;
expandGlobSync = (await import("https://deno.land/std/fs/mod.ts")).expandGlobSync;
parse = (await import("https://deno.land/std/flags/mod.ts")).parse;
}
const flags = parse(Deno.args, {
boolean: ["json-for-ci", "verbose", "dry-run"],
string: ["n", "timing-file"],
default: {
verbose: false,
"dry-run": false,
"json-for-ci": false,
"timing-file": "timing.txt",
},
});
const timingFile = flags["timing-file"];
const detailedSmokeAll = flags["json-for-ci"];
const smokeAllTestFile = "./smoke/smoke-all.test.ts";
let timingFileContent;
try {
timingFileContent = Deno.readTextFileSync(timingFile);
} catch (e) {
console.log(e);
console.log(
`'${timingFile}' missing. Run './run-tests.sh' with QUARTO_TEST_TIMING='${timingFile}'`,
);
Deno.exit(1);
}
const lines = timingFileContent.trim().split("\n");
const currentTests = new Set(
[...expandGlobSync("**/*.test.ts", { globstar: true })].map((entry) =>
`./${relative(Deno.cwd(), entry.path)}`
),
);
const currentSmokeFiles = new Set<string>(
detailedSmokeAll
? [
...expandGlobSync("docs/smoke-all/**/*.{md,qmd,ipynb}", {
globstar: true,
}),
]
.filter((entry) => /^[^_]/.test(basename(entry.path)))
.map((entry) => `${relative(Deno.cwd(), entry.path)}`)
: [],
);
const timedTests = new Set<string>();
const timedSmokeAllDocs = new Set<string>();
type Timing = {
real: number;
user: number;
sys: number;
};
type TestTiming = {
name: string;
timing: Timing;
};
const testTimings: TestTiming[] = [];
const RegSmokeAllFile = new RegExp("smoke\/smoke-all\.test\.ts -- (.*)$");
let dontUseDetailledSmokeAll = false;
if (
detailedSmokeAll &&
lines.filter((line) => RegSmokeAllFile.test(line.trim())).length == 0
) {
throw new Error(
`No smoke-all timed tests found in ${timingFile}. Run './run-tests.sh' with QUARTO_TEST_TIMING to create a new file, and pass another file with '--timing-file='`,
);
}
for (let i = 0; i < lines.length; i += 2) {
const name = lines[i].trim();
if (/(^|\/)test\.ts$/.test(name)) continue;
if (RegSmokeAllFile.test(name)) {
if (!detailedSmokeAll) {
dontUseDetailledSmokeAll = true;
continue;
} else {
const smokeFile = name.split(" -- ")[1];
if (!currentSmokeFiles.has(smokeFile)) {
flags.verbose &&
console.log(
`Test ${name} in '${timingFile}' does not exists anymore. Update '${timingFile} with 'run ./run-tests.sh with QUARTO_TEST_TIMING='${timingFile}'`,
);
continue;
}
timedSmokeAllDocs.add(smokeFile);
}
} else {
if (!currentTests.has(name)) {
flags.verbose &&
console.log(
`Test ${name} in '${timingFile}' does not exists anymore. Update '${timingFile} with 'run ./run-tests.sh with QUARTO_TEST_TIMING='${timingFile}'`,
);
continue;
}
}
const timingStrs = lines[i + 1].trim().replaceAll(/ +/g, " ").split(" ");
const timing = {
real: Number(timingStrs[0]),
user: Number(timingStrs[2]),
sys: Number(timingStrs[4]),
};
testTimings.push({ name, timing });
timedTests.add(name);
}
let failed = false;
const buckets: TestTiming[][] = [];
const nBuckets = Number(flags.n) || navigator.hardwareConcurrency;
const bucketSizes = (new Array(nBuckets)).fill(0);
const argmin = (a: number[]): number => {
let best = -1, bestValue = Infinity;
for (let i = 0; i < a.length; ++i) {
if (a[i] < bestValue) {
best = i;
bestValue = a[i];
}
}
return best;
};
for (let i = 0; i < nBuckets; ++i) {
buckets.push([]);
}
if (dontUseDetailledSmokeAll) {
failed = true;
flags.verbose &&
console.log(`${smokeAllTestFile} will run it is own bucket`);
buckets[0].push({
name: smokeAllTestFile,
timing: { real: 99999, user: 99999, sys: 99999 },
});
bucketSizes[0] += 99999;
}
for (const timing of testTimings) {
const ix = argmin(bucketSizes);
buckets[ix].push(timing);
bucketSizes[ix] += timing.timing.real;
}
for (const currentTest of currentTests) {
let missingTests = new Set<string>();
if (currentTest.match(/smoke-all\.test\.ts/)) {
if (detailedSmokeAll && !dontUseDetailledSmokeAll) {
for (const currentSmokeFile of currentSmokeFiles) {
if (!timedSmokeAllDocs.has(currentSmokeFile)) {
flags.verbose &&
console.log(
`Missing smoke-all docs '${currentSmokeFile}' in ${timingFile}`,
);
failed = true;
missingTests.add(`${smokeAllTestFile} -- ${currentSmokeFile}`);
}
}
}
} else if (!timedTests.has(currentTest)) {
flags.verbose &&
console.log(`Missing test '${currentTest}' in ${timingFile}`);
failed = true;
missingTests.add(currentTest);
}
if (missingTests.size !== 0) {
missingTests.forEach((missingTest) => {
buckets[Math.floor(Math.random() * nBuckets)].push({
name: missingTest,
timing: { real: 0, user: 0, sys: 0 },
});
});
}
}
flags.verbose && console.log(`Will run in ${nBuckets} cores`);
if (!failed && flags.verbose) {
console.log(
`Expected speedup: ${
(bucketSizes.reduce((a, b) => a + b, 0) / Math.max(...bucketSizes))
.toFixed(
2,
)
}`,
);
}
if (flags["dry-run"]) {
console.log(JSON.stringify(buckets, null, 2));
Deno.exit(0);
}
if (flags["json-for-ci"]) {
flags.verbose && console.log("Buckets of tests to run in parallel");
const bucketSimple = buckets.map((bucket) => {
return bucket.map((tt) => {
tt.name = RegSmokeAllFile.test(tt.name)
? tt.name.split(" -- ")[1]
: tt.name;
return tt.name;
});
});
console.log(JSON.stringify(bucketSimple, null, 2));
} else {
console.log("Running `run-test.sh` in parallel... ");
Promise.all(buckets.map((bucket, i) => {
const cmd: string[] = ["./run-tests.sh"];
cmd.push(...bucket.map((tt) => tt.name));
return Deno.run({ cmd }).status();
})).then(() => {
console.log("Running `run-test.sh` in parallel... END");
});
}