Browse Source

Merge pull request #9688 from gyuho/fix-watch

etcdctl/ctlv3: fix watch with exec commands, interactive mode flag parsing
Gyuho Lee 7 years ago
parent
commit
14a2d0d54d

+ 1 - 0
.gitignore

@@ -9,6 +9,7 @@
 *.log
 *.log
 /etcd
 /etcd
 /hack/insta-discovery/.env
 /hack/insta-discovery/.env
+*.coverprofile
 *.test
 *.test
 hack/tls-setup/certs
 hack/tls-setup/certs
 .idea
 .idea

+ 3 - 0
Makefile

@@ -20,9 +20,12 @@ clean:
 	rm -f ./codecov
 	rm -f ./codecov
 	rm -rf ./agent-*
 	rm -rf ./agent-*
 	rm -rf ./covdir
 	rm -rf ./covdir
+	rm -f ./*.coverprofile
 	rm -f ./*.log
 	rm -f ./*.log
 	rm -f ./bin/Dockerfile-release
 	rm -f ./bin/Dockerfile-release
 	rm -rf ./bin/*.etcd
 	rm -rf ./bin/*.etcd
+	rm -rf ./default.etcd
+	rm -rf ./tests/e2e/default.etcd
 	rm -rf ./gopath
 	rm -rf ./gopath
 	rm -rf ./gopath.proto
 	rm -rf ./gopath.proto
 	rm -rf ./release
 	rm -rf ./release

+ 106 - 65
etcdctl/ctlv3/command/watch_command.go

@@ -179,118 +179,159 @@ func printWatchCh(c *clientv3.Client, ch clientv3.WatchChan, execArgs []string)
 // "--" characters are invalid arguments for "spf13/cobra" library,
 // "--" characters are invalid arguments for "spf13/cobra" library,
 // so no need to handle such cases.
 // so no need to handle such cases.
 func parseWatchArgs(osArgs, commandArgs []string, envKey, envRange string, interactive bool) (watchArgs []string, execArgs []string, err error) {
 func parseWatchArgs(osArgs, commandArgs []string, envKey, envRange string, interactive bool) (watchArgs []string, execArgs []string, err error) {
-	watchArgs = commandArgs
+	rawArgs := make([]string, len(osArgs))
+	copy(rawArgs, osArgs)
+	watchArgs = make([]string, len(commandArgs))
+	copy(watchArgs, commandArgs)
 
 
-	// remove preceding commands (e.g. "watch foo bar" in interactive mode)
-	idx := 0
-	for idx = range watchArgs {
-		if watchArgs[idx] == "watch" {
+	// remove preceding commands (e.g. ./bin/etcdctl watch)
+	// handle "./bin/etcdctl watch foo -- echo watch event"
+	for idx := range rawArgs {
+		if rawArgs[idx] == "watch" {
+			rawArgs = rawArgs[idx+1:]
 			break
 			break
 		}
 		}
 	}
 	}
-	if idx < len(watchArgs)-1 || envKey != "" {
-		if idx < len(watchArgs)-1 {
-			watchArgs = watchArgs[idx+1:]
+
+	// remove preceding commands (e.g. "watch foo bar" in interactive mode)
+	// handle "./bin/etcdctl watch foo -- echo watch event"
+	if interactive {
+		if watchArgs[0] != "watch" {
+			// "watch" not found
+			watchPrefix, watchRev, watchPrevKey = false, 0, false
+			return nil, nil, errBadArgsInteractiveWatch
 		}
 		}
+		watchArgs = watchArgs[1:]
+	}
 
 
-		execIdx, execExist := 0, false
-		for execIdx = range osArgs {
-			v := osArgs[execIdx]
-			if v == "--" && execIdx != len(osArgs)-1 {
+	execIdx, execExist := 0, false
+	if !interactive {
+		for execIdx = range rawArgs {
+			if rawArgs[execIdx] == "--" {
 				execExist = true
 				execExist = true
 				break
 				break
 			}
 			}
 		}
 		}
-
-		if idx == len(watchArgs)-1 && envKey != "" {
-			if len(watchArgs) > 0 && !interactive {
-				// "watch --rev 1 -- echo Hello World" has no conflict
-				if !execExist {
-					// "watch foo" with ETCDCTL_WATCH_KEY=foo
-					// (watchArgs==["foo"])
-					return nil, nil, errBadArgsNumConflictEnv
-				}
-			}
-			// otherwise, watch with no argument and environment key is set
-			// if interactive, first "watch" command string should be removed
-			if interactive {
-				watchArgs = []string{}
-			}
+		if execExist && execIdx == len(rawArgs)-1 {
+			// "watch foo bar --" should error
+			return nil, nil, errBadArgsNumSeparator
 		}
 		}
-
-		// "watch foo -- echo hello" with ETCDCTL_WATCH_KEY=foo
-		// (watchArgs==["foo","echo","hello"])
-		if envKey != "" && execExist {
-			widx, oidx := 0, len(osArgs)-1
-			for widx = len(watchArgs) - 1; widx >= 0; widx-- {
-				if watchArgs[widx] == osArgs[oidx] {
-					oidx--
+		// "watch" with no argument should error
+		if !execExist && len(rawArgs) < 1 && envKey == "" {
+			return nil, nil, errBadArgsNum
+		}
+		if execExist && envKey != "" {
+			// "ETCDCTL_WATCH_KEY=foo watch foo -- echo 1" should error
+			// (watchArgs==["foo","echo","1"])
+			widx, ridx := len(watchArgs)-1, len(rawArgs)-1
+			for ; widx >= 0; widx-- {
+				if watchArgs[widx] == rawArgs[ridx] {
+					ridx--
 					continue
 					continue
 				}
 				}
-				if oidx == execIdx { // watchArgs has extra
+				// watchArgs has extra:
+				// ETCDCTL_WATCH_KEY=foo watch foo  --  echo 1
+				// watchArgs:                       foo echo 1
+				if ridx == execIdx {
 					return nil, nil, errBadArgsNumConflictEnv
 					return nil, nil, errBadArgsNumConflictEnv
 				}
 				}
 			}
 			}
 		}
 		}
-	} else if interactive { // "watch" not found
-		return nil, nil, errBadArgsInteractiveWatch
-	}
-	if len(watchArgs) < 1 && envKey == "" {
-		return nil, nil, errBadArgsNum
-	}
+		// check conflicting arguments
+		// e.g. "watch --rev 1 -- echo Hello World" has no conflict
+		if !execExist && len(watchArgs) > 0 && envKey != "" {
+			// "ETCDCTL_WATCH_KEY=foo watch foo" should error
+			// (watchArgs==["foo"])
+			return nil, nil, errBadArgsNumConflictEnv
+		}
+	} else {
+		for execIdx = range watchArgs {
+			if watchArgs[execIdx] == "--" {
+				execExist = true
+				break
+			}
+		}
+		if execExist && execIdx == len(watchArgs)-1 {
+			// "watch foo bar --" should error
+			watchPrefix, watchRev, watchPrevKey = false, 0, false
+			return nil, nil, errBadArgsNumSeparator
+		}
 
 
-	// remove preceding commands (e.g. ./bin/etcdctl watch)
-	for idx = range osArgs {
-		if osArgs[idx] == "watch" {
-			break
+		flagset := NewWatchCommand().Flags()
+		if err := flagset.Parse(watchArgs); err != nil {
+			watchPrefix, watchRev, watchPrevKey = false, 0, false
+			return nil, nil, err
+		}
+		pArgs := flagset.Args()
+
+		// "watch" with no argument should error
+		if !execExist && envKey == "" && len(pArgs) < 1 {
+			watchPrefix, watchRev, watchPrevKey = false, 0, false
+			return nil, nil, errBadArgsNum
+		}
+		// check conflicting arguments
+		// e.g. "watch --rev 1 -- echo Hello World" has no conflict
+		if !execExist && len(pArgs) > 0 && envKey != "" {
+			// "ETCDCTL_WATCH_KEY=foo watch foo" should error
+			// (watchArgs==["foo"])
+			watchPrefix, watchRev, watchPrevKey = false, 0, false
+			return nil, nil, errBadArgsNumConflictEnv
 		}
 		}
-	}
-	if idx < len(osArgs)-1 {
-		osArgs = osArgs[idx+1:]
-	} else if envKey == "" {
-		return nil, nil, errBadArgsNum
 	}
 	}
 
 
-	argsWithSep := osArgs
-	if interactive { // interactive mode pass "--" to the command args
+	argsWithSep := rawArgs
+	if interactive {
+		// interactive mode directly passes "--" to the command args
 		argsWithSep = watchArgs
 		argsWithSep = watchArgs
 	}
 	}
-	foundSep := false
+
+	idx, foundSep := 0, false
 	for idx = range argsWithSep {
 	for idx = range argsWithSep {
 		if argsWithSep[idx] == "--" {
 		if argsWithSep[idx] == "--" {
 			foundSep = true
 			foundSep = true
 			break
 			break
 		}
 		}
 	}
 	}
+	if foundSep {
+		execArgs = argsWithSep[idx+1:]
+	}
+
 	if interactive {
 	if interactive {
 		flagset := NewWatchCommand().Flags()
 		flagset := NewWatchCommand().Flags()
 		if err := flagset.Parse(argsWithSep); err != nil {
 		if err := flagset.Parse(argsWithSep); err != nil {
 			return nil, nil, err
 			return nil, nil, err
 		}
 		}
 		watchArgs = flagset.Args()
 		watchArgs = flagset.Args()
+
+		watchPrefix, err = flagset.GetBool("prefix")
+		if err != nil {
+			return nil, nil, err
+		}
+		watchRev, err = flagset.GetInt64("rev")
+		if err != nil {
+			return nil, nil, err
+		}
+		watchPrevKey, err = flagset.GetBool("prev-kv")
+		if err != nil {
+			return nil, nil, err
+		}
 	}
 	}
 
 
-	// "watch -- echo hello" with ETCDCTL_WATCH_KEY=foo
-	// should be translated to "watch foo -- echo hello"
+	// "ETCDCTL_WATCH_KEY=foo watch -- echo hello"
+	// should translate "watch foo -- echo hello"
 	// (watchArgs=["echo","hello"] should be ["foo","echo","hello"])
 	// (watchArgs=["echo","hello"] should be ["foo","echo","hello"])
 	if envKey != "" {
 	if envKey != "" {
-		tmp := []string{envKey}
+		ranges := []string{envKey}
 		if envRange != "" {
 		if envRange != "" {
-			tmp = append(tmp, envRange)
+			ranges = append(ranges, envRange)
 		}
 		}
-		watchArgs = append(tmp, watchArgs...)
+		watchArgs = append(ranges, watchArgs...)
 	}
 	}
 
 
 	if !foundSep {
 	if !foundSep {
 		return watchArgs, nil, nil
 		return watchArgs, nil, nil
 	}
 	}
 
 
-	if idx == len(argsWithSep)-1 {
-		// "watch foo bar --" should error
-		return nil, nil, errBadArgsNumSeparator
-	}
-	execArgs = argsWithSep[idx+1:]
-
 	// "watch foo bar --rev 1 -- echo hello" or "watch foo --rev 1 bar -- echo hello",
 	// "watch foo bar --rev 1 -- echo hello" or "watch foo --rev 1 bar -- echo hello",
 	// then "watchArgs" is "foo bar echo hello"
 	// then "watchArgs" is "foo bar echo hello"
 	// so need ignore args after "argsWithSep[idx]", which is "--"
 	// so need ignore args after "argsWithSep[idx]", which is "--"

+ 322 - 123
etcdctl/ctlv3/command/watch_command_test.go

@@ -26,6 +26,10 @@ func Test_parseWatchArgs(t *testing.T) {
 		envKey, envRange string
 		envKey, envRange string
 		interactive      bool
 		interactive      bool
 
 
+		interactiveWatchPrefix  bool
+		interactiveWatchRev     int64
+		interactiveWatchPrevKey bool
+
 		watchArgs []string
 		watchArgs []string
 		execArgs  []string
 		execArgs  []string
 		err       error
 		err       error
@@ -145,6 +149,14 @@ func Test_parseWatchArgs(t *testing.T) {
 			execArgs:    []string{"echo", "Hello", "World"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			err:         nil,
 			err:         nil,
 		},
 		},
+		{
+			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "--", "echo", "watch", "event", "received"},
+			commandArgs: []string{"foo", "echo", "watch", "event", "received"},
+			interactive: false,
+			watchArgs:   []string{"foo"},
+			execArgs:    []string{"echo", "watch", "event", "received"},
+			err:         nil,
+		},
 		{
 		{
 			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "--rev", "1", "--", "echo", "Hello", "World"},
 			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "--rev", "1", "--", "echo", "Hello", "World"},
 			commandArgs: []string{"foo", "echo", "Hello", "World"},
 			commandArgs: []string{"foo", "echo", "Hello", "World"},
@@ -153,6 +165,22 @@ func Test_parseWatchArgs(t *testing.T) {
 			execArgs:    []string{"echo", "Hello", "World"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			err:         nil,
 			err:         nil,
 		},
 		},
+		{
+			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "--rev", "1", "--", "echo", "watch", "event", "received"},
+			commandArgs: []string{"foo", "echo", "watch", "event", "received"},
+			interactive: false,
+			watchArgs:   []string{"foo"},
+			execArgs:    []string{"echo", "watch", "event", "received"},
+			err:         nil,
+		},
+		{
+			osArgs:      []string{"./bin/etcdctl", "watch", "--rev", "1", "foo", "--", "echo", "watch", "event", "received"},
+			commandArgs: []string{"foo", "echo", "watch", "event", "received"},
+			interactive: false,
+			watchArgs:   []string{"foo"},
+			execArgs:    []string{"echo", "watch", "event", "received"},
+			err:         nil,
+		},
 		{
 		{
 			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "bar", "--", "echo", "Hello", "World"},
 			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "bar", "--", "echo", "Hello", "World"},
 			commandArgs: []string{"foo", "bar", "echo", "Hello", "World"},
 			commandArgs: []string{"foo", "bar", "echo", "Hello", "World"},
@@ -186,162 +214,322 @@ func Test_parseWatchArgs(t *testing.T) {
 			err:         nil,
 			err:         nil,
 		},
 		},
 		{
 		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "--rev", "1", "--", "echo", "Hello", "World"},
-			commandArgs: []string{"echo", "Hello", "World"},
-			envKey:      "foo",
-			envRange:    "",
+			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "bar", "--rev", "1", "--", "echo", "watch", "event", "received"},
+			commandArgs: []string{"foo", "bar", "echo", "watch", "event", "received"},
 			interactive: false,
 			interactive: false,
-			watchArgs:   []string{"foo"},
-			execArgs:    []string{"echo", "Hello", "World"},
+			watchArgs:   []string{"foo", "bar"},
+			execArgs:    []string{"echo", "watch", "event", "received"},
 			err:         nil,
 			err:         nil,
 		},
 		},
 		{
 		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "--rev", "1", "--", "echo", "Hello", "World"},
-			commandArgs: []string{"echo", "Hello", "World"},
-			envKey:      "foo",
-			envRange:    "bar",
+			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "--rev", "1", "bar", "--", "echo", "Hello", "World"},
+			commandArgs: []string{"foo", "bar", "echo", "Hello", "World"},
 			interactive: false,
 			interactive: false,
 			watchArgs:   []string{"foo", "bar"},
 			watchArgs:   []string{"foo", "bar"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			err:         nil,
 			err:         nil,
 		},
 		},
 		{
 		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "bar", "--rev", "1", "--", "echo", "Hello", "World"},
+			osArgs:      []string{"./bin/etcdctl", "watch", "--rev", "1", "foo", "bar", "--", "echo", "Hello", "World"},
 			commandArgs: []string{"foo", "bar", "echo", "Hello", "World"},
 			commandArgs: []string{"foo", "bar", "echo", "Hello", "World"},
-			envKey:      "foo",
 			interactive: false,
 			interactive: false,
-			watchArgs:   nil,
-			execArgs:    nil,
-			err:         errBadArgsNumConflictEnv,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"foo", "bar", "--", "echo", "Hello", "World"},
-			interactive: true,
-			watchArgs:   nil,
-			execArgs:    nil,
-			err:         errBadArgsInteractiveWatch,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "foo"},
-			interactive: true,
-			watchArgs:   []string{"foo"},
-			execArgs:    nil,
-			err:         nil,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "foo", "bar"},
-			interactive: true,
 			watchArgs:   []string{"foo", "bar"},
 			watchArgs:   []string{"foo", "bar"},
-			execArgs:    nil,
-			err:         nil,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch"},
-			envKey:      "foo",
-			envRange:    "bar",
-			interactive: true,
-			watchArgs:   []string{"foo", "bar"},
-			execArgs:    nil,
-			err:         nil,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch"},
-			envKey:      "hello world!",
-			envRange:    "bar",
-			interactive: true,
-			watchArgs:   []string{"hello world!", "bar"},
-			execArgs:    nil,
-			err:         nil,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "foo", "--rev", "1"},
-			interactive: true,
-			watchArgs:   []string{"foo"},
-			execArgs:    nil,
-			err:         nil,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "foo", "--rev", "1", "--", "echo", "Hello", "World"},
-			interactive: true,
-			watchArgs:   []string{"foo"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			err:         nil,
 			err:         nil,
 		},
 		},
 		{
 		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "--rev", "1", "foo", "--", "echo", "Hello", "World"},
-			interactive: true,
-			watchArgs:   []string{"foo"},
-			execArgs:    []string{"echo", "Hello", "World"},
-			err:         nil,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "--", "echo", "Hello", "World"},
+			osArgs:      []string{"./bin/etcdctl", "watch", "--rev", "1", "--", "echo", "Hello", "World"},
+			commandArgs: []string{"echo", "Hello", "World"},
 			envKey:      "foo",
 			envKey:      "foo",
-			interactive: true,
+			envRange:    "",
+			interactive: false,
 			watchArgs:   []string{"foo"},
 			watchArgs:   []string{"foo"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			err:         nil,
 			err:         nil,
 		},
 		},
 		{
 		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "--", "echo", "Hello", "World"},
+			osArgs:      []string{"./bin/etcdctl", "watch", "--rev", "1", "--", "echo", "Hello", "World"},
+			commandArgs: []string{"echo", "Hello", "World"},
 			envKey:      "foo",
 			envKey:      "foo",
 			envRange:    "bar",
 			envRange:    "bar",
-			interactive: true,
-			watchArgs:   []string{"foo", "bar"},
-			execArgs:    []string{"echo", "Hello", "World"},
-			err:         nil,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "foo", "bar", "--", "echo", "Hello", "World"},
-			interactive: true,
-			watchArgs:   []string{"foo", "bar"},
-			execArgs:    []string{"echo", "Hello", "World"},
-			err:         nil,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "--rev", "1", "foo", "bar", "--", "echo", "Hello", "World"},
-			interactive: true,
+			interactive: false,
 			watchArgs:   []string{"foo", "bar"},
 			watchArgs:   []string{"foo", "bar"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			execArgs:    []string{"echo", "Hello", "World"},
 			err:         nil,
 			err:         nil,
 		},
 		},
 		{
 		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "--rev", "1", "--", "echo", "Hello", "World"},
+			osArgs:      []string{"./bin/etcdctl", "watch", "foo", "bar", "--rev", "1", "--", "echo", "Hello", "World"},
+			commandArgs: []string{"foo", "bar", "echo", "Hello", "World"},
 			envKey:      "foo",
 			envKey:      "foo",
-			envRange:    "bar",
-			interactive: true,
-			watchArgs:   []string{"foo", "bar"},
-			execArgs:    []string{"echo", "Hello", "World"},
-			err:         nil,
-		},
-		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "foo", "--rev", "1", "bar", "--", "echo", "Hello", "World"},
-			interactive: true,
-			watchArgs:   []string{"foo", "bar"},
-			execArgs:    []string{"echo", "Hello", "World"},
-			err:         nil,
+			interactive: false,
+			watchArgs:   nil,
+			execArgs:    nil,
+			err:         errBadArgsNumConflictEnv,
 		},
 		},
 		{
 		{
-			osArgs:      []string{"./bin/etcdctl", "watch", "-i"},
-			commandArgs: []string{"watch", "foo", "bar", "--rev", "1", "--", "echo", "Hello", "World"},
-			interactive: true,
-			watchArgs:   []string{"foo", "bar"},
-			execArgs:    []string{"echo", "Hello", "World"},
-			err:         nil,
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"foo", "bar", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               nil,
+			execArgs:                nil,
+			err:                     errBadArgsInteractiveWatch,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "foo"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo"},
+			execArgs:                nil,
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "foo", "bar"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                nil,
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch"},
+			envKey:                  "foo",
+			envRange:                "bar",
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                nil,
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch"},
+			envKey:                  "hello world!",
+			envRange:                "bar",
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"hello world!", "bar"},
+			execArgs:                nil,
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "foo", "--rev", "1"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     1,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo"},
+			execArgs:                nil,
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "foo", "--rev", "1", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     1,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--rev", "1", "foo", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     1,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--rev", "5", "--prev-kv", "foo", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     5,
+			interactiveWatchPrevKey: true,
+			watchArgs:               []string{"foo"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--rev", "1"},
+			envKey:                  "foo",
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     1,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo"},
+			execArgs:                nil,
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--rev", "1"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               nil,
+			execArgs:                nil,
+			err:                     errBadArgsNum,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--rev", "1", "--prefix"},
+			envKey:                  "foo",
+			interactive:             true,
+			interactiveWatchPrefix:  true,
+			interactiveWatchRev:     1,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo"},
+			execArgs:                nil,
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--rev", "100", "--prefix", "--prev-kv"},
+			envKey:                  "foo",
+			interactive:             true,
+			interactiveWatchPrefix:  true,
+			interactiveWatchRev:     100,
+			interactiveWatchPrevKey: true,
+			watchArgs:               []string{"foo"},
+			execArgs:                nil,
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--rev", "1", "--prefix"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               nil,
+			execArgs:                nil,
+			err:                     errBadArgsNum,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--", "echo", "Hello", "World"},
+			envKey:                  "foo",
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--", "echo", "Hello", "World"},
+			envKey:                  "foo",
+			envRange:                "bar",
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "foo", "bar", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     0,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--rev", "1", "foo", "bar", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     1,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "--rev", "1", "--", "echo", "Hello", "World"},
+			envKey:                  "foo",
+			envRange:                "bar",
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     1,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "foo", "--rev", "1", "bar", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     1,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "foo", "bar", "--rev", "1", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  false,
+			interactiveWatchRev:     1,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "foo", "bar", "--rev", "7", "--prefix", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  true,
+			interactiveWatchRev:     7,
+			interactiveWatchPrevKey: false,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
+		},
+		{
+			osArgs:                  []string{"./bin/etcdctl", "watch", "-i"},
+			commandArgs:             []string{"watch", "foo", "bar", "--rev", "7", "--prefix", "--prev-kv", "--", "echo", "Hello", "World"},
+			interactive:             true,
+			interactiveWatchPrefix:  true,
+			interactiveWatchRev:     7,
+			interactiveWatchPrevKey: true,
+			watchArgs:               []string{"foo", "bar"},
+			execArgs:                []string{"echo", "Hello", "World"},
+			err:                     nil,
 		},
 		},
 	}
 	}
 	for i, ts := range tt {
 	for i, ts := range tt {
@@ -355,5 +543,16 @@ func Test_parseWatchArgs(t *testing.T) {
 		if !reflect.DeepEqual(execArgs, ts.execArgs) {
 		if !reflect.DeepEqual(execArgs, ts.execArgs) {
 			t.Fatalf("#%d: execArgs expected %q, got %v", i, ts.execArgs, execArgs)
 			t.Fatalf("#%d: execArgs expected %q, got %v", i, ts.execArgs, execArgs)
 		}
 		}
+		if ts.interactive {
+			if ts.interactiveWatchPrefix != watchPrefix {
+				t.Fatalf("#%d: interactive watchPrefix expected %v, got %v", i, ts.interactiveWatchPrefix, watchPrefix)
+			}
+			if ts.interactiveWatchRev != watchRev {
+				t.Fatalf("#%d: interactive watchRev expected %d, got %d", i, ts.interactiveWatchRev, watchRev)
+			}
+			if ts.interactiveWatchPrevKey != watchPrevKey {
+				t.Fatalf("#%d: interactive watchPrevKey expected %v, got %v", i, ts.interactiveWatchPrevKey, watchPrevKey)
+			}
+		}
 	}
 	}
 }
 }

+ 134 - 0
tests/e2e/ctl_v3_watch_cov_test.go

@@ -0,0 +1,134 @@
+// Copyright 2018 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build cov
+
+package e2e
+
+import (
+	"os"
+	"testing"
+)
+
+func TestCtlV3Watch(t *testing.T)          { testCtl(t, watchTest) }
+func TestCtlV3WatchNoTLS(t *testing.T)     { testCtl(t, watchTest, withCfg(configNoTLS)) }
+func TestCtlV3WatchClientTLS(t *testing.T) { testCtl(t, watchTest, withCfg(configClientTLS)) }
+func TestCtlV3WatchPeerTLS(t *testing.T)   { testCtl(t, watchTest, withCfg(configPeerTLS)) }
+func TestCtlV3WatchTimeout(t *testing.T)   { testCtl(t, watchTest, withDialTimeout(0)) }
+
+func TestCtlV3WatchInteractive(t *testing.T) {
+	testCtl(t, watchTest, withInteractive())
+}
+func TestCtlV3WatchInteractiveNoTLS(t *testing.T) {
+	testCtl(t, watchTest, withInteractive(), withCfg(configNoTLS))
+}
+func TestCtlV3WatchInteractiveClientTLS(t *testing.T) {
+	testCtl(t, watchTest, withInteractive(), withCfg(configClientTLS))
+}
+func TestCtlV3WatchInteractivePeerTLS(t *testing.T) {
+	testCtl(t, watchTest, withInteractive(), withCfg(configPeerTLS))
+}
+
+func watchTest(cx ctlCtx) {
+	tests := []struct {
+		puts     []kv
+		envKey   string
+		envRange string
+		args     []string
+
+		wkv []kvExec
+	}{
+		{ // watch 1 key
+			puts: []kv{{"sample", "value"}},
+			args: []string{"sample", "--rev", "1"},
+			wkv:  []kvExec{{key: "sample", val: "value"}},
+		},
+		{ // watch 1 key with env
+			puts:   []kv{{"sample", "value"}},
+			envKey: "sample",
+			args:   []string{"--rev", "1"},
+			wkv:    []kvExec{{key: "sample", val: "value"}},
+		},
+
+		// coverage tests get extra arguments:
+		// ./bin/etcdctl_test -test.coverprofile=e2e.1525392462795198897.coverprofile -test.outputdir=../..
+		// do not test watch exec commands
+
+		{ // watch 3 keys by prefix
+			puts: []kv{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}},
+			args: []string{"key", "--rev", "1", "--prefix"},
+			wkv:  []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}, {key: "key3", val: "val3"}},
+		},
+		{ // watch 3 keys by prefix, with env
+			puts:   []kv{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}},
+			envKey: "key",
+			args:   []string{"--rev", "1", "--prefix"},
+			wkv:    []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}, {key: "key3", val: "val3"}},
+		},
+		{ // watch by revision
+			puts: []kv{{"etcd", "revision_1"}, {"etcd", "revision_2"}, {"etcd", "revision_3"}},
+			args: []string{"etcd", "--rev", "2"},
+			wkv:  []kvExec{{key: "etcd", val: "revision_2"}, {key: "etcd", val: "revision_3"}},
+		},
+		{ // watch 3 keys by range
+			puts: []kv{{"key1", "val1"}, {"key3", "val3"}, {"key2", "val2"}},
+			args: []string{"key", "key3", "--rev", "1"},
+			wkv:  []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}},
+		},
+		{ // watch 3 keys by range, with env
+			puts:     []kv{{"key1", "val1"}, {"key3", "val3"}, {"key2", "val2"}},
+			envKey:   "key",
+			envRange: "key3",
+			args:     []string{"--rev", "1"},
+			wkv:      []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}},
+		},
+	}
+
+	for i, tt := range tests {
+		donec := make(chan struct{})
+		go func(i int, puts []kv) {
+			for j := range puts {
+				if err := ctlV3Put(cx, puts[j].key, puts[j].val, ""); err != nil {
+					cx.t.Fatalf("watchTest #%d-%d: ctlV3Put error (%v)", i, j, err)
+				}
+			}
+			close(donec)
+		}(i, tt.puts)
+
+		unsetEnv := func() {}
+		if tt.envKey != "" || tt.envRange != "" {
+			if tt.envKey != "" {
+				os.Setenv("ETCDCTL_WATCH_KEY", tt.envKey)
+				unsetEnv = func() { os.Unsetenv("ETCDCTL_WATCH_KEY") }
+			}
+			if tt.envRange != "" {
+				os.Setenv("ETCDCTL_WATCH_RANGE_END", tt.envRange)
+				unsetEnv = func() { os.Unsetenv("ETCDCTL_WATCH_RANGE_END") }
+			}
+			if tt.envKey != "" && tt.envRange != "" {
+				unsetEnv = func() {
+					os.Unsetenv("ETCDCTL_WATCH_KEY")
+					os.Unsetenv("ETCDCTL_WATCH_RANGE_END")
+				}
+			}
+		}
+		if err := ctlV3Watch(cx, tt.args, tt.wkv...); err != nil {
+			if cx.dialTimeout > 0 && !isGRPCTimedout(err) {
+				cx.t.Errorf("watchTest #%d: ctlV3Watch error (%v)", i, err)
+			}
+		}
+		unsetEnv()
+		<-donec
+	}
+}

+ 167 - 0
tests/e2e/ctl_v3_watch_no_cov_test.go

@@ -0,0 +1,167 @@
+// Copyright 2018 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build !cov
+
+package e2e
+
+import (
+	"os"
+	"testing"
+)
+
+func TestCtlV3Watch(t *testing.T)          { testCtl(t, watchTest) }
+func TestCtlV3WatchNoTLS(t *testing.T)     { testCtl(t, watchTest, withCfg(configNoTLS)) }
+func TestCtlV3WatchClientTLS(t *testing.T) { testCtl(t, watchTest, withCfg(configClientTLS)) }
+func TestCtlV3WatchPeerTLS(t *testing.T)   { testCtl(t, watchTest, withCfg(configPeerTLS)) }
+func TestCtlV3WatchTimeout(t *testing.T)   { testCtl(t, watchTest, withDialTimeout(0)) }
+
+func TestCtlV3WatchInteractive(t *testing.T) {
+	testCtl(t, watchTest, withInteractive())
+}
+func TestCtlV3WatchInteractiveNoTLS(t *testing.T) {
+	testCtl(t, watchTest, withInteractive(), withCfg(configNoTLS))
+}
+func TestCtlV3WatchInteractiveClientTLS(t *testing.T) {
+	testCtl(t, watchTest, withInteractive(), withCfg(configClientTLS))
+}
+func TestCtlV3WatchInteractivePeerTLS(t *testing.T) {
+	testCtl(t, watchTest, withInteractive(), withCfg(configPeerTLS))
+}
+
+func watchTest(cx ctlCtx) {
+	tests := []struct {
+		puts     []kv
+		envKey   string
+		envRange string
+		args     []string
+
+		wkv []kvExec
+	}{
+		{ // watch 1 key
+			puts: []kv{{"sample", "value"}},
+			args: []string{"sample", "--rev", "1"},
+			wkv:  []kvExec{{key: "sample", val: "value"}},
+		},
+		{ // watch 1 key with env
+			puts:   []kv{{"sample", "value"}},
+			envKey: "sample",
+			args:   []string{"--rev", "1"},
+			wkv:    []kvExec{{key: "sample", val: "value"}},
+		},
+		{ // watch 1 key with ${ETCD_WATCH_VALUE}
+			puts: []kv{{"sample", "value"}},
+			args: []string{"sample", "--rev", "1", "--", "env"},
+			wkv:  []kvExec{{key: "sample", val: "value", execOutput: `ETCD_WATCH_VALUE="value"`}},
+		},
+		{ // watch 1 key with "echo watch event received", with env
+			puts:   []kv{{"sample", "value"}},
+			envKey: "sample",
+			args:   []string{"--rev", "1", "--", "echo", "watch event received"},
+			wkv:    []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
+		},
+		{ // watch 1 key with "echo watch event received"
+			puts: []kv{{"sample", "value"}},
+			args: []string{"--rev", "1", "sample", "--", "echo", "watch event received"},
+			wkv:  []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
+		},
+		{ // watch 1 key with "echo \"Hello World!\""
+			puts: []kv{{"sample", "value"}},
+			args: []string{"--rev", "1", "sample", "--", "echo", "\"Hello World!\""},
+			wkv:  []kvExec{{key: "sample", val: "value", execOutput: "Hello World!"}},
+		},
+		{ // watch 1 key with "echo watch event received"
+			puts: []kv{{"sample", "value"}},
+			args: []string{"sample", "samplx", "--rev", "1", "--", "echo", "watch event received"},
+			wkv:  []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
+		},
+		{ // watch 1 key with "echo watch event received"
+			puts:     []kv{{"sample", "value"}},
+			envKey:   "sample",
+			envRange: "samplx",
+			args:     []string{"--rev", "1", "--", "echo", "watch event received"},
+			wkv:      []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
+		},
+		{ // watch 1 key with "echo watch event received"
+			puts: []kv{{"sample", "value"}},
+			args: []string{"sample", "--rev", "1", "samplx", "--", "echo", "watch event received"},
+			wkv:  []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
+		},
+		{ // watch 3 keys by prefix
+			puts: []kv{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}},
+			args: []string{"key", "--rev", "1", "--prefix"},
+			wkv:  []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}, {key: "key3", val: "val3"}},
+		},
+		{ // watch 3 keys by prefix, with env
+			puts:   []kv{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}},
+			envKey: "key",
+			args:   []string{"--rev", "1", "--prefix"},
+			wkv:    []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}, {key: "key3", val: "val3"}},
+		},
+		{ // watch by revision
+			puts: []kv{{"etcd", "revision_1"}, {"etcd", "revision_2"}, {"etcd", "revision_3"}},
+			args: []string{"etcd", "--rev", "2"},
+			wkv:  []kvExec{{key: "etcd", val: "revision_2"}, {key: "etcd", val: "revision_3"}},
+		},
+		{ // watch 3 keys by range
+			puts: []kv{{"key1", "val1"}, {"key3", "val3"}, {"key2", "val2"}},
+			args: []string{"key", "key3", "--rev", "1"},
+			wkv:  []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}},
+		},
+		{ // watch 3 keys by range, with env
+			puts:     []kv{{"key1", "val1"}, {"key3", "val3"}, {"key2", "val2"}},
+			envKey:   "key",
+			envRange: "key3",
+			args:     []string{"--rev", "1"},
+			wkv:      []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}},
+		},
+	}
+
+	for i, tt := range tests {
+		donec := make(chan struct{})
+		go func(i int, puts []kv) {
+			for j := range puts {
+				if err := ctlV3Put(cx, puts[j].key, puts[j].val, ""); err != nil {
+					cx.t.Fatalf("watchTest #%d-%d: ctlV3Put error (%v)", i, j, err)
+				}
+			}
+			close(donec)
+		}(i, tt.puts)
+
+		unsetEnv := func() {}
+		if tt.envKey != "" || tt.envRange != "" {
+			if tt.envKey != "" {
+				os.Setenv("ETCDCTL_WATCH_KEY", tt.envKey)
+				unsetEnv = func() { os.Unsetenv("ETCDCTL_WATCH_KEY") }
+			}
+			if tt.envRange != "" {
+				os.Setenv("ETCDCTL_WATCH_RANGE_END", tt.envRange)
+				unsetEnv = func() { os.Unsetenv("ETCDCTL_WATCH_RANGE_END") }
+			}
+			if tt.envKey != "" && tt.envRange != "" {
+				unsetEnv = func() {
+					os.Unsetenv("ETCDCTL_WATCH_KEY")
+					os.Unsetenv("ETCDCTL_WATCH_RANGE_END")
+				}
+			}
+		}
+		if err := ctlV3Watch(cx, tt.args, tt.wkv...); err != nil {
+			if cx.dialTimeout > 0 && !isGRPCTimedout(err) {
+				cx.t.Errorf("watchTest #%d: ctlV3Watch error (%v)", i, err)
+			}
+		}
+		unsetEnv()
+		<-donec
+	}
+}

+ 2 - 156
tests/e2e/ctl_v3_watch_test.go

@@ -1,4 +1,4 @@
-// Copyright 2016 The etcd Authors
+// Copyright 2018 The etcd Authors
 //
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // you may not use this file except in compliance with the License.
@@ -14,167 +14,13 @@
 
 
 package e2e
 package e2e
 
 
-import (
-	"os"
-	"strings"
-	"testing"
-)
-
-func TestCtlV3Watch(t *testing.T)          { testCtl(t, watchTest) }
-func TestCtlV3WatchNoTLS(t *testing.T)     { testCtl(t, watchTest, withCfg(configNoTLS)) }
-func TestCtlV3WatchClientTLS(t *testing.T) { testCtl(t, watchTest, withCfg(configClientTLS)) }
-func TestCtlV3WatchPeerTLS(t *testing.T)   { testCtl(t, watchTest, withCfg(configPeerTLS)) }
-func TestCtlV3WatchTimeout(t *testing.T)   { testCtl(t, watchTest, withDialTimeout(0)) }
-
-func TestCtlV3WatchInteractive(t *testing.T) {
-	testCtl(t, watchTest, withInteractive())
-}
-func TestCtlV3WatchInteractiveNoTLS(t *testing.T) {
-	testCtl(t, watchTest, withInteractive(), withCfg(configNoTLS))
-}
-func TestCtlV3WatchInteractiveClientTLS(t *testing.T) {
-	testCtl(t, watchTest, withInteractive(), withCfg(configClientTLS))
-}
-func TestCtlV3WatchInteractivePeerTLS(t *testing.T) {
-	testCtl(t, watchTest, withInteractive(), withCfg(configPeerTLS))
-}
+import "strings"
 
 
 type kvExec struct {
 type kvExec struct {
 	key, val   string
 	key, val   string
 	execOutput string
 	execOutput string
 }
 }
 
 
-func watchTest(cx ctlCtx) {
-	tests := []struct {
-		puts     []kv
-		envKey   string
-		envRange string
-		args     []string
-
-		wkv []kvExec
-	}{
-		{ // watch 1 key
-			puts: []kv{{"sample", "value"}},
-			args: []string{"sample", "--rev", "1"},
-			wkv:  []kvExec{{key: "sample", val: "value"}},
-		},
-		{ // watch 1 key with env
-			puts:   []kv{{"sample", "value"}},
-			envKey: "sample",
-			args:   []string{"--rev", "1"},
-			wkv:    []kvExec{{key: "sample", val: "value"}},
-		},
-		{ // watch 1 key with "echo watch event received"
-			puts: []kv{{"sample", "value"}},
-			args: []string{"sample", "--rev", "1", "--", "echo", "watch event received"},
-			wkv:  []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
-		},
-		{ // watch 1 key with ${ETCD_WATCH_VALUE}
-			puts: []kv{{"sample", "value"}},
-			args: []string{"sample", "--rev", "1", "--", "env"},
-			wkv:  []kvExec{{key: "sample", val: "value", execOutput: `ETCD_WATCH_VALUE="value"`}},
-		},
-		{ // watch 1 key with "echo watch event received", with env
-			puts:   []kv{{"sample", "value"}},
-			envKey: "sample",
-			args:   []string{"--rev", "1", "--", "echo", "watch event received"},
-			wkv:    []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
-		},
-		{ // watch 1 key with "echo watch event received"
-			puts: []kv{{"sample", "value"}},
-			args: []string{"--rev", "1", "sample", "--", "echo", "watch event received"},
-			wkv:  []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
-		},
-		{ // watch 1 key with "echo \"Hello World!\""
-			puts: []kv{{"sample", "value"}},
-			args: []string{"--rev", "1", "sample", "--", "echo", "\"Hello World!\""},
-			wkv:  []kvExec{{key: "sample", val: "value", execOutput: "Hello World!"}},
-		},
-		{ // watch 1 key with "echo watch event received"
-			puts: []kv{{"sample", "value"}},
-			args: []string{"sample", "samplx", "--rev", "1", "--", "echo", "watch event received"},
-			wkv:  []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
-		},
-		{ // watch 1 key with "echo watch event received"
-			puts:     []kv{{"sample", "value"}},
-			envKey:   "sample",
-			envRange: "samplx",
-			args:     []string{"--rev", "1", "--", "echo", "watch event received"},
-			wkv:      []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
-		},
-		{ // watch 1 key with "echo watch event received"
-			puts: []kv{{"sample", "value"}},
-			args: []string{"sample", "--rev", "1", "samplx", "--", "echo", "watch event received"},
-			wkv:  []kvExec{{key: "sample", val: "value", execOutput: "watch event received"}},
-		},
-		{ // watch 3 keys by prefix
-			puts: []kv{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}},
-			args: []string{"key", "--rev", "1", "--prefix"},
-			wkv:  []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}, {key: "key3", val: "val3"}},
-		},
-		{ // watch 3 keys by prefix, with env
-			puts:   []kv{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}},
-			envKey: "key",
-			args:   []string{"--rev", "1", "--prefix"},
-			wkv:    []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}, {key: "key3", val: "val3"}},
-		},
-		{ // watch by revision
-			puts: []kv{{"etcd", "revision_1"}, {"etcd", "revision_2"}, {"etcd", "revision_3"}},
-			args: []string{"etcd", "--rev", "2"},
-			wkv:  []kvExec{{key: "etcd", val: "revision_2"}, {key: "etcd", val: "revision_3"}},
-		},
-		{ // watch 3 keys by range
-			puts: []kv{{"key1", "val1"}, {"key3", "val3"}, {"key2", "val2"}},
-			args: []string{"key", "key3", "--rev", "1"},
-			wkv:  []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}},
-		},
-		{ // watch 3 keys by range, with env
-			puts:     []kv{{"key1", "val1"}, {"key3", "val3"}, {"key2", "val2"}},
-			envKey:   "key",
-			envRange: "key3",
-			args:     []string{"--rev", "1"},
-			wkv:      []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}},
-		},
-	}
-
-	for i, tt := range tests {
-		donec := make(chan struct{})
-		go func(i int, puts []kv) {
-			for j := range puts {
-				if err := ctlV3Put(cx, puts[j].key, puts[j].val, ""); err != nil {
-					cx.t.Fatalf("watchTest #%d-%d: ctlV3Put error (%v)", i, j, err)
-				}
-			}
-			close(donec)
-		}(i, tt.puts)
-
-		unsetEnv := func() {}
-		if tt.envKey != "" || tt.envRange != "" {
-			if tt.envKey != "" {
-				os.Setenv("ETCDCTL_WATCH_KEY", tt.envKey)
-				unsetEnv = func() { os.Unsetenv("ETCDCTL_WATCH_KEY") }
-			}
-			if tt.envRange != "" {
-				os.Setenv("ETCDCTL_WATCH_RANGE_END", tt.envRange)
-				unsetEnv = func() { os.Unsetenv("ETCDCTL_WATCH_RANGE_END") }
-			}
-			if tt.envKey != "" && tt.envRange != "" {
-				unsetEnv = func() {
-					os.Unsetenv("ETCDCTL_WATCH_KEY")
-					os.Unsetenv("ETCDCTL_WATCH_RANGE_END")
-				}
-			}
-		}
-		if err := ctlV3Watch(cx, tt.args, tt.wkv...); err != nil {
-			if cx.dialTimeout > 0 && !isGRPCTimedout(err) {
-				cx.t.Errorf("watchTest #%d: ctlV3Watch error (%v)", i, err)
-			}
-		}
-		unsetEnv()
-		<-donec
-	}
-}
-
 func setupWatchArgs(cx ctlCtx, args []string) []string {
 func setupWatchArgs(cx ctlCtx, args []string) []string {
 	cmdArgs := append(cx.PrefixArgs(), "watch")
 	cmdArgs := append(cx.PrefixArgs(), "watch")
 	if cx.interactive {
 	if cx.interactive {