go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/clock/testclock/testtimer_test.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package testclock 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 "sync" 22 "testing" 23 "time" 24 25 "go.chromium.org/luci/common/clock" 26 27 . "github.com/smartystreets/goconvey/convey" 28 ) 29 30 // trashTimer is a useless implementation of clock.Timer specifically designed 31 // to exist and not be a test timer type. 32 type trashTimer struct { 33 clock.Timer 34 } 35 36 func TestTestTimer(t *testing.T) { 37 t.Parallel() 38 39 Convey(`A testing clock instance`, t, func() { 40 ctx, cancelFunc := context.WithCancel(context.Background()) 41 defer cancelFunc() 42 43 now := TestTimeLocal 44 clk := New(now) 45 46 Convey(`A timer instance`, func() { 47 t := clk.NewTimer(ctx) 48 49 Convey(`Should have a non-nil C.`, func() { 50 So(t.GetC(), ShouldNotBeNil) 51 }) 52 53 Convey(`When activated`, func() { 54 So(t.Reset(1*time.Second), ShouldBeFalse) 55 56 Convey(`When reset, should return active.`, func() { 57 So(t.Reset(1*time.Hour), ShouldBeTrue) 58 So(t.GetC(), ShouldNotBeNil) 59 }) 60 61 Convey(`When stopped, should return active.`, func() { 62 So(t.Stop(), ShouldBeTrue) 63 So(t.GetC(), ShouldNotBeNil) 64 65 Convey(`And when stopped again, should return inactive.`, func() { 66 So(t.Stop(), ShouldBeFalse) 67 So(t.GetC(), ShouldNotBeNil) 68 }) 69 }) 70 71 Convey(`When stopped after expiring, will return false.`, func() { 72 clk.Add(1 * time.Second) 73 So(t.Stop(), ShouldBeFalse) 74 75 var signalled bool 76 select { 77 case <-t.GetC(): 78 signalled = true 79 default: 80 break 81 } 82 So(signalled, ShouldBeTrue) 83 }) 84 }) 85 86 Convey(`Should successfully signal.`, func() { 87 So(t.Reset(1*time.Second), ShouldBeFalse) 88 clk.Add(1 * time.Second) 89 90 So(<-t.GetC(), ShouldResemble, clock.TimerResult{Time: now.Add(1 * time.Second)}) 91 }) 92 93 Convey(`Should signal immediately if the timer is in the past.`, func() { 94 So(t.Reset(-1*time.Second), ShouldBeFalse) 95 So(<-t.GetC(), ShouldResemble, clock.TimerResult{Time: now}) 96 }) 97 98 Convey(`Will trigger immediately if the Context is canceled when reset.`, func() { 99 cancelFunc() 100 101 // Works for the first timer? 102 So(t.Reset(time.Hour), ShouldBeFalse) 103 So((<-t.GetC()).Err, ShouldEqual, context.Canceled) 104 105 // Works for the second timer? 106 So(t.Reset(time.Hour), ShouldBeFalse) 107 So((<-t.GetC()).Err, ShouldEqual, context.Canceled) 108 }) 109 110 Convey(`Will trigger when the Context is canceled.`, func() { 111 clk.SetTimerCallback(func(time.Duration, clock.Timer) { 112 cancelFunc() 113 }) 114 115 // Works for the first timer? 116 So(t.Reset(time.Hour), ShouldBeFalse) 117 So((<-t.GetC()).Err, ShouldEqual, context.Canceled) 118 119 // Works for the second timer? 120 So(t.Reset(time.Hour), ShouldBeFalse) 121 So((<-t.GetC()).Err, ShouldEqual, context.Canceled) 122 }) 123 124 Convey(`Will use the same channel when reset.`, func() { 125 timerC := t.GetC() 126 t.Reset(time.Second) 127 clk.Add(time.Second) 128 So(<-timerC, ShouldResemble, clock.TimerResult{Time: now.Add(1 * time.Second)}) 129 }) 130 131 Convey(`Will not signal the timer channel if stopped.`, func() { 132 t.Reset(time.Second) 133 So(t.Stop(), ShouldBeTrue) 134 clk.Add(time.Second) 135 136 triggered := false 137 select { 138 case <-t.GetC(): 139 triggered = true 140 default: 141 break 142 } 143 So(triggered, ShouldBeFalse) 144 }) 145 146 Convey(`Will not trigger on previous time thresholds if reset.`, func() { 147 t.Reset(time.Second) 148 t.Reset(2 * time.Second) 149 clk.Add(time.Second) 150 clk.Add(time.Second) 151 So((<-t.GetC()).Time, ShouldResemble, clk.Now()) 152 }) 153 154 Convey(`Can set and retrieve timer tags.`, func() { 155 var tagMu sync.Mutex 156 tagMap := map[string]struct{}{} 157 158 // On the last timer callback, advance time past the timer threshold. 159 timers := make([]clock.Timer, 10) 160 count := 0 161 clk.SetTimerCallback(func(_ time.Duration, t clock.Timer) { 162 tagMu.Lock() 163 defer tagMu.Unlock() 164 165 tagMap[strings.Join(GetTags(t), "")] = struct{}{} 166 count++ 167 if count == len(timers) { 168 clk.Add(time.Second) 169 } 170 }) 171 172 for i := range timers { 173 t := clk.NewTimer(clock.Tag(ctx, fmt.Sprintf("%d", i))) 174 t.Reset(time.Second) 175 timers[i] = t 176 } 177 178 // Wait until all timers have expired. 179 for _, t := range timers { 180 <-t.GetC() 181 } 182 183 // Did we see all of their tags? 184 for i := range timers { 185 _, ok := tagMap[fmt.Sprintf("%d", i)] 186 So(ok, ShouldBeTrue) 187 } 188 }) 189 190 Convey(`A non-test timer has a nil tag.`, func() { 191 t := trashTimer{} 192 So(GetTags(t), ShouldBeNil) 193 }) 194 }) 195 196 Convey(`Multiple goroutines using timers...`, func() { 197 // Mark when timers are started, so we can ensure that our signalling 198 // happens after the timers have been instantiated. 199 timerStartedC := make(chan bool) 200 clk.SetTimerCallback(func(_ time.Duration, _ clock.Timer) { 201 timerStartedC <- true 202 }) 203 204 resultC := make(chan clock.TimerResult) 205 for i := time.Duration(0); i < 5; i++ { 206 go func(d time.Duration) { 207 timer := clk.NewTimer(ctx) 208 timer.Reset(d) 209 resultC <- <-timer.GetC() 210 }(i * time.Second) 211 <-timerStartedC 212 } 213 214 moreResults := func() bool { 215 select { 216 case <-resultC: 217 return true 218 default: 219 return false 220 } 221 } 222 223 // Advance the clock to +2s. Three timers should signal. 224 clk.Set(now.Add(2 * time.Second)) 225 <-resultC 226 <-resultC 227 <-resultC 228 So(moreResults(), ShouldBeFalse) 229 230 // Advance clock to +3s. One timer should signal. 231 clk.Set(now.Add(3 * time.Second)) 232 <-resultC 233 So(moreResults(), ShouldBeFalse) 234 235 // Advance clock to +10s. One final timer should signal. 236 clk.Set(now.Add(10 * time.Second)) 237 <-resultC 238 So(moreResults(), ShouldBeFalse) 239 }) 240 }) 241 } 242 243 func TestTimerTags(t *testing.T) { 244 t.Parallel() 245 246 Convey(`A context with tags {"A", "B"}`, t, func() { 247 c := clock.Tag(clock.Tag(context.Background(), "A"), "B") 248 249 Convey(`Has tags, {"A", "B"}`, func() { 250 So(clock.Tags(c), ShouldResemble, []string{"A", "B"}) 251 }) 252 253 Convey(`Will be retained by a testclock.Timer.`, func() { 254 tc := New(TestTimeUTC) 255 t := tc.NewTimer(c) 256 257 So(GetTags(t), ShouldResemble, []string{"A", "B"}) 258 259 Convey(`The timer tests positive for tags {"A", "B"}`, func() { 260 So(HasTags(t, "A", "B"), ShouldBeTrue) 261 }) 262 263 Convey(`The timer tests negative for tags {"A"}, {"B"}, and {"A", "C"}`, func() { 264 So(HasTags(t, "A"), ShouldBeFalse) 265 So(HasTags(t, "B"), ShouldBeFalse) 266 So(HasTags(t, "A", "C"), ShouldBeFalse) 267 }) 268 }) 269 270 Convey(`A non-test timer tests negative for {"A"} and {"A", "B"}.`, func() { 271 t := &trashTimer{} 272 273 So(HasTags(t, "A"), ShouldBeFalse) 274 So(HasTags(t, "A", "B"), ShouldBeFalse) 275 }) 276 }) 277 }