Skip to content

Commit fe54f05

Browse files
committed
test(validator): make the cancellation test reach the guard it documents
Third instance in this PR of the same shape, and the review caught it: a guard added, a test added, and the test never entering the guard's branch. The test canceled the context before calling waitForDeclaredTrainer. getTrainerObject checks ctx.Err() at the top of its first read and returns a 'canceled before checking' Timeout, so the err path returned before the select ever ran. The assertions held — Timeout, not NotFound — but via the probe's pre-existing behavior. Confirmed by control: with the guard deleted, the old test still passed. The cancellation now lands after a probe completes. A reactor cancels and returns NotFound on the first CRD read, and a missing CRD makes isTrainerInstalled return immediately, so that probe is the only one and the next stop is the select with a pending deadline and a canceled parent. Adds the witness the rollout test had and this one lacked: assert the message is the guard's 'canceled while waiting' and not getTrainerObject's 'canceled before checking'. Without a witness that the branch was entered, a test cannot distinguish exercising a guard from bypassing it. Control-verified both ways. Two nits from the same review: The nil-discovery-client comment claimed a fall-through would fail immediately. installTrainer fetches the release archive from GitHub before it touches discovery, so on a machine with egress it would download tens of megabytes first. What actually catches the regression is the assertions — no error, no claimed resources, probes >= 2 — and the comment now says so. Raises the test rollout budget from 20ms to 200ms. It has to cover two full probes of roughly six sequential fake Gets each, plus the sleep, under -race on a shared runner. Both are instantaneous in practice; the larger one is not flaky. Refs #2297 Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
1 parent bf6eba4 commit fe54f05

1 file changed

Lines changed: 37 additions & 9 deletions

File tree

validators/performance/trainer_ensure_test.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ func TestEnsureTrainerInstalled_RecipeDrivenLifecycle(t *testing.T) {
320320
func withShortTrainerWait(t *testing.T) func() {
321321
t.Helper()
322322
oldTimeout, oldInterval := trainerInstallWaitTimeout, trainerInstallPollInterval
323-
trainerInstallWaitTimeout = 20 * time.Millisecond
323+
trainerInstallWaitTimeout = 200 * time.Millisecond
324324
trainerInstallPollInterval = time.Millisecond
325325
return func() {
326326
trainerInstallWaitTimeout, trainerInstallPollInterval = oldTimeout, oldInterval
@@ -333,15 +333,31 @@ func withShortTrainerWait(t *testing.T) func() {
333333
// Both expire the poll context, but they mean opposite things: the deadline means
334334
// the delivered Trainer never became complete, which is the customer's deployment
335335
// defect this PR exists to surface; cancellation means the run was aborted — a
336-
// catalog timeout, a canceled phase, a killed Job — which is not. Reporting the
337-
// second as the first is the same misclassification that made ErrCodeUnavailable
338-
// wrong for the deadline case.
336+
// catalog timeout, a canceled phase, a killed Job — which is not.
337+
//
338+
// The cancellation has to land *after* a probe completes, not before. Canceling up
339+
// front is caught by getTrainerObject's own ctx.Err() check on its first read, which
340+
// returns a "canceled before checking" Timeout through the err path — so the select,
341+
// and the guard being tested, are never reached. An earlier version of this test did
342+
// exactly that and passed with the guard deleted.
343+
//
344+
// The reactor below makes it deterministic: it cancels and returns NotFound on the
345+
// first CRD read, and a missing CRD makes isTrainerInstalled return immediately, so
346+
// that probe is the only one and the next stop is the select.
339347
func TestWaitForDeclaredTrainer_CanceledRunIsNotADeploymentDefect(t *testing.T) {
340348
defer withShortTrainerWait(t)()
341-
client := newTrainerFakeClient()
342349

350+
client := newTrainerFakeClient()
343351
ctx, cancel := context.WithCancel(context.Background())
344-
cancel()
352+
defer cancel()
353+
354+
client.PrependReactor("get", "customresourcedefinitions",
355+
func(k8stesting.Action) (bool, runtime.Object, error) {
356+
cancel()
357+
return true, nil, apierrors.NewNotFound(
358+
schema.GroupResource{Group: "apiextensions.k8s.io", Resource: "customresourcedefinitions"},
359+
"trainjobs.trainer.kubeflow.org")
360+
})
345361

346362
_, err := waitForDeclaredTrainer(ctx, client)
347363
if err == nil {
@@ -354,6 +370,16 @@ func TestWaitForDeclaredTrainer_CanceledRunIsNotADeploymentDefect(t *testing.T)
354370
if !stderrors.Is(err, aicrErrors.New(aicrErrors.ErrCodeTimeout, "")) {
355371
t.Errorf("error code = %v, want ErrCodeTimeout", err)
356372
}
373+
// Witness that the guard ran rather than getTrainerObject's own pre-read check.
374+
// Without this the test passes on the probe-error path and would not notice the
375+
// guard being removed — which is how the earlier version of it was vacuous.
376+
if strings.Contains(err.Error(), "canceled before checking") {
377+
t.Errorf("error %q came from getTrainerObject's pre-read check, not the "+
378+
"cancellation guard in the select; this test is not exercising the guard", err)
379+
}
380+
if !strings.Contains(err.Error(), "canceled while waiting") {
381+
t.Errorf("error %q is not the cancellation guard's message", err)
382+
}
357383
}
358384

359385
// TestEnsureTrainerInstalled_DeclaredRolloutDoesNotFallThrough pins the branch the
@@ -387,9 +413,11 @@ func TestEnsureTrainerInstalled_DeclaredRolloutDoesNotFallThrough(t *testing.T)
387413
}
388414
}
389415

390-
// A nil discovery client makes installTrainer panic or error immediately, so if
391-
// the code falls through to the install path this test fails loudly rather than
392-
// silently passing.
416+
// A nil discovery client is not what catches a fall-through here: installTrainer
417+
// fetches the release archive from GitHub before it touches discovery, so on a
418+
// machine with egress a fall-through would download tens of megabytes first. The
419+
// assertions below are what catch it — no error, no claimed resources, and a
420+
// probe count proving the wait was entered.
393421
refs, err := ensureTrainerInstalled(context.Background(), client, nil, true)
394422
if err != nil {
395423
t.Fatalf("unexpected error: %v", err)

0 commit comments

Comments
 (0)