blob: 6791a395dcef556aa1bf9d89bf4df841c3d52659 [file] [log] [blame]
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package resource
import (
"slices"
"sync"
"sync/atomic"
"testing"
"time"
)
type fake struct {
id int64
closed bool
mu sync.Mutex
}
func (f *fake) Close() error {
f.mu.Lock()
defer f.mu.Unlock()
if f.closed {
panic("duplicate close")
}
f.closed = true
return nil
}
func (f *fake) isClosed() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.closed
}
// fakeTimer allows manual control over time-based events.
type fakeTimer struct {
mu sync.Mutex
fs []func()
}
func newFakeTimer() *fakeTimer {
return &fakeTimer{}
}
func (t *fakeTimer) after(f func()) {
t.mu.Lock()
defer t.mu.Unlock()
t.fs = append(t.fs, f)
}
func (t *fakeTimer) advance(tt *testing.T) {
tt.Helper()
t.mu.Lock()
fs := slices.Clone(t.fs)
t.fs = nil
t.mu.Unlock()
if len(fs) == 0 {
tt.Fatal("timer did not fire")
}
for _, f := range fs {
f()
}
t.fs = nil
}
func TestResource_Reuse(t *testing.T) {
var nextID atomic.Int64
get := func() *fake {
return &fake{id: nextID.Add(1)}
}
timer := newFakeTimer()
r := newAfter(get, timer.after)
f1, release1 := r.Get()
if f1.id != 1 {
t.Fatalf("f1.id = %d, want 1", f1.id)
}
f2, release2 := r.Get()
if f2.id != 1 {
t.Fatalf("f2.id = %d, want 1", f2.id)
}
release1()
if f1.isClosed() {
t.Fatal("f1 closed, want not closed")
}
release2()
if f1.isClosed() {
t.Fatal("f1 closed, want not closed")
}
// The resource holds its own reference, which is released by the timer.
timer.advance(t)
// Now all references are released, it should be closed.
if !f1.isClosed() {
t.Fatal("f1 not closed, want closed")
}
}
func TestResource_Expire(t *testing.T) {
var nextID atomic.Int64
get := func() *fake {
return &fake{id: nextID.Add(1)}
}
timer := newFakeTimer()
r := newAfter(get, timer.after)
f1, release1 := r.Get()
if f1.id != 1 {
t.Fatalf("f1.id = %d, want 1", f1.id)
}
release1() // Release our hold on it.
// Advance time, causing the resource's internal reference to be released.
timer.advance(t)
if !f1.isClosed() {
t.Fatal("f1 not closed, want closed")
}
f2, release2 := r.Get()
if f2.id != 2 {
t.Fatalf("f2.id = %d, want 2", f2.id)
}
release2()
timer.advance(t)
if !f2.isClosed() {
t.Fatal("f2 not closed, want closed")
}
}
func TestResource_Concurrent(t *testing.T) {
var nextID atomic.Int64
get := func() *fake {
return &fake{id: nextID.Add(1)}
}
timer := newFakeTimer()
r := newAfter(get, timer.after)
// Get the first resource so we have a handle to it.
f1, release1 := r.Get()
if f1.id != 1 {
t.Fatalf("f1.id = %d, want 1", f1.id)
}
var wg sync.WaitGroup
for range 10 {
wg.Add(1)
go func() {
defer wg.Done()
f, release := r.Get()
if f.id != 1 {
t.Errorf("got id %d, want 1", f.id)
}
// Hold the resource for a bit to create contention.
time.Sleep(1 * time.Millisecond)
release()
}()
}
wg.Wait()
// All goroutines have released. Now we release our initial hold.
release1()
// At this point, only the resource's own reference remains.
if f1.isClosed() {
t.Fatal("f1 closed, want not closed")
}
// Advance time to release the final reference.
timer.advance(t)
if !f1.isClosed() {
t.Fatal("f1 not closed, want closed")
}
// Getting a new resource should give a new ID.
f2, release2 := r.Get()
if f2.id != 2 {
t.Fatalf("f2.id = %d, want 2", f2.id)
}
release2()
}