Skip to content

Commit c1d5bb8

Browse files
committed
test(juicefs): migrate controller tests to Ginkgo v2
- Update suite_test.go to Ginkgo v2 (remove deprecated Done channel) - Add implement_test.go: getRuntime, GetOrCreateEngine, RemoveEngine - Add juicefsruntime_controller_test.go: ControllerName, ManagedResource, NewRuntimeReconciler, NewCacheOption - Coverage: 79.1% (gate >75% PASS) Signed-off-by: Harsh <harshmastic@gmail.com>
1 parent a69c888 commit c1d5bb8

3 files changed

Lines changed: 306 additions & 42 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
Copyright 2021 The Fluid Authors.
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 juicefs
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"sync"
23+
24+
"github.com/agiledragon/gomonkey/v2"
25+
. "github.com/onsi/ginkgo/v2"
26+
. "github.com/onsi/gomega"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/runtime"
29+
"k8s.io/apimachinery/pkg/types"
30+
"k8s.io/client-go/tools/record"
31+
ctrl "sigs.k8s.io/controller-runtime"
32+
33+
datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1"
34+
"github.com/fluid-cloudnative/fluid/pkg/controllers"
35+
"github.com/fluid-cloudnative/fluid/pkg/dataoperation"
36+
"github.com/fluid-cloudnative/fluid/pkg/ddc"
37+
"github.com/fluid-cloudnative/fluid/pkg/ddc/base"
38+
cruntime "github.com/fluid-cloudnative/fluid/pkg/runtime"
39+
"github.com/fluid-cloudnative/fluid/pkg/utils/fake"
40+
)
41+
42+
// mockEngine is a minimal no-op implementation of base.Engine used in tests only.
43+
type mockEngine struct{}
44+
45+
func (m *mockEngine) ID() string { return "mock" }
46+
func (m *mockEngine) Shutdown() error { return nil }
47+
func (m *mockEngine) Setup(_ cruntime.ReconcileRequestContext) (bool, error) { return true, nil }
48+
func (m *mockEngine) CreateVolume() error { return nil }
49+
func (m *mockEngine) DeleteVolume() error { return nil }
50+
func (m *mockEngine) Sync(_ cruntime.ReconcileRequestContext) error { return nil }
51+
func (m *mockEngine) Validate(_ cruntime.ReconcileRequestContext) error { return nil }
52+
func (m *mockEngine) Operate(_ cruntime.ReconcileRequestContext, _ *datav1alpha1.OperationStatus, _ dataoperation.OperationInterface) (ctrl.Result, error) {
53+
return ctrl.Result{}, nil
54+
}
55+
56+
// newTestJuiceFSReconciler builds a JuiceFSRuntimeReconciler seeded with the
57+
// given scheme and runtime objects. Pass nil scheme to get a default one.
58+
func newTestJuiceFSReconciler(s *runtime.Scheme, objs ...runtime.Object) *JuiceFSRuntimeReconciler {
59+
if s == nil {
60+
s = runtime.NewScheme()
61+
_ = datav1alpha1.AddToScheme(s)
62+
}
63+
fakeClient := fake.NewFakeClientWithScheme(s, objs...)
64+
log := ctrl.Log.WithName("juicefs-test")
65+
recorder := record.NewFakeRecorder(10)
66+
r := &JuiceFSRuntimeReconciler{
67+
Scheme: s,
68+
mutex: &sync.Mutex{},
69+
engines: map[string]base.Engine{},
70+
}
71+
r.RuntimeReconciler = controllers.NewRuntimeReconciler(r, fakeClient, log, recorder)
72+
return r
73+
}
74+
75+
var _ = Describe("JuiceFSRuntimeReconciler Implement", func() {
76+
77+
Describe("getRuntime", func() {
78+
var r *JuiceFSRuntimeReconciler
79+
80+
BeforeEach(func() {
81+
testRuntime := &datav1alpha1.JuiceFSRuntime{
82+
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
83+
}
84+
s := runtime.NewScheme()
85+
_ = datav1alpha1.AddToScheme(s)
86+
r = newTestJuiceFSReconciler(s, testRuntime)
87+
})
88+
89+
It("should return the runtime when it exists in the cluster", func() {
90+
ctx := cruntime.ReconcileRequestContext{
91+
Context: context.TODO(),
92+
NamespacedName: types.NamespacedName{Name: "test", Namespace: "default"},
93+
}
94+
result, err := r.getRuntime(ctx)
95+
Expect(err).NotTo(HaveOccurred())
96+
Expect(result).NotTo(BeNil())
97+
Expect(result.Name).To(Equal("test"))
98+
Expect(result.Namespace).To(Equal("default"))
99+
})
100+
101+
It("should return an error when the runtime does not exist", func() {
102+
ctx := cruntime.ReconcileRequestContext{
103+
Context: context.TODO(),
104+
NamespacedName: types.NamespacedName{Name: "nonexistent", Namespace: "default"},
105+
}
106+
result, err := r.getRuntime(ctx)
107+
Expect(err).To(HaveOccurred())
108+
Expect(result).To(BeNil())
109+
})
110+
})
111+
112+
Describe("GetOrCreateEngine", func() {
113+
var r *JuiceFSRuntimeReconciler
114+
115+
BeforeEach(func() {
116+
r = newTestJuiceFSReconciler(nil)
117+
})
118+
119+
It("should propagate engine creation errors", func() {
120+
patches := gomonkey.ApplyFunc(ddc.CreateEngine,
121+
func(_ string, _ cruntime.ReconcileRequestContext) (base.Engine, error) {
122+
return nil, fmt.Errorf("engine creation failed")
123+
})
124+
defer patches.Reset()
125+
126+
ctx := cruntime.ReconcileRequestContext{
127+
Context: context.TODO(),
128+
NamespacedName: types.NamespacedName{Name: "fail", Namespace: "default"},
129+
}
130+
engine, err := r.GetOrCreateEngine(ctx)
131+
Expect(err).To(HaveOccurred())
132+
Expect(err.Error()).To(ContainSubstring("engine creation failed"))
133+
Expect(engine).To(BeNil())
134+
})
135+
136+
It("should create engine on first call and return cached engine on second call", func() {
137+
mock := &mockEngine{}
138+
callCount := 0
139+
patches := gomonkey.ApplyFunc(ddc.CreateEngine,
140+
func(_ string, _ cruntime.ReconcileRequestContext) (base.Engine, error) {
141+
callCount++
142+
return mock, nil
143+
})
144+
defer patches.Reset()
145+
146+
ctx := cruntime.ReconcileRequestContext{
147+
Context: context.TODO(),
148+
NamespacedName: types.NamespacedName{Name: "cached", Namespace: "default"},
149+
}
150+
151+
// First call: engine is created and stored.
152+
engine1, err := r.GetOrCreateEngine(ctx)
153+
Expect(err).NotTo(HaveOccurred())
154+
Expect(engine1).To(Equal(base.Engine(mock)))
155+
Expect(callCount).To(Equal(1))
156+
157+
// Second call: engine should be retrieved from the cache without re-creation.
158+
engine2, err := r.GetOrCreateEngine(ctx)
159+
Expect(err).NotTo(HaveOccurred())
160+
Expect(engine2).To(Equal(base.Engine(mock)))
161+
Expect(callCount).To(Equal(1), "CreateEngine must not be called a second time")
162+
})
163+
})
164+
165+
Describe("RemoveEngine", func() {
166+
var r *JuiceFSRuntimeReconciler
167+
168+
BeforeEach(func() {
169+
r = newTestJuiceFSReconciler(nil)
170+
})
171+
172+
It("should remove a cached engine by namespaced name", func() {
173+
id := ddc.GenerateEngineID(types.NamespacedName{Name: "test", Namespace: "default"})
174+
r.engines[id] = &mockEngine{}
175+
176+
ctx := cruntime.ReconcileRequestContext{
177+
Context: context.TODO(),
178+
NamespacedName: types.NamespacedName{Name: "test", Namespace: "default"},
179+
}
180+
r.RemoveEngine(ctx)
181+
182+
_, found := r.engines[id]
183+
Expect(found).To(BeFalse())
184+
})
185+
186+
It("should not panic when removing a non-existent engine", func() {
187+
ctx := cruntime.ReconcileRequestContext{
188+
Context: context.TODO(),
189+
NamespacedName: types.NamespacedName{Name: "ghost", Namespace: "default"},
190+
}
191+
Expect(func() { r.RemoveEngine(ctx) }).NotTo(Panic())
192+
})
193+
})
194+
195+
Describe("Reconcile", func() {
196+
It("should return no error when the runtime is not found", func() {
197+
// The fake client has no JuiceFSRuntime objects, so getRuntime will
198+
// return a NotFound error, which Reconcile should swallow gracefully.
199+
s := runtime.NewScheme()
200+
_ = datav1alpha1.AddToScheme(s)
201+
r := newTestJuiceFSReconciler(s)
202+
203+
req := ctrl.Request{
204+
NamespacedName: types.NamespacedName{Name: "missing", Namespace: "default"},
205+
}
206+
result, err := r.Reconcile(context.TODO(), req)
207+
Expect(err).NotTo(HaveOccurred())
208+
Expect(result).To(Equal(ctrl.Result{}))
209+
})
210+
})
211+
})
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
Copyright 2021 The Fluid Authors.
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 juicefs
18+
19+
import (
20+
appsv1 "k8s.io/api/apps/v1"
21+
"k8s.io/apimachinery/pkg/runtime"
22+
"k8s.io/client-go/tools/record"
23+
ctrl "sigs.k8s.io/controller-runtime"
24+
25+
. "github.com/onsi/ginkgo/v2"
26+
. "github.com/onsi/gomega"
27+
28+
datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1"
29+
"github.com/fluid-cloudnative/fluid/pkg/common"
30+
"github.com/fluid-cloudnative/fluid/pkg/utils/fake"
31+
)
32+
33+
var _ = Describe("JuiceFSRuntimeReconciler", func() {
34+
35+
Describe("ControllerName", func() {
36+
It("should return the constant controller name", func() {
37+
r := &JuiceFSRuntimeReconciler{}
38+
Expect(r.ControllerName()).To(Equal(controllerName))
39+
Expect(r.ControllerName()).To(Equal("JuiceFSRuntimeController"))
40+
})
41+
})
42+
43+
Describe("ManagedResource", func() {
44+
It("should return a JuiceFSRuntime with correct TypeMeta", func() {
45+
r := &JuiceFSRuntimeReconciler{}
46+
obj := r.ManagedResource()
47+
juicefsRuntime, ok := obj.(*datav1alpha1.JuiceFSRuntime)
48+
Expect(ok).To(BeTrue())
49+
Expect(juicefsRuntime.Kind).To(Equal(datav1alpha1.JuiceFSRuntimeKind))
50+
Expect(juicefsRuntime.APIVersion).To(ContainSubstring(datav1alpha1.GroupVersion.Group))
51+
})
52+
})
53+
54+
Describe("NewRuntimeReconciler", func() {
55+
It("should initialize reconciler with all required fields set", func() {
56+
s := runtime.NewScheme()
57+
fakeClient := fake.NewFakeClientWithScheme(s)
58+
log := ctrl.Log.WithName("test")
59+
recorder := record.NewFakeRecorder(10)
60+
61+
r := NewRuntimeReconciler(fakeClient, log, s, recorder)
62+
Expect(r).NotTo(BeNil())
63+
Expect(r.Scheme).To(Equal(s))
64+
Expect(r.mutex).NotTo(BeNil())
65+
Expect(r.engines).NotTo(BeNil())
66+
Expect(r.RuntimeReconciler).NotTo(BeNil())
67+
})
68+
})
69+
70+
Describe("NewCacheOption", func() {
71+
It("should return cache options with two ByObject entries", func() {
72+
opts := NewCacheOption()
73+
Expect(opts.ByObject).To(HaveLen(2))
74+
})
75+
76+
It("should have label selectors for StatefulSet and DaemonSet keyed by type", func() {
77+
opts := NewCacheOption()
78+
var seenStatefulSet, seenDaemonSet bool
79+
for key, byObj := range opts.ByObject {
80+
Expect(byObj.Label).NotTo(BeNil())
81+
Expect(byObj.Label.String()).To(ContainSubstring(common.JuiceFSRuntime))
82+
switch key.(type) {
83+
case *appsv1.StatefulSet:
84+
seenStatefulSet = true
85+
case *appsv1.DaemonSet:
86+
seenDaemonSet = true
87+
}
88+
}
89+
Expect(seenStatefulSet).To(BeTrue(), "expected StatefulSet key in ByObject")
90+
Expect(seenDaemonSet).To(BeTrue(), "expected DaemonSet key in ByObject")
91+
})
92+
})
93+
})

