feature/TOOL-654-dont-rerun-full-test-suite - #89
Conversation
kjonescertinia
left a comment
There was a problem hiding this comment.
Review summary
The core logic of both fixes looks correct and the new specs exercise the main new paths. I didn't find correctness bugs in the reset/restart flow. Requesting changes (minor) for: one behavioural change in the aborter that now affects the user-cancel and run-timeout paths with a long default wait, one untested error branch in the confirmation poll, and two files the PR leaves unformatted. Details are in the inline comments; a few items that don't map to a diff line are listed below.
Verdict: Request changes (minor). Happy to approve once the two Medium items are addressed or consciously deferred.
Local verification (head 2e00c99)
| Step | Result |
|---|---|
pnpm install |
OK |
pnpm build |
OK |
eslint ./src (check only) |
Clean |
prettier --check src test |
7 files flagged; 2 are new to this PR (src/runner/TestRunCancelAborter.ts, test/runner/TestRunCancelAborter.spec.ts), the other 5 were already unformatted on main |
pnpm test |
104 tests, all pass (one spec, TestResultMatcher.spec.ts, only failed under my sandbox because it writes ../.apexTestRerun; it passes normally and is untouched by this PR) |
CI only runs install/build/test and eslint doesn't enforce prettier, so the formatting items won't fail CI.
Checks that passed
numberOfResetsis still incremented on theallCompletepath and asserted in both new specs.- When
maxTestRunRetriesis exhausted,restartstaysunknown, control falls through torunInternal, which throws the max-retriesTestErrorexactly as on main. The existing specshould abort and abandon without re-running when retries are exhaustedstill passes. - Already-complete run:
abortRundoes no DML and the confirmation poll returns on the first query; the existingshould cancel when no tests still runningspec covers it. CancelTestRunOptions,getCancelPollInterval,getCancelPollTimeoutandlogWaitingForCancelalready existed on main following thegetX(options)pattern; the PR only wires them in. No changes tosrc/index.ts.- No
console.log; all logging goes throughLogger.
Test coverage
- restart: existing specs. allComplete: two new specs. unknown: existing spec. Cancel poll success / timeout: two new specs.
- Gaps: the confirmation-poll query-error branch (
pollRetryIf, line 120 of the aborter) is never executed; there is no Testall-level test that a runner result withrun.Status === 'Processing'and noerrorflows into missing-test detection.
Items not tied to a diff line
- Deferring the
allCompletecase to Testall means those missing tests are now subject tomaxErrorsForReRun(Testall.asyncRun), whereas an internal restart was not. Probably desirable, but worth a line in the PR description since it's a change from main. cancelPollTimoutMins(typo,Timout) inTestOptions.tspredates this PR, but this PR makes the option functional for the first time, so this is the cheapest moment to add a correctly spelled name alongside it.- Commit message on the second commit: "Pool after aborting" should be "Poll".
| } | ||
| } | ||
|
|
||
| await this.waitForCancelConfirmation(logger, connection, testRunId, options); |
There was a problem hiding this comment.
Medium: the confirmation wait now applies to every abort path, not only restarts.
The comment below (lines 90-93) says the goal is to avoid ALREADY_IN_PROCESS on resubmission. That only matters for the hang-restart path and the allComplete/stuck paths that hand off to Testall. It's wasted on the two other callers in TestRunner.runInternal: the run-timeout abort (~line 193) and the caller's cancellation-token abort (~line 198), where nothing is resubmitted.
With defaults, the first re-poll is after 30s and the timeout is 10 minutes, so a user pressing cancel can now wait much longer for run() to resolve than before. Suggest either a flag (on abortRun or CancelTestRunOptions) so those two call sites skip confirmation, or documenting the change and lowering the default timeout.
There was a problem hiding this comment.
Good idea. I will add an option for this into TestOptions
| }, | ||
|
|
||
| pollUntil: outstandingCount => outstandingCount === 0, | ||
| pollRetryIf: () => true, |
There was a problem hiding this comment.
Medium: a persistent SOQL failure here spins until the full timeout.
pollRetryIf: () => true means an expired session or network failure is retried every 30s for 10 minutes (logging "Poll failed" each time) before the best-effort warning fires. Coverage shows this branch is never executed by the specs.
Either bail early on non-transient errors, or at least add a spec for the query-throws case so the intended behaviour is pinned down.
There was a problem hiding this comment.
Changed to pollRetryIf: () => false so we don't retry at all. If there is an error checking if the abort finished we'll try to restart and we're basically where we were before anyway.
| await this.abortTestRun(result.run.AsyncApexJobId); | ||
| result.numberOfResets = this._stats.getNumberOfTimesReset(); | ||
| this._logger.logRunCompleteMissingResults(testRunIdResult.testRunId); | ||
| return result; |
There was a problem hiding this comment.
Low: Processing can leak into the final report / public API.
This returns a result whose run.Status is still Processing with no error. Testall handles it correctly (TestResultStore.hasAborted only checks Aborted, so missing-test detection runs). But if the missing set turns out empty (all results did arrive, the run just never flipped status) nothing overwrites the status and the summary reports a finished run as Processing.
Also, AsyncTestRunner is public via src/index.ts, so direct consumers can now receive a non-terminal status with no error where they previously got a re-run. Worth a note in the PR description, and ideally a Testall-level spec covering this handoff.
There was a problem hiding this comment.
I agree. Adding test cases and updating the PR description.
| // restart here. Whether results actually are missing is Testall's | ||
| // call: its missing-test check compares expected vs. persisted | ||
| // results and re-runs anything absent. | ||
| return { kind: 'allComplete' }; |
There was a problem hiding this comment.
Low (behavioural nuance): by deferring here, the missing tests become subject to maxErrorsForReRun in Testall.asyncRun, whereas an internal restart re-ran pending classes regardless of the failure count. Likely the desired behaviour, just flagging it as a change from main so it's a conscious decision.
There was a problem hiding this comment.
I have updated the PR description so we are explicit about this. I don't think we want to change how it works. I don't think missing tests are very common actually.
| logRegex(`Cancelling test run '${testRunId}'`) | ||
| ); | ||
| expect(logger.entries[1]).to.match( | ||
| logRegex(`Waiting for test run '${testRunId}' to cancel... 1 tests queued`) |
There was a problem hiding this comment.
Low: prettier. This file (lines 150 and 184-186) and src/runner/TestRunCancelAborter.ts:71 fail prettier --check; both were clean on main. Not enforced by CI or eslint, so purely housekeeping.
| this._stats = this._stats.reset(); | ||
| await this.abortTestRun(result.run.AsyncApexJobId); | ||
| result.numberOfResets = this._stats.getNumberOfTimesReset(); | ||
| this._logger.logRunCompleteMissingResults(testRunIdResult.testRunId); |
There was a problem hiding this comment.
Nit (log ordering): this explanatory message is logged after the abort, so in the output it appears after "Cancelling test run" / "has been cancelled". Logging it before abortTestRun reads more naturally.
There was a problem hiding this comment.
I don't think it's worth trying to change this.
| import { retry } from './Poll'; | ||
| import { Pollable, poll, retry } from './Poll'; | ||
|
|
||
| const PENDING_STATUSES = "'Holding', 'Queued', 'Preparing', 'Processing'"; |
There was a problem hiding this comment.
Simplification: this hardcoded string duplicates PENDING_QUEUE_STATUSES in TestRunner.ts. Exporting one array from src/model/ApexTestQueueItem.ts and building the IN (...) clause from it would keep the two in sync.
There was a problem hiding this comment.
I can't imagine SF adding a new status to the apex test runner. But you never know.
|
|
||
| await this.waitForCancelConfirmation(logger, connection, testRunId, options); | ||
|
|
||
| logger.logRunCancelled(testRunId); |
There was a problem hiding this comment.
Nit: on the timeout path this logs "has been cancelled" immediately after the "Could not confirm ... finished cancelling" warning, which reads as contradictory. Consider a different message (or skipping this one) when confirmation failed.
There was a problem hiding this comment.
That makes sense. Changed.
…cuteAnonymous to reduce repeated code.
…ed but failed to do so.
Summary
Fix 1 (TOOL-654): AsyncTestRunner.prepareRestart no longer falls back to a full re-run when a reset finds no pending classes. Previously, if every class in the queue had reached a terminal status but the run was still stalled, the runner assumed it couldn't tell what was incomplete and resubmitted the entire suite. In production this meant a single missing result (5 out of ~40k tests, caused by the org closing a class's queue item before all its results were persisted) triggered a full ~25-minute re-run. Now that case returns the partial result directly and defers to Testall.asyncRun's existing missing-test detection, which finds and re-runs only the specific absent tests. The three possible outcomes of prepareRestart are now modeled as a PrepareRestartResult discriminated union (restart / allComplete / unknown) rather than overloading undefined.
Fix 2: TestRunCancelAborter.abortRun now confirms a cancellation actually took effect org-side before returning, instead of firing the abort DML and returning immediately. Without this, a caller that resubmits tests for the same classes right after an abort (e.g. Testall's missing-test rerun) could race the still-live original run and hit ALREADY_IN_PROCESS: Test already enqueued. It now polls ApexTestQueueItem until no pending items remain, using the previously-unused CancelTestRunOptions/getCancelPollInterval/getCancelPollTimeout and Logger.logWaitingForCancel (leftovers from an equivalent check that had been removed). It's best-effort: on timeout it logs a warning and returns rather than blocking the caller.
Notes for consumers
AsyncTestRunner is exported directly, and when a run stalls with nothing left in the queue it now returns a partial result whose run.Status is still Processing and with no error set — where previously it re-ran the full suite. Driven through Testall this is handled: missing-test detection runs, and the follow-up run's status replaces it. If nothing turns out to be missing there is no follow-up run, so TestRunSummary.runResult.Status stays Processing. This doesn't reach the generated reports — ReportGenerator derives the outcome from the test results and never reads Status. The pre-existing "stalled before it started processing" path added in 3.4.0 already returns a non-terminal status the same way.
When a stalled run defers to missing-test detection, those missing tests are now subject to maxErrorsForReRun like any other Testall re-run. Previously the internal restart re-ran pending classes regardless of the failure count, so with more than maxErrorsForReRun genuine failures the missing tests will now be left absent rather than chased — logged as Aborting missing test check as N failed - max re-run limit exceeded. This is intentional: the limit exists to stop spending org time on a run that is already failing.
Testing
Lots of new jest test cases.
Apart from that, it's a bit of a faff to manually test. I did manage to get an abort and reset in the middle of a run. You can see the new logging as it polled.
00:01:00 [Processing] Passed: 9 | Failed: 0 | 9/1549 Complete (0%) | No progress 4/5
00:01:05 [Processing] Passed: 9 | Failed: 0 | 9/1549 Complete (0%) | No progress 5/5
Test run '707RL00001ewDMZ' was not progressing, cancelling and retrying...
Reset 1/2 before abandoning run
Reusing 9 tests from 4 completed classes; rerunning 1540 remaining tests across 158 classes
Cancelling test run '707RL00001ewDMZYA2'
Test run '707RL00001ewDMZYA2' has been cancelled
Test run started with AsyncApexJob Id: 707RL00001ew2Fe
00:00:00 [Queued] Passed: 0 | Failed: 0 | 0/1540 Complete (0%) | No progress 1/5
00:00:05 [Processing] Passed: 3 | Failed: 0 | 3/1540 Complete (0%)
00:00:11 [Processing] Passed: 29 | Failed: 0 | 29/1540 Complete (1%)