github.phpd.cn/thought-machine/please@v12.2.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  	"build"
    22  	"cli"
    23  	"core"
    24  	"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.Results is closed)
    60  // and prints output while it's happening.
    61  func MonitorState(state *core.BuildState, numThreads int, plainOutput, keepGoing, shouldBuild, shouldTest, shouldRun, showStatus bool, traceFile string) bool {
    62  	failedTargetMap := map[core.BuildLabel]error{}
    63  	buildingTargets := make([]buildingTarget, numThreads)
    64  
    65  	if len(state.Config.Please.Motd) != 0 {
    66  		printf("%s\n", state.Config.Please.Motd[rand.Intn(len(state.Config.Please.Motd))])
    67  	}
    68  
    69  	displayDone := make(chan struct{})
    70  	stop := make(chan struct{})
    71  	if plainOutput {
    72  		go logProgress(state, &buildingTargets, stop, displayDone)
    73  	} else {
    74  		go display(state, &buildingTargets, stop, displayDone)
    75  	}
    76  	aggregatedResults := core.TestResults{}
    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, &aggregatedResults, 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  		// Die immediately and unsuccessfully, this avoids awkward interactions with
   103  		// --failing_tests_ok later on.
   104  		os.Exit(-1)
   105  	}
   106  	// Check all the targets we wanted to build actually have been built.
   107  	for _, label := range state.ExpandOriginalTargets() {
   108  		if target := state.Graph.Target(label); target == nil {
   109  			log.Fatalf("Target %s doesn't exist in build graph", label)
   110  		} else if (state.NeedHashesOnly || state.PrepareOnly) && target.State() == core.Stopped {
   111  			// Do nothing, we will output about this shortly.
   112  		} else if shouldBuild && target != nil && target.State() < core.Built && len(failedTargetMap) == 0 && !target.AddedPostBuild {
   113  			// N.B. Currently targets that are added post-build are excluded here, because in some legit cases this
   114  			//      check can fail incorrectly. It'd be better to verify this more precisely though.
   115  			cycle := graphCycleMessage(state.Graph, target)
   116  			log.Fatalf("Target %s hasn't built but we have no pending tasks left.\n%s", label, cycle)
   117  		}
   118  	}
   119  	if state.Verbosity > 0 && shouldBuild {
   120  		if shouldTest { // Got to the test phase, report their results.
   121  			printTestResults(state, aggregatedResults, failedTargets, duration)
   122  		} else if state.NeedHashesOnly {
   123  			printHashes(state, duration)
   124  		} else if state.PrepareOnly {
   125  			printTempDirs(state, duration)
   126  		} else if !shouldRun { // Must be plz build or similar, report build outputs.
   127  			printBuildResults(state, duration, showStatus)
   128  		}
   129  	}
   130  	return len(failedTargetMap) == 0
   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, aggregatedResults *core.TestResults, plainOutput bool,
   175  	keepGoing bool, failedTargets, failedNonTests *[]core.BuildLabel, failedTargetMap map[core.BuildLabel]error, shouldTrace bool) {
   176  	label := result.Label
   177  	active := result.Status == core.PackageParsing || result.Status == core.TargetBuilding || result.Status == core.TargetTesting
   178  	failed := result.Status == core.ParseFailed || result.Status == core.TargetBuildFailed || result.Status == core.TargetTestFailed
   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  	if shouldTrace {
   183  		addTrace(result, buildingTargets[result.ThreadID].Label, active)
   184  	}
   185  	if failed && result.Tests.NumTests == 0 && result.Tests.Failed == 0 {
   186  		result.Tests.NumTests = 1
   187  		result.Tests.Failed = 1 // Ensure there's one test failure when there're no results to parse.
   188  	}
   189  	// Only aggregate test results the first time it finishes.
   190  	if buildingTargets[result.ThreadID].Active && !active {
   191  		aggregatedResults.Aggregate(&result.Tests)
   192  	}
   193  	target := state.Graph.Target(label)
   194  	if !parse { // Parse tasks happen on a different set of threads.
   195  		updateTarget(state, plainOutput, &buildingTargets[result.ThreadID], label, active, failed, cached, result.Description, result.Err, targetColour(target), target)
   196  	}
   197  	if failed {
   198  		failedTargetMap[label] = result.Err
   199  		// Don't stop here after test failure, aggregate them for later.
   200  		if !keepGoing && result.Status != core.TargetTestFailed {
   201  			// Reset colour so the entire compiler error output doesn't appear red.
   202  			log.Errorf("%s failed:${RESET}\n%s", result.Label, shortError(result.Err))
   203  			state.KillAll()
   204  		} else if !plainOutput { // plain output will have already logged this
   205  			log.Errorf("%s failed: %s", result.Label, shortError(result.Err))
   206  		}
   207  		*failedTargets = append(*failedTargets, label)
   208  		if result.Status != core.TargetTestFailed {
   209  			*failedNonTests = append(*failedNonTests, label)
   210  		}
   211  	} else if stopped {
   212  		failedTargetMap[result.Label] = nil
   213  	} else if plainOutput && state.ShowTestOutput && result.Status == core.TargetTested && target != nil {
   214  		// If using interactive output we'll print it afterwards.
   215  		printf("Finished test %s:\n%s\n", label, target.Results.Output)
   216  	}
   217  }
   218  
   219  func printTestResults(state *core.BuildState, aggregatedResults core.TestResults, failedTargets []core.BuildLabel, duration time.Duration) {
   220  	if len(failedTargets) > 0 {
   221  		for _, failed := range failedTargets {
   222  			target := state.Graph.TargetOrDie(failed)
   223  			if len(target.Results.Failures) == 0 {
   224  				if target.Results.TimedOut {
   225  					printf("${WHITE_ON_RED}Fail:${RED_NO_BG} %s ${WHITE_ON_RED}Timed out${RESET}\n", target.Label)
   226  				} else {
   227  					printf("${WHITE_ON_RED}Fail:${RED_NO_BG} %s ${WHITE_ON_RED}Failed to run test${RESET}\n", target.Label)
   228  				}
   229  			} else {
   230  				printf("${WHITE_ON_RED}Fail:${RED_NO_BG} %s ${BOLD_GREEN}%3d passed ${BOLD_YELLOW}%3d skipped ${BOLD_RED}%3d failed ${BOLD_WHITE}Took %s${RESET}\n",
   231  					target.Label, target.Results.Passed, target.Results.Skipped, target.Results.Failed, target.Results.Duration.Round(durationGranularity))
   232  				for _, failure := range target.Results.Failures {
   233  					printf("${BOLD_RED}Failure: %s in %s${RESET}\n", failure.Type, failure.Name)
   234  					printf("%s\n", failure.Traceback)
   235  					if len(failure.Stdout) > 0 {
   236  						printf("${BOLD_RED}Standard output:${RESET}\n%s\n", failure.Stdout)
   237  					}
   238  					if len(failure.Stderr) > 0 {
   239  						printf("${BOLD_RED}Standard error:${RESET}\n%s\n", failure.Stderr)
   240  					}
   241  				}
   242  			}
   243  			if len(target.Results.Output) > 0 {
   244  				printf("${BOLD_RED}Full output:${RESET}\n%s\n", target.Results.Output)
   245  			}
   246  			if target.Results.Flakes > 0 {
   247  				printf("${BOLD_MAGENTA}Flaky target; made %s before giving up${RESET}\n", pluralise(target.Results.Flakes, "attempt", "attempts"))
   248  			}
   249  		}
   250  	}
   251  	// Print individual test results
   252  	i := 0
   253  	for _, target := range state.Graph.AllTargets() {
   254  		if target.IsTest && target.Results.NumTests > 0 {
   255  			if target.Results.Failed > 0 {
   256  				printf("${RED}%s${RESET} %s\n", target.Label, testResultMessage(target.Results, failedTargets))
   257  			} else {
   258  				printf("${GREEN}%s${RESET} %s\n", target.Label, testResultMessage(target.Results, failedTargets))
   259  			}
   260  			if state.ShowTestOutput && target.Results.Output != "" {
   261  				printf("Test output:\n%s\n", target.Results.Output)
   262  			}
   263  			i++
   264  		}
   265  	}
   266  	aggregatedResults.Duration = -100 * time.Millisecond // Exclude this from being displayed later.
   267  	printf(fmt.Sprintf("${BOLD_WHITE}%s and %s${BOLD_WHITE}. Total time %s.${RESET}\n",
   268  		pluralise(i, "test target", "test targets"), testResultMessage(aggregatedResults, failedTargets), duration))
   269  }
   270  
   271  // logProgress continually logs progress messages every 10s explaining where we're up to.
   272  func logProgress(state *core.BuildState, buildingTargets *[]buildingTarget, stop <-chan struct{}, done chan<- struct{}) {
   273  	t := time.NewTicker(10 * time.Second)
   274  	defer t.Stop()
   275  	for {
   276  		select {
   277  		case <-t.C:
   278  			busy := 0
   279  			for i := 0; i < len(*buildingTargets); i++ {
   280  				if (*buildingTargets)[i].Active {
   281  					busy++
   282  				}
   283  			}
   284  			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"))
   285  		case <-stop:
   286  			done <- struct{}{}
   287  			return
   288  		}
   289  	}
   290  }
   291  
   292  // Produces a string describing the results of one test (or a single aggregation).
   293  func testResultMessage(results core.TestResults, failedTargets []core.BuildLabel) string {
   294  	if results.NumTests == 0 {
   295  		if len(failedTargets) > 0 {
   296  			return "Tests failed"
   297  		}
   298  		return "No tests found"
   299  	}
   300  	msg := fmt.Sprintf("%s run", pluralise(results.NumTests, "test", "tests"))
   301  	if results.Duration >= 0.0 {
   302  		msg += fmt.Sprintf(" in %s", results.Duration.Round(testDurationGranularity))
   303  	}
   304  	msg += fmt.Sprintf("; ${BOLD_GREEN}%d passed${RESET}", results.Passed)
   305  	if results.Failed > 0 {
   306  		msg += fmt.Sprintf(", ${BOLD_RED}%d failed${RESET}", results.Failed)
   307  	}
   308  	if results.Skipped > 0 {
   309  		msg += fmt.Sprintf(", ${BOLD_YELLOW}%d skipped${RESET}", results.Skipped)
   310  	}
   311  	if results.Flakes > 0 {
   312  		msg += fmt.Sprintf(", ${BOLD_MAGENTA}%s${RESET}", pluralise(results.Flakes, "flake", "flakes"))
   313  	}
   314  	if results.Cached {
   315  		msg += " ${GREEN}[cached]${RESET}"
   316  	}
   317  	return msg
   318  }
   319  
   320  func printBuildResults(state *core.BuildState, duration time.Duration, showStatus bool) {
   321  	// Count incrementality.
   322  	totalBuilt := 0
   323  	totalReused := 0
   324  	for _, target := range state.Graph.AllTargets() {
   325  		if target.State() == core.Built {
   326  			totalBuilt++
   327  		} else if target.State() == core.Reused {
   328  			totalReused++
   329  		}
   330  	}
   331  	incrementality := 100.0 * float64(totalReused) / float64(totalBuilt+totalReused)
   332  	if totalBuilt+totalReused == 0 {
   333  		incrementality = 100 // avoid NaN
   334  	}
   335  	// Print this stuff so we always see it.
   336  	printf("Build finished; total time %s, incrementality %.1f%%. Outputs:\n", duration, incrementality)
   337  	for _, label := range state.ExpandVisibleOriginalTargets() {
   338  		target := state.Graph.TargetOrDie(label)
   339  		if showStatus {
   340  			fmt.Printf("%s [%s]:\n", label, target.State())
   341  		} else {
   342  			fmt.Printf("%s:\n", label)
   343  		}
   344  		for _, result := range buildResult(target) {
   345  			fmt.Printf("  %s\n", result)
   346  		}
   347  	}
   348  }
   349  
   350  func printHashes(state *core.BuildState, duration time.Duration) {
   351  	fmt.Printf("Hashes calculated, total time %s:\n", duration)
   352  	for _, label := range state.ExpandVisibleOriginalTargets() {
   353  		hash, err := build.OutputHash(state.Graph.TargetOrDie(label))
   354  		if err != nil {
   355  			fmt.Printf("  %s: cannot calculate: %s\n", label, err)
   356  		} else {
   357  			fmt.Printf("  %s: %s\n", label, hex.EncodeToString(hash))
   358  		}
   359  	}
   360  }
   361  
   362  func printTempDirs(state *core.BuildState, duration time.Duration) {
   363  	fmt.Printf("Temp directories prepared, total time %s:\n", duration)
   364  	for _, label := range state.ExpandVisibleOriginalTargets() {
   365  		target := state.Graph.TargetOrDie(label)
   366  		cmd := build.ReplaceSequences(state, target, target.GetCommand(state))
   367  		env := core.BuildEnvironment(state, target, false)
   368  		fmt.Printf("  %s: %s\n", label, target.TmpDir())
   369  		fmt.Printf("    Command: %s\n", cmd)
   370  		if !state.PrepareShell {
   371  			// This isn't very useful if we're opening a shell (since then the vars will be set anyway)
   372  			fmt.Printf("   Expanded: %s\n", os.Expand(cmd, env.ReplaceEnvironment))
   373  		} else {
   374  			fmt.Printf("\n")
   375  			cmd := exec.Command("bash", "--noprofile", "--norc", "-o", "pipefail") // plz requires bash, some commands contain bashisms.
   376  			cmd.Dir = target.TmpDir()
   377  			cmd.Env = env
   378  			cmd.Stdin = os.Stdin
   379  			cmd.Stdout = os.Stdout
   380  			cmd.Stderr = os.Stderr
   381  			cmd.Run() // Ignore errors, it will typically end by the user killing it somehow.
   382  		}
   383  	}
   384  }
   385  
   386  func buildResult(target *core.BuildTarget) []string {
   387  	results := []string{}
   388  	if target != nil {
   389  		for _, out := range target.Outputs() {
   390  			if core.StartedAtRepoRoot() {
   391  				results = append(results, path.Join(target.OutDir(), out))
   392  			} else {
   393  				results = append(results, path.Join(core.RepoRoot, target.OutDir(), out))
   394  			}
   395  		}
   396  	}
   397  	return results
   398  }
   399  
   400  func printFailedBuildResults(failedTargets []core.BuildLabel, failedTargetMap map[core.BuildLabel]error, duration time.Duration) {
   401  	printf("${WHITE_ON_RED}Build stopped after %s. %s failed:${RESET}\n", duration, pluralise(len(failedTargetMap), "target", "targets"))
   402  	for _, label := range failedTargets {
   403  		err := failedTargetMap[label]
   404  		if err != nil {
   405  			printf("    ${BOLD_RED}%s\n${RESET}%s${RESET}\n", label, colouriseError(err))
   406  		} else {
   407  			printf("    ${BOLD_RED}%s${RESET}\n", label)
   408  		}
   409  	}
   410  }
   411  
   412  func updateTarget(state *core.BuildState, plainOutput bool, buildingTarget *buildingTarget, label core.BuildLabel,
   413  	active bool, failed bool, cached bool, description string, err error, colour string, target *core.BuildTarget) {
   414  	updateTarget2(buildingTarget, label, active, failed, cached, description, err, colour, target)
   415  	if plainOutput {
   416  		if failed {
   417  			log.Errorf("%s: %s: %s", label.String(), description, shortError(err))
   418  		} else {
   419  			if !active {
   420  				active := pluralise(state.NumActive(), "task", "tasks")
   421  				log.Info("[%d/%s] %s: %s [%3.1fs]", state.NumDone(), active, label.String(), description, time.Since(buildingTarget.Started).Seconds())
   422  			} else {
   423  				log.Info("%s: %s", label.String(), description)
   424  			}
   425  		}
   426  	}
   427  }
   428  
   429  func updateTarget2(target *buildingTarget, label core.BuildLabel, active bool, failed bool, cached bool, description string, err error, colour string, t *core.BuildTarget) {
   430  	target.Lock()
   431  	defer target.Unlock()
   432  	target.Label = label
   433  	target.Description = description
   434  	if !target.Active {
   435  		// Starting to build now.
   436  		target.Started = time.Now()
   437  		target.Finished = target.Started
   438  	} else if !active {
   439  		// finished building
   440  		target.Finished = time.Now()
   441  	}
   442  	target.Active = active
   443  	target.Failed = failed
   444  	target.Cached = cached
   445  	target.Err = err
   446  	target.Colour = colour
   447  	target.Target = t
   448  }
   449  
   450  func targetColour(target *core.BuildTarget) string {
   451  	if target == nil {
   452  		return "${BOLD_CYAN}" // unknown
   453  	} else if target.IsBinary {
   454  		return "${BOLD}" + targetColour2(target)
   455  	} else {
   456  		return targetColour2(target)
   457  	}
   458  }
   459  
   460  func targetColour2(target *core.BuildTarget) string {
   461  	// Quick heuristic on language types. May want to make this configurable.
   462  	for _, require := range target.Requires {
   463  		if require == "py" {
   464  			return "${GREEN}"
   465  		} else if require == "java" {
   466  			return "${RED}"
   467  		} else if require == "go" {
   468  			return "${YELLOW}"
   469  		} else if require == "js" {
   470  			return "${BLUE}"
   471  		}
   472  	}
   473  	if strings.Contains(target.Label.PackageName, "third_party") {
   474  		return "${MAGENTA}"
   475  	}
   476  	return "${WHITE}"
   477  }
   478  
   479  // Since this is a gentleman's build tool, we'll make an effort to get plurals correct
   480  // in at least this one place.
   481  func pluralise(num int, singular, plural string) string {
   482  	if num == 1 {
   483  		return fmt.Sprintf("1 %s", singular)
   484  	}
   485  	return fmt.Sprintf("%d %s", num, plural)
   486  }
   487  
   488  // PrintCoverage writes out coverage metrics after a test run in a file tree setup.
   489  // Only files that were covered by tests and not excluded are shown.
   490  func PrintCoverage(state *core.BuildState, includeFiles []string) {
   491  	printf("${BOLD_WHITE}Coverage results:${RESET}\n")
   492  	totalCovered := 0
   493  	totalTotal := 0
   494  	lastDir := "_"
   495  	for _, file := range state.Coverage.OrderedFiles() {
   496  		if !shouldInclude(file, includeFiles) {
   497  			continue
   498  		}
   499  		dir := filepath.Dir(file)
   500  		if dir != lastDir {
   501  			printf("${WHITE}%s:${RESET}\n", strings.TrimRight(dir, "/"))
   502  		}
   503  		lastDir = dir
   504  		covered, total := test.CountCoverage(state.Coverage.Files[file])
   505  		printf("  %s\n", coveragePercentage(covered, total, file[len(dir)+1:]))
   506  		totalCovered += covered
   507  		totalTotal += total
   508  	}
   509  	printf("${BOLD_WHITE}Total coverage: %s${RESET}\n", coveragePercentage(totalCovered, totalTotal, ""))
   510  }
   511  
   512  // PrintLineCoverageReport writes out line-by-line coverage metrics after a test run.
   513  func PrintLineCoverageReport(state *core.BuildState, includeFiles []string) {
   514  	coverageColours := map[core.LineCoverage]string{
   515  		core.NotExecutable: "${GREY}",
   516  		core.Unreachable:   "${YELLOW}",
   517  		core.Uncovered:     "${RED}",
   518  		core.Covered:       "${GREEN}",
   519  	}
   520  
   521  	printf("${BOLD_WHITE}Covered files:${RESET}\n")
   522  	for _, file := range state.Coverage.OrderedFiles() {
   523  		if !shouldInclude(file, includeFiles) {
   524  			continue
   525  		}
   526  		coverage := state.Coverage.Files[file]
   527  		covered, total := test.CountCoverage(coverage)
   528  		printf("${BOLD_WHITE}%s: %s${RESET}\n", file, coveragePercentage(covered, total, ""))
   529  		f, err := os.Open(file)
   530  		if err != nil {
   531  			printf("${BOLD_RED}Can't open: %s${RESET}\n", err)
   532  			continue
   533  		}
   534  		defer f.Close()
   535  		scanner := bufio.NewScanner(f)
   536  		i := 0
   537  		for scanner.Scan() {
   538  			if i < len(coverage) {
   539  				printf("${WHITE}%4d %s%s\n", i, coverageColours[coverage[i]], scanner.Text())
   540  			} else {
   541  				// Assume the lines are not executable. This happens for python, for example.
   542  				printf("${WHITE}%4d ${GREY}%s\n", i, scanner.Text())
   543  			}
   544  			i++
   545  		}
   546  		printf("${RESET}\n")
   547  	}
   548  }
   549  
   550  // shouldInclude returns true if we should include a file in the coverage display.
   551  func shouldInclude(file string, files []string) bool {
   552  	if len(files) == 0 {
   553  		return true
   554  	}
   555  	for _, f := range files {
   556  		if file == f {
   557  			return true
   558  		}
   559  	}
   560  	return false
   561  }
   562  
   563  // Returns some appropriate ANSI colour code for a coverage percentage.
   564  func coverageColour(percentage float32) string {
   565  	// TODO(pebers): consider making these configurable?
   566  	if percentage < 20.0 {
   567  		return "${MAGENTA}"
   568  	} else if percentage < 60.0 {
   569  		return "${BOLD_RED}"
   570  	} else if percentage < 80.0 {
   571  		return "${BOLD_YELLOW}"
   572  	}
   573  	return "${BOLD_GREEN}"
   574  }
   575  
   576  func coveragePercentage(covered, total int, label string) string {
   577  	if total == 0 {
   578  		return fmt.Sprintf("${BOLD_MAGENTA}%s No data${RESET}", label)
   579  	}
   580  	percentage := 100.0 * float32(covered) / float32(total)
   581  	return fmt.Sprintf("%s%s %d/%s, %2.1f%%${RESET}", coverageColour(percentage), label, covered, pluralise(total, "line", "lines"), percentage)
   582  }
   583  
   584  // colouriseError adds a splash of colour to a compiler error message.
   585  // This is a similar effect to -fcolor-diagnostics in Clang, but we attempt to apply it fairly generically.
   586  func colouriseError(err error) error {
   587  	msg := []string{}
   588  	for _, line := range strings.Split(err.Error(), "\n") {
   589  		if groups := errorMessageRe.FindStringSubmatch(line); groups != nil {
   590  			if groups[3] != "" {
   591  				groups[3] = ", column " + groups[3]
   592  			}
   593  			if groups[4] != "" {
   594  				groups[4] += ": "
   595  			}
   596  			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]))
   597  		} else {
   598  			msg = append(msg, line)
   599  		}
   600  	}
   601  	return fmt.Errorf("%s", strings.Join(msg, "\n"))
   602  }
   603  
   604  // errorMessageRe is a regex to find lines that look like they're specifying a file.
   605  var errorMessageRe = regexp.MustCompile(`^([^ ]+\.[^: /]+):([0-9]+):(?:([0-9]+):)? *(?:([a-z-_ ]+):)? (.*)$`)
   606  
   607  // graphCycleMessage attempts to detect graph cycles and produces a readable message from it.
   608  func graphCycleMessage(graph *core.BuildGraph, target *core.BuildTarget) string {
   609  	if cycle := findGraphCycle(graph, target); len(cycle) > 0 {
   610  		msg := "Dependency cycle found:\n"
   611  		msg += fmt.Sprintf("    %s\n", cycle[len(cycle)-1].Label)
   612  		for i := len(cycle) - 2; i >= 0; i-- {
   613  			msg += fmt.Sprintf(" -> %s\n", cycle[i].Label)
   614  		}
   615  		msg += fmt.Sprintf(" -> %s\n", cycle[len(cycle)-1].Label)
   616  		return msg + fmt.Sprintf("Sorry, but you'll have to refactor your build files to avoid this cycle.")
   617  	}
   618  	return unbuiltTargetsMessage(graph)
   619  }
   620  
   621  // Attempts to detect cycles in the build graph. Returns an empty slice if none is found,
   622  // otherwise returns a slice of labels describing the cycle.
   623  func findGraphCycle(graph *core.BuildGraph, target *core.BuildTarget) []*core.BuildTarget {
   624  	index := func(haystack []*core.BuildTarget, needle *core.BuildTarget) int {
   625  		for i, straw := range haystack {
   626  			if straw == needle {
   627  				return i
   628  			}
   629  		}
   630  		return -1
   631  	}
   632  
   633  	done := map[core.BuildLabel]bool{}
   634  	var detectCycle func(*core.BuildTarget, []*core.BuildTarget) []*core.BuildTarget
   635  	detectCycle = func(target *core.BuildTarget, deps []*core.BuildTarget) []*core.BuildTarget {
   636  		if i := index(deps, target); i != -1 {
   637  			return deps[i:]
   638  		} else if done[target.Label] {
   639  			return nil
   640  		}
   641  		done[target.Label] = true
   642  		deps = append(deps, target)
   643  		for _, dep := range target.Dependencies() {
   644  			if cycle := detectCycle(dep, deps); len(cycle) > 0 {
   645  				return cycle
   646  			}
   647  		}
   648  		return nil
   649  	}
   650  	return detectCycle(target, nil)
   651  }
   652  
   653  // unbuiltTargetsMessage returns a message for any targets that are supposed to build but haven't yet.
   654  func unbuiltTargetsMessage(graph *core.BuildGraph) string {
   655  	msg := ""
   656  	for _, target := range graph.AllTargets() {
   657  		if target.State() == core.Active {
   658  			if graph.AllDepsBuilt(target) {
   659  				msg += fmt.Sprintf("  %s (waiting for deps to build)\n", target.Label)
   660  			} else {
   661  				msg += fmt.Sprintf("  %s\n", target.Label)
   662  			}
   663  		} else if target.State() == core.Pending {
   664  			msg += fmt.Sprintf("  %s (pending build)\n", target.Label)
   665  		}
   666  	}
   667  	if msg != "" {
   668  		return "\nThe following targets have not yet built:\n" + msg
   669  	}
   670  	return ""
   671  }
   672  
   673  // shortError returns the message for an error, shortening it if the error supports that.
   674  func shortError(err error) string {
   675  	if se, ok := err.(shortenableError); ok {
   676  		return se.ShortError()
   677  	}
   678  	return err.Error()
   679  }
   680  
   681  // A shortenableError describes any error type that can communicate a short-form error.
   682  type shortenableError interface {
   683  	ShortError() string
   684  }