package controller
import (
"context"
"errors"
"testing"
"time"
"github.com/agiledragon/gomonkey/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
v1 "openfuyao.com/npu-operator/api/v1"
)
type listErrClient struct {
client.Client
err error
}
func (c listErrClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
return c.err
}
func TestNPUClusterPolicyReconcilerGetReferrerToNode(t *testing.T) {
instance1 := getInstance()
instance1.Name = "cluster-a"
instance2 := getInstance()
instance2.Name = "cluster-b"
scheme := mockScheme()
cl := fakeClientBuilder(scheme, instance1, instance2)
r := &NPUClusterPolicyReconciler{Client: cl}
reqs := r.getReferrerToNode(context.Background(), &corev1.Node{})
require.Len(t, reqs, 2)
assert.ElementsMatch(t, []string{"cluster-a", "cluster-b"}, []string{
reqs[0].NamespacedName.Name, reqs[1].NamespacedName.Name,
})
}
func TestNPUClusterPolicyReconcilerGetReferrerToNode_ListError(t *testing.T) {
r := &NPUClusterPolicyReconciler{
Client: listErrClient{
Client: fakeClient(),
err: errors.New("list failed"),
},
}
reqs := r.getReferrerToNode(context.Background(), &corev1.Node{})
assert.Empty(t, reqs)
}
func TestNodeEventFilter(t *testing.T) {
createEvent := event.CreateEvent{
Object: &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
npuDeviceLabelPrefix + "d100.present": npuDeviceLabelValue,
},
},
},
}
assert.True(t, nodeEventFilter.Create(createEvent))
updateEvent := event.UpdateEvent{
ObjectNew: &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
npuPresentLabelKey: "true",
},
},
},
}
assert.True(t, nodeEventFilter.Update(updateEvent))
deleteEvent := event.DeleteEvent{
Object: &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
npuDeviceLabelPrefix + "d500.present": npuDeviceLabelValue,
},
},
},
}
assert.True(t, nodeEventFilter.Delete(deleteEvent))
}
func TestNPUClusterPolicyReconcilerReconcile(t *testing.T) {
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "cluster"}}
ctx := context.Background()
t.Run("ignore not found", func(t *testing.T) {
r := &NPUClusterPolicyReconciler{}
patch := gomonkey.ApplyPrivateMethod(r, "getInstance", func(*NPUClusterPolicyReconciler, context.Context, string) error {
return apierrors.NewNotFound(schema.GroupResource{Group: "npu.openfuyao.com", Resource: "npuclusterpolicies"}, "cluster")
})
defer patch.Reset()
result, err := r.Reconcile(ctx, req)
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, result)
})
t.Run("load components failed", func(t *testing.T) {
r := &NPUClusterPolicyReconciler{}
p1 := gomonkey.ApplyPrivateMethod(r, "getInstance", func(*NPUClusterPolicyReconciler, context.Context, string) error {
return nil
})
p2 := gomonkey.ApplyPrivateMethod(r, "loadComponents", func(*NPUClusterPolicyReconciler, context.Context) error {
return errors.New("load failed")
})
p3 := gomonkey.ApplyPrivateMethod(r, "setConditionsError", func(*NPUClusterPolicyReconciler, context.Context, string, string) error {
return nil
})
defer p1.Reset()
defer p2.Reset()
defer p3.Reset()
_, err := r.Reconcile(ctx, req)
require.Error(t, err)
assert.ErrorContains(t, err, "load components")
})
t.Run("label npu nodes failed", func(t *testing.T) {
r := &NPUClusterPolicyReconciler{}
p1 := gomonkey.ApplyPrivateMethod(r, "getInstance", func(*NPUClusterPolicyReconciler, context.Context, string) error {
return nil
})
p2 := gomonkey.ApplyPrivateMethod(r, "loadComponents", func(*NPUClusterPolicyReconciler, context.Context) error {
return nil
})
p3 := gomonkey.ApplyPrivateMethod(r, "labelNPUNodes", func(*NPUClusterPolicyReconciler, context.Context) error {
return errors.New("label failed")
})
p4 := gomonkey.ApplyPrivateMethod(r, "setConditionsError", func(*NPUClusterPolicyReconciler, context.Context, string, string) error {
return nil
})
defer p1.Reset()
defer p2.Reset()
defer p3.Reset()
defer p4.Reset()
_, err := r.Reconcile(ctx, req)
require.Error(t, err)
assert.ErrorContains(t, err, "label NPU nodes")
})
t.Run("success and requeue result", func(t *testing.T) {
r := &NPUClusterPolicyReconciler{}
expected := ctrl.Result{RequeueAfter: 2 * time.Second}
p1 := gomonkey.ApplyPrivateMethod(r, "getInstance", func(*NPUClusterPolicyReconciler, context.Context, string) error {
return nil
})
p2 := gomonkey.ApplyPrivateMethod(r, "loadComponents", func(*NPUClusterPolicyReconciler, context.Context) error {
return nil
})
p3 := gomonkey.ApplyPrivateMethod(r, "labelNPUNodes", func(rr *NPUClusterPolicyReconciler, _ context.Context) error {
rr.hasNFDLabels = false
rr.hasNPUNodes = false
return nil
})
p4 := gomonkey.ApplyPrivateMethod(r, "reconcileComponents", func(*NPUClusterPolicyReconciler, context.Context) (ctrl.Result, error) {
return expected, nil
})
defer p1.Reset()
defer p2.Reset()
defer p3.Reset()
defer p4.Reset()
result, err := r.Reconcile(ctx, req)
require.NoError(t, err)
assert.Equal(t, expected, result)
})
}
func TestSetConditionsFunctions(t *testing.T) {
r := &NPUClusterPolicyReconciler{}
t.Run("setConditionsReady and callback updates status", func(t *testing.T) {
var callback updateStatusCallback
patch := gomonkey.ApplyPrivateMethod(r, "updateStatus", func(*NPUClusterPolicyReconciler, context.Context, updateStatusCallback) error {
callback = func(s *v1.NPUClusterPolicyStatus) bool {
return s.Phase != v1.PolicyReady
}
return nil
})
defer patch.Reset()
require.NoError(t, r.setConditionsReady(context.Background(), reconciledReason, "ok"))
require.NotNil(t, callback)
})
t.Run("setConditionsError returns updateStatus error", func(t *testing.T) {
patch := gomonkey.ApplyPrivateMethod(r, "updateStatus", func(*NPUClusterPolicyReconciler, context.Context, updateStatusCallback) error {
return errors.New("status update failed")
})
defer patch.Reset()
err := r.setConditionsError(context.Background(), reconcileFailedReason, "failed")
require.Error(t, err)
assert.ErrorContains(t, err, "status update failed")
})
}
func fakeClientBuilder(s *runtime.Scheme, objs ...client.Object) client.Client {
return fake.NewClientBuilder().WithScheme(s).WithObjects(objs...).Build()
}