Skip to content

Commit f375064

Browse files
authored
Add unit tests for pkg/sharding/leases (#484)
* Add unit tests for `pkg/sharding/leases` * Nits
1 parent b6db1a8 commit f375064

File tree

5 files changed

+418
-5
lines changed

5 files changed

+418
-5
lines changed

pkg/sharding/key.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
shardingv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/sharding/v1alpha1"
2727
)
2828

29-
// KeyFuncForResource returns the key function that maps the given resource or its controller dependening on whether
29+
// KeyFuncForResource returns the key function that maps the given resource or its controller depending on whether
3030
// the resource is listed as a resource or controlled resource in the given ring.
3131
func KeyFuncForResource(gr metav1.GroupResource, ring *shardingv1alpha1.ControllerRing) (KeyFunc, error) {
3232
ringResources := sets.New[metav1.GroupResource]()

pkg/sharding/leases/doc.go renamed to pkg/sharding/leases/leases_suite_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2023 Tim Ebert.
2+
Copyright 2025 Tim Ebert.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -14,6 +14,16 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// Package leases implements logic for determining the state of shards based on their membership Lease object.
18-
// It is used by the shardlease controller to maintain the state label.
19-
package leases
17+
package leases_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestLeases(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "Leases Suite")
29+
}

pkg/sharding/leases/shards_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
Copyright 2025 Tim Ebert.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package leases_test
18+
19+
import (
20+
"time"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
"github.com/onsi/gomega/gstruct"
25+
coordinationv1 "k8s.io/api/coordination/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/utils/ptr"
28+
29+
. "github.com/timebertt/kubernetes-controller-sharding/pkg/sharding/leases"
30+
)
31+
32+
var _ = Describe("Shards", func() {
33+
Describe("#ByID", func() {
34+
It("should return the matching shard", func() {
35+
shards := Shards{
36+
{ID: "shard-1"},
37+
{ID: "shard-2"},
38+
{ID: "shard-3"},
39+
}
40+
Expect(shards.ByID("shard-2").ID).To(Equal("shard-2"))
41+
})
42+
43+
It("should return a zero shard if the ID has not been found", func() {
44+
Expect(Shards{{ID: "shard-1"}}.ByID("shard-2").ID).To(BeZero())
45+
Expect(Shards{}.ByID("shard-2").ID).To(BeZero())
46+
Expect(Shards(nil).ByID("shard-2").ID).To(BeZero())
47+
})
48+
})
49+
50+
Describe("#AvailableShards", func() {
51+
It("should return the available shards", func() {
52+
shards := Shards{
53+
{ID: "shard-1", State: Ready},
54+
{ID: "shard-2", State: Expired},
55+
{ID: "shard-3", State: Uncertain},
56+
{ID: "shard-4", State: Dead},
57+
{ID: "shard-5", State: Orphaned},
58+
}
59+
Expect(shards.AvailableShards().IDs()).To(ConsistOf("shard-1", "shard-2", "shard-3"))
60+
})
61+
})
62+
63+
Describe("#IDs", func() {
64+
It("should return the shard IDs", func() {
65+
shards := Shards{
66+
{ID: "shard-1"},
67+
{ID: "shard-2"},
68+
}
69+
Expect(shards.IDs()).To(ConsistOf("shard-1", "shard-2"))
70+
})
71+
})
72+
73+
Describe("#ToShards", func() {
74+
var (
75+
now time.Time
76+
lease *coordinationv1.Lease
77+
)
78+
79+
BeforeEach(func() {
80+
now = time.Now()
81+
82+
lease = &coordinationv1.Lease{
83+
Spec: coordinationv1.LeaseSpec{
84+
HolderIdentity: ptr.To("shard"),
85+
LeaseDurationSeconds: ptr.To[int32](10),
86+
AcquireTime: ptr.To(metav1.NewMicroTime(now.Add(-2 * time.Minute))),
87+
RenewTime: ptr.To(metav1.NewMicroTime(now.Add(-2 * time.Second))),
88+
},
89+
}
90+
})
91+
92+
It("should correctly transform the lease objects", func() {
93+
leases := make([]coordinationv1.Lease, 2)
94+
95+
leases[0] = *lease.DeepCopy()
96+
leases[0].Name = "shard-1"
97+
leases[0].Spec.HolderIdentity = ptr.To("shard-1")
98+
99+
leases[1] = *lease.DeepCopy()
100+
leases[1].Name = "shard-2"
101+
leases[1].Spec.HolderIdentity = nil
102+
leases[1].Spec.LeaseDurationSeconds = ptr.To[int32](1)
103+
leases[1].Spec.AcquireTime = ptr.To(metav1.NewMicroTime(now))
104+
leases[1].Spec.RenewTime = ptr.To(metav1.NewMicroTime(now))
105+
106+
Expect(ToShards(leases, now)).To(ConsistOf(
107+
gstruct.MatchAllFields(gstruct.Fields{
108+
"ID": Equal("shard-1"),
109+
"State": Equal(Ready),
110+
"Times": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
111+
"Expiration": Equal(now.Add(8 * time.Second)),
112+
}),
113+
}),
114+
gstruct.MatchAllFields(gstruct.Fields{
115+
"ID": Equal("shard-2"),
116+
"State": Equal(Dead),
117+
"Times": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
118+
"ToOrphaned": Equal(time.Second + time.Minute),
119+
}),
120+
}),
121+
))
122+
})
123+
})
124+
})

