//nolint:typecheck package v1 import ( "context" _ "embed" "encoding/json" "io/fs" "log/slog" "os" "path/filepath" "testing" "time" testfs "gotest.tools/v3/fs" . "github.com/onsi/ginkgo/v2" //nolint:revive . "github.com/onsi/gomega" //nolint:revive metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client/fake" "edge-infra.dev/pkg/lib/kernel/devices" "edge-infra.dev/pkg/sds/devices/logger" v1 "edge-infra.dev/pkg/sds/ien/k8s/apis/v1" ) var ( subSystemUSB = "usb" timeout = 5 * time.Second testPath = etcPath etcPath = "/etc" soundName = "sound" ) //go:embed testdata/deviceclass_valid.json var deviceClassValidBytes []byte func TestDeviceRead(t *testing.T) { dir := testfs.NewDir(t, etcPath) defer dir.Remove() testPath = dir.Path() RegisterFailHandler(Fail) RunSpecs(t, "Device Class Validation Tests") } var _ = BeforeEach(func() { _ = os.Remove(filepath.Join(testPath, "deviceclasses.json")) }) var _ = Describe("Validation Tests", func() { It("Valid name and includes subsystem", func() { validName := "display" cr := generateDeviceClassTestCR(validName, 1) Expect(cr.Validate()).To(BeNil()) }) It("Valid name but device set is missing subsystem", func() { validName := "display" classCR := generateDeviceClassTestCR(validName, 1) setCR := generateDeviceSetsTestCR(validName, false, map[string]string{}, map[string]devices.Device{}) Expect(classCR.Validate()).To(BeNil()) Expect(setCR.Validate()).To(MatchError(ErrInvalidMissingSubsystem)) }) It("Invalid name", func() { invalidName := "displaydevices.edge.ncr.com/display" cr := generateDeviceClassTestCR(invalidName, 1) Expect(cr.Validate()).To(MatchError(ErrInvalidName)) }) }) var _ = Describe("Should Block Tests", func() { It("Should block", func() { cr := generateDeviceSetsTestCR(soundName, true, map[string]string{"SUBSYSTEM": subSystemUSB}, map[string]devices.Device{}) shouldBlock, _ := cr.WillBlock() Expect(shouldBlock).To(BeTrue()) }) It("Should not block", func() { cr := generateDeviceSetsTestCR(soundName, true, map[string]string{"SUBSYSTEM": subSystemUSB}, map[string]devices.Device{"dev1": &dummyDevice{subsystem: subSystemUSB}}) shouldBlock, _ := cr.WillBlock() Expect(shouldBlock).To(BeFalse()) }) }) var _ = Describe("Caching Tests", func() { It("Should not cache", func() { classCR := generateDeviceClassTestCR(soundName, 1) setCR := generateDeviceSetsTestCR(soundName, true, map[string]string{"SUBSYSTEM": subSystemUSB}, nil) c := fake.NewClientBuilder().WithScheme(createScheme()).WithObjects(&classCR, &setCR).Build() ctx := context.Background() log := logger.New(logger.WithLevel(slog.LevelDebug)) ctx = logger.IntoContext(ctx, log) ctx, cancelFn := context.WithTimeout(ctx, timeout) defer cancelFn() opts := []ListOption{ WithPersistence(false), WithPersistencePath(testPath), } deviceClassMap, err := ListFromClient(ctx, c, opts...) Expect(err).To(BeNil()) Expect(deviceClassMap).To(HaveLen(1)) filePath := filepath.Join(testPath, "deviceclasses.json") _, err = os.Stat(filePath) Expect(err).To(MatchError(fs.ErrNotExist)) }) It("Should cache", func() { classCR := generateDeviceClassTestCR(soundName, 1) setCR := generateDeviceSetsTestCR(soundName, true, map[string]string{"SUBSYSTEM": subSystemUSB}, nil) c := fake.NewClientBuilder().WithScheme(createScheme()).WithObjects(&classCR, &setCR).Build() ctx := context.Background() log := logger.New(logger.WithLevel(slog.LevelDebug)) ctx = logger.IntoContext(ctx, log) ctx, cancelFn := context.WithTimeout(ctx, timeout) defer cancelFn() opts := []ListOption{ WithPersistence(true), WithPersistencePath(testPath), } deviceClassMap, err := ListFromClient(ctx, c, opts...) Expect(err).To(BeNil()) Expect(deviceClassMap).To(HaveLen(1)) filePath := filepath.Join(testPath, deviceClassCacheName) cachedClasses, err := os.ReadFile(filePath) Expect(err).To(BeNil()) deviceClasses := DeviceClassList{} Expect(json.Unmarshal(cachedClasses, &deviceClasses)).To(BeNil()) Expect(deviceClasses.Items).To(HaveLen(1)) Expect(deviceClasses.Items[0].ObjectMeta.Name).To(Equal(classCR.ObjectMeta.Name)) Expect(deviceClasses.Items[0].ObjectMeta.Generation).To(Equal(classCR.ObjectMeta.Generation)) filePath = filepath.Join(testPath, deviceCacheName) cachedSets, err := os.ReadFile(filePath) Expect(err).To(BeNil()) deviceSets := DeviceSetList{} Expect(json.Unmarshal(cachedSets, &deviceSets)).To(BeNil()) Expect(deviceSets.Items).To(HaveLen(1)) Expect(deviceSets.Items[0].ObjectMeta.Name).To(Equal(setCR.ObjectMeta.Name)) Expect(deviceSets.Items[0].ObjectMeta.Generation).To(Equal(setCR.ObjectMeta.Generation)) }) It("Generation has changed", func() { setCR := generateDeviceSetsTestCR(soundName, true, map[string]string{"SUBSYSTEM": subSystemUSB}, nil) oldCR := generateDeviceClassTestCR("sound", 1) newCR := generateDeviceClassTestCR("sound", 2) ctx := context.Background() log := logger.New(logger.WithLevel(slog.LevelDebug)) ctx = logger.IntoContext(ctx, log) ctx, cancelFn := context.WithTimeout(ctx, timeout) defer cancelFn() Expect(writeDeviceClassCache(ctx, testPath, []DeviceClass{oldCR})).To(BeNil()) c := fake.NewClientBuilder().WithScheme(createScheme()).WithObjects(&newCR, &setCR).Build() opts := []ListOption{ WithPersistence(true), WithPersistencePath(testPath), } deviceClassMap, err := ListFromClient(ctx, c, opts...) Expect(err).To(BeNil()) Expect(deviceClassMap).To(HaveLen(1)) Expect(deviceClassMap[newCR.ClassName()]).ToNot(BeNil()) Expect(deviceClassMap[newCR.ClassName()].ObjectMeta.Generation).To(Equal(newCR.ObjectMeta.Generation)) filePath := filepath.Join(testPath, deviceClassCacheName) cachedClasses, err := os.ReadFile(filePath) Expect(err).To(BeNil()) deviceClasses := DeviceClassList{} Expect(json.Unmarshal(cachedClasses, &deviceClasses)).To(BeNil()) Expect(deviceClasses.Items).To(HaveLen(1)) Expect(deviceClasses.Items[0].ObjectMeta.Name).To(Equal(newCR.ObjectMeta.Name)) }) }) var _ = Describe("Device System CR Type Conversion", func() { It("Should convert successfully to dsv1.DeviceClass", func() { deviceClass := &DeviceClass{} Expect(json.Unmarshal(deviceClassValidBytes, deviceClass)).To(BeNil()) }) }) func generateDeviceClassTestCR(name string, generation int64) DeviceClass { return DeviceClass{ ObjectMeta: metav1.ObjectMeta{ Name: name, Generation: generation, }, Spec: DeviceClassSpec{ Devices: []DeviceRef{ { Name: name, }, }, Logging: Logging{ Level: "info", }, }, } } func generateDeviceSetsTestCR(name string, shouldBlock bool, deviceSetProperties map[string]string, devices map[string]devices.Device) DeviceSet { deviceSet := DeviceSetReference{ Name: name, Properties: []Rule{}, } if shouldBlock { deviceSet.Blocking = &Blocking{} } for key, value := range deviceSetProperties { deviceSet.Properties = append(deviceSet.Properties, Rule{ Name: key, RegexValue: value, }) } return DeviceSet{ ObjectMeta: metav1.ObjectMeta{ Name: name, Generation: 1, }, Spec: DeviceSpec{ DeviceSets: []DeviceSetReference{ deviceSet, }, }, DeviceStatus: deviceStatus{ devices: devices, }, } } func createScheme() *runtime.Scheme { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(v1.AddToScheme(scheme)) utilruntime.Must(AddToScheme(scheme)) return scheme } // mock implmentations of device and device node type dummyDevice struct{ subsystem string } type dummyNode struct{} var devType = "char" // dummy device implementation func (dd *dummyDevice) Path() string { return "" } func (dd *dummyDevice) Node() (devices.Node, error) { return &dummyNode{}, nil } func (dd *dummyDevice) Attribute(_ string) (string, bool, error) { return "", true, nil } func (dd *dummyDevice) Property(_ string) (string, bool, error) { return dd.subsystem, true, nil } // dummy node implementation func (dn *dummyNode) Path() string { return "" } func (dn *dummyNode) Type() (string, error) { return devType, nil } func (dn *dummyNode) GroupID() (int64, error) { return 1, nil } func (dn *dummyNode) UserID() (int64, error) { return 1, nil } func (dn *dummyNode) FileMode() (os.FileMode, error) { return 0, nil }