package specconv import ( "os" "strings" "testing" dbus "github.com/godbus/dbus/v5" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/configs/validate" "github.com/opencontainers/runc/libcontainer/devices" "github.com/opencontainers/runtime-spec/specs-go" "golang.org/x/sys/unix" ) func TestCreateCommandHookTimeout(t *testing.T) { timeout := 3600 hook := specs.Hook{ Path: "/some/hook/path", Args: []string{"--some", "thing"}, Env: []string{"SOME=value"}, Timeout: &timeout, } command := createCommandHook(hook) timeoutStr := command.Timeout.String() if timeoutStr != "1h0m0s" { t.Errorf("Expected the Timeout to be 1h0m0s, got: %s", timeoutStr) } } func TestCreateHooks(t *testing.T) { rspec := &specs.Spec{ Hooks: &specs.Hooks{ Prestart: []specs.Hook{ { Path: "/some/hook/path", }, { Path: "/some/hook2/path", Args: []string{"--some", "thing"}, }, }, CreateRuntime: []specs.Hook{ { Path: "/some/hook/path", }, { Path: "/some/hook2/path", Args: []string{"--some", "thing"}, }, }, CreateContainer: []specs.Hook{ { Path: "/some/hook/path", }, { Path: "/some/hook2/path", Args: []string{"--some", "thing"}, }, }, StartContainer: []specs.Hook{ { Path: "/some/hook/path", }, { Path: "/some/hook2/path", Args: []string{"--some", "thing"}, }, }, Poststart: []specs.Hook{ { Path: "/some/hook/path", Args: []string{"--some", "thing"}, Env: []string{"SOME=value"}, }, { Path: "/some/hook2/path", }, { Path: "/some/hook3/path", }, }, Poststop: []specs.Hook{ { Path: "/some/hook/path", Args: []string{"--some", "thing"}, Env: []string{"SOME=value"}, }, { Path: "/some/hook2/path", }, { Path: "/some/hook3/path", }, { Path: "/some/hook4/path", Args: []string{"--some", "thing"}, }, }, }, } conf := &configs.Config{} createHooks(rspec, conf) prestart := conf.Hooks[configs.Prestart] if len(prestart) != 2 { t.Error("Expected 2 Prestart hooks") } createRuntime := conf.Hooks[configs.CreateRuntime] if len(createRuntime) != 2 { t.Error("Expected 2 createRuntime hooks") } createContainer := conf.Hooks[configs.CreateContainer] if len(createContainer) != 2 { t.Error("Expected 2 createContainer hooks") } startContainer := conf.Hooks[configs.StartContainer] if len(startContainer) != 2 { t.Error("Expected 2 startContainer hooks") } poststart := conf.Hooks[configs.Poststart] if len(poststart) != 3 { t.Error("Expected 3 Poststart hooks") } poststop := conf.Hooks[configs.Poststop] if len(poststop) != 4 { t.Error("Expected 4 Poststop hooks") } } func TestSetupSeccompNil(t *testing.T) { seccomp, err := SetupSeccomp(nil) if err != nil { t.Error("Expected error to be nil") } if seccomp != nil { t.Error("Expected seccomp to be nil") } } func TestSetupSeccompEmpty(t *testing.T) { conf := &specs.LinuxSeccomp{} seccomp, err := SetupSeccomp(conf) if err != nil { t.Error("Expected error to be nil") } if seccomp != nil { t.Error("Expected seccomp to be nil") } } // TestSetupSeccompWrongAction tests that a wrong action triggers an error func TestSetupSeccompWrongAction(t *testing.T) { conf := &specs.LinuxSeccomp{ DefaultAction: "SCMP_ACT_NON_EXIXTENT_ACTION", } _, err := SetupSeccomp(conf) if err == nil { t.Error("Expected error") } } // TestSetupSeccompWrongArchitecture tests that a wrong architecture triggers an error func TestSetupSeccompWrongArchitecture(t *testing.T) { conf := &specs.LinuxSeccomp{ DefaultAction: "SCMP_ACT_ALLOW", Architectures: []specs.Arch{"SCMP_ARCH_NON_EXISTENT_ARCH"}, } _, err := SetupSeccomp(conf) if err == nil { t.Error("Expected error") } } func TestSetupSeccomp(t *testing.T) { errnoRet := uint(55) conf := &specs.LinuxSeccomp{ DefaultAction: "SCMP_ACT_ERRNO", Architectures: []specs.Arch{specs.ArchX86_64, specs.ArchARM}, ListenerPath: "/var/run/mysocket", ListenerMetadata: "mymetadatastring", Syscalls: []specs.LinuxSyscall{ { Names: []string{"clone"}, Action: "SCMP_ACT_ALLOW", Args: []specs.LinuxSeccompArg{ { Index: 0, Value: unix.CLONE_NEWNS | unix.CLONE_NEWUTS | unix.CLONE_NEWIPC | unix.CLONE_NEWUSER | unix.CLONE_NEWPID | unix.CLONE_NEWNET | unix.CLONE_NEWCGROUP, ValueTwo: 0, Op: "SCMP_CMP_MASKED_EQ", }, }, }, { Names: []string{"semctl"}, Action: "SCMP_ACT_KILL", }, { Names: []string{"semget"}, Action: "SCMP_ACT_ERRNO", }, { Names: []string{"send"}, Action: "SCMP_ACT_ERRNO", ErrnoRet: &errnoRet, }, { Names: []string{"lchown"}, Action: "SCMP_ACT_TRAP", }, { Names: []string{"lremovexattr"}, Action: "SCMP_ACT_TRACE", }, { Names: []string{"mbind"}, Action: "SCMP_ACT_LOG", }, { Names: []string{"mknod"}, Action: "SCMP_ACT_NOTIFY", }, { Names: []string{"rmdir"}, Action: "SCMP_ACT_KILL_THREAD", }, { Names: []string{"mkdir"}, Action: "SCMP_ACT_KILL_PROCESS", }, }, } seccomp, err := SetupSeccomp(conf) if err != nil { t.Errorf("Couldn't create Seccomp config: %v", err) } if seccomp.DefaultAction != configs.Errno { t.Error("Wrong conversion for DefaultAction") } if len(seccomp.Architectures) != 2 { t.Error("Wrong number of architectures") } if seccomp.Architectures[0] != "amd64" || seccomp.Architectures[1] != "arm" { t.Error("Expected architectures are not found") } if seccomp.ListenerPath != "/var/run/mysocket" { t.Error("Expected ListenerPath is wrong") } if seccomp.ListenerMetadata != "mymetadatastring" { t.Error("Expected ListenerMetadata is wrong") } calls := seccomp.Syscalls if len(calls) != len(conf.Syscalls) { t.Error("Mismatched number of syscalls") } for _, call := range calls { switch call.Name { case "clone": if call.Action != configs.Allow { t.Error("Wrong conversion for the clone syscall action") } expectedCloneSyscallArgs := configs.Arg{ Index: 0, Op: configs.MaskEqualTo, Value: unix.CLONE_NEWNS | unix.CLONE_NEWUTS | unix.CLONE_NEWIPC | unix.CLONE_NEWUSER | unix.CLONE_NEWPID | unix.CLONE_NEWNET | unix.CLONE_NEWCGROUP, ValueTwo: 0, } if expectedCloneSyscallArgs != *call.Args[0] { t.Errorf("Wrong arguments conversion for the clone syscall under test") } case "semctl": if call.Action != configs.Kill { t.Errorf("Wrong conversion for the %s syscall action", call.Name) } case "semget": if call.Action != configs.Errno { t.Errorf("Wrong conversion for the %s syscall action", call.Name) } if call.ErrnoRet != nil { t.Errorf("Wrong error ret for the %s syscall", call.Name) } case "send": if call.Action != configs.Errno { t.Errorf("Wrong conversion for the %s syscall action", call.Name) } if *call.ErrnoRet != errnoRet { t.Errorf("Wrong error ret for the %s syscall", call.Name) } case "lchown": if call.Action != configs.Trap { t.Errorf("Wrong conversion for the %s syscall action", call.Name) } case "lremovexattr": if call.Action != configs.Trace { t.Errorf("Wrong conversion for the %s syscall action", call.Name) } case "mbind": if call.Action != configs.Log { t.Errorf("Wrong conversion for the %s syscall action", call.Name) } case "mknod": if call.Action != configs.Notify { t.Errorf("Wrong conversion for the %s syscall action", call.Name) } case "rmdir": if call.Action != configs.KillThread { t.Errorf("Wrong conversion for the %s syscall action", call.Name) } case "mkdir": if call.Action != configs.KillProcess { t.Errorf("Wrong conversion for the %s syscall action", call.Name) } default: t.Errorf("Unexpected syscall %s found", call.Name) } } } func TestLinuxCgroupWithMemoryResource(t *testing.T) { cgroupsPath := "/user/cgroups/path/id" spec := &specs.Spec{} devices := []specs.LinuxDeviceCgroup{ { Allow: false, Access: "rwm", }, } limit := int64(100) reservation := int64(50) swap := int64(20) kernel := int64(40) kernelTCP := int64(45) swappiness := uint64(1) swappinessPtr := &swappiness disableOOMKiller := true resources := &specs.LinuxResources{ Devices: devices, Memory: &specs.LinuxMemory{ Limit: &limit, Reservation: &reservation, Swap: &swap, Kernel: &kernel, KernelTCP: &kernelTCP, Swappiness: swappinessPtr, DisableOOMKiller: &disableOOMKiller, }, } spec.Linux = &specs.Linux{ CgroupsPath: cgroupsPath, Resources: resources, } opts := &CreateOpts{ CgroupName: "ContainerID", UseSystemdCgroup: false, Spec: spec, } cgroup, err := CreateCgroupConfig(opts, nil) if err != nil { t.Errorf("Couldn't create Cgroup config: %v", err) } if cgroup.Path != cgroupsPath { t.Errorf("Wrong cgroupsPath, expected '%s' got '%s'", cgroupsPath, cgroup.Path) } if cgroup.Resources.Memory != limit { t.Errorf("Expected to have %d as memory limit, got %d", limit, cgroup.Resources.Memory) } if cgroup.Resources.MemoryReservation != reservation { t.Errorf("Expected to have %d as memory reservation, got %d", reservation, cgroup.Resources.MemoryReservation) } if cgroup.Resources.MemorySwap != swap { t.Errorf("Expected to have %d as swap, got %d", swap, cgroup.Resources.MemorySwap) } if cgroup.Resources.MemorySwappiness != swappinessPtr { t.Errorf("Expected to have %d as memory swappiness, got %d", swappinessPtr, cgroup.Resources.MemorySwappiness) } if cgroup.Resources.OomKillDisable != disableOOMKiller { t.Errorf("The OOMKiller should be enabled") } } func TestLinuxCgroupSystemd(t *testing.T) { cgroupsPath := "parent:scopeprefix:name" spec := &specs.Spec{} spec.Linux = &specs.Linux{ CgroupsPath: cgroupsPath, } opts := &CreateOpts{ UseSystemdCgroup: true, Spec: spec, } cgroup, err := CreateCgroupConfig(opts, nil) if err != nil { t.Errorf("Couldn't create Cgroup config: %v", err) } expectedParent := "parent" if cgroup.Parent != expectedParent { t.Errorf("Expected to have %s as Parent instead of %s", expectedParent, cgroup.Parent) } expectedScopePrefix := "scopeprefix" if cgroup.ScopePrefix != expectedScopePrefix { t.Errorf("Expected to have %s as ScopePrefix instead of %s", expectedScopePrefix, cgroup.ScopePrefix) } expectedName := "name" if cgroup.Name != expectedName { t.Errorf("Expected to have %s as Name instead of %s", expectedName, cgroup.Name) } } func TestLinuxCgroupSystemdWithEmptyPath(t *testing.T) { cgroupsPath := "" spec := &specs.Spec{} spec.Linux = &specs.Linux{ CgroupsPath: cgroupsPath, } opts := &CreateOpts{ CgroupName: "ContainerID", UseSystemdCgroup: true, Spec: spec, } cgroup, err := CreateCgroupConfig(opts, nil) if err != nil { t.Errorf("Couldn't create Cgroup config: %v", err) } expectedParent := "" if cgroup.Parent != expectedParent { t.Errorf("Expected to have %s as Parent instead of %s", expectedParent, cgroup.Parent) } expectedScopePrefix := "runc" if cgroup.ScopePrefix != expectedScopePrefix { t.Errorf("Expected to have %s as ScopePrefix instead of %s", expectedScopePrefix, cgroup.ScopePrefix) } if cgroup.Name != opts.CgroupName { t.Errorf("Expected to have %s as Name instead of %s", opts.CgroupName, cgroup.Name) } } func TestLinuxCgroupSystemdWithInvalidPath(t *testing.T) { cgroupsPath := "/user/cgroups/path/id" spec := &specs.Spec{} spec.Linux = &specs.Linux{ CgroupsPath: cgroupsPath, } opts := &CreateOpts{ CgroupName: "ContainerID", UseSystemdCgroup: true, Spec: spec, } _, err := CreateCgroupConfig(opts, nil) if err == nil { t.Error("Expected to produce an error if not using the correct format for cgroup paths belonging to systemd") } } func TestLinuxCgroupsPathSpecified(t *testing.T) { cgroupsPath := "/user/cgroups/path/id" spec := &specs.Spec{} spec.Linux = &specs.Linux{ CgroupsPath: cgroupsPath, } opts := &CreateOpts{ CgroupName: "ContainerID", UseSystemdCgroup: false, Spec: spec, } cgroup, err := CreateCgroupConfig(opts, nil) if err != nil { t.Errorf("Couldn't create Cgroup config: %v", err) } if cgroup.Path != cgroupsPath { t.Errorf("Wrong cgroupsPath, expected '%s' got '%s'", cgroupsPath, cgroup.Path) } } func TestLinuxCgroupsPathNotSpecified(t *testing.T) { spec := &specs.Spec{} opts := &CreateOpts{ CgroupName: "ContainerID", UseSystemdCgroup: false, Spec: spec, } cgroup, err := CreateCgroupConfig(opts, nil) if err != nil { t.Errorf("Couldn't create Cgroup config: %v", err) } if cgroup.Path != "" { t.Errorf("Wrong cgroupsPath, expected it to be empty string, got '%s'", cgroup.Path) } } func TestSpecconvExampleValidate(t *testing.T) { spec := Example() spec.Root.Path = "/" opts := &CreateOpts{ CgroupName: "ContainerID", UseSystemdCgroup: false, Spec: spec, } config, err := CreateLibcontainerConfig(opts) if err != nil { t.Errorf("Couldn't create libcontainer config: %v", err) } if config.NoNewPrivileges != spec.Process.NoNewPrivileges { t.Errorf("specconv NoNewPrivileges mismatch. Expected %v got %v", spec.Process.NoNewPrivileges, config.NoNewPrivileges) } validator := validate.New() if err := validator.Validate(config); err != nil { t.Errorf("Expected specconv to produce valid container config: %v", err) } } func TestSpecconvNoLinuxSection(t *testing.T) { spec := Example() spec.Root.Path = "/" spec.Linux = nil spec.Hostname = "" opts := &CreateOpts{ CgroupName: "ContainerID", Spec: spec, } config, err := CreateLibcontainerConfig(opts) if err != nil { t.Errorf("Couldn't create libcontainer config: %v", err) } validator := validate.New() if err := validator.Validate(config); err != nil { t.Errorf("Expected specconv to produce valid container config: %v", err) } } func TestDupNamespaces(t *testing.T) { spec := &specs.Spec{ Root: &specs.Root{ Path: "rootfs", }, Linux: &specs.Linux{ Namespaces: []specs.LinuxNamespace{ { Type: "pid", }, { Type: "pid", Path: "/proc/1/ns/pid", }, }, }, } _, err := CreateLibcontainerConfig(&CreateOpts{ Spec: spec, }) if !strings.Contains(err.Error(), "malformed spec file: duplicated ns") { t.Errorf("Duplicated namespaces should be forbidden") } } func TestUserNamespaceMappingAndPath(t *testing.T) { if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { t.Skip("Test requires userns.") } spec := &specs.Spec{ Root: &specs.Root{ Path: "rootfs", }, Linux: &specs.Linux{ UIDMappings: []specs.LinuxIDMapping{ {ContainerID: 0, HostID: 1000, Size: 1000}, }, GIDMappings: []specs.LinuxIDMapping{ {ContainerID: 0, HostID: 2000, Size: 1000}, }, Namespaces: []specs.LinuxNamespace{ { Type: "user", Path: "/proc/1/ns/user", }, }, }, } _, err := CreateLibcontainerConfig(&CreateOpts{ Spec: spec, }) if !strings.Contains(err.Error(), "both namespace path and non-matching mapping specified") { t.Errorf("user namespace with path and non-matching mapping should be forbidden, got error %v", err) } } func TestNonZeroEUIDCompatibleSpecconvValidate(t *testing.T) { if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { t.Skip("Test requires userns.") } spec := Example() spec.Root.Path = "/" ToRootless(spec) opts := &CreateOpts{ CgroupName: "ContainerID", UseSystemdCgroup: false, Spec: spec, RootlessEUID: true, RootlessCgroups: true, } config, err := CreateLibcontainerConfig(opts) if err != nil { t.Errorf("Couldn't create libcontainer config: %v", err) } validator := validate.New() if err := validator.Validate(config); err != nil { t.Errorf("Expected specconv to produce valid rootless container config: %v", err) } } func TestInitSystemdProps(t *testing.T) { type inT struct { name, value string } type expT struct { isErr bool name string value interface{} } testCases := []struct { desc string in inT exp expT }{ { in: inT{"org.systemd.property.TimeoutStopUSec", "uint64 123456789"}, exp: expT{false, "TimeoutStopUSec", uint64(123456789)}, }, { desc: "convert USec to Sec (default numeric type)", in: inT{"org.systemd.property.TimeoutStopSec", "456"}, exp: expT{false, "TimeoutStopUSec", uint64(456000000)}, }, { desc: "convert USec to Sec (byte)", in: inT{"org.systemd.property.TimeoutStopSec", "byte 234"}, exp: expT{false, "TimeoutStopUSec", uint64(234000000)}, }, { desc: "convert USec to Sec (int16)", in: inT{"org.systemd.property.TimeoutStopSec", "int16 234"}, exp: expT{false, "TimeoutStopUSec", uint64(234000000)}, }, { desc: "convert USec to Sec (uint16)", in: inT{"org.systemd.property.TimeoutStopSec", "uint16 234"}, exp: expT{false, "TimeoutStopUSec", uint64(234000000)}, }, { desc: "convert USec to Sec (int32)", in: inT{"org.systemd.property.TimeoutStopSec", "int32 234"}, exp: expT{false, "TimeoutStopUSec", uint64(234000000)}, }, { desc: "convert USec to Sec (uint32)", in: inT{"org.systemd.property.TimeoutStopSec", "uint32 234"}, exp: expT{false, "TimeoutStopUSec", uint64(234000000)}, }, { desc: "convert USec to Sec (int64)", in: inT{"org.systemd.property.TimeoutStopSec", "int64 234"}, exp: expT{false, "TimeoutStopUSec", uint64(234000000)}, }, { desc: "convert USec to Sec (uint64)", in: inT{"org.systemd.property.TimeoutStopSec", "uint64 234"}, exp: expT{false, "TimeoutStopUSec", uint64(234000000)}, }, { desc: "convert USec to Sec (float)", in: inT{"org.systemd.property.TimeoutStopSec", "234.789"}, exp: expT{false, "TimeoutStopUSec", uint64(234789000)}, }, { desc: "convert USec to Sec (bool -- invalid value)", in: inT{"org.systemd.property.TimeoutStopSec", "false"}, exp: expT{true, "", ""}, }, { desc: "convert USec to Sec (string -- invalid value)", in: inT{"org.systemd.property.TimeoutStopSec", "'covfefe'"}, exp: expT{true, "", ""}, }, { desc: "convert USec to Sec (bad variable name, no conversion)", in: inT{"org.systemd.property.FOOSec", "123"}, exp: expT{false, "FOOSec", 123}, }, { in: inT{"org.systemd.property.CollectMode", "'inactive-or-failed'"}, exp: expT{false, "CollectMode", "inactive-or-failed"}, }, { desc: "unrelated property", in: inT{"some.other.annotation", "0"}, exp: expT{false, "", ""}, }, { desc: "too short property name", in: inT{"org.systemd.property.Xo", "1"}, exp: expT{true, "", ""}, }, { desc: "invalid character in property name", in: inT{"org.systemd.property.Number1", "1"}, exp: expT{true, "", ""}, }, { desc: "invalid property value", in: inT{"org.systemd.property.ValidName", "invalid-value"}, exp: expT{true, "", ""}, }, } spec := &specs.Spec{} for _, tc := range testCases { tc := tc spec.Annotations = map[string]string{tc.in.name: tc.in.value} outMap, err := initSystemdProps(spec) // t.Logf("input %+v, expected %+v, got err:%v out:%+v", tc.in, tc.exp, err, outMap) if tc.exp.isErr != (err != nil) { t.Errorf("input %+v, expecting error: %v, got %v", tc.in, tc.exp.isErr, err) } expLen := 1 // expect a single item if tc.exp.name == "" { expLen = 0 // expect nothing } if len(outMap) != expLen { t.Fatalf("input %+v, expected %d, got %d entries: %v", tc.in, expLen, len(outMap), outMap) } if expLen == 0 { continue } out := outMap[0] if tc.exp.name != out.Name { t.Errorf("input %+v, expecting name: %q, got %q", tc.in, tc.exp.name, out.Name) } expValue := dbus.MakeVariant(tc.exp.value).String() if expValue != out.Value.String() { t.Errorf("input %+v, expecting value: %s, got %s", tc.in, expValue, out.Value) } } } func TestIsValidName(t *testing.T) { testCases := []struct { in string valid bool }{ {"", false}, // too short {"xx", false}, // too short {"xxx", true}, {"someValidName", true}, {"A name", false}, // space {"3335", false}, // numbers {"Name1", false}, // numbers {"Кир", false}, // non-ascii {"მადლობა", false}, // non-ascii {"合い言葉", false}, // non-ascii } for _, tc := range testCases { err := checkPropertyName(tc.in) if (err == nil) != tc.valid { t.Errorf("case %q: expected valid: %v, got error: %v", tc.in, tc.valid, err) } } } func BenchmarkIsValidName(b *testing.B) { for i := 0; i < b.N; i++ { for _, s := range []string{"", "xx", "xxx", "someValidName", "A name", "Кир", "მადლობა", "合い言葉"} { _ = checkPropertyName(s) } } } func TestNullProcess(t *testing.T) { spec := Example() spec.Process = nil _, err := CreateLibcontainerConfig(&CreateOpts{ Spec: spec, }) if err != nil { t.Errorf("Null process should be forbidden") } } func TestCreateDevices(t *testing.T) { spec := Example() // dummy uid/gid for /dev/tty; will enable the test to check if createDevices() // preferred the spec's device over the redundant default device ttyUid := uint32(1000) ttyGid := uint32(1000) fm := os.FileMode(0o666) spec.Linux = &specs.Linux{ Devices: []specs.LinuxDevice{ { // This is purposely redundant with one of runc's default devices Path: "/dev/tty", Type: "c", Major: 5, Minor: 0, FileMode: &fm, UID: &ttyUid, GID: &ttyGid, }, { // This is purposely not redundant with one of runc's default devices Path: "/dev/ram0", Type: "b", Major: 1, Minor: 0, }, }, } conf := &configs.Config{} defaultDevs, err := createDevices(spec, conf) if err != nil { t.Errorf("failed to create devices: %v", err) } // Verify the returned default devices has the /dev/tty entry deduplicated found := false for _, d := range defaultDevs { if d.Path == "/dev/tty" { if found { t.Errorf("createDevices failed: returned a duplicated device entry: %v", defaultDevs) } found = true } } // Verify that createDevices() placed all default devices in the config for _, allowedDev := range AllowedDevices { if allowedDev.Path == "" { continue } found := false for _, configDev := range conf.Devices { if configDev.Path == allowedDev.Path { found = true } } if !found { configDevPaths := []string{} for _, configDev := range conf.Devices { configDevPaths = append(configDevPaths, configDev.Path) } t.Errorf("allowedDevice %s was not found in the config's devices: %v", allowedDev.Path, configDevPaths) } } // Verify that createDevices() deduplicated the /dev/tty entry in the config for _, configDev := range conf.Devices { if configDev.Path == "/dev/tty" { wantDev := &devices.Device{ Path: "/dev/tty", FileMode: 0o666, Uid: 1000, Gid: 1000, Rule: devices.Rule{ Type: devices.CharDevice, Major: 5, Minor: 0, }, } if *configDev != *wantDev { t.Errorf("redundant dev was not deduplicated correctly: want %v, got %v", wantDev, configDev) } } } // Verify that createDevices() added the entry for /dev/ram0 in the config found = false for _, configDev := range conf.Devices { if configDev.Path == "/dev/ram0" { found = true break } } if !found { t.Errorf("device /dev/ram0 not found in config devices; got %v", conf.Devices) } }