Skip to content

Commit 94b87c6

Browse files
authored
test(efc): migrate pkg/controllers/v1alpha1/efc to Ginkgo v2 with fake-client unit tests (#5698)
* test(efc): migrate controller tests to Ginkgo v2 Signed-off-by: Harsh <harshmastic@gmail.com> * test(efc): use background context in implement tests Signed-off-by: Harsh <harshmastic@gmail.com> --------- Signed-off-by: Harsh <harshmastic@gmail.com>
1 parent 6ef2eec commit 94b87c6

3 files changed

Lines changed: 282 additions & 45 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Copyright 2026 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 efc
18+
19+
import (
20+
"k8s.io/apimachinery/pkg/runtime"
21+
"k8s.io/client-go/tools/record"
22+
ctrl "sigs.k8s.io/controller-runtime"
23+
24+
. "github.com/onsi/ginkgo/v2"
25+
. "github.com/onsi/gomega"
26+
27+
datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1"
28+
"github.com/fluid-cloudnative/fluid/pkg/utils/fake"
29+
)
30+
31+
var _ = Describe("RuntimeReconciler (EFC)", func() {
32+
33+
Describe("ControllerName", func() {
34+
It("should return the constant controller name", func() {
35+
r := &RuntimeReconciler{}
36+
Expect(r.ControllerName()).To(Equal("EFCRuntimeController"))
37+
})
38+
})
39+
40+
Describe("ManagedResource", func() {
41+
It("should return an EFCRuntime with correct TypeMeta", func() {
42+
r := &RuntimeReconciler{}
43+
obj := r.ManagedResource()
44+
efcRuntime, ok := obj.(*datav1alpha1.EFCRuntime)
45+
Expect(ok).To(BeTrue())
46+
Expect(efcRuntime.Kind).To(Equal(datav1alpha1.EFCRuntimeKind))
47+
Expect(efcRuntime.APIVersion).To(ContainSubstring(datav1alpha1.GroupVersion.Group))
48+
})
49+
})
50+
51+
Describe("NewRuntimeReconciler", func() {
52+
It("should initialize reconciler with all required fields set", func() {
53+
s := runtime.NewScheme()
54+
fakeClient := fake.NewFakeClientWithScheme(s)
55+
log := ctrl.Log.WithName("test")
56+
recorder := record.NewFakeRecorder(10)
57+
58+
r := NewRuntimeReconciler(fakeClient, log, s, recorder)
59+
Expect(r).NotTo(BeNil())
60+
Expect(r.Scheme).To(Equal(s))
61+
Expect(r.mutex).NotTo(BeNil())
62+
Expect(r.engines).NotTo(BeNil())
63+
Expect(r.RuntimeReconciler).NotTo(BeNil())
64+
})
65+
})
66+
})
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
Copyright 2026 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 efc
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+
// newTestEFCReconciler builds a RuntimeReconciler seeded with the
57+
// given scheme and runtime objects. Pass nil scheme to get a default one.
58+
func newTestEFCReconciler(s *runtime.Scheme, objs ...runtime.Object) *RuntimeReconciler {
59+
if s == nil {
60+
s = runtime.NewScheme()
61+
_ = datav1alpha1.AddToScheme(s)
62+
}
63+
fakeClient := fake.NewFakeClientWithScheme(s, objs...)
64+
log := ctrl.Log.WithName("efc-test")
65+
recorder := record.NewFakeRecorder(10)
66+
r := &RuntimeReconciler{
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("RuntimeReconciler (EFC) Implement", func() {
76+
77+
Describe("getRuntime", func() {
78+
var r *RuntimeReconciler
79+
80+
BeforeEach(func() {
81+
testRuntime := &datav1alpha1.EFCRuntime{
82+
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
83+
}
84+
s := runtime.NewScheme()
85+
_ = datav1alpha1.AddToScheme(s)
86+
r = newTestEFCReconciler(s, testRuntime)
87+
})
88+
89+
It("should return the runtime when it exists in the cluster", func() {
90+
ctx := cruntime.ReconcileRequestContext{
91+
Context: context.Background(),
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.Background(),
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 *RuntimeReconciler
114+
115+
BeforeEach(func() {
116+
r = newTestEFCReconciler(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.Background(),
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.Background(),
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 *RuntimeReconciler
167+
168+
BeforeEach(func() {
169+
r = newTestEFCReconciler(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.Background(),
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.Background(),
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 EFCRuntime objects, so getRuntime will
198+
// return a NotFound error, which Reconcile should swallow gracefully.
199+
s := runtime.NewScheme()
200+
_ = datav1alpha1.AddToScheme(s)
201+
r := newTestEFCReconciler(s)
202+
203+
req := ctrl.Request{
204+
NamespacedName: types.NamespacedName{Name: "missing", Namespace: "default"},
205+
}
206+
result, err := r.Reconcile(context.Background(), req)
207+
Expect(err).NotTo(HaveOccurred())
208+
Expect(result).To(Equal(ctrl.Result{}))
209+
})
210+
})
211+
})
Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2023 The Fluid Authors.
2+
Copyright 2026 The Fluid Authors.
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.
@@ -17,63 +17,23 @@ limitations under the License.
1717
package efc
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"
30-
"sigs.k8s.io/controller-runtime/pkg/log/zap"
3125

32-
datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1"
33-
// +kubebuilder:scaffold:imports
26+
"github.com/fluid-cloudnative/fluid/pkg/utils/fake"
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, "EFC Controller Suite")
4835
}
4936

50-
var _ = BeforeSuite(func(done Done) {
51-
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
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())
37+
var _ = BeforeSuite(func() {
38+
logf.SetLogger(fake.NullLogger())
7939
})

0 commit comments

Comments
 (0)