github.com/endophage/docker@v1.4.2-0.20161027011718-242853499895/runconfig/opts/parse_test.go (about)

     1  package opts
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"runtime"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/docker/docker/api/types/container"
    15  	networktypes "github.com/docker/docker/api/types/network"
    16  	"github.com/docker/docker/runconfig"
    17  	"github.com/docker/go-connections/nat"
    18  	"github.com/spf13/pflag"
    19  )
    20  
    21  func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) {
    22  	flags := pflag.NewFlagSet("run", pflag.ContinueOnError)
    23  	flags.SetOutput(ioutil.Discard)
    24  	flags.Usage = nil
    25  	copts := AddFlags(flags)
    26  	if err := flags.Parse(args); err != nil {
    27  		return nil, nil, nil, err
    28  	}
    29  	return Parse(flags, copts)
    30  }
    31  
    32  func parse(t *testing.T, args string) (*container.Config, *container.HostConfig, error) {
    33  	config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " "))
    34  	return config, hostConfig, err
    35  }
    36  
    37  func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) {
    38  	config, hostConfig, err := parse(t, args)
    39  	if err != nil {
    40  		t.Fatal(err)
    41  	}
    42  	return config, hostConfig
    43  }
    44  
    45  func TestParseRunLinks(t *testing.T) {
    46  	if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" {
    47  		t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links)
    48  	}
    49  	if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" {
    50  		t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links)
    51  	}
    52  	if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 {
    53  		t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links)
    54  	}
    55  }
    56  
    57  func TestParseRunAttach(t *testing.T) {
    58  	if config, _ := mustParse(t, "-a stdin"); !config.AttachStdin || config.AttachStdout || config.AttachStderr {
    59  		t.Fatalf("Error parsing attach flags. Expect only Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
    60  	}
    61  	if config, _ := mustParse(t, "-a stdin -a stdout"); !config.AttachStdin || !config.AttachStdout || config.AttachStderr {
    62  		t.Fatalf("Error parsing attach flags. Expect only Stdin and Stdout enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
    63  	}
    64  	if config, _ := mustParse(t, "-a stdin -a stdout -a stderr"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr {
    65  		t.Fatalf("Error parsing attach flags. Expect all attach enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
    66  	}
    67  	if config, _ := mustParse(t, ""); config.AttachStdin || !config.AttachStdout || !config.AttachStderr {
    68  		t.Fatalf("Error parsing attach flags. Expect Stdin disabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
    69  	}
    70  	if config, _ := mustParse(t, "-i"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr {
    71  		t.Fatalf("Error parsing attach flags. Expect Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
    72  	}
    73  
    74  	if _, _, err := parse(t, "-a"); err == nil {
    75  		t.Fatalf("Error parsing attach flags, `-a` should be an error but is not")
    76  	}
    77  	if _, _, err := parse(t, "-a invalid"); err == nil {
    78  		t.Fatalf("Error parsing attach flags, `-a invalid` should be an error but is not")
    79  	}
    80  	if _, _, err := parse(t, "-a invalid -a stdout"); err == nil {
    81  		t.Fatalf("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not")
    82  	}
    83  	if _, _, err := parse(t, "-a stdout -a stderr -d"); err == nil {
    84  		t.Fatalf("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not")
    85  	}
    86  	if _, _, err := parse(t, "-a stdin -d"); err == nil {
    87  		t.Fatalf("Error parsing attach flags, `-a stdin -d` should be an error but is not")
    88  	}
    89  	if _, _, err := parse(t, "-a stdout -d"); err == nil {
    90  		t.Fatalf("Error parsing attach flags, `-a stdout -d` should be an error but is not")
    91  	}
    92  	if _, _, err := parse(t, "-a stderr -d"); err == nil {
    93  		t.Fatalf("Error parsing attach flags, `-a stderr -d` should be an error but is not")
    94  	}
    95  	if _, _, err := parse(t, "-d --rm"); err == nil {
    96  		t.Fatalf("Error parsing attach flags, `-d --rm` should be an error but is not")
    97  	}
    98  }
    99  
   100  func TestParseRunVolumes(t *testing.T) {
   101  
   102  	// A single volume
   103  	arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
   104  	if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
   105  		t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
   106  	} else if _, exists := config.Volumes[arr[0]]; !exists {
   107  		t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes)
   108  	}
   109  
   110  	// Two volumes
   111  	arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`})
   112  	if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
   113  		t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
   114  	} else if _, exists := config.Volumes[arr[0]]; !exists {
   115  		t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes)
   116  	} else if _, exists := config.Volumes[arr[1]]; !exists {
   117  		t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes)
   118  	}
   119  
   120  	// A single bind-mount
   121  	arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
   122  	if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
   123  		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)
   124  	}
   125  
   126  	// Two bind-mounts.
   127  	arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
   128  	if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
   129  		t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
   130  	}
   131  
   132  	// Two bind-mounts, first read-only, second read-write.
   133  	// TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4
   134  	arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`})
   135  	if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
   136  		t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
   137  	}
   138  
   139  	// Similar to previous test but with alternate modes which are only supported by Linux
   140  	if runtime.GOOS != "windows" {
   141  		arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{})
   142  		if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
   143  			t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
   144  		}
   145  
   146  		arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{})
   147  		if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
   148  			t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
   149  		}
   150  	}
   151  
   152  	// One bind mount and one volume
   153  	arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`})
   154  	if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] {
   155  		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)
   156  	} else if _, exists := config.Volumes[arr[1]]; !exists {
   157  		t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes)
   158  	}
   159  
   160  	// Root to non-c: drive letter (Windows specific)
   161  	if runtime.GOOS == "windows" {
   162  		arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`})
   163  		if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 {
   164  			t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
   165  		}
   166  	}
   167  
   168  }
   169  
   170  // This tests the cases for binds which are generated through
   171  // DecodeContainerConfig rather than Parse()
   172  func TestDecodeContainerConfigVolumes(t *testing.T) {
   173  
   174  	// Root to root
   175  	bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`})
   176  	if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   177  		t.Fatalf("binds %v should have failed", bindsOrVols)
   178  	}
   179  	if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   180  		t.Fatalf("volume %v should have failed", bindsOrVols)
   181  	}
   182  
   183  	// No destination path
   184  	bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`})
   185  	if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   186  		t.Fatalf("binds %v should have failed", bindsOrVols)
   187  	}
   188  	if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   189  		t.Fatalf("volume %v should have failed", bindsOrVols)
   190  	}
   191  
   192  	//	// No destination path or mode
   193  	bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`})
   194  	if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   195  		t.Fatalf("binds %v should have failed", bindsOrVols)
   196  	}
   197  	if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   198  		t.Fatalf("volume %v should have failed", bindsOrVols)
   199  	}
   200  
   201  	// A whole lot of nothing
   202  	bindsOrVols = []string{`:`}
   203  	if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   204  		t.Fatalf("binds %v should have failed", bindsOrVols)
   205  	}
   206  	if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   207  		t.Fatalf("volume %v should have failed", bindsOrVols)
   208  	}
   209  
   210  	// A whole lot of nothing with no mode
   211  	bindsOrVols = []string{`::`}
   212  	if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   213  		t.Fatalf("binds %v should have failed", bindsOrVols)
   214  	}
   215  	if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   216  		t.Fatalf("volume %v should have failed", bindsOrVols)
   217  	}
   218  
   219  	// Too much including an invalid mode
   220  	wTmp := os.Getenv("TEMP")
   221  	bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp})
   222  	if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   223  		t.Fatalf("binds %v should have failed", bindsOrVols)
   224  	}
   225  	if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   226  		t.Fatalf("volume %v should have failed", bindsOrVols)
   227  	}
   228  
   229  	// Windows specific error tests
   230  	if runtime.GOOS == "windows" {
   231  		// Volume which does not include a drive letter
   232  		bindsOrVols = []string{`\tmp`}
   233  		if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   234  			t.Fatalf("binds %v should have failed", bindsOrVols)
   235  		}
   236  		if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   237  			t.Fatalf("volume %v should have failed", bindsOrVols)
   238  		}
   239  
   240  		// Root to C-Drive
   241  		bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`}
   242  		if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   243  			t.Fatalf("binds %v should have failed", bindsOrVols)
   244  		}
   245  		if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   246  			t.Fatalf("volume %v should have failed", bindsOrVols)
   247  		}
   248  
   249  		// Container path that does not include a drive letter
   250  		bindsOrVols = []string{`c:\windows:\somewhere`}
   251  		if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   252  			t.Fatalf("binds %v should have failed", bindsOrVols)
   253  		}
   254  		if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   255  			t.Fatalf("volume %v should have failed", bindsOrVols)
   256  		}
   257  	}
   258  
   259  	// Linux-specific error tests
   260  	if runtime.GOOS != "windows" {
   261  		// Just root
   262  		bindsOrVols = []string{`/`}
   263  		if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
   264  			t.Fatalf("binds %v should have failed", bindsOrVols)
   265  		}
   266  		if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
   267  			t.Fatalf("volume %v should have failed", bindsOrVols)
   268  		}
   269  
   270  		// A single volume that looks like a bind mount passed in Volumes.
   271  		// This should be handled as a bind mount, not a volume.
   272  		vols := []string{`/foo:/bar`}
   273  		if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil {
   274  			t.Fatal("Volume /foo:/bar should have succeeded as a volume name")
   275  		} else if hostConfig.Binds != nil {
   276  			t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds)
   277  		} else if _, exists := config.Volumes[vols[0]]; !exists {
   278  			t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes)
   279  		}
   280  
   281  	}
   282  }
   283  
   284  // callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes
   285  // to call DecodeContainerConfig. It effectively does what a client would
   286  // do when calling the daemon by constructing a JSON stream of a
   287  // ContainerConfigWrapper which is populated by the set of volume specs
   288  // passed into it. It returns a config and a hostconfig which can be
   289  // validated to ensure DecodeContainerConfig has manipulated the structures
   290  // correctly.
   291  func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) {
   292  	var (
   293  		b   []byte
   294  		err error
   295  		c   *container.Config
   296  		h   *container.HostConfig
   297  	)
   298  	w := runconfig.ContainerConfigWrapper{
   299  		Config: &container.Config{
   300  			Volumes: map[string]struct{}{},
   301  		},
   302  		HostConfig: &container.HostConfig{
   303  			NetworkMode: "none",
   304  			Binds:       binds,
   305  		},
   306  	}
   307  	for _, v := range volumes {
   308  		w.Config.Volumes[v] = struct{}{}
   309  	}
   310  	if b, err = json.Marshal(w); err != nil {
   311  		return nil, nil, fmt.Errorf("Error on marshal %s", err.Error())
   312  	}
   313  	c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b))
   314  	if err != nil {
   315  		return nil, nil, fmt.Errorf("Error parsing %s: %v", string(b), err)
   316  	}
   317  	if c == nil || h == nil {
   318  		return nil, nil, fmt.Errorf("Empty config or hostconfig")
   319  	}
   320  
   321  	return c, h, err
   322  }
   323  
   324  // check if (a == c && b == d) || (a == d && b == c)
   325  // because maps are randomized
   326  func compareRandomizedStrings(a, b, c, d string) error {
   327  	if a == c && b == d {
   328  		return nil
   329  	}
   330  	if a == d && b == c {
   331  		return nil
   332  	}
   333  	return fmt.Errorf("strings don't match")
   334  }
   335  
   336  // setupPlatformVolume takes two arrays of volume specs - a Unix style
   337  // spec and a Windows style spec. Depending on the platform being unit tested,
   338  // it returns one of them, along with a volume string that would be passed
   339  // on the docker CLI (eg -v /bar -v /foo).
   340  func setupPlatformVolume(u []string, w []string) ([]string, string) {
   341  	var a []string
   342  	if runtime.GOOS == "windows" {
   343  		a = w
   344  	} else {
   345  		a = u
   346  	}
   347  	s := ""
   348  	for _, v := range a {
   349  		s = s + "-v " + v + " "
   350  	}
   351  	return a, s
   352  }
   353  
   354  // Simple parse with MacAddress validation
   355  func TestParseWithMacAddress(t *testing.T) {
   356  	invalidMacAddress := "--mac-address=invalidMacAddress"
   357  	validMacAddress := "--mac-address=92:d0:c6:0a:29:33"
   358  	if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
   359  		t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
   360  	}
   361  	if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" {
   362  		t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress)
   363  	}
   364  }
   365  
   366  func TestParseWithMemory(t *testing.T) {
   367  	invalidMemory := "--memory=invalid"
   368  	validMemory := "--memory=1G"
   369  	if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err != nil && err.Error() != "invalid size: 'invalid'" {
   370  		t.Fatalf("Expected an error with '%v' Memory, got '%v'", invalidMemory, err)
   371  	}
   372  	if _, hostconfig := mustParse(t, validMemory); hostconfig.Memory != 1073741824 {
   373  		t.Fatalf("Expected the config to have '1G' as Memory, got '%v'", hostconfig.Memory)
   374  	}
   375  }
   376  
   377  func TestParseWithMemorySwap(t *testing.T) {
   378  	invalidMemory := "--memory-swap=invalid"
   379  	validMemory := "--memory-swap=1G"
   380  	anotherValidMemory := "--memory-swap=-1"
   381  	if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err == nil || err.Error() != "invalid size: 'invalid'" {
   382  		t.Fatalf("Expected an error with '%v' MemorySwap, got '%v'", invalidMemory, err)
   383  	}
   384  	if _, hostconfig := mustParse(t, validMemory); hostconfig.MemorySwap != 1073741824 {
   385  		t.Fatalf("Expected the config to have '1073741824' as MemorySwap, got '%v'", hostconfig.MemorySwap)
   386  	}
   387  	if _, hostconfig := mustParse(t, anotherValidMemory); hostconfig.MemorySwap != -1 {
   388  		t.Fatalf("Expected the config to have '-1' as MemorySwap, got '%v'", hostconfig.MemorySwap)
   389  	}
   390  }
   391  
   392  func TestParseHostname(t *testing.T) {
   393  	validHostnames := map[string]string{
   394  		"hostname":    "hostname",
   395  		"host-name":   "host-name",
   396  		"hostname123": "hostname123",
   397  		"123hostname": "123hostname",
   398  		"hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error",
   399  	}
   400  	hostnameWithDomain := "--hostname=hostname.domainname"
   401  	hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
   402  	for hostname, expectedHostname := range validHostnames {
   403  		if config, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname {
   404  			t.Fatalf("Expected the config to have 'hostname' as hostname, got '%v'", config.Hostname)
   405  		}
   406  	}
   407  	if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" && config.Domainname != "" {
   408  		t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got '%v'", config.Hostname)
   409  	}
   410  	if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" && config.Domainname != "" {
   411  		t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got '%v'", config.Hostname)
   412  	}
   413  }
   414  
   415  func TestParseWithExpose(t *testing.T) {
   416  	invalids := map[string]string{
   417  		":":                   "invalid port format for --expose: :",
   418  		"8080:9090":           "invalid port format for --expose: 8080:9090",
   419  		"/tcp":                "invalid range format for --expose: /tcp, error: Empty string specified for ports.",
   420  		"/udp":                "invalid range format for --expose: /udp, error: Empty string specified for ports.",
   421  		"NaN/tcp":             `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
   422  		"NaN-NaN/tcp":         `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
   423  		"8080-NaN/tcp":        `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
   424  		"1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`,
   425  	}
   426  	valids := map[string][]nat.Port{
   427  		"8080/tcp":      {"8080/tcp"},
   428  		"8080/udp":      {"8080/udp"},
   429  		"8080/ncp":      {"8080/ncp"},
   430  		"8080-8080/udp": {"8080/udp"},
   431  		"8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"},
   432  	}
   433  	for expose, expectedError := range invalids {
   434  		if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError {
   435  			t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err)
   436  		}
   437  	}
   438  	for expose, exposedPorts := range valids {
   439  		config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
   440  		if err != nil {
   441  			t.Fatal(err)
   442  		}
   443  		if len(config.ExposedPorts) != len(exposedPorts) {
   444  			t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts))
   445  		}
   446  		for _, port := range exposedPorts {
   447  			if _, ok := config.ExposedPorts[port]; !ok {
   448  				t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts)
   449  			}
   450  		}
   451  	}
   452  	// Merge with actual published port
   453  	config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"})
   454  	if err != nil {
   455  		t.Fatal(err)
   456  	}
   457  	if len(config.ExposedPorts) != 2 {
   458  		t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts)
   459  	}
   460  	ports := []nat.Port{"80/tcp", "81/tcp"}
   461  	for _, port := range ports {
   462  		if _, ok := config.ExposedPorts[port]; !ok {
   463  			t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts)
   464  		}
   465  	}
   466  }
   467  
   468  func TestParseDevice(t *testing.T) {
   469  	valids := map[string]container.DeviceMapping{
   470  		"/dev/snd": {
   471  			PathOnHost:        "/dev/snd",
   472  			PathInContainer:   "/dev/snd",
   473  			CgroupPermissions: "rwm",
   474  		},
   475  		"/dev/snd:rw": {
   476  			PathOnHost:        "/dev/snd",
   477  			PathInContainer:   "/dev/snd",
   478  			CgroupPermissions: "rw",
   479  		},
   480  		"/dev/snd:/something": {
   481  			PathOnHost:        "/dev/snd",
   482  			PathInContainer:   "/something",
   483  			CgroupPermissions: "rwm",
   484  		},
   485  		"/dev/snd:/something:rw": {
   486  			PathOnHost:        "/dev/snd",
   487  			PathInContainer:   "/something",
   488  			CgroupPermissions: "rw",
   489  		},
   490  	}
   491  	for device, deviceMapping := range valids {
   492  		_, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"})
   493  		if err != nil {
   494  			t.Fatal(err)
   495  		}
   496  		if len(hostconfig.Devices) != 1 {
   497  			t.Fatalf("Expected 1 devices, got %v", hostconfig.Devices)
   498  		}
   499  		if hostconfig.Devices[0] != deviceMapping {
   500  			t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices)
   501  		}
   502  	}
   503  
   504  }
   505  
   506  func TestParseModes(t *testing.T) {
   507  	// ipc ko
   508  	if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" {
   509  		t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err)
   510  	}
   511  	// ipc ok
   512  	_, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"})
   513  	if err != nil {
   514  		t.Fatal(err)
   515  	}
   516  	if !hostconfig.IpcMode.Valid() {
   517  		t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode)
   518  	}
   519  	// pid ko
   520  	if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" {
   521  		t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err)
   522  	}
   523  	// pid ok
   524  	_, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"})
   525  	if err != nil {
   526  		t.Fatal(err)
   527  	}
   528  	if !hostconfig.PidMode.Valid() {
   529  		t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode)
   530  	}
   531  	// uts ko
   532  	if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" {
   533  		t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err)
   534  	}
   535  	// uts ok
   536  	_, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"})
   537  	if err != nil {
   538  		t.Fatal(err)
   539  	}
   540  	if !hostconfig.UTSMode.Valid() {
   541  		t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode)
   542  	}
   543  	// shm-size ko
   544  	if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != "invalid size: 'a128m'" {
   545  		t.Fatalf("Expected an error with message 'invalid size: a128m', got %v", err)
   546  	}
   547  	// shm-size ok
   548  	_, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"})
   549  	if err != nil {
   550  		t.Fatal(err)
   551  	}
   552  	if hostconfig.ShmSize != 134217728 {
   553  		t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize)
   554  	}
   555  }
   556  
   557  func TestParseRestartPolicy(t *testing.T) {
   558  	invalids := map[string]string{
   559  		"always:2:3":         "invalid restart policy format",
   560  		"on-failure:invalid": "maximum retry count must be an integer",
   561  	}
   562  	valids := map[string]container.RestartPolicy{
   563  		"": {},
   564  		"always": {
   565  			Name:              "always",
   566  			MaximumRetryCount: 0,
   567  		},
   568  		"on-failure:1": {
   569  			Name:              "on-failure",
   570  			MaximumRetryCount: 1,
   571  		},
   572  	}
   573  	for restart, expectedError := range invalids {
   574  		if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError {
   575  			t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err)
   576  		}
   577  	}
   578  	for restart, expected := range valids {
   579  		_, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"})
   580  		if err != nil {
   581  			t.Fatal(err)
   582  		}
   583  		if hostconfig.RestartPolicy != expected {
   584  			t.Fatalf("Expected %v, got %v", expected, hostconfig.RestartPolicy)
   585  		}
   586  	}
   587  }
   588  
   589  func TestParseHealth(t *testing.T) {
   590  	checkOk := func(args ...string) *container.HealthConfig {
   591  		config, _, _, err := parseRun(args)
   592  		if err != nil {
   593  			t.Fatalf("%#v: %v", args, err)
   594  		}
   595  		return config.Healthcheck
   596  	}
   597  	checkError := func(expected string, args ...string) {
   598  		config, _, _, err := parseRun(args)
   599  		if err == nil {
   600  			t.Fatalf("Expected error, but got %#v", config)
   601  		}
   602  		if err.Error() != expected {
   603  			t.Fatalf("Expected %#v, got %#v", expected, err)
   604  		}
   605  	}
   606  	health := checkOk("--no-healthcheck", "img", "cmd")
   607  	if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" {
   608  		t.Fatalf("--no-healthcheck failed: %#v", health)
   609  	}
   610  
   611  	health = checkOk("--health-cmd=/check.sh -q", "img", "cmd")
   612  	if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" {
   613  		t.Fatalf("--health-cmd: got %#v", health.Test)
   614  	}
   615  	if health.Timeout != 0 {
   616  		t.Fatalf("--health-cmd: timeout = %f", health.Timeout)
   617  	}
   618  
   619  	checkError("--no-healthcheck conflicts with --health-* options",
   620  		"--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd")
   621  
   622  	health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "img", "cmd")
   623  	if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond {
   624  		t.Fatalf("--health-*: got %#v", health)
   625  	}
   626  }
   627  
   628  func TestParseLoggingOpts(t *testing.T) {
   629  	// logging opts ko
   630  	if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" {
   631  		t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err)
   632  	}
   633  	// logging opts ok
   634  	_, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"})
   635  	if err != nil {
   636  		t.Fatal(err)
   637  	}
   638  	if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 {
   639  		t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy)
   640  	}
   641  }
   642  
   643  func TestParseEnvfileVariables(t *testing.T) {
   644  	e := "open nonexistent: no such file or directory"
   645  	if runtime.GOOS == "windows" {
   646  		e = "open nonexistent: The system cannot find the file specified."
   647  	}
   648  	// env ko
   649  	if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
   650  		t.Fatalf("Expected an error with message '%s', got %v", e, err)
   651  	}
   652  	// env ok
   653  	config, _, _, err := parseRun([]string{"--env-file=fixtures/valid.env", "img", "cmd"})
   654  	if err != nil {
   655  		t.Fatal(err)
   656  	}
   657  	if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" {
   658  		t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env)
   659  	}
   660  	config, _, _, err = parseRun([]string{"--env-file=fixtures/valid.env", "--env=ENV2=value2", "img", "cmd"})
   661  	if err != nil {
   662  		t.Fatal(err)
   663  	}
   664  	if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" {
   665  		t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env)
   666  	}
   667  }
   668  
   669  func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
   670  	// UTF8 with BOM
   671  	config, _, _, err := parseRun([]string{"--env-file=fixtures/utf8.env", "img", "cmd"})
   672  	if err != nil {
   673  		t.Fatal(err)
   674  	}
   675  	env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"}
   676  	if len(config.Env) != len(env) {
   677  		t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env)
   678  	}
   679  	for i, v := range env {
   680  		if config.Env[i] != v {
   681  			t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i]))
   682  		}
   683  	}
   684  
   685  	// UTF16 with BOM
   686  	e := "contains invalid utf8 bytes at line"
   687  	if _, _, _, err := parseRun([]string{"--env-file=fixtures/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
   688  		t.Fatalf("Expected an error with message '%s', got %v", e, err)
   689  	}
   690  	// UTF16BE with BOM
   691  	if _, _, _, err := parseRun([]string{"--env-file=fixtures/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
   692  		t.Fatalf("Expected an error with message '%s', got %v", e, err)
   693  	}
   694  }
   695  
   696  func TestParseLabelfileVariables(t *testing.T) {
   697  	e := "open nonexistent: no such file or directory"
   698  	if runtime.GOOS == "windows" {
   699  		e = "open nonexistent: The system cannot find the file specified."
   700  	}
   701  	// label ko
   702  	if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
   703  		t.Fatalf("Expected an error with message '%s', got %v", e, err)
   704  	}
   705  	// label ok
   706  	config, _, _, err := parseRun([]string{"--label-file=fixtures/valid.label", "img", "cmd"})
   707  	if err != nil {
   708  		t.Fatal(err)
   709  	}
   710  	if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" {
   711  		t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels)
   712  	}
   713  	config, _, _, err = parseRun([]string{"--label-file=fixtures/valid.label", "--label=LABEL2=value2", "img", "cmd"})
   714  	if err != nil {
   715  		t.Fatal(err)
   716  	}
   717  	if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" {
   718  		t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels)
   719  	}
   720  }
   721  
   722  func TestParseEntryPoint(t *testing.T) {
   723  	config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"})
   724  	if err != nil {
   725  		t.Fatal(err)
   726  	}
   727  	if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" {
   728  		t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint)
   729  	}
   730  }
   731  
   732  func TestValidateLink(t *testing.T) {
   733  	valid := []string{
   734  		"name",
   735  		"dcdfbe62ecd0:alias",
   736  		"7a67485460b7642516a4ad82ecefe7f57d0c4916f530561b71a50a3f9c4e33da",
   737  		"angry_torvalds:linus",
   738  	}
   739  	invalid := map[string]string{
   740  		"":               "empty string specified for links",
   741  		"too:much:of:it": "bad format for links: too:much:of:it",
   742  	}
   743  
   744  	for _, link := range valid {
   745  		if _, err := ValidateLink(link); err != nil {
   746  			t.Fatalf("ValidateLink(`%q`) should succeed: error %q", link, err)
   747  		}
   748  	}
   749  
   750  	for link, expectedError := range invalid {
   751  		if _, err := ValidateLink(link); err == nil {
   752  			t.Fatalf("ValidateLink(`%q`) should have failed validation", link)
   753  		} else {
   754  			if !strings.Contains(err.Error(), expectedError) {
   755  				t.Fatalf("ValidateLink(`%q`) error should contain %q", link, expectedError)
   756  			}
   757  		}
   758  	}
   759  }
   760  
   761  func TestParseLink(t *testing.T) {
   762  	name, alias, err := ParseLink("name:alias")
   763  	if err != nil {
   764  		t.Fatalf("Expected not to error out on a valid name:alias format but got: %v", err)
   765  	}
   766  	if name != "name" {
   767  		t.Fatalf("Link name should have been name, got %s instead", name)
   768  	}
   769  	if alias != "alias" {
   770  		t.Fatalf("Link alias should have been alias, got %s instead", alias)
   771  	}
   772  	// short format definition
   773  	name, alias, err = ParseLink("name")
   774  	if err != nil {
   775  		t.Fatalf("Expected not to error out on a valid name only format but got: %v", err)
   776  	}
   777  	if name != "name" {
   778  		t.Fatalf("Link name should have been name, got %s instead", name)
   779  	}
   780  	if alias != "name" {
   781  		t.Fatalf("Link alias should have been name, got %s instead", alias)
   782  	}
   783  	// empty string link definition is not allowed
   784  	if _, _, err := ParseLink(""); err == nil || !strings.Contains(err.Error(), "empty string specified for links") {
   785  		t.Fatalf("Expected error 'empty string specified for links' but got: %v", err)
   786  	}
   787  	// more than two colons are not allowed
   788  	if _, _, err := ParseLink("link:alias:wrong"); err == nil || !strings.Contains(err.Error(), "bad format for links: link:alias:wrong") {
   789  		t.Fatalf("Expected error 'bad format for links: link:alias:wrong' but got: %v", err)
   790  	}
   791  }
   792  
   793  func TestValidateDevice(t *testing.T) {
   794  	valid := []string{
   795  		"/home",
   796  		"/home:/home",
   797  		"/home:/something/else",
   798  		"/with space",
   799  		"/home:/with space",
   800  		"relative:/absolute-path",
   801  		"hostPath:/containerPath:r",
   802  		"/hostPath:/containerPath:rw",
   803  		"/hostPath:/containerPath:mrw",
   804  	}
   805  	invalid := map[string]string{
   806  		"":        "bad format for path: ",
   807  		"./":      "./ is not an absolute path",
   808  		"../":     "../ is not an absolute path",
   809  		"/:../":   "../ is not an absolute path",
   810  		"/:path":  "path is not an absolute path",
   811  		":":       "bad format for path: :",
   812  		"/tmp:":   " is not an absolute path",
   813  		":test":   "bad format for path: :test",
   814  		":/test":  "bad format for path: :/test",
   815  		"tmp:":    " is not an absolute path",
   816  		":test:":  "bad format for path: :test:",
   817  		"::":      "bad format for path: ::",
   818  		":::":     "bad format for path: :::",
   819  		"/tmp:::": "bad format for path: /tmp:::",
   820  		":/tmp::": "bad format for path: :/tmp::",
   821  		"path:ro": "ro is not an absolute path",
   822  		"path:rr": "rr is not an absolute path",
   823  		"a:/b:ro": "bad mode specified: ro",
   824  		"a:/b:rr": "bad mode specified: rr",
   825  	}
   826  
   827  	for _, path := range valid {
   828  		if _, err := ValidateDevice(path); err != nil {
   829  			t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err)
   830  		}
   831  	}
   832  
   833  	for path, expectedError := range invalid {
   834  		if _, err := ValidateDevice(path); err == nil {
   835  			t.Fatalf("ValidateDevice(`%q`) should have failed validation", path)
   836  		} else {
   837  			if err.Error() != expectedError {
   838  				t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error())
   839  			}
   840  		}
   841  	}
   842  }
   843  
   844  func TestVolumeSplitN(t *testing.T) {
   845  	for _, x := range []struct {
   846  		input    string
   847  		n        int
   848  		expected []string
   849  	}{
   850  		{`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}},
   851  		{`:C:\foo:d:`, -1, nil},
   852  		{`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}},
   853  		{`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}},
   854  		{`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}},
   855  
   856  		{`d:\`, -1, []string{`d:\`}},
   857  		{`d:`, -1, []string{`d:`}},
   858  		{`d:\path`, -1, []string{`d:\path`}},
   859  		{`d:\path with space`, -1, []string{`d:\path with space`}},
   860  		{`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}},
   861  		{`c:\:d:\`, -1, []string{`c:\`, `d:\`}},
   862  		{`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}},
   863  		{`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}},
   864  		{`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}},
   865  		{`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}},
   866  		{`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}},
   867  		{`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}},
   868  		{`name:D:`, -1, []string{`name`, `D:`}},
   869  		{`name:D::rW`, -1, []string{`name`, `D:`, `rW`}},
   870  		{`name:D::RW`, -1, []string{`name`, `D:`, `RW`}},
   871  		{`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}},
   872  		{`c:\Windows`, -1, []string{`c:\Windows`}},
   873  		{`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}},
   874  
   875  		{``, -1, nil},
   876  		{`.`, -1, []string{`.`}},
   877  		{`..\`, -1, []string{`..\`}},
   878  		{`c:\:..\`, -1, []string{`c:\`, `..\`}},
   879  		{`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}},
   880  
   881  		// Cover directories with one-character name
   882  		{`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}},
   883  	} {
   884  		res := volumeSplitN(x.input, x.n)
   885  		if len(res) < len(x.expected) {
   886  			t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
   887  		}
   888  		for i, e := range res {
   889  			if e != x.expected[i] {
   890  				t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
   891  			}
   892  		}
   893  	}
   894  }