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  }