github.com/tiagovtristao/plz@v13.4.0+incompatible/src/output/shell_output.go (about)

     1  // Package for displaying output on the command line of the current build state.
     2  
     3  package output
     4  
     5  import (
     6  	"bufio"
     7  	"encoding/hex"
     8  	"fmt"
     9  	"math/rand"
    10  	"os"
    11  	"os/exec"
    12  	"path"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"gopkg.in/op/go-logging.v1"
    20  
    21  	"github.com/thought-machine/please/src/build"
    22  	"github.com/thought-machine/please/src/cli"
    23  	"github.com/thought-machine/please/src/core"
    24  	"github.com/thought-machine/please/src/test"
    25  )
    26  
    27  var log = logging.MustGetLogger("output")
    28  
    29  // durationGranularity is the granularity that we build durations at.
    30  const durationGranularity = 10 * time.Millisecond
    31  const testDurationGranularity = time.Millisecond
    32  
    33  // SetColouredOutput forces on or off coloured output in logging and other console output.
    34  func SetColouredOutput(on bool) {
    35  	cli.StdErrIsATerminal = on
    36  }
    37  
    38  // Used to track currently building targets.
    39  type buildingTarget struct {
    40  	sync.Mutex
    41  	buildingTargetData
    42  }
    43  
    44  type buildingTargetData struct {
    45  	Label        core.BuildLabel
    46  	Started      time.Time
    47  	Finished     time.Time
    48  	Description  string
    49  	Active       bool
    50  	Failed       bool
    51  	Cached       bool
    52  	Err          error
    53  	Colour       string
    54  	Target       *core.BuildTarget
    55  	LastProgress float32
    56  	Eta          time.Duration
    57  }
    58  
    59  // MonitorState monitors the build while it's running (essentially until state.TestCases is closed)
    60  // and prints output while it's happening.
    61  func MonitorState(state *core.BuildState, numThreads int, plainOutput, keepGoing, detailedTests bool, traceFile string) {
    62  	failedTargetMap := map[core.BuildLabel]error{}
    63  	buildingTargets := make([]buildingTarget, numThreads)
    64  
    65  	if len(state.Config.Please.Motd) != 0 {
    66  		r := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
    67  		printf("%s\n", state.Config.Please.Motd[r.Intn(len(state.Config.Please.Motd))])
    68  	}
    69  
    70  	displayDone := make(chan struct{})
    71  	stop := make(chan struct{})
    72  	if plainOutput {
    73  		go logProgress(state, &buildingTargets, stop, displayDone)
    74  	} else {
    75  		go display(state, &buildingTargets, stop, displayDone)
    76  	}
    77  	failedTargets := []core.BuildLabel{}
    78  	failedNonTests := []core.BuildLabel{}
    79  	for result := range state.Results() {
    80  		if state.DebugTests && result.Status == core.TargetTesting {
    81  			stop <- struct{}{}
    82  			<-displayDone
    83  			// Ensure that this works again later and we don't deadlock
    84  			// TODO(peterebden): this does not seem like a gloriously elegant synchronisation mechanism...
    85  			go func() {
    86  				<-stop
    87  				displayDone <- struct{}{}
    88  			}()
    89  		}
    90  		processResult(state, result, buildingTargets, plainOutput, keepGoing, &failedTargets, &failedNonTests, failedTargetMap, traceFile != "")
    91  	}
    92  	stop <- struct{}{}
    93  	<-displayDone
    94  	if traceFile != "" {
    95  		writeTrace(traceFile)
    96  	}
    97  	duration := time.Since(state.StartTime).Round(durationGranularity)
    98  	if len(failedNonTests) > 0 { // Something failed in the build step.
    99  		if state.Verbosity > 0 {
   100  			printFailedBuildResults(failedNonTests, failedTargetMap, duration)
   101  		}
   102  		if !keepGoing && !state.Watch {
   103  			// Die immediately and unsuccessfully, this avoids awkward interactions with various things later.
   104  			os.Exit(-1)
   105  		}
   106  	}
   107  	// Check all the targets we wanted to build actually have been built.
   108  	for _, label := range state.ExpandOriginalTargets() {
   109  		if target := state.Graph.Target(label); target == nil {
   110  			log.Fatalf("Target %s doesn't exist in build graph", label)
   111  		} else if (state.NeedHashesOnly || state.PrepareOnly || state.PrepareShell) && target.State() == core.Stopped {
   112  			// Do nothing, we will output about this shortly.
   113  		} else if state.NeedBuild && target != nil && target.State() < core.Built && len(failedTargetMap) == 0 && !target.AddedPostBuild {
   114  			// N.B. Currently targets that are added post-build are excluded here, because in some legit cases this
   115  			//      check can fail incorrectly. It'd be better to verify this more precisely though.
   116  			cycle := graphCycleMessage(state.Graph, target)
   117  			log.Fatalf("Target %s hasn't built but we have no pending tasks left.\n%s", label, cycle)
   118  		}
   119  	}
   120  	if state.Verbosity > 0 && state.NeedBuild && len(failedNonTests) == 0 {
   121  		if state.PrepareOnly || state.PrepareShell {
   122  			printTempDirs(state, duration)
   123  		} else if state.NeedTests { // Got to the test phase, report their results.
   124  			printTestResults(state, failedTargets, duration, detailedTests)
   125  		} else if state.NeedHashesOnly {
   126  			printHashes(state, duration)
   127  		} else if !state.NeedRun { // Must be plz build or similar, report build outputs.
   128  			printBuildResults(state, duration)
   129  		}
   130  	}
   131  }
   132  
   133  // PrintConnectionMessage prints the message when we're initially connected to a remote server.
   134  func PrintConnectionMessage(url string, targets []core.BuildLabel, tests, coverage bool) {
   135  	printf("${WHITE}Connection established to remote plz server at ${BOLD_WHITE}%s${RESET}.\n", url)
   136  	printf("${WHITE}It's building the following %s: ", pluralise(len(targets), "target", "targets"))
   137  	for i, t := range targets {
   138  		if i > 5 {
   139  			printf("${BOLD_WHITE}...${RESET}")
   140  			break
   141  		} else {
   142  			if i > 0 {
   143  				printf(", ")
   144  			}
   145  			printf("${BOLD_WHITE}%s${RESET}", t)
   146  		}
   147  	}
   148  	printf("\n${WHITE}Running tests: ${BOLD_WHITE}%s${RESET}\n", yesNo(tests))
   149  	printf("${WHITE}Coverage: ${BOLD_WHITE}%s${RESET}\n", yesNo(coverage))
   150  	printf("${BOLD_WHITE}Ctrl+C${RESET}${WHITE} to disconnect from it; that will ${BOLD_WHITE}not${RESET}${WHITE} stop the remote build.${RESET}\n")
   151  }
   152  
   153  // PrintDisconnectionMessage prints the message when we're disconnected from the remote server.
   154  func PrintDisconnectionMessage(success, closed, disconnected bool) {
   155  	printf("${BOLD_WHITE}Disconnected from remote plz server.\nStatus: ")
   156  	if disconnected {
   157  		printf("${BOLD_YELLOW}Disconnected${RESET}\n")
   158  	} else if !closed {
   159  		printf("${BOLD_MAGENTA}Unknown${RESET}\n")
   160  	} else if success {
   161  		printf("${BOLD_GREEN}Success${RESET}\n")
   162  	} else {
   163  		printf("${BOLD_RED}Failure${RESET}\n")
   164  	}
   165  }
   166  
   167  func yesNo(b bool) string {
   168  	if b {
   169  		return "yes"
   170  	}
   171  	return "no"
   172  }
   173  
   174  func processResult(state *core.BuildState, result *core.BuildResult, buildingTargets []buildingTarget, plainOutput bool,
   175  	keepGoing bool, failedTargets, failedNonTests *[]core.BuildLabel, failedTargetMap map[core.BuildLabel]error, shouldTrace bool) {
   176  	label := result.Label
   177  	active := result.Status.IsActive()
   178  	failed := result.Status.IsFailure()
   179  	cached := result.Status == core.TargetCached || result.Tests.Cached
   180  	stopped := result.Status == core.TargetBuildStopped
   181  	parse := result.Status == core.PackageParsing || result.Status == core.PackageParsed || result.Status == core.ParseFailed
   182  	// Parse events can overlap in weird ways that mess up the display.
   183  	if shouldTrace && !parse {
   184  		addTrace(result, buildingTargets[result.ThreadID].Label, active)
   185  	}
   186  	target := state.Graph.Target(label)
   187  	if !parse { // Parse tasks happen on a different set of threads.
   188  		updateTarget(state, plainOutput, &buildingTargets[result.ThreadID], label, active, failed, cached, result.Description, result.Err, targetColour(target), target)
   189  	}
   190  	if failed {
   191  		failedTargetMap[label] = result.Err
   192  		// Don't stop here after test failure, aggregate them for later.
   193  		if !keepGoing && result.Status != core.TargetTestFailed {
   194  			// Reset colour so the entire compiler error output doesn't appear red.
   195  			log.Errorf("%s failed:${RESET}\n%s", result.Label, shortError(result.Err))
   196  			state.KillAll()
   197  		} else if !plainOutput { // plain output will have already logged this
   198  			log.Errorf("%s failed: %s", result.Label, shortError(result.Err))
   199  		}
   200  		if keepGoing {
   201  			// This will wait until we've finished up all possible tasks then kill everything off.
   202  			go state.DelayedKillAll()
   203  		}
   204  		*failedTargets = append(*failedTargets, label)
   205  		if result.Status != core.TargetTestFailed {
   206  			*failedNonTests = append(*failedNonTests, label)
   207  		}
   208  	} else if stopped {
   209  		failedTargetMap[result.Label] = nil
   210  	} else if plainOutput && state.ShowTestOutput && result.Status == core.TargetTested && target != nil {
   211  		// If using interactive output we'll print it afterwards.
   212  		for _, testCase := range target.Results.TestCases {
   213  			printf("Finished test %s:\n", testCase.Name)
   214  			for _, testExecution := range testCase.Executions {
   215  				showExecutionOutput(testExecution)
   216  			}
   217  		}
   218  	}
   219  }
   220  
   221  func printTestResults(state *core.BuildState, failedTargets []core.BuildLabel, duration time.Duration, detailed bool) {
   222  	if len(failedTargets) > 0 {
   223  		for _, failed := range failedTargets {
   224  			target := state.Graph.TargetOrDie(failed)
   225  			if target.Results.Failures() == 0 && target.Results.Errors() == 0 {
   226  				if target.Results.TimedOut {
   227  				} else {
   228  					printf("${WHITE_ON_RED}Fail:${RED_NO_BG} %s ${WHITE_ON_RED}Failed to run test${RESET}\n", target.Label)
   229  					target.Results.TestCases = append(target.Results.TestCases, core.TestCase{
   230  						Executions: []core.TestExecution{
   231  							{
   232  								Error: &core.TestResultFailure{
   233  									Type:    "FailedToRun",
   234  									Message: "Failed to run test",
   235  								},
   236  							},
   237  						},
   238  					})
   239  				}
   240  			} else {
   241  				printf("${WHITE_ON_RED}Fail:${RED_NO_BG} %s ${BOLD_GREEN}%3d passed ${BOLD_YELLOW}%3d skipped ${BOLD_RED}%3d failed ${BOLD_CYAN}%3d errored${RESET} Took ${BOLD_WHITE}%s${RESET}\n",
   242  					target.Label, target.Results.Passes(), target.Results.Skips(), target.Results.Failures(), target.Results.Errors(), target.Results.Duration.Round(durationGranularity))
   243  				for _, failingTestCase := range target.Results.TestCases {
   244  					if failingTestCase.Success() != nil {
   245  						continue
   246  					}
   247  					var execution core.TestExecution
   248  					var failure *core.TestResultFailure
   249  					if failures := failingTestCase.Failures(); len(failures) > 0 {
   250  						execution = failures[0]
   251  						failure = execution.Failure
   252  						printf("${BOLD_RED}Failure${RESET}: ${RED}%s${RESET} in %s\n", failure.Type, failingTestCase.Name)
   253  					} else if errors := failingTestCase.Errors(); len(errors) > 0 {
   254  						execution = errors[0]
   255  						failure = execution.Error
   256  						printf("${BOLD_CYAN}Error${RESET}: ${CYAN}%s${RESET} in %s\n", failure.Type, failingTestCase.Name)
   257  					}
   258  					if failure != nil {
   259  						if failure.Message != "" {
   260  							printf("%s\n", failure.Message)
   261  						}
   262  						printf("%s\n", failure.Traceback)
   263  						if len(execution.Stdout) > 0 {
   264  							printf("${BOLD_RED}Standard output${RESET}:\n%s\n", execution.Stdout)
   265  						}
   266  						if len(execution.Stderr) > 0 {
   267  							printf("${BOLD_RED}Standard error${RESET}:\n%s\n", execution.Stderr)
   268  						}
   269  					}
   270  				}
   271  			}
   272  		}
   273  	}
   274  	// Print individual test results
   275  	targets := 0
   276  	aggregate := core.TestSuite{}
   277  	for _, target := range state.Graph.AllTargets() {
   278  		if target.IsTest {
   279  			aggregate.TestCases = append(aggregate.TestCases, target.Results.TestCases...)
   280  			if len(target.Results.TestCases) > 0 {
   281  				if target.Results.Errors() > 0 {
   282  					printf("${CYAN}%s${RESET} %s\n", target.Label, testResultMessage(target.Results))
   283  				} else if target.Results.Failures() > 0 {
   284  					printf("${RED}%s${RESET} %s\n", target.Label, testResultMessage(target.Results))
   285  				} else if detailed || len(failedTargets) == 0 {
   286  					// Succeeded or skipped
   287  					printf("${GREEN}%s${RESET} %s\n", target.Label, testResultMessage(target.Results))
   288  				}
   289  				if state.ShowTestOutput || detailed {
   290  					// Determine max width of test name so we align them
   291  					width := 0
   292  					for _, result := range target.Results.TestCases {
   293  						if len(result.Name) > width {
   294  							width = len(result.Name)
   295  						}
   296  					}
   297  					format := fmt.Sprintf("%%-%ds", width+1)
   298  					for _, result := range target.Results.TestCases {
   299  						printf("    %s\n", formatTestCase(result, fmt.Sprintf(format, result.Name)))
   300  						if len(result.Executions) > 1 {
   301  							for run, execution := range result.Executions {
   302  								printf("        RUN %d: %s\n", run+1, formatTestExecution(execution))
   303  								if state.ShowTestOutput {
   304  									showExecutionOutput(execution)
   305  								}
   306  							}
   307  						} else {
   308  							if state.ShowTestOutput {
   309  								showExecutionOutput(result.Executions[0])
   310  							}
   311  						}
   312  					}
   313  				}
   314  				targets++
   315  			} else if target.Results.TimedOut {
   316  				printf("${RED}%s${RESET} ${WHITE_ON_RED}Timed out${RESET}\n", target.Label)
   317  				targets++
   318  			}
   319  		}
   320  	}
   321  	printf(fmt.Sprintf("${BOLD_WHITE}%s and %s${BOLD_WHITE}. Total time %s.${RESET}\n",
   322  		pluralise(targets, "test target", "test targets"), testResultMessage(aggregate), duration))
   323  }
   324  
   325  func showExecutionOutput(execution core.TestExecution) {
   326  	if execution.Stdout != "" && execution.Stderr != "" {
   327  		printf("StdOut:\n%s\nStdErr:\n%s\n", execution.Stdout, execution.Stderr)
   328  	} else if execution.Stdout != "" {
   329  		print(execution.Stdout)
   330  	} else if execution.Stderr != "" {
   331  		print(execution.Stderr)
   332  	}
   333  }
   334  
   335  func formatTestCase(result core.TestCase, name string) string {
   336  	if len(result.Executions) == 0 {
   337  		return fmt.Sprintf("%s (No results)", formatTestName(result, name))
   338  	}
   339  	var outcome core.TestExecution
   340  	if len(result.Executions) > 1 && result.Success() != nil {
   341  		return fmt.Sprintf("%s ${BOLD_MAGENTA}%s${RESET}", formatTestName(result, name), "FLAKY PASS")
   342  	}
   343  
   344  	if result.Success() != nil {
   345  		outcome = *result.Success()
   346  	} else if result.Skip() != nil {
   347  		outcome = *result.Skip()
   348  	} else if len(result.Errors()) > 0 {
   349  		outcome = result.Errors()[0]
   350  	} else if len(result.Failures()) > 0 {
   351  		outcome = result.Failures()[0]
   352  	}
   353  	return fmt.Sprintf("%s %s", formatTestName(result, name), formatTestExecution(outcome))
   354  }
   355  
   356  func formatTestName(testCase core.TestCase, name string) string {
   357  	if testCase.Success() != nil {
   358  		return fmt.Sprintf("${GREEN}%s${RESET}", name)
   359  	}
   360  	if testCase.Skip() != nil {
   361  		return fmt.Sprintf("${YELLOW}%s${RESET}", name)
   362  	}
   363  	if len(testCase.Errors()) > 0 {
   364  		return fmt.Sprintf("${CYAN}%s${RESET}", name)
   365  	}
   366  	if len(testCase.Failures()) > 0 {
   367  		return fmt.Sprintf("${RED}%s${RESET}", name)
   368  	}
   369  	return testCase.Name
   370  }
   371  
   372  func formatTestExecution(execution core.TestExecution) string {
   373  	if execution.Error != nil {
   374  		return "${BOLD_CYAN}ERROR${RESET}"
   375  	}
   376  	if execution.Failure != nil {
   377  		return fmt.Sprintf("${BOLD_RED}FAIL${RESET} %s", maybeToString(execution.Duration))
   378  	}
   379  	if execution.Skip != nil {
   380  		// Not usually interesting to have a duration when we did no work.
   381  		return "${BOLD_YELLOW}SKIP${RESET}"
   382  	}
   383  	return fmt.Sprintf("${BOLD_GREEN}PASS${RESET} %s", maybeToString(execution.Duration))
   384  }
   385  
   386  func maybeToString(duration *time.Duration) string {
   387  	if duration == nil {
   388  		return ""
   389  	}
   390  	return fmt.Sprintf(" ${BOLD_WHITE}%s${RESET}", duration.Round(testDurationGranularity))
   391  }
   392  
   393  // logProgress continually logs progress messages every 10s explaining where we're up to.
   394  func logProgress(state *core.BuildState, buildingTargets *[]buildingTarget, stop <-chan struct{}, done chan<- struct{}) {
   395  	t := time.NewTicker(10 * time.Second)
   396  	defer t.Stop()
   397  	for {
   398  		select {
   399  		case <-t.C:
   400  			busy := 0
   401  			for i := 0; i < len(*buildingTargets); i++ {
   402  				if (*buildingTargets)[i].Active {
   403  					busy++
   404  				}
   405  			}
   406  			log.Notice("Build running for %s, %d / %d tasks done, %s busy", time.Since(state.StartTime).Round(time.Second), state.NumDone(), state.NumActive(), pluralise(busy, "worker", "workers"))
   407  		case <-stop:
   408  			done <- struct{}{}
   409  			return
   410  		}
   411  	}
   412  }
   413  
   414  // Produces a string describing the results of one test (or a single aggregation).
   415  func testResultMessage(results core.TestSuite) string {
   416  	msg := fmt.Sprintf("%s run", pluralise(results.Tests(), "test", "tests"))
   417  	if results.Duration >= 0.0 {
   418  		msg += fmt.Sprintf(" in ${BOLD_WHITE}%s${RESET}", results.Duration.Round(testDurationGranularity))
   419  	}
   420  	msg += fmt.Sprintf("; ${BOLD_GREEN}%d passed${RESET}", results.Passes())
   421  	if results.Errors() > 0 {
   422  		msg += fmt.Sprintf(", ${BOLD_CYAN}%d errored${RESET}", results.Errors())
   423  	}
   424  	if results.Failures() > 0 {
   425  		msg += fmt.Sprintf(", ${BOLD_RED}%d failed${RESET}", results.Failures())
   426  	}
   427  	if results.Skips() > 0 {
   428  		msg += fmt.Sprintf(", ${BOLD_YELLOW}%d skipped${RESET}", results.Skips())
   429  	}
   430  	if results.FlakyPasses() > 0 {
   431  		msg += fmt.Sprintf(", ${BOLD_MAGENTA}%s${RESET}", pluralise(results.FlakyPasses(), "flake", "flakes"))
   432  	}
   433  	if results.TimedOut {
   434  		msg += ", ${RED_ON_WHITE}TIMED OUT${RESET}"
   435  	}
   436  	if results.Cached {
   437  		msg += " ${GREEN}[cached]${RESET}"
   438  	}
   439  	return msg
   440  }
   441  
   442  func printBuildResults(state *core.BuildState, duration time.Duration) {
   443  	// Count incrementality.
   444  	totalBuilt := 0
   445  	totalReused := 0
   446  	for _, target := range state.Graph.AllTargets() {
   447  		if target.State() == core.Built {
   448  			totalBuilt++
   449  		} else if target.State() == core.Reused {
   450  			totalReused++
   451  		}
   452  	}
   453  	incrementality := 100.0 * float64(totalReused) / float64(totalBuilt+totalReused)
   454  	if totalBuilt+totalReused == 0 {
   455  		incrementality = 100 // avoid NaN
   456  	}
   457  	// Print this stuff so we always see it.
   458  	printf("Build finished; total time %s, incrementality %.1f%%. Outputs:\n", duration, incrementality)
   459  	for _, label := range state.ExpandVisibleOriginalTargets() {
   460  		target := state.Graph.TargetOrDie(label)
   461  		fmt.Printf("%s:\n", label)
   462  		for _, result := range buildResult(target) {
   463  			fmt.Printf("  %s\n", result)
   464  		}
   465  	}
   466  }
   467  
   468  func printHashes(state *core.BuildState, duration time.Duration) {
   469  	fmt.Printf("Hashes calculated, total time %s:\n", duration)
   470  	for _, label := range state.ExpandVisibleOriginalTargets() {
   471  		hash, err := build.OutputHash(state, state.Graph.TargetOrDie(label))
   472  		if err != nil {
   473  			fmt.Printf("  %s: cannot calculate: %s\n", label, err)
   474  		} else {
   475  			fmt.Printf("  %s: %s\n", label, hex.EncodeToString(hash))
   476  		}
   477  	}
   478  }
   479  
   480  func printTempDirs(state *core.BuildState, duration time.Duration) {
   481  	fmt.Printf("Temp directories prepared, total time %s:\n", duration)
   482  	for _, label := range state.ExpandVisibleOriginalTargets() {
   483  		target := state.Graph.TargetOrDie(label)
   484  		cmd := target.GetCommand(state)
   485  		dir := target.TmpDir()
   486  		env := core.BuildEnvironment(state, target)
   487  		if state.NeedTests {
   488  			cmd = target.GetTestCommand(state)
   489  			dir = path.Join(core.RepoRoot, target.TestDir())
   490  			env = core.TestEnvironment(state, target, dir)
   491  		}
   492  		cmd = build.ReplaceSequences(state, target, cmd)
   493  		fmt.Printf("  %s: %s\n", label, dir)
   494  		fmt.Printf("    Command: %s\n", cmd)
   495  		if !state.PrepareShell {
   496  			// This isn't very useful if we're opening a shell (since then the vars will be set anyway)
   497  			fmt.Printf("   Expanded: %s\n", os.Expand(cmd, env.ReplaceEnvironment))
   498  		} else {
   499  			fmt.Printf("\n")
   500  			argv := []string{"bash", "--noprofile", "--norc", "-o", "pipefail"}
   501  			if (state.NeedTests && target.TestSandbox) || (!state.NeedTests && target.Sandbox) {
   502  				argv = core.MustSandboxCommand(state, argv)
   503  			}
   504  			log.Debug("Full command: %s", strings.Join(argv, " "))
   505  			cmd := exec.Command(argv[0], argv[1:]...)
   506  			cmd.Dir = dir
   507  			cmd.Env = env
   508  			cmd.Stdin = os.Stdin
   509  			cmd.Stdout = os.Stdout
   510  			cmd.Stderr = os.Stderr
   511  			cmd.Run() // Ignore errors, it will typically end by the user killing it somehow.
   512  		}
   513  	}
   514  }
   515  
   516  func buildResult(target *core.BuildTarget) []string {
   517  	results := []string{}
   518  	if target != nil {
   519  		for _, out := range target.Outputs() {
   520  			if core.StartedAtRepoRoot() {
   521  				results = append(results, path.Join(target.OutDir(), out))
   522  			} else {
   523  				results = append(results, path.Join(core.RepoRoot, target.OutDir(), out))
   524  			}
   525  		}
   526  	}
   527  	return results
   528  }
   529  
   530  func printFailedBuildResults(failedTargets []core.BuildLabel, failedTargetMap map[core.BuildLabel]error, duration time.Duration) {
   531  	printf("${WHITE_ON_RED}Build stopped after %s. %s failed:${RESET}\n", duration, pluralise(len(failedTargetMap), "target", "targets"))
   532  	for _, label := range failedTargets {
   533  		err := failedTargetMap[label]
   534  		if err != nil {
   535  			if cli.StdErrIsATerminal {
   536  				printf("    ${BOLD_RED}%s\n${RESET}%s${RESET}\n", label, colouriseError(err))
   537  			} else {
   538  				printf("    %s\n%s\n", label, err)
   539  			}
   540  		} else {
   541  			printf("    ${BOLD_RED}%s${RESET}\n", label)
   542  		}
   543  	}
   544  }
   545  
   546  func updateTarget(state *core.BuildState, plainOutput bool, buildingTarget *buildingTarget, label core.BuildLabel,
   547  	active bool, failed bool, cached bool, description string, err error, colour string, target *core.BuildTarget) {
   548  	updateTarget2(buildingTarget, label, active, failed, cached, description, err, colour, target)
   549  	if plainOutput {
   550  		if !active {
   551  			active := pluralise(state.NumActive(), "task", "tasks")
   552  			log.Info("[%d/%s] %s: %s [%3.1fs]", state.NumDone(), active, label.String(), description, time.Since(buildingTarget.Started).Seconds())
   553  		} else {
   554  			log.Info("%s: %s", label.String(), description)
   555  		}
   556  	}
   557  }
   558  
   559  func updateTarget2(target *buildingTarget, label core.BuildLabel, active bool, failed bool, cached bool, description string, err error, colour string, t *core.BuildTarget) {
   560  	target.Lock()
   561  	defer target.Unlock()
   562  	target.Label = label
   563  	target.Description = description
   564  	if !target.Active {
   565  		// Starting to build now.
   566  		target.Started = time.Now()
   567  		target.Finished = target.Started
   568  	} else if !active {
   569  		// finished building
   570  		target.Finished = time.Now()
   571  	}
   572  	target.Active = active
   573  	target.Failed = failed
   574  	target.Cached = cached
   575  	target.Err = err
   576  	target.Colour = colour
   577  	target.Target = t
   578  }
   579  
   580  func targetColour(target *core.BuildTarget) string {
   581  	if target == nil {
   582  		return "${BOLD_CYAN}" // unknown
   583  	} else if target.IsBinary {
   584  		return "${BOLD}" + targetColour2(target)
   585  	} else {
   586  		return targetColour2(target)
   587  	}
   588  }
   589  
   590  func targetColour2(target *core.BuildTarget) string {
   591  	// Quick heuristic on language types. May want to make this configurable.
   592  	for _, require := range target.Requires {
   593  		if require == "py" {
   594  			return "${GREEN}"
   595  		} else if require == "java" {
   596  			return "${RED}"
   597  		} else if require == "go" {
   598  			return "${YELLOW}"
   599  		} else if require == "js" {
   600  			return "${BLUE}"
   601  		}
   602  	}
   603  	if strings.Contains(target.Label.PackageName, "third_party") {
   604  		return "${MAGENTA}"
   605  	}
   606  	return "${WHITE}"
   607  }
   608  
   609  // Since this is a gentleman's build tool, we'll make an effort to get plurals correct
   610  // in at least this one place.
   611  func pluralise(num int, singular, plural string) string {
   612  	if num == 1 {
   613  		return fmt.Sprintf("1 %s", singular)
   614  	}
   615  	return fmt.Sprintf("%d %s", num, plural)
   616  }
   617  
   618  // PrintCoverage writes out coverage metrics after a test run in a file tree setup.
   619  // Only files that were covered by tests and not excluded are shown.
   620  func PrintCoverage(state *core.BuildState, includeFiles []string) {
   621  	printf("${BOLD_WHITE}Coverage results:${RESET}\n")
   622  	totalCovered := 0
   623  	totalTotal := 0
   624  	lastDir := "_"
   625  	for _, file := range state.Coverage.OrderedFiles() {
   626  		if !shouldInclude(file, includeFiles) {
   627  			continue
   628  		}
   629  		dir := filepath.Dir(file)
   630  		if dir != lastDir {
   631  			printf("${WHITE}%s:${RESET}\n", strings.TrimRight(dir, "/"))
   632  		}
   633  		lastDir = dir
   634  		covered, total := test.CountCoverage(state.Coverage.Files[file])
   635  		printf("  %s\n", coveragePercentage(covered, total, file[len(dir)+1:]))
   636  		totalCovered += covered
   637  		totalTotal += total
   638  	}
   639  	printf("${BOLD_WHITE}Total coverage: %s${RESET}\n", coveragePercentage(totalCovered, totalTotal, ""))
   640  }
   641  
   642  // PrintLineCoverageReport writes out line-by-line coverage metrics after a test run.
   643  func PrintLineCoverageReport(state *core.BuildState, includeFiles []string) {
   644  	coverageColours := map[core.LineCoverage]string{
   645  		core.NotExecutable: "${GREY}",
   646  		core.Unreachable:   "${YELLOW}",
   647  		core.Uncovered:     "${RED}",
   648  		core.Covered:       "${GREEN}",
   649  	}
   650  
   651  	printf("${BOLD_WHITE}Covered files:${RESET}\n")
   652  	for _, file := range state.Coverage.OrderedFiles() {
   653  		if !shouldInclude(file, includeFiles) {
   654  			continue
   655  		}
   656  		coverage := state.Coverage.Files[file]
   657  		covered, total := test.CountCoverage(coverage)
   658  		printf("${BOLD_WHITE}%s: %s${RESET}\n", file, coveragePercentage(covered, total, ""))
   659  		f, err := os.Open(file)
   660  		if err != nil {
   661  			printf("${BOLD_RED}Can't open: %s${RESET}\n", err)
   662  			continue
   663  		}
   664  		defer f.Close()
   665  		scanner := bufio.NewScanner(f)
   666  		i := 0
   667  		for scanner.Scan() {
   668  			if i < len(coverage) {
   669  				printf("${WHITE}%4d %s%s\n", i, coverageColours[coverage[i]], scanner.Text())
   670  			} else {
   671  				// Assume the lines are not executable. This happens for python, for example.
   672  				printf("${WHITE}%4d ${GREY}%s\n", i, scanner.Text())
   673  			}
   674  			i++
   675  		}
   676  		printf("${RESET}\n")
   677  	}
   678  }
   679  
   680  // shouldInclude returns true if we should include a file in the coverage display.
   681  func shouldInclude(file string, files []string) bool {
   682  	if len(files) == 0 {
   683  		return true
   684  	}
   685  	for _, f := range files {
   686  		if file == f {
   687  			return true
   688  		}
   689  	}
   690  	return false
   691  }
   692  
   693  // Returns some appropriate ANSI colour code for a coverage percentage.
   694  func coverageColour(percentage float32) string {
   695  	// TODO(pebers): consider making these configurable?
   696  	if percentage < 20.0 {
   697  		return "${MAGENTA}"
   698  	} else if percentage < 60.0 {
   699  		return "${BOLD_RED}"
   700  	} else if percentage < 80.0 {
   701  		return "${BOLD_YELLOW}"
   702  	}
   703  	return "${BOLD_GREEN}"
   704  }
   705  
   706  func coveragePercentage(covered, total int, label string) string {
   707  	if total == 0 {
   708  		return fmt.Sprintf("${BOLD_MAGENTA}%s No data${RESET}", label)
   709  	}
   710  	percentage := 100.0 * float32(covered) / float32(total)
   711  	return fmt.Sprintf("%s%s %d/%s, %2.1f%%${RESET}", coverageColour(percentage), label, covered, pluralise(total, "line", "lines"), percentage)
   712  }
   713  
   714  // colouriseError adds a splash of colour to a compiler error message.
   715  // This is a similar effect to -fcolor-diagnostics in Clang, but we attempt to apply it fairly generically.
   716  func colouriseError(err error) error {
   717  	msg := []string{}
   718  	for _, line := range strings.Split(err.Error(), "\n") {
   719  		if groups := errorMessageRe.FindStringSubmatch(line); groups != nil {
   720  			if groups[3] != "" {
   721  				groups[3] = ", column " + groups[3]
   722  			}
   723  			if groups[4] != "" {
   724  				groups[4] += ": "
   725  			}
   726  			msg = append(msg, fmt.Sprintf("${BOLD_WHITE}%s, line %s%s:${RESET} ${BOLD_RED}%s${RESET}${BOLD_WHITE}%s${RESET}", groups[1], groups[2], groups[3], groups[4], groups[5]))
   727  		} else {
   728  			msg = append(msg, line)
   729  		}
   730  	}
   731  	return fmt.Errorf("%s", strings.Join(msg, "\n"))
   732  }
   733  
   734  // errorMessageRe is a regex to find lines that look like they're specifying a file.
   735  var errorMessageRe = regexp.MustCompile(`^([^ ]+\.[^: /]+):([0-9]+):(?:([0-9]+):)? *(?:([a-z-_ ]+):)? (.*)$`)
   736  
   737  // graphCycleMessage attempts to detect graph cycles and produces a readable message from it.
   738  func graphCycleMessage(graph *core.BuildGraph, target *core.BuildTarget) string {
   739  	if cycle := findGraphCycle(graph, target); len(cycle) > 0 {
   740  		msg := "Dependency cycle found:\n"
   741  		msg += fmt.Sprintf("    %s\n", cycle[len(cycle)-1].Label)
   742  		for i := len(cycle) - 2; i >= 0; i-- {
   743  			msg += fmt.Sprintf(" -> %s\n", cycle[i].Label)
   744  		}
   745  		msg += fmt.Sprintf(" -> %s\n", cycle[len(cycle)-1].Label)
   746  		return msg + fmt.Sprintf("Sorry, but you'll have to refactor your build files to avoid this cycle.")
   747  	}
   748  	return unbuiltTargetsMessage(graph)
   749  }
   750  
   751  // Attempts to detect cycles in the build graph. Returns an empty slice if none is found,
   752  // otherwise returns a slice of labels describing the cycle.
   753  func findGraphCycle(graph *core.BuildGraph, target *core.BuildTarget) []*core.BuildTarget {
   754  	index := func(haystack []*core.BuildTarget, needle *core.BuildTarget) int {
   755  		for i, straw := range haystack {
   756  			if straw == needle {
   757  				return i
   758  			}
   759  		}
   760  		return -1
   761  	}
   762  
   763  	done := map[core.BuildLabel]bool{}
   764  	var detectCycle func(*core.BuildTarget, []*core.BuildTarget) []*core.BuildTarget
   765  	detectCycle = func(target *core.BuildTarget, deps []*core.BuildTarget) []*core.BuildTarget {
   766  		if i := index(deps, target); i != -1 {
   767  			return deps[i:]
   768  		} else if done[target.Label] {
   769  			return nil
   770  		}
   771  		done[target.Label] = true
   772  		deps = append(deps, target)
   773  		for _, dep := range target.Dependencies() {
   774  			if cycle := detectCycle(dep, deps); len(cycle) > 0 {
   775  				return cycle
   776  			}
   777  		}
   778  		return nil
   779  	}
   780  	return detectCycle(target, nil)
   781  }
   782  
   783  // unbuiltTargetsMessage returns a message for any targets that are supposed to build but haven't yet.
   784  func unbuiltTargetsMessage(graph *core.BuildGraph) string {
   785  	msg := ""
   786  	for _, target := range graph.AllTargets() {
   787  		if target.State() == core.Active {
   788  			if graph.AllDepsBuilt(target) {
   789  				msg += fmt.Sprintf("  %s (waiting for deps to build)\n", target.Label)
   790  			} else {
   791  				msg += fmt.Sprintf("  %s\n", target.Label)
   792  			}
   793  		} else if target.State() == core.Pending {
   794  			msg += fmt.Sprintf("  %s (pending build)\n", target.Label)
   795  		}
   796  	}
   797  	if msg != "" {
   798  		return "\nThe following targets have not yet built:\n" + msg
   799  	}
   800  	return ""
   801  }
   802  
   803  // shortError returns the message for an error, shortening it if the error supports that.
   804  func shortError(err error) string {
   805  	if se, ok := err.(shortenableError); ok {
   806  		return se.ShortError()
   807  	} else if err == nil {
   808  		return "unknown error" // This shouldn't really happen...
   809  	}
   810  	return err.Error()
   811  }
   812  
   813  // A shortenableError describes any error type that can communicate a short-form error.
   814  type shortenableError interface {
   815  	ShortError() string
   816  }