Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function parseSwiftTimingLine(line) {
};
}
const swiftFinish = plain.match(
/(?:^|\s)Test (?!run\b|case\b)(.+?)(?: with (\d+) test cases)? (passed|failed|skipped) after ([0-9.]+) seconds\.$/,
/(?:^|\s)Test (?!run\b|case\b)(.+?)(?: with (\d+) test cases)? (passed|failed|skipped) after ([0-9.]+) seconds(?: with \d+ issues?)?\.$/,
);
if (swiftFinish) {
return {
Expand Down Expand Up @@ -57,6 +57,17 @@ export function parseSwiftSuiteLine(line) {
};
}

export function isSwiftTestCompletionFragment(fragment, testName) {
const plain = fragment.replace(/\u001B\[[0-9;]*m/g, "");
const completed = parseSwiftTimingLine(plain);
if (completed && completed.event !== "started") return completed.name === testName;

const result = plain.match(/(?:^|\s)[✔↷] Test (.+)$/u);
if (!result) return false;
const reported = result[1];
return testName.startsWith(reported) || reported.startsWith(testName);
}

function parseArguments(arguments_) {
const separator = arguments_.indexOf("--");
if (separator < 0 || separator === arguments_.length - 1) {
Expand All @@ -83,17 +94,37 @@ function parseArguments(arguments_) {
return options;
}

export async function run(options, { runProcessImpl = runProcess } = {}) {
export async function run(
options,
{
runProcessImpl = runProcess,
setTimeoutImpl = setTimeout,
clearTimeoutImpl = clearTimeout,
} = {},
) {
mkdirSync(path.dirname(options.report), { recursive: true });
const logPath = options.report.replace(/\.json$/i, ".log");
const log = createWriteStream(logPath, { flags: "w" });
const active = new Map();
const records = [];
let currentSuite = null;
let timedOutTest = null;
let testTimer = null;
let terminateChild = () => {};

const clearTestTimer = (name) => {
const activeTest = active.get(name);
if (!activeTest || activeTest.timer === null) return;
clearTimeoutImpl(activeTest.timer);
activeTest.timer = null;
};

const recordPartialLine = (line) => {
const matchingTests = [...active.keys()].filter((name) =>
isSwiftTestCompletionFragment(line, name),
);
if (matchingTests.length === 1) clearTestTimer(matchingTests[0]);
};

const recordLine = (line, stream) => {
log.write(`${stream}: ${line}\n`);
const suiteEvent = parseSwiftSuiteLine(line);
Expand All @@ -102,22 +133,25 @@ export async function run(options, { runProcessImpl = runProcess } = {}) {
const event = parseSwiftTimingLine(line);
if (!event) return;
if (event.event === "started") {
const startedAt = performance.now();
active.set(event.name, { startedAt, suite: currentSuite });
if (testTimer) clearTimeout(testTimer);
testTimer = setTimeout(() => {
timedOutTest = { name: event.name, suite: currentSuite };
terminateChild();
clearTestTimer(event.name);
const activeTest = {
startedAt: performance.now(),
suite: currentSuite,
timer: null,
};
active.set(event.name, activeTest);
activeTest.timer = setTimeoutImpl(() => {
if (active.get(event.name) !== activeTest || timedOutTest) return;
timedOutTest = { name: event.name, suite: activeTest.suite };
activeTest.timer = null;
void terminateChild();
}, options.maxMs);
return;
}

const activeTest = active.get(event.name);
clearTestTimer(event.name);
active.delete(event.name);
if (testTimer) {
clearTimeout(testTimer);
testTimer = null;
}
records.push({
name: event.name,
...(activeTest?.suite ? { suite: activeTest.suite } : {}),
Expand All @@ -134,18 +168,20 @@ export async function run(options, { runProcessImpl = runProcess } = {}) {
command: options.command,
args: options.commandArguments,
cwd: REPOSITORY_ROOT,
env: { ...process.env, NSUnbufferedIO: "YES" },
timeoutMs: options.suiteTimeoutMs,
onSpawn: ({ terminate }) => {
terminateChild = terminate;
},
onStdoutLine: (line) => recordLine(line, "stdout"),
onStderrLine: (line) => recordLine(line, "stderr"),
onStdoutPartialLine: recordPartialLine,
streamStdout: true,
streamStderr: true,
});

const result = await childPromise;
if (testTimer) clearTimeout(testTimer);
for (const name of active.keys()) clearTestTimer(name);
await new Promise((resolve, reject) => {
log.once("error", reject);
log.end(resolve);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export async function terminateProcessTree(
return waitForProcessGroupExit(child.pid, forcedTerminationTimeoutMs, pollIntervalMs);
}

function lineCollector(callback) {
function lineCollector(callback, partialCallback = () => {}) {
const decoder = new StringDecoder("utf8");
let pending = "";
return {
Expand All @@ -67,6 +67,7 @@ function lineCollector(callback) {
const lines = pending.split(/\r?\n/);
pending = lines.pop() ?? "";
for (const line of lines) callback(line);
if (pending) partialCallback(pending);
},
finish() {
pending += decoder.end();
Expand All @@ -84,6 +85,8 @@ export function runProcess({
timeoutMs,
onStdoutLine = () => {},
onStderrLine = () => {},
onStdoutPartialLine = () => {},
onStderrPartialLine = () => {},
onSpawn = () => {},
streamStdout = false,
streamStderr = false,
Expand All @@ -102,8 +105,8 @@ export function runProcess({
});
const stdoutChunks = [];
const stderrChunks = [];
const stdoutLines = lineCollector(onStdoutLine);
const stderrLines = lineCollector(onStderrLine);
const stdoutLines = lineCollector(onStdoutLine, onStdoutPartialLine);
const stderrLines = lineCollector(onStderrLine, onStderrPartialLine);
let timedOut = false;
let timeout;
let terminationPromise = null;
Expand Down
Loading
Loading