github.com/windmilleng/wat@v0.0.2-0.20180626175338-9349b638e250/cli/wat/wat.go (about)

     1  package wat
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"time"
    10  
    11  	"strconv"
    12  
    13  	"github.com/spf13/cobra"
    14  )
    15  
    16  var CmdTimeout time.Duration
    17  
    18  const Divider = "--------------------\n"
    19  
    20  const appNameWat = "wat"
    21  
    22  var dryRun bool
    23  var numCmds int
    24  
    25  var rootCmd = &cobra.Command{
    26  	Use:   "wat",
    27  	Short: "WAT (Win At Tests!) figures out what tests you should run next, and runs them for you",
    28  	Run:   wat,
    29  }
    30  
    31  func init() {
    32  	rootCmd.PersistentFlags().DurationVarP(&CmdTimeout, "timeout", "t", 2*time.Minute, "timeout for training/running commands")
    33  	rootCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "just print recommended commands, don't run them")
    34  	rootCmd.Flags().IntVarP(&numCmds, "numCmds", "n", nDecideCommands, "number of commands WAT should suggest/run")
    35  
    36  	rootCmd.AddCommand(initCmd)
    37  	rootCmd.AddCommand(recentCmd)
    38  	rootCmd.AddCommand(populateCmd)
    39  	rootCmd.AddCommand(listCmd)
    40  	rootCmd.AddCommand(trainCmd)
    41  }
    42  
    43  func Execute() (outerErr error) {
    44  	_, analyticsCmd, err := initAnalytics()
    45  	if err != nil {
    46  		return err
    47  	}
    48  
    49  	rootCmd.AddCommand(analyticsCmd)
    50  
    51  	return rootCmd.Execute()
    52  }
    53  
    54  func wat(_ *cobra.Command, args []string) {
    55  	ctx := context.Background()
    56  
    57  	ws, err := GetOrInitWatWorkspace()
    58  	if err != nil {
    59  		ws.Fatal("GetWatWorkspace", err)
    60  	}
    61  
    62  	// TODO: should probs be able to pass edits into `Decide` (or use the edits that
    63  	// `Decide` found) rather than needing to get them twice.
    64  	recentEdits, err := RecentFileNames(ws)
    65  
    66  	cmds, err := Decide(ctx, ws, numCmds)
    67  	if err != nil {
    68  		ws.Fatal("Decide", err)
    69  	}
    70  
    71  	if dryRun {
    72  		fmt.Fprintln(os.Stderr, "WAT recommends the following commands:")
    73  	} else {
    74  		fmt.Fprintln(os.Stderr, "WAT will run the following commands:")
    75  	}
    76  	for _, cmd := range cmds {
    77  		// print recommended cmds to terminal (properly escaped, but not wrapped in quotes,
    78  		// in case user wants to copy/paste, pipe somewhere, etc.)
    79  		safe := fmt.Sprintf("%q", cmd.Command)
    80  		fmt.Printf("\t%s\n", tryUnquote(safe))
    81  	}
    82  
    83  	if dryRun {
    84  		// it's a dry run, don't actually run the commands
    85  		return
    86  	}
    87  
    88  	logContext := LogContext{
    89  		RecentEdits: recentEdits,
    90  		StartTime:   time.Now(),
    91  		Source:      LogSourceUser,
    92  	}
    93  
    94  	err = RunCommands(ctx, ws, cmds, CmdTimeout, os.Stdout, os.Stderr, logContext)
    95  	if err != nil {
    96  		ws.Fatal("RunCommands", err)
    97  	}
    98  }
    99  
   100  func runCmdAndLog(ctx context.Context, root string, c WatCommand, outStream, errStream io.Writer) (CommandLog, error) {
   101  	start := time.Now()
   102  
   103  	err := runCmd(ctx, root, c.Command, outStream, errStream)
   104  
   105  	if ctx.Err() != nil {
   106  		// Propagate cancel/timeout errors
   107  		return CommandLog{}, ctx.Err()
   108  	}
   109  
   110  	if err != nil {
   111  		if _, ok := err.(*exec.ExitError); !ok {
   112  			// NOT an exit error, i.e. it's an unexpected error; stop execution.
   113  			return CommandLog{}, err
   114  		}
   115  	}
   116  
   117  	// Either we have no error, or an ExitError (i.e. expected case: cmd
   118  	// exited with non-zero exit code).
   119  	return CommandLog{
   120  		Command:  c.Command,
   121  		Success:  err == nil,
   122  		Duration: time.Since(start),
   123  	}, nil
   124  }
   125  
   126  func runCmd(ctx context.Context, root, command string, outStream, errStream io.Writer) error {
   127  	cmd := exec.CommandContext(ctx, "bash", "-c", command)
   128  	cmd.Dir = root
   129  	cmd.Stdout = outStream
   130  	cmd.Stderr = errStream
   131  
   132  	return cmd.Run()
   133  }
   134  
   135  func runCmds(ctx context.Context, root string, cmds []WatCommand, timeout time.Duration,
   136  	outStream, errStream io.Writer) ([]CommandLog, error) {
   137  	logs := []CommandLog{}
   138  
   139  	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
   140  	defer cancel()
   141  
   142  	errStream.Write([]byte(Divider))
   143  	for _, c := range cmds {
   144  		outStream.Write([]byte(c.PrettyCmd()))
   145  
   146  		log, err := runCmdAndLog(timeoutCtx, root, c, outStream, errStream)
   147  		if err != nil {
   148  			return logs, err
   149  		}
   150  
   151  		errStream.Write([]byte(Divider))
   152  		logs = append(logs, log)
   153  	}
   154  
   155  	return logs, nil
   156  }
   157  
   158  // Runs the given commands and logs their results to file for use in making our ML model
   159  func RunCommands(ctx context.Context, ws WatWorkspace, cmds []WatCommand, timeout time.Duration,
   160  	outStream, errStream io.Writer, logContext LogContext) error {
   161  	t := time.Now()
   162  	logs, err := runCmds(ctx, ws.Root(), cmds, timeout, outStream, errStream)
   163  	if err != nil {
   164  		// If we got an unexpected err running commands, don't bother logging
   165  		return err
   166  	}
   167  	ws.a.Timer(timerCommandsRun, time.Since(t), nil)
   168  	logGroup := CommandLogGroup{
   169  		Logs:    logs,
   170  		Context: logContext,
   171  	}
   172  	return CmdLogGroupsToFile(ws, []CommandLogGroup{logGroup})
   173  }
   174  
   175  func tryUnquote(s string) string {
   176  	res, err := strconv.Unquote(s)
   177  	if err == nil {
   178  		return res
   179  	}
   180  	return s
   181  }