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 }