lib: v23test: more migrations

Also adds "Cmd.ExitErrorIsOk" option, handy for tests where
commands are expected to fail.

MultiPart: 1/2

Change-Id: If7a7558c574bdc0f4561f9e425d0fd9424e32abb
diff --git a/gosh/.api b/gosh/.api
index 61d44e5..3206ab5 100644
--- a/gosh/.api
+++ b/gosh/.api
@@ -36,6 +36,8 @@
 pkg gosh, method (*Shell) Wait()
 pkg gosh, type Cmd struct
 pkg gosh, type Cmd struct, Args []string
+pkg gosh, type Cmd struct, Err error
+pkg gosh, type Cmd struct, ExitErrorIsOk bool
 pkg gosh, type Cmd struct, OutputDir string
 pkg gosh, type Cmd struct, Stdin io.Reader
 pkg gosh, type Cmd struct, SuppressOutput bool
diff --git a/gosh/cmd.go b/gosh/cmd.go
index a3d2a76..2337696 100644
--- a/gosh/cmd.go
+++ b/gosh/cmd.go
@@ -26,17 +26,22 @@
 // Cmd represents a command. Not thread-safe.
 // Public fields should not be modified after calling Start.
 type Cmd struct {
+	// Err is the most recent error from this Cmd (may be nil).
+	Err error
 	// Vars is the map of env vars for this Cmd.
 	Vars map[string]string
 	// Args is the list of args for this Cmd.
 	Args []string
+	// Stdin specifies this Cmd's stdin. See comments in exec.Cmd for detailed
+	// semantics.
+	Stdin io.Reader
 	// SuppressOutput is inherited from Shell.Opts.SuppressChildOutput.
 	SuppressOutput bool
 	// OutputDir is inherited from Shell.Opts.ChildOutputDir.
 	OutputDir string
-	// Stdin specifies this Cmd's stdin. See comments in exec.Cmd for detailed
-	// semantics.
-	Stdin io.Reader
+	// ExitErrorIsOk specifies whether an *exec.ExitError should be reported via
+	// Shell.HandleError.
+	ExitErrorIsOk bool
 	// Internal state.
 	sh             *Shell
 	c              *exec.Cmd
@@ -51,37 +56,37 @@
 	recvVars       map[string]string // protected by condVars.L
 }
 
-// StdoutPipe returns a Reader backed by a buffered pipe for this command's
+// StdoutPipe returns a Reader backed by a buffered pipe for the command's
 // stdout. Must be called before Start. May be called more than once; each
 // invocation creates a new pipe.
 func (c *Cmd) StdoutPipe() io.Reader {
 	c.sh.Ok()
 	res, err := c.stdoutPipe()
-	c.sh.HandleError(err)
+	c.handleError(err)
 	return res
 }
 
-// StderrPipe returns a Reader backed by a buffered pipe for this command's
+// StderrPipe returns a Reader backed by a buffered pipe for the command's
 // stderr. Must be called before Start. May be called more than once; each
 // invocation creates a new pipe.
 func (c *Cmd) StderrPipe() io.Reader {
 	c.sh.Ok()
 	res, err := c.stderrPipe()
-	c.sh.HandleError(err)
+	c.handleError(err)
 	return res
 }
 
-// Start starts this command.
+// Start starts the command.
 func (c *Cmd) Start() {
 	c.sh.Ok()
-	c.sh.HandleError(c.start())
+	c.handleError(c.start())
 }
 
 // AwaitReady waits for the child process to call SendReady. Must not be called
 // before Start or after Wait.
 func (c *Cmd) AwaitReady() {
 	c.sh.Ok()
-	c.sh.HandleError(c.awaitReady())
+	c.handleError(c.awaitReady())
 }
 
 // AwaitVars waits for the child process to send values for the given vars
@@ -89,54 +94,54 @@
 func (c *Cmd) AwaitVars(keys ...string) map[string]string {
 	c.sh.Ok()
 	res, err := c.awaitVars(keys...)
-	c.sh.HandleError(err)
+	c.handleError(err)
 	return res
 }
 
-// Wait waits for this command to exit.
+// Wait waits for the command to exit.
 func (c *Cmd) Wait() {
 	c.sh.Ok()
-	c.sh.HandleError(c.wait())
+	c.handleError(c.wait())
 }
 
 // TODO(sadovsky): Maybe add a method to send SIGINT, wait for a bit, then send
 // SIGKILL if the process hasn't exited.
 
