Skip to content

Commit f79e25f

Browse files
authored
test(thinruntime): add Ginkgo/Gomega unit tests (#5694)
* 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> * test(juicefs): remove redundant ControllerName assertion Remove the duplicate assertion that tested against the unexported constant; use the string literal form only as suggested by code review. Signed-off-by: Harsh <harshmastic@gmail.com> * test(thinruntime): add Ginkgo/Gomega unit tests Signed-off-by: Harsh <harshmastic@gmail.com> --------- Signed-off-by: Harsh <harshmastic@gmail.com>
1 parent 94b87c6 commit f79e25f

3 files changed

Lines changed: 317 additions & 55 deletions

File tree

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 thinruntime
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+
// newTestThinRuntimeReconciler builds a ThinRuntimeReconciler seeded with the
57+
// given scheme and runtime objects. Pass nil scheme to get a default one.
58+
func newTestThinRuntimeReconciler(s *runtime.Scheme, objs ...runtime.Object) *ThinRuntimeReconciler {
59+
if s == nil {
60+
s = runtime.NewScheme()
61+
_ = datav1alpha1.AddToScheme(s)
62+
}
63+
fakeClient := fake.NewFakeClientWithScheme(s, objs...)
64+
log := ctrl.Log.WithName("thinruntime-test")
65+
recorder := record.NewFakeRecorder(10)
66+
r := &ThinRuntimeReconciler{
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("ThinRuntimeReconciler Implement", func() {
76+
77+
Describe("getRuntime", func() {
78+
var r *ThinRuntimeReconciler
79+
80+
BeforeEach(func() {
81+
testRuntime := &datav1alpha1.ThinRuntime{
82+
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
83+
}
84+
s := runtime.NewScheme()
85+
_ = datav1alpha1.AddToScheme(s)
86+
r = newTestThinRuntimeReconciler(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 *ThinRuntimeReconciler
114+
115+
BeforeEach(func() {
116+
r = newTestThinRuntimeReconciler(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 *ThinRuntimeReconciler
167+
168+
BeforeEach(func() {
169+
r = newTestThinRuntimeReconciler(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 ThinRuntime objects, so getRuntime will
198+
// return a NotFound error, which Reconcile should swallow gracefully.
199+
s := runtime.NewScheme()
200+
_ = datav1alpha1.AddToScheme(s)
201+
r := newTestThinRuntimeReconciler(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: 5 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,5 @@
11
/*
2-
Copyright 2022 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-
/*
2+
Copyright 2026 The Fluid Authors.
183
194
Licensed under the Apache License, Version 2.0 (the "License");
205
you may not use this file except in compliance with the License.
@@ -32,58 +17,23 @@ limitations under the License.
3217
package thinruntime
3318

3419
import (
35-
"path/filepath"
3620
"testing"
3721

3822
. "github.com/onsi/ginkgo/v2"
3923
. "github.com/onsi/gomega"
40-
"k8s.io/client-go/kubernetes/scheme"
41-
"k8s.io/client-go/rest"
42-
"sigs.k8s.io/controller-runtime/pkg/client"
43-
"sigs.k8s.io/controller-runtime/pkg/envtest"
4424
logf "sigs.k8s.io/controller-runtime/pkg/log"
45-
"sigs.k8s.io/controller-runtime/pkg/log/zap"
46-
//+kubebuilder:scaffold:imports
25+
26+
"github.com/fluid-cloudnative/fluid/pkg/utils/fake"
4727
)
4828

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

52-
var cfg *rest.Config
53-
var k8sClient client.Client
54-
var testEnv *envtest.Environment
55-
5632
func TestAPIs(t *testing.T) {
5733
RegisterFailHandler(Fail)
58-
59-
RunSpecs(t,
60-
"Controller Suite")
34+
RunSpecs(t, "ThinRuntime Controller Suite")
6135
}
6236

6337
var _ = BeforeSuite(func() {
64-
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
65-
66-
By("bootstrapping test environment")
67-
testEnv = &envtest.Environment{
68-
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
69-
ErrorIfCRDPathMissing: false,
70-
}
71-
72-
var err error
73-
cfg, err = testEnv.Start()
74-
Expect(err).NotTo(HaveOccurred())
75-
Expect(cfg).NotTo(BeNil())
76-
77-
//+kubebuilder:scaffold:scheme
78-
79-
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
80-
Expect(err).NotTo(HaveOccurred())
81-
Expect(k8sClient).NotTo(BeNil())
82-
83-
}, 60)
84-
85-
var _ = AfterSuite(func() {
86-
By("tearing down the test environment")
87-
err := testEnv.Stop()
88-
Expect(err).NotTo(HaveOccurred())
38+
logf.SetLogger(fake.NullLogger())
8939
})
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 thinruntime
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/utils/fake"
30+
)
31+
32+
var _ = Describe("ThinRuntimeReconciler", func() {
33+
34+
Describe("ControllerName", func() {
35+
It("should return the constant controller name", func() {
36+
r := &ThinRuntimeReconciler{}
37+
Expect(r.ControllerName()).To(Equal("ThinRuntimeController"))
38+
})
39+
})
40+
41+
Describe("ManagedResource", func() {
42+
It("should return a ThinRuntime with correct TypeMeta", func() {
43+
r := &ThinRuntimeReconciler{}
44+
obj := r.ManagedResource()
45+
thinRuntime, ok := obj.(*datav1alpha1.ThinRuntime)
46+
Expect(ok).To(BeTrue())
47+
Expect(thinRuntime.Kind).To(Equal(datav1alpha1.ThinRuntimeKind))
48+
Expect(thinRuntime.APIVersion).To(ContainSubstring(datav1alpha1.GroupVersion.Group))
49+
})
50+
})
51+
52+
Describe("NewRuntimeReconciler", func() {
53+
It("should initialize reconciler with all required fields set", func() {
54+
s := runtime.NewScheme()
55+
fakeClient := fake.NewFakeClientWithScheme(s)
56+
log := ctrl.Log.WithName("test")
57+
recorder := record.NewFakeRecorder(10)
58+
59+
r := NewRuntimeReconciler(fakeClient, log, s, recorder)
60+
Expect(r).NotTo(BeNil())
61+
Expect(r.Scheme).To(Equal(s))
62+
Expect(r.mutex).NotTo(BeNil())
63+
Expect(r.engines).NotTo(BeNil())
64+
Expect(r.RuntimeReconciler).NotTo(BeNil())
65+
})
66+
})
67+
68+
Describe("NewCache", func() {
69+
It("should return cache options with two ByObject entries", func() {
70+
opts := NewCache()
71+
Expect(opts.ByObject).To(HaveLen(2))
72+
})
73+
74+
It("should contain StatefulSet and DaemonSet keys in ByObject", func() {
75+
opts := NewCache()
76+
var seenStatefulSet, seenDaemonSet bool
77+
for key := range opts.ByObject {
78+
switch key.(type) {
79+
case *appsv1.StatefulSet:
80+
seenStatefulSet = true
81+
case *appsv1.DaemonSet:
82+
seenDaemonSet = true
83+
}
84+
}
85+
Expect(seenStatefulSet).To(BeTrue(), "expected StatefulSet key in ByObject")
86+
Expect(seenDaemonSet).To(BeTrue(), "expected DaemonSet key in ByObject")
87+
})
88+
89+
It("should scope the StatefulSet selector to the thin runtime label", func() {
90+
opts := NewCache()
91+
var statefulSetLabel string
92+
for key, byObj := range opts.ByObject {
93+
if _, ok := key.(*appsv1.StatefulSet); ok {
94+
Expect(byObj.Label).NotTo(BeNil())
95+
statefulSetLabel = byObj.Label.String()
96+
}
97+
}
98+
Expect(statefulSetLabel).To(ContainSubstring("thin"))
99+
})
100+
})
101+
})

0 commit comments

Comments
 (0)