pkg/sharding/leases/state_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
Copyright 2025 Tim Ebert.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package leases_test
18+
19+
import (
20+
"time"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
coordinationv1 "k8s.io/api/coordination/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/utils/ptr"
27+
28+
. "github.com/timebertt/kubernetes-controller-sharding/pkg/sharding/leases"
29+
)
30+
31+
var _ = Describe("ShardState", func() {
32+
Describe("#String", func() {
33+
It("should return the state as a string", func() {
34+
Expect(Unknown.String()).To(Equal("unknown"))
35+
Expect(Orphaned.String()).To(Equal("orphaned"))
36+
Expect(Dead.String()).To(Equal("dead"))
37+
Expect(Uncertain.String()).To(Equal("uncertain"))
38+
Expect(Expired.String()).To(Equal("expired"))
39+
Expect(Ready.String()).To(Equal("ready"))
40+
Expect(ShardState(-1).String()).To(Equal("unknown"))
41+
})
42+
})
43+
44+
Describe("#StateFromString", func() {
45+
It("should return the correct state matching the string", func() {
46+
Expect(StateFromString("unknown")).To(Equal(Unknown))
47+
Expect(StateFromString("orphaned")).To(Equal(Orphaned))
48+
Expect(StateFromString("dead")).To(Equal(Dead))
49+
Expect(StateFromString("uncertain")).To(Equal(Uncertain))
50+
Expect(StateFromString("expired")).To(Equal(Expired))
51+
Expect(StateFromString("ready")).To(Equal(Ready))
52+
Expect(StateFromString("foo")).To(Equal(Unknown))
53+
})
54+
})
55+
56+
Describe("#IsAvailable", func() {
57+
It("should return false for unavailable states", func() {
58+
Expect(Unknown.IsAvailable()).To(BeFalse())
59+
Expect(Orphaned.IsAvailable()).To(BeFalse())
60+
Expect(Dead.IsAvailable()).To(BeFalse())
61+
})
62+
63+
It("should return true for available states", func() {
64+
Expect(Uncertain.IsAvailable()).To(BeTrue())
65+
Expect(Expired.IsAvailable()).To(BeTrue())
66+
Expect(Ready.IsAvailable()).To(BeTrue())
67+
})
68+
})
69+
70+
Describe("#ToState", func() {
71+
var (
72+
now time.Time
73+
lease *coordinationv1.Lease
74+
)
75+
76+
BeforeEach(func() {
77+
now = time.Now()
78+
79+
lease = &coordinationv1.Lease{
80+
ObjectMeta: metav1.ObjectMeta{
81+
Name: "shard",
82+
},
83+
Spec: coordinationv1.LeaseSpec{
84+
HolderIdentity: ptr.To("shard"),
85+
LeaseDurationSeconds: ptr.To[int32](10),
86+
AcquireTime: ptr.To(metav1.NewMicroTime(now.Add(-2 * time.Minute))),
87+
RenewTime: ptr.To(metav1.NewMicroTime(now.Add(-2 * time.Second))),
88+
},
89+
}
90+
})
91+
92+
It("should return ready if lease has not expired", func() {
93+
Expect(ToState(lease, now)).To(Equal(Ready))
94+
})
95+
96+
It("should return expired if lease has expired", func() {
97+
lease.Spec.RenewTime = ptr.To(metav1.NewMicroTime(now.Add(-15 * time.Second)))
98+
Expect(ToState(lease, now)).To(Equal(Expired))
99+
})
100+
101+
It("should return expired if acquireTime is missing", func() {
102+
lease.Spec.AcquireTime = nil
103+
Expect(ToState(lease, now)).To(Equal(Expired))
104+
})
105+
106+
It("should return expired if renewTime is missing", func() {
107+
lease.Spec.RenewTime = nil
108+
Expect(ToState(lease, now)).To(Equal(Expired))
109+
})
110+
111+
It("should return expired if leaseDuration is missing", func() {
112+
lease.Spec.LeaseDurationSeconds = nil
113+
Expect(ToState(lease, now)).To(Equal(Expired))
114+
})
115+
116+
It("should return uncertain if lease has expired more than leaseDuration ago", func() {
117+
lease.Spec.RenewTime = ptr.To(metav1.NewMicroTime(now.Add(-25 * time.Second)))
118+
Expect(ToState(lease, now)).To(Equal(Uncertain))
119+
})
120+
121+
It("should return dead if lease has been released", func() {
122+
lease.Spec.HolderIdentity = nil
123+
Expect(ToState(lease, now)).To(Equal(Dead))
124+
125+
lease.Spec.HolderIdentity = ptr.To("")
126+
Expect(ToState(lease, now)).To(Equal(Dead))
127+
})
128+
129+
It("should return dead if lease has been acquired by sharder", func() {
130+
lease.Spec.HolderIdentity = ptr.To("shardlease-controller")
131+
lease.Spec.LeaseDurationSeconds = ptr.To[int32](20)
132+
lease.Spec.AcquireTime = ptr.To(metav1.NewMicroTime(now))
133+
lease.Spec.RenewTime = ptr.To(metav1.NewMicroTime(now))
134+
Expect(ToState(lease, now)).To(Equal(Dead))
135+
})
136+
137+
It("should return orphaned if lease has been released 1 minute and leaseDuration ago", func() {
138+
lease.Spec.HolderIdentity = nil
139+
lease.Spec.LeaseDurationSeconds = ptr.To[int32](1)
140+
lease.Spec.AcquireTime = ptr.To(metav1.NewMicroTime(now.Add(-time.Minute - time.Second)))
141+
lease.Spec.RenewTime = ptr.To(metav1.NewMicroTime(now.Add(-time.Minute - time.Second)))
142+
Expect(ToState(lease, now)).To(Equal(Orphaned))
143+
})
144+
145+
It("should return orphaned if lease has been acquired by sharder 1 minute and ago", func() {
146+
lease.Spec.HolderIdentity = ptr.To("shardlease-controller")
147+
lease.Spec.LeaseDurationSeconds = ptr.To[int32](20)
148+
lease.Spec.AcquireTime = ptr.To(metav1.NewMicroTime(now.Add(-time.Minute - 20*time.Second)))
149+
lease.Spec.RenewTime = ptr.To(metav1.NewMicroTime(now.Add(-time.Minute - 20*time.Second)))
150+
Expect(ToState(lease, now)).To(Equal(Orphaned))
151+
})
152+
})
153+
})

0 commit comments

Comments
 (0)