// Copyright 2025 The etcd Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cache import ( "context" "errors" "sync" "testing" "time" ) func TestWaitMethods(t *testing.T) { tests := []struct { name string initialReady bool expectReady bool waitMethod string expectBlock bool }{ { name: "not_ready_testing_WaitNotReady", initialReady: false, expectReady: false, waitMethod: "not_ready", expectBlock: false, }, { name: "not_ready_testing_WaitReady", initialReady: false, expectReady: false, waitMethod: "ready", expectBlock: true, }, { name: "ready_testing_WaitReady", initialReady: true, expectReady: true, waitMethod: "ready", expectBlock: false, }, { name: "ready_testing_WaitNotReady", initialReady: true, expectReady: true, waitMethod: "not_ready", expectBlock: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := newReady() if tt.initialReady { r.Set() } if got := r.Ready(); got != tt.expectReady { t.Fatalf("Ready() = %t; want %t", got, tt.expectReady) } ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) defer cancel() var err error switch tt.waitMethod { case "ready": err = r.WaitReady(ctx) case "not_ready": err = r.WaitNotReady(ctx) default: t.Fatalf("invalid waitMethod: %s", tt.waitMethod) } if tt.expectBlock { if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("expected timeout but got: %v", err) } } else { if err != nil { t.Fatalf("expected immediate return but got error: %v", err) } } }) } } func TestSetUnblocksWaiters(t *testing.T) { testStateTransitionUnblocksWaiters(t, false, true, (*ready).Set, true, "WaitReady") } func TestResetUnblocksWaiters(t *testing.T) { testStateTransitionUnblocksWaiters(t, true, false, (*ready).Reset, false, "WaitNotReady") } func testStateTransitionUnblocksWaiters(t *testing.T, initialSet bool, waitForReady bool, transition func(*ready), expectedReady bool, waitMethodName string) { cases := []struct { name string n int }{ {"one_waiter", 1}, {"several_waiters", 16}, {"many_waiters", 128}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { r := newReady() if initialSet { r.Set() } var startWg sync.WaitGroup var readyWg sync.WaitGroup errs := make(chan error, tc.n) for i := 0; i < tc.n; i++ { startWg.Add(1) readyWg.Add(1) go func() { defer readyWg.Done() startWg.Done() ctx, cancel := context.WithTimeout(t.Context(), time.Second) defer cancel() if waitForReady { errs <- r.WaitReady(ctx) } else { errs <- r.WaitNotReady(ctx) } }() } startWg.Wait() time.Sleep(50 * time.Millisecond) transition(r) readyWg.Wait() for i := 0; i < tc.n; i++ { if err := <-errs; err != nil { t.Fatalf("waiter %d: %s = %v; want: nil", i, waitMethodName, err) } } if r.Ready() != expectedReady { t.Fatalf("Ready() = %t after transition; want %t", r.Ready(), expectedReady) } if waitForReady { if err := r.WaitReady(t.Context()); err != nil { t.Fatalf("immediate WaitReady() after transition = %v; want: nil", err) } } else { if err := r.WaitNotReady(t.Context()); err != nil { t.Fatalf("immediate WaitNotReady() after transition = %v; want: nil", err) } } }) } } func TestIdempotentStateTransitions(t *testing.T) { r := newReady() r.Set() r.Set() if !r.Ready() { t.Fatalf("Ready() = false after double Set(); want: true") } if err := r.WaitReady(t.Context()); err != nil { t.Fatalf("WaitReady() after double Set() = %v; want: nil", err) } r.Reset() r.Reset() if r.Ready() { t.Fatalf("Ready() = true after double Reset(); want: false") } if err := r.WaitNotReady(t.Context()); err != nil { t.Fatalf("WaitNotReady() after double Reset() = %v; want ", err) } }