Enable ASAN with linux-x64-asan in CI

This commit is contained in:
Jarred Sumner
2025-05-02 10:44:09 -07:00
committed by GitHub
parent 32c1dcb70d
commit d8a69d6823
32 changed files with 615 additions and 144 deletions

View File

@@ -25,17 +25,23 @@ import {
} from "node:fs";
import { readFile } from "node:fs/promises";
import { userInfo } from "node:os";
import { basename, dirname, join, relative, sep } from "node:path";
import { basename, dirname, join, relative, sep, extname } from "node:path";
import { parseArgs } from "node:util";
import {
getAbi,
getAbiVersion,
getArch,
getBranch,
getBuildLabel,
getBuildUrl,
getCommit,
getDistro,
getDistroVersion,
getEnv,
getFileUrl,
getHostname,
getLoggedInUserCountOrDetails,
getOs,
getSecret,
getShell,
getWindowsExitReason,
@@ -156,7 +162,116 @@ if (options["quiet"]) {
}
/**
*
* @typedef {Object} TestExpectation
* @property {string} filename
* @property {string[]} expectations
* @property {string[] | undefined} bugs
* @property {string[] | undefined} modifiers
* @property {string | undefined} comment
*/
/**
* @returns {TestExpectation[]}
*/
function getTestExpectations() {
const expectationsPath = join(cwd, "test", "expectations.txt");
if (!existsSync(expectationsPath)) {
return [];
}
const lines = readFileSync(expectationsPath, "utf-8").split(/\r?\n/);
/** @type {TestExpectation[]} */
const expectations = [];
for (const line of lines) {
const content = line.trim();
if (!content || content.startsWith("#")) {
continue;
}
let comment;
const commentIndex = content.indexOf("#");
let cleanLine = content;
if (commentIndex !== -1) {
comment = content.substring(commentIndex + 1).trim();
cleanLine = content.substring(0, commentIndex).trim();
}
let modifiers = [];
let remaining = cleanLine;
let modifierMatch = remaining.match(/^\[(.*?)\]/);
if (modifierMatch) {
modifiers = modifierMatch[1].trim().split(/\s+/);
remaining = remaining.substring(modifierMatch[0].length).trim();
}
let expectationValues = ["Skip"];
const expectationMatch = remaining.match(/\[(.*?)\]$/);
if (expectationMatch) {
expectationValues = expectationMatch[1].trim().split(/\s+/);
remaining = remaining.substring(0, remaining.length - expectationMatch[0].length).trim();
}
const filename = remaining.trim();
if (filename) {
expectations.push({
filename,
expectations: expectationValues,
bugs: undefined,
modifiers: modifiers.length ? modifiers : undefined,
comment,
});
}
}
return expectations;
}
/**
* @param {string} testPath
* @returns {string[]}
*/
function getTestModifiers(testPath) {
const ext = extname(testPath);
const filename = basename(testPath, ext);
const modifiers = filename.split("-").filter(value => value !== "bun");
const os = getOs();
const arch = getArch();
modifiers.push(os, arch, `${os}-${arch}`);
const distro = getDistro();
if (distro) {
modifiers.push(distro, `${os}-${distro}`, `${os}-${arch}-${distro}`);
const distroVersion = getDistroVersion();
if (distroVersion) {
modifiers.push(
distroVersion,
`${distro}-${distroVersion}`,
`${os}-${distro}-${distroVersion}`,
`${os}-${arch}-${distro}-${distroVersion}`,
);
}
}
const abi = getAbi();
if (abi) {
modifiers.push(abi, `${os}-${abi}`, `${os}-${arch}-${abi}`);
const abiVersion = getAbiVersion();
if (abiVersion) {
modifiers.push(
abiVersion,
`${abi}-${abiVersion}`,
`${os}-${abi}-${abiVersion}`,
`${os}-${arch}-${abi}-${abiVersion}`,
);
}
}
return modifiers.map(value => value.toUpperCase());
}
/**
* @returns {Promise<TestResult[]>}
*/
async function runTests() {
@@ -168,10 +283,14 @@ async function runTests() {
}
!isQuiet && console.log("Bun:", execPath);
const expectations = getTestExpectations();
const modifiers = getTestModifiers(execPath);
!isQuiet && console.log("Modifiers:", modifiers);
const revision = getRevision(execPath);
!isQuiet && console.log("Revision:", revision);
const tests = getRelevantTests(testsPath);
const tests = getRelevantTests(testsPath, modifiers, expectations);
!isQuiet && console.log("Running tests:", tests.length);
/** @type {VendorTest[] | undefined} */
@@ -693,8 +812,8 @@ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) {
BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0",
BUN_INSTALL_CACHE_DIR: tmpdirPath,
SHELLOPTS: isWindows ? "igncr" : undefined, // ignore "\r" on Windows
// Used in Node.js tests.
TEST_TMPDIR: tmpdirPath,
ASAN_OPTIONS: "allow_user_segv_handler=1",
TEST_TMPDIR: tmpdirPath, // Used in Node.js tests.
};
if (env) {
@@ -1072,9 +1191,6 @@ function isHidden(path) {
return /node_modules|node.js/.test(dirname(path)) || /^\./.test(basename(path));
}
/** Files with these extensions are not treated as test cases */
const IGNORED_EXTENSIONS = new Set([".md"]);
/**
* @param {string} cwd
* @returns {string[]}
@@ -1084,13 +1200,14 @@ function getTests(cwd) {
const dirname = join(cwd, path);
for (const entry of readdirSync(dirname, { encoding: "utf-8", withFileTypes: true })) {
const { name } = entry;
const ext = name.slice(name.lastIndexOf("."));
const filename = join(path, name);
if (isHidden(filename) || IGNORED_EXTENSIONS.has(ext)) {
if (isHidden(filename)) {
continue;
}
if (entry.isFile() && isTest(filename)) {
yield filename;
if (entry.isFile()) {
if (isTest(filename)) {
yield filename;
}
} else if (entry.isDirectory()) {
yield* getFiles(cwd, filename);
}
@@ -1226,9 +1343,11 @@ async function getVendorTests(cwd) {
/**
* @param {string} cwd
* @param {string[]} testModifiers
* @param {TestExpectation[]} testExpectations
* @returns {string[]}
*/
function getRelevantTests(cwd) {
function getRelevantTests(cwd, testModifiers, testExpectations) {
let tests = getTests(cwd);
const availableTests = [];
const filteredTests = [];
@@ -1272,6 +1391,25 @@ function getRelevantTests(cwd) {
}
}
const skipExpectations = testExpectations
.filter(
({ modifiers, expectations }) =>
!modifiers?.length || testModifiers.some(modifier => modifiers?.includes(modifier)),
)
.map(({ filename }) => filename.replace("test/", ""));
if (skipExpectations.length) {
const skippedTests = availableTests.filter(testPath => skipExpectations.some(filter => isMatch(testPath, filter)));
if (skippedTests.length) {
for (const testPath of skippedTests) {
const index = availableTests.indexOf(testPath);
if (index !== -1) {
availableTests.splice(index, 1);
}
}
!isQuiet && console.log("Skipping tests:", skipExpectations, skippedTests.length, "/", availableTests.length);
}
}
const shardId = parseInt(options["shard"]);
const maxShards = parseInt(options["max-shards"]);
if (filters?.length) {
@@ -1368,13 +1506,17 @@ async function getExecPathFromBuildKite(target, buildId) {
await spawnSafe({
command: "buildkite-agent",
args,
timeout: 60000,
});
for (const entry of readdirSync(releasePath, { recursive: true, encoding: "utf-8" })) {
if (/^bun.*\.zip$/i.test(entry) && !entry.includes("-profile.zip")) {
zipPath = join(releasePath, entry);
break downloadLoop;
}
zipPath = readdirSync(releasePath, { recursive: true, encoding: "utf-8" })
.filter(filename => /^bun.*\.zip$/i.test(filename))
.map(filename => join(releasePath, filename))
.sort((a, b) => b.includes("profile") - a.includes("profile"))
.at(0);
if (zipPath) {
break downloadLoop;
}
console.warn(`Waiting for ${target}.zip to be available...`);
@@ -1390,12 +1532,12 @@ async function getExecPathFromBuildKite(target, buildId) {
const releaseFiles = readdirSync(releasePath, { recursive: true, encoding: "utf-8" });
for (const entry of releaseFiles) {
const execPath = join(releasePath, entry);
if (/bun(?:\.exe)?$/i.test(entry) && statSync(execPath).isFile()) {
if (/bun(?:-[a-z]+)?(?:\.exe)?$/i.test(entry) && statSync(execPath).isFile()) {
return execPath;
}
}
console.warn(`Found ${releaseFiles.length} files in ${releasePath}:`);
console.warn(`Found ${releaseFiles.length} files in ${releasePath}:`, releaseFiles);
throw new Error(`Could not find executable from BuildKite: ${releasePath}`);
}