package imgcache import ( "context" "errors" "testing" "time" ) // holdDatabase takes the one connection of the test service's database, so // that every other query waits for it, and returns the func that frees it. func holdDatabase(t *testing.T, svc *Service) func() { t.Helper() conn, err := svc.cache.db.Conn(t.Context()) if err != nil { t.Fatalf("failed to take the database connection: %v", err) } release := func() { _ = conn.Close() } t.Cleanup(release) return release } // TestService_Get_ReturnsByItsDeadlineWhileTheDatabaseIsBusy holds the // database's one connection while a request whose fetch is held reaches its // deadline. The request must still return by its deadline with the // deadline's error, and its miss must be counted once the connection is free. func TestService_Get_ReturnsByItsDeadlineWhileTheDatabaseIsBusy(t *testing.T) { t.Parallel() svc, fixtures, fetcher := setupHeldFetchService(t) const timeout = 200 * time.Millisecond ctx, cancel := context.WithTimeout(t.Context(), timeout) defer cancel() results := startGet(ctx, svc, photoVariant(fixtures, 85, FitCover)) // The request has made its database reads by the time it fetches. <-fetcher.started releaseDatabase := holdDatabase(t, svc) select { case got := <-results: deadline, _ := ctx.Deadline() t.Logf("Get() returned %v after its deadline, error = %v", time.Since(deadline), got.err) if !errors.Is(got.err, context.DeadlineExceeded) { t.Errorf("Get() error = %v, want %v", got.err, context.DeadlineExceeded) } case <-time.After(timeout + time.Second): t.Fatal("request did not return by its deadline while the database was busy") } releaseDatabase() waitCtx, cancelWait := context.WithTimeout(t.Context(), 5*time.Second) defer cancelWait() if !svc.WaitForCountWrites(waitCtx) { t.Fatal("the miss was not counted once the database was free") } want := cacheStatsCounters{missCount: 1} if got := readCacheStatsCounters(t, svc.cache); got != want { t.Errorf("counters = %+v, want %+v", got, want) } } // TestService_WaitForCountWrites holds the database's one connection while a // request past its deadline counts a miss. WaitForCountWrites must report the // count unwritten when its context ends, and written once the connection is // free. func TestService_WaitForCountWrites(t *testing.T) { t.Parallel() svc, _, _ := setupHeldFetchService(t) releaseDatabase := holdDatabase(t, svc) ended, cancel := context.WithDeadline(t.Context(), time.Now()) defer cancel() svc.writeCount(ended, func(ctx context.Context) { svc.cache.IncrementStats(ctx, false, 0) }) shortCtx, cancelShort := context.WithTimeout(t.Context(), 100*time.Millisecond) defer cancelShort() written := svc.WaitForCountWrites(shortCtx) t.Logf("WaitForCountWrites() while the database was busy = %t", written) if written { t.Fatal("WaitForCountWrites() = true while the database was busy") } releaseDatabase() waitCtx, cancelWait := context.WithTimeout(t.Context(), 5*time.Second) defer cancelWait() if !svc.WaitForCountWrites(waitCtx) { t.Fatal("WaitForCountWrites() = false once the database was free") } want := cacheStatsCounters{missCount: 1} if got := readCacheStatsCounters(t, svc.cache); got != want { t.Errorf("counters = %+v, want %+v", got, want) } }