internal/clock/simulated_clock.go (64 lines of code) (raw):

// Copyright 2025 Google LLC // // 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 clock import ( "sync" "time" ) // afterRequest holds the information for a pending After call in SimulatedClock. type afterRequest struct { targetTime time.Time ch chan time.Time } // SimulatedClock is a clock that allows for manipulation of the time, // which does not change unless AdvanceTime or SetTime is called. // The zero value is a clock initialized to the zero time. type SimulatedClock struct { mu sync.RWMutex t time.Time // GUARDED_BY(mu) pending []*afterRequest // GUARDED_BY(mu) } func NewSimulatedClock(startTime time.Time) *SimulatedClock { return &SimulatedClock{ t: startTime, pending: nil, } } func (sc *SimulatedClock) Now() time.Time { sc.mu.RLock() defer sc.mu.RUnlock() return sc.t } // SetTime sets the current time according to the clock. // It also processes any pending After calls that should fire. func (sc *SimulatedClock) SetTime(t time.Time) { sc.mu.Lock() defer sc.mu.Unlock() sc.t = t sc.processPending() } // AdvanceTime advances the current time according to the clock by the supplied duration. // It also processes any pending After calls that should fire. func (sc *SimulatedClock) AdvanceTime(d time.Duration) { sc.mu.Lock() defer sc.mu.Unlock() sc.t = sc.t.Add(d) sc.processPending() } // After returns a time channel and the expectedFired time is sent over the returned // channel after the provided duration. // If the duration is zero or negative, it sends the current simulated time immediately. func (sc *SimulatedClock) After(d time.Duration) <-chan time.Time { sc.mu.Lock() defer sc.mu.Unlock() ch := make(chan time.Time, 1) effectiveTargetTime := sc.t.Add(d) // If duration is non-positive, fire immediately with the current time. // Or if the target time is not after the current time (e.g. d <= 0) if !effectiveTargetTime.After(sc.t) { ch <- sc.t // Send current time as per time.After behavior for d <= 0 return ch } // Otherwise, schedule it. ar := &afterRequest{ targetTime: effectiveTargetTime, ch: ch, } sc.pending = append(sc.pending, ar) return ch } // processPending checks all pending After requests and fires those // whose target time has been reached or passed by the current simulated time. // This method must be called with sc.mu held. func (sc *SimulatedClock) processPending() { var stillPending []*afterRequest for _, ar := range sc.pending { // If current time sc.t is not before the targetTime (i.e., sc.t >= ar.targetTime) if !sc.t.Before(ar.targetTime) { ar.ch <- ar.targetTime // Send the time it was scheduled to fire // Do not close the channel, to mimic time.After behavior } else { stillPending = append(stillPending, ar) } } sc.pending = stillPending }