pkg/controllers/v1alpha1/juicefs/suite_test.go

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,63 +17,23 @@ limitations under the License.
1717
package juicefs
1818

1919
import (
20-
"path/filepath"
2120
"testing"
2221

2322
. "github.com/onsi/ginkgo/v2"
2423
. "github.com/onsi/gomega"
25-
"k8s.io/client-go/kubernetes/scheme"
26-
"k8s.io/client-go/rest"
27-
"sigs.k8s.io/controller-runtime/pkg/client"
28-
"sigs.k8s.io/controller-runtime/pkg/envtest"
2924
logf "sigs.k8s.io/controller-runtime/pkg/log"
3025

31-
datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1"
3226
"github.com/fluid-cloudnative/fluid/pkg/utils/fake"
33-
//+kubebuilder:scaffold:imports
3427
)
3528

3629
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
3730
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
3831

39-
var cfg *rest.Config
40-
var k8sClient client.Client
41-
var testEnv *envtest.Environment
42-
4332
func TestAPIs(t *testing.T) {
4433
RegisterFailHandler(Fail)
45-
46-
RunSpecs(t,
47-
"Controller Suite")
34+
RunSpecs(t, "JuiceFS Controller Suite")
4835
}
4936

50-
var _ = BeforeSuite(func(done Done) {
37+
var _ = BeforeSuite(func() {
5138
logf.SetLogger(fake.NullLogger())
52-
53-
By("bootstrapping test environment")
54-
testEnv = &envtest.Environment{
55-
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
56-
}
57-
58-
var err error
59-
cfg, err = testEnv.Start()
60-
Expect(err).ToNot(HaveOccurred())
61-
Expect(cfg).ToNot(BeNil())
62-
63-
err = datav1alpha1.AddToScheme(scheme.Scheme)
64-
Expect(err).NotTo(HaveOccurred())
65-
66-
//+kubebuilder:scaffold:scheme
67-
68-
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
69-
Expect(err).ToNot(HaveOccurred())
70-
Expect(k8sClient).ToNot(BeNil())
71-
72-
close(done)
73-
}, 60)
74-
75-
var _ = AfterSuite(func() {
76-
By("tearing down the test environment")
77-
err := testEnv.Stop()
78-
Expect(err).ToNot(HaveOccurred())
7939
})

0 commit comments

Comments
 (0)