e6bbc769创建于 2025年12月22日历史提交
// Copyright (c) 2024 Huawei Technologies Co., Ltd.
// openFuyao is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//          http://license.coscl.org.cn/MulanPSL2
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.

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)
}