-// Shutdown sends the given signal to this command, then waits for it to exit.
+// Shutdown sends the given signal to the command, then waits for it to exit.
 func (c *Cmd) Shutdown(sig os.Signal) {
 	c.sh.Ok()
-	c.sh.HandleError(c.shutdown(sig))
+	c.handleError(c.shutdown(sig))
 }
 
 // Run calls Start followed by Wait.
 func (c *Cmd) Run() {
 	c.sh.Ok()
-	c.sh.HandleError(c.run())
+	c.handleError(c.run())
 }
 
-// Output calls Start followed by Wait, then returns this command's stdout and
+// Output calls Start followed by Wait, then returns the command's stdout and
 // stderr.
 func (c *Cmd) Output() (string, string) {
 	c.sh.Ok()
 	stdout, stderr, err := c.output()
-	c.sh.HandleError(err)
+	c.handleError(err)
 	return stdout, stderr
 }
 
-// CombinedOutput calls Start followed by Wait, then returns this command's
+// CombinedOutput calls Start followed by Wait, then returns the command's
 // combined stdout and stderr.
 func (c *Cmd) CombinedOutput() string {
 	c.sh.Ok()
 	res, err := c.combinedOutput()
-	c.sh.HandleError(err)
+	c.handleError(err)
 	return res
 }
 
-// Process returns the underlying process handle for this command.
+// Process returns the underlying process handle for the command.
 func (c *Cmd) Process() *os.Process {
 	c.sh.Ok()
 	res, err := c.process()
-	c.sh.HandleError(err)
+	c.handleError(err)
 	return res
 }
 
@@ -171,6 +176,16 @@
 	return c, nil
 }
 
+func (c *Cmd) handleError(err error) {
+	c.Err = err
+	if c.ExitErrorIsOk {
+		if _, ok := err.(*exec.ExitError); ok {
+			return
+		}
+	}
+	c.sh.HandleError(err)
+}
+
 func (c *Cmd) calledStart() bool {
 	return c.c != nil
 }
@@ -296,7 +311,7 @@
 	c.c = exec.Command(c.name, c.Args...)
 	c.c.Env = mapToSlice(c.Vars)
 	c.c.Stdin = c.Stdin
-	t := time.Now().UTC().Format("20060102.150405.000000")
+	t := time.Now().Format("20060102.150405.000000")
 	var err error
 	if c.c.Stdout, err = c.initMultiWriter(os.Stdout, t); err != nil {
 		return err
diff --git a/gosh/shell.go b/gosh/shell.go
index 1c0c3ce..370f4e3 100644
--- a/gosh/shell.go
+++ b/gosh/shell.go
@@ -43,7 +43,8 @@
 
 // Shell represents a shell. Not thread-safe.
 type Shell struct {
-	// Err is the most recent error (may be nil).
+	// Err is the most recent error from this Shell or any of its Cmds (may be
+	// nil).
 	Err error
 	// Opts is the Opts struct for this Shell, with default values filled in.
 	Opts Opts
diff --git a/gosh/shell_test.go b/gosh/shell_test.go
index c583987..80fcfc2 100644
--- a/gosh/shell_test.go
+++ b/gosh/shell_test.go
@@ -242,6 +242,35 @@
 	}
 }
 
+var exit = gosh.Register("exit", func(code int) {
+	os.Exit(code)
+})
+
+func TestExitErrorIsOk(t *testing.T) {
+	sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+	defer sh.Cleanup()
+
+	// Exit code 0 is not an error.
+	c := sh.Fn(exit, 0)
+	c.Run()
+	ok(t, c.Err)
+	ok(t, sh.Err)
+
+	// Exit code 1 is an error.
+	c = sh.Fn(exit, 1)
+	c.ExitErrorIsOk = true
+	c.Run()
+	nok(t, c.Err)
+	ok(t, sh.Err)
+
+	// If ExitErrorIsOk is false, exit code 1 triggers sh.HandleError.
+	sh.Opts.Errorf = func(string, ...interface{}) {}
+	c = sh.Fn(exit, 1)
+	c.Run()
+	nok(t, c.Err)
+	nok(t, sh.Err)
+}
+
 // Tests that sh.Ok panics under various conditions.
 func TestOkPanics(t *testing.T) {
 	func() { // errDidNotCallNewShell