summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorPercy Wegmann <percy@tailscale.com>2025-12-10 12:46:34 -0600
committerPercy Wegmann <percy@tailscale.com>2025-12-10 12:46:34 -0600
commitb6ac2220be84ae9aefd7e8ec7634a3ec6d2a36f2 (patch)
tree19f2f4d087544d4ca21a415baec2d10ffcd5a47c
parentd349370e5500e6f583a15e38ad945199e5e11ea1 (diff)
downloadtailscale-percy/corp35008.tar.xz
tailscale-percy/corp35008.zip
Try to get rid of tstest.Clockpercy/corp35008
Signed-off-by: Percy Wegmann <percy@tailscale.com>
-rw-r--r--appc/appconnector_test.go71
-rw-r--r--cmd/k8s-operator/api-server-proxy-pg_test.go4
-rw-r--r--cmd/k8s-operator/connector_test.go10
-rw-r--r--cmd/k8s-operator/dnsrecords_test.go4
-rw-r--r--cmd/k8s-operator/egress-eps_test.go4
-rw-r--r--cmd/k8s-operator/egress-pod-readiness_test.go6
-rw-r--r--cmd/k8s-operator/egress-services-readiness_test.go3
-rw-r--r--cmd/k8s-operator/egress-services_test.go5
-rw-r--r--cmd/k8s-operator/ingress_test.go6
-rw-r--r--cmd/k8s-operator/nameserver_test.go4
-rw-r--r--cmd/k8s-operator/nodeport-services-ports_test.go4
-rw-r--r--cmd/k8s-operator/operator_test.go362
-rw-r--r--cmd/k8s-operator/proxyclass_test.go4
-rw-r--r--cmd/k8s-operator/proxygroup_test.go18
-rw-r--r--cmd/k8s-operator/svc-for-pg_test.go6
-rw-r--r--cmd/k8s-operator/tsrecorder_test.go4
-rw-r--r--derp/client_test.go6
-rw-r--r--drive/driveimpl/dirfs/dirfs_test.go6
-rw-r--r--feature/taildrop/peerapi_test.go4
-rw-r--r--health/health_test.go2
-rw-r--r--ipn/ipnlocal/bus_test.go150
-rw-r--r--ipn/ipnlocal/network-lock_test.go4
-rw-r--r--tstest/clock.go694
-rw-r--r--tstest/clock_test.go2474
-rw-r--r--tstime/tstime.go2
25 files changed, 346 insertions, 3511 deletions
diff --git a/appc/appconnector_test.go b/appc/appconnector_test.go
index 5c362d6fd..ec2561fbb 100644
--- a/appc/appconnector_test.go
+++ b/appc/appconnector_test.go
@@ -11,13 +11,13 @@ import (
"slices"
"sync/atomic"
"testing"
+ "testing/synctest"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc/appctest"
- "tailscale.com/tstest"
"tailscale.com/types/appctype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus/eventbustest"
@@ -689,50 +689,51 @@ func TestRoutesWithout(t *testing.T) {
}
func TestRateLogger(t *testing.T) {
- clock := tstest.Clock{}
- wasCalled := false
- rl := newRateLogger(func() time.Time { return clock.Now() }, 1*time.Second, func(count int64, _ time.Time, _ int64) {
- if count != 3 {
- t.Fatalf("count for prev period: got %d, want 3", count)
+ synctest.Test(t, func(t *testing.T) {
+ wasCalled := false
+ rl := newRateLogger(time.Now, 1*time.Second, func(count int64, _ time.Time, _ int64) {
+ if count != 3 {
+ t.Fatalf("count for prev period: got %d, want 3", count)
+ }
+ wasCalled = true
+ })
+
+ for i := 0; i < 3; i++ {
+ time.Sleep(1 * time.Millisecond)
+ rl.update(0)
+ if wasCalled {
+ t.Fatalf("wasCalled: got true, want false")
+ }
}
- wasCalled = true
- })
- for i := 0; i < 3; i++ {
- clock.Advance(1 * time.Millisecond)
+ time.Sleep(1 * time.Second)
rl.update(0)
- if wasCalled {
- t.Fatalf("wasCalled: got true, want false")
+ if !wasCalled {
+ t.Fatalf("wasCalled: got false, want true")
}
- }
- clock.Advance(1 * time.Second)
- rl.update(0)
- if !wasCalled {
- t.Fatalf("wasCalled: got false, want true")
- }
+ wasCalled = false
+ rl = newRateLogger(time.Now, 1*time.Hour, func(count int64, _ time.Time, _ int64) {
+ if count != 3 {
+ t.Fatalf("count for prev period: got %d, want 3", count)
+ }
+ wasCalled = true
+ })
- wasCalled = false
- rl = newRateLogger(func() time.Time { return clock.Now() }, 1*time.Hour, func(count int64, _ time.Time, _ int64) {
- if count != 3 {
- t.Fatalf("count for prev period: got %d, want 3", count)
+ for i := 0; i < 3; i++ {
+ time.Sleep(1 * time.Minute)
+ rl.update(0)
+ if wasCalled {
+ t.Fatalf("wasCalled: got true, want false")
+ }
}
- wasCalled = true
- })
- for i := 0; i < 3; i++ {
- clock.Advance(1 * time.Minute)
+ time.Sleep(1 * time.Hour)
rl.update(0)
- if wasCalled {
- t.Fatalf("wasCalled: got true, want false")
+ if !wasCalled {
+ t.Fatalf("wasCalled: got false, want true")
}
- }
-
- clock.Advance(1 * time.Hour)
- rl.update(0)
- if !wasCalled {
- t.Fatalf("wasCalled: got false, want true")
- }
+ })
}
func TestRouteStoreMetrics(t *testing.T) {
diff --git a/cmd/k8s-operator/api-server-proxy-pg_test.go b/cmd/k8s-operator/api-server-proxy-pg_test.go
index dee505723..1483db881 100644
--- a/cmd/k8s-operator/api-server-proxy-pg_test.go
+++ b/cmd/k8s-operator/api-server-proxy-pg_test.go
@@ -23,7 +23,7 @@ import (
"tailscale.com/kube/k8s-proxy/conf"
"tailscale.com/kube/kubetypes"
"tailscale.com/tailcfg"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
)
@@ -124,7 +124,7 @@ func TestAPIServerProxyReconciler(t *testing.T) {
logger: zap.Must(zap.NewDevelopment()).Sugar(),
recorder: record.NewFakeRecorder(10),
lc: lc,
- clock: tstest.NewClock(tstest.ClockOpts{}),
+ clock: tstime.StdClock{},
operatorID: "self-id",
}
diff --git a/cmd/k8s-operator/connector_test.go b/cmd/k8s-operator/connector_test.go
index afc7d2d6e..e7623f8a1 100644
--- a/cmd/k8s-operator/connector_test.go
+++ b/cmd/k8s-operator/connector_test.go
@@ -21,7 +21,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
)
@@ -57,7 +57,7 @@ func TestConnector(t *testing.T) {
t.Fatal(err)
}
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
cr := &ConnectorReconciler{
Client: fc,
recorder: record.NewFakeRecorder(10),
@@ -247,7 +247,7 @@ func TestConnectorWithProxyClass(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
cr := &ConnectorReconciler{
Client: fc,
clock: cl,
@@ -340,7 +340,7 @@ func TestConnectorWithAppConnector(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
fr := record.NewFakeRecorder(1)
cr := &ConnectorReconciler{
Client: fc,
@@ -440,7 +440,7 @@ func TestConnectorWithMultipleReplicas(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
fr := record.NewFakeRecorder(1)
cr := &ConnectorReconciler{
Client: fc,
diff --git a/cmd/k8s-operator/dnsrecords_test.go b/cmd/k8s-operator/dnsrecords_test.go
index 13898078f..dac41a56b 100644
--- a/cmd/k8s-operator/dnsrecords_test.go
+++ b/cmd/k8s-operator/dnsrecords_test.go
@@ -24,7 +24,7 @@ import (
operatorutils "tailscale.com/k8s-operator"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/ptr"
)
@@ -65,7 +65,7 @@ func TestDNSRecordsReconciler(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
// Set the ready condition of the DNSConfig
mustUpdateStatus(t, fc, "", "test", func(c *tsapi.DNSConfig) {
operatorutils.SetDNSConfigCondition(c, tsapi.NameserverReady, metav1.ConditionTrue, reasonNameserverCreated, reasonNameserverCreated, 0, cl, zl.Sugar())
diff --git a/cmd/k8s-operator/egress-eps_test.go b/cmd/k8s-operator/egress-eps_test.go
index bd80112ae..ee04fa3bd 100644
--- a/cmd/k8s-operator/egress-eps_test.go
+++ b/cmd/k8s-operator/egress-eps_test.go
@@ -21,12 +21,12 @@ import (
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/egressservices"
"tailscale.com/kube/kubetypes"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/util/mak"
)
func TestTailscaleEgressEndpointSlices(t *testing.T) {
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
diff --git a/cmd/k8s-operator/egress-pod-readiness_test.go b/cmd/k8s-operator/egress-pod-readiness_test.go
index 3c35d9043..102a226e9 100644
--- a/cmd/k8s-operator/egress-pod-readiness_test.go
+++ b/cmd/k8s-operator/egress-pod-readiness_test.go
@@ -23,7 +23,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/ptr"
)
@@ -35,7 +35,7 @@ func TestEgressPodReadiness(t *testing.T) {
WithStatusSubresource(&corev1.Pod{}).
Build()
zl, _ := zap.NewDevelopment()
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
rec := &egressPodsReconciler{
tsNamespace: "operator-ns",
Client: fc,
@@ -470,7 +470,7 @@ func newSvc(name string, port int32) (*corev1.Service, string) {
return svc, fmt.Sprintf("http://%s.operator-ns.svc.cluster.local:%d/healthz", name, port)
}
-func podSetReady(pod *corev1.Pod, cl *tstest.Clock) {
+func podSetReady(pod *corev1.Pod, cl tstime.Clock) {
pod.Status.Conditions = append(pod.Status.Conditions, corev1.PodCondition{
Type: tsEgressReadinessGate,
Status: corev1.ConditionTrue,
diff --git a/cmd/k8s-operator/egress-services-readiness_test.go b/cmd/k8s-operator/egress-services-readiness_test.go
index fdff4fafa..71b5693db 100644
--- a/cmd/k8s-operator/egress-services-readiness_test.go
+++ b/cmd/k8s-operator/egress-services-readiness_test.go
@@ -18,7 +18,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"
tsoperator "tailscale.com/k8s-operator"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
- "tailscale.com/tstest"
"tailscale.com/tstime"
)
@@ -30,7 +29,7 @@ func TestEgressServiceReadiness(t *testing.T) {
WithStatusSubresource(&tsapi.ProxyGroup{}).
Build()
zl, _ := zap.NewDevelopment()
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
rec := &egressSvcsReadinessReconciler{
tsNamespace: "operator-ns",
Client: fc,
diff --git a/cmd/k8s-operator/egress-services_test.go b/cmd/k8s-operator/egress-services_test.go
index 202804d30..1310bef21 100644
--- a/cmd/k8s-operator/egress-services_test.go
+++ b/cmd/k8s-operator/egress-services_test.go
@@ -23,7 +23,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/egressservices"
- "tailscale.com/tstest"
"tailscale.com/tstime"
)
@@ -54,7 +53,7 @@ func TestTailscaleEgressServices(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
esr := &egressSvcsReconciler{
Client: fc,
@@ -130,7 +129,7 @@ func TestTailscaleEgressServices(t *testing.T) {
})
}
-func validateReadyService(t *testing.T, fc client.WithWatch, esr *egressSvcsReconciler, svc *corev1.Service, clock *tstest.Clock, zl *zap.Logger, cm *corev1.ConfigMap) {
+func validateReadyService(t *testing.T, fc client.WithWatch, esr *egressSvcsReconciler, svc *corev1.Service, clock tstime.Clock, zl *zap.Logger, cm *corev1.ConfigMap) {
expectReconciled(t, esr, "default", "test")
// Verify that a ClusterIP Service has been created.
name := findGenNameForEgressSvcResources(t, fc, svc)
diff --git a/cmd/k8s-operator/ingress_test.go b/cmd/k8s-operator/ingress_test.go
index 038c746a9..6feb6de6f 100644
--- a/cmd/k8s-operator/ingress_test.go
+++ b/cmd/k8s-operator/ingress_test.go
@@ -23,7 +23,7 @@ import (
"tailscale.com/ipn"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
)
@@ -439,7 +439,7 @@ func TestTailscaleIngressWithServiceMonitor(t *testing.T) {
}
func TestIngressProxyClassAnnotation(t *testing.T) {
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
zl := zap.Must(zap.NewDevelopment())
pcLEStaging, pcLEStagingFalse, _ := proxyClassesForLEStagingTest()
@@ -547,7 +547,7 @@ func TestIngressProxyClassAnnotation(t *testing.T) {
}
func TestIngressLetsEncryptStaging(t *testing.T) {
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
zl := zap.Must(zap.NewDevelopment())
pcLEStaging, pcLEStagingFalse, pcOther := proxyClassesForLEStagingTest()
diff --git a/cmd/k8s-operator/nameserver_test.go b/cmd/k8s-operator/nameserver_test.go
index 858cd973d..5b4be97c0 100644
--- a/cmd/k8s-operator/nameserver_test.go
+++ b/cmd/k8s-operator/nameserver_test.go
@@ -22,7 +22,7 @@ import (
operatorutils "tailscale.com/k8s-operator"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
)
@@ -68,7 +68,7 @@ func TestNameserverReconciler(t *testing.T) {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
reconciler := &NameserverReconciler{
Client: fc,
clock: clock,
diff --git a/cmd/k8s-operator/nodeport-services-ports_test.go b/cmd/k8s-operator/nodeport-services-ports_test.go
index 9418bb844..220810f70 100644
--- a/cmd/k8s-operator/nodeport-services-ports_test.go
+++ b/cmd/k8s-operator/nodeport-services-ports_test.go
@@ -13,7 +13,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
)
func TestGetServicesNodePortRangeFromErr(t *testing.T) {
@@ -193,7 +193,7 @@ func TestValidateNodePortRanges(t *testing.T) {
// as part of this test, we want to create an adjacent ProxyClass in order to ensure that if it clashes with the one created in this test
// that we get an error
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
opc := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{
Name: "other-pc",
diff --git a/cmd/k8s-operator/operator_test.go b/cmd/k8s-operator/operator_test.go
index e11235768..9ff976cac 100644
--- a/cmd/k8s-operator/operator_test.go
+++ b/cmd/k8s-operator/operator_test.go
@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"testing"
+ "testing/synctest"
"time"
"github.com/google/go-cmp/cmp"
@@ -28,7 +29,6 @@ import (
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
"tailscale.com/net/dns/resolvconffile"
- "tailscale.com/tstest"
"tailscale.com/tstime"
"tailscale.com/types/ptr"
"tailscale.com/util/dnsname"
@@ -36,185 +36,187 @@ import (
)
func TestLoadBalancerClass(t *testing.T) {
- fc := fake.NewFakeClient()
- ft := &fakeTSClient{}
- zl, err := zap.NewDevelopment()
- if err != nil {
- t.Fatal(err)
- }
- clock := tstest.NewClock(tstest.ClockOpts{})
- sr := &ServiceReconciler{
- Client: fc,
- ssr: &tailscaleSTSReconciler{
- Client: fc,
- tsClient: ft,
- defaultTags: []string{"tag:k8s"},
- operatorNamespace: "operator-ns",
- proxyImage: "tailscale/tailscale",
- },
- logger: zl.Sugar(),
- clock: clock,
- recorder: record.NewFakeRecorder(100),
- }
+ synctest.Test(t, func(t *testing.T) {
+ fc := fake.NewFakeClient()
+ ft := &fakeTSClient{}
+ zl, err := zap.NewDevelopment()
+ if err != nil {
+ t.Fatal(err)
+ }
+ clock := tstime.StdClock{}
+ sr := &ServiceReconciler{
+ Client: fc,
+ ssr: &tailscaleSTSReconciler{
+ Client: fc,
+ tsClient: ft,
+ defaultTags: []string{"tag:k8s"},
+ operatorNamespace: "operator-ns",
+ proxyImage: "tailscale/tailscale",
+ },
+ logger: zl.Sugar(),
+ clock: clock,
+ recorder: record.NewFakeRecorder(100),
+ }
- // Create a service that we should manage, but start with a miconfiguration
- // in the annotations.
- mustCreate(t, fc, &corev1.Service{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test",
- Namespace: "default",
- // The apiserver is supposed to set the UID, but the fake client
- // doesn't. So, set it explicitly because other code later depends
- // on it being set.
- UID: types.UID("1234-UID"),
- Annotations: map[string]string{
- AnnotationTailnetTargetFQDN: "invalid.example.com",
+ // Create a service that we should manage, but start with a miconfiguration
+ // in the annotations.
+ mustCreate(t, fc, &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ Namespace: "default",
+ // The apiserver is supposed to set the UID, but the fake client
+ // doesn't. So, set it explicitly because other code later depends
+ // on it being set.
+ UID: types.UID("1234-UID"),
+ Annotations: map[string]string{
+ AnnotationTailnetTargetFQDN: "invalid.example.com",
+ },
},
- },
- Spec: corev1.ServiceSpec{
- ClusterIP: "10.20.30.40",
- Type: corev1.ServiceTypeLoadBalancer,
- LoadBalancerClass: ptr.To("tailscale"),
- },
- })
+ Spec: corev1.ServiceSpec{
+ ClusterIP: "10.20.30.40",
+ Type: corev1.ServiceTypeLoadBalancer,
+ LoadBalancerClass: ptr.To("tailscale"),
+ },
+ })
- expectReconciled(t, sr, "default", "test")
+ expectReconciled(t, sr, "default", "test")
- // The expected value of .status.conditions[0].LastTransitionTime until the
- // proxy becomes ready.
- t0 := conditionTime(clock)
+ // The expected value of .status.conditions[0].LastTransitionTime until the
+ // proxy becomes ready.
+ t0 := conditionTime(clock)
- // Should have an error about invalid config.
- want := &corev1.Service{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test",
- Namespace: "default",
- UID: types.UID("1234-UID"),
- Annotations: map[string]string{
- AnnotationTailnetTargetFQDN: "invalid.example.com",
+ // Should have an error about invalid config.
+ want := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ Namespace: "default",
+ UID: types.UID("1234-UID"),
+ Annotations: map[string]string{
+ AnnotationTailnetTargetFQDN: "invalid.example.com",
+ },
},
- },
- Spec: corev1.ServiceSpec{
- ClusterIP: "10.20.30.40",
- Type: corev1.ServiceTypeLoadBalancer,
- LoadBalancerClass: ptr.To("tailscale"),
- },
- Status: corev1.ServiceStatus{
+ Spec: corev1.ServiceSpec{
+ ClusterIP: "10.20.30.40",
+ Type: corev1.ServiceTypeLoadBalancer,
+ LoadBalancerClass: ptr.To("tailscale"),
+ },
+ Status: corev1.ServiceStatus{
+ Conditions: []metav1.Condition{{
+ Type: string(tsapi.ProxyReady),
+ Status: metav1.ConditionFalse,
+ LastTransitionTime: t0,
+ Reason: reasonProxyInvalid,
+ Message: `unable to provision proxy resources: invalid Service: invalid value of annotation tailscale.com/tailnet-fqdn: "invalid.example.com" does not appear to be a valid MagicDNS name`,
+ }},
+ },
+ }
+ expectEqual(t, fc, want)
+
+ // Delete the misconfiguration so the proxy starts getting created on the
+ // next reconcile.
+ mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
+ s.ObjectMeta.Annotations = nil
+ })
+
+ time.Sleep(time.Second)
+ expectReconciled(t, sr, "default", "test")
+
+ fullName, shortName := findGenName(t, fc, "default", "test", "svc")
+ opts := configOpts{
+ replicas: ptr.To[int32](1),
+ stsName: shortName,
+ secretName: fullName,
+ namespace: "default",
+ parentType: "svc",
+ hostname: "default-test",
+ clusterTargetIP: "10.20.30.40",
+ app: kubetypes.AppIngressProxy,
+ }
+
+ expectEqual(t, fc, expectedSecret(t, fc, opts))
+ expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
+ expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
+
+ want.Annotations = nil
+ want.ObjectMeta.Finalizers = []string{"tailscale.com/finalizer"}
+ want.Status = corev1.ServiceStatus{
Conditions: []metav1.Condition{{
Type: string(tsapi.ProxyReady),
Status: metav1.ConditionFalse,
- LastTransitionTime: t0,
- Reason: reasonProxyInvalid,
- Message: `unable to provision proxy resources: invalid Service: invalid value of annotation tailscale.com/tailnet-fqdn: "invalid.example.com" does not appear to be a valid MagicDNS name`,
+ LastTransitionTime: t0, // Status is still false, no update to transition time
+ Reason: reasonProxyPending,
+ Message: "no Tailscale hostname known yet, waiting for proxy pod to finish auth",
}},
- },
- }
- expectEqual(t, fc, want)
-
- // Delete the misconfiguration so the proxy starts getting created on the
- // next reconcile.
- mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
- s.ObjectMeta.Annotations = nil
- })
-
- clock.Advance(time.Second)
- expectReconciled(t, sr, "default", "test")
+ }
+ expectEqual(t, fc, want)
- fullName, shortName := findGenName(t, fc, "default", "test", "svc")
- opts := configOpts{
- replicas: ptr.To[int32](1),
- stsName: shortName,
- secretName: fullName,
- namespace: "default",
- parentType: "svc",
- hostname: "default-test",
- clusterTargetIP: "10.20.30.40",
- app: kubetypes.AppIngressProxy,
- }
+ // Normally the Tailscale proxy pod would come up here and write its info
+ // into the secret. Simulate that, then verify reconcile again and verify
+ // that we get to the end.
+ mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
+ if s.Data == nil {
+ s.Data = map[string][]byte{}
+ }
+ s.Data["device_id"] = []byte("ts-id-1234")
+ s.Data["device_fqdn"] = []byte("tailscale.device.name.")
+ s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`)
+ })
+ time.Sleep(time.Second)
+ expectReconciled(t, sr, "default", "test")
+ want.Status.Conditions = proxyCreatedCondition(clock)
+ want.Status.LoadBalancer = corev1.LoadBalancerStatus{
+ Ingress: []corev1.LoadBalancerIngress{
+ {
+ Hostname: "tailscale.device.name",
+ },
+ {
+ IP: "100.99.98.97",
+ },
+ },
+ }
- expectEqual(t, fc, expectedSecret(t, fc, opts))
- expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
- expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs)
+ // Perform an additional reconciliation loop here to ensure resources don't change through side effects. Mainly
+ // to prevent infinite reconciliation
+ expectReconciled(t, sr, "default", "test")
+ expectEqual(t, fc, want)
- want.Annotations = nil
- want.ObjectMeta.Finalizers = []string{"tailscale.com/finalizer"}
- want.Status = corev1.ServiceStatus{
- Conditions: []metav1.Condition{{
- Type: string(tsapi.ProxyReady),
- Status: metav1.ConditionFalse,
- LastTransitionTime: t0, // Status is still false, no update to transition time
- Reason: reasonProxyPending,
- Message: "no Tailscale hostname known yet, waiting for proxy pod to finish auth",
- }},
- }
- expectEqual(t, fc, want)
+ // Turn the service back into a ClusterIP service, which should make the
+ // operator clean up.
+ mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
+ s.Spec.Type = corev1.ServiceTypeClusterIP
+ s.Spec.LoadBalancerClass = nil
+ })
+ mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) {
+ // Fake client doesn't automatically delete the LoadBalancer status when
+ // changing away from the LoadBalancer type, we have to do
+ // controller-manager's work by hand.
+ s.Status = corev1.ServiceStatus{}
+ })
+ // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
+ // didn't create any child resources since this is all faked, so the
+ // deletion goes through immediately.
+ expectReconciled(t, sr, "default", "test")
+ expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
+ // The deletion triggers another reconcile, to finish the cleanup.
+ expectReconciled(t, sr, "default", "test")
+ expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
+ expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
+ expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
- // Normally the Tailscale proxy pod would come up here and write its info
- // into the secret. Simulate that, then verify reconcile again and verify
- // that we get to the end.
- mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
- if s.Data == nil {
- s.Data = map[string][]byte{}
- }
- s.Data["device_id"] = []byte("ts-id-1234")
- s.Data["device_fqdn"] = []byte("tailscale.device.name.")
- s.Data["device_ips"] = []byte(`["100.99.98.97", "2c0a:8083:94d4:2012:3165:34a5:3616:5fdf"]`)
- })
- clock.Advance(time.Second)
- expectReconciled(t, sr, "default", "test")
- want.Status.Conditions = proxyCreatedCondition(clock)
- want.Status.LoadBalancer = corev1.LoadBalancerStatus{
- Ingress: []corev1.LoadBalancerIngress{
- {
- Hostname: "tailscale.device.name",
+ // Note that the Tailscale-specific condition status should be gone now.
+ want = &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ Namespace: "default",
+ UID: types.UID("1234-UID"),
},
- {
- IP: "100.99.98.97",
+ Spec: corev1.ServiceSpec{
+ ClusterIP: "10.20.30.40",
+ Type: corev1.ServiceTypeClusterIP,
},
- },
- }
-
- // Perform an additional reconciliation loop here to ensure resources don't change through side effects. Mainly
- // to prevent infinite reconciliation
- expectReconciled(t, sr, "default", "test")
- expectEqual(t, fc, want)
-
- // Turn the service back into a ClusterIP service, which should make the
- // operator clean up.
- mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
- s.Spec.Type = corev1.ServiceTypeClusterIP
- s.Spec.LoadBalancerClass = nil
- })
- mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) {
- // Fake client doesn't automatically delete the LoadBalancer status when
- // changing away from the LoadBalancer type, we have to do
- // controller-manager's work by hand.
- s.Status = corev1.ServiceStatus{}
+ }
+ expectEqual(t, fc, want)
})
- // synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
- // didn't create any child resources since this is all faked, so the
- // deletion goes through immediately.
- expectReconciled(t, sr, "default", "test")
- expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
- // The deletion triggers another reconcile, to finish the cleanup.
- expectReconciled(t, sr, "default", "test")
- expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
- expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
- expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
-
- // Note that the Tailscale-specific condition status should be gone now.
- want = &corev1.Service{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test",
- Namespace: "default",
- UID: types.UID("1234-UID"),
- },
- Spec: corev1.ServiceSpec{
- ClusterIP: "10.20.30.40",
- Type: corev1.ServiceTypeClusterIP,
- },
- }
- expectEqual(t, fc, want)
}
func TestTailnetTargetFQDNAnnotation(t *testing.T) {
@@ -225,7 +227,7 @@ func TestTailnetTargetFQDNAnnotation(t *testing.T) {
t.Fatal(err)
}
tailnetTargetFQDN := "foo.bar.ts.net."
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -338,7 +340,7 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
t.Fatal(err)
}
tailnetTargetIP := "100.66.66.66"
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -450,7 +452,7 @@ func TestTailnetTargetIPAnnotation_IPCouldNotBeParsed(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -521,7 +523,7 @@ func TestTailnetTargetIPAnnotation_InvalidIP(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -592,7 +594,7 @@ func TestAnnotations(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -699,7 +701,7 @@ func TestAnnotationIntoLB(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -832,7 +834,7 @@ func TestLBIntoAnnotation(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -970,7 +972,7 @@ func TestCustomHostname(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -1082,7 +1084,7 @@ func TestCustomPriorityClassName(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -1137,7 +1139,7 @@ func TestCustomPriorityClassName(t *testing.T) {
}
func TestServiceProxyClassAnnotation(t *testing.T) {
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
zl := zap.Must(zap.NewDevelopment())
pcIfNotPresent := &tsapi.ProxyClass{
@@ -1337,7 +1339,7 @@ func TestProxyClassForService(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -1429,7 +1431,7 @@ func TestDefaultLoadBalancer(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -1486,7 +1488,7 @@ func TestProxyFirewallMode(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -1813,7 +1815,7 @@ func Test_authKeyRemoval(t *testing.T) {
// 1. A new Service that should be exposed via Tailscale gets created, a Secret with a config that contains auth
// key is generated.
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -1881,7 +1883,7 @@ func Test_externalNameService(t *testing.T) {
// 1. A External name Service that should be exposed via Tailscale gets
// created.
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -1978,7 +1980,7 @@ func Test_metricsResourceCreation(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
@@ -2052,7 +2054,7 @@ func TestIgnorePGService(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
diff --git a/cmd/k8s-operator/proxyclass_test.go b/cmd/k8s-operator/proxyclass_test.go
index ae0f63d99..f681d9470 100644
--- a/cmd/k8s-operator/proxyclass_test.go
+++ b/cmd/k8s-operator/proxyclass_test.go
@@ -20,7 +20,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"
tsoperator "tailscale.com/k8s-operator"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
)
func TestProxyClass(t *testing.T) {
@@ -60,7 +60,7 @@ func TestProxyClass(t *testing.T) {
t.Fatal(err)
}
fr := record.NewFakeRecorder(3) // bump this if you expect a test case to throw more events
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
pcr := &ProxyClassReconciler{
Client: fc,
logger: zl.Sugar(),
diff --git a/cmd/k8s-operator/proxygroup_test.go b/cmd/k8s-operator/proxygroup_test.go
index 2bcc9fb7a..06903c959 100644
--- a/cmd/k8s-operator/proxygroup_test.go
+++ b/cmd/k8s-operator/proxygroup_test.go
@@ -34,7 +34,7 @@ import (
"tailscale.com/kube/k8s-proxy/conf"
"tailscale.com/kube/kubetypes"
"tailscale.com/tailcfg"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
)
@@ -592,7 +592,7 @@ func TestProxyGroupWithStaticEndpoints(t *testing.T) {
tsClient := &fakeTSClient{}
zl, _ := zap.NewDevelopment()
fr := record.NewFakeRecorder(10)
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
pc := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{
@@ -834,7 +834,7 @@ func TestProxyGroup(t *testing.T) {
tsClient := &fakeTSClient{}
zl, _ := zap.NewDevelopment()
fr := record.NewFakeRecorder(1)
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
reconciler := &ProxyGroupReconciler{
tsNamespace: tsNamespace,
tsProxyImage: testProxyImage,
@@ -1051,7 +1051,7 @@ func TestProxyGroupTypes(t *testing.T) {
Client: fc,
log: zl.Sugar(),
tsClient: &fakeTSClient{},
- clock: tstest.NewClock(tstest.ClockOpts{}),
+ clock: tstime.StdClock{},
}
t.Run("egress_type", func(t *testing.T) {
@@ -1291,7 +1291,7 @@ func TestKubeAPIServerStatusConditionFlow(t *testing.T) {
Client: fc,
log: zap.Must(zap.NewDevelopment()).Sugar(),
tsClient: &fakeTSClient{},
- clock: tstest.NewClock(tstest.ClockOpts{}),
+ clock: tstime.StdClock{},
}
expectReconciled(t, r, "", pg.Name)
@@ -1344,7 +1344,7 @@ func TestKubeAPIServerType_DoesNotOverwriteServicesConfig(t *testing.T) {
Client: fc,
log: zap.Must(zap.NewDevelopment()).Sugar(),
tsClient: &fakeTSClient{},
- clock: tstest.NewClock(tstest.ClockOpts{}),
+ clock: tstime.StdClock{},
}
pg := &tsapi.ProxyGroup{
@@ -1429,7 +1429,7 @@ func TestIngressAdvertiseServicesConfigPreserved(t *testing.T) {
Client: fc,
log: zap.Must(zap.NewDevelopment()).Sugar(),
tsClient: &fakeTSClient{},
- clock: tstest.NewClock(tstest.ClockOpts{}),
+ clock: tstime.StdClock{},
}
existingServices := []string{"svc1", "svc2"}
@@ -1686,7 +1686,7 @@ func proxyClassesForLEStagingTest() (*tsapi.ProxyClass, *tsapi.ProxyClass, *tsap
return pcLEStaging, pcLEStagingFalse, pcOther
}
-func setProxyClassReady(t *testing.T, fc client.Client, cl *tstest.Clock, name string) *tsapi.ProxyClass {
+func setProxyClassReady(t *testing.T, fc client.Client, cl tstime.Clock, name string) *tsapi.ProxyClass {
t.Helper()
pc := &tsapi.ProxyClass{}
if err := fc.Get(t.Context(), client.ObjectKey{Name: name}, pc); err != nil {
@@ -1838,7 +1838,7 @@ func addNodeIDToStateSecrets(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyG
}
func TestProxyGroupLetsEncryptStaging(t *testing.T) {
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
zl := zap.Must(zap.NewDevelopment())
// Set up test cases- most are shared with non-HA Ingress.
diff --git a/cmd/k8s-operator/svc-for-pg_test.go b/cmd/k8s-operator/svc-for-pg_test.go
index baaa07727..31fc0642d 100644
--- a/cmd/k8s-operator/svc-for-pg_test.go
+++ b/cmd/k8s-operator/svc-for-pg_test.go
@@ -27,7 +27,7 @@ import (
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/ingressservices"
"tailscale.com/kube/kubetypes"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
@@ -112,7 +112,7 @@ func TestServicePGReconciler_UpdateHostname(t *testing.T) {
}
}
-func setupServiceTest(t *testing.T) (*HAServiceReconciler, *corev1.Secret, client.Client, *fakeTSClient, *tstest.Clock) {
+func setupServiceTest(t *testing.T) (*HAServiceReconciler, *corev1.Secret, client.Client, *fakeTSClient, tstime.Clock) {
// Pre-create the ProxyGroup
pg := &tsapi.ProxyGroup{
ObjectMeta: metav1.ObjectMeta{
@@ -203,7 +203,7 @@ func setupServiceTest(t *testing.T) (*HAServiceReconciler, *corev1.Secret, clien
},
}
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
svcPGR := &HAServiceReconciler{
Client: fc,
tsClient: ft,
diff --git a/cmd/k8s-operator/tsrecorder_test.go b/cmd/k8s-operator/tsrecorder_test.go
index f7ff797b1..ce7a6fc72 100644
--- a/cmd/k8s-operator/tsrecorder_test.go
+++ b/cmd/k8s-operator/tsrecorder_test.go
@@ -24,7 +24,7 @@ import (
tsoperator "tailscale.com/k8s-operator"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/ptr"
)
@@ -52,7 +52,7 @@ func TestRecorder(t *testing.T) {
tsClient := &fakeTSClient{}
zl, _ := zap.NewDevelopment()
fr := record.NewFakeRecorder(2)
- cl := tstest.NewClock(tstest.ClockOpts{})
+ cl := tstime.StdClock{}
reconciler := &RecorderReconciler{
tsNamespace: tsNamespace,
Client: fc,
diff --git a/derp/client_test.go b/derp/client_test.go
index a731ad197..f9cce8983 100644
--- a/derp/client_test.go
+++ b/derp/client_test.go
@@ -13,7 +13,7 @@ import (
"testing"
"time"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
"tailscale.com/types/key"
)
@@ -79,7 +79,7 @@ func TestClientRecv(t *testing.T) {
nc: dummyNetConn{},
br: bufio.NewReader(bytes.NewReader(tt.input)),
logf: t.Logf,
- clock: &tstest.Clock{},
+ clock: tstime.StdClock{},
}
got, err := c.Recv()
if err != nil {
@@ -186,7 +186,7 @@ func TestClientSendRateLimiting(t *testing.T) {
cw := new(countWriter)
c := &Client{
bw: bufio.NewWriter(cw),
- clock: &tstest.Clock{},
+ clock: tstime.StdClock{},
}
c.setSendRateLimiter(ServerInfoMessage{})
diff --git a/drive/driveimpl/dirfs/dirfs_test.go b/drive/driveimpl/dirfs/dirfs_test.go
index 4d83765d9..00534a997 100644
--- a/drive/driveimpl/dirfs/dirfs_test.go
+++ b/drive/driveimpl/dirfs/dirfs_test.go
@@ -16,7 +16,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/tailscale/xnet/webdav"
"tailscale.com/drive/driveimpl/shared"
- "tailscale.com/tstest"
+ "tailscale.com/tstime"
)
func TestStat(t *testing.T) {
@@ -276,7 +276,7 @@ func TestRename(t *testing.T) {
}
}
-func createFileSystem(t *testing.T) (webdav.FileSystem, string, string, *tstest.Clock) {
+func createFileSystem(t *testing.T) (webdav.FileSystem, string, string, tstime.Clock) {
s1, dir1 := startRemote(t)
s2, dir2 := startRemote(t)
@@ -301,7 +301,7 @@ func createFileSystem(t *testing.T) (webdav.FileSystem, string, string, *tstest.
t.Fatal(err)
}
- clock := tstest.NewClock(tstest.ClockOpts{Start: time.Now()})
+ clock := tstime.StdClock{}
fs := &FS{
Clock: clock,
StaticRoot: "domain",
diff --git a/feature/taildrop/peerapi_test.go b/feature/taildrop/peerapi_test.go
index 254d8794e..72589eeb0 100644
--- a/feature/taildrop/peerapi_test.go
+++ b/feature/taildrop/peerapi_test.go
@@ -501,7 +501,7 @@ func TestHandlePeerAPI(t *testing.T) {
ext := &fakeExtension{
logf: e.logBuf.Logf,
capFileSharing: tt.capSharing,
- clock: &tstest.Clock{},
+ clock: tstime.StdClock{},
taildrop: e.taildrop,
}
e.ph = &peerAPIHandler{
@@ -557,7 +557,7 @@ func TestFileDeleteRace(t *testing.T) {
fakeLB := &fakeExtension{
logf: t.Logf,
capFileSharing: true,
- clock: &tstest.Clock{},
+ clock: tstime.StdClock{},
taildrop: taildropMgr,
}
buf := make([]byte, 2<<20)
diff --git a/health/health_test.go b/health/health_test.go
index af7d06c8f..36157717a 100644
--- a/health/health_test.go
+++ b/health/health_test.go
@@ -984,7 +984,7 @@ func TestCurrentStateETagWarnable(t *testing.T) {
})
t.Run("no_change", func(t *testing.T) {
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
ht1 := newTracker(clock)
ht1.SetUnhealthy(testWarnable, Args{})
diff --git a/ipn/ipnlocal/bus_test.go b/ipn/ipnlocal/bus_test.go
index 5c75ac54d..55c936594 100644
--- a/ipn/ipnlocal/bus_test.go
+++ b/ipn/ipnlocal/bus_test.go
@@ -8,11 +8,11 @@ import (
"reflect"
"slices"
"testing"
+ "testing/synctest"
"time"
"tailscale.com/drive"
"tailscale.com/ipn"
- "tailscale.com/tstest"
"tailscale.com/tstime"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
@@ -78,21 +78,17 @@ func TestIsNotableNotify(t *testing.T) {
}
type rateLimitingBusSenderTester struct {
- tb testing.TB
- got []*ipn.Notify
- clock *tstest.Clock
- s *rateLimitingBusSender
+ tb testing.TB
+ got []*ipn.Notify
+ s *rateLimitingBusSender
}
func (st *rateLimitingBusSenderTester) init() {
if st.s != nil {
return
}
- st.clock = tstest.NewClock(tstest.ClockOpts{
- Start: time.Unix(1731777537, 0), // time I wrote this test :)
- })
st.s = &rateLimitingBusSender{
- clock: tstime.DefaultClock{Clock: st.clock},
+ clock: tstime.DefaultClock{},
fn: func(n *ipn.Notify) bool {
st.got = append(st.got, n)
return true
@@ -110,7 +106,7 @@ func (st *rateLimitingBusSenderTester) send(n *ipn.Notify) {
func (st *rateLimitingBusSenderTester) advance(d time.Duration) {
st.tb.Helper()
- st.clock.Advance(d)
+ time.Sleep(d)
select {
case <-st.s.flushChan():
if !st.s.flush() {
@@ -138,83 +134,87 @@ func TestRateLimitingBusSender(t *testing.T) {
})
t.Run("buffered", func(t *testing.T) {
- st := &rateLimitingBusSenderTester{tb: t}
- st.init()
- st.s.interval = 1 * time.Second
- st.send(&ipn.Notify{Version: "initial"})
- if len(st.got) != 1 {
- t.Fatalf("got %d items; expected 1 (first to flush immediately)", len(st.got))
- }
- st.send(nm1)
- st.send(nm2)
- st.send(eng1)
- st.send(eng2)
- if len(st.got) != 1 {
+ synctest.Test(t, func(t *testing.T) {
+ st := &rateLimitingBusSenderTester{tb: t}
+ st.init()
+ st.s.interval = 1 * time.Second
+ st.send(&ipn.Notify{Version: "initial"})
if len(st.got) != 1 {
- t.Fatalf("got %d items; expected still just that first 1", len(st.got))
+ t.Fatalf("got %d items; expected 1 (first to flush immediately)", len(st.got))
+ }
+ st.send(nm1)
+ st.send(nm2)
+ st.send(eng1)
+ st.send(eng2)
+ if len(st.got) != 1 {
+ if len(st.got) != 1 {
+ t.Fatalf("got %d items; expected still just that first 1", len(st.got))
+ }
}
- }
- // But moving the clock should flush the rest, collasced into one new one.
- st.advance(5 * time.Second)
- if len(st.got) != 2 {
- t.Fatalf("got %d items; want 2", len(st.got))
- }
- gotn := st.got[1]
- if gotn.NetMap != nm2.NetMap {
- t.Errorf("got wrong NetMap; got %p", gotn.NetMap)
- }
- if gotn.Engine != eng2.Engine {
- t.Errorf("got wrong Engine; got %p", gotn.Engine)
- }
- if t.Failed() {
- t.Logf("failed Notify was: %v", logger.AsJSON(gotn))
- }
+ // But moving the clock should flush the rest, collasced into one new one.
+ st.advance(5 * time.Second)
+ if len(st.got) != 2 {
+ t.Fatalf("got %d items; want 2", len(st.got))
+ }
+ gotn := st.got[1]
+ if gotn.NetMap != nm2.NetMap {
+ t.Errorf("got wrong NetMap; got %p", gotn.NetMap)
+ }
+ if gotn.Engine != eng2.Engine {
+ t.Errorf("got wrong Engine; got %p", gotn.Engine)
+ }
+ if t.Failed() {
+ t.Logf("failed Notify was: %v", logger.AsJSON(gotn))
+ }
+ })
})
// Test the Run method
t.Run("run", func(t *testing.T) {
- st := &rateLimitingBusSenderTester{tb: t}
- st.init()
- st.s.interval = 1 * time.Second
- st.s.lastFlush = st.clock.Now() // pretend we just flushed
+ synctest.Test(t, func(t *testing.T) {
+ st := &rateLimitingBusSenderTester{tb: t}
+ st.init()
+ st.s.interval = 1 * time.Second
+ st.s.lastFlush = time.Now() // pretend we just flushed
- flushc := make(chan *ipn.Notify, 1)
- st.s.fn = func(n *ipn.Notify) bool {
- flushc <- n
- return true
- }
- didSend := make(chan bool, 2)
- st.s.didSendTestHook = func() { didSend <- true }
- waitSend := func() {
- select {
- case <-didSend:
- case <-time.After(5 * time.Second):
- t.Error("timeout waiting for call to send")
+ flushc := make(chan *ipn.Notify, 1)
+ st.s.fn = func(n *ipn.Notify) bool {
+ flushc <- n
+ return true
+ }
+ didSend := make(chan bool, 2)
+ st.s.didSendTestHook = func() { didSend <- true }
+ waitSend := func() {
+ select {
+ case <-didSend:
+ case <-time.After(5 * time.Second):
+ t.Error("timeout waiting for call to send")
+ }
}
- }
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
- incoming := make(chan *ipn.Notify, 2)
- go func() {
- incoming <- nm1
- waitSend()
- incoming <- nm2
- waitSend()
- st.advance(5 * time.Second)
- select {
- case n := <-flushc:
- if n.NetMap != nm2.NetMap {
- t.Errorf("got wrong NetMap; got %p", n.NetMap)
+ incoming := make(chan *ipn.Notify, 2)
+ go func() {
+ incoming <- nm1
+ waitSend()
+ incoming <- nm2
+ waitSend()
+ st.advance(5 * time.Second)
+ select {
+ case n := <-flushc:
+ if n.NetMap != nm2.NetMap {
+ t.Errorf("got wrong NetMap; got %p", n.NetMap)
+ }
+ case <-time.After(10 * time.Second):
+ t.Error("timeout")
}
- case <-time.After(10 * time.Second):
- t.Error("timeout")
- }
- cancel()
- }()
+ cancel()
+ }()
- st.s.Run(ctx, incoming)
+ st.s.Run(ctx, incoming)
+ })
})
}
diff --git a/ipn/ipnlocal/network-lock_test.go b/ipn/ipnlocal/network-lock_test.go
index e5df38bdb..d2b6c9f4f 100644
--- a/ipn/ipnlocal/network-lock_test.go
+++ b/ipn/ipnlocal/network-lock_test.go
@@ -32,8 +32,8 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/tsd"
- "tailscale.com/tstest"
"tailscale.com/tstest/tkatest"
+ "tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
@@ -470,7 +470,7 @@ func TestTKASyncTriggersCompact(t *testing.T) {
//
// Our compaction algorithm preserves AUMs received in the last 14 days, so
// we need to backdate the commit times to make the AUMs eligible for compaction.
- clock := tstest.NewClock(tstest.ClockOpts{})
+ clock := tstime.StdClock{}
clock.Advance(-30 * 24 * time.Hour)
// Set up the TKA authority on the control plane.
diff --git a/tstest/clock.go b/tstest/clock.go
deleted file mode 100644
index ee7523430..000000000
--- a/tstest/clock.go
+++ /dev/null
@@ -1,694 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package tstest
-
-import (
- "container/heap"
- "sync"
- "time"
-
- "tailscale.com/tstime"
- "tailscale.com/util/mak"
-)
-
-// ClockOpts is used to configure the initial settings for a Clock. Once the
-// settings are configured as desired, call NewClock to get the resulting Clock.
-type ClockOpts struct {
- // Start is the starting time for the Clock. When FollowRealTime is false,
- // Start is also the value that will be returned by the first call
- // to Clock.Now.
- Start time.Time
- // Step is the amount of time the Clock will advance whenever Clock.Now is
- // called. If set to zero, the Clock will only advance when Clock.Advance is
- // called and/or if FollowRealTime is true.
- //
- // FollowRealTime and Step cannot be enabled at the same time.
- Step time.Duration
-
- // TimerChannelSize configures the maximum buffered ticks that are
- // permitted in the channel of any Timer and Ticker created by this Clock.
- // The special value 0 means to use the default of 1. The buffer may need to
- // be increased if time is advanced by more than a single tick and proper
- // functioning of the test requires that the ticks are not lost.
- TimerChannelSize int
-
- // FollowRealTime makes the simulated time increment along with real time.
- // It is a compromise between determinism and the difficulty of explicitly
- // managing the simulated time via Step or Clock.Advance. When
- // FollowRealTime is set, calls to Now() and PeekNow() will add the
- // elapsed real-world time to the simulated time.
- //
- // FollowRealTime and Step cannot be enabled at the same time.
- FollowRealTime bool
-}
-
-// NewClock creates a Clock with the specified settings. To create a
-// Clock with only the default settings, new(Clock) is equivalent, except that
-// the start time will not be computed until one of the receivers is called.
-func NewClock(co ClockOpts) *Clock {
- if co.FollowRealTime && co.Step != 0 {
- panic("only one of FollowRealTime and Step are allowed in NewClock")
- }
-
- return newClockInternal(co, nil)
-}
-
-// newClockInternal creates a Clock with the specified settings and allows
-// specifying a non-standard realTimeClock.
-func newClockInternal(co ClockOpts, rtClock tstime.Clock) *Clock {
- if !co.FollowRealTime && rtClock != nil {
- panic("rtClock can only be set with FollowRealTime enabled")
- }
-
- if co.FollowRealTime && rtClock == nil {
- rtClock = new(tstime.StdClock)
- }
-
- c := &Clock{
- start: co.Start,
- realTimeClock: rtClock,
- step: co.Step,
- timerChannelSize: co.TimerChannelSize,
- }
- c.init() // init now to capture the current time when co.Start.IsZero()
- return c
-}
-
-// Clock is a testing clock that advances every time its Now method is
-// called, beginning at its start time. If no start time is specified using
-// ClockBuilder, an arbitrary start time will be selected when the Clock is
-// created and can be retrieved by calling Clock.Start().
-type Clock struct {
- // start is the first value returned by Now. It must not be modified after
- // init is called.
- start time.Time
-
- // realTimeClock, if not nil, indicates that the Clock shall move forward
- // according to realTimeClock + the accumulated calls to Advance. This can
- // make writing tests easier that require some control over the clock but do
- // not need exact control over the clock. While step can also be used for
- // this purpose, it is harder to control how quickly time moves using step.
- realTimeClock tstime.Clock
-
- initOnce sync.Once
- mu sync.Mutex
-
- // step is how much to advance with each Now call.
- step time.Duration
- // present is the last value returned by Now (and will be returned again by
- // PeekNow).
- present time.Time
- // realTime is the time from realTimeClock corresponding to the current
- // value of present.
- realTime time.Time
- // skipStep indicates that the next call to Now should not add step to
- // present. This occurs after initialization and after Advance.
- skipStep bool
- // timerChannelSize is the buffer size to use for channels created by
- // NewTimer and NewTicker.
- timerChannelSize int
-
- events eventManager
-}
-
-func (c *Clock) init() {
- c.initOnce.Do(func() {
- if c.realTimeClock != nil {
- c.realTime = c.realTimeClock.Now()
- }
- if c.start.IsZero() {
- if c.realTime.IsZero() {
- c.start = time.Now()
- } else {
- c.start = c.realTime
- }
- }
- if c.timerChannelSize == 0 {
- c.timerChannelSize = 1
- }
- c.present = c.start
- c.skipStep = true
- c.events.AdvanceTo(c.present)
- })
-}
-
-// Now returns the virtual clock's current time, and advances it
-// according to its step configuration.
-func (c *Clock) Now() time.Time {
- c.init()
- rt := c.maybeGetRealTime()
-
- c.mu.Lock()
- defer c.mu.Unlock()
-
- step := c.step
- if c.skipStep {
- step = 0
- c.skipStep = false
- }
- c.advanceLocked(rt, step)
-
- return c.present
-}
-
-func (c *Clock) maybeGetRealTime() time.Time {
- if c.realTimeClock == nil {
- return time.Time{}
- }
- return c.realTimeClock.Now()
-}
-
-func (c *Clock) advanceLocked(now time.Time, add time.Duration) {
- if !now.IsZero() {
- add += now.Sub(c.realTime)
- c.realTime = now
- }
- if add == 0 {
- return
- }
- c.present = c.present.Add(add)
- c.events.AdvanceTo(c.present)
-}
-
-// PeekNow returns the last time reported by Now. If Now has never been called,
-// PeekNow returns the same value as GetStart.
-func (c *Clock) PeekNow() time.Time {
- c.init()
- c.mu.Lock()
- defer c.mu.Unlock()
- return c.present
-}
-
-// Advance moves simulated time forward or backwards by a relative amount. Any
-// Timer or Ticker that is waiting will fire at the requested point in simulated
-// time. Advance returns the new simulated time. If this Clock follows real time
-// then the next call to Now will equal the return value of Advance + the
-// elapsed time since calling Advance. Otherwise, the next call to Now will
-// equal the return value of Advance, regardless of the current step.
-func (c *Clock) Advance(d time.Duration) time.Time {
- c.init()
- rt := c.maybeGetRealTime()
-
- c.mu.Lock()
- defer c.mu.Unlock()
- c.skipStep = true
-
- c.advanceLocked(rt, d)
- return c.present
-}
-
-// AdvanceTo moves simulated time to a new absolute value. Any Timer or Ticker
-// that is waiting will fire at the requested point in simulated time. If this
-// Clock follows real time then the next call to Now will equal t + the elapsed
-// time since calling Advance. Otherwise, the next call to Now will equal t,
-// regardless of the configured step.
-func (c *Clock) AdvanceTo(t time.Time) {
- c.init()
- rt := c.maybeGetRealTime()
-
- c.mu.Lock()
- defer c.mu.Unlock()
- c.skipStep = true
- c.realTime = rt
- c.present = t
- c.events.AdvanceTo(c.present)
-}
-
-// GetStart returns the initial simulated time when this Clock was created.
-func (c *Clock) GetStart() time.Time {
- c.init()
- c.mu.Lock()
- defer c.mu.Unlock()
- return c.start
-}
-
-// GetStep returns the amount that simulated time advances on every call to Now.
-func (c *Clock) GetStep() time.Duration {
- c.init()
- c.mu.Lock()
- defer c.mu.Unlock()
- return c.step
-}
-
-// SetStep updates the amount that simulated time advances on every call to Now.
-func (c *Clock) SetStep(d time.Duration) {
- c.init()
- c.mu.Lock()
- defer c.mu.Unlock()
- c.step = d
-}
-
-// SetTimerChannelSize changes the channel size for any Timer or Ticker created
-// in the future. It does not affect those that were already created.
-func (c *Clock) SetTimerChannelSize(n int) {
- c.init()
- c.mu.Lock()
- defer c.mu.Unlock()
- c.timerChannelSize = n
-}
-
-// NewTicker returns a Ticker that uses this Clock for accessing the current
-// time.
-func (c *Clock) NewTicker(d time.Duration) (tstime.TickerController, <-chan time.Time) {
- c.init()
- rt := c.maybeGetRealTime()
-
- c.mu.Lock()
- defer c.mu.Unlock()
-
- c.advanceLocked(rt, 0)
- t := &Ticker{
- nextTrigger: c.present.Add(d),
- period: d,
- em: &c.events,
- }
- t.init(c.timerChannelSize)
- return t, t.C
-}
-
-// NewTimer returns a Timer that uses this Clock for accessing the current
-// time.
-func (c *Clock) NewTimer(d time.Duration) (tstime.TimerController, <-chan time.Time) {
- c.init()
- rt := c.maybeGetRealTime()
-
- c.mu.Lock()
- defer c.mu.Unlock()
-
- c.advanceLocked(rt, 0)
- t := &Timer{
- nextTrigger: c.present.Add(d),
- em: &c.events,
- }
- t.init(c.timerChannelSize, nil)
- return t, t.C
-}
-
-// AfterFunc returns a Timer that calls f when it fires, using this Clock for
-// accessing the current time.
-func (c *Clock) AfterFunc(d time.Duration, f func()) tstime.TimerController {
- c.init()
- rt := c.maybeGetRealTime()
-
- c.mu.Lock()
- defer c.mu.Unlock()
-
- c.advanceLocked(rt, 0)
- t := &Timer{
- nextTrigger: c.present.Add(d),
- em: &c.events,
- }
- t.init(c.timerChannelSize, f)
- return t
-}
-
-// Since subtracts specified duration from Now().
-func (c *Clock) Since(t time.Time) time.Duration {
- return c.Now().Sub(t)
-}
-
-// eventHandler offers a common interface for Timer and Ticker events to avoid
-// code duplication in eventManager.
-type eventHandler interface {
- // Fire signals the event. The provided time is written to the event's
- // channel as the current time. The return value is the next time this event
- // should fire, otherwise if it is zero then the event will be removed from
- // the eventManager.
- Fire(time.Time) time.Time
-}
-
-// event tracks details about an upcoming Timer or Ticker firing.
-type event struct {
- position int // The current index in the heap, needed for heap.Fix and heap.Remove.
- when time.Time // A cache of the next time the event triggers to avoid locking issues if we were to get it from eh.
- eh eventHandler
-}
-
-// eventManager tracks pending events created by Timer and Ticker. eventManager
-// implements heap.Interface for efficient lookups of the next event.
-type eventManager struct {
- // clock is a real time clock for scheduling events with. When clock is nil,
- // events only fire when AdvanceTo is called by the simulated clock that
- // this eventManager belongs to. When clock is not nil, events may fire when
- // timer triggers.
- clock tstime.Clock
-
- mu sync.Mutex
- now time.Time
- heap []*event
- reverseLookup map[eventHandler]*event
-
- // timer is an AfterFunc that triggers at heap[0].when.Sub(now) relative to
- // the time represented by clock. In other words, if clock is real world
- // time, then if an event is scheduled 1 second into the future in the
- // simulated time, then the event will trigger after 1 second of actual test
- // execution time (unless the test advances simulated time, in which case
- // the timer is updated accordingly). This makes tests easier to write in
- // situations where the simulated time only needs to be partially
- // controlled, and the test writer wishes for simulated time to pass with an
- // offset but still synchronized with the real world.
- //
- // In the future, this could be extended to allow simulated time to run at a
- // multiple of real world time.
- timer tstime.TimerController
-}
-
-func (em *eventManager) handleTimer() {
- rt := em.clock.Now()
- em.AdvanceTo(rt)
-}
-
-// Push implements heap.Interface.Push and must only be called by heap funcs
-// with em.mu already held.
-func (em *eventManager) Push(x any) {
- e, ok := x.(*event)
- if !ok {
- panic("incorrect event type")
- }
- if e == nil {
- panic("nil event")
- }
-
- mak.Set(&em.reverseLookup, e.eh, e)
- e.position = len(em.heap)
- em.heap = append(em.heap, e)
-}
-
-// Pop implements heap.Interface.Pop and must only be called by heap funcs with
-// em.mu already held.
-func (em *eventManager) Pop() any {
- e := em.heap[len(em.heap)-1]
- em.heap = em.heap[:len(em.heap)-1]
- delete(em.reverseLookup, e.eh)
- return e
-}
-
-// Len implements sort.Interface.Len and must only be called by heap funcs with
-// em.mu already held.
-func (em *eventManager) Len() int {
- return len(em.heap)
-}
-
-// Less implements sort.Interface.Less and must only be called by heap funcs
-// with em.mu already held.
-func (em *eventManager) Less(i, j int) bool {
- return em.heap[i].when.Before(em.heap[j].when)
-}
-
-// Swap implements sort.Interface.Swap and must only be called by heap funcs
-// with em.mu already held.
-func (em *eventManager) Swap(i, j int) {
- em.heap[i], em.heap[j] = em.heap[j], em.heap[i]
- em.heap[i].position = i
- em.heap[j].position = j
-}
-
-// Reschedule adds/updates/deletes an event in the heap, whichever
-// operation is applicable (use a zero time to delete).
-func (em *eventManager) Reschedule(eh eventHandler, t time.Time) {
- em.mu.Lock()
- defer em.mu.Unlock()
- defer em.updateTimerLocked()
-
- e, ok := em.reverseLookup[eh]
- if !ok {
- if t.IsZero() {
- // eh is not scheduled and also not active, so do nothing.
- return
- }
- // eh is not scheduled but is active, so add it.
- heap.Push(em, &event{
- when: t,
- eh: eh,
- })
- em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
- return
- }
-
- if t.IsZero() {
- // e is scheduled but not active, so remove it.
- heap.Remove(em, e.position)
- return
- }
-
- // e is scheduled and active, so update it.
- e.when = t
- heap.Fix(em, e.position)
- em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
-}
-
-// AdvanceTo updates the current time to tm and fires all events scheduled
-// before or equal to tm. When an event fires, it may request rescheduling and
-// the rescheduled events will be combined with the other existing events that
-// are waiting, and will be run in the unified ordering. A poorly behaved event
-// may theoretically prevent this from ever completing, but both Timer and
-// Ticker require positive steps into the future.
-func (em *eventManager) AdvanceTo(tm time.Time) {
- em.mu.Lock()
- defer em.mu.Unlock()
- defer em.updateTimerLocked()
-
- em.processEventsLocked(tm)
- em.now = tm
-}
-
-// Now returns the cached current time. It is intended for use by a Timer or
-// Ticker that needs to convert a relative time to an absolute time.
-func (em *eventManager) Now() time.Time {
- em.mu.Lock()
- defer em.mu.Unlock()
- return em.now
-}
-
-func (em *eventManager) processEventsLocked(tm time.Time) {
- for len(em.heap) > 0 && !em.heap[0].when.After(tm) {
- // Ideally some jitter would be added here but it's difficult to do so
- // in a deterministic fashion.
- em.now = em.heap[0].when
-
- if nextFire := em.heap[0].eh.Fire(em.now); !nextFire.IsZero() {
- em.heap[0].when = nextFire
- heap.Fix(em, 0)
- } else {
- heap.Pop(em)
- }
- }
-}
-
-func (em *eventManager) updateTimerLocked() {
- if em.clock == nil {
- return
- }
- if len(em.heap) == 0 {
- if em.timer != nil {
- em.timer.Stop()
- }
- return
- }
-
- timeToEvent := em.heap[0].when.Sub(em.now)
- if em.timer == nil {
- em.timer = em.clock.AfterFunc(timeToEvent, em.handleTimer)
- return
- }
- em.timer.Reset(timeToEvent)
-}
-
-// Ticker is a time.Ticker lookalike for use in tests that need to control when
-// events fire. Ticker could be made standalone in future but for now is
-// expected to be paired with a Clock and created by Clock.NewTicker.
-type Ticker struct {
- C <-chan time.Time // The channel on which ticks are delivered.
-
- // em is the eventManager to be notified when nextTrigger changes.
- // eventManager has its own mutex, and the pointer is immutable, therefore
- // em can be accessed without holding mu.
- em *eventManager
-
- c chan<- time.Time // The writer side of C.
-
- mu sync.Mutex
-
- // nextTrigger is the time of the ticker's next scheduled activation. When
- // Fire activates the ticker, nextTrigger is the timestamp written to the
- // channel.
- nextTrigger time.Time
-
- // period is the duration that is added to nextTrigger when the ticker
- // fires.
- period time.Duration
-}
-
-func (t *Ticker) init(channelSize int) {
- if channelSize <= 0 {
- panic("ticker channel size must be non-negative")
- }
- c := make(chan time.Time, channelSize)
- t.c = c
- t.C = c
- t.em.Reschedule(t, t.nextTrigger)
-}
-
-// Fire triggers the ticker. curTime is the timestamp to write to the channel.
-// The next trigger time for the ticker is updated to the last computed trigger
-// time + the ticker period (set at creation or using Reset). The next trigger
-// time is computed this way to match standard time.Ticker behavior, which
-// prevents accumulation of long term drift caused by delays in event execution.
-func (t *Ticker) Fire(curTime time.Time) time.Time {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- if t.nextTrigger.IsZero() {
- return time.Time{}
- }
- select {
- case t.c <- curTime:
- default:
- }
- t.nextTrigger = t.nextTrigger.Add(t.period)
-
- return t.nextTrigger
-}
-
-// Reset adjusts the Ticker's period to d and reschedules the next fire time to
-// the current simulated time + d.
-func (t *Ticker) Reset(d time.Duration) {
- if d <= 0 {
- // The standard time.Ticker requires a positive period.
- panic("non-positive period for Ticker.Reset")
- }
-
- now := t.em.Now()
-
- t.mu.Lock()
- t.resetLocked(now.Add(d), d)
- t.mu.Unlock()
-
- t.em.Reschedule(t, t.nextTrigger)
-}
-
-// ResetAbsolute adjusts the Ticker's period to d and reschedules the next fire
-// time to nextTrigger.
-func (t *Ticker) ResetAbsolute(nextTrigger time.Time, d time.Duration) {
- if nextTrigger.IsZero() {
- panic("zero nextTrigger time for ResetAbsolute")
- }
- if d <= 0 {
- panic("non-positive period for ResetAbsolute")
- }
-
- t.mu.Lock()
- t.resetLocked(nextTrigger, d)
- t.mu.Unlock()
-
- t.em.Reschedule(t, t.nextTrigger)
-}
-
-func (t *Ticker) resetLocked(nextTrigger time.Time, d time.Duration) {
- t.nextTrigger = nextTrigger
- t.period = d
-}
-
-// Stop deactivates the Ticker.
-func (t *Ticker) Stop() {
- t.mu.Lock()
- t.nextTrigger = time.Time{}
- t.mu.Unlock()
-
- t.em.Reschedule(t, t.nextTrigger)
-}
-
-// Timer is a time.Timer lookalike for use in tests that need to control when
-// events fire. Timer could be made standalone in future but for now must be
-// paired with a Clock and created by Clock.NewTimer.
-type Timer struct {
- C <-chan time.Time // The channel on which ticks are delivered.
-
- // em is the eventManager to be notified when nextTrigger changes.
- // eventManager has its own mutex, and the pointer is immutable, therefore
- // em can be accessed without holding mu.
- em *eventManager
-
- f func(time.Time) // The function to call when the timer expires.
-
- mu sync.Mutex
-
- // nextTrigger is the time of the ticker's next scheduled activation. When
- // Fire activates the ticker, nextTrigger is the timestamp written to the
- // channel.
- nextTrigger time.Time
-}
-
-func (t *Timer) init(channelSize int, afterFunc func()) {
- if channelSize <= 0 {
- panic("ticker channel size must be non-negative")
- }
- c := make(chan time.Time, channelSize)
- t.C = c
- if afterFunc == nil {
- t.f = func(curTime time.Time) {
- select {
- case c <- curTime:
- default:
- }
- }
- } else {
- t.f = func(_ time.Time) { afterFunc() }
- }
- t.em.Reschedule(t, t.nextTrigger)
-}
-
-// Fire triggers the ticker. curTime is the timestamp to write to the channel.
-// The next trigger time for the ticker is updated to the last computed trigger
-// time + the ticker period (set at creation or using Reset). The next trigger
-// time is computed this way to match standard time.Ticker behavior, which
-// prevents accumulation of long term drift caused by delays in event execution.
-func (t *Timer) Fire(curTime time.Time) time.Time {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- if t.nextTrigger.IsZero() {
- return time.Time{}
- }
- t.nextTrigger = time.Time{}
- t.f(curTime)
- return time.Time{}
-}
-
-// Reset reschedules the next fire time to the current simulated time + d.
-// Reset reports whether the timer was still active before the reset.
-func (t *Timer) Reset(d time.Duration) bool {
- if d <= 0 {
- // The standard time.Timer requires a positive delay.
- panic("non-positive delay for Timer.Reset")
- }
-
- return t.reset(t.em.Now().Add(d))
-}
-
-// ResetAbsolute reschedules the next fire time to nextTrigger.
-// ResetAbsolute reports whether the timer was still active before the reset.
-func (t *Timer) ResetAbsolute(nextTrigger time.Time) bool {
- if nextTrigger.IsZero() {
- panic("zero nextTrigger time for ResetAbsolute")
- }
-
- return t.reset(nextTrigger)
-}
-
-// Stop deactivates the Timer. Stop reports whether the timer was active before
-// stopping.
-func (t *Timer) Stop() bool {
- return t.reset(time.Time{})
-}
-
-func (t *Timer) reset(nextTrigger time.Time) bool {
- t.mu.Lock()
- wasActive := !t.nextTrigger.IsZero()
- t.nextTrigger = nextTrigger
- t.mu.Unlock()
-
- t.em.Reschedule(t, t.nextTrigger)
- return wasActive
-}
diff --git a/tstest/clock_test.go b/tstest/clock_test.go
deleted file mode 100644
index 2ebaf752a..000000000
--- a/tstest/clock_test.go
+++ /dev/null
@@ -1,2474 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package tstest
-
-import (
- "slices"
- "sync/atomic"
- "testing"
- "time"
-
- "tailscale.com/tstime"
-)
-
-func TestClockWithDefinedStartTime(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- start time.Time
- step time.Duration
- wants []time.Time // The return values of sequential calls to Now().
- }{
- {
- name: "increment ms",
- start: time.Unix(12345, 1000),
- step: 1000,
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 2000),
- time.Unix(12345, 3000),
- time.Unix(12345, 4000),
- },
- },
- {
- name: "increment second",
- start: time.Unix(12345, 1000),
- step: time.Second,
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12346, 1000),
- time.Unix(12347, 1000),
- time.Unix(12348, 1000),
- },
- },
- {
- name: "no increment",
- start: time.Unix(12345, 1000),
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 1000),
- time.Unix(12345, 1000),
- time.Unix(12345, 1000),
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- clock := NewClock(ClockOpts{
- Start: tt.start,
- Step: tt.step,
- })
-
- if start := clock.GetStart(); !start.Equal(tt.start) {
- t.Errorf("clock has start %v, want %v", start, tt.start)
- }
- if step := clock.GetStep(); step != tt.step {
- t.Errorf("clock has step %v, want %v", step, tt.step)
- }
-
- for i := range tt.wants {
- if got := clock.Now(); !got.Equal(tt.wants[i]) {
- t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i])
- }
- if got := clock.PeekNow(); !got.Equal(tt.wants[i]) {
- t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i])
- }
- }
- })
- }
-}
-
-func TestClockWithDefaultStartTime(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- step time.Duration
- wants []time.Duration // The return values of sequential calls to Now() after added to Start()
- }{
- {
- name: "increment ms",
- step: 1000,
- wants: []time.Duration{
- 0,
- 1000,
- 2000,
- 3000,
- },
- },
- {
- name: "increment second",
- step: time.Second,
- wants: []time.Duration{
- 0 * time.Second,
- 1 * time.Second,
- 2 * time.Second,
- 3 * time.Second,
- },
- },
- {
- name: "no increment",
- wants: []time.Duration{0, 0, 0, 0},
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- clock := NewClock(ClockOpts{
- Step: tt.step,
- })
- start := clock.GetStart()
-
- if step := clock.GetStep(); step != tt.step {
- t.Errorf("clock has step %v, want %v", step, tt.step)
- }
-
- for i := range tt.wants {
- want := start.Add(tt.wants[i])
- if got := clock.Now(); !got.Equal(want) {
- t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i])
- }
- if got := clock.PeekNow(); !got.Equal(want) {
- t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i])
- }
- }
- })
- }
-}
-
-func TestZeroInitClock(t *testing.T) {
- t.Parallel()
-
- var clock Clock
- start := clock.GetStart()
-
- if step := clock.GetStep(); step != 0 {
- t.Errorf("clock has step %v, want 0", step)
- }
-
- for i := range 10 {
- if got := clock.Now(); !got.Equal(start) {
- t.Errorf("step %v: clock.Now() = %v, want %v", i, got, start)
- }
- if got := clock.PeekNow(); !got.Equal(start) {
- t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, start)
- }
- }
-}
-
-func TestClockSetStep(t *testing.T) {
- t.Parallel()
-
- type stepInfo struct {
- when int
- step time.Duration
- }
-
- tests := []struct {
- name string
- start time.Time
- step time.Duration
- stepChanges []stepInfo
- wants []time.Time // The return values of sequential calls to Now().
- }{
- {
- name: "increment ms then s",
- start: time.Unix(12345, 1000),
- step: 1000,
- stepChanges: []stepInfo{
- {
- when: 4,
- step: time.Second,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 2000),
- time.Unix(12345, 3000),
- time.Unix(12345, 4000),
- time.Unix(12346, 4000),
- time.Unix(12347, 4000),
- time.Unix(12348, 4000),
- time.Unix(12349, 4000),
- },
- },
- {
- name: "multiple changes over time",
- start: time.Unix(12345, 1000),
- step: 1,
- stepChanges: []stepInfo{
- {
- when: 2,
- step: time.Second,
- },
- {
- when: 4,
- step: 0,
- },
- {
- when: 6,
- step: 1000,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 1001),
- time.Unix(12346, 1001),
- time.Unix(12347, 1001),
- time.Unix(12347, 1001),
- time.Unix(12347, 1001),
- time.Unix(12347, 2001),
- time.Unix(12347, 3001),
- },
- },
- {
- name: "multiple changes at once",
- start: time.Unix(12345, 1000),
- step: 1,
- stepChanges: []stepInfo{
- {
- when: 2,
- step: time.Second,
- },
- {
- when: 2,
- step: 0,
- },
- {
- when: 2,
- step: 1000,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 1001),
- time.Unix(12345, 2001),
- time.Unix(12345, 3001),
- },
- },
- {
- name: "changes at start",
- start: time.Unix(12345, 1000),
- step: 0,
- stepChanges: []stepInfo{
- {
- when: 0,
- step: time.Second,
- },
- {
- when: 0,
- step: 1000,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 2000),
- time.Unix(12345, 3000),
- time.Unix(12345, 4000),
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- clock := NewClock(ClockOpts{
- Start: tt.start,
- Step: tt.step,
- })
- wantStep := tt.step
- changeIndex := 0
-
- for i := range tt.wants {
- for len(tt.stepChanges) > changeIndex && tt.stepChanges[changeIndex].when == i {
- wantStep = tt.stepChanges[changeIndex].step
- clock.SetStep(wantStep)
- changeIndex++
- }
-
- if start := clock.GetStart(); !start.Equal(tt.start) {
- t.Errorf("clock has start %v, want %v", start, tt.start)
- }
- if step := clock.GetStep(); step != wantStep {
- t.Errorf("clock has step %v, want %v", step, tt.step)
- }
-
- if got := clock.Now(); !got.Equal(tt.wants[i]) {
- t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i])
- }
- if got := clock.PeekNow(); !got.Equal(tt.wants[i]) {
- t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i])
- }
- }
- })
- }
-}
-
-func TestClockAdvance(t *testing.T) {
- t.Parallel()
-
- type advanceInfo struct {
- when int
- advance time.Duration
- }
-
- tests := []struct {
- name string
- start time.Time
- step time.Duration
- advances []advanceInfo
- wants []time.Time // The return values of sequential calls to Now().
- }{
- {
- name: "increment ms then advance 1s",
- start: time.Unix(12345, 1000),
- step: 1000,
- advances: []advanceInfo{
- {
- when: 4,
- advance: time.Second,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 2000),
- time.Unix(12345, 3000),
- time.Unix(12345, 4000),
- time.Unix(12346, 4000),
- time.Unix(12346, 5000),
- time.Unix(12346, 6000),
- time.Unix(12346, 7000),
- },
- },
- {
- name: "multiple advances over time",
- start: time.Unix(12345, 1000),
- step: 1,
- advances: []advanceInfo{
- {
- when: 2,
- advance: time.Second,
- },
- {
- when: 4,
- advance: 0,
- },
- {
- when: 6,
- advance: 1000,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 1001),
- time.Unix(12346, 1001),
- time.Unix(12346, 1002),
- time.Unix(12346, 1002),
- time.Unix(12346, 1003),
- time.Unix(12346, 2003),
- time.Unix(12346, 2004),
- },
- },
- {
- name: "multiple advances at once",
- start: time.Unix(12345, 1000),
- step: 1,
- advances: []advanceInfo{
- {
- when: 2,
- advance: time.Second,
- },
- {
- when: 2,
- advance: 0,
- },
- {
- when: 2,
- advance: 1000,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 1001),
- time.Unix(12346, 2001),
- time.Unix(12346, 2002),
- },
- },
- {
- name: "changes at start",
- start: time.Unix(12345, 1000),
- step: 5,
- advances: []advanceInfo{
- {
- when: 0,
- advance: time.Second,
- },
- {
- when: 0,
- advance: 1000,
- },
- },
- wants: []time.Time{
- time.Unix(12346, 2000),
- time.Unix(12346, 2005),
- time.Unix(12346, 2010),
- time.Unix(12346, 2015),
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- clock := NewClock(ClockOpts{
- Start: tt.start,
- Step: tt.step,
- })
- wantStep := tt.step
- changeIndex := 0
-
- for i := range tt.wants {
- for len(tt.advances) > changeIndex && tt.advances[changeIndex].when == i {
- clock.Advance(tt.advances[changeIndex].advance)
- changeIndex++
- }
-
- if start := clock.GetStart(); !start.Equal(tt.start) {
- t.Errorf("clock has start %v, want %v", start, tt.start)
- }
- if step := clock.GetStep(); step != wantStep {
- t.Errorf("clock has step %v, want %v", step, tt.step)
- }
-
- if got := clock.Now(); !got.Equal(tt.wants[i]) {
- t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i])
- }
- if got := clock.PeekNow(); !got.Equal(tt.wants[i]) {
- t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i])
- }
- }
- })
- }
-}
-
-func expectNoTicks(t *testing.T, tickC <-chan time.Time) {
- t.Helper()
- select {
- case tick := <-tickC:
- t.Errorf("wanted no ticks, got %v", tick)
- default:
- }
-}
-
-func TestSingleTicker(t *testing.T) {
- t.Parallel()
-
- type testStep struct {
- stop bool
- reset time.Duration
- resetAbsolute time.Time
- setStep time.Duration
- advance time.Duration
- advanceRealTime time.Duration
- wantTime time.Time
- wantTicks []time.Time
- }
-
- tests := []struct {
- name string
- realTimeOpts *ClockOpts
- start time.Time
- step time.Duration
- period time.Duration
- channelSize int
- steps []testStep
- }{
- {
- name: "no tick advance",
- start: time.Unix(12345, 0),
- period: time.Second,
- steps: []testStep{
- {
- advance: time.Second - 1,
- wantTime: time.Unix(12345, 999_999_999),
- },
- },
- },
- {
- name: "no tick step",
- start: time.Unix(12345, 0),
- step: time.Second - 1,
- period: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12345, 999_999_999),
- },
- },
- },
- {
- name: "single tick advance exact",
- start: time.Unix(12345, 0),
- period: time.Second,
- steps: []testStep{
- {
- advance: time.Second,
- wantTime: time.Unix(12346, 0),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- },
- },
- {
- name: "single tick advance extra",
- start: time.Unix(12345, 0),
- period: time.Second,
- steps: []testStep{
- {
- advance: time.Second + 1,
- wantTime: time.Unix(12346, 1),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- },
- },
- {
- name: "single tick step exact",
- start: time.Unix(12345, 0),
- step: time.Second,
- period: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 0),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- },
- },
- {
- name: "single tick step extra",
- start: time.Unix(12345, 0),
- step: time.Second + 1,
- period: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 1),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- },
- },
- {
- name: "single tick per advance",
- start: time.Unix(12345, 0),
- period: 3 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advance: 4 * time.Second,
- wantTime: time.Unix(12349, 0),
- wantTicks: []time.Time{time.Unix(12348, 0)},
- },
- {
- advance: 2 * time.Second,
- wantTime: time.Unix(12351, 0),
- wantTicks: []time.Time{time.Unix(12351, 0)},
- },
- {
- advance: 2 * time.Second,
- wantTime: time.Unix(12353, 0),
- },
- {
- advance: 2 * time.Second,
- wantTime: time.Unix(12355, 0),
- wantTicks: []time.Time{time.Unix(12354, 0)},
- },
- },
- },
- {
- name: "single tick per step",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- period: 3 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- },
- {
- wantTime: time.Unix(12349, 0),
- wantTicks: []time.Time{time.Unix(12348, 0)},
- },
- {
- wantTime: time.Unix(12351, 0),
- wantTicks: []time.Time{time.Unix(12351, 0)},
- },
- {
- wantTime: time.Unix(12353, 0),
- },
- {
- wantTime: time.Unix(12355, 0),
- wantTicks: []time.Time{time.Unix(12354, 0)},
- },
- },
- },
- {
- name: "multiple tick per advance",
- start: time.Unix(12345, 0),
- period: time.Second,
- channelSize: 3,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advance: 2 * time.Second,
- wantTime: time.Unix(12347, 0),
- wantTicks: []time.Time{
- time.Unix(12346, 0),
- time.Unix(12347, 0),
- },
- },
- {
- advance: 4 * time.Second,
- wantTime: time.Unix(12351, 0),
- wantTicks: []time.Time{
- time.Unix(12348, 0),
- time.Unix(12349, 0),
- time.Unix(12350, 0),
- // fourth tick dropped due to channel size
- },
- },
- },
- },
- {
- name: "multiple tick per step",
- start: time.Unix(12345, 0),
- step: 3 * time.Second,
- period: 2 * time.Second,
- channelSize: 3,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12348, 0),
- wantTicks: []time.Time{
- time.Unix(12347, 0),
- },
- },
- {
- wantTime: time.Unix(12351, 0),
- wantTicks: []time.Time{
- time.Unix(12349, 0),
- time.Unix(12351, 0),
- },
- },
- {
- wantTime: time.Unix(12354, 0),
- wantTicks: []time.Time{
- time.Unix(12353, 0),
- },
- },
- {
- wantTime: time.Unix(12357, 0),
- wantTicks: []time.Time{
- time.Unix(12355, 0),
- time.Unix(12357, 0),
- },
- },
- },
- },
- {
- name: "stop",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- period: time.Second,
- channelSize: 3,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- wantTicks: []time.Time{
- time.Unix(12346, 0),
- time.Unix(12347, 0),
- },
- },
- {
- stop: true,
- wantTime: time.Unix(12349, 0),
- },
- {
- wantTime: time.Unix(12351, 0),
- },
- {
- advance: 10 * time.Second,
- wantTime: time.Unix(12361, 0),
- },
- },
- },
- {
- name: "reset while running",
- start: time.Unix(12345, 0),
- period: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12346, 0),
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12347, 0),
- wantTicks: []time.Time{
- time.Unix(12347, 0),
- },
- },
- {
- advance: time.Second,
- reset: time.Second,
- wantTime: time.Unix(12348, 0),
- wantTicks: []time.Time{
- time.Unix(12348, 0),
- },
- },
- {
- setStep: 5 * time.Second,
- reset: 10 * time.Second,
- wantTime: time.Unix(12353, 0),
- },
- {
- wantTime: time.Unix(12358, 0),
- wantTicks: []time.Time{
- time.Unix(12358, 0),
- },
- },
- },
- },
- {
- name: "reset while stopped",
- start: time.Unix(12345, 0),
- step: time.Second,
- period: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- wantTicks: []time.Time{
- time.Unix(12347, 0),
- },
- },
- {
- stop: true,
- wantTime: time.Unix(12348, 0),
- },
- {
- wantTime: time.Unix(12349, 0),
- },
- {
- reset: time.Second,
- wantTime: time.Unix(12350, 0),
- wantTicks: []time.Time{
- time.Unix(12350, 0),
- },
- },
- {
- wantTime: time.Unix(12351, 0),
- wantTicks: []time.Time{
- time.Unix(12351, 0),
- },
- },
- },
- },
- {
- name: "reset absolute",
- start: time.Unix(12345, 0),
- step: time.Second,
- period: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- wantTicks: []time.Time{
- time.Unix(12347, 0),
- },
- },
- {
- reset: time.Second,
- resetAbsolute: time.Unix(12354, 50),
- advance: 7 * time.Second,
- wantTime: time.Unix(12354, 0),
- },
- {
- wantTime: time.Unix(12355, 0),
- wantTicks: []time.Time{
- time.Unix(12354, 50),
- },
- },
- {
- wantTime: time.Unix(12356, 0),
- wantTicks: []time.Time{
- time.Unix(12355, 50),
- },
- },
- },
- },
- {
- name: "follow real time",
- realTimeOpts: new(ClockOpts),
- start: time.Unix(12345, 0),
- period: 2 * time.Second,
- channelSize: 3,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advanceRealTime: 5 * time.Second,
- wantTime: time.Unix(12350, 0),
- wantTicks: []time.Time{
- time.Unix(12347, 0),
- time.Unix(12349, 0),
- },
- },
- {
- advance: 5 * time.Second,
- wantTime: time.Unix(12355, 0),
- wantTicks: []time.Time{
- time.Unix(12351, 0),
- time.Unix(12353, 0),
- time.Unix(12355, 0),
- },
- },
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- var realTimeClockForTestClock tstime.Clock
- var realTimeClock *Clock
- if tt.realTimeOpts != nil {
- realTimeClock = NewClock(*tt.realTimeOpts)
- // Passing realTimeClock into newClockInternal results in a
- // non-nil interface with a nil pointer, so this is necessary.
- realTimeClockForTestClock = realTimeClock
- }
-
- clock := newClockInternal(ClockOpts{
- Start: tt.start,
- Step: tt.step,
- TimerChannelSize: tt.channelSize,
- FollowRealTime: realTimeClock != nil,
- }, realTimeClockForTestClock)
- tc, tickC := clock.NewTicker(tt.period)
- tickControl := tc.(*Ticker)
-
- t.Cleanup(tickControl.Stop)
-
- expectNoTicks(t, tickC)
-
- for i, step := range tt.steps {
- if step.stop {
- tickControl.Stop()
- }
-
- if !step.resetAbsolute.IsZero() {
- tickControl.ResetAbsolute(step.resetAbsolute, step.reset)
- } else if step.reset > 0 {
- tickControl.Reset(step.reset)
- }
-
- if step.setStep > 0 {
- clock.SetStep(step.setStep)
- }
-
- if step.advance > 0 {
- clock.Advance(step.advance)
- }
- if step.advanceRealTime > 0 {
- realTimeClock.Advance(step.advanceRealTime)
- }
-
- if now := clock.Now(); !step.wantTime.IsZero() && !now.Equal(step.wantTime) {
- t.Errorf("step %v now = %v, want %v", i, now, step.wantTime)
- }
-
- for j, want := range step.wantTicks {
- select {
- case tick := <-tickC:
- if tick.Equal(want) {
- continue
- }
- t.Errorf("step %v tick %v = %v, want %v", i, j, tick, want)
- default:
- t.Errorf("step %v tick %v missing", i, j)
- }
- }
-
- expectNoTicks(t, tickC)
- }
- })
- }
-}
-
-func TestSingleTimer(t *testing.T) {
- t.Parallel()
-
- type testStep struct {
- stop bool
- stopReturn bool // The expected return value for Stop() if stop is true.
- reset time.Duration
- resetAbsolute time.Time
- resetReturn bool // The expected return value for Reset() or ResetAbsolute().
- setStep time.Duration
- advance time.Duration
- advanceRealTime time.Duration
- wantTime time.Time
- wantTicks []time.Time
- }
-
- tests := []struct {
- name string
- realTimeOpts *ClockOpts
- start time.Time
- step time.Duration
- delay time.Duration
- steps []testStep
- }{
- {
- name: "no tick advance",
- start: time.Unix(12345, 0),
- delay: time.Second,
- steps: []testStep{
- {
- advance: time.Second - 1,
- wantTime: time.Unix(12345, 999_999_999),
- },
- },
- },
- {
- name: "no tick step",
- start: time.Unix(12345, 0),
- step: time.Second - 1,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12345, 999_999_999),
- },
- },
- },
- {
- name: "single tick advance exact",
- start: time.Unix(12345, 0),
- delay: time.Second,
- steps: []testStep{
- {
- advance: time.Second,
- wantTime: time.Unix(12346, 0),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12347, 0),
- },
- },
- },
- {
- name: "single tick advance extra",
- start: time.Unix(12345, 0),
- delay: time.Second,
- steps: []testStep{
- {
- advance: time.Second + 1,
- wantTime: time.Unix(12346, 1),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12347, 1),
- },
- },
- },
- {
- name: "single tick step exact",
- start: time.Unix(12345, 0),
- step: time.Second,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 0),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- {
- wantTime: time.Unix(12347, 0),
- },
- },
- },
- {
- name: "single tick step extra",
- start: time.Unix(12345, 0),
- step: time.Second + 1,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 1),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- {
- wantTime: time.Unix(12347, 2),
- },
- },
- },
- {
- name: "reset for single tick per advance",
- start: time.Unix(12345, 0),
- delay: 3 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advance: 4 * time.Second,
- wantTime: time.Unix(12349, 0),
- wantTicks: []time.Time{time.Unix(12348, 0)},
- },
- {
- resetAbsolute: time.Unix(12351, 0),
- advance: 2 * time.Second,
- wantTime: time.Unix(12351, 0),
- wantTicks: []time.Time{time.Unix(12351, 0)},
- },
- {
- reset: 3 * time.Second,
- advance: 2 * time.Second,
- wantTime: time.Unix(12353, 0),
- },
- {
- advance: 2 * time.Second,
- wantTime: time.Unix(12355, 0),
- wantTicks: []time.Time{time.Unix(12354, 0)},
- },
- {
- advance: 10 * time.Second,
- wantTime: time.Unix(12365, 0),
- },
- },
- },
- {
- name: "reset for single tick per step",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: 3 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- },
- {
- wantTime: time.Unix(12349, 0),
- wantTicks: []time.Time{time.Unix(12348, 0)},
- },
- {
- reset: time.Second,
- wantTime: time.Unix(12351, 0),
- wantTicks: []time.Time{time.Unix(12350, 0)},
- },
- {
- resetAbsolute: time.Unix(12354, 0),
- wantTime: time.Unix(12353, 0),
- },
- {
- wantTime: time.Unix(12355, 0),
- wantTicks: []time.Time{time.Unix(12354, 0)},
- },
- },
- },
- {
- name: "reset while active",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: 3 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- },
- {
- reset: 3 * time.Second,
- resetReturn: true,
- wantTime: time.Unix(12349, 0),
- },
- {
- resetAbsolute: time.Unix(12354, 0),
- resetReturn: true,
- wantTime: time.Unix(12351, 0),
- },
- {
- wantTime: time.Unix(12353, 0),
- },
- {
- wantTime: time.Unix(12355, 0),
- wantTicks: []time.Time{time.Unix(12354, 0)},
- },
- },
- },
- {
- name: "stop after fire",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- {
- stop: true,
- wantTime: time.Unix(12349, 0),
- },
- {
- wantTime: time.Unix(12351, 0),
- },
- {
- advance: 10 * time.Second,
- wantTime: time.Unix(12361, 0),
- },
- },
- },
- {
- name: "stop before fire",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: time.Second,
- steps: []testStep{
- {
- stop: true,
- stopReturn: true,
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- },
- {
- wantTime: time.Unix(12349, 0),
- },
- {
- wantTime: time.Unix(12351, 0),
- },
- {
- advance: 10 * time.Second,
- wantTime: time.Unix(12361, 0),
- },
- },
- },
- {
- name: "stop after reset",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- wantTicks: []time.Time{time.Unix(12346, 0)},
- },
- {
- reset: 10 * time.Second,
- wantTime: time.Unix(12349, 0),
- },
- {
- stop: true,
- stopReturn: true,
- wantTime: time.Unix(12351, 0),
- },
- {
- advance: 10 * time.Second,
- wantTime: time.Unix(12361, 0),
- },
- },
- },
- {
- name: "reset while running",
- start: time.Unix(12345, 0),
- delay: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12346, 0),
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12347, 0),
- wantTicks: []time.Time{
- time.Unix(12347, 0),
- },
- },
- {
- advance: time.Second,
- reset: time.Second,
- wantTime: time.Unix(12348, 0),
- wantTicks: []time.Time{
- time.Unix(12348, 0),
- },
- },
- {
- setStep: 5 * time.Second,
- reset: 10 * time.Second,
- wantTime: time.Unix(12353, 0),
- },
- {
- wantTime: time.Unix(12358, 0),
- wantTicks: []time.Time{
- time.Unix(12358, 0),
- },
- },
- },
- },
- {
- name: "reset while stopped",
- start: time.Unix(12345, 0),
- step: time.Second,
- delay: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 0),
- },
- {
- stop: true,
- stopReturn: true,
- wantTime: time.Unix(12347, 0),
- },
- {
- wantTime: time.Unix(12348, 0),
- },
- {
- wantTime: time.Unix(12349, 0),
- },
- {
- reset: time.Second,
- wantTime: time.Unix(12350, 0),
- wantTicks: []time.Time{
- time.Unix(12350, 0),
- },
- },
- {
- wantTime: time.Unix(12351, 0),
- },
- },
- },
- {
- name: "reset absolute",
- start: time.Unix(12345, 0),
- step: time.Second,
- delay: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- wantTicks: []time.Time{
- time.Unix(12347, 0),
- },
- },
- {
- resetAbsolute: time.Unix(12354, 50),
- advance: 7 * time.Second,
- wantTime: time.Unix(12354, 0),
- },
- {
- wantTime: time.Unix(12355, 0),
- wantTicks: []time.Time{
- time.Unix(12354, 50),
- },
- },
- {
- wantTime: time.Unix(12356, 0),
- },
- },
- },
- {
- name: "follow real time",
- realTimeOpts: new(ClockOpts),
- start: time.Unix(12345, 0),
- delay: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advanceRealTime: 5 * time.Second,
- wantTime: time.Unix(12350, 0),
- wantTicks: []time.Time{
- time.Unix(12347, 0),
- },
- },
- {
- reset: 2 * time.Second,
- advance: 5 * time.Second,
- wantTime: time.Unix(12355, 0),
- wantTicks: []time.Time{
- time.Unix(12352, 0),
- },
- },
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- var realTimeClockForTestClock tstime.Clock
- var realTimeClock *Clock
- if tt.realTimeOpts != nil {
- realTimeClock = NewClock(*tt.realTimeOpts)
- // Passing realTimeClock into newClockInternal results in a
- // non-nil interface with a nil pointer, so this is necessary.
- realTimeClockForTestClock = realTimeClock
- }
-
- clock := newClockInternal(ClockOpts{
- Start: tt.start,
- Step: tt.step,
- FollowRealTime: realTimeClock != nil,
- }, realTimeClockForTestClock)
- tc, tickC := clock.NewTimer(tt.delay)
- timerControl := tc.(*Timer)
-
- t.Cleanup(func() { timerControl.Stop() })
-
- expectNoTicks(t, tickC)
-
- for i, step := range tt.steps {
- if step.stop {
- if got := timerControl.Stop(); got != step.stopReturn {
- t.Errorf("step %v Stop returned %v, want %v", i, got, step.stopReturn)
- }
- }
-
- if !step.resetAbsolute.IsZero() {
- if got := timerControl.ResetAbsolute(step.resetAbsolute); got != step.resetReturn {
- t.Errorf("step %v Reset returned %v, want %v", i, got, step.resetReturn)
- }
- }
-
- if step.reset > 0 {
- if got := timerControl.Reset(step.reset); got != step.resetReturn {
- t.Errorf("step %v Reset returned %v, want %v", i, got, step.resetReturn)
- }
- }
-
- if step.setStep > 0 {
- clock.SetStep(step.setStep)
- }
-
- if step.advance > 0 {
- clock.Advance(step.advance)
- }
- if step.advanceRealTime > 0 {
- realTimeClock.Advance(step.advanceRealTime)
- }
-
- if now := clock.Now(); !step.wantTime.IsZero() && !now.Equal(step.wantTime) {
- t.Errorf("step %v now = %v, want %v", i, now, step.wantTime)
- }
-
- for j, want := range step.wantTicks {
- select {
- case tick := <-tickC:
- if tick.Equal(want) {
- continue
- }
- t.Errorf("step %v tick %v = %v, want %v", i, j, tick, want)
- default:
- t.Errorf("step %v tick %v missing", i, j)
- }
- }
-
- expectNoTicks(t, tickC)
- }
- })
- }
-}
-
-type testEvent struct {
- fireTimes []time.Time
- scheduleTimes []time.Time
-}
-
-func (te *testEvent) Fire(t time.Time) time.Time {
- var ret time.Time
-
- te.fireTimes = append(te.fireTimes, t)
- if len(te.scheduleTimes) > 0 {
- ret = te.scheduleTimes[0]
- te.scheduleTimes = te.scheduleTimes[1:]
- }
- return ret
-}
-
-func TestEventManager(t *testing.T) {
- t.Parallel()
-
- var em eventManager
-
- testEvents := []testEvent{
- {
- scheduleTimes: []time.Time{
- time.Unix(12300, 0), // step 1
- time.Unix(12340, 0), // step 1
- time.Unix(12345, 0), // step 1
- time.Unix(12346, 0), // step 1
- time.Unix(12347, 0), // step 3
- time.Unix(12348, 0), // step 4
- time.Unix(12349, 0), // step 4
- },
- },
- {
- scheduleTimes: []time.Time{
- time.Unix(12350, 0), // step 4
- time.Unix(12360, 0), // step 5
- time.Unix(12370, 0), // rescheduled
- time.Unix(12380, 0), // step 6
- time.Unix(12381, 0), // step 6
- time.Unix(12382, 0), // step 6
- time.Unix(12393, 0), // stopped
- },
- },
- {
- scheduleTimes: []time.Time{
- time.Unix(12350, 1), // step 4
- time.Unix(12360, 1), // rescheduled
- time.Unix(12370, 1), // step 6
- time.Unix(12380, 1), // step 6
- time.Unix(12381, 1), // step 6
- time.Unix(12382, 1), // step 6
- time.Unix(12383, 1), // step 6
- },
- },
- {
- scheduleTimes: []time.Time{
- time.Unix(12355, 0), // step 5
- time.Unix(12365, 0), // step 5
- time.Unix(12370, 0), // step 6
- time.Unix(12390, 0), // step 6
- time.Unix(12391, 0), // step 7
- time.Unix(12392, 0), // step 7
- time.Unix(12393, 0), // step 7
- },
- },
- {
- scheduleTimes: []time.Time{
- time.Unix(100000, 0), // step 7
- },
- },
- {
- scheduleTimes: []time.Time{
- time.Unix(12346, 0), // step 1
- },
- },
- {
- scheduleTimes: []time.Time{
- time.Unix(12305, 0), // step 5
- },
- },
- {
- scheduleTimes: []time.Time{
- time.Unix(12372, 0), // step 6
- time.Unix(12374, 0), // step 6
- time.Unix(12376, 0), // step 6
- time.Unix(12386, 0), // step 6
- time.Unix(12396, 0), // step 7
- },
- },
- }
-
- steps := []struct {
- reschedule []int
- stop []int
- advanceTo time.Time
- want map[int][]time.Time
- waitingEvents int
- }{
- {
- advanceTo: time.Unix(12345, 0),
- },
- {
- reschedule: []int{0, 1, 2, 3, 4, 5}, // add 0, 1, 2, 3, 4, 5
- advanceTo: time.Unix(12346, 0),
- want: map[int][]time.Time{
- 0: {
- time.Unix(12300, 0),
- time.Unix(12340, 0),
- time.Unix(12345, 0),
- time.Unix(12346, 0),
- },
- 5: {
- time.Unix(12346, 0),
- },
- },
- waitingEvents: 5, // scheduled 0, 1, 2, 3, 4, 5; retired 5
- },
- {
- advanceTo: time.Unix(12346, 50),
- waitingEvents: 5, // no change
- },
- {
- advanceTo: time.Unix(12347, 50),
- want: map[int][]time.Time{
- 0: {
- time.Unix(12347, 0),
- },
- },
- waitingEvents: 5, // no change
- },
- {
- advanceTo: time.Unix(12350, 50),
- want: map[int][]time.Time{
- 0: {
- time.Unix(12348, 0),
- time.Unix(12349, 0),
- },
- 1: {
- time.Unix(12350, 0),
- },
- 2: {
- time.Unix(12350, 1),
- },
- },
- waitingEvents: 4, // retired 0
- },
- {
- reschedule: []int{6, 7}, // add 6, 7
- stop: []int{2},
- advanceTo: time.Unix(12365, 0),
- want: map[int][]time.Time{
- 1: {
- time.Unix(12360, 0),
- },
- 3: {
- time.Unix(12355, 0),
- time.Unix(12365, 0),
- },
- 6: {
- time.Unix(12305, 0),
- },
- },
- waitingEvents: 4, // scheduled 6, 7; retired 2, 5
- },
- {
- reschedule: []int{1, 2}, // update 1; add 2
- stop: []int{6},
- advanceTo: time.Unix(12390, 0),
- want: map[int][]time.Time{
- 1: {
- time.Unix(12380, 0),
- time.Unix(12381, 0),
- time.Unix(12382, 0),
- },
- 2: {
- time.Unix(12370, 1),
- time.Unix(12380, 1),
- time.Unix(12381, 1),
- time.Unix(12382, 1),
- time.Unix(12383, 1),
- },
- 3: {
- time.Unix(12370, 0),
- time.Unix(12390, 0),
- },
- 7: {
- time.Unix(12372, 0),
- time.Unix(12374, 0),
- time.Unix(12376, 0),
- time.Unix(12386, 0),
- },
- },
- waitingEvents: 3, // scheduled 2, retired 2, stopped 6
- },
- {
- stop: []int{1}, // no-op: already stopped
- advanceTo: time.Unix(200000, 0),
- want: map[int][]time.Time{
- 3: {
- time.Unix(12391, 0),
- time.Unix(12392, 0),
- time.Unix(12393, 0),
- },
- 4: {
- time.Unix(100000, 0),
- },
- 7: {
- time.Unix(12396, 0),
- },
- },
- waitingEvents: 0, // retired 3, 4, 7
- },
- {
- advanceTo: time.Unix(300000, 0),
- },
- }
-
- for i, step := range steps {
- for _, idx := range step.reschedule {
- ev := &testEvents[idx]
- t := ev.scheduleTimes[0]
- ev.scheduleTimes = ev.scheduleTimes[1:]
- em.Reschedule(ev, t)
- }
- for _, idx := range step.stop {
- ev := &testEvents[idx]
- em.Reschedule(ev, time.Time{})
- }
- em.AdvanceTo(step.advanceTo)
- for j := range testEvents {
- if !slices.Equal(testEvents[j].fireTimes, step.want[j]) {
- t.Errorf("step %v event %v fire times = %v, want %v", i, j, testEvents[j].fireTimes, step.want[j])
- }
- testEvents[j].fireTimes = nil
- }
- }
-}
-
-func TestClockFollowRealTime(t *testing.T) {
- t.Parallel()
-
- type advanceInfo struct {
- when int
- advanceTestClock time.Duration
- advanceTestClockTo time.Time
- advanceRealTimeClock time.Duration
- }
-
- tests := []struct {
- name string
- start time.Time
- wantStart time.Time // This may differ from start when start.IsZero().
- realTimeClockOpts ClockOpts
- advances []advanceInfo
- wants []time.Time // The return values of sequential calls to Now().
- }{
- {
- name: "increment ms then advance 1s",
- start: time.Unix(12345, 1000),
- wantStart: time.Unix(12345, 1000),
- advances: []advanceInfo{
- {
- when: 1,
- advanceRealTimeClock: 1000,
- },
- {
- when: 2,
- advanceRealTimeClock: 1000,
- },
- {
- when: 3,
- advanceRealTimeClock: 1000,
- },
- {
- when: 4,
- advanceTestClock: time.Second,
- },
- {
- when: 5,
- advanceRealTimeClock: 1000,
- },
- {
- when: 6,
- advanceRealTimeClock: 1000,
- },
- {
- when: 7,
- advanceRealTimeClock: 1000,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 2000),
- time.Unix(12345, 3000),
- time.Unix(12345, 4000),
- time.Unix(12346, 4000),
- time.Unix(12346, 5000),
- time.Unix(12346, 6000),
- time.Unix(12346, 7000),
- },
- },
- {
- name: "multiple advances over time",
- start: time.Unix(12345, 1000),
- wantStart: time.Unix(12345, 1000),
- advances: []advanceInfo{
- {
- when: 1,
- advanceRealTimeClock: 1,
- },
- {
- when: 2,
- advanceTestClock: time.Second,
- },
- {
- when: 3,
- advanceRealTimeClock: 1,
- },
- {
- when: 4,
- advanceTestClock: 0,
- },
- {
- when: 5,
- advanceRealTimeClock: 1,
- },
- {
- when: 6,
- advanceTestClock: 1000,
- },
- {
- when: 7,
- advanceRealTimeClock: 1,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 1001),
- time.Unix(12346, 1001),
- time.Unix(12346, 1002),
- time.Unix(12346, 1002),
- time.Unix(12346, 1003),
- time.Unix(12346, 2003),
- time.Unix(12346, 2004),
- },
- },
- {
- name: "multiple advances at once",
- start: time.Unix(12345, 1000),
- wantStart: time.Unix(12345, 1000),
- advances: []advanceInfo{
- {
- when: 1,
- advanceRealTimeClock: 1,
- },
- {
- when: 2,
- advanceTestClock: time.Second,
- },
- {
- when: 2,
- advanceTestClock: 0,
- },
- {
- when: 2,
- advanceTestClock: 1000,
- },
- {
- when: 3,
- advanceRealTimeClock: 1,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 1000),
- time.Unix(12345, 1001),
- time.Unix(12346, 2001),
- time.Unix(12346, 2002),
- },
- },
- {
- name: "changes at start",
- start: time.Unix(12345, 1000),
- wantStart: time.Unix(12345, 1000),
- advances: []advanceInfo{
- {
- when: 0,
- advanceTestClock: time.Second,
- },
- {
- when: 0,
- advanceTestClock: 1000,
- },
- {
- when: 1,
- advanceRealTimeClock: 5,
- },
- {
- when: 2,
- advanceRealTimeClock: 5,
- },
- {
- when: 3,
- advanceRealTimeClock: 5,
- },
- },
- wants: []time.Time{
- time.Unix(12346, 2000),
- time.Unix(12346, 2005),
- time.Unix(12346, 2010),
- time.Unix(12346, 2015),
- },
- },
- {
- name: "start from current time",
- realTimeClockOpts: ClockOpts{
- Start: time.Unix(12345, 0),
- },
- wantStart: time.Unix(12345, 0),
- advances: []advanceInfo{
- {
- when: 1,
- advanceTestClock: time.Second,
- },
- {
- when: 2,
- advanceRealTimeClock: 10 * time.Second,
- },
- {
- when: 3,
- advanceTestClock: time.Minute,
- },
- {
- when: 4,
- advanceRealTimeClock: time.Hour,
- },
- {
- when: 5,
- advanceTestClockTo: time.Unix(100, 0),
- },
- {
- when: 6,
- advanceRealTimeClock: time.Hour,
- },
- },
- wants: []time.Time{
- time.Unix(12345, 0),
- time.Unix(12346, 0),
- time.Unix(12356, 0),
- time.Unix(12416, 0),
- time.Unix(16016, 0),
- time.Unix(100, 0),
- time.Unix(3700, 0),
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- realTimeClock := NewClock(tt.realTimeClockOpts)
- clock := newClockInternal(ClockOpts{
- Start: tt.start,
- FollowRealTime: true,
- }, realTimeClock)
- changeIndex := 0
-
- for i := range tt.wants {
- for len(tt.advances) > changeIndex && tt.advances[changeIndex].when == i {
- advance := tt.advances[changeIndex]
- if advance.advanceTestClockTo.IsZero() {
- clock.Advance(advance.advanceTestClock)
- } else {
- clock.AdvanceTo(advance.advanceTestClockTo)
- }
- realTimeClock.Advance(advance.advanceRealTimeClock)
- changeIndex++
- }
-
- if start := clock.GetStart(); !start.Equal(tt.wantStart) {
- t.Errorf("clock has start %v, want %v", start, tt.wantStart)
- }
-
- if got := clock.Now(); !got.Equal(tt.wants[i]) {
- t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i])
- }
- if got := clock.PeekNow(); !got.Equal(tt.wants[i]) {
- t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i])
- }
- }
- })
- }
-}
-
-func TestAfterFunc(t *testing.T) {
- t.Parallel()
-
- type testStep struct {
- stop bool
- stopReturn bool // The expected return value for Stop() if stop is true.
- reset time.Duration
- resetAbsolute time.Time
- resetReturn bool // The expected return value for Reset() or ResetAbsolute().
- setStep time.Duration
- advance time.Duration
- advanceRealTime time.Duration
- wantTime time.Time
- wantTick bool
- }
-
- tests := []struct {
- name string
- realTimeOpts *ClockOpts
- start time.Time
- step time.Duration
- delay time.Duration
- steps []testStep
- }{
- {
- name: "no tick advance",
- start: time.Unix(12345, 0),
- delay: time.Second,
- steps: []testStep{
- {
- advance: time.Second - 1,
- wantTime: time.Unix(12345, 999_999_999),
- },
- },
- },
- {
- name: "no tick step",
- start: time.Unix(12345, 0),
- step: time.Second - 1,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12345, 999_999_999),
- },
- },
- },
- {
- name: "single tick advance exact",
- start: time.Unix(12345, 0),
- delay: time.Second,
- steps: []testStep{
- {
- advance: time.Second,
- wantTime: time.Unix(12346, 0),
- wantTick: true,
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12347, 0),
- },
- },
- },
- {
- name: "single tick advance extra",
- start: time.Unix(12345, 0),
- delay: time.Second,
- steps: []testStep{
- {
- advance: time.Second + 1,
- wantTime: time.Unix(12346, 1),
- wantTick: true,
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12347, 1),
- },
- },
- },
- {
- name: "single tick step exact",
- start: time.Unix(12345, 0),
- step: time.Second,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 0),
- wantTick: true,
- },
- {
- wantTime: time.Unix(12347, 0),
- },
- },
- },
- {
- name: "single tick step extra",
- start: time.Unix(12345, 0),
- step: time.Second + 1,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 1),
- wantTick: true,
- },
- {
- wantTime: time.Unix(12347, 2),
- },
- },
- },
- {
- name: "reset for single tick per advance",
- start: time.Unix(12345, 0),
- delay: 3 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advance: 4 * time.Second,
- wantTime: time.Unix(12349, 0),
- wantTick: true,
- },
- {
- resetAbsolute: time.Unix(12351, 0),
- advance: 2 * time.Second,
- wantTime: time.Unix(12351, 0),
- wantTick: true,
- },
- {
- reset: 3 * time.Second,
- advance: 2 * time.Second,
- wantTime: time.Unix(12353, 0),
- },
- {
- advance: 2 * time.Second,
- wantTime: time.Unix(12355, 0),
- wantTick: true,
- },
- {
- advance: 10 * time.Second,
- wantTime: time.Unix(12365, 0),
- },
- },
- },
- {
- name: "reset for single tick per step",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: 3 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- },
- {
- wantTime: time.Unix(12349, 0),
- wantTick: true,
- },
- {
- reset: time.Second,
- wantTime: time.Unix(12351, 0),
- wantTick: true,
- },
- {
- resetAbsolute: time.Unix(12354, 0),
- wantTime: time.Unix(12353, 0),
- },
- {
- wantTime: time.Unix(12355, 0),
- wantTick: true,
- },
- },
- },
- {
- name: "reset while active",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: 3 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- },
- {
- reset: 3 * time.Second,
- resetReturn: true,
- wantTime: time.Unix(12349, 0),
- },
- {
- resetAbsolute: time.Unix(12354, 0),
- resetReturn: true,
- wantTime: time.Unix(12351, 0),
- },
- {
- wantTime: time.Unix(12353, 0),
- },
- {
- wantTime: time.Unix(12355, 0),
- wantTick: true,
- },
- },
- },
- {
- name: "stop after fire",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- wantTick: true,
- },
- {
- stop: true,
- wantTime: time.Unix(12349, 0),
- },
- {
- wantTime: time.Unix(12351, 0),
- },
- {
- advance: 10 * time.Second,
- wantTime: time.Unix(12361, 0),
- },
- },
- },
- {
- name: "stop before fire",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: time.Second,
- steps: []testStep{
- {
- stop: true,
- stopReturn: true,
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- },
- {
- wantTime: time.Unix(12349, 0),
- },
- {
- wantTime: time.Unix(12351, 0),
- },
- {
- advance: 10 * time.Second,
- wantTime: time.Unix(12361, 0),
- },
- },
- },
- {
- name: "stop after reset",
- start: time.Unix(12345, 0),
- step: 2 * time.Second,
- delay: time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- wantTick: true,
- },
- {
- reset: 10 * time.Second,
- wantTime: time.Unix(12349, 0),
- },
- {
- stop: true,
- stopReturn: true,
- wantTime: time.Unix(12351, 0),
- },
- {
- advance: 10 * time.Second,
- wantTime: time.Unix(12361, 0),
- },
- },
- },
- {
- name: "reset while running",
- start: time.Unix(12345, 0),
- delay: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12346, 0),
- },
- {
- advance: time.Second,
- wantTime: time.Unix(12347, 0),
- wantTick: true,
- },
- {
- advance: time.Second,
- reset: time.Second,
- wantTime: time.Unix(12348, 0),
- wantTick: true,
- },
- {
- setStep: 5 * time.Second,
- reset: 10 * time.Second,
- wantTime: time.Unix(12353, 0),
- },
- {
- wantTime: time.Unix(12358, 0),
- wantTick: true,
- },
- },
- },
- {
- name: "reset while stopped",
- start: time.Unix(12345, 0),
- step: time.Second,
- delay: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 0),
- },
- {
- stop: true,
- stopReturn: true,
- wantTime: time.Unix(12347, 0),
- },
- {
- wantTime: time.Unix(12348, 0),
- },
- {
- wantTime: time.Unix(12349, 0),
- },
- {
- reset: time.Second,
- wantTime: time.Unix(12350, 0),
- wantTick: true,
- },
- {
- wantTime: time.Unix(12351, 0),
- },
- },
- },
- {
- name: "reset absolute",
- start: time.Unix(12345, 0),
- step: time.Second,
- delay: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- wantTime: time.Unix(12346, 0),
- },
- {
- wantTime: time.Unix(12347, 0),
- wantTick: true,
- },
- {
- resetAbsolute: time.Unix(12354, 50),
- advance: 7 * time.Second,
- wantTime: time.Unix(12354, 0),
- },
- {
- wantTime: time.Unix(12355, 0),
- wantTick: true,
- },
- {
- wantTime: time.Unix(12356, 0),
- },
- },
- },
- {
- name: "follow real time",
- realTimeOpts: new(ClockOpts),
- start: time.Unix(12345, 0),
- delay: 2 * time.Second,
- steps: []testStep{
- {
- wantTime: time.Unix(12345, 0),
- },
- {
- advanceRealTime: 5 * time.Second,
- wantTime: time.Unix(12350, 0),
- wantTick: true,
- },
- {
- reset: 2 * time.Second,
- advance: 5 * time.Second,
- wantTime: time.Unix(12355, 0),
- wantTick: true,
- },
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- var realTimeClockForTestClock tstime.Clock
- var realTimeClock *Clock
- if tt.realTimeOpts != nil {
- realTimeClock = NewClock(*tt.realTimeOpts)
- // Passing realTimeClock into newClockInternal results in a
- // non-nil interface with a nil pointer, so this is necessary.
- realTimeClockForTestClock = realTimeClock
- }
-
- var gotTick atomic.Bool
-
- clock := newClockInternal(ClockOpts{
- Start: tt.start,
- Step: tt.step,
- FollowRealTime: realTimeClock != nil,
- }, realTimeClockForTestClock)
- tc := clock.AfterFunc(tt.delay, func() {
- if gotTick.Swap(true) == true {
- t.Error("multiple ticks detected")
- }
- })
- timerControl := tc.(*Timer)
-
- t.Cleanup(func() { timerControl.Stop() })
-
- if gotTick.Load() {
- t.Error("initial tick detected, want none")
- }
-
- for i, step := range tt.steps {
- if step.stop {
- if got := timerControl.Stop(); got != step.stopReturn {
- t.Errorf("step %v Stop returned %v, want %v", i, got, step.stopReturn)
- }
- }
-
- if !step.resetAbsolute.IsZero() {
- if got := timerControl.ResetAbsolute(step.resetAbsolute); got != step.resetReturn {
- t.Errorf("step %v Reset returned %v, want %v", i, got, step.resetReturn)
- }
- }
-
- if step.reset > 0 {
- if got := timerControl.Reset(step.reset); got != step.resetReturn {
- t.Errorf("step %v Reset returned %v, want %v", i, got, step.resetReturn)
- }
- }
-
- if step.setStep > 0 {
- clock.SetStep(step.setStep)
- }
-
- if step.advance > 0 {
- clock.Advance(step.advance)
- }
- if step.advanceRealTime > 0 {
- realTimeClock.Advance(step.advanceRealTime)
- }
-
- if now := clock.Now(); !step.wantTime.IsZero() && !now.Equal(step.wantTime) {
- t.Errorf("step %v now = %v, want %v", i, now, step.wantTime)
- }
-
- if got := gotTick.Swap(false); got != step.wantTick {
- t.Errorf("step %v tick %v, want %v", i, got, step.wantTick)
- }
- }
- })
- }
-}
-
-func TestSince(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- start time.Time
- since time.Time
- want time.Duration
- }{
- {
- name: "positive",
- start: time.Unix(12345, 1000),
- since: time.Unix(11111, 1000),
- want: 1234 * time.Second,
- },
- {
- name: "negative",
- start: time.Unix(12345, 1000),
- since: time.Unix(15436, 1000),
- want: -3091 * time.Second,
- },
- {
- name: "zero",
- start: time.Unix(12345, 1000),
- since: time.Unix(12345, 1000),
- want: 0,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- clock := NewClock(ClockOpts{
- Start: tt.start,
- })
- got := clock.Since(tt.since)
- if got != tt.want {
- t.Errorf("Since duration %v, want %v", got, tt.want)
- }
- })
- }
-}
diff --git a/tstime/tstime.go b/tstime/tstime.go
index 6e5b7f9f4..c60c1aad4 100644
--- a/tstime/tstime.go
+++ b/tstime/tstime.go
@@ -108,6 +108,8 @@ func (c DefaultClock) Since(t time.Time) time.Duration {
// time precisely, something required for certain types of tests to be possible
// at all, speeds up execution by not needing to sleep, and can dramatically
// reduce the risk of flakes due to tests executing too slowly or quickly.
+//
+// DEPRECATED: now that testing/synctest is available, use that for testing.
type Clock interface {
// Now returns the current time, as in time.Now.
Now() time.Time