/* * * 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) }