| /* | |
| * | |
| * Copyright 2026 gRPC 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 grpcsync | |
| import ( | |
| "sync" | |
| "sync/atomic" | |
| "testing" | |
| "google.golang.org/grpc/internal/grpctest" | |
| ) | |
| // Test verifies the scenario where a RefCounted instance is created and its | |
| // reference count is decremented. It verifies that the encapsulated value is | |
| // correctly retrieved and that the onZero callback is invoked when the | |
| // reference count drops to zero. | |
| func (s) TestRefCounted(t *testing.T) { | |
| val := 666 | |
| var onZeroCalled atomic.Bool | |
| rc := NewRefCounted(&val, func() { onZeroCalled.Store(true) }) | |
| if got := rc.Value(); *got != val { | |
| t.Fatalf("Value() = %v, want %v", *got, val) | |
| } | |
| if got := onZeroCalled.Load(); got { | |
| t.Fatalf("Before decrement, onZeroCalled = %v, want false", got) | |
| } | |
| rc.Decrement() | |
| if got := onZeroCalled.Load(); !got { | |
| t.Fatalf("After decrementing count to zero, onZeroCalled = %v, want true", got) | |
| } | |
| } | |
| // Test verifies the scenario where the reference count of a resource is | |
| // explicitly incremented and decremented multiple times. It verifies that the | |
| // onZero callback is only executed when the count drops to zero, and not | |
| // beforehand. | |
| func (s) TestRefCounted_IncrementDecrement(t *testing.T) { | |
| val := 666 | |
| var onZeroCount atomic.Int32 | |
| rc := NewRefCounted(&val, func() { onZeroCount.Add(1) }) | |
| rc.Increment() | |
| rc.Decrement() | |
| if got := onZeroCount.Load(); got != 0 { | |
| t.Fatal("OnZero called unexpectedly after decrementing refcount from 2 to 1") | |
| } | |
| rc.Decrement() | |
| if got := onZeroCount.Load(); got != 1 { | |
| t.Fatalf("After decrementing count from 1 to 0, onZero called = %v times, want 1", got) | |
| } | |
| } | |
| // Test verifies the scenario where TryIncrement is called on both an active and | |
| // a dead resource. It verifies that TryIncrement successfully increments active | |
| // resources but fails on resources whose reference count has already dropped to | |
| // zero. | |
| func (s) TestRefCounted_TryIncrement(t *testing.T) { | |
| val := 666 | |
| var onZeroCount atomic.Int32 | |
| rc := NewRefCounted(&val, func() { onZeroCount.Add(1) }) | |
| if got := rc.TryIncrement(); !got { | |
| t.Fatalf("TryIncrement() on active resource = %v, want true", got) | |
| } | |
| rc.Decrement() | |
| rc.Decrement() | |
| if got := onZeroCount.Load(); got != 1 { | |
| t.Fatalf("After decrementing count to zero, onZero called = %v times, want 1", got) | |
| } | |
| if got := rc.TryIncrement(); got { | |
| t.Fatalf("TryIncrement() on dead resource = %v, want false", got) | |
| } | |
| } | |
| // Test verifies the scenario where multiple goroutines concurrently increment | |
| // and decrement the reference count. It verifies that the reference counting is | |
| // thread-safe and the onZero callback is invoked exactly once. | |
| func (s) TestRefCounted_Concurrent(t *testing.T) { | |
| val := 666 | |
| var onZeroCount atomic.Int32 | |
| rc := NewRefCounted(&val, func() { onZeroCount.Add(1) }) | |
| var wg sync.WaitGroup | |
| const numGoroutines = 100 | |
| for i := 0; i < numGoroutines; i++ { | |
| wg.Go(func() { | |
| if rc.TryIncrement() { | |
| rc.Decrement() | |
| } | |
| }) | |
| } | |
| rc.Decrement() | |
| wg.Wait() | |
| if got := onZeroCount.Load(); got != 1 { | |
| t.Fatalf("After concurrent increments/decrements and final decrement, onZeroCount = %v, want 1", got) | |
| } | |
| } | |
| // Test verifies that Decrementing a resource whose reference count has already | |
| // dropped to zero results in a negative refcount log error. | |
| func (s) TestRefCounted_DecrementNegative(_ *testing.T) { | |
| val := 666 | |
| rc := NewRefCounted(&val, func() {}) | |
| rc.Decrement() | |
| grpctest.ExpectError("Refcount cannot be negative") | |
| rc.Decrement() | |
| } | |
| // Test verifies that Incrementing a dead resource results in a closed resource | |
| // log error. | |
| func (s) TestRefCounted_IncrementDead(_ *testing.T) { | |
| val := 666 | |
| rc := NewRefCounted(&val, func() {}) | |
| rc.Decrement() | |
| grpctest.ExpectError("Resource already closed or dead") | |
| rc.Increment() | |
| } | |
| // Test verifies that NewRefCounted returns an error if the provided onZero | |
| // callback is nil. | |
| func (s) TestNilOnZero(t *testing.T) { | |
| const wantErr = "grpcsync: onZero callback cannot be nil" | |
| defer func() { | |
| if r := recover(); r == nil || r != wantErr { | |
| t.Fatalf("NewRefCounted(_, nil) panic = %v, want: %q", r, wantErr) | |
| } | |
| }() | |
| val := 666 | |
| NewRefCounted(&val, nil) | |
| } |