TBR
v: renaming the v directory to go
Change-Id: I4fd9f6ee2895d8034c23b65927eb118980b3c17a
diff --git a/lib/cmdline/cmdline.go b/lib/cmdline/cmdline.go
new file mode 100644
index 0000000..d6705e6
--- /dev/null
+++ b/lib/cmdline/cmdline.go
@@ -0,0 +1,362 @@
+// Package cmdline provides a data-driven framework to simplify writing
+// command-line programs. It includes built-in support for formatted help.
+//
+// Commands may be linked together to form a command tree. Since commands may
+// be arbitrarily nested within other commands, it's easy to create wrapper
+// programs that invoke existing commands.
+//
+// The syntax for each command-line program is:
+//
+// command [flags] [subcommand [flags]]* [args]
+//
+// Each sequence of flags on the command-line is associated with the command
+// that immediately precedes them. Global flags registered with the standard
+// flags package are allowed anywhere a command-specific flag is allowed.
+package cmdline
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+)
+
+// ErrUsage is returned to indicate an error in command usage; e.g. unknown
+// flags, subcommands or args.
+var ErrUsage = errors.New("usage error")
+
+// Command represents a single command in a command-line program. A program
+// with subcommands is represented as a root Command with children representing
+// each subcommand. The command graph must be a tree; each command may either
+// have exactly one parent (a sub-command), or no parent (the root), and cycles
+// are not allowed. This makes it easier to display the usage for subcommands.
+type Command struct {
+ Name string // Name of the command.
+ Short string // Short description, shown in help called on parent.
+ Long string // Long description, shown in help called on itself.
+ Flags flag.FlagSet // Flags for the command.
+ ArgsName string // Name of the args, shown in usage line.
+ ArgsLong string // Long description of the args, shown in help.
+
+ // Children of the command. The framework will match args[0] against each
+ // child's name, and call Run on the first matching child.
+ Children []*Command
+
+ // Run is a function that runs cmd with args. If both Children and Run are
+ // specified, Run will only be called if none of the children match. It is an
+ // error if neither is specified.
+ Run func(cmd *Command, args []string) error
+
+ // parent holds the parent of this Command, or nil if this is the root.
+ parent *Command
+
+ // Stdout and stderr are set through Init.
+ stdout, stderr io.Writer
+
+ // parseFlags holds the merged flags used for parsing. Each command starts
+ // with its own Flags, and we merge in all global flags. If the same flag is
+ // specified in both sets, the command's own flag wins.
+ parseFlags *flag.FlagSet
+
+ // TODO(toddw): If necessary we can add alias support, e.g. for abbreviations.
+ // Alias map[string]string
+}
+
+// style describes the formatting style for usage descriptions.
+type style int
+
+const (
+ styleText style = iota // Default style, good for cmdline output.
+ styleGoDoc // Style good for godoc processing.
+)
+
+// String returns the human-readable representation of the style.
+func (s *style) String() string {
+ switch *s {
+ case styleText:
+ return "text"
+ case styleGoDoc:
+ return "godoc"
+ default:
+ panic(fmt.Errorf("Unhandled style %d", *s))
+ }
+}
+
+// Set implements the flag.Value interface method.
+func (s *style) Set(value string) error {
+ switch value {
+ case "text":
+ *s = styleText
+ case "godoc":
+ *s = styleGoDoc
+ default:
+ return fmt.Errorf("Unknown style %q", value)
+ }
+ return nil
+}
+
+// Stdout is where output goes. Typically os.Stdout.
+func (cmd *Command) Stdout() io.Writer {
+ return cmd.stdout
+}
+
+// Stderr is where error messages go. Typically os.Stderr
+func (cmd *Command) Stderr() io.Writer {
+ return cmd.stderr
+}
+
+// Errorf should be called to signal an invalid usage of the command.
+func (cmd *Command) Errorf(format string, v ...interface{}) error {
+ fmt.Fprint(cmd.stderr, "ERROR: ")
+ fmt.Fprintf(cmd.stderr, format, v...)
+ fmt.Fprint(cmd.stderr, "\n\n")
+ cmd.usage(cmd.stderr, styleText, true)
+ return ErrUsage
+}
+
+// usage prints the usage of cmd to the writer, with the given style. The
+// firstCall boolean is set to false when printing usage for multiple commands,
+// and is used to avoid printing redundant information (e.g. section headers,
+// global flags).
+func (cmd *Command) usage(w io.Writer, style style, firstCall bool) {
+ var names []string
+ for c := cmd; c != nil; c = c.parent {
+ names = append([]string{c.Name}, names...)
+ }
+ namestr := strings.Join(names, " ")
+ if !firstCall && style == styleGoDoc {
+ // Title-case names so that godoc recognizes it as a section header.
+ fmt.Fprintf(w, "%s\n\n", strings.Title(namestr))
+ }
+ // Long description.
+ fmt.Fprint(w, strings.Trim(cmd.Long, "\n"))
+ fmt.Fprintln(w)
+ // Usage line.
+ hasFlags := false
+ cmd.Flags.VisitAll(func(*flag.Flag) {
+ hasFlags = true
+ })
+ fmt.Fprintf(w, "\nUsage:\n")
+ nameflags := " " + namestr
+ if hasFlags {
+ nameflags += " [flags]"
+ }
+ if len(cmd.Children) > 0 {
+ fmt.Fprintf(w, "%s <command>\n", nameflags)
+ }
+ if cmd.Run != nil {
+ if cmd.ArgsName != "" {
+ fmt.Fprintf(w, "%s %s\n", nameflags, cmd.ArgsName)
+ } else {
+ fmt.Fprintf(w, "%s\n", nameflags)
+ }
+ }
+ if len(cmd.Children) == 0 && cmd.Run == nil {
+ // This is a specification error.
+ fmt.Fprintf(w, "%s [ERROR: neither Children nor Run is specified]\n", nameflags)
+ }
+ // Commands.
+ if len(cmd.Children) > 0 {
+ fmt.Fprintf(w, "\nThe %s commands are:\n", cmd.Name)
+ for _, child := range cmd.Children {
+ fmt.Fprintf(w, " %-11s %s\n", child.Name, child.Short)
+ }
+ }
+ // Args.
+ if cmd.Run != nil && cmd.ArgsLong != "" {
+ fmt.Fprintf(w, "\n")
+ fmt.Fprint(w, strings.Trim(cmd.ArgsLong, "\n"))
+ fmt.Fprintf(w, "\n")
+ }
+ // Flags.
+ if hasFlags {
+ fmt.Fprintf(w, "\nThe %s flags are:\n", cmd.Name)
+ cmd.Flags.VisitAll(func(f *flag.Flag) {
+ fmt.Fprintf(w, " -%s=%s: %s\n", f.Name, f.DefValue, f.Usage)
+ })
+ }
+ // Global flags.
+ hasGlobalFlags := false
+ flag.VisitAll(func(*flag.Flag) {
+ hasGlobalFlags = true
+ })
+ if firstCall && hasGlobalFlags {
+ fmt.Fprintf(w, "\nThe global flags are:\n")
+ flag.VisitAll(func(f *flag.Flag) {
+ fmt.Fprintf(w, " -%s=%s: %s\n", f.Name, f.DefValue, f.Usage)
+ })
+ }
+}
+
+// newDefaultHelp creates a new default help command. We need to create new
+// instances since the parent for each help command is different.
+func newDefaultHelp() *Command {
+ helpStyle := styleText
+ help := &Command{
+ Name: helpName,
+ Short: "Display help for commands",
+ Long: `
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+`,
+ ArgsName: "<command>",
+ ArgsLong: `
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+`,
+ Run: func(cmd *Command, args []string) error {
+ // Help applies to its parent - e.g. "foo help" applies to the foo command.
+ return runHelp(cmd.parent, args, helpStyle)
+ },
+ }
+ help.Flags.Var(&helpStyle, "style", `The formatting style for help output, either "text" or "godoc".`)
+ return help
+}
+
+const helpName = "help"
+
+// runHelp runs the "help" command.
+func runHelp(cmd *Command, args []string, style style) error {
+ if len(args) == 0 {
+ cmd.usage(cmd.stdout, style, true)
+ return nil
+ }
+ if args[0] == "..." {
+ recursiveHelp(cmd, style, true)
+ return nil
+ }
+ // Find the subcommand to display help.
+ subName := args[0]
+ subArgs := args[1:]
+ for _, child := range cmd.Children {
+ if child.Name == subName {
+ return runHelp(child, subArgs, style)
+ }
+ }
+ return cmd.Errorf("%s: unknown command %q", cmd.Name, subName)
+}
+
+// recursiveHelp prints help recursively via DFS from this cmd onward.
+func recursiveHelp(cmd *Command, style style, firstCall bool) {
+ cmd.usage(cmd.stdout, style, firstCall)
+ switch style {
+ case styleText:
+ fmt.Fprintln(cmd.stdout, strings.Repeat("=", 80))
+ case styleGoDoc:
+ fmt.Fprintln(cmd.stdout)
+ }
+ for _, child := range cmd.Children {
+ recursiveHelp(child, style, false)
+ }
+}
+
+// prefixErrorWriter simply wraps a regular io.Writer and adds an "ERROR: "
+// prefix if Write is ever called. It's used to ensure errors are clearly
+// marked when flag.FlagSet.Parse encounters errors.
+type prefixErrorWriter struct {
+ writer io.Writer
+ prefixWritten bool
+}
+
+func (p *prefixErrorWriter) Write(b []byte) (int, error) {
+ if !p.prefixWritten {
+ io.WriteString(p.writer, "ERROR: ")
+ p.prefixWritten = true
+ }
+ return p.writer.Write(b)
+}
+
+// Init initializes all nodes in the command tree rooted at cmd. Init must be
+// called before Execute.
+func (cmd *Command) Init(parent *Command, stdout, stderr io.Writer) {
+ cmd.parent = parent
+ cmd.stdout = stdout
+ cmd.stderr = stderr
+ // Add help command, if it doesn't already exist.
+ hasHelp := false
+ for _, child := range cmd.Children {
+ if child.Name == helpName {
+ hasHelp = true
+ break
+ }
+ }
+ if !hasHelp && cmd.Name != helpName && len(cmd.Children) > 0 {
+ cmd.Children = append(cmd.Children, newDefaultHelp())
+ }
+ // Merge command-specific and global flags into parseFlags.
+ cmd.parseFlags = flag.NewFlagSet(cmd.Name, flag.ContinueOnError)
+ cmd.parseFlags.SetOutput(&prefixErrorWriter{writer: stderr})
+ cmd.parseFlags.Usage = func() {
+ cmd.usage(stderr, styleText, true)
+ }
+ flagMerger := func(f *flag.Flag) {
+ if cmd.parseFlags.Lookup(f.Name) == nil {
+ cmd.parseFlags.Var(f.Value, f.Name, f.Usage)
+ }
+ }
+ cmd.Flags.VisitAll(flagMerger)
+ flag.VisitAll(flagMerger)
+ // Call children recursively.
+ for _, child := range cmd.Children {
+ child.Init(cmd, stdout, stderr)
+ }
+}
+
+// Execute the command with the given args. The returned error is ErrUsage if
+// there are usage errors, otherwise it is whatever the leaf command returns
+// from its Run function.
+func (cmd *Command) Execute(args []string) error {
+ // Parse the merged flags.
+ if err := cmd.parseFlags.Parse(args); err != nil {
+ return ErrUsage
+ }
+ args = cmd.parseFlags.Args()
+ // Look for matching children.
+ if len(args) > 0 {
+ subName := args[0]
+ subArgs := args[1:]
+ for _, child := range cmd.Children {
+ if child.Name == subName {
+ return child.Execute(subArgs)
+ }
+ }
+ }
+ // No matching children, try Run.
+ if cmd.Run != nil {
+ if cmd.ArgsName == "" && len(args) > 0 {
+ if len(cmd.Children) > 0 {
+ return cmd.Errorf("%s: unknown command %q", cmd.Name, args[0])
+ } else {
+ return cmd.Errorf("%s doesn't take any arguments", cmd.Name)
+ }
+ }
+ return cmd.Run(cmd, args)
+ }
+ switch {
+ case len(cmd.Children) == 0:
+ return cmd.Errorf("%s: neither Children nor Run is specified", cmd.Name)
+ case len(args) > 0:
+ return cmd.Errorf("%s: unknown command %q", cmd.Name, args[0])
+ default:
+ return cmd.Errorf("%s: no command specified", cmd.Name)
+ }
+}
+
+// Main executes the command tree rooted at cmd, writing output to os.Stdout,
+// writing errors to os.Stderr, and getting args from os.Args. We'll call
+// os.Exit with a non-zero exit code on errors. It's meant as a simple
+// one-liner for the main function of command-line tools.
+func (cmd *Command) Main() {
+ cmd.Init(nil, os.Stdout, os.Stderr)
+ if err := cmd.Execute(os.Args[1:]); err != nil {
+ if err == ErrUsage {
+ os.Exit(1)
+ } else {
+ fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
+ os.Exit(2)
+ }
+ }
+}
diff --git a/lib/cmdline/cmdline_test.go b/lib/cmdline/cmdline_test.go
new file mode 100644
index 0000000..6fdf3c5
--- /dev/null
+++ b/lib/cmdline/cmdline_test.go
@@ -0,0 +1,1879 @@
+package cmdline
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "regexp"
+ "strings"
+ "testing"
+)
+
+var (
+ errEcho = errors.New("echo error")
+ flagExtra bool
+ optNoNewline bool
+ flagTopLevelExtra bool
+ globalFlag1 string
+ globalFlag2 *int64
+)
+
+// runEcho is used to implement commands for our tests.
+func runEcho(cmd *Command, args []string) error {
+ if len(args) == 1 {
+ if args[0] == "error" {
+ return errEcho
+ } else if args[0] == "bad_arg" {
+ return cmd.Errorf("Invalid argument %v", args[0])
+ }
+ }
+ if flagExtra {
+ args = append(args, "extra")
+ }
+ if flagTopLevelExtra {
+ args = append(args, "tlextra")
+ }
+ if optNoNewline {
+ fmt.Fprint(cmd.Stdout(), args)
+ } else {
+ fmt.Fprintln(cmd.Stdout(), args)
+ }
+ return nil
+}
+
+// runHello is another function for test commands.
+func runHello(cmd *Command, args []string) error {
+ if flagTopLevelExtra {
+ args = append(args, "tlextra")
+ }
+ fmt.Fprintln(cmd.Stdout(), strings.Join(append([]string{"Hello"}, args...), " "))
+ return nil
+}
+
+type testCase struct {
+ Args []string
+ Err error
+ Stdout string
+ Stderr string
+ GlobalFlag1 string
+ GlobalFlag2 int64
+}
+
+func init() {
+ flag.StringVar(&globalFlag1, "global1", "", "global test flag 1")
+ globalFlag2 = flag.Int64("global2", 0, "global test flag 2")
+}
+
+func matchOutput(actual, expect string) bool {
+ // The global flags include the flags from the testing package, so strip them
+ // out before the comparison.
+ re := regexp.MustCompile(" -test.*\n")
+ return re.ReplaceAllLiteralString(actual, "") == expect
+}
+
+func runTestCases(t *testing.T, cmd *Command, tests []testCase) {
+ for _, test := range tests {
+ // Reset global variables before running each test case.
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ flagExtra = false
+ flagTopLevelExtra = false
+ optNoNewline = false
+ globalFlag1 = ""
+ *globalFlag2 = 0
+
+ // Run the execute function and check against expected results.
+ cmd.Init(nil, &stdout, &stderr)
+ if err := cmd.Execute(test.Args); err != test.Err {
+ t.Errorf("Ran with args %q\nEXPECTED error:\n%q\nACTUAL error:\n%q", test.Args, test.Err, err)
+ }
+ if !matchOutput(stdout.String(), test.Stdout) {
+ t.Errorf("Ran with args %q\nEXPECTED stdout:\n%q\nACTUAL stdout:\n%q", test.Args, test.Stdout, stdout.String())
+ }
+ if !matchOutput(stderr.String(), test.Stderr) {
+ t.Errorf("Ran with args %q\nEXPECTED stderr:\n%q\nACTUAL stderr:\n%q", test.Args, test.Stderr, stderr.String())
+ }
+ if globalFlag1 != test.GlobalFlag1 {
+ t.Errorf("Value for global1 flag %q\nEXPECTED %q", globalFlag1, test.GlobalFlag1)
+ }
+ if *globalFlag2 != test.GlobalFlag2 {
+ t.Errorf("Value for global2 flag %q\nEXPECTED %q", globalFlag2, test.GlobalFlag2)
+ }
+ }
+}
+
+func TestNoCommands(t *testing.T) {
+ cmd := &Command{
+ Name: "nocmds",
+ Short: "Nocmds is invalid.",
+ Long: "Nocmds has no commands and no run function.",
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: nocmds: neither Children nor Run is specified
+
+Nocmds has no commands and no run function.
+
+Usage:
+ nocmds [ERROR: neither Children nor Run is specified]
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: nocmds: neither Children nor Run is specified
+
+Nocmds has no commands and no run function.
+
+Usage:
+ nocmds [ERROR: neither Children nor Run is specified]
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ }
+ runTestCases(t, cmd, tests)
+}
+
+func TestOneCommand(t *testing.T) {
+ cmdEcho := &Command{
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ Run: runEcho,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+
+ prog := &Command{
+ Name: "onecmd",
+ Short: "Onecmd program.",
+ Long: "Onecmd only has the echo command.",
+ Children: []*Command{cmdEcho},
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: onecmd: no command specified
+
+Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: onecmd: unknown command "foo"
+
+Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "echo"},
+ Stdout: `Echo prints any strings passed in to stdout.
+
+Usage:
+ onecmd echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "help"},
+ Stdout: `Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ onecmd help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Echo prints any strings passed in to stdout.
+
+Usage:
+ onecmd echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ onecmd help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: onecmd: unknown command "foo"
+
+Onecmd only has the echo command.
+
+Usage:
+ onecmd <command>
+
+The onecmd commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echo", "bad_arg"},
+ Err: ErrUsage,
+ Stderr: `ERROR: Invalid argument bad_arg
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ onecmd echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
+
+func TestMultiCommands(t *testing.T) {
+ cmdEcho := &Command{
+ Run: runEcho,
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+ var cmdEchoOpt = &Command{
+ Run: runEcho,
+ Name: "echoopt",
+ Short: "Print strings on stdout, with opts",
+ // Try varying number of header/trailer newlines around the long description.
+ Long: `Echoopt prints any args passed in to stdout.
+
+
+`,
+ ArgsName: "[args]",
+ ArgsLong: "[args] are arbitrary strings that will be echoed.",
+ }
+ cmdEchoOpt.Flags.BoolVar(&optNoNewline, "n", false, "Do not output trailing newline")
+
+ prog := &Command{
+ Name: "multi",
+ Short: "Multi test command",
+ Long: "Multi has two variants of echo.",
+ Children: []*Command{cmdEcho, cmdEchoOpt},
+ }
+ prog.Flags.BoolVar(&flagExtra, "extra", false, "Print an extra arg")
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: multi: no command specified
+
+Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands
+
+The multi flags are:
+ -extra=false: Print an extra arg
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands
+
+The multi flags are:
+ -extra=false: Print an extra arg
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands
+
+The multi flags are:
+ -extra=false: Print an extra arg
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Echo prints any strings passed in to stdout.
+
+Usage:
+ multi echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Echoopt prints any args passed in to stdout.
+
+Usage:
+ multi echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The echoopt flags are:
+ -n=false: Do not output trailing newline
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ multi help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"help", "echo"},
+ Stdout: `Echo prints any strings passed in to stdout.
+
+Usage:
+ multi echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "echoopt"},
+ Stdout: `Echoopt prints any args passed in to stdout.
+
+Usage:
+ multi echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The echoopt flags are:
+ -n=false: Do not output trailing newline
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: multi: unknown command "foo"
+
+Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands
+
+The multi flags are:
+ -extra=false: Print an extra arg
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"-extra", "echo", "foo", "bar"},
+ Stdout: "[foo bar extra]\n",
+ },
+ {
+ Args: []string{"echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echoopt", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"-extra", "echoopt", "foo", "bar"},
+ Stdout: "[foo bar extra]\n",
+ },
+ {
+ Args: []string{"echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar]",
+ },
+ {
+ Args: []string{"-extra", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar extra]",
+ },
+ {
+ Args: []string{"-global1=globalStringValue", "-extra", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar extra]",
+ GlobalFlag1: "globalStringValue",
+ },
+ {
+ Args: []string{"-global2=42", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar]",
+ GlobalFlag2: 42,
+ },
+ {
+ Args: []string{"-global1=globalStringOtherValue", "-global2=43", "-extra", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar extra]",
+ GlobalFlag1: "globalStringOtherValue",
+ GlobalFlag2: 43,
+ },
+ {
+ Args: []string{"echoopt", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echo", "-n", "foo", "bar"},
+ Err: ErrUsage,
+ Stderr: `ERROR: flag provided but not defined: -n
+Echo prints any strings passed in to stdout.
+
+Usage:
+ multi echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"-nosuchflag", "echo", "foo", "bar"},
+ Err: ErrUsage,
+ Stderr: `ERROR: flag provided but not defined: -nosuchflag
+Multi has two variants of echo.
+
+Usage:
+ multi [flags] <command>
+
+The multi commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands
+
+The multi flags are:
+ -extra=false: Print an extra arg
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
+
+func TestMultiLevelCommands(t *testing.T) {
+ cmdEcho := &Command{
+ Run: runEcho,
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+ cmdEchoOpt := &Command{
+ Run: runEcho,
+ Name: "echoopt",
+ Short: "Print strings on stdout, with opts",
+ // Try varying number of header/trailer newlines around the long description.
+ Long: `Echoopt prints any args passed in to stdout.
+
+
+`,
+ ArgsName: "[args]",
+ ArgsLong: "[args] are arbitrary strings that will be echoed.",
+ }
+ cmdEchoOpt.Flags.BoolVar(&optNoNewline, "n", false, "Do not output trailing newline")
+ cmdHello := &Command{
+ Run: runHello,
+ Name: "hello",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ }
+ echoProg := &Command{
+ Name: "echoprog",
+ Short: "Set of echo commands",
+ Long: "Echoprog has two variants of echo.",
+ Children: []*Command{cmdEcho, cmdEchoOpt},
+ }
+ echoProg.Flags.BoolVar(&flagExtra, "extra", false, "Print an extra arg")
+ prog := &Command{
+ Name: "toplevelprog",
+ Short: "Top level prog",
+ Long: "Toplevelprog has the echo subprogram and the hello command.",
+ Children: []*Command{echoProg, cmdHello},
+ }
+ prog.Flags.BoolVar(&flagTopLevelExtra, "tlextra", false, "Print an extra arg for all commands")
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: toplevelprog: no command specified
+
+Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+The toplevelprog flags are:
+ -tlextra=false: Print an extra arg for all commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+The toplevelprog flags are:
+ -tlextra=false: Print an extra arg for all commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+The toplevelprog flags are:
+ -tlextra=false: Print an extra arg for all commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Echoprog has two variants of echo.
+
+Usage:
+ toplevelprog echoprog [flags] <command>
+
+The echoprog commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands
+
+The echoprog flags are:
+ -extra=false: Print an extra arg
+================================================================================
+Echo prints any strings passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Echoopt prints any args passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The echoopt flags are:
+ -n=false: Do not output trailing newline
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ toplevelprog echoprog help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ toplevelprog hello [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ toplevelprog help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"help", "echoprog"},
+ Stdout: `Echoprog has two variants of echo.
+
+Usage:
+ toplevelprog echoprog [flags] <command>
+
+The echoprog commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands
+
+The echoprog flags are:
+ -extra=false: Print an extra arg
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"echoprog", "help", "..."},
+ Stdout: `Echoprog has two variants of echo.
+
+Usage:
+ toplevelprog echoprog [flags] <command>
+
+The echoprog commands are:
+ echo Print strings on stdout
+ echoopt Print strings on stdout, with opts
+ help Display help for commands
+
+The echoprog flags are:
+ -extra=false: Print an extra arg
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Echo prints any strings passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Echoopt prints any args passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The echoopt flags are:
+ -n=false: Do not output trailing newline
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ toplevelprog echoprog help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"echoprog", "help", "echoopt"},
+ Stdout: `Echoopt prints any args passed in to stdout.
+
+Usage:
+ toplevelprog echoprog echoopt [flags] [args]
+
+[args] are arbitrary strings that will be echoed.
+
+The echoopt flags are:
+ -n=false: Do not output trailing newline
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "hello"},
+ Stdout: `Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ toplevelprog hello [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: toplevelprog: unknown command "foo"
+
+Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+The toplevelprog flags are:
+ -tlextra=false: Print an extra arg for all commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"echoprog", "echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echoprog", "-extra", "echo", "foo", "bar"},
+ Stdout: "[foo bar extra]\n",
+ },
+ {
+ Args: []string{"echoprog", "echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echoprog", "echoopt", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echoprog", "-extra", "echoopt", "foo", "bar"},
+ Stdout: "[foo bar extra]\n",
+ },
+ {
+ Args: []string{"echoprog", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar]",
+ },
+ {
+ Args: []string{"echoprog", "-extra", "echoopt", "-n", "foo", "bar"},
+ Stdout: "[foo bar extra]",
+ },
+ {
+ Args: []string{"echoprog", "echoopt", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"--tlextra", "echoprog", "-extra", "echoopt", "foo", "bar"},
+ Stdout: "[foo bar extra tlextra]\n",
+ },
+ {
+ Args: []string{"hello", "foo", "bar"},
+ Stdout: "Hello foo bar\n",
+ },
+ {
+ Args: []string{"--tlextra", "hello", "foo", "bar"},
+ Stdout: "Hello foo bar tlextra\n",
+ },
+ {
+ Args: []string{"hello", "--extra", "foo", "bar"},
+ Err: ErrUsage,
+ Stderr: `ERROR: flag provided but not defined: -extra
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ toplevelprog hello [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"-extra", "echoprog", "echoopt", "foo", "bar"},
+ Err: ErrUsage,
+ Stderr: `ERROR: flag provided but not defined: -extra
+Toplevelprog has the echo subprogram and the hello command.
+
+Usage:
+ toplevelprog [flags] <command>
+
+The toplevelprog commands are:
+ echoprog Set of echo commands
+ hello Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+The toplevelprog flags are:
+ -tlextra=false: Print an extra arg for all commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
+
+func TestMultiLevelCommandsOrdering(t *testing.T) {
+ cmdHello11 := &Command{
+ Name: "hello11",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello12 := &Command{
+ Name: "hello12",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello21 := &Command{
+ Name: "hello21",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello22 := &Command{
+ Name: "hello22",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello31 := &Command{
+ Name: "hello31",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ cmdHello32 := &Command{
+ Name: "hello32",
+ Short: "Print strings on stdout preceded by \"Hello\"",
+ Long: `
+Hello prints any strings passed in to stdout preceded by "Hello".
+`,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ Run: runHello,
+ }
+ progHello3 := &Command{
+ Name: "prog3",
+ Short: "Set of hello commands",
+ Long: "Prog3 has two variants of hello.",
+ Children: []*Command{cmdHello31, cmdHello32},
+ }
+ progHello2 := &Command{
+ Name: "prog2",
+ Short: "Set of hello commands",
+ Long: "Prog2 has two variants of hello and a subprogram prog3.",
+ Children: []*Command{cmdHello21, progHello3, cmdHello22},
+ }
+ progHello1 := &Command{
+ Name: "prog1",
+ Short: "Set of hello commands",
+ Long: "Prog1 has two variants of hello and a subprogram prog2.",
+ Children: []*Command{cmdHello11, cmdHello12, progHello2},
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Err: ErrUsage,
+ Stderr: `ERROR: prog1: no command specified
+
+Prog1 has two variants of hello and a subprogram prog2.
+
+Usage:
+ prog1 <command>
+
+The prog1 commands are:
+ hello11 Print strings on stdout preceded by "Hello"
+ hello12 Print strings on stdout preceded by "Hello"
+ prog2 Set of hello commands
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Prog1 has two variants of hello and a subprogram prog2.
+
+Usage:
+ prog1 <command>
+
+The prog1 commands are:
+ hello11 Print strings on stdout preceded by "Hello"
+ hello12 Print strings on stdout preceded by "Hello"
+ prog2 Set of hello commands
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Prog1 has two variants of hello and a subprogram prog2.
+
+Usage:
+ prog1 <command>
+
+The prog1 commands are:
+ hello11 Print strings on stdout preceded by "Hello"
+ hello12 Print strings on stdout preceded by "Hello"
+ prog2 Set of hello commands
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 hello11 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 hello12 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog2 has two variants of hello and a subprogram prog3.
+
+Usage:
+ prog1 prog2 <command>
+
+The prog2 commands are:
+ hello21 Print strings on stdout preceded by "Hello"
+ prog3 Set of hello commands
+ hello22 Print strings on stdout preceded by "Hello"
+ help Display help for commands
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello21 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+ help Display help for commands
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 prog2 prog3 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello22 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 prog2 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"prog2", "help", "..."},
+ Stdout: `Prog2 has two variants of hello and a subprogram prog3.
+
+Usage:
+ prog1 prog2 <command>
+
+The prog2 commands are:
+ hello21 Print strings on stdout preceded by "Hello"
+ prog3 Set of hello commands
+ hello22 Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello21 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+ help Display help for commands
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 prog2 prog3 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello22 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 prog2 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"prog2", "prog3", "help", "..."},
+ Stdout: `Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 prog2 prog3 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"help", "prog2", "prog3", "..."},
+ Stdout: `Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 prog2 prog3 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"help", "-style=godoc", "..."},
+ Stdout: `Prog1 has two variants of hello and a subprogram prog2.
+
+Usage:
+ prog1 <command>
+
+The prog1 commands are:
+ hello11 Print strings on stdout preceded by "Hello"
+ hello12 Print strings on stdout preceded by "Hello"
+ prog2 Set of hello commands
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+
+Prog1 Hello11
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 hello11 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Hello12
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 hello12 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Prog2
+
+Prog2 has two variants of hello and a subprogram prog3.
+
+Usage:
+ prog1 prog2 <command>
+
+The prog2 commands are:
+ hello21 Print strings on stdout preceded by "Hello"
+ prog3 Set of hello commands
+ hello22 Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+Prog1 Prog2 Hello21
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello21 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Prog2 Prog3
+
+Prog3 has two variants of hello.
+
+Usage:
+ prog1 prog2 prog3 <command>
+
+The prog3 commands are:
+ hello31 Print strings on stdout preceded by "Hello"
+ hello32 Print strings on stdout preceded by "Hello"
+ help Display help for commands
+
+Prog1 Prog2 Prog3 Hello31
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello31 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Prog2 Prog3 Hello32
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 prog3 hello32 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Prog2 Prog3 Help
+
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 prog2 prog3 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+
+Prog1 Prog2 Hello22
+
+Hello prints any strings passed in to stdout preceded by "Hello".
+
+Usage:
+ prog1 prog2 hello22 [strings]
+
+[strings] are arbitrary strings that will be printed.
+
+Prog1 Prog2 Help
+
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 prog2 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+
+Prog1 Help
+
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ prog1 help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+
+`,
+ },
+ }
+
+ runTestCases(t, progHello1, tests)
+}
+
+func TestCommandAndArgs(t *testing.T) {
+ cmdEcho := &Command{
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ Run: runEcho,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+
+ prog := &Command{
+ Name: "cmdargs",
+ Short: "Cmdargs program.",
+ Long: "Cmdargs has the echo command and a Run function with args.",
+ Children: []*Command{cmdEcho},
+ Run: runHello,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be printed.",
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Stdout: "Hello\n",
+ },
+ {
+ Args: []string{"foo"},
+ Stdout: "Hello foo\n",
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Cmdargs has the echo command and a Run function with args.
+
+Usage:
+ cmdargs <command>
+ cmdargs [strings]
+
+The cmdargs commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "echo"},
+ Stdout: `Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdargs echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Cmdargs has the echo command and a Run function with args.
+
+Usage:
+ cmdargs <command>
+ cmdargs [strings]
+
+The cmdargs commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdargs echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ cmdargs help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: cmdargs: unknown command "foo"
+
+Cmdargs has the echo command and a Run function with args.
+
+Usage:
+ cmdargs <command>
+ cmdargs [strings]
+
+The cmdargs commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+[strings] are arbitrary strings that will be printed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echo", "bad_arg"},
+ Err: ErrUsage,
+ Stderr: `ERROR: Invalid argument bad_arg
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdargs echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
+
+func TestCommandAndRunNoArgs(t *testing.T) {
+ cmdEcho := &Command{
+ Name: "echo",
+ Short: "Print strings on stdout",
+ Long: `
+Echo prints any strings passed in to stdout.
+`,
+ Run: runEcho,
+ ArgsName: "[strings]",
+ ArgsLong: "[strings] are arbitrary strings that will be echoed.",
+ }
+
+ prog := &Command{
+ Name: "cmdrun",
+ Short: "Cmdrun program.",
+ Long: "Cmdrun has the echo command and a Run function with no args.",
+ Children: []*Command{cmdEcho},
+ Run: runHello,
+ }
+
+ var tests = []testCase{
+ {
+ Args: []string{},
+ Stdout: "Hello\n",
+ },
+ {
+ Args: []string{"foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: cmdrun: unknown command "foo"
+
+Cmdrun has the echo command and a Run function with no args.
+
+Usage:
+ cmdrun <command>
+ cmdrun
+
+The cmdrun commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help"},
+ Stdout: `Cmdrun has the echo command and a Run function with no args.
+
+Usage:
+ cmdrun <command>
+ cmdrun
+
+The cmdrun commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "echo"},
+ Stdout: `Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdrun echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"help", "..."},
+ Stdout: `Cmdrun has the echo command and a Run function with no args.
+
+Usage:
+ cmdrun <command>
+ cmdrun
+
+The cmdrun commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+================================================================================
+Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdrun echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+================================================================================
+Help displays usage descriptions for this command, or usage descriptions for
+sub-commands.
+
+Usage:
+ cmdrun help [flags] <command>
+
+<command> is an optional sequence of commands to display detailed per-command
+usage. The special-case "help ..." recursively displays help for this command
+and all sub-commands.
+
+The help flags are:
+ -style=text: The formatting style for help output, either "text" or "godoc".
+================================================================================
+`,
+ },
+ {
+ Args: []string{"help", "foo"},
+ Err: ErrUsage,
+ Stderr: `ERROR: cmdrun: unknown command "foo"
+
+Cmdrun has the echo command and a Run function with no args.
+
+Usage:
+ cmdrun <command>
+ cmdrun
+
+The cmdrun commands are:
+ echo Print strings on stdout
+ help Display help for commands
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ {
+ Args: []string{"echo", "foo", "bar"},
+ Stdout: "[foo bar]\n",
+ },
+ {
+ Args: []string{"echo", "error"},
+ Err: errEcho,
+ },
+ {
+ Args: []string{"echo", "bad_arg"},
+ Err: ErrUsage,
+ Stderr: `ERROR: Invalid argument bad_arg
+
+Echo prints any strings passed in to stdout.
+
+Usage:
+ cmdrun echo [strings]
+
+[strings] are arbitrary strings that will be echoed.
+
+The global flags are:
+ -global1=: global test flag 1
+ -global2=0: global test flag 2
+`,
+ },
+ }
+ runTestCases(t, prog, tests)
+}
diff --git a/lib/glob/glob.go b/lib/glob/glob.go
new file mode 100644
index 0000000..b1356c1
--- /dev/null
+++ b/lib/glob/glob.go
@@ -0,0 +1,165 @@
+// glob implements a glob language.
+//
+// Globs match a slash separated series of glob expressions.
+//
+// pattern:
+// term ['/' term]*
+// term:
+// '*' matches any sequence of non-Separator characters
+// '?' matches any single non-Separator character
+// '[' [ '^' ] { character-range } ']'
+// character class (must be non-empty)
+// c matches character c (c != '*', '?', '\\', '[', '/')
+// '\\' c matches character c
+// character-range:
+// c matches character c (c != '\\', '-', ']')
+// '\\' c matches character c
+// lo '-' hi matches character c for lo <= c <= hi
+
+package glob
+
+import (
+ "path/filepath"
+ "strings"
+)
+
+// Glob represents a slash separated path glob expression.
+type Glob struct {
+ elems []string
+ recursive bool
+}
+
+// Parse returns a new Glob.
+func Parse(pattern string) (*Glob, error) {
+ if len(pattern) > 0 && pattern[0] == '/' {
+ return nil, filepath.ErrBadPattern
+ }
+
+ g := &Glob{}
+ if pattern != "" {
+ g.elems = strings.Split(pattern, "/")
+ }
+ if last := len(g.elems) - 1; last >= 0 && g.elems[last] == "..." {
+ g.elems = g.elems[:last]
+ g.recursive = true
+ }
+
+ // The only error we can get from the filepath library is badpattern.
+ // A future implementation would most likely recognize that here, so for now
+ // I'll just check every part to make sure it's error free.
+ for _, elem := range g.elems {
+ if _, err := filepath.Match(elem, ""); err != nil {
+ return nil, err
+ }
+ }
+
+ return g, nil
+}
+
+// Len returns the number of path elements represented by the glob expression.
+func (g *Glob) Len() int {
+ return len(g.elems)
+}
+
+// Finished returns true if the pattern cannot match anything.
+func (g *Glob) Finished() bool {
+ return !g.recursive && len(g.elems) == 0
+}
+
+// Split returns the suffix of g starting at the path element corresponding to start.
+func (g *Glob) Split(start int) *Glob {
+ if start >= len(g.elems) {
+ return &Glob{elems: nil, recursive: g.recursive}
+ }
+ return &Glob{elems: g.elems[start:], recursive: g.recursive}
+}
+
+// MatchInitialSegment tries to match segment against the initial element of g.
+// Returns a boolean indicating whether the match was successful and the
+// Glob representing the unmatched remainder of g.
+func (g *Glob) MatchInitialSegment(segment string) (bool, *Glob) {
+ if len(g.elems) == 0 {
+ if !g.recursive {
+ return false, nil
+ }
+ return true, g
+ }
+
+ if matches, err := filepath.Match(g.elems[0], segment); err != nil {
+ panic("Error in glob pattern found.")
+ } else if matches {
+ return true, g.Split(1)
+ }
+ return false, nil
+}
+
+// PartialMatch tries matching elems against part of a glob pattern.
+// The first return value is true if each element e_i of elems matches
+// the (start + i)th element of the glob pattern. If the first return
+// value is true, the second return value returns the unmatched suffix
+// of the pattern. It will be empty if the pattern is completely
+// matched.
+//
+// Note that if the glob is recursive elems can have more elements then
+// the glob pattern and still get a true result.
+func (g *Glob) PartialMatch(start int, elems []string) (bool, *Glob) {
+ g = g.Split(start)
+ for ; len(elems) > 0; elems = elems[1:] {
+ var matched bool
+ if matched, g = g.MatchInitialSegment(elems[0]); !matched {
+ return false, nil
+ }
+ }
+ return true, g
+}
+
+// isFixed returns the unescaped string and true if 's' is a pattern specifying
+// a fixed string. Otherwise it returns the original string and false.
+func isFixed(s string) (string, bool) {
+ // No special characters.
+ if !strings.ContainsAny(s, "*?[") {
+ return s, true
+ }
+ // Special characters and no backslash.
+ if !strings.ContainsAny(s, "\\") {
+ return "", false
+ }
+ unescaped := ""
+ escape := false
+ for _, c := range s {
+ if escape {
+ escape = false
+ unescaped += string(c)
+ } else if strings.ContainsRune("*?[", c) {
+ // S contains an unescaped special character.
+ return s, false
+ } else if c == '\\' {
+ escape = true
+ } else {
+ unescaped += string(c)
+ }
+ }
+ return unescaped, true
+}
+
+func (g *Glob) SplitFixedPrefix() ([]string, *Glob) {
+ var prefix []string
+ start := 0
+ for _, elem := range g.elems {
+ if u, q := isFixed(elem); q {
+ prefix = append(prefix, u)
+ start++
+ } else {
+ break
+ }
+ }
+ return prefix, g.Split(start)
+}
+
+func (g *Glob) String() string {
+ e := g.elems
+ if g.recursive {
+ e = append(e, "...")
+ }
+ return filepath.Join(e...)
+}
diff --git a/lib/glob/glob_test.go b/lib/glob/glob_test.go
new file mode 100644
index 0000000..bbef347
--- /dev/null
+++ b/lib/glob/glob_test.go
@@ -0,0 +1,40 @@
+package glob
+
+import (
+ "testing"
+)
+
+func same(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func TestStripFixedPrefix(t *testing.T) {
+ tests := []struct {
+ pattern string
+ fixed []string
+ rest string
+ }{
+ {"*", nil, "*"},
+ {"a/b/c/*", []string{"a", "b", "c"}, "*"},
+ {"a/b/*/...", []string{"a", "b"}, "*/..."},
+ {"a/b/c/...", []string{"a", "b", "c"}, "..."},
+ {"a/the\\?rain.in\\*spain", []string{"a", "the?rain.in*spain"}, ""},
+ }
+ for _, test := range tests {
+ g, err := Parse(test.pattern)
+ if err != nil {
+ t.Fatalf("parsing %q: %q", test.pattern, err.Error())
+ }
+ if f, ng := g.SplitFixedPrefix(); !same(f, test.fixed) || test.rest != ng.String() {
+ t.Fatalf("SplitFixedPrefix(%q) got %q,%q, expected %q,%q", test.pattern, f, ng.String(), test.fixed, test.rest)
+ }
+ }
+}
diff --git a/lib/signals/signals.go b/lib/signals/signals.go
new file mode 100644
index 0000000..e9b88fe
--- /dev/null
+++ b/lib/signals/signals.go
@@ -0,0 +1,77 @@
+package signals
+
+import (
+ "os"
+ "os/signal"
+ "syscall"
+
+ "veyron2/rt"
+)
+
+type stopSignal string
+
+func (stopSignal) Signal() {}
+func (s stopSignal) String() string { return string(s) }
+
+const (
+ STOP = stopSignal("")
+ DoubleStopExitCode = 1
+)
+
+// defaultSignals returns a set of platform-specific signals that an application
+// is encouraged to listen on.
+func defaultSignals() []os.Signal {
+ return []os.Signal{syscall.SIGTERM, syscall.SIGINT, STOP}
+}
+
+// TODO(caprita): Rename this to Shutdown() and the package to shutdown since
+// it's not just signals anymore.
+
+// ShutdownOnSignals registers signal handlers for the specified signals, or, if
+// none are specified, the default signals. The first signal received will be
+// made available on the returned channel; upon receiving a second signal, the
+// process will exit.
+func ShutdownOnSignals(signals ...os.Signal) <-chan os.Signal {
+ if len(signals) == 0 {
+ signals = defaultSignals()
+ }
+ // At least a buffer of length two so that we don't drop the first two
+ // signals we get on account of the channel being full.
+ ch := make(chan os.Signal, 2)
+ sawStop := false
+ for i := 0; i < len(signals); {
+ if s := signals[i]; s == STOP {
+ signals = append(signals[:i], signals[i+1:]...)
+ if sawStop {
+ continue
+ }
+ sawStop = true
+ if r := rt.R(); r != nil {
+ stopWaiter := make(chan string, 1)
+ r.WaitForStop(stopWaiter)
+ go func() {
+ for {
+ ch <- stopSignal(<-stopWaiter)
+ }
+ }()
+ }
+ } else {
+ i++
+ }
+ }
+ if len(signals) > 0 {
+ signal.Notify(ch, signals...)
+ }
+ // At least a buffer of length one so that we don't block on ret <- sig.
+ ret := make(chan os.Signal, 1)
+ go func() {
+ // First signal received.
+ sig := <-ch
+ ret <- sig
+ // Wait for a second signal, and force an exit if the process is
+ // still executing cleanup code.
+ <-ch
+ os.Exit(DoubleStopExitCode)
+ }()
+ return ret
+}
diff --git a/lib/signals/signals_test.go b/lib/signals/signals_test.go
new file mode 100644
index 0000000..03a480c
--- /dev/null
+++ b/lib/signals/signals_test.go
@@ -0,0 +1,212 @@
+package signals
+
+import (
+ "fmt"
+ "os"
+ "syscall"
+ "testing"
+
+ "veyron2/mgmt"
+ "veyron2/rt"
+
+ _ "veyron/lib/testutil"
+ "veyron/lib/testutil/blackbox"
+)
+
+// TestHelperProcess is boilerplate for the blackbox setup.
+func TestHelperProcess(t *testing.T) {
+ blackbox.HelperProcess(t)
+}
+
+func init() {
+ blackbox.CommandTable["handleDefaults"] = handleDefaults
+ blackbox.CommandTable["handleCustom"] = handleCustom
+ blackbox.CommandTable["handleDefaultsIgnoreChan"] = handleDefaultsIgnoreChan
+}
+
+func stopLoop(ch chan<- struct{}) {
+ for {
+ switch blackbox.ReadLineFromStdin() {
+ case "close":
+ close(ch)
+ return
+ case "stop":
+ rt.R().Stop()
+ }
+ }
+}
+
+func program(signals ...os.Signal) {
+ defer rt.Init().Shutdown()
+ closeStopLoop := make(chan struct{})
+ go stopLoop(closeStopLoop)
+ wait := ShutdownOnSignals(signals...)
+ fmt.Println("ready")
+ fmt.Println("received signal", <-wait)
+ <-closeStopLoop
+}
+
+func handleDefaults([]string) {
+ program()
+}
+
+func handleCustom([]string) {
+ program(syscall.SIGABRT)
+}
+
+func handleDefaultsIgnoreChan([]string) {
+ defer rt.Init().Shutdown()
+ closeStopLoop := make(chan struct{})
+ go stopLoop(closeStopLoop)
+ ShutdownOnSignals()
+ fmt.Println("ready")
+ <-closeStopLoop
+}
+
+func isSignalInSet(sig os.Signal, set []os.Signal) bool {
+ for _, s := range set {
+ if sig == s {
+ return true
+ }
+ }
+ return false
+}
+
+func checkSignalIsDefault(t *testing.T, sig os.Signal) {
+ if !isSignalInSet(sig, defaultSignals()) {
+ t.Errorf("signal %s not in default signal set, as expected", sig)
+ }
+}
+
+func checkSignalIsNotDefault(t *testing.T, sig os.Signal) {
+ if isSignalInSet(sig, defaultSignals()) {
+ t.Errorf("signal %s unexpectedly in default signal set", sig)
+ }
+}
+
+// TestCleanShutdownSignal verifies that sending a signal to a child that
+// handles it by default causes the child to shut down cleanly.
+func TestCleanShutdownSignal(t *testing.T) {
+ c := blackbox.HelperCommand(t, "handleDefaults")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("ready")
+ checkSignalIsDefault(t, syscall.SIGINT)
+ syscall.Kill(c.Cmd.Process.Pid, syscall.SIGINT)
+ c.Expect(fmt.Sprintf("received signal %s", syscall.SIGINT))
+ c.WriteLine("close")
+ c.ExpectEOFAndWait()
+}
+
+// TestCleanShutdownStop verifies that sending a stop comamnd to a child that
+// handles stop commands by default causes the child to shut down cleanly.
+func TestCleanShutdownStop(t *testing.T) {
+ c := blackbox.HelperCommand(t, "handleDefaults")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("ready")
+ c.WriteLine("stop")
+ c.Expect(fmt.Sprintf("received signal %s", mgmt.LocalStop))
+ c.WriteLine("close")
+ c.ExpectEOFAndWait()
+}
+
+// TestStopNoHandler verifies that sending a stop command to a child that does
+// not handle stop commands causes the child to exit immediately.
+func TestStopNoHandler(t *testing.T) {
+ c := blackbox.HelperCommand(t, "handleCustom")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("ready")
+ c.WriteLine("stop")
+ c.ExpectEOFAndWaitForExitCode(fmt.Errorf("exit status %d", mgmt.UnhandledStopExitCode))
+}
+
+// TestDoubleSignal verifies that sending a succession of two signals to a child
+// that handles these signals by default causes the child to exit immediately
+// upon receiving the second signal.
+func TestDoubleSignal(t *testing.T) {
+ c := blackbox.HelperCommand(t, "handleDefaults")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("ready")
+ checkSignalIsDefault(t, syscall.SIGTERM)
+ syscall.Kill(c.Cmd.Process.Pid, syscall.SIGTERM)
+ c.Expect(fmt.Sprintf("received signal %s", syscall.SIGTERM))
+ checkSignalIsDefault(t, syscall.SIGINT)
+ syscall.Kill(c.Cmd.Process.Pid, syscall.SIGINT)
+ c.ExpectEOFAndWaitForExitCode(fmt.Errorf("exit status %d", DoubleStopExitCode))
+}
+
+// TestSignalAndStop verifies that sending a signal followed by a stop command
+// to a child that handles these by default causes the child to exit immediately
+// upon receiving the stop command.
+func TestSignalAndStop(t *testing.T) {
+ c := blackbox.HelperCommand(t, "handleDefaults")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("ready")
+ checkSignalIsDefault(t, syscall.SIGTERM)
+ syscall.Kill(c.Cmd.Process.Pid, syscall.SIGTERM)
+ c.Expect(fmt.Sprintf("received signal %s", syscall.SIGTERM))
+ c.WriteLine("stop")
+ c.ExpectEOFAndWaitForExitCode(fmt.Errorf("exit status %d", DoubleStopExitCode))
+}
+
+// TestDoubleStop verifies that sending a succession of stop commands to a child
+// that handles stop commands by default causes the child to exit immediately
+// upon receiving the second stop command.
+func TestDoubleStop(t *testing.T) {
+ c := blackbox.HelperCommand(t, "handleDefaults")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("ready")
+ c.WriteLine("stop")
+ c.Expect(fmt.Sprintf("received signal %s", mgmt.LocalStop))
+ c.WriteLine("stop")
+ c.ExpectEOFAndWaitForExitCode(fmt.Errorf("exit status %d", DoubleStopExitCode))
+}
+
+// TestSendUnhandledSignal verifies that sending a signal that the child does
+// not handle causes the child to exit as per the signal being sent.
+func TestSendUnhandledSignal(t *testing.T) {
+ c := blackbox.HelperCommand(t, "handleDefaults")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("ready")
+ checkSignalIsNotDefault(t, syscall.SIGABRT)
+ syscall.Kill(c.Cmd.Process.Pid, syscall.SIGABRT)
+ c.ExpectEOFAndWaitForExitCode(fmt.Errorf("exit status 2"))
+}
+
+// TestDoubleSignalIgnoreChan verifies that, even if we ignore the channel that
+// ShutdownOnSignals returns, sending two signals should still cause the
+// process to exit (ensures that there is no dependency in ShutdownOnSignals
+// on having a goroutine read from the returned channel).
+func TestDoubleSignalIgnoreChan(t *testing.T) {
+ c := blackbox.HelperCommand(t, "handleDefaultsIgnoreChan")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("ready")
+ // Even if we ignore the channel that ShutdownOnSignals returns,
+ // sending two signals should still cause the process to exit.
+ checkSignalIsDefault(t, syscall.SIGTERM)
+ syscall.Kill(c.Cmd.Process.Pid, syscall.SIGTERM)
+ checkSignalIsDefault(t, syscall.SIGINT)
+ syscall.Kill(c.Cmd.Process.Pid, syscall.SIGINT)
+ c.ExpectEOFAndWaitForExitCode(fmt.Errorf("exit status %d", DoubleStopExitCode))
+}
+
+// TestHandlerCustomSignal verifies that sending a non-default signal to a
+// server that listens for that signal causes the server to shut down cleanly.
+func TestHandlerCustomSignal(t *testing.T) {
+ c := blackbox.HelperCommand(t, "handleCustom")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("ready")
+ checkSignalIsNotDefault(t, syscall.SIGABRT)
+ syscall.Kill(c.Cmd.Process.Pid, syscall.SIGABRT)
+ c.Expect(fmt.Sprintf("received signal %s", syscall.SIGABRT))
+ c.WriteLine("close")
+ c.ExpectEOFAndWait()
+}
diff --git a/lib/testutil/blackbox/doc.go b/lib/testutil/blackbox/doc.go
new file mode 100644
index 0000000..e4af049
--- /dev/null
+++ b/lib/testutil/blackbox/doc.go
@@ -0,0 +1,58 @@
+// Package blackbox provides routines to help with blackbox testing.
+//
+// Some tests need to run a subprocess. We reuse the same test binary
+// to do so. A fake test 'TestHelperProcess' contains the code we need to
+// run in the child and we simply run this same binary with a test.run= arg
+// that runs just that test. This idea was taken from the tests for os/exec.
+//
+// To use this, you need to specify entry points to be called as subprocesses.
+//
+// // File: mytestsubprocess.go
+// import (
+// "veyron/lib/testutil/blackbox"
+// )
+//
+// func init() {
+// blackbox.CommandTable["myTestEntryPoint"] = myTestEntryPoint
+// }
+//
+// func myTestEntryPoint(argv []string) {
+// // argv contains all non-flag arguments. Any flags should be handled
+// // using the flag package.
+// ...
+// blackbox.WaitForEOFOnStdin()
+// }
+//
+// You also need to specify a TestHelperProcess() entry point as part of your
+// test suite. This is boilerplate; the code reads as follows.
+//
+// // File: driver_test.go
+// package foo
+//
+// import (
+// "veyron/lib/testutil/blackbox"
+// )
+//
+// func TestHelperProcess(t *testing.T) {
+// blackbox.HelperProcess(t)
+// }
+//
+// Finally, to start a subprocess, use the HelperCommand(). defer execution
+// of the Cleanup method to ensure that logs from the child process
+// are collected, printed (if the vlog level is >= the number specified), and
+// the log files are deleted.
+//
+// // Starts myTestEntryPoint("myTestEntryPoint", "arg1", ..., "argN")
+// // in a subprocess.
+// child := blackbox.HelperCommand(t, "myTestEntryPoint", "arg1", ..., "argN")
+// child.Cmd.Start()
+// defer child.Cleanup(2)
+//
+// // Use the ExpectXXX() functions to examine the output.
+// child.Expect("sometext\n")
+// child.ExpectEOFAndWait()
+//
+// // Close stdin when you are done.
+// child.CloseStdin()
+//
+package blackbox
diff --git a/lib/testutil/blackbox/example_test.go b/lib/testutil/blackbox/example_test.go
new file mode 100644
index 0000000..399ff8f
--- /dev/null
+++ b/lib/testutil/blackbox/example_test.go
@@ -0,0 +1,53 @@
+package blackbox_test
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "testing"
+
+ "veyron/lib/testutil/blackbox"
+)
+
+func init() {
+ blackbox.CommandTable["print"] = print
+ blackbox.CommandTable["echo"] = echo
+}
+
+func print(args []string) {
+ for _, v := range args {
+ fmt.Printf("%s\n", v)
+ }
+ blackbox.WaitForEOFOnStdin()
+ fmt.Printf("done\n")
+}
+
+func echo(args []string) {
+ scanner := bufio.NewScanner(os.Stdin)
+ for scanner.Scan() {
+ fmt.Println(scanner.Text())
+ }
+ if err := scanner.Err(); err != nil {
+ fmt.Printf("error reading stdin: %s", err)
+ } else {
+ fmt.Printf("done\n")
+ }
+}
+
+func ExampleEcho() {
+ // Normally t is provided by testing and this example can't
+ // possible work outside of that environment.
+ t := &testing.T{}
+ c := blackbox.HelperCommand(t, "print", "a", "b", "c")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("a")
+ c.Expect("b")
+ c.Expect("c")
+ c.CloseStdin()
+ c.Expect("done")
+ c.ExpectEOFAndWait()
+ if !t.Failed() {
+ fmt.Printf("ok\n")
+ }
+}
diff --git a/lib/testutil/blackbox/faultyconn.go b/lib/testutil/blackbox/faultyconn.go
new file mode 100644
index 0000000..368068e
--- /dev/null
+++ b/lib/testutil/blackbox/faultyconn.go
@@ -0,0 +1,104 @@
+package blackbox
+
+import (
+ "io"
+ "net"
+ "sync"
+ "time"
+
+ "veyron2/vlog"
+)
+
+type syncBool struct {
+ mu sync.Mutex
+ value bool
+}
+
+func (s *syncBool) get() bool {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.value
+}
+func (s *syncBool) set(value bool) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.value = value
+}
+
+// Wrap a net.Conn and to force a failure when some amount of data has been transfered.
+type faultyConn struct {
+ conn net.Conn
+ readBytes int
+ readMax int
+ writeBytes int
+ writeMax int
+ forcedErr syncBool
+}
+
+func NewFaultyConn(conn net.Conn, readMax, writeMax int) net.Conn {
+ return &faultyConn{conn: conn, readMax: readMax, writeMax: writeMax}
+}
+
+func (conn *faultyConn) Close() error {
+ return conn.conn.Close()
+}
+
+func (conn *faultyConn) Read(buf []byte) (int, error) {
+ if conn.forcedErr.get() {
+ return 0, io.EOF
+ }
+ amount := conn.readMax - conn.readBytes
+ if amount == 0 {
+ vlog.VI(2).Info(formatLogLine("faultyConn(%p): closed", conn))
+ // subsequent Read/Write's should fail
+ conn.forcedErr.set(true)
+ return 0, io.EOF
+ }
+ if amount < len(buf) {
+ buf = buf[0:amount]
+ }
+ num, err := conn.conn.Read(buf)
+ conn.readBytes += num
+ return num, err
+}
+
+func (conn *faultyConn) Write(buf []byte) (int, error) {
+ if conn.forcedErr.get() {
+ return 0, io.EOF
+ }
+ amount := conn.writeMax - conn.writeBytes
+ shortWrite := false
+ if amount < len(buf) {
+ buf = buf[0:amount]
+ shortWrite = true
+ }
+ num, err := conn.conn.Write(buf)
+ conn.writeBytes += num
+ if shortWrite && err == nil {
+ vlog.VI(2).Info(formatLogLine("faultyConn(%p): write closed", conn))
+ // subsequent Read/Write's should fail
+ conn.forcedErr.set(true)
+ err = io.ErrShortWrite
+ }
+ return num, err
+}
+
+func (conn *faultyConn) LocalAddr() net.Addr {
+ return conn.conn.LocalAddr()
+}
+
+func (conn *faultyConn) RemoteAddr() net.Addr {
+ return conn.conn.RemoteAddr()
+}
+
+func (conn *faultyConn) SetDeadline(t time.Time) error {
+ return conn.conn.SetDeadline(t)
+}
+
+func (conn *faultyConn) SetReadDeadline(t time.Time) error {
+ return conn.conn.SetReadDeadline(t)
+}
+
+func (conn *faultyConn) SetWriteDeadline(t time.Time) error {
+ return conn.conn.SetWriteDeadline(t)
+}
diff --git a/lib/testutil/blackbox/subprocess.go b/lib/testutil/blackbox/subprocess.go
new file mode 100644
index 0000000..db4d40c
--- /dev/null
+++ b/lib/testutil/blackbox/subprocess.go
@@ -0,0 +1,472 @@
+package blackbox
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "veyron2/vlog"
+)
+
+type mainFunc func(argv []string)
+
+type commandMap map[string]mainFunc
+
+var (
+ CommandTable = make(commandMap)
+ subcommand string
+)
+
+func init() {
+ flag.StringVar(&subcommand, "subcommand", "", "the subcommand to run")
+}
+
+// Child represents a child process.
+type Child struct {
+ Name string
+ Cmd *exec.Cmd
+ ioLock sync.Mutex
+ Stdout *bufio.Reader // GUARDED_BY(ioLock)
+ Stdin io.WriteCloser
+ stderr *os.File
+ t *testing.T
+}
+
+// HelperCommand() takes an argument list and starts a helper subprocess.
+func HelperCommand(t *testing.T, command string, args ...string) *Child {
+ cs := []string{"-test.run=TestHelperProcess", "--subcommand=" + command}
+ for fname, fval := range vlog.Log.ExplicitlySetFlags() {
+ cs = append(cs, fmt.Sprintf("--%s=%s", fname, fval))
+ }
+ cs = append(cs, args...)
+ vlog.VI(2).Infof("running: %s %s", os.Args[0], cs)
+ cmd := exec.Command(os.Args[0], cs...)
+ stderr, err := ioutil.TempFile("", "__test__"+strings.TrimLeft(command, "-\n\t "))
+ if err != nil {
+ vlog.Errorf("Failed to open temp file, using stderr instead: err %s", err)
+ cmd.Stderr = os.Stderr
+ stderr = nil
+ } else {
+ cmd.Stderr = stderr
+ }
+ cmd.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
+ stdout, _ := cmd.StdoutPipe()
+ stdin, _ := cmd.StdinPipe()
+ return &Child{
+ Cmd: cmd,
+ Name: command,
+ Stdout: bufio.NewReader(stdout),
+ Stdin: stdin,
+ stderr: stderr,
+ t: t,
+ }
+}
+
+// HelperProcess is the entry point for helper subprocesses.
+// Children should Write errors to stderr and test output to stdout since
+// the parent process will read from stdout a line at a time to monitor
+// the progress of the child.
+func HelperProcess(*testing.T) {
+ // Return immediately if this is not run as the child helper
+ // for the other tests.
+ if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
+ return
+ }
+ if len(subcommand) == 0 {
+ vlog.Fatalf("No command found in: %s", os.Args)
+ }
+ mainFunc, ok := CommandTable[subcommand]
+ if !ok {
+ vlog.Fatalf("Unknown cmd: '%s'", subcommand)
+ }
+ mainFunc(flag.Args())
+ os.Exit(0)
+}
+
+type Reader func(r *bufio.Reader) (string, error)
+
+// ReadAll will read up to 16K of data from a buffered I/O reader, it is
+// intended to be used with ReadWithTimeout.
+func ReadAll(r *bufio.Reader) (string, error) {
+ buf := make([]byte, 4096*4)
+ n, err := r.Read(buf)
+ if err != nil {
+ return "", err
+ }
+ return string(buf[:n]), nil
+}
+
+// ReadLine will read a single line from a buffered I/O reader, it is
+// intended to be used with ReadWithTimeout.
+func ReadLine(r *bufio.Reader) (string, error) {
+ return r.ReadString('\n')
+}
+
+// ReadWithTimeout reads data from bufio.Reader instance using a function
+// of type Reader such as ReadAll or ReadLine. It will return true if it
+// times out, false otherwise. It will terminate the Child if it encounters
+// a timeout.
+func (c *Child) ReadWithTimeout(f Reader, r *bufio.Reader, timeout time.Duration) (string, bool, error) {
+ ch := make(chan string, 1)
+ ech := make(chan error, 1)
+ go func(c *Child) {
+ c.ioLock.Lock()
+ s, err := f(r)
+ c.ioLock.Unlock()
+ if err != nil {
+ if err != io.EOF {
+ vlog.VI(2).Info(formatLogLine("failed to read message: error %s: '%s'", err, s))
+ }
+ ech <- err
+ return
+ }
+ ch <- s
+ }(c)
+ select {
+ case err := <-ech:
+ return "", false, err
+ case m := <-ch:
+ return strings.TrimRight(m, "\n"), false, nil
+ case <-time.After(timeout):
+ // Kill the sub process to get the read calls running
+ // in the goroutine above to return an err.
+ c.Cmd.Process.Kill()
+ return "", true, nil
+ }
+ panic("unreachable")
+}
+
+func (c *Child) readRemaining(stream string, r *bufio.Reader) {
+ text, timedout, err := c.ReadWithTimeout(ReadAll, r, waitSeconds)
+ switch {
+ case timedout:
+ c.t.Error(formatLogLine("%s: timedout reading %s: err '%v'", c.Name, stream, err))
+ case err != nil:
+ c.t.Error(formatLogLine("%s: err reading %s", c.Name, stream))
+ default:
+ vlog.Info(formatLogLine("%s: %s: '%s'", c.Name, stream, text))
+ }
+}
+
+func (c *Child) expectLine(expected string) bool {
+ actual, timedout, err := c.ReadWithTimeout(ReadLine, c.Stdout, waitSeconds)
+ switch {
+ case timedout:
+ c.t.Error(formatLogLine("%s: timedout reading from stdout, expecting '%s'", c.Name, expected))
+ return false
+ case err != nil && err != io.EOF:
+ c.t.Error(formatLogLine("%s: unexpected error: %s, expecting '%s'", c.Name, err, expected))
+ return false
+ case actual == expected:
+ vlog.VI(2).Info(formatLogLine("%s: got: '%s'", c.Name, strings.TrimRight(actual, "")))
+ return err == io.EOF
+ default:
+ c.t.Error(formatLogLine("%s: expected '%s': got '%s'", c.Name, expected, actual))
+ return err == io.EOF
+ }
+}
+
+// removeIfFound removes the give item from the given list, if present
+// (returning if the item was present). If several copies of the item exist,
+// removeIfFound only removes the first occurrence.
+func removeIfFound(item string, list *[]string) bool {
+ for i, e := range *list {
+ if e == item {
+ *list = append((*list)[0:i], (*list)[i+1:len(*list)]...)
+ return true
+ }
+ }
+ return false
+}
+
+func (c *Child) expectSetEventually(expected []string, timeout time.Duration) bool {
+ for {
+ actual, timedout, err := c.ReadWithTimeout(ReadLine, c.Stdout, timeout)
+ switch {
+ case timedout:
+ m := formatLogLine("%s: timedout reading from stdout", c.Name)
+ vlog.VI(2).Info(m)
+ c.t.Error(m)
+ return false
+ case err != nil && err != io.EOF:
+ m := formatLogLine("%s: unexpected error: %s", c.Name, err)
+ vlog.VI(2).Info(m)
+ c.t.Error(m)
+ return false
+ case removeIfFound(actual, &expected):
+ if len(expected) == 0 {
+ return err == io.EOF
+ }
+ case err == io.EOF:
+ m := formatLogLine("%s: eof", c.Name)
+ vlog.VI(2).Info(m)
+ c.t.Error(m)
+ return true
+ default:
+ vlog.VI(2).Info(formatLogLine("%s: got: '%s'", c.Name, strings.TrimRight(actual, "\n")))
+ }
+ }
+}
+
+// Expect the specified string to be read from the Child's stdout pipe.
+// Trailing \n's are stripped from the Child's output and hence should not
+// be included in the "expected" parameter.
+func (c *Child) Expect(expected string) {
+ if c.t.Failed() {
+ vlog.VI(2).Info(formatLogLine("Already failed"))
+ return
+ }
+ eof := c.expectLine(expected)
+ if eof {
+ c.t.Error(formatLogLine("%s: unexpected EOF when expecting '%s'", c.Name, strings.TrimRight(expected, "\n")))
+ }
+}
+
+// ExpectSet verifies whether the given set of strings (expressed as a list,
+// though order is irrelevant) matches the next len(expected) lines in stdout.
+// The set is allowed to contain repetitions if the same line is expected
+// multiple times.
+func (c *Child) ExpectSet(expected []string) {
+ if c.t.Failed() {
+ return
+ }
+ actual := make([]string, 0, len(expected))
+ for i := 0; i < len(expected); i++ {
+ str, err := c.ReadLineFromChild()
+ if err != nil {
+ c.t.Errorf("ReadLineFromChild: failed %v", err)
+ return
+ }
+ actual = append(actual, str)
+ }
+ sort.Strings(expected)
+ sort.Strings(actual)
+ for i, exp := range expected {
+ if exp != actual[i] {
+ c.t.Errorf(formatLogLine("expected %v, actual %v", expected, actual))
+ return
+ }
+ }
+}
+
+// Expect the specified string to be read from the Child's stdout pipe
+// eventually. There may be additional output beforehand.
+func (c *Child) ExpectEventually(expected string, timeout time.Duration) {
+ if c.t.Failed() {
+ vlog.VI(2).Info(formatLogLine("Already failed"))
+ return
+ }
+ vlog.VI(2).Info(formatLogLine("Waiting for client, timeout %s", timeout))
+ eof := c.expectSetEventually([]string{expected}, timeout)
+ if eof {
+ c.t.Error(formatLogLine("%s: unexpected EOF when expecting %q", c.Name, strings.TrimRight(expected, "\n")))
+ }
+}
+
+// ExpectSetEventually verifies whether the given set of strings (expressed as a
+// list, though order is irrelevant) appear in the stdout pipe of the child
+// eventually. The set is allowed to contain repetitions if the same line is
+// expected multiple times.
+func (c *Child) ExpectSetEventually(expected []string, timeout time.Duration) {
+ if c.t.Failed() {
+ vlog.VI(2).Info(formatLogLine("Already failed"))
+ return
+ }
+ vlog.VI(2).Info(formatLogLine("Waiting for client, timeout %s", timeout))
+ eof := c.expectSetEventually(expected, timeout)
+ if eof {
+ c.t.Error(formatLogLine("%s: unexpected EOF when expecting %q", c.Name, expected))
+ }
+}
+
+// Expect EOF to be read from the Child's stdout pipe and wait for the child
+// if it is still running to clean up process state.
+func (c *Child) ExpectEOFAndWait() {
+ c.ExpectEOFAndWaitForExitCode(nil)
+}
+
+// ExpectEOFAndWaitForExitCode is the same as ExpectEOFAndWait except that it
+// allows for a non-nil error to be returned by the child.
+func (c *Child) ExpectEOFAndWaitForExitCode(exit error) {
+ if c.t.Failed() {
+ c.t.Error(formatLogLine("%s: cleanup", c.Name))
+ c.readRemaining("stdout", c.Stdout)
+ if c.Cmd.Process != nil {
+ c.Cmd.Process.Kill()
+ }
+ } else {
+ eof := c.expectLine("")
+ if !eof {
+ c.t.Error(formatLogLine("%s: failed to exit (failed to read EOF)", c.Name))
+ c.readRemaining("stdout", c.Stdout)
+ if c.Cmd.Process != nil {
+ c.Cmd.Process.Kill()
+ }
+ }
+ }
+ c.ioLock.Lock()
+ defer c.ioLock.Unlock()
+ err := c.Cmd.Wait()
+ if exit == nil {
+ if err != nil {
+ c.t.Error(formatLogLine("Client exited with error: %s", err))
+ }
+ return
+ }
+ if err == nil {
+ c.t.Error(formatLogLine("Client exited without error: expecting %s", exit))
+ return
+ }
+ if err.Error() != exit.Error() {
+ c.t.Error(formatLogLine("Client exited with unexpected error: %q and not %q", err, exit))
+ }
+}
+
+// The WaitForXXX() functions are similar to expectations, but they do less
+// checking and for use in benchmarking.
+
+// WaitForLine reads the input until the expected line is read,
+// returning an error if EOF is reached or there is a timeout.
+func (c *Child) WaitForLine(expected string, timeout time.Duration) error {
+ if c.t.Failed() {
+ vlog.VI(2).Info(formatLogLine("Already failed"))
+ return fmt.Errorf("Already failed")
+ }
+ deadline := time.Now().Add(timeout)
+ for {
+ actual, _, err := c.ReadWithTimeout(ReadLine, c.Stdout, deadline.Sub(time.Now()))
+ if err != nil {
+ return err
+ }
+ if actual == expected {
+ return nil
+ }
+ }
+}
+
+// WaitForEOF reads the input until an EOF is reached, returning an
+// error if there is a timeout.
+func (c *Child) WaitForEOF(timeout time.Duration) error {
+ deadline := time.Now().Add(timeout)
+ for {
+ _, _, err := c.ReadWithTimeout(ReadLine, c.Stdout, deadline.Sub(time.Now()))
+ if err == io.EOF {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ }
+}
+
+// WaitForPortFromChild waits for a line in the format "port %d" and returns the
+// port number, or an error if there is a timeout or EOF is reached.
+func (c *Child) WaitForPortFromChild() (string, error) {
+ actual, _, err := c.ReadWithTimeout(ReadLine, c.Stdout, waitSeconds)
+ if err != nil {
+ return "", err
+ }
+ last := strings.LastIndex(actual, ":")
+ if last == -1 {
+ return "", fmt.Errorf("%s", formatLogLine("%s: failed to parse port from %q - missing ':'", c.Name, actual))
+ }
+ port := actual[last+1:]
+ port = strings.TrimSuffix(port, "'\n")
+ // Check that the port is a number.
+ _, err = strconv.Atoi(port)
+ if err != nil {
+ return "", fmt.Errorf("%s", formatLogLine("%s: failed to parse port from %q: err %s", c.Name, actual, err))
+ }
+ return port, nil
+}
+
+// ReadPortFromChild reads a port number written to the child's stdout,
+// returning an error if it fails to do so and not calling t.Error internal.
+func (c *Child) ReadPortFromChild() (string, error) {
+ if c.t.Failed() {
+ return "", fmt.Errorf("%s", "Already failed")
+ }
+ actual, timedout, err := c.ReadWithTimeout(ReadLine, c.Stdout, waitSeconds)
+ if timedout {
+ return "", fmt.Errorf("%s", formatLogLine("%s: timeout reading port", c.Name))
+ }
+ if err != nil {
+ return "", fmt.Errorf("%s", formatLogLine("%s: error reading port: %s", c.Name, err))
+ }
+ last := strings.LastIndex(actual, ":")
+ if last == -1 {
+ return "", fmt.Errorf("%s", formatLogLine("%s: failed to parse port from '%s' - missing ':'", c.Name, actual))
+ }
+ port := actual[last+1:]
+ port = strings.TrimRight(port, "'\n")
+ _, err = strconv.Atoi(port)
+ if err != nil {
+ return "", fmt.Errorf("%s", formatLogLine("%s: failed to parse port from '%s': err %s", c.Name, actual, err))
+ }
+ return port, nil
+}
+
+// ReadLineFromChild reads a single line from the child's stdout,
+// returning an error if it fails to do so and not calling t.Error internal.
+func (c *Child) ReadLineFromChild() (string, error) {
+ if c.t.Failed() {
+ return "", fmt.Errorf("Already failed")
+ }
+ actual, timedout, err := c.ReadWithTimeout(ReadLine, c.Stdout, waitSeconds)
+ if timedout {
+ return "", fmt.Errorf("%s", formatLogLine("%s: timeout reading line", c.Name))
+ }
+ if err != nil {
+ return "", fmt.Errorf("%s", formatLogLine("%s: error reading line: %s", c.Name, err))
+ }
+ return strings.TrimRight(actual, "\n"), nil
+}
+
+// WriteLine writes the given line to the child's stdin, and appends a \n.
+func (c *Child) WriteLine(line string) {
+ if c.t.Failed() {
+ vlog.VI(2).Info(formatLogLine("Already failed"))
+ return
+ }
+ c.Stdin.Write([]byte(line))
+ c.Stdin.Write([]byte("\n"))
+}
+
+// CloseStding closes the stdin pipe the child is likely blocked on.
+func (c *Child) CloseStdin() {
+ // Child should exit now.
+ c.Stdin.Close()
+}
+
+// Cleanup sends a kill signal to the child, prints any stderr logs from
+// the child and deletes the log files generated by the child.
+func (c *Child) Cleanup() {
+ vlog.FlushLog()
+ if c.Cmd.Process != nil {
+ c.Cmd.Process.Kill()
+ }
+ if c.stderr != nil {
+ defer func() {
+ c.stderr.Close()
+ os.Remove(c.stderr.Name())
+ }()
+ if _, err := c.stderr.Seek(0, 0); err != nil {
+ return
+ }
+ scanner := bufio.NewScanner(c.stderr)
+ for scanner.Scan() {
+ // Avoid printing two sets of line headers
+ vlog.Info(c.Name + ": " + scanner.Text())
+ }
+ }
+}
diff --git a/lib/testutil/blackbox/subprocess_test.go b/lib/testutil/blackbox/subprocess_test.go
new file mode 100644
index 0000000..8da6ace
--- /dev/null
+++ b/lib/testutil/blackbox/subprocess_test.go
@@ -0,0 +1,128 @@
+package blackbox_test
+
+// TODO(cnicolaou): add tests for error cases that result in t.Fatalf being
+// called. We need to mock testing.T in order to be able to catch these.
+
+import (
+ "io"
+ "testing"
+ "time"
+
+ "veyron/lib/testutil"
+ "veyron/lib/testutil/blackbox"
+)
+
+func isFatalf(t *testing.T, err error, format string, args ...interface{}) {
+ if err != nil {
+ t.Fatalf(testutil.FormatLogLine(2, format, args...))
+ }
+}
+
+func TestHelperProcess(t *testing.T) {
+ blackbox.HelperProcess(t)
+}
+
+func finish(c *blackbox.Child) {
+ c.CloseStdin()
+ c.Expect("done")
+ c.ExpectEOFAndWait()
+}
+
+func TestExpect(t *testing.T) {
+ c := blackbox.HelperCommand(t, "print", "a", "b", "c")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.Expect("a")
+ c.Expect("b")
+ c.Expect("c")
+ finish(c)
+}
+
+func TestExpectSet(t *testing.T) {
+ c := blackbox.HelperCommand(t, "print", "a", "b", "c")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.ExpectSet([]string{"c", "b", "a"})
+ finish(c)
+}
+
+func TestExpectEventually(t *testing.T) {
+ c := blackbox.HelperCommand(t, "print", "a", "b", "c")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.ExpectEventually("c", time.Second)
+ finish(c)
+}
+
+func TestExpectSetEventually(t *testing.T) {
+ c := blackbox.HelperCommand(t, "print", "a", "b", "c", "d", "b")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.ExpectSetEventually([]string{"d", "b", "b"}, time.Second)
+ finish(c)
+}
+
+func TestReadPort(t *testing.T) {
+ c := blackbox.HelperCommand(t, "print", "host:1234")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ port, err := c.ReadPortFromChild()
+ if err != nil {
+ t.Fatalf("failed to read port from child: %s", err)
+ }
+ if port != "1234" {
+ t.Fatalf("unexpected port from child: %q", port)
+ }
+ finish(c)
+}
+
+func TestEcho(t *testing.T) {
+ c := blackbox.HelperCommand(t, "echo")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.WriteLine("a")
+ c.WriteLine("b")
+ c.WriteLine("c")
+ c.ExpectEventually("c", time.Second)
+ finish(c)
+}
+
+func TestTimeout(t *testing.T) {
+ c := blackbox.HelperCommand(t, "echo")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ c.WriteLine("a")
+ l, timeout, err := c.ReadWithTimeout(blackbox.ReadLine, c.Stdout, time.Second)
+ isFatalf(t, err, "unexpected error: %s", err)
+ if timeout {
+ t.Fatalf("unexpected timeout")
+ }
+ if l != "a" {
+ t.Fatalf("unexpected input: got %q", l)
+ }
+ _, timeout, _ = c.ReadWithTimeout(blackbox.ReadLine, c.Stdout, 250*time.Millisecond)
+ if !timeout {
+ t.Fatalf("failed to timeout")
+ }
+ _, _, err = c.ReadWithTimeout(blackbox.ReadLine, c.Stdout, 250*time.Millisecond)
+ // The previous call to ReadWithTimeout which timed out kills the
+ // Child, so a subsequent call will read an EOF.
+ if err != io.EOF {
+ t.Fatalf("expected EOF: got %q instead", err)
+ }
+}
+
+func TestWait(t *testing.T) {
+ c := blackbox.HelperCommand(t, "print", "a", "b:1234")
+ defer c.Cleanup()
+ c.Cmd.Start()
+ err := c.WaitForLine("a", time.Second)
+ isFatalf(t, err, "unexpected error: %s", err)
+ port, err := c.WaitForPortFromChild()
+ isFatalf(t, err, "unexpected error: %s", err)
+ if port != "1234" {
+ isFatalf(t, err, "unexpected port: %s", port)
+ }
+ c.CloseStdin()
+ c.WaitForEOF(time.Second)
+}
diff --git a/lib/testutil/blackbox/util.go b/lib/testutil/blackbox/util.go
new file mode 100644
index 0000000..4ba6790
--- /dev/null
+++ b/lib/testutil/blackbox/util.go
@@ -0,0 +1,52 @@
+package blackbox
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "time"
+)
+
+var (
+ waitSeconds = 100 * time.Second
+)
+
+// Prepend the file+lineno of the first caller from outside this package
+func formatLogLine(format string, args ...interface{}) string {
+ _, file, line, _ := runtime.Caller(1)
+ cwd := filepath.Dir(file)
+ for d := 2; d < 10; d++ {
+ _, file, line, _ = runtime.Caller(d)
+ if cwd != filepath.Dir(file) {
+ break
+ }
+ }
+ nargs := []interface{}{filepath.Base(file), line}
+ nargs = append(nargs, args...)
+ return fmt.Sprintf("%s:%d: "+format, nargs...)
+}
+
+// Wait for EOF on os.Stdin, used to signal to the Child that it should exit.
+func WaitForEOFOnStdin() {
+ var buf [1]byte
+ for {
+ _, err := os.Stdin.Read(buf[:])
+ if err != nil {
+ break
+ }
+ }
+}
+
+// ReadLineFromStdin reads a line from os.Stdin, blocking until one is available
+// or until EOF is encoutnered. The line is returned with the newline character
+// chopped off.
+func ReadLineFromStdin() string {
+ if read, err := bufio.NewReader(os.Stdin).ReadString('\n'); err != nil {
+ return ""
+ } else {
+ return strings.TrimRight(read, "\n")
+ }
+}
diff --git a/lib/testutil/init.go b/lib/testutil/init.go
new file mode 100644
index 0000000..2d42a41
--- /dev/null
+++ b/lib/testutil/init.go
@@ -0,0 +1,34 @@
+// Package testutil provides initalization and utility routines for unit tests.
+//
+// All tests should import it, even if only for its initialization:
+// import _ "veyron/lib/testutil"
+//
+package testutil
+
+import (
+ "flag"
+ "os"
+ "runtime"
+ // Need to import all of the packages that could possibly
+ // define flags that we care about. In practice, this is the
+ // flags defined by the testing package, the logging library
+ // and any flags defined by the blackbox package below.
+ _ "testing"
+
+ // Import blackbox to ensure that it gets to define its flags.
+ _ "veyron/lib/testutil/blackbox"
+
+ "veyron2/vlog"
+)
+
+func init() {
+ if os.Getenv("GOMAXPROCS") == "" {
+ // Set the number of logical processors to the number of CPUs,
+ // if GOMAXPROCS is not set in the environment.
+ runtime.GOMAXPROCS(runtime.NumCPU())
+ }
+ // At this point all of the flags that we're going to use for
+ // tests must be defined.
+ flag.Parse()
+ vlog.ConfigureLibraryLoggerFromFlags()
+}
diff --git a/lib/testutil/util.go b/lib/testutil/util.go
new file mode 100644
index 0000000..b6a8bf9
--- /dev/null
+++ b/lib/testutil/util.go
@@ -0,0 +1,86 @@
+package testutil
+
+import (
+ "fmt"
+ "io/ioutil"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "time"
+
+ isecurity "veyron/runtimes/google/security"
+
+ "veyron2/security"
+)
+
+// FormatLogLine will prepend the file and line number of the caller
+// at the specificied depth (as per runtime.Caller) to the supplied
+// format and args and return a formatted string. It is useful when
+// implementing functions that factor out error handling and reporting
+// in tests.
+func FormatLogLine(depth int, format string, args ...interface{}) string {
+ _, file, line, _ := runtime.Caller(depth)
+ nargs := []interface{}{filepath.Base(file), line}
+ nargs = append(nargs, args...)
+ return fmt.Sprintf("%s:%d: "+format, nargs...)
+}
+
+// DepthToExternalCaller determines the number of stack frames to the first
+// enclosing caller that is external to the package that this function is
+// called from. Drectory name is used as a proxy for package name,
+// that is, the directory component of the file return runtime.Caller is
+// compared to that of the lowest level caller until a different one is
+// encountered as the stack is walked upwards.
+func DepthToExternalCaller() int {
+ _, file, _, _ := runtime.Caller(1)
+ cwd := filepath.Dir(file)
+ for d := 2; d < 10; d++ {
+ _, file, _, _ := runtime.Caller(d)
+ if cwd != filepath.Dir(file) {
+ return d
+ }
+ }
+ return 1
+}
+
+// SaveIdentityToFile saves the provided identity in Base64VOM format
+// to a randomly created temporary file, and returns the path to the file.
+// This function is meant to be used for testing purposes only, it panics
+// if there is an error. The caller must ensure that the created file
+// is removed once it is no longer needed.
+func SaveIdentityToFile(id security.PrivateID) string {
+ f, err := ioutil.TempFile("", strconv.Itoa(rand.Int()))
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+ filePath := f.Name()
+
+ if err := security.SaveIdentity(f, id); err != nil {
+ os.Remove(filePath)
+ panic(err)
+ }
+ return filePath
+}
+
+// NewBlessedIdentity creates a new identity and blesses it using the provided blesser
+// under the provided name. This function is meant to be used for testing purposes only,
+// it panics if there is an error.
+func NewBlessedIdentity(blesser security.PrivateID, name string) security.PrivateID {
+ id, err := isecurity.NewChainPrivateID("test")
+ if err != nil {
+ panic(err)
+ }
+
+ blessedID, err := blesser.Bless(id.PublicID(), name, 5*time.Minute, nil)
+ if err != nil {
+ panic(err)
+ }
+ derivedID, err := id.Derive(blessedID)
+ if err != nil {
+ panic(err)
+ }
+ return derivedID
+}
diff --git a/lib/testutil/util_test.go b/lib/testutil/util_test.go
new file mode 100644
index 0000000..2209d92
--- /dev/null
+++ b/lib/testutil/util_test.go
@@ -0,0 +1,87 @@
+package testutil_test
+
+import (
+ "fmt"
+ "os"
+ "reflect"
+ "regexp"
+ "testing"
+
+ "veyron/lib/testutil"
+ isecurity "veyron/runtimes/google/security"
+
+ "veyron2/rt"
+ "veyron2/security"
+)
+
+func TestFormatLogline(t *testing.T) {
+ depth, want := testutil.DepthToExternalCaller(), 2
+ if depth != want {
+ t.Errorf("got %v, want %v", depth, want)
+ }
+ {
+ line, want := testutil.FormatLogLine(depth, "test"), "testing.go:.*"
+ if ok, err := regexp.MatchString(want, line); !ok || err != nil {
+ t.Errorf("got %v, want %v", line, want)
+ }
+ }
+}
+
+func TestSaveIdentityToFile(t *testing.T) {
+ r, err := rt.New()
+ if err != nil {
+ t.Fatalf("rt.New failed: %v", err)
+ }
+ defer r.Shutdown()
+ id, err := r.NewIdentity("test")
+ if err != nil {
+ t.Fatalf("r.NewIdentity failed: %v", err)
+ }
+
+ filePath := testutil.SaveIdentityToFile(id)
+ defer os.Remove(filePath)
+
+ f, err := os.Open(filePath)
+ if err != nil {
+ t.Fatalf("os.Open(%v) failed: %v", filePath, err)
+ }
+ defer f.Close()
+ loadedID, err := security.LoadIdentity(f)
+ if err != nil {
+ t.Fatalf("LoadIdentity failed: %v", err)
+ }
+ if !reflect.DeepEqual(loadedID, id) {
+ t.Fatalf("Got Identity %v, but want %v", loadedID, id)
+ }
+}
+
+func TestNewBlessedIdentity(t *testing.T) {
+ r, err := rt.New()
+ if err != nil {
+ t.Fatalf("rt.New failed: %v", err)
+ }
+ defer r.Shutdown()
+ newID := func(name string) security.PrivateID {
+ id, err := r.NewIdentity(name)
+ if err != nil {
+ t.Fatalf("r.NewIdentity failed: %v", err)
+ }
+ isecurity.TrustIdentityProviders(id)
+ return id
+ }
+ testdata := []struct {
+ blesser security.PrivateID
+ blessingName, name string
+ }{
+ {blesser: newID("google"), blessingName: "alice", name: "PrivateID:google/alice"},
+ {blesser: newID("google"), blessingName: "bob", name: "PrivateID:google/bob"},
+ {blesser: newID("veyron"), blessingName: "alice", name: "PrivateID:veyron/alice"},
+ {blesser: newID("veyron"), blessingName: "bob", name: "PrivateID:veyron/bob"},
+ {blesser: testutil.NewBlessedIdentity(newID("google"), "alice"), blessingName: "tv", name: "PrivateID:google/alice/tv"},
+ }
+ for _, d := range testdata {
+ if got, want := fmt.Sprintf("%s", testutil.NewBlessedIdentity(d.blesser, d.blessingName)), d.name; got != want {
+ t.Errorf("NewBlessedIdentity(%q, %q): Got %q, want %q", d.blesser, d.blessingName, got, want)
+ }
+ }
+}
diff --git a/lib/testutil/vtest.go b/lib/testutil/vtest.go
new file mode 100644
index 0000000..fccf74d
--- /dev/null
+++ b/lib/testutil/vtest.go
@@ -0,0 +1,12 @@
+package testutil
+
+// CallAndRecover calls the function f and returns the result of recover().
+// This minimizes the scope of the deferred recover, to ensure f is actually the
+// function that paniced.
+func CallAndRecover(f func()) (result interface{}) {
+ defer func() {
+ result = recover()
+ }()
+ f()
+ return
+}
diff --git a/lib/testutil/vtest_test.go b/lib/testutil/vtest_test.go
new file mode 100644
index 0000000..a4523ab
--- /dev/null
+++ b/lib/testutil/vtest_test.go
@@ -0,0 +1,23 @@
+package testutil
+
+import (
+ "testing"
+)
+
+func TestCallAndRecover(t *testing.T) {
+ tests := []struct {
+ f func()
+ expect interface{}
+ }{
+ {func() {}, nil},
+ {func() { panic(nil) }, nil},
+ {func() { panic(123) }, 123},
+ {func() { panic("abc") }, "abc"},
+ }
+ for _, test := range tests {
+ got := CallAndRecover(test.f)
+ if got != test.expect {
+ t.Errorf(`CallAndRecover got "%v", want "%v"`, got, test.expect)
+ }
+ }
+}
diff --git a/lib/toposort/sort.go b/lib/toposort/sort.go
new file mode 100644
index 0000000..eadbe3a
--- /dev/null
+++ b/lib/toposort/sort.go
@@ -0,0 +1,177 @@
+/*
+A package that implements topological sort. For details see:
+http://en.wikipedia.org/wiki/Topological_sorting
+*/
+package toposort
+
+type Sorter interface {
+ // AddNode adds a node value. Arbitrary value types are supported, but the
+ // values must be comparable - they'll be used as map keys. Typically this is
+ // only used to add potentially loner nodes with no incoming or outgoing edges.
+ AddNode(value interface{})
+
+ // AddEdge adds nodes corresponding to from and to, and adds an edge from -> to.
+ // You don't need to call AddNode first; the nodes will be implicitly added if
+ // they don't already exist. The direction means that from depends on to;
+ // i.e. to will appear before from in the sorted output. Cycles are allowed.
+ AddEdge(from, to interface{})
+
+ // Sort returns the topologically sorted values, along with cycles which
+ // represents some of the cycles (if any) that were encountered. You're
+ // guaranteed that len(cycles)==0 iff there are no cycles in the graph,
+ // otherwise an arbitrary (but non-empty) list of cycles is returned.
+ //
+ // If there are cycles the sorting is best-effort; portions of the graph that
+ // are acyclic will still be ordered correctly, and the cyclic portions have an
+ // arbitrary ordering.
+ //
+ // Sort is deterministic; given the same sequence of inputs it will always
+ // return the same output, even if the inputs are only partially ordered. This
+ // is useful for idl compilation where we re-order type definitions, and it's
+ // nice to get a deterministic order.
+ Sort() ([]interface{}, [][]interface{})
+}
+
+// NewSorter returns a new topological sorter.
+func NewSorter() Sorter {
+ return &topoSort{}
+}
+
+// topoSort performs a topological sort. This is meant to be simple, not
+// high-performance. For details see:
+// http://en.wikipedia.org/wiki/Topological_sorting
+type topoSort struct {
+ values map[interface{}]int
+ nodes []*topoNode
+}
+
+// topoNode is a node in the graph representing the topological information.
+type topoNode struct {
+ index int
+ value interface{}
+ children []*topoNode
+}
+
+func (tn *topoNode) addChild(child *topoNode) {
+ tn.children = append(tn.children, child)
+}
+
+func (ts *topoSort) addOrGetNode(value interface{}) *topoNode {
+ if ts.nodes == nil {
+ ts.values = make(map[interface{}]int)
+ }
+ if index, ok := ts.values[value]; ok {
+ return ts.nodes[index]
+ }
+ index := len(ts.nodes)
+ newNode := &topoNode{index: index, value: value}
+ ts.values[value] = index
+ ts.nodes = append(ts.nodes, newNode)
+ return newNode
+}
+
+// AddNode adds a node value. Arbitrary value types are supported, but the
+// values must be comparable - they'll be used as map keys. Typically this is
+// only used to add potentially loner nodes with no incoming or outgoing edges.
+func (ts *topoSort) AddNode(value interface{}) {
+ ts.addOrGetNode(value)
+}
+
+// AddEdge adds nodes corresponding to from and to, and adds an edge from -> to.
+// You don't need to call AddNode first; the nodes will be implicitly added if
+// they don't already exist. The direction means that from depends on to;
+// i.e. to will appear before from in the sorted output. Cycles are allowed.
+func (ts *topoSort) AddEdge(from interface{}, to interface{}) {
+ fromNode := ts.addOrGetNode(from)
+ toNode := ts.addOrGetNode(to)
+ fromNode.addChild(toNode)
+}
+
+// Sort returns the topologically sorted values, along with cycles which
+// represents some of the cycles (if any) that were encountered. You're
+// guaranteed that len(cycles)==0 iff there are no cycles in the graph,
+// otherwise an arbitrary (but non-empty) list of cycles is returned.
+//
+// If there are cycles the sorting is best-effort; portions of the graph that
+// are acyclic will still be ordered correctly, and the cyclic portions have an
+// arbitrary ordering.
+//
+// Sort is deterministic; given the same sequence of inputs it will always
+// return the same output, even if the inputs are only partially ordered. This
+// is useful for idl compilation where we re-order type definitions, and it's
+// nice to get a deterministic order.
+func (ts *topoSort) Sort() (sorted []interface{}, cycles [][]interface{}) {
+ // The strategy is the standard simple approach of performing DFS on the
+ // graph. Details are outlined in the above wikipedia article.
+ done := make(topoNodeSet)
+ for _, node := range ts.nodes {
+ cycles = appendCycles(cycles, node.visit(done, make(topoNodeSet), &sorted))
+ }
+ return
+}
+
+type topoNodeSet map[*topoNode]struct{}
+
+// visit performs DFS on the graph, and fills in sorted and cycles as it
+// traverses. We use done to indicate a node has been fully explored, and
+// visiting to indicate a node is currently being explored.
+//
+// The cycle collection strategy is to wait until we've hit a repeated node in
+// visiting, and add that node to cycles and return. Thereafter as the
+// recursive stack is unwound, nodes append themselves to the end of each cycle,
+// until we're back at the repeated node. This guarantees that if the graph is
+// cyclic we'll return at least one of the cycles.
+func (tn *topoNode) visit(done, visiting topoNodeSet, sorted *[]interface{}) (cycles [][]interface{}) {
+ if _, ok := done[tn]; ok {
+ return
+ }
+ if _, ok := visiting[tn]; ok {
+ cycles = [][]interface{}{{tn.value}}
+ return
+ }
+ visiting[tn] = struct{}{}
+ for _, child := range tn.children {
+ cycles = appendCycles(cycles, child.visit(done, visiting, sorted))
+ }
+ done[tn] = struct{}{}
+ *sorted = append(*sorted, tn.value)
+ // Update cycles. If it's empty none of our children detected any cycles, and
+ // there's nothing to do. Otherwise we append ourselves to the cycle, iff the
+ // cycle hasn't completed yet. We know the cycle has completed if the first
+ // and last item in the cycle are the same, with an exception for the single
+ // item case; self-cycles are represented as the same node appearing twice.
+ for cx := range cycles {
+ len := len(cycles[cx])
+ if len == 1 || cycles[cx][0] != cycles[cx][len-1] {
+ cycles[cx] = append(cycles[cx], tn.value)
+ }
+ }
+ return
+}
+
+// appendCycles returns the combined cycles in a and b.
+func appendCycles(a [][]interface{}, b [][]interface{}) [][]interface{} {
+ for _, bcycle := range b {
+ a = append(a, bcycle)
+ }
+ return a
+}
+
+// PrintCycles prints the cycles returned from topoSort.Sort, using f to convert
+// each node into a string.
+func PrintCycles(cycles [][]interface{}, f func(n interface{}) string) (str string) {
+ for cyclex, cycle := range cycles {
+ if cyclex > 0 {
+ str += " "
+ }
+ str += "["
+ for nodex, node := range cycle {
+ if nodex > 0 {
+ str += " <= "
+ }
+ str += f(node)
+ }
+ str += "]"
+ }
+ return
+}
diff --git a/lib/toposort/sort_test.go b/lib/toposort/sort_test.go
new file mode 100644
index 0000000..633e913
--- /dev/null
+++ b/lib/toposort/sort_test.go
@@ -0,0 +1,215 @@
+package toposort
+
+import (
+ "reflect"
+ "testing"
+)
+
+func toStringSlice(input []interface{}) (output []string) {
+ output = make([]string, len(input))
+ for ix, ival := range input {
+ output[ix] = ival.(string)
+ }
+ return
+}
+
+func toStringCycles(input [][]interface{}) (output [][]string) {
+ output = make([][]string, len(input))
+ for ix, islice := range input {
+ output[ix] = toStringSlice(islice)
+ }
+ return
+}
+
+type orderChecker struct {
+ t *testing.T
+ original []string
+ orderMap map[string]int
+}
+
+func makeOrderChecker(t *testing.T, slice []interface{}) orderChecker {
+ result := orderChecker{t, toStringSlice(slice), make(map[string]int)}
+ for ix, val := range result.original {
+ result.orderMap[val] = ix
+ }
+ return result
+}
+
+func (oc *orderChecker) findValue(val string) int {
+ if index, ok := oc.orderMap[val]; ok {
+ return index
+ }
+ oc.t.Errorf("Couldn't find val %v in slice %v", val, oc.original)
+ return -1
+}
+
+func (oc *orderChecker) expectOrder(before, after string) {
+ if oc.findValue(before) >= oc.findValue(after) {
+ oc.t.Errorf("Expected %v before %v, slice %v", before, after, oc.original)
+ }
+}
+
+// Since sort is deterministic we can expect a particular total order, in
+// addition to the partial order checks.
+func (oc *orderChecker) expectTotalOrder(expect ...string) {
+ if !reflect.DeepEqual(oc.original, expect) {
+ oc.t.Errorf("Expected order %v, actual %v", expect, oc.original)
+ }
+}
+
+func expectCycles(t *testing.T, actual [][]interface{}, expect [][]string) {
+ actualStr := toStringCycles(actual)
+ if !reflect.DeepEqual(actualStr, expect) {
+ t.Errorf("Expected cycles %v, actual %v", expect, actualStr)
+ }
+}
+
+func TestSortDag(t *testing.T) {
+ // This is the graph:
+ // ,-->B
+ // |
+ // A-->C---->D
+ // | \
+ // | `-->E--.
+ // `-------------`-->F
+ var sorter topoSort
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("A", "C")
+ sorter.AddEdge("A", "F")
+ sorter.AddEdge("C", "D")
+ sorter.AddEdge("C", "E")
+ sorter.AddEdge("E", "F")
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectOrder("B", "A")
+ oc.expectOrder("C", "A")
+ oc.expectOrder("D", "A")
+ oc.expectOrder("E", "A")
+ oc.expectOrder("F", "A")
+ oc.expectOrder("D", "C")
+ oc.expectOrder("E", "C")
+ oc.expectOrder("F", "C")
+ oc.expectOrder("F", "E")
+ oc.expectTotalOrder("B", "D", "F", "E", "C", "A")
+ expectCycles(t, cycles, [][]string{})
+}
+
+func TestSortSelfCycle(t *testing.T) {
+ // This is the graph:
+ // ,---.
+ // | |
+ // A<--'
+ var sorter topoSort
+ sorter.AddEdge("A", "A")
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectTotalOrder("A")
+ expectCycles(t, cycles, [][]string{{"A", "A"}})
+}
+
+func TestSortCycle(t *testing.T) {
+ // This is the graph:
+ // ,-->B-->C
+ // | |
+ // A<------'
+ var sorter topoSort
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("B", "C")
+ sorter.AddEdge("C", "A")
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectTotalOrder("C", "B", "A")
+ expectCycles(t, cycles, [][]string{{"A", "C", "B", "A"}})
+}
+
+func TestSortContainsCycle1(t *testing.T) {
+ // This is the graph:
+ // ,-->B
+ // | ,-----.
+ // | v |
+ // A-->C---->D
+ // | \
+ // | `-->E--.
+ // `-------------`-->F
+ var sorter topoSort
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("A", "C")
+ sorter.AddEdge("A", "F")
+ sorter.AddEdge("C", "D")
+ sorter.AddEdge("C", "E")
+ sorter.AddEdge("D", "C") // creates the cycle
+ sorter.AddEdge("E", "F")
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectOrder("B", "A")
+ oc.expectOrder("C", "A")
+ oc.expectOrder("D", "A")
+ oc.expectOrder("E", "A")
+ oc.expectOrder("F", "A")
+ // The difference with the dag is C, D may be in either order.
+ oc.expectOrder("E", "C")
+ oc.expectOrder("F", "C")
+ oc.expectOrder("F", "E")
+ oc.expectTotalOrder("B", "D", "F", "E", "C", "A")
+ expectCycles(t, cycles, [][]string{{"C", "D", "C"}})
+}
+
+func TestSortContainsCycle2(t *testing.T) {
+ // This is the graph:
+ // ,-->B
+ // | ,-------------.
+ // | v |
+ // A-->C---->D |
+ // | \ |
+ // | `-->E--. |
+ // `-------------`-->F
+ var sorter topoSort
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("A", "C")
+ sorter.AddEdge("A", "F")
+ sorter.AddEdge("C", "D")
+ sorter.AddEdge("C", "E")
+ sorter.AddEdge("E", "F")
+ sorter.AddEdge("F", "C") // creates the cycle
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectOrder("B", "A")
+ oc.expectOrder("C", "A")
+ oc.expectOrder("D", "A")
+ oc.expectOrder("E", "A")
+ oc.expectOrder("F", "A")
+ oc.expectOrder("D", "C")
+ // The difference with the dag is C, E, F may be in any order.
+ oc.expectTotalOrder("B", "D", "F", "E", "C", "A")
+ expectCycles(t, cycles, [][]string{{"C", "F", "E", "C"}})
+}
+
+func TestSortMultiCycles(t *testing.T) {
+ // This is the graph:
+ // ,-->B
+ // | ,------------.
+ // | v |
+ // .--A-->C---->D |
+ // | ^ \ |
+ // | | `-->E--. |
+ // | | | | |
+ // | `---------' | |
+ // `---------------`-->F
+ var sorter topoSort
+ sorter.AddEdge("A", "B")
+ sorter.AddEdge("A", "C")
+ sorter.AddEdge("A", "F")
+ sorter.AddEdge("C", "D")
+ sorter.AddEdge("C", "E")
+ sorter.AddEdge("E", "A") // creates a cycle
+ sorter.AddEdge("E", "F")
+ sorter.AddEdge("F", "C") // creates a cycle
+ sorted, cycles := sorter.Sort()
+ oc := makeOrderChecker(t, sorted)
+ oc.expectOrder("B", "A")
+ oc.expectOrder("D", "A")
+ oc.expectOrder("F", "A")
+ oc.expectOrder("D", "C")
+ oc.expectTotalOrder("B", "D", "F", "E", "C", "A")
+ expectCycles(t, cycles, [][]string{{"A", "E", "C", "A"}, {"C", "F", "E", "C"}})
+}