github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/container/opts_test.go (about)

     1  package container
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"runtime"
     8  	"strings"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/docker/docker/api/types/container"
    13  	networktypes "github.com/docker/docker/api/types/network"
    14  	"github.com/docker/go-connections/nat"
    15  	"github.com/pkg/errors"
    16  	"github.com/spf13/pflag"
    17  	"gotest.tools/v3/assert"
    18  	is "gotest.tools/v3/assert/cmp"
    19  	"gotest.tools/v3/skip"
    20  )
    21  
    22  func TestValidateAttach(t *testing.T) {
    23  	valid := []string{
    24  		"stdin",
    25  		"stdout",
    26  		"stderr",
    27  		"STDIN",
    28  		"STDOUT",
    29  		"STDERR",
    30  	}
    31  	if _, err := validateAttach("invalid"); err == nil {
    32  		t.Fatal("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing")
    33  	}
    34  
    35  	for _, attach := range valid {
    36  		value, err := validateAttach(attach)
    37  		if err != nil {
    38  			t.Fatal(err)
    39  		}
    40  		if value != strings.ToLower(attach) {
    41  			t.Fatalf("Expected [%v], got [%v]", attach, value)
    42  		}
    43  	}
    44  }
    45  
    46  func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) {
    47  	flags, copts := setupRunFlags()
    48  	if err := flags.Parse(args); err != nil {
    49  		return nil, nil, nil, err
    50  	}
    51  	// TODO: fix tests to accept ContainerConfig
    52  	containerCfg, err := parse(flags, copts, runtime.GOOS)
    53  	if err != nil {
    54  		return nil, nil, nil, err
    55  	}
    56  	return containerCfg.Config, containerCfg.HostConfig, containerCfg.NetworkingConfig, err
    57  }
    58  
    59  func setupRunFlags() (*pflag.FlagSet, *containerOptions) {
    60  	flags := pflag.NewFlagSet("run", pflag.ContinueOnError)
    61  	flags.SetOutput(io.Discard)
    62  	flags.Usage = nil
    63  	copts := addFlags(flags)
    64  	return flags, copts
    65  }
    66  
    67  func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig) {
    68  	t.Helper()
    69  	config, hostConfig, nwConfig, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
    70  	assert.NilError(t, err)
    71  	return config, hostConfig, nwConfig
    72  }
    73  
    74  func TestParseRunLinks(t *testing.T) {
    75  	if _, hostConfig, _ := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" {
    76  		t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links)
    77  	}
    78  	if _, hostConfig, _ := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" {
    79  		t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links)
    80  	}
    81  	if _, hostConfig, _ := mustParse(t, ""); len(hostConfig.Links) != 0 {
    82  		t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links)
    83  	}
    84  }
    85  
    86  func TestParseRunAttach(t *testing.T) {
    87  	tests := []struct {
    88  		input    string
    89  		expected container.Config
    90  	}{
    91  		{
    92  			input: "",
    93  			expected: container.Config{
    94  				AttachStdout: true,
    95  				AttachStderr: true,
    96  			},
    97  		},
    98  		{
    99  			input: "-i",
   100  			expected: container.Config{
   101  				AttachStdin:  true,
   102  				AttachStdout: true,
   103  				AttachStderr: true,
   104  			},
   105  		},
   106  		{
   107  			input: "-a stdin",
   108  			expected: container.Config{
   109  				AttachStdin: true,
   110  			},
   111  		},
   112  		{
   113  			input: "-a stdin -a stdout",
   114  			expected: container.Config{
   115  				AttachStdin:  true,
   116  				AttachStdout: true,
   117  			},
   118  		},
   119  		{
   120  			input: "-a stdin -a stdout -a stderr",
   121  			expected: container.Config{
   122  				AttachStdin:  true,
   123  				AttachStdout: true,
   124  				AttachStderr: true,
   125  			},
   126  		},
   127  	}
   128  	for _, tc := range tests {
   129  		tc := tc
   130  		t.Run(tc.input, func(t *testing.T) {
   131  			config, _, _ := mustParse(t, tc.input)
   132  			assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
   133  			assert.Equal(t, config.AttachStdout, tc.expected.AttachStdout)
   134  			assert.Equal(t, config.AttachStderr, tc.expected.AttachStderr)
   135  		})
   136  	}
   137  }
   138  
   139  func TestParseRunWithInvalidArgs(t *testing.T) {
   140  	tests := []struct {
   141  		args  []string
   142  		error string
   143  	}{
   144  		{
   145  			args:  []string{"-a", "ubuntu", "bash"},
   146  			error: `invalid argument "ubuntu" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
   147  		},
   148  		{
   149  			args:  []string{"-a", "invalid", "ubuntu", "bash"},
   150  			error: `invalid argument "invalid" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
   151  		},
   152  		{
   153  			args:  []string{"-a", "invalid", "-a", "stdout", "ubuntu", "bash"},
   154  			error: `invalid argument "invalid" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
   155  		},
   156  		{
   157  			args:  []string{"-a", "stdout", "-a", "stderr", "-z", "ubuntu", "bash"},
   158  			error: `unknown shorthand flag: 'z' in -z`,
   159  		},
   160  		{
   161  			args:  []string{"-a", "stdin", "-z", "ubuntu", "bash"},
   162  			error: `unknown shorthand flag: 'z' in -z`,
   163  		},
   164  		{
   165  			args:  []string{"-a", "stdout", "-z", "ubuntu", "bash"},
   166  			error: `unknown shorthand flag: 'z' in -z`,
   167  		},
   168  		{
   169  			args:  []string{"-a", "stderr", "-z", "ubuntu", "bash"},
   170  			error: `unknown shorthand flag: 'z' in -z`,
   171  		},
   172  		{
   173  			args:  []string{"-z", "--rm", "ubuntu", "bash"},
   174  			error: `unknown shorthand flag: 'z' in -z`,
   175  		},
   176  	}
   177  	flags, _ := setupRunFlags()
   178  	for _, tc := range tests {
   179  		t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
   180  			assert.Error(t, flags.Parse(tc.args), tc.error)
   181  		})
   182  	}
   183  }
   184  
   185  //nolint:gocyclo
   186  func TestParseWithVolumes(t *testing.T) {
   187  	// A single volume
   188  	arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
   189  	if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil {
   190  		t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
   191  	} else if _, exists := config.Volumes[arr[0]]; !exists {
   192  		t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes)
   193  	}
   194  
   195  	// Two volumes
   196  	arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`})
   197  	if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil {
   198  		t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
   199  	} else if _, exists := config.Volumes[arr[0]]; !exists {
   200  		t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes)
   201  	} else if _, exists := config.Volumes[arr[1]]; !exists { //nolint:govet // ignore shadow-check
   202  		t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes)
   203  	}
   204  
   205  	// A single bind mount
   206  	arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
   207  	if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
   208  		t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
   209  	}
   210  
   211  	// Two bind mounts.
   212  	arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
   213  	if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
   214  		t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
   215  	}
   216  
   217  	// Two bind mounts, first read-only, second read-write.
   218  	// TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4
   219  	arr, tryit = setupPlatformVolume(
   220  		[]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`},
   221  		[]string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`})
   222  	if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
   223  		t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
   224  	}
   225  
   226  	// Similar to previous test but with alternate modes which are only supported by Linux
   227  	if runtime.GOOS != "windows" {
   228  		arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{})
   229  		if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
   230  			t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
   231  		}
   232  
   233  		arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{})
   234  		if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
   235  			t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
   236  		}
   237  	}
   238  
   239  	// One bind mount and one volume
   240  	arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`})
   241  	if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] {
   242  		t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds)
   243  	} else if _, exists := config.Volumes[arr[1]]; !exists {
   244  		t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes)
   245  	}
   246  
   247  	// Root to non-c: drive letter (Windows specific)
   248  	if runtime.GOOS == "windows" {
   249  		arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`})
   250  		if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 {
   251  			t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
   252  		}
   253  	}
   254  }
   255  
   256  // setupPlatformVolume takes two arrays of volume specs - a Unix style
   257  // spec and a Windows style spec. Depending on the platform being unit tested,
   258  // it returns one of them, along with a volume string that would be passed
   259  // on the docker CLI (e.g. -v /bar -v /foo).
   260  func setupPlatformVolume(u []string, w []string) ([]string, string) {
   261  	var a []string
   262  	if runtime.GOOS == "windows" {
   263  		a = w
   264  	} else {
   265  		a = u
   266  	}
   267  	s := ""
   268  	for _, v := range a {
   269  		s = s + "-v " + v + " "
   270  	}
   271  	return a, s
   272  }
   273  
   274  // check if (a == c && b == d) || (a == d && b == c)
   275  // because maps are randomized
   276  func compareRandomizedStrings(a, b, c, d string) error {
   277  	if a == c && b == d {
   278  		return nil
   279  	}
   280  	if a == d && b == c {
   281  		return nil
   282  	}
   283  	return errors.Errorf("strings don't match")
   284  }
   285  
   286  // Simple parse with MacAddress validation
   287  func TestParseWithMacAddress(t *testing.T) {
   288  	invalidMacAddress := "--mac-address=invalidMacAddress"
   289  	validMacAddress := "--mac-address=92:d0:c6:0a:29:33"
   290  	if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
   291  		t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
   292  	}
   293  	config, hostConfig, nwConfig := mustParse(t, validMacAddress)
   294  	if config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
   295  		t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as container-wide MacAddress, got '%v'",
   296  			config.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
   297  	}
   298  	defaultNw := hostConfig.NetworkMode.NetworkName()
   299  	if nwConfig.EndpointsConfig[defaultNw].MacAddress != "92:d0:c6:0a:29:33" {
   300  		t.Fatalf("Expected the default endpoint to have the MacAddress '92:d0:c6:0a:29:33' set, got '%v'", nwConfig.EndpointsConfig[defaultNw].MacAddress)
   301  	}
   302  }
   303  
   304  func TestRunFlagsParseWithMemory(t *testing.T) {
   305  	flags, _ := setupRunFlags()
   306  	args := []string{"--memory=invalid", "img", "cmd"}
   307  	err := flags.Parse(args)
   308  	assert.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`)
   309  
   310  	_, hostconfig, _ := mustParse(t, "--memory=1G")
   311  	assert.Check(t, is.Equal(int64(1073741824), hostconfig.Memory))
   312  }
   313  
   314  func TestParseWithMemorySwap(t *testing.T) {
   315  	flags, _ := setupRunFlags()
   316  	args := []string{"--memory-swap=invalid", "img", "cmd"}
   317  	err := flags.Parse(args)
   318  	assert.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`)
   319  
   320  	_, hostconfig, _ := mustParse(t, "--memory-swap=1G")
   321  	assert.Check(t, is.Equal(int64(1073741824), hostconfig.MemorySwap))
   322  
   323  	_, hostconfig, _ = mustParse(t, "--memory-swap=-1")
   324  	assert.Check(t, is.Equal(int64(-1), hostconfig.MemorySwap))
   325  }
   326  
   327  func TestParseHostname(t *testing.T) {
   328  	validHostnames := map[string]string{
   329  		"hostname":    "hostname",
   330  		"host-name":   "host-name",
   331  		"hostname123": "hostname123",
   332  		"123hostname": "123hostname",
   333  		"hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error",
   334  	}
   335  	hostnameWithDomain := "--hostname=hostname.domainname"
   336  	hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
   337  	for hostname, expectedHostname := range validHostnames {
   338  		if config, _, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname {
   339  			t.Fatalf("Expected the config to have 'hostname' as %q, got %q", expectedHostname, config.Hostname)
   340  		}
   341  	}
   342  	if config, _, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" || config.Domainname != "" {
   343  		t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got %q", config.Hostname)
   344  	}
   345  	if config, _, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" || config.Domainname != "" {
   346  		t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got %q", config.Hostname)
   347  	}
   348  }
   349  
   350  func TestParseHostnameDomainname(t *testing.T) {
   351  	validDomainnames := map[string]string{
   352  		"domainname":    "domainname",
   353  		"domain-name":   "domain-name",
   354  		"domainname123": "domainname123",
   355  		"123domainname": "123domainname",
   356  		"domainname-63-bytes-long-should-be-valid-and-without-any-errors": "domainname-63-bytes-long-should-be-valid-and-without-any-errors",
   357  	}
   358  	for domainname, expectedDomainname := range validDomainnames {
   359  		if config, _, _ := mustParse(t, "--domainname="+domainname); config.Domainname != expectedDomainname {
   360  			t.Fatalf("Expected the config to have 'domainname' as %q, got %q", expectedDomainname, config.Domainname)
   361  		}
   362  	}
   363  	if config, _, _ := mustParse(t, "--hostname=some.prefix --domainname=domainname"); config.Hostname != "some.prefix" || config.Domainname != "domainname" {
   364  		t.Fatalf("Expected the config to have 'hostname' as 'some.prefix' and 'domainname' as 'domainname', got %q and %q", config.Hostname, config.Domainname)
   365  	}
   366  	if config, _, _ := mustParse(t, "--hostname=another-prefix --domainname=domainname.tld"); config.Hostname != "another-prefix" || config.Domainname != "domainname.tld" {
   367  		t.Fatalf("Expected the config to have 'hostname' as 'another-prefix' and 'domainname' as 'domainname.tld', got %q and %q", config.Hostname, config.Domainname)
   368  	}
   369  }
   370  
   371  func TestParseWithExpose(t *testing.T) {
   372  	invalids := map[string]string{
   373  		":":                   "invalid port format for --expose: :",
   374  		"8080:9090":           "invalid port format for --expose: 8080:9090",
   375  		"/tcp":                "invalid range format for --expose: /tcp, error: empty string specified for ports",
   376  		"/udp":                "invalid range format for --expose: /udp, error: empty string specified for ports",
   377  		"NaN/tcp":             `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
   378  		"NaN-NaN/tcp":         `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
   379  		"8080-NaN/tcp":        `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
   380  		"1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`,
   381  	}
   382  	valids := map[string][]nat.Port{
   383  		"8080/tcp":      {"8080/tcp"},
   384  		"8080/udp":      {"8080/udp"},
   385  		"8080/ncp":      {"8080/ncp"},
   386  		"8080-8080/udp": {"8080/udp"},
   387  		"8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"},
   388  	}
   389  	for expose, expectedError := range invalids {
   390  		if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError {
   391  			t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err)
   392  		}
   393  	}
   394  	for expose, exposedPorts := range valids {
   395  		config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
   396  		if err != nil {
   397  			t.Fatal(err)
   398  		}
   399  		if len(config.ExposedPorts) != len(exposedPorts) {
   400  			t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts))
   401  		}
   402  		for _, port := range exposedPorts {
   403  			if _, ok := config.ExposedPorts[port]; !ok {
   404  				t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts)
   405  			}
   406  		}
   407  	}
   408  	// Merge with actual published port
   409  	config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"})
   410  	if err != nil {
   411  		t.Fatal(err)
   412  	}
   413  	if len(config.ExposedPorts) != 2 {
   414  		t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts)
   415  	}
   416  	ports := []nat.Port{"80/tcp", "81/tcp"}
   417  	for _, port := range ports {
   418  		if _, ok := config.ExposedPorts[port]; !ok {
   419  			t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts)
   420  		}
   421  	}
   422  }
   423  
   424  func TestParseDevice(t *testing.T) {
   425  	skip.If(t, runtime.GOOS != "linux") // Windows and macOS validate server-side
   426  	testCases := []struct {
   427  		devices        []string
   428  		deviceMapping  *container.DeviceMapping
   429  		deviceRequests []container.DeviceRequest
   430  	}{
   431  		{
   432  			devices: []string{"/dev/snd"},
   433  			deviceMapping: &container.DeviceMapping{
   434  				PathOnHost:        "/dev/snd",
   435  				PathInContainer:   "/dev/snd",
   436  				CgroupPermissions: "rwm",
   437  			},
   438  		},
   439  		{
   440  			devices: []string{"/dev/snd:rw"},
   441  			deviceMapping: &container.DeviceMapping{
   442  				PathOnHost:        "/dev/snd",
   443  				PathInContainer:   "/dev/snd",
   444  				CgroupPermissions: "rw",
   445  			},
   446  		},
   447  		{
   448  			devices: []string{"/dev/snd:/something"},
   449  			deviceMapping: &container.DeviceMapping{
   450  				PathOnHost:        "/dev/snd",
   451  				PathInContainer:   "/something",
   452  				CgroupPermissions: "rwm",
   453  			},
   454  		},
   455  		{
   456  			devices: []string{"/dev/snd:/something:rw"},
   457  			deviceMapping: &container.DeviceMapping{
   458  				PathOnHost:        "/dev/snd",
   459  				PathInContainer:   "/something",
   460  				CgroupPermissions: "rw",
   461  			},
   462  		},
   463  		{
   464  			devices:       []string{"vendor.com/class=name"},
   465  			deviceMapping: nil,
   466  			deviceRequests: []container.DeviceRequest{
   467  				{
   468  					Driver:    "cdi",
   469  					DeviceIDs: []string{"vendor.com/class=name"},
   470  				},
   471  			},
   472  		},
   473  		{
   474  			devices: []string{"vendor.com/class=name", "/dev/snd:/something:rw"},
   475  			deviceMapping: &container.DeviceMapping{
   476  				PathOnHost:        "/dev/snd",
   477  				PathInContainer:   "/something",
   478  				CgroupPermissions: "rw",
   479  			},
   480  			deviceRequests: []container.DeviceRequest{
   481  				{
   482  					Driver:    "cdi",
   483  					DeviceIDs: []string{"vendor.com/class=name"},
   484  				},
   485  			},
   486  		},
   487  	}
   488  
   489  	for _, tc := range testCases {
   490  		t.Run(fmt.Sprintf("%s", tc.devices), func(t *testing.T) {
   491  			var args []string
   492  			for _, d := range tc.devices {
   493  				args = append(args, fmt.Sprintf("--device=%v", d))
   494  			}
   495  			args = append(args, "img", "cmd")
   496  
   497  			_, hostconfig, _, err := parseRun(args)
   498  
   499  			assert.NilError(t, err)
   500  
   501  			if tc.deviceMapping != nil {
   502  				if assert.Check(t, is.Len(hostconfig.Devices, 1)) {
   503  					assert.Check(t, is.DeepEqual(*tc.deviceMapping, hostconfig.Devices[0]))
   504  				}
   505  			} else {
   506  				assert.Check(t, is.Len(hostconfig.Devices, 0))
   507  			}
   508  
   509  			assert.Check(t, is.DeepEqual(tc.deviceRequests, hostconfig.DeviceRequests))
   510  		})
   511  	}
   512  }
   513  
   514  func TestParseNetworkConfig(t *testing.T) {
   515  	tests := []struct {
   516  		name            string
   517  		flags           []string
   518  		expected        map[string]*networktypes.EndpointSettings
   519  		expectedCfg     container.Config
   520  		expectedHostCfg container.HostConfig
   521  		expectedErr     string
   522  	}{
   523  		{
   524  			name:            "single-network-legacy",
   525  			flags:           []string{"--network", "net1"},
   526  			expected:        map[string]*networktypes.EndpointSettings{},
   527  			expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
   528  		},
   529  		{
   530  			name:            "single-network-advanced",
   531  			flags:           []string{"--network", "name=net1"},
   532  			expected:        map[string]*networktypes.EndpointSettings{},
   533  			expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
   534  		},
   535  		{
   536  			name: "single-network-legacy-with-options",
   537  			flags: []string{
   538  				"--ip", "172.20.88.22",
   539  				"--ip6", "2001:db8::8822",
   540  				"--link", "foo:bar",
   541  				"--link", "bar:baz",
   542  				"--link-local-ip", "169.254.2.2",
   543  				"--link-local-ip", "fe80::169:254:2:2",
   544  				"--network", "name=net1",
   545  				"--network-alias", "web1",
   546  				"--network-alias", "web2",
   547  			},
   548  			expected: map[string]*networktypes.EndpointSettings{
   549  				"net1": {
   550  					IPAMConfig: &networktypes.EndpointIPAMConfig{
   551  						IPv4Address:  "172.20.88.22",
   552  						IPv6Address:  "2001:db8::8822",
   553  						LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
   554  					},
   555  					Links:   []string{"foo:bar", "bar:baz"},
   556  					Aliases: []string{"web1", "web2"},
   557  				},
   558  			},
   559  			expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
   560  		},
   561  		{
   562  			name: "multiple-network-advanced-mixed",
   563  			flags: []string{
   564  				"--ip", "172.20.88.22",
   565  				"--ip6", "2001:db8::8822",
   566  				"--link", "foo:bar",
   567  				"--link", "bar:baz",
   568  				"--link-local-ip", "169.254.2.2",
   569  				"--link-local-ip", "fe80::169:254:2:2",
   570  				"--network", "name=net1,driver-opt=field1=value1",
   571  				"--network-alias", "web1",
   572  				"--network-alias", "web2",
   573  				"--network", "net2",
   574  				"--network", "name=net3,alias=web3,driver-opt=field3=value3,ip=172.20.88.22,ip6=2001:db8::8822",
   575  				"--network", "name=net4,mac-address=02:32:1c:23:00:04,link-local-ip=169.254.169.254",
   576  			},
   577  			expected: map[string]*networktypes.EndpointSettings{
   578  				"net1": {
   579  					DriverOpts: map[string]string{"field1": "value1"},
   580  					IPAMConfig: &networktypes.EndpointIPAMConfig{
   581  						IPv4Address:  "172.20.88.22",
   582  						IPv6Address:  "2001:db8::8822",
   583  						LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
   584  					},
   585  					Links:   []string{"foo:bar", "bar:baz"},
   586  					Aliases: []string{"web1", "web2"},
   587  				},
   588  				"net2": {},
   589  				"net3": {
   590  					DriverOpts: map[string]string{"field3": "value3"},
   591  					IPAMConfig: &networktypes.EndpointIPAMConfig{
   592  						IPv4Address: "172.20.88.22",
   593  						IPv6Address: "2001:db8::8822",
   594  					},
   595  					Aliases: []string{"web3"},
   596  				},
   597  				"net4": {
   598  					MacAddress: "02:32:1c:23:00:04",
   599  					IPAMConfig: &networktypes.EndpointIPAMConfig{
   600  						LinkLocalIPs: []string{"169.254.169.254"},
   601  					},
   602  				},
   603  			},
   604  			expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
   605  		},
   606  		{
   607  			name:  "single-network-advanced-with-options",
   608  			flags: []string{"--network", "name=net1,alias=web1,alias=web2,driver-opt=field1=value1,driver-opt=field2=value2,ip=172.20.88.22,ip6=2001:db8::8822,mac-address=02:32:1c:23:00:04"},
   609  			expected: map[string]*networktypes.EndpointSettings{
   610  				"net1": {
   611  					DriverOpts: map[string]string{
   612  						"field1": "value1",
   613  						"field2": "value2",
   614  					},
   615  					IPAMConfig: &networktypes.EndpointIPAMConfig{
   616  						IPv4Address: "172.20.88.22",
   617  						IPv6Address: "2001:db8::8822",
   618  					},
   619  					Aliases:    []string{"web1", "web2"},
   620  					MacAddress: "02:32:1c:23:00:04",
   621  				},
   622  			},
   623  			expectedCfg:     container.Config{MacAddress: "02:32:1c:23:00:04"},
   624  			expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
   625  		},
   626  		{
   627  			name:            "multiple-networks",
   628  			flags:           []string{"--network", "net1", "--network", "name=net2"},
   629  			expected:        map[string]*networktypes.EndpointSettings{"net1": {}, "net2": {}},
   630  			expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
   631  		},
   632  		{
   633  			name:  "advanced-options-with-standalone-mac-address-flag",
   634  			flags: []string{"--network=name=net1,alias=foobar", "--mac-address", "52:0f:f3:dc:50:10"},
   635  			expected: map[string]*networktypes.EndpointSettings{
   636  				"net1": {
   637  					Aliases:    []string{"foobar"},
   638  					MacAddress: "52:0f:f3:dc:50:10",
   639  				},
   640  			},
   641  			expectedCfg:     container.Config{MacAddress: "52:0f:f3:dc:50:10"},
   642  			expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
   643  		},
   644  		{
   645  			name:        "conflict-network",
   646  			flags:       []string{"--network", "duplicate", "--network", "name=duplicate"},
   647  			expectedErr: `network "duplicate" is specified multiple times`,
   648  		},
   649  		{
   650  			name:        "conflict-options-alias",
   651  			flags:       []string{"--network", "name=net1,alias=web1", "--network-alias", "web1"},
   652  			expectedErr: `conflicting options: cannot specify both --network-alias and per-network alias`,
   653  		},
   654  		{
   655  			name:        "conflict-options-ip",
   656  			flags:       []string{"--network", "name=net1,ip=172.20.88.22,ip6=2001:db8::8822", "--ip", "172.20.88.22"},
   657  			expectedErr: `conflicting options: cannot specify both --ip and per-network IPv4 address`,
   658  		},
   659  		{
   660  			name:        "conflict-options-ip6",
   661  			flags:       []string{"--network", "name=net1,ip=172.20.88.22,ip6=2001:db8::8822", "--ip6", "2001:db8::8822"},
   662  			expectedErr: `conflicting options: cannot specify both --ip6 and per-network IPv6 address`,
   663  		},
   664  		{
   665  			name:        "invalid-mixed-network-types",
   666  			flags:       []string{"--network", "name=host", "--network", "net1"},
   667  			expectedErr: `conflicting options: cannot attach both user-defined and non-user-defined network-modes`,
   668  		},
   669  		{
   670  			name:        "conflict-options-link-local-ip",
   671  			flags:       []string{"--network", "name=net1,link-local-ip=169.254.169.254", "--link-local-ip", "169.254.10.8"},
   672  			expectedErr: `conflicting options: cannot specify both --link-local-ip and per-network link-local IP addresses`,
   673  		},
   674  		{
   675  			name:        "conflict-options-mac-address",
   676  			flags:       []string{"--network", "name=net1,mac-address=02:32:1c:23:00:04", "--mac-address", "02:32:1c:23:00:04"},
   677  			expectedErr: `conflicting options: cannot specify both --mac-address and per-network MAC address`,
   678  		},
   679  		{
   680  			name:        "invalid-mac-address",
   681  			flags:       []string{"--network", "name=net1,mac-address=foobar"},
   682  			expectedErr: "foobar is not a valid mac address",
   683  		},
   684  	}
   685  
   686  	for _, tc := range tests {
   687  		t.Run(tc.name, func(t *testing.T) {
   688  			config, hConfig, nwConfig, err := parseRun(tc.flags)
   689  
   690  			if tc.expectedErr != "" {
   691  				assert.Error(t, err, tc.expectedErr)
   692  				return
   693  			}
   694  
   695  			assert.NilError(t, err)
   696  			assert.DeepEqual(t, config.MacAddress, tc.expectedCfg.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
   697  			assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedHostCfg.NetworkMode)
   698  			assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected)
   699  		})
   700  	}
   701  }
   702  
   703  func TestParseModes(t *testing.T) {
   704  	// pid ko
   705  	flags, copts := setupRunFlags()
   706  	args := []string{"--pid=container:", "img", "cmd"}
   707  	assert.NilError(t, flags.Parse(args))
   708  	_, err := parse(flags, copts, runtime.GOOS)
   709  	assert.ErrorContains(t, err, "--pid: invalid PID mode")
   710  
   711  	// pid ok
   712  	_, hostconfig, _, err := parseRun([]string{"--pid=host", "img", "cmd"})
   713  	assert.NilError(t, err)
   714  	if !hostconfig.PidMode.Valid() {
   715  		t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode)
   716  	}
   717  
   718  	// uts ko
   719  	_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled
   720  	assert.ErrorContains(t, err, "--uts: invalid UTS mode")
   721  
   722  	// uts ok
   723  	_, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"})
   724  	assert.NilError(t, err)
   725  	if !hostconfig.UTSMode.Valid() {
   726  		t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode)
   727  	}
   728  }
   729  
   730  func TestRunFlagsParseShmSize(t *testing.T) {
   731  	// shm-size ko
   732  	flags, _ := setupRunFlags()
   733  	args := []string{"--shm-size=a128m", "img", "cmd"}
   734  	expectedErr := `invalid argument "a128m" for "--shm-size" flag:`
   735  	err := flags.Parse(args)
   736  	assert.ErrorContains(t, err, expectedErr)
   737  
   738  	// shm-size ok
   739  	_, hostconfig, _, err := parseRun([]string{"--shm-size=128m", "img", "cmd"})
   740  	assert.NilError(t, err)
   741  	if hostconfig.ShmSize != 134217728 {
   742  		t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize)
   743  	}
   744  }
   745  
   746  func TestParseRestartPolicy(t *testing.T) {
   747  	tests := []struct {
   748  		input       string
   749  		expected    container.RestartPolicy
   750  		expectedErr string
   751  	}{
   752  		{
   753  			input: "",
   754  		},
   755  		{
   756  			input: "no",
   757  			expected: container.RestartPolicy{
   758  				Name: container.RestartPolicyDisabled,
   759  			},
   760  		},
   761  		{
   762  			input:       ":1",
   763  			expectedErr: "invalid restart policy format: no policy provided before colon",
   764  		},
   765  		{
   766  			input: "always",
   767  			expected: container.RestartPolicy{
   768  				Name: container.RestartPolicyAlways,
   769  			},
   770  		},
   771  		{
   772  			input: "always:1",
   773  			expected: container.RestartPolicy{
   774  				Name:              container.RestartPolicyAlways,
   775  				MaximumRetryCount: 1,
   776  			},
   777  		},
   778  		{
   779  			input:       "always:2:3",
   780  			expectedErr: "invalid restart policy format: maximum retry count must be an integer",
   781  		},
   782  		{
   783  			input: "on-failure:1",
   784  			expected: container.RestartPolicy{
   785  				Name:              container.RestartPolicyOnFailure,
   786  				MaximumRetryCount: 1,
   787  			},
   788  		},
   789  		{
   790  			input:       "on-failure:invalid",
   791  			expectedErr: "invalid restart policy format: maximum retry count must be an integer",
   792  		},
   793  		{
   794  			input: "unless-stopped",
   795  			expected: container.RestartPolicy{
   796  				Name: container.RestartPolicyUnlessStopped,
   797  			},
   798  		},
   799  		{
   800  			input:       "unless-stopped:invalid",
   801  			expectedErr: "invalid restart policy format: maximum retry count must be an integer",
   802  		},
   803  	}
   804  	for _, tc := range tests {
   805  		tc := tc
   806  		t.Run(tc.input, func(t *testing.T) {
   807  			_, hostConfig, _, err := parseRun([]string{"--restart=" + tc.input, "img", "cmd"})
   808  			if tc.expectedErr != "" {
   809  				assert.Check(t, is.Error(err, tc.expectedErr))
   810  				assert.Check(t, is.Nil(hostConfig))
   811  			} else {
   812  				assert.NilError(t, err)
   813  				assert.Check(t, is.DeepEqual(hostConfig.RestartPolicy, tc.expected))
   814  			}
   815  		})
   816  	}
   817  }
   818  
   819  func TestParseRestartPolicyAutoRemove(t *testing.T) {
   820  	expected := "Conflicting options: --restart and --rm"
   821  	_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled
   822  	if err == nil || err.Error() != expected {
   823  		t.Fatalf("Expected error %v, but got none", expected)
   824  	}
   825  }
   826  
   827  func TestParseHealth(t *testing.T) {
   828  	checkOk := func(args ...string) *container.HealthConfig {
   829  		config, _, _, err := parseRun(args)
   830  		if err != nil {
   831  			t.Fatalf("%#v: %v", args, err)
   832  		}
   833  		return config.Healthcheck
   834  	}
   835  	checkError := func(expected string, args ...string) {
   836  		config, _, _, err := parseRun(args)
   837  		if err == nil {
   838  			t.Fatalf("Expected error, but got %#v", config)
   839  		}
   840  		if err.Error() != expected {
   841  			t.Fatalf("Expected %#v, got %#v", expected, err)
   842  		}
   843  	}
   844  	health := checkOk("--no-healthcheck", "img", "cmd")
   845  	if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" {
   846  		t.Fatalf("--no-healthcheck failed: %#v", health)
   847  	}
   848  
   849  	health = checkOk("--health-cmd=/check.sh -q", "img", "cmd")
   850  	if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" {
   851  		t.Fatalf("--health-cmd: got %#v", health.Test)
   852  	}
   853  	if health.Timeout != 0 {
   854  		t.Fatalf("--health-cmd: timeout = %s", health.Timeout)
   855  	}
   856  
   857  	checkError("--no-healthcheck conflicts with --health-* options",
   858  		"--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd")
   859  
   860  	health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "--health-start-interval=1s", "img", "cmd")
   861  	if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second || health.StartInterval != 1*time.Second {
   862  		t.Fatalf("--health-*: got %#v", health)
   863  	}
   864  }
   865  
   866  func TestParseLoggingOpts(t *testing.T) {
   867  	// logging opts ko
   868  	if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" {
   869  		t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err)
   870  	}
   871  	// logging opts ok
   872  	_, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"})
   873  	if err != nil {
   874  		t.Fatal(err)
   875  	}
   876  	if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 {
   877  		t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy)
   878  	}
   879  }
   880  
   881  func TestParseEnvfileVariables(t *testing.T) {
   882  	e := "open nonexistent: no such file or directory"
   883  	if runtime.GOOS == "windows" {
   884  		e = "open nonexistent: The system cannot find the file specified."
   885  	}
   886  	// env ko
   887  	if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
   888  		t.Fatalf("Expected an error with message '%s', got %v", e, err)
   889  	}
   890  	// env ok
   891  	config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"})
   892  	if err != nil {
   893  		t.Fatal(err)
   894  	}
   895  	if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" {
   896  		t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env)
   897  	}
   898  	config, _, _, err = parseRun([]string{"--env-file=testdata/valid.env", "--env=ENV2=value2", "img", "cmd"})
   899  	if err != nil {
   900  		t.Fatal(err)
   901  	}
   902  	if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" {
   903  		t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env)
   904  	}
   905  }
   906  
   907  func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
   908  	// UTF8 with BOM
   909  	config, _, _, err := parseRun([]string{"--env-file=testdata/utf8.env", "img", "cmd"})
   910  	if err != nil {
   911  		t.Fatal(err)
   912  	}
   913  	env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"}
   914  	if len(config.Env) != len(env) {
   915  		t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env)
   916  	}
   917  	for i, v := range env {
   918  		if config.Env[i] != v {
   919  			t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i]))
   920  		}
   921  	}
   922  
   923  	// UTF16 with BOM
   924  	e := "contains invalid utf8 bytes at line"
   925  	if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
   926  		t.Fatalf("Expected an error with message '%s', got %v", e, err)
   927  	}
   928  	// UTF16BE with BOM
   929  	if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
   930  		t.Fatalf("Expected an error with message '%s', got %v", e, err)
   931  	}
   932  }
   933  
   934  func TestParseLabelfileVariables(t *testing.T) {
   935  	e := "open nonexistent: no such file or directory"
   936  	if runtime.GOOS == "windows" {
   937  		e = "open nonexistent: The system cannot find the file specified."
   938  	}
   939  	// label ko
   940  	if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
   941  		t.Fatalf("Expected an error with message '%s', got %v", e, err)
   942  	}
   943  	// label ok
   944  	config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"})
   945  	if err != nil {
   946  		t.Fatal(err)
   947  	}
   948  	if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" {
   949  		t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels)
   950  	}
   951  	config, _, _, err = parseRun([]string{"--label-file=testdata/valid.label", "--label=LABEL2=value2", "img", "cmd"})
   952  	if err != nil {
   953  		t.Fatal(err)
   954  	}
   955  	if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" {
   956  		t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels)
   957  	}
   958  }
   959  
   960  func TestParseEntryPoint(t *testing.T) {
   961  	config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"})
   962  	if err != nil {
   963  		t.Fatal(err)
   964  	}
   965  	if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" {
   966  		t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint)
   967  	}
   968  }
   969  
   970  func TestValidateDevice(t *testing.T) {
   971  	skip.If(t, runtime.GOOS != "linux") // Windows and macOS validate server-side
   972  	valid := []string{
   973  		"/home",
   974  		"/home:/home",
   975  		"/home:/something/else",
   976  		"/with space",
   977  		"/home:/with space",
   978  		"relative:/absolute-path",
   979  		"hostPath:/containerPath:r",
   980  		"/hostPath:/containerPath:rw",
   981  		"/hostPath:/containerPath:mrw",
   982  	}
   983  	invalid := map[string]string{
   984  		"":        "bad format for path: ",
   985  		"./":      "./ is not an absolute path",
   986  		"../":     "../ is not an absolute path",
   987  		"/:../":   "../ is not an absolute path",
   988  		"/:path":  "path is not an absolute path",
   989  		":":       "bad format for path: :",
   990  		"/tmp:":   " is not an absolute path",
   991  		":test":   "bad format for path: :test",
   992  		":/test":  "bad format for path: :/test",
   993  		"tmp:":    " is not an absolute path",
   994  		":test:":  "bad format for path: :test:",
   995  		"::":      "bad format for path: ::",
   996  		":::":     "bad format for path: :::",
   997  		"/tmp:::": "bad format for path: /tmp:::",
   998  		":/tmp::": "bad format for path: :/tmp::",
   999  		"path:ro": "ro is not an absolute path",
  1000  		"path:rr": "rr is not an absolute path",
  1001  		"a:/b:ro": "bad mode specified: ro",
  1002  		"a:/b:rr": "bad mode specified: rr",
  1003  	}
  1004  
  1005  	for _, path := range valid {
  1006  		if _, err := validateDevice(path, runtime.GOOS); err != nil {
  1007  			t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err)
  1008  		}
  1009  	}
  1010  
  1011  	for path, expectedError := range invalid {
  1012  		if _, err := validateDevice(path, runtime.GOOS); err == nil {
  1013  			t.Fatalf("ValidateDevice(`%q`) should have failed validation", path)
  1014  		} else if err.Error() != expectedError {
  1015  			t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error())
  1016  		}
  1017  	}
  1018  }
  1019  
  1020  func TestParseSystemPaths(t *testing.T) {
  1021  	tests := []struct {
  1022  		doc                       string
  1023  		in, out, masked, readonly []string
  1024  	}{
  1025  		{
  1026  			doc: "not set",
  1027  			in:  []string{},
  1028  			out: []string{},
  1029  		},
  1030  		{
  1031  			doc: "not set, preserve other options",
  1032  			in: []string{
  1033  				"seccomp=unconfined",
  1034  				"apparmor=unconfined",
  1035  				"label=user:USER",
  1036  				"foo=bar",
  1037  			},
  1038  			out: []string{
  1039  				"seccomp=unconfined",
  1040  				"apparmor=unconfined",
  1041  				"label=user:USER",
  1042  				"foo=bar",
  1043  			},
  1044  		},
  1045  		{
  1046  			doc:      "unconfined",
  1047  			in:       []string{"systempaths=unconfined"},
  1048  			out:      []string{},
  1049  			masked:   []string{},
  1050  			readonly: []string{},
  1051  		},
  1052  		{
  1053  			doc:      "unconfined and other options",
  1054  			in:       []string{"foo=bar", "bar=baz", "systempaths=unconfined"},
  1055  			out:      []string{"foo=bar", "bar=baz"},
  1056  			masked:   []string{},
  1057  			readonly: []string{},
  1058  		},
  1059  		{
  1060  			doc: "unknown option",
  1061  			in:  []string{"foo=bar", "systempaths=unknown", "bar=baz"},
  1062  			out: []string{"foo=bar", "systempaths=unknown", "bar=baz"},
  1063  		},
  1064  	}
  1065  
  1066  	for _, tc := range tests {
  1067  		securityOpts, maskedPaths, readonlyPaths := parseSystemPaths(tc.in)
  1068  		assert.DeepEqual(t, securityOpts, tc.out)
  1069  		assert.DeepEqual(t, maskedPaths, tc.masked)
  1070  		assert.DeepEqual(t, readonlyPaths, tc.readonly)
  1071  	}
  1072  }
  1073  
  1074  func TestConvertToStandardNotation(t *testing.T) {
  1075  	valid := map[string][]string{
  1076  		"20:10/tcp":               {"target=10,published=20"},
  1077  		"40:30":                   {"40:30"},
  1078  		"20:20 80:4444":           {"20:20", "80:4444"},
  1079  		"1500:2500/tcp 1400:1300": {"target=2500,published=1500", "1400:1300"},
  1080  		"1500:200/tcp 90:80/tcp":  {"published=1500,target=200", "target=80,published=90"},
  1081  	}
  1082  
  1083  	invalid := [][]string{
  1084  		{"published=1500,target:444"},
  1085  		{"published=1500,444"},
  1086  		{"published=1500,target,444"},
  1087  	}
  1088  
  1089  	for key, ports := range valid {
  1090  		convertedPorts, err := convertToStandardNotation(ports)
  1091  		if err != nil {
  1092  			assert.NilError(t, err)
  1093  		}
  1094  		assert.DeepEqual(t, strings.Split(key, " "), convertedPorts)
  1095  	}
  1096  
  1097  	for _, ports := range invalid {
  1098  		if _, err := convertToStandardNotation(ports); err == nil {
  1099  			t.Fatalf("ConvertToStandardNotation(`%q`) should have failed conversion", ports)
  1100  		}
  1101  	}
  1102  }