Merge "v.io/jiri: remove support for old style profiles"
diff --git a/profiles/commandline/driver.go b/profiles/commandline/driver.go
index 94b8380..6fa89ef 100644
--- a/profiles/commandline/driver.go
+++ b/profiles/commandline/driver.go
@@ -126,6 +126,7 @@
specificVersionsFlag bool
cleanupFlag bool
rmAllFlag bool
+ rewriteManifestFlag bool
)
func Main(name string) {
@@ -183,8 +184,8 @@
// uninstall accept --all-targets but with different defaults.
cmdUninstall.Flags.BoolVar(&allFlag, "all-targets", false, "apply to all targets for the specified profile(s)")
- // list accepts --show-manifest, --available, --dir, --default, --versions
- cmdList.Flags.BoolVar(&showManifestFlag, "show-manifest", false, "print out the manifest file")
+ // list accepts --show-profiles-manifest, --available, --dir, --default, --versions
+ cmdList.Flags.BoolVar(&showManifestFlag, "show-profiles-manifest", false, "print out the manifest file")
cmdList.Flags.BoolVar(&availableFlag, "available", false, "print the list of available profiles")
cmdList.Flags.StringVar(&infoFlag, "info", "", infoUsage())
@@ -196,7 +197,8 @@
// cleanup accepts the following flags:
cmdCleanup.Flags.BoolVar(&cleanupFlag, "gc", false, "uninstall profile targets that are older than the current default")
cmdCleanup.Flags.BoolVar(&specificVersionsFlag, "ensure-specific-versions-are-set", false, "ensure that profile targets have a specific version set")
- cmdCleanup.Flags.BoolVar(&rmAllFlag, "rm-all", false, "remove profile manifest and all profile generated output files.")
+ cmdCleanup.Flags.BoolVar(&rmAllFlag, "rm-all", false, "remove profiles manifest and all profile generated output files.")
+ cmdCleanup.Flags.BoolVar(&rewriteManifestFlag, "rewrite-profiles-manifest", false, "rewrite the profiles manifest file to use the latest schema version")
}
func runList(env *cmdline.Env, args []string) error {
@@ -518,12 +520,16 @@
}
for _, target := range profile.Targets() {
if len(target.Version()) == 0 {
- prior := target
+ prior := *target
version, err := mgr.VersionInfo().Select(target.Version())
if err != nil {
return err
}
target.SetVersion(version)
+ profiles.RemoveProfileTarget(name, prior)
+ if err := profiles.AddProfileTarget(name, *target); err != nil {
+ return err
+ }
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "%s %s had no version, now set to: %s\n", name, prior, target)
}
@@ -534,11 +540,18 @@
}
func runRmAll(ctx *tool.Context) error {
- if err := ctx.Run().Remove(manifestFlag); err != nil {
- return err
+ s := ctx.NewSeq()
+ if exists, err := s.FileExists(manifestFlag); err != nil || exists {
+ if err := s.Remove(manifestFlag).Done(); err != nil {
+ return err
+ }
}
- if err := ctx.Run().RemoveAll(rootPath.Expand()); err != nil {
- return err
+ rp := rootPath.Expand()
+ if exists, err := s.DirectoryExists(rp); err != nil || exists {
+ if err := s.Run("chmod", "-R", "u+w", rp).
+ RemoveAll(rp).Done(); err != nil {
+ return err
+ }
}
return nil
}
@@ -581,6 +594,9 @@
// Don't write out the profiles manifest file again.
return nil
}
+ if rewriteManifestFlag {
+ dirty = true
+ }
if !dirty {
return fmt.Errorf("at least one option must be specified")
}
@@ -647,7 +663,7 @@
return err
}
if allFlag && targetFlag.IsSet() {
- return fmt.Errorf("don't specify a target (%v) in conjunction with --all-targets", targetFlag)
+ fmt.Fprintf(ctx.Stdout(), "ignore target (%v) when used in conjunction with --all-targets\n", targetFlag)
}
if allFlag {
for _, name := range args {
diff --git a/profiles/env_test.go b/profiles/env_test.go
index 535bfdd..f057c3c 100644
--- a/profiles/env_test.go
+++ b/profiles/env_test.go
@@ -32,7 +32,10 @@
}
ch.Vars = envvar.VarsFromOS()
ch.Delete("CGO_CFLAGS")
- native, _ := profiles.NewTarget("native")
+ native, err := profiles.NewTarget("amd64-darwin")
+ if err != nil {
+ t.Fatal(err)
+ }
ch.MergeEnvFromProfiles(profiles.JiriMergePolicies(), native, "go", "syncbase")
if got, want := ch.Get("CGO_CFLAGS"), "-IX -IY -IA -IB"; got != want {
t.Errorf("got %v, want %v", got, want)
@@ -66,7 +69,10 @@
t.Fatal(err)
}
ch.Vars = envvar.VarsFromSlice([]string{})
- t1Target, _ := profiles.NewTarget("cpu1-os1@1")
+ t1Target, err := profiles.NewTarget("cpu1-os1@1")
+ if err != nil {
+ t.Fatal(err)
+ }
ch.MergeEnvFromProfiles(map[string]profiles.MergePolicy{
"A": profiles.AppendFlag,
"B": profiles.UseLast,
diff --git a/profiles/manifest.go b/profiles/manifest.go
index 4631ac6..cb1c505 100644
--- a/profiles/manifest.go
+++ b/profiles/manifest.go
@@ -83,7 +83,7 @@
}
func newDB() *profileDB {
- return &profileDB{db: make(map[string]*Profile), version: 0}
+ return &profileDB{db: make(map[string]*Profile), version: V4}
}
var (
@@ -283,7 +283,7 @@
defer pdb.Unlock()
var schema profilesSchema
- schema.Version = V3
+ schema.Version = V4
for i, name := range pdb.profilesUnlocked() {
profile := pdb.db[name]
schema.Profiles = append(schema.Profiles, &profileSchema{
diff --git a/profiles/manifest_test.go b/profiles/manifest_test.go
index 8a8d2dd..aa6ef33 100644
--- a/profiles/manifest_test.go
+++ b/profiles/manifest_test.go
@@ -16,6 +16,7 @@
"testing"
"v.io/jiri/profiles"
+ "v.io/jiri/project"
"v.io/jiri/tool"
)
@@ -121,7 +122,7 @@
}
}
-func TestBackwardsCompatibility(t *testing.T) {
+func TestReadingV0(t *testing.T) {
profiles.Clear()
getProfiles := func() []*profiles.Profile {
@@ -162,7 +163,7 @@
t.Fatal(err)
}
- if got, want := profiles.SchemaVersion(), profiles.V3; got != want {
+ if got, want := profiles.SchemaVersion(), profiles.V4; got != want {
t.Errorf("got %v, want %v", got, want)
}
nprofiles := getProfiles()
@@ -180,3 +181,54 @@
}
}
}
+
+func handleRelativePath(root profiles.RelativePath, s string) string {
+ // Handle the transition from absolute to relative paths.
+ if filepath.IsAbs(s) {
+ return s
+ }
+ return root.RootJoin(s).Expand()
+}
+
+func TestReadingV3AndV4(t *testing.T) {
+ ctx := tool.NewDefaultContext()
+ root, err := project.JiriRoot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for i, c := range []struct {
+ filename, prefix, variable string
+ version profiles.Version
+ }{
+ {"v3.xml", "", "", profiles.V3},
+ {"v4.xml", root, "${JIRI_ROOT}", profiles.V4},
+ } {
+ ch, err := profiles.NewConfigHelper(ctx, profiles.UseProfiles, filepath.Join("testdata", c.filename))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, want := profiles.SchemaVersion(), c.version; got != want {
+ t.Errorf("%d: got %v, want %v", i, got, want)
+ }
+ target, err := profiles.NewTarget("cpu1-os1@1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ p := profiles.LookupProfile("a")
+ // We need to expand the variable here for a V4 profile if we want
+ // to get the full absolute path.
+ if got, want := p.Root, c.variable+"/an/absolute/root"; got != want {
+ t.Errorf("%d: got %v, want %v", i, got, want)
+ }
+ lt := profiles.LookupProfileTarget("a", target)
+ if got, want := lt.InstallationDir, c.variable+"/an/absolute/dir"; got != want {
+ t.Errorf("%d: got %v, want %v", i, got, want)
+ }
+ // The merged environment variables are expanded appropriately
+ // internally by MergeEnvFromProfiles.
+ ch.MergeEnvFromProfiles(profiles.JiriMergePolicies(), target, "a")
+ if got, want := ch.Get("ABS"), "-I"+c.prefix+"/an/absolute/path"; got != want {
+ t.Errorf("%d: got %v, want %v", i, got, want)
+ }
+ }
+}
diff --git a/profiles/target.go b/profiles/target.go
index cffcbd9..5bf831c 100644
--- a/profiles/target.go
+++ b/profiles/target.go
@@ -323,10 +323,6 @@
// only a single target available in targets then that one target is considered
// as matching.
func FindTarget(targets OrderedTargets, target *Target) *Target {
- if len(targets) == 1 && !target.IsSet() {
- tmp := *targets[0]
- return &tmp
- }
for _, t := range targets {
if target.Match(t) {
tmp := *t
diff --git a/profiles/testdata/m1.xml b/profiles/testdata/m1.xml
index d54ea6f..c14d319 100644
--- a/profiles/testdata/m1.xml
+++ b/profiles/testdata/m1.xml
@@ -1,4 +1,4 @@
-<profiles version="3">
+<profiles version="4">
<profile name="a" root="">
<target tag="" arch="cpu1" os="os1" installation-directory="" version="1">
<envvars></envvars>
diff --git a/profiles/testdata/v3.xml b/profiles/testdata/v3.xml
new file mode 100644
index 0000000..5b6a3cb
--- /dev/null
+++ b/profiles/testdata/v3.xml
@@ -0,0 +1,13 @@
+<profiles version="3">
+ <profile name="a" root="/an/absolute/root">
+ <target arch="cpu1" os="os1" installation-directory="/an/absolute/dir" version="1">
+ <envvars>
+ <var>ABS=-I/an/absolute/path</var>
+ </envvars>
+ <command-line>
+ <var>A=B</var>
+ <var>C=D</var>
+ </command-line>
+ </target>
+ </profile>
+</profiles>
diff --git a/profiles/testdata/v4.xml b/profiles/testdata/v4.xml
new file mode 100644
index 0000000..83fddb7
--- /dev/null
+++ b/profiles/testdata/v4.xml
@@ -0,0 +1,13 @@
+<profiles version="4">
+ <profile name="a" root="${JIRI_ROOT}/an/absolute/root">
+ <target arch="cpu1" os="os1" installation-directory="${JIRI_ROOT}/an/absolute/dir" version="1">
+ <envvars>
+ <var>ABS=-I${JIRI_ROOT}/an/absolute/path</var>
+ </envvars>
+ <command-line>
+ <var>A=B</var>
+ <var>C=D</var>
+ </command-line>
+ </target>
+ </profile>
+</profiles>
diff --git a/runutil/.api b/runutil/.api
index d303875..ba929ec 100644
--- a/runutil/.api
+++ b/runutil/.api
@@ -1,3 +1,4 @@
+pkg runutil, func IsFNLHost() bool
pkg runutil, func LookPath(string, map[string]string) (string, error)
pkg runutil, func NewRun(map[string]string, io.Reader, io.Writer, io.Writer, bool, bool, bool) *Run
pkg runutil, func NewSequence(map[string]string, io.Reader, io.Writer, io.Writer, bool, bool, bool) *Sequence
@@ -6,6 +7,8 @@
pkg runutil, method (*Run) Chmod(string, os.FileMode) error
pkg runutil, method (*Run) Command(string, ...string) error
pkg runutil, method (*Run) CommandWithOpts(Opts, string, ...string) error
+pkg runutil, method (*Run) Copy(*os.File, io.Reader) (int64, error)
+pkg runutil, method (*Run) Create(string) (*os.File, error)
pkg runutil, method (*Run) DirectoryExists(string) bool
pkg runutil, method (*Run) FileExists(string) bool
pkg runutil, method (*Run) Function(func() error, string, ...interface{}) error
@@ -13,6 +16,7 @@
pkg runutil, method (*Run) IsDir(string) (bool, error)
pkg runutil, method (*Run) MkdirAll(string, os.FileMode) error
pkg runutil, method (*Run) Open(string) (*os.File, error)
+pkg runutil, method (*Run) OpenFile(string, int, os.FileMode) (*os.File, error)
pkg runutil, method (*Run) Output([]string)
pkg runutil, method (*Run) OutputWithOpts(Opts, []string)
pkg runutil, method (*Run) ReadDir(string) ([]os.FileInfo, error)
@@ -23,6 +27,7 @@
pkg runutil, method (*Run) Stat(string) (os.FileInfo, error)
pkg runutil, method (*Run) Symlink(string, string) error
pkg runutil, method (*Run) TempDir(string, string) (string, error)
+pkg runutil, method (*Run) TempFile(string, string) (*os.File, error)
pkg runutil, method (*Run) TimedCommand(time.Duration, string, ...string) error
pkg runutil, method (*Run) TimedCommandWithOpts(time.Duration, Opts, string, ...string) error
pkg runutil, method (*Run) WriteFile(string, []byte, os.FileMode) error
@@ -36,6 +41,7 @@
pkg runutil, method (*Sequence) FileExists(string) (bool, error)
pkg runutil, method (*Sequence) GetOpts() Opts
pkg runutil, method (*Sequence) IsDir(string) (bool, error)
+pkg runutil, method (*Sequence) Last(string, ...string) error
pkg runutil, method (*Sequence) MkdirAll(string, os.FileMode) *Sequence
pkg runutil, method (*Sequence) Open(string) (*os.File, error)
pkg runutil, method (*Sequence) Opts(Opts) *Sequence
diff --git a/runutil/executor.go b/runutil/executor.go
index 44a18e9..31f2fff 100644
--- a/runutil/executor.go
+++ b/runutil/executor.go
@@ -204,13 +204,11 @@
}
fmt.Fprintf(e.opts.Stderr, "Waiting for command to exit: %q\n", command.Args)
// Give the process some time to shut down cleanly.
- for i := 0; i < 10; i++ {
- select {
- case <-time.After(time.Second):
- if err := syscall.Kill(pid, 0); err != nil {
- return
- }
+ for i := 0; i < 50; i++ {
+ if err := syscall.Kill(pid, 0); err != nil {
+ return
}
+ time.Sleep(200 * time.Millisecond)
}
// If it still exists, send SIGKILL to it.
if err := syscall.Kill(pid, 0); err == nil {
diff --git a/runutil/sequence.go b/runutil/sequence.go
index 1d1b564..68c00b9 100644
--- a/runutil/sequence.go
+++ b/runutil/sequence.go
@@ -5,8 +5,13 @@
package runutil
import (
+ "bytes"
+ "fmt"
"io"
+ "io/ioutil"
"os"
+ "path/filepath"
+ "runtime"
"time"
)
@@ -19,23 +24,31 @@
// methods and the result of that first error is returned by the
// Done method or any of the other 'terminating methods' (see below).
//
+// Unless directed to specific stdout and stderr io.Writers using Capture(), the
+// stdout and stderr output from the command is discarded, except in verbose
+// mode or upon error: when in verbose mode (set either via NewSequence or an
+// Opt) or when the command fails, all the command's output (stdout and stderr)
+// is written to the stdout io.Writer configured either via NewSequence or an
+// Opt. In addition, in verbose mode, command execution logging is written to
+// the stdout and stderr io.Writers configured via NewSequence.
+//
// Modifier methods are provided that influence the behaviour of the
-// next invocation of the Run method to set timeouts (Timeout) and to
-// capture output (Capture), an additional modifier method (Opts) is
-// provided the set the options for the remaining methods and is generally
-// used to control logging.
+// next invocation of the Run method to set timeouts (Timeout), to
+// capture output (Capture), an set options (Opts).
// For example, the following will result in a timeout error.
//
// err := s.Timed(time.Second).Run("sleep","10").Done()
+// err := s.Timed(time.Second).Last("sleep","10")
//
// A sequence of commands must be terminated with a call to a 'terminating'
-// method. The simplest is the Done method used in the examples above, but there
-// are other methods which typically return results in addition to error, such
-// as ReadFile(filename string) ([]byte, error). Here the usage would be:
+// method. The simplest are the Done or Last methods used in the examples above,
+// but there are other methods which typically return results in addition to
+// error, such as ReadFile(filename string) ([]byte, error). Here the usage
+// would be:
//
// o.Stdout, _ = os.Create("foo")
// data, err := s.Opts(o).Run("echo","b").ReadFile("foo")
-// // data == "foo"
+// // data == "b"
//
// Note that terminating functions, even those that take an action, may
// return an error generated by a previous method.
@@ -46,8 +59,10 @@
type Sequence struct {
r *Run
err error
+ caller string
stdout, stderr io.Writer
opts *Opts
+ dirs []string
timeout time.Duration
}
@@ -57,9 +72,9 @@
return &Sequence{r: NewRun(env, stdin, stdout, stderr, color, dryRun, verbose)}
}
-// Capture arranges for the next call to Run to write its stdout and stderr
-// output to the supplied io.Writers. This will be cleared and not used for
-// any calls to Run beyond the next one.
+// Capture arranges for the next call to Run or Last to write its stdout and
+// stderr output to the supplied io.Writers. This will be cleared and not used
+// for any calls to Run or Last beyond the next one.
func (s *Sequence) Capture(stdout, stderr io.Writer) *Sequence {
if s.err != nil {
return s
@@ -68,8 +83,9 @@
return s
}
-// Opts arranges for the next call to Run to use the supplied options.
-// They will be cleared and not used for any calls to Run beyond the next one.
+// Opts arranges for the next call to Run or Last to use the supplied options.
+// This will be cleared and not used for any calls to Run or Last beyond the
+// next one.
func (s *Sequence) Opts(opts Opts) *Sequence {
if s.err != nil {
return s
@@ -78,9 +94,9 @@
return s
}
-// Timeout arranges for the next call to Run to be subject to the specified
-// timeout. The timeout will be cleared and not used any calls to Run beyond
-// the next one.
+// Timeout arranges for the next call to Run or Last to be subject to the
+// specified timeout. The timeout will be cleared and not used any calls to Run
+// or Last beyond the next one.
func (s *Sequence) Timeout(timeout time.Duration) *Sequence {
if s.err != nil {
return s
@@ -97,6 +113,9 @@
}
func (s *Sequence) Error() error {
+ if s.err != nil && len(s.caller) > 0 {
+ return fmt.Errorf("%s: %v", s.caller, s.err)
+ }
return s.err
}
@@ -104,6 +123,19 @@
s.opts = &opts
}
+func fmtError(depth int, err error, detail string) string {
+ _, file, line, _ := runtime.Caller(depth + 1)
+ return fmt.Sprintf("%s:%d: %s", filepath.Base(file), line, detail)
+}
+
+func (s *Sequence) setError(err error, detail string) {
+ if err == nil || s.err != nil {
+ return
+ }
+ s.err = err
+ s.caller = fmtError(2, err, detail)
+}
+
func (s *Sequence) reset() {
s.stdout, s.stderr, s.opts = nil, nil, nil
s.timeout = 0
@@ -128,7 +160,34 @@
func (s *Sequence) initAndDefer() func() {
if s.stdout == nil && s.stderr == nil {
- return func() {}
+ f, err := ioutil.TempFile("", "seq")
+ if err != nil {
+ return func() {}
+ }
+ opts := s.GetOpts()
+ stdout := opts.Stdout
+ if stdout == nil {
+ return func() {}
+ }
+ opts.Stdout = f
+ opts.Stderr = f
+ s.setOpts(opts)
+ return func() {
+ filename := f.Name()
+ f.Close()
+ if opts.Verbose || s.err != nil {
+ // TODO(cnicolaou): probably best to stream this out rather
+ // than buffer the whole file into memory.
+ if data, err := ioutil.ReadFile(filename); err == nil {
+ fmt.Fprint(stdout, string(data))
+ if wd, err := os.Getwd(); err == nil {
+ fmt.Fprintf(stdout, "Current Directory: %v\n", wd)
+ }
+ }
+ }
+ os.Remove(filename)
+ s.opts = nil
+ }
}
opts := s.GetOpts()
rStdin, wStdin := io.Pipe()
@@ -152,16 +211,87 @@
}
}
+func fmtArgs(args ...interface{}) string {
+ if len(args) == 0 {
+ return ""
+ }
+ out := &bytes.Buffer{}
+ for _, a := range args {
+ if _, ok := a.(string); ok {
+ out.WriteString(fmt.Sprintf(" ,%q", a))
+ } else {
+ out.WriteString(fmt.Sprintf(" ,%s", a))
+ }
+ }
+ return out.String()
+}
+
+func fmtStringArgs(args ...string) string {
+ if len(args) == 0 {
+ return ""
+ }
+ out := &bytes.Buffer{}
+ for _, a := range args {
+ out.WriteString(", \"")
+ out.WriteString(a)
+ out.WriteString("\"")
+ }
+ return out.String()
+}
+
+// Pushd pushes the current directory onto a stack and changes directory
+// to the specified one. Calling any terminating function will pop back
+// to the first element in the stack on completion of that function.
+func (s *Sequence) Pushd(dir string) *Sequence {
+ cwd, err := os.Getwd()
+ if err != nil {
+ s.setError(err, "Pushd("+dir+"): os.Getwd")
+ return s
+ }
+ s.dirs = append(s.dirs, cwd)
+ s.setError(s.r.Chdir(dir), "Pushd("+dir+")")
+ return s
+}
+
+// Popd popds the last directory from the directory stack and chdir's to it.
+// Calling any termination function will pop back to the first element in
+// the stack on completion of that function.
+func (s *Sequence) Popd() *Sequence {
+ if s.err != nil {
+ return s
+ }
+ if len(s.dirs) == 0 {
+ s.setError(fmt.Errorf("directory stack is empty"), "Popd()")
+ return s
+ }
+ last := s.dirs[len(s.dirs)-1]
+ s.dirs = s.dirs[:len(s.dirs)-1]
+ s.setError(s.r.Chdir(last), "Popd() -> "+last)
+ return s
+}
+
// Run runs the given command as a subprocess.
func (s *Sequence) Run(path string, args ...string) *Sequence {
if s.err != nil {
return s
}
defer s.initAndDefer()()
- s.err = s.r.command(s.timeout, s.GetOpts(), path, args...)
+ s.setError(s.r.command(s.timeout, s.GetOpts(), path, args...), fmt.Sprintf("Run(%q%s)", path, fmtStringArgs(args...)))
return s
}
+// Last runs the given command as a subprocess and returns an error
+// immediately terminating the sequence, it is equivalent to
+// calling s.Run(path, args...).Done().
+func (s *Sequence) Last(path string, args ...string) error {
+ if s.err != nil {
+ return s.Done()
+ }
+ defer s.initAndDefer()()
+ s.setError(s.r.command(s.timeout, s.GetOpts(), path, args...), fmt.Sprintf("Last(%q%s)", path, fmtStringArgs(args...)))
+ return s.Done()
+}
+
// Call runs the given function. Note that Capture and Timeout have no
// effect on invocations of Call, but Opts can control logging.
func (s *Sequence) Call(fn func() error, format string, args ...interface{}) *Sequence {
@@ -169,7 +299,7 @@
return s
}
defer s.initAndDefer()()
- s.err = s.r.FunctionWithOpts(s.GetOpts(), fn, format, args...)
+ s.setError(s.r.FunctionWithOpts(s.GetOpts(), fn, format, args...), fmt.Sprintf("Call(%s,%s%s)", fn, format, fmtArgs(args)))
return s
}
@@ -179,7 +309,7 @@
if s.err != nil {
return s
}
- s.err = s.r.Chdir(dir)
+ s.setError(s.r.Chdir(dir), "Chdir("+dir+")")
return s
}
@@ -190,7 +320,7 @@
return s
}
if err := s.r.Chmod(dir, mode); err != nil {
- s.err = err
+ s.setError(err, fmt.Sprintf("Chmod(%s, %s)", dir, mode))
}
return s
}
@@ -201,7 +331,7 @@
if s.err != nil {
return s
}
- s.err = s.r.MkdirAll(dir, mode)
+ s.setError(s.r.MkdirAll(dir, mode), fmt.Sprintf("MkdirAll(%s, %s)", dir, mode))
return s
}
@@ -211,7 +341,7 @@
if s.err != nil {
return s
}
- s.err = s.r.RemoveAll(dir)
+ s.setError(s.r.RemoveAll(dir), fmt.Sprintf("RemoveAll(%s)", dir))
return s
}
@@ -221,7 +351,7 @@
if s.err != nil {
return s
}
- s.err = s.r.Remove(file)
+ s.setError(s.r.Remove(file), fmt.Sprintf("Remove(%s)", file))
return s
}
@@ -231,7 +361,7 @@
if s.err != nil {
return s
}
- s.err = s.r.Rename(src, dst)
+ s.setError(s.r.Rename(src, dst), fmt.Sprintf("Rename(%s, %s)", src, dst))
return s
}
@@ -241,7 +371,7 @@
if s.err != nil {
return s
}
- s.err = s.r.Symlink(src, dst)
+ s.setError(s.r.Symlink(src, dst), fmt.Sprintf("Symlink(%s, %s)", src, dst))
return s
}
@@ -255,12 +385,32 @@
return s
}
-// Done returns the error stored in the Sequence. Done is a terminating function.
+// Done returns the error stored in the Sequence and pops back to the first
+// entry in the directory stack if Pushd has been called. Done is a terminating
+// function. There is no need to ensure that Done is called before returning
+// from a function that uses a sequence unless it is necessary to pop the
+// stack.
func (s *Sequence) Done() error {
- err := s.err
+ rerr := s.Error()
s.err = nil
+ s.caller = ""
s.reset()
- return err
+ if len(s.dirs) > 0 {
+ cwd := s.dirs[0]
+ s.dirs = nil
+ if err := s.r.Chdir(cwd); err != nil {
+ detail := "Done: Chdir(" + cwd + ")"
+ if rerr == nil {
+ s.setError(err, detail)
+ } else {
+ // In the unlikely event that Chdir fails in addition to an
+ // earlier error, we append an appropriate error message.
+ s.err = fmt.Errorf("%v\n%v", rerr, fmtError(1, err, detail))
+ }
+ return s.Error()
+ }
+ }
+ return rerr
}
// Open is a wrapper around os.Open that handles options such as
@@ -270,7 +420,7 @@
return nil, s.Done()
}
f, err := s.r.Open(name)
- s.err = err
+ s.setError(err, fmt.Sprintf("Open(%s)", name))
return f, s.Done()
}
@@ -281,7 +431,7 @@
return nil, s.Done()
}
fi, err := s.r.ReadDir(dirname)
- s.err = err
+ s.setError(err, fmt.Sprintf("ReadDir(%s)", dirname))
return fi, s.Done()
}
@@ -292,10 +442,21 @@
return nil, s.Done()
}
data, err := s.r.ReadFile(filename)
- s.err = err
+ s.setError(err, fmt.Sprintf("ReadFile(%s)", filename))
return data, s.Done()
}
+// WriteFile is a wrapper around ioutil.WriteFile that handles options
+// such as "verbose" or "dry run".
+func (s *Sequence) WriteFile(filename string, data []byte, perm os.FileMode) *Sequence {
+ if s.err != nil {
+ return s
+ }
+ err := s.r.WriteFile(filename, data, perm)
+ s.setError(err, fmt.Sprintf("WriteFile(%s, %10s, %s)", filename, data, perm))
+ return s
+}
+
// Stat is a wrapper around os.Stat that handles options such as
// "verbose" or "dry run". Stat is a terminating function.
func (s *Sequence) Stat(name string) (os.FileInfo, error) {
@@ -303,7 +464,7 @@
return nil, s.Done()
}
fi, err := s.r.Stat(name)
- s.err = err
+ s.setError(err, fmt.Sprintf("Stat(%s)", name))
return fi, s.Done()
}
@@ -314,18 +475,18 @@
return "", s.Done()
}
name, err := s.r.TempDir(dir, prefix)
- s.err = err
+ s.setError(err, fmt.Sprintf("TempDir(%s,%s)", dir, prefix))
return name, s.Done()
}
// IsDir is a wrapper around os.Stat with appropriate logging.
// IsDir is a terminating function.
-func (s *Sequence) IsDir(name string) (bool, error) {
+func (s *Sequence) IsDir(dirname string) (bool, error) {
if s.err != nil {
return false, s.Done()
}
- t, err := s.r.IsDir(name)
- s.err = err
+ t, err := s.r.IsDir(dirname)
+ s.setError(err, fmt.Sprintf("IsDir(%s)", dirname))
return t, s.Done()
}
diff --git a/runutil/sequence_test.go b/runutil/sequence_test.go
index 566f500..62df6f0 100644
--- a/runutil/sequence_test.go
+++ b/runutil/sequence_test.go
@@ -9,28 +9,181 @@
"fmt"
"io/ioutil"
"os"
+ "path/filepath"
+ "regexp"
+ "strings"
"testing"
"time"
"v.io/jiri/runutil"
)
+func rmLineNumbers(s string) string {
+ re := regexp.MustCompile("(.*\\.go):\\d+:(.*)")
+ return re.ReplaceAllString(s, "$1:-:$2")
+}
+
+func sanitizeTimestamps(s string) string {
+ re := regexp.MustCompile(`\[(\d\d:\d\d:\d\d.\d\d)\]`)
+ return re.ReplaceAllString(s, "[hh:mm:ss.xx]")
+}
+
func ExampleSequence() {
seq := runutil.NewSequence(nil, os.Stdin, ioutil.Discard, ioutil.Discard, false, false, true)
err := seq.
Capture(os.Stdout, nil).Run("echo", "a").
- Capture(os.Stdout, nil).Run("echo", "b").
- Done()
+ Capture(os.Stdout, nil).Last("echo", "b")
err = seq.
Run("echo", "c").
Run("xxxxxxx").
- Capture(os.Stdout, nil).Run("echo", "d").
- Done()
- fmt.Println(err)
+ Capture(os.Stdout, nil).Last("echo", "d")
+ // Get rid of the line#s in the error output.
+ fmt.Println(rmLineNumbers(err.Error()))
// Output:
// a
// b
- // exec: "xxxxxxx": executable file not found in $PATH
+ // sequence_test.go:-: Run("xxxxxxx"): exec: "xxxxxxx": executable file not found in $PATH
+}
+
+// TestStdoutStderr exercises the various possible configurations for stdout and
+// stderr (via NewSequence, Opts, or Capture) as well as the verbose flag.
+func TestStdoutStderr(t *testing.T) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Case 1: we only specify stdout/stderr at constructor time.
+ //
+ // Verbose mode: All the command's output and execution logging goes to
+ // stdout, execution error messages to stderr.
+ //
+ // Non-Verbose mode: No stdout output; execution error messages to
+ // stderr.
+ for _, verbose := range []bool{false, true} {
+ var cnstrStdout, cnstrStderr bytes.Buffer
+ seq := runutil.NewSequence(nil, os.Stdin, &cnstrStdout, &cnstrStderr, false, false, verbose)
+ seq.Run("bash", "-c", "echo a; echo b >&2").
+ Timeout(time.Microsecond).
+ Run("sleep", "10000")
+ want := "Current Directory: " + cwd + "\n"
+ if verbose {
+ want = `[hh:mm:ss.xx] >> bash -c "echo a; echo b >&2"
+[hh:mm:ss.xx] >> OK
+a
+b
+Current Directory: ` + cwd + `
+[hh:mm:ss.xx] >> sleep 10000
+[hh:mm:ss.xx] >> TIMED OUT
+Current Directory: ` + cwd + `
+`
+ }
+ if got := sanitizeTimestamps(cnstrStdout.String()); want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ if got, want := cnstrStderr.String(), "Waiting for command to exit: [\"sleep\" \"10000\"]\n"; want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ }
+
+ // Case 2: we specify stdout/stderr at constructor time, and also via
+ // Opts. The verbose setting from opts takes precedence and controls
+ // the output that goes both to constructor stdout and to opts stdout.
+ //
+ // Verbose mode: The command execution logging goes to constructor
+ // stdout, command execution errors go to constructor stderr, and the
+ // actual command output goes to opts stdout. Nothing goes to opts
+ // stderr.
+ //
+ // Non-Verbose mode: No stdout output; execution error messages to
+ // constructor stderr.
+ for _, verbose := range []bool{false, true} {
+ var cnstrStdout, cnstrStderr, optsStdout, optsStderr bytes.Buffer
+ cstrVerbose := false // irellevant, the opts verbose flag takes precedence.
+ seq := runutil.NewSequence(nil, os.Stdin, &cnstrStdout, &cnstrStderr, false, false, cstrVerbose)
+ opts := runutil.Opts{Stdout: &optsStdout, Stderr: &optsStderr, Verbose: verbose}
+ seq.Opts(opts).
+ Run("bash", "-c", "echo a; echo b >&2").
+ Opts(opts).
+ Timeout(time.Microsecond).
+ Run("sleep", "10000")
+ want := ""
+ if verbose {
+ want = `[hh:mm:ss.xx] >> bash -c "echo a; echo b >&2"
+[hh:mm:ss.xx] >> OK
+[hh:mm:ss.xx] >> sleep 10000
+[hh:mm:ss.xx] >> TIMED OUT
+`
+ }
+ if got := sanitizeTimestamps(cnstrStdout.String()); want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ if got, want := cnstrStderr.String(), "Waiting for command to exit: [\"sleep\" \"10000\"]\n"; want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ want = "Current Directory: " + cwd + "\n"
+ if verbose {
+ want = "a\nb\nCurrent Directory: " + cwd + "\nCurrent Directory: " + cwd + "\n"
+ }
+ if got := optsStdout.String(); want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ if got, want := optsStderr.String(), ""; want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ }
+
+ // Case 3: we specify stdout/stderr at constructor time, also via Opts,
+ // and also via Capture. The verbose setting from opts takes
+ // precedence.
+ //
+ // Verbose mode: The command execution log goes to constructor stdout,
+ // command execution errors go to constructor stderr, and the
+ // stdout/stderr output from the command goes to capture stdout/stderr
+ // respectively. Nothing goes to opts stdout/stderr.
+ //
+ // Non-Verbose mode: The stdout/stderr output from the command goes to
+ // capture stdout/stderr respectively. No command execution log, but
+ // the command execution errors go to constructor stderr. Nothing goes
+ // to opts stdout/stderr.
+ for _, verbose := range []bool{false, true} {
+ var cnstrStdout, cnstrStderr, optsStdout, optsStderr, captureStdout, captureStderr bytes.Buffer
+ cstrVerbose := false // irellevant, the opts verbose flag takes precedence.
+ seq := runutil.NewSequence(nil, os.Stdin, &cnstrStdout, &cnstrStderr, false, false, cstrVerbose)
+ opts := runutil.Opts{Stdout: &optsStdout, Stderr: &optsStderr, Verbose: verbose}
+ seq.Opts(opts).
+ Capture(&captureStdout, &captureStderr).
+ Run("bash", "-c", "echo a; echo b >&2").
+ Opts(opts).
+ Timeout(time.Microsecond).
+ Run("sleep", "10000")
+ want := ""
+ if verbose {
+ want = `[hh:mm:ss.xx] >> bash -c "echo a; echo b >&2"
+[hh:mm:ss.xx] >> OK
+[hh:mm:ss.xx] >> sleep 10000
+[hh:mm:ss.xx] >> TIMED OUT
+`
+ }
+ if got := sanitizeTimestamps(cnstrStdout.String()); want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ if got, want := cnstrStderr.String(), "Waiting for command to exit: [\"sleep\" \"10000\"]\n"; want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ if got, want := optsStdout.String(), "Current Directory: "+cwd+"\n"; want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ if got, want := optsStderr.String(), ""; want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ if got, want := captureStdout.String(), "a\n"; want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ if got, want := captureStderr.String(), "b\n"; want != got {
+ t.Errorf("verbose: %t, got %v, want %v", verbose, got, want)
+ }
+ }
}
func TestSequence(t *testing.T) {
@@ -66,14 +219,13 @@
t.Errorf("got %v, want %v", got, want)
}
out.Reset()
- err = seq.Run("./bound-to-fail").Done()
+ err = seq.Run("./bound-to-fail", "fail").Done()
if err == nil {
t.Fatalf("should have experience an error")
}
- if got, want := err.Error(), "fork/exec ./bound-to-fail: no such file or directory"; got != want {
+ if got, want := rmLineNumbers(err.Error()), "sequence_test.go:-: Run(\"./bound-to-fail\", \"fail\"): fork/exec ./bound-to-fail: no such file or directory"; got != want {
t.Errorf("got %v, want %v", got, want)
}
-
err = seq.
Capture(&out, nil).Run("echo", "works, despite previous error").Done()
if err != nil {
@@ -83,9 +235,8 @@
t.Errorf("got %v, want %v", got, want)
}
out.Reset()
-
err = seq.Timeout(time.Second).Run("sleep", "10").Done()
- if got, want := err.Error(), "command timed out"; got != want {
+ if got, want := rmLineNumbers(err.Error()), "sequence_test.go:-: Run(\"sleep\", \"10\"): command timed out"; got != want {
t.Errorf("got %v, want %v", got, want)
}
@@ -102,8 +253,7 @@
}
err := seq.
Capture(&out, nil).Opts(opts).Run("sh", "-c", "echo $MYTEST").
- Capture(&out, nil).Run("sh", "-c", "echo $MYTEST").
- Done()
+ Capture(&out, nil).Last("sh", "-c", "echo $MYTEST")
if err != nil {
t.Fatal(err)
}
@@ -124,6 +274,50 @@
}
}
+func TestSequenceOutputOnError(t *testing.T) {
+ var out bytes.Buffer
+ // Only the output from the command that generates an error is written
+ // to out when not in verbose mode.
+ seq := runutil.NewSequence(nil, os.Stdin, &out, os.Stderr, false, false, false)
+ err := seq.Run("sh", "-c", "echo not me").
+ Run("sh", "-c", "echo ooh; echo ah; echo me; exit 1").
+ Last("sh", "-c", "echo not me either")
+ if err == nil {
+ t.Errorf("expected an error")
+ }
+ if got, want := out.String(), "oh\nah"; !strings.Contains(got, want) {
+ t.Errorf("got %v doesn't contain %v", got, want)
+ }
+ if got, notWant := out.String(), "not me either"; strings.Contains(got, notWant) {
+ t.Errorf("got %v contains %v", got, notWant)
+ }
+
+ out.Reset()
+ err = seq.Run("sh", "-c", "echo hard to not include me").
+ Run("sh", "-c", "echo ooh; echo ah; echo me").
+ Last("sh", "-c", "echo not me either")
+ if err != nil {
+ t.Error(err)
+ }
+ if got, want := len(out.String()), 0; got != want {
+ t.Logf(out.String())
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ out.Reset()
+ // All output is written to out when in verbose mode.
+ seq = runutil.NewSequence(nil, os.Stdin, &out, os.Stderr, false, false, true)
+ err = seq.Run("sh", "-c", "echo AA").
+ Run("sh", "-c", "echo BB; exit 1").
+ Last("sh", "-c", "echo CC")
+ if got, want := strings.Count(out.String(), "\n"), 8; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ if got, want := strings.Count(out.String(), "AA"), 2; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
type timestamped struct {
times []time.Time
data [][]byte
@@ -173,11 +367,65 @@
if err != nil {
t.Fatal(err)
}
- if got, want := string(data), "aha\n"; got != want {
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, want := string(data), "aha\nCurrent Directory: "+cwd+"\n"; got != want {
t.Errorf("got %v, want %v", got, want)
}
}
+func getwd(t *testing.T) string {
+ here, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return here
+}
+
+func TestSequencePushPop(t *testing.T) {
+ here := getwd(t)
+ s := runutil.NewSequence(nil, os.Stdin, os.Stdout, os.Stderr, false, false, true)
+ components := []string{here, "test", "a", "b", "c"}
+ tree := filepath.Join(components...)
+ s.MkdirAll(tree, os.FileMode(0755))
+ if err := s.Error(); err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(filepath.Join(here, "test"))
+
+ td := ""
+ for _, d := range components {
+ s.Pushd(d)
+ td = filepath.Join(td, d)
+ if got, want := getwd(t), td; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ }
+ s.Done()
+ if got, want := getwd(t), here; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ s.Pushd("test").Pushd("a").Pushd("b")
+ if got, want := getwd(t), filepath.Join(here, "test", "a", "b"); got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ err := s.Pushd("x").Done()
+ if err == nil {
+ t.Fatal(fmt.Errorf("expected an error"))
+ }
+ // Make sure the stack is unwound on error.
+ if got, want := getwd(t), here; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ if err := os.Chdir(here); err != nil {
+ panic(fmt.Sprintf("failed to chdir back to %s", here))
+ }
+
+ }
+}
+
// TODO(cnicolaou):
// - tests for functions
// - tests for terminating functions, make sure they clean up correctly.
diff --git a/runutil/start_test.go b/runutil/start_test.go
index 2682983..ef23759 100644
--- a/runutil/start_test.go
+++ b/runutil/start_test.go
@@ -9,7 +9,7 @@
"io/ioutil"
"os"
"path/filepath"
- "strings"
+ "syscall"
"testing"
"time"
)
@@ -17,23 +17,28 @@
// TestStartCommandOK tests start.Command() returns immediately without waiting
// for the command to complete.
func TestStartCommandOK(t *testing.T) {
- var out bytes.Buffer
- start := NewStart(nil, os.Stdin, &out, ioutil.Discard, false, false, true)
- bin, err := buildTestProgram(NewRun(nil, os.Stdin, &out, ioutil.Discard, false, false, true), "slow_hello2")
+ start := NewStart(nil, os.Stdin, ioutil.Discard, ioutil.Discard, false, false, true)
+ bin, err := buildTestProgram(NewRun(nil, os.Stdin, ioutil.Discard, ioutil.Discard, false, false, true), "slow_hello2")
if bin != "" {
defer os.RemoveAll(filepath.Dir(bin))
}
if err != nil {
t.Fatalf("%v", err)
}
- if _, err := start.Command(bin); err != nil {
- t.Fatalf(`Command("go run ./testdata/slow_hello2.go") failed: %v`, err)
+ cmd, err := start.Command(bin)
+ if err != nil {
+ t.Fatalf(`Command("go run ./testdata/slow_hello2.go") failed to start: %v`, err)
}
- // Note that the output shouldn't have "hello!!" because start.Command won't
- // wait for the command to finish.
- output := removeTimestamps(t, &out)
- if strings.Index(output, "hello!!") != -1 {
- t.Fatalf("output shouldn't contain 'hello!!':\n%v", output)
+ pid := cmd.Process.Pid
+ // Wait a sec and check that the child process is still around.
+ time.Sleep(time.Second)
+ if err := syscall.Kill(pid, 0); err != nil {
+ t.Fatalf(`Command("go run ./testdata/slow_hello2.go") already exited`)
+ }
+ // We're satisfied. Go ahead and kill the child to avoid leaving it
+ // running after the test completes.
+ if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
+ t.Fatalf(`Command("go run ./testdata/slow_hello2.go") couldn't be killed`)
}
}
@@ -42,10 +47,13 @@
start := NewStart(nil, os.Stdin, &runOut, ioutil.Discard, false, false, true)
opts := start.Opts()
opts.Stdout = &cmdOut
- if _, err := start.CommandWithOpts(opts, "go", "run", "./testdata/ok_hello.go"); err != nil {
+ cmd, err := start.CommandWithOpts(opts, "go", "run", "./testdata/ok_hello.go")
+ if err != nil {
+ t.Fatalf(`Command("go run ./testdata/ok_hello.go") failed to start: %v`, err)
+ }
+ if err := cmd.Wait(); err != nil {
t.Fatalf(`Command("go run ./testdata/ok_hello.go") failed: %v`, err)
}
- time.Sleep(time.Second * 3)
if got, want := removeTimestamps(t, &cmdOut), "hello\n"; got != want {
t.Fatalf("unexpected output:\ngot\n%v\nwant\n%v", got, want)
}
diff --git a/tool/context.go b/tool/context.go
index fc2da35..0efecbb 100644
--- a/tool/context.go
+++ b/tool/context.go
@@ -89,12 +89,10 @@
func NewContext(opts ContextOpts) *Context {
initOpts(newContextOpts(), &opts)
run := runutil.NewRun(opts.Env, opts.Stdin, opts.Stdout, opts.Stderr, *opts.Color, *opts.DryRun, *opts.Verbose)
- seq := runutil.NewSequence(opts.Env, opts.Stdin, opts.Stdout, opts.Stderr, *opts.Color, *opts.DryRun, *opts.Verbose)
start := runutil.NewStart(opts.Env, opts.Stdin, opts.Stdout, opts.Stderr, *opts.Color, *opts.DryRun, *opts.Verbose)
return &Context{
opts: opts,
run: run,
- seq: seq,
start: start,
}
}
@@ -197,9 +195,10 @@
return ctx.run
}
-// Seq returns the sequence instance of the context.
-func (ctx Context) Seq() *runutil.Sequence {
- return ctx.seq
+// NewSeq returns a new instance of Sequence initialized using the options
+// stored in the context.
+func (ctx Context) NewSeq() *runutil.Sequence {
+ return runutil.NewSequence(ctx.opts.Env, ctx.opts.Stdin, ctx.opts.Stdout, ctx.opts.Stderr, *ctx.opts.Color, *ctx.opts.DryRun, *ctx.opts.Verbose)
}
// Start returns the start instance of the context.