package controller
import (
"context"
"fmt"
"github.com/agiledragon/gomonkey/v2"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
"io/ioutil"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
v1 "openfuyao.com/npu-operator/api/v1"
"os"
"path/filepath"
"reflect"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"testing"
)
type mockResource struct {
kind string
}
func (m *mockResource) clone() componentResource {
return &mockResource{kind: m.kind}
}
func (m *mockResource) gvk() string {
return m.kind
}
func (m *mockResource) reconcile(ctx context.Context, r *NPUClusterPolicyReconciler, isManaged bool) (*v1.ComponentState, error) {
return &v1.ComponentState{Phase: v1.ComponentRunning}, nil
}
func TestReconcilerLoadComponent(t *testing.T) {
r := &NPUClusterPolicyReconciler{instance: &v1.NPUClusterPolicy{
Spec: v1.NPUClusterPolicySpec{
Driver: v1.DriverSpec{Managed: true},
},
Status: v1.NPUClusterPolicyStatus{
ComponentStatuses: []v1.ComponentStatus{{Name: "/var/lib/npu-operator/components/driver", State: v1.ComponentState{Phase: v1.ComponentPending}}},
},
},
}
test := []struct {
name string
resource componentResource
phase *v1.ComponentState
stateErr error
updateErr error
}{
{
name: "success",
resource: &mockResource{
kind: "NameSpace"},
phase: &v1.ComponentState{Phase: v1.ComponentRunning},
},
{
name: "pending",
resource: &mockResource{
kind: "Pod",
},
phase: nil,
stateErr: fmt.Errorf("component not ready"),
},
{
name: "deploying",
resource: &mockResource{
kind: "Pod",
},
phase: &v1.ComponentState{Phase: v1.ComponentDeploying},
updateErr: fmt.Errorf("uodate state err"),
},
}
for _, tt := range test {
t.Run(tt.name, func(t *testing.T) {
comp := component{
name: "/var/lib/npu-operator/components/driver",
resources: []componentResource{tt.resource},
}
g := gomonkey.ApplyPrivateMethod(r, "updateStatus", func(_ *NPUClusterPolicyReconciler, _ context.Context, _ updateStatusCallback) error {
return tt.updateErr
})
h := gomonkey.ApplyPrivateMethod(tt.resource, "reconcile", func(_ *mockResource, _ context.Context, _ *NPUClusterPolicyReconciler, _ bool) (*v1.ComponentState, error) {
return tt.phase, tt.stateErr
})
defer g.Reset()
defer h.Reset()
state, _ := comp.reconcile(context.TODO(), r)
if tt.phase == nil {
assert.Equal(t, v1.ComponentPending, state.Phase)
} else {
assert.Equal(t, tt.phase.Phase, state.Phase)
}
})
}
}
func mockConponent() []component {
comp := []component{
{
name: driverComponent,
},
{
name: ociRuntimeComponent,
},
{
name: devicePluginComponent,
},
{
name: trainerComponent,
},
{
name: nodeDComponent,
},
{
name: vccontrollerComponent,
},
{
name: vcschedulerComponent,
},
{
name: clusterdComponent,
},
{
name: rsControllerComponent,
},
{
name: exporterComponent,
},
{
name: mindIOTFTComponent,
},
{
name: mindIOACPComponent,
},
}
return comp
}
func mockReconciler() *NPUClusterPolicyReconciler {
return &NPUClusterPolicyReconciler{instance: &v1.NPUClusterPolicy{
Spec: v1.NPUClusterPolicySpec{
Driver: v1.DriverSpec{Managed: true},
OCIRuntime: v1.OCIRuntimeSpec{Managed: true},
VCScheduler: v1.SchedulerSpec{Managed: true},
VCController: v1.ControllerSpec{Managed: true},
DevicePlugin: v1.DevicePluginSpec{Managed: true},
Trainer: v1.TrainerSpec{Managed: false},
NodeD: v1.NodeDSpec{Managed: true},
ClusterD: v1.ClusterDSpec{Managed: true},
RSController: v1.RSControllerSpec{Managed: true},
Exporter: v1.ExporterSpec{Managed: true},
MindIOTFT: v1.MindIOTFTSpec{Managed: true},
MindIOACP: v1.MindIOACPSpec{Managed: true},
},
},
components: mockConponent(),
namespace: "default",
client: &kubernetes.Clientset{},
Scheme: runtime.NewScheme(),
}
}
func TestGetComponentManagedState(t *testing.T) {
comp := mockConponent()
r := mockReconciler()
for _, c := range comp {
isManaged := isComponentManaged[c.name](r)
if c.name == trainerComponent {
assert.Equal(t, isManaged, false)
} else {
assert.Equal(t, isManaged, true)
}
}
}
func mockScheme() *runtime.Scheme {
scheme := runtime.NewScheme()
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(v1.AddToScheme(scheme))
return scheme
}
func TestComponentLoadResource(t *testing.T) {
scheme := mockScheme()
manifest := []byte(`
apiVersion: v1
kind: Namespace
metadata:
name: test-namespace
labels:
env: dev
`)
serializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{
Yaml: true,
Pretty: false,
Strict: false,
})
comp := component{
name: "test-component",
resources: []componentResource{},
}
err := comp.loadResource(context.TODO(), manifest, serializer)
assert.NoError(t, err, "loadResource should not return error")
var expected corev1.Namespace
err = yaml.Unmarshal(manifest, &expected)
assert.NoError(t, err, "yaml.Unmarshal should not fail")
for _, res := range comp.resources {
spew.Dump(res)
typed, ok := res.(typedComponentResource[corev1.Namespace, *corev1.Namespace])
if !ok {
t.Errorf("expected typedComponentResource[Namespace], got %T", res)
continue
}
actual := typed.obj
assert.Equal(t, expected.Name, actual.Name, "name should match")
assert.Equal(t, expected.Labels, actual.Labels, "labels should match")
assert.Equal(t, expected.Spec, actual.Spec, "spec should match")
}
}
func TestReadManifests(t *testing.T) {
tempDir, err := ioutil.TempDir("", "test-manifests")
if err != nil {
t.Fatalf("Failed to create temporary directory: %v", err)
}
defer os.RemoveAll(tempDir)
files := []struct {
name string
content []byte
}{
{"file1.txt", []byte("content1")},
{"file2.txt", []byte("content2")},
{"subdir/file3.txt", []byte("content3")},
}
for _, file := range files {
filePath := filepath.Join(tempDir, file.name)
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
t.Fatalf("Failed to create directory: %v", err)
}
if err := ioutil.WriteFile(filePath, file.content, os.ModePerm); err != nil {
t.Fatalf("Failed to write file: %v", err)
}
}
ctx := context.TODO()
manifests := readManifests(ctx, tempDir)
expected := [][]byte{
[]byte("content1"),
[]byte("content2"),
[]byte("content3"),
}
if len(manifests) != len(expected) {
t.Errorf("Expected %d manifests, got %d", len(expected), len(manifests))
}
for i, manifest := range manifests {
if !equal(manifest, expected[i]) {
t.Errorf("Manifest content mismatch for file %d: expected %q, got %q", i, expected[i], manifest)
}
}
}
func equal(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func TestNPUClusterPolicyReconcilerLoadComponents(t *testing.T) {
r := mockReconciler()
r.components = nil
scheme := mockScheme()
fakeCli := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(&v1.NPUClusterPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-policy",
},
}).Build()
r.Client = fakeCli
statusWriter := fakeCli.Status()
realType := reflect.TypeOf(statusWriter)
method, ok := realType.MethodByName("Update")
if !ok {
panic("cannot find method 'Update' on statusWriter")
}
patchFunc := reflect.MakeFunc(method.Type, func(args []reflect.Value) []reflect.Value {
return []reflect.Value{reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())}
})
patch := gomonkey.ApplyMethod(realType, "Update", patchFunc.Interface())
g := gomonkey.ApplyPrivateMethod(r, "getInstance", func(_ *NPUClusterPolicyReconciler, _ context.Context, _ string) error {
return nil
})
defer g.Reset()
defer patch.Reset()
assert.NoError(t, r.loadComponents(context.TODO()))
}
type mockDriver struct {
name string
}
func (m *mockDriver) loadResource(ctx context.Context, manifest []byte, serializer *json.Serializer) error {
return nil
}
func (m *mockDriver) reconcile(ctx context.Context, r *NPUClusterPolicyReconciler) (v1.ComponentState, error) {
return v1.ComponentState{Phase: v1.ComponentDeploying}, nil
}
func TestReconcileComponents(t *testing.T) {
r := mockReconciler()
comp := &component{
name: "driver",
resources: nil,
}
r.components = []component{*comp}
patch := gomonkey.ApplyPrivateMethod(
comp,
"reconcile",
func(_ *component, _ context.Context, _ *NPUClusterPolicyReconciler) (v1.ComponentState, error) {
return v1.ComponentState{Phase: v1.ComponentRunning}, nil
},
)
defer patch.Reset()
patch2 := gomonkey.ApplyPrivateMethod(
r,
"updateStatus",
func(_ *NPUClusterPolicyReconciler, _ context.Context, _ updateStatusCallback) error {
return nil
},
)
defer patch2.Reset()
_, err := r.reconcileComponents(context.TODO())
assert.NoError(t, err)
}