Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# test-runner - Changelog

## 3.4.0 - 2026-06-23

* Only re-run incomplete tests after a test run reset.
* When a run is cancelled and restarted due to lack of progress, results from classes that already completed are kept and only the unfinished classes are re-run, instead of re-running everything. Genuine failures are still re-run sequentially at the end.
* Write a per-reset diagnostic file (`<name>-reset-<n>-<runId>.json`) alongside the other output, recording the queue item statuses and a results summary at the point of the reset.
* Abort and abandon a run that stalls before it starts processing (e.g. while stuck queued), rather than leaving it in-flight to be re-run.
* Test run status logging now shows a `No progress <n>/<limit>` suffix while a run is stalling, and resets log the reuse summary and a `Reset <n>/<max>` count.
* `numberOfResets` in `TestRunSummary` now reports the total resets across all runs in a `Testall` invocation (including missing-test reruns), not just the outer run's count.

## 3.3.1-beta.0 - 2026-06-14

* Attempt to abort Apex test runs after runner timeout while preserving the original timeout error if aborting fails.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@apexdevtools/test-runner",
"version": "3.3.1-beta.0",
"version": "3.4.0",
"description": "Apex parallel test runner with reliability goodness",
"author": {
"name": "Apex Dev Tools Team",
Expand Down
18 changes: 1 addition & 17 deletions src/command/Testall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
TestRerun,
TestRunSummary,
} from '../results/OutputGenerator';
import { TestRunnerOptions } from '../runner/TestOptions';
import { getOutputFileBase, TestRunnerOptions } from '../runner/TestOptions';
import { TestRunner } from '../runner/TestRunner';
import { formatTestName, getTestName } from '../results/TestResultUtils';
import { TestResultStore } from '../results/TestResultStore';
Expand All @@ -43,8 +43,6 @@ import { retry } from '../runner/Poll';

export interface TestallOptions extends TestRunnerOptions, QueryOptions {
maxErrorsForReRun?: number; // Don't re-run if > failed tests (excluding pattern matched tests), default 10
outputDirBase?: string; // Base for junit and other output files, default 'test-result*'
outputFileName?: string; //File name base
disableCoverageReport?: boolean; // if enabled disables coverage collection
rerunOption?: RerunOption; // see RerunOption - default 'pattern'
}
Expand All @@ -56,7 +54,6 @@ export enum RerunOption {
}

const DEFAULT_MAX_ERRORS_FOR_RERUN = 10;
const DEFAULT_OUTPUT_FILE_BASE = 'test-result';
const DEFAULT_RERUN_OPTION = RerunOption.Pattern;

export function getMaxErrorsForReRun(options: TestallOptions): number {
Expand All @@ -65,18 +62,6 @@ export function getMaxErrorsForReRun(options: TestallOptions): number {
else return DEFAULT_MAX_ERRORS_FOR_RERUN;
}

export function getOutputFileBase(options: TestallOptions): {
fileName: string;
outputDir: string;
} {
if (options.outputDirBase && options.outputFileName)
return {
outputDir: options.outputDirBase,
fileName: options.outputFileName,
};
else return { outputDir: '', fileName: DEFAULT_OUTPUT_FILE_BASE };
}

export function getReRunOption(options: TestallOptions): RerunOption {
const opt = options.rerunOption;
if (opt && Object.values(RerunOption).find(v => v === opt)) return opt;
Expand Down Expand Up @@ -215,7 +200,6 @@ export class Testall {
store
);

store.numberOfResets = result.numberOfResets;
}
}

Expand Down
35 changes: 33 additions & 2 deletions src/log/BaseLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,34 @@ export abstract class BaseLogger implements Logger {
);
}

logRunStuck(testRunId: string, status: string): void {
this.logMessage(
`Test run '${testRunId}' stuck in ${status} with no progress, abandoning this attempt`
);
}

logResetCount(resetNumber: number, maxResets: number): void {
this.logMessage(`Reset ${resetNumber}/${maxResets} before abandoning run`);
}

logRunReset(
reusedTests: number,
completedClasses: number,
remainingTests: number,
pendingClasses: number
): void {
this.logMessage(
`Reusing ${reusedTests} tests from ${completedClasses} completed classes; ` +
`rerunning ${remainingTests} remaining tests across ${pendingClasses} classes`
);
}

logStatus(
testRunResult: ApexTestRunResult,
tests: ApexTestResult[],
elapsedTime: string
elapsedTime: string,
noProgressPolls: number,
noProgressLimit: number
): void {
const status = testRunResult.Status;
const outcomes = groupByOutcome(tests);
Expand All @@ -154,8 +178,15 @@ export abstract class BaseLogger implements Logger {
const total = testRunResult.MethodsEnqueued;
const complete = total > 0 ? Math.floor((completed * 100) / total) : 0;

// While a run is in flight, surface how close it is to a reset so a stall
// is visible without an extra log line per poll.
const noProgress =
noProgressPolls > 0
? ` | No progress ${noProgressPolls}/${noProgressLimit}`
: '';

this.logMessage(
`${elapsedTime} [${status}] Passed: ${passed} | Failed: ${failed} | ${completed}/${total} Complete (${complete}%)`
`${elapsedTime} [${status}] Passed: ${passed} | Failed: ${failed} | ${completed}/${total} Complete (${complete}%)${noProgress}`
);
}

Expand Down
12 changes: 11 additions & 1 deletion src/log/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ export interface Logger {
// Test runner
logRunStarted(testRunId: string): void;
logNoProgress(testRunId: string): void;
logRunStuck(testRunId: string, status: string): void;
logResetCount(resetNumber: number, maxResets: number): void;
logRunReset(
reusedTests: number,
completedClasses: number,
remainingTests: number,
pendingClasses: number
): void;
logStatus(
status: ApexTestRunResult,
tests: ApexTestResult[],
elapsedTime: string
elapsedTime: string,
noProgressPolls: number,
noProgressLimit: number
): void;
logTestFailures(newResults: ApexTestResult[]): void;

Expand Down
2 changes: 1 addition & 1 deletion src/results/TestResultStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class TestResultStore {
});

this.asyncError = res.error;
this.numberOfResets = res.numberOfResets;
this.numberOfResets += res.numberOfResets;
}

public saveSyncResult(reruns: TestRerun[]): void {
Expand Down
15 changes: 15 additions & 0 deletions src/runner/TestOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function getCancelPollTimeout(options: CancelTestRunOptions): Duration {
else return Duration.minutes(DEFAULT_POLL_TIMEOUT_MINS);
}

export const DEFAULT_OUTPUT_FILE_BASE = 'test-result';
export const DEFAULT_STATUS_POLL_INTERVAL_MS = 30000;
export const DEFAULT_TEST_RUN_TIMEOUT_MINS = 120;
const DEFAULT_MAX_TEST_RUN_RETRIES = 3;
Expand Down Expand Up @@ -65,6 +66,20 @@ export interface TestRunnerOptions extends CancelTestRunOptions {
pollLimitToAssumeHangingTests?: number; // Number polls without test progress before a hang is assumed, default 60
callbacks?: TestRunnerCallbacks; // Callbacks for events in test runner
codeCoverage?: boolean; // Collect code coverage data, defaults false
outputDirBase?: string; // Base directory for junit and other output files
outputFileName?: string; // File name base for output files, default 'test-result'
}

export function getOutputFileBase(options: TestRunnerOptions): {
fileName: string;
outputDir: string;
} {
if (options.outputDirBase && options.outputFileName)
return {
outputDir: options.outputDirBase,
fileName: options.outputFileName,
};
else return { outputDir: '', fileName: DEFAULT_OUTPUT_FILE_BASE };
}

export function getTestRunAborter(options: TestRunnerOptions): TestRunAborter {
Expand Down
Loading
Loading