| // Copyright 2023 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // Bisect finds changes responsible for causing a failure. | |
| // A typical use is to identify the source locations in a program | |
| // that are miscompiled by a given compiler optimization. | |
| // | |
| // Usage: | |
| // | |
| // bisect [flags] [var=value...] command [arguments...] | |
| // | |
| // Bisect operates on a target command line – the target – that can be | |
| // run with various changes individually enabled or disabled. With none | |
| // of the changes enabled, the target is known to succeed (exit with exit | |
| // code zero). With all the changes enabled, the target is known to fail | |
| // (exit any other way). Bisect repeats the target with different sets of | |
| // changes enabled, using binary search to find (non-overlapping) minimal | |
| // change sets that provoke the failure. | |
| // | |
| // The target must cooperate with bisect by accepting a change pattern | |
| // and then enabling and reporting the changes that match that pattern. | |
| // The change pattern is passed to the target by substituting it anywhere | |
| // the string PATTERN appears in the environment values or the command | |
| // arguments. For each change that matches the pattern, the target must | |
| // enable that change and also print one or more “match lines” | |
| // (to standard output or standard error) describing the change. | |
| // The [golang.org/x/tools/internal/bisect] package provides functions to help | |
| // targets implement this protocol. We plan to publish that package | |
| // in a non-internal location after finalizing its API. | |
| // | |
| // Bisect starts by running the target with no changes enabled and then | |
| // with all changes enabled. It expects the former to succeed and the latter to fail, | |
| // and then it will search for the minimal set of changes that must be enabled | |
| // to provoke the failure. If the situation is reversed – the target fails with no | |
| // changes enabled and succeeds with all changes enabled – then bisect | |
| // automatically runs in reverse as well, searching for the minimal set of changes | |
| // that must be disabled to provoke the failure. | |
| // | |
| // Bisect prints tracing logs to standard error and the minimal change sets | |
| // to standard output. | |
| // | |
| // # Command Line Flags | |
| // | |
| // Bisect supports the following command-line flags: | |
| // | |
| // -max=M | |
| // | |
| // Stop after finding M minimal change sets. The default is no maximum, meaning to run until | |
| // all changes that provoke a failure have been identified. | |
| // | |
| // -maxset=S | |
| // | |
| // Disallow change sets larger than S elements. The default is no maximum. | |
| // | |
| // -timeout=D | |
| // | |
| // If the target runs for longer than duration D, stop the target and interpret that as a failure. | |
| // The default is no timeout. | |
| // | |
| // -count=N | |
| // | |
| // Run each trial N times (default 2), checking for consistency. | |
| // | |
| // -v | |
| // | |
| // Print verbose output, showing each run and its match lines. | |
| // | |
| // In addition to these general flags, | |
| // bisect supports a few “shortcut” flags that make it more convenient | |
| // to use with specific targets. | |
| // | |
| // -compile=<rewrite> | |
| // | |
| // This flag is equivalent to adding an environment variable | |
| // “GOCOMPILEDEBUG=<rewrite>hash=PATTERN”, | |
| // which, as discussed in more detail in the example below, | |
| // allows bisect to identify the specific source locations where the | |
| // compiler rewrite causes the target to fail. | |
| // | |
| // -godebug=<name>=<value> | |
| // | |
| // This flag is equivalent to adding an environment variable | |
| // “GODEBUG=<name>=<value>#PATTERN”, | |
| // which allows bisect to identify the specific call stacks where | |
| // the changed [GODEBUG setting] value causes the target to fail. | |
| // | |
| // # Example | |
| // | |
| // The Go compiler provides support for enabling or disabling certain rewrites | |
| // and optimizations to allow bisect to identify specific source locations where | |
| // the rewrite causes the program to fail. For example, to bisect a failure caused | |
| // by the new loop variable semantics: | |
| // | |
| // bisect go test -gcflags=all=-d=loopvarhash=PATTERN | |
| // | |
| // The -gcflags=all= instructs the go command to pass the -d=... to the Go compiler | |
| // when compiling all packages. Bisect varies PATTERN to determine the minimal set of changes | |
| // needed to reproduce the failure. | |
| // | |
| // The go command also checks the GOCOMPILEDEBUG environment variable for flags | |
| // to pass to the compiler, so the above command is equivalent to: | |
| // | |
| // bisect GOCOMPILEDEBUG=loopvarhash=PATTERN go test | |
| // | |
| // Finally, as mentioned earlier, the -compile flag allows shortening this command further: | |
| // | |
| // bisect -compile=loopvar go test | |
| // | |
| // # Defeating Build Caches | |
| // | |
| // Build systems cache build results, to avoid repeating the same compilations | |
| // over and over. When using a cached build result, the go command (correctly) | |
| // reprints the cached standard output and standard error associated with that | |
| // command invocation. (This makes commands like 'go build -gcflags=-S' for | |
| // printing an assembly listing work reliably.) | |
| // | |
| // Unfortunately, most build systems, including Bazel, are not as careful | |
| // as the go command about reprinting compiler output. If the compiler is | |
| // what prints match lines, a build system that suppresses compiler | |
| // output when using cached compiler results will confuse bisect. | |
| // To defeat such build caches, bisect replaces the literal text “RANDOM” | |
| // in environment values and command arguments with a random 64-bit value | |
| // during each invocation. The Go compiler conveniently accepts a | |
| // -d=ignore=... debug flag that ignores its argument, so to run the | |
| // previous example using Bazel, the invocation is: | |
| // | |
| // bazel test --define=gc_goopts=-d=loopvarhash=PATTERN,unused=RANDOM //path/to:test | |
| // | |
| // [GODEBUG setting]: https://tip.golang.org/doc/godebug | |
| package main | |
| import ( | |
| "context" | |
| "flag" | |
| "fmt" | |
| "io" | |
| "log" | |
| "math/bits" | |
| "math/rand" | |
| "os" | |
| "os/exec" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "time" | |
| "golang.org/x/tools/internal/bisect" | |
| ) | |
| // Preserve import of bisect, to allow [bisect.Match] in the doc comment. | |
| var _ bisect.Matcher | |
| func usage() { | |
| fmt.Fprintf(os.Stderr, "usage: bisect [flags] [var=value...] command [arguments...]\n") | |
| flag.PrintDefaults() | |
| os.Exit(2) | |
| } | |
| func main() { | |
| log.SetFlags(0) | |
| log.SetPrefix("bisect: ") | |
| var b Bisect | |
| b.Stdout = os.Stdout | |
| b.Stderr = os.Stderr | |
| flag.IntVar(&b.Max, "max", 0, "stop after finding `m` failing change sets") | |
| flag.IntVar(&b.MaxSet, "maxset", 0, "do not search for change sets larger than `s` elements") | |
| flag.DurationVar(&b.Timeout, "timeout", 0, "stop target and consider failed after duration `d`") | |
| flag.IntVar(&b.Count, "count", 2, "run target `n` times for each trial") | |
| flag.BoolVar(&b.Verbose, "v", false, "enable verbose output") | |
| env := "" | |
| envFlag := "" | |
| flag.Func("compile", "bisect source locations affected by Go compiler `rewrite` (fma, loopvar, ...)", func(value string) error { | |
| if envFlag != "" { | |
| return fmt.Errorf("cannot use -%s and -compile", envFlag) | |
| } | |
| envFlag = "compile" | |
| env = "GOCOMPILEDEBUG=" + value + "hash=PATTERN" | |
| return nil | |
| }) | |
| flag.Func("godebug", "bisect call stacks affected by GODEBUG setting `name=value`", func(value string) error { | |
| if envFlag != "" { | |
| return fmt.Errorf("cannot use -%s and -godebug", envFlag) | |
| } | |
| envFlag = "godebug" | |
| env = "GODEBUG=" + value + "#PATTERN" | |
| return nil | |
| }) | |
| flag.Usage = usage | |
| flag.Parse() | |
| args := flag.Args() | |
| // Split command line into env settings, command name, args. | |
| i := 0 | |
| for i < len(args) && strings.Contains(args[i], "=") { | |
| i++ | |
| } | |
| if i == len(args) { | |
| usage() | |
| } | |
| b.Env, b.Cmd, b.Args = args[:i], args[i], args[i+1:] | |
| if env != "" { | |
| b.Env = append([]string{env}, b.Env...) | |
| } | |
| // Check that PATTERN is available for us to vary. | |
| found := false | |
| for _, e := range b.Env { | |
| if _, v, _ := strings.Cut(e, "="); strings.Contains(v, "PATTERN") { | |
| found = true | |
| } | |
| } | |
| for _, a := range b.Args { | |
| if strings.Contains(a, "PATTERN") { | |
| found = true | |
| } | |
| } | |
| if !found { | |
| log.Fatalf("no PATTERN in target environment or args") | |
| } | |
| if !b.Search() { | |
| os.Exit(1) | |
| } | |
| } | |
| // A Bisect holds the state for a bisect invocation. | |
| type Bisect struct { | |
| // Env is the additional environment variables for the command. | |
| // PATTERN and RANDOM are substituted in the values, but not the names. | |
| Env []string | |
| // Cmd is the command (program name) to run. | |
| // PATTERN and RANDOM are not substituted. | |
| Cmd string | |
| // Args is the command arguments. | |
| // PATTERN and RANDOM are substituted anywhere they appear. | |
| Args []string | |
| // Command-line flags controlling bisect behavior. | |
| Max int // maximum number of sets to report (0 = unlimited) | |
| MaxSet int // maximum number of elements in a set (0 = unlimited) | |
| Timeout time.Duration // kill target and assume failed after this duration (0 = unlimited) | |
| Count int // run target this many times for each trial and give up if flaky (min 1 assumed; default 2 on command line set in main) | |
| Verbose bool // print long output about each trial (only useful for debugging bisect itself) | |
| // State for running bisect, replaced during testing. | |
| // Failing change sets are printed to Stdout; all other output goes to Stderr. | |
| Stdout io.Writer // where to write standard output (usually os.Stdout) | |
| Stderr io.Writer // where to write standard error (usually os.Stderr) | |
| TestRun func(env []string, cmd string, args []string) (out []byte, err error) // if non-nil, used instead of exec.Command | |
| // State maintained by Search. | |
| // By default, Search looks for a minimal set of changes that cause a failure when enabled. | |
| // If Disable is true, the search is inverted and seeks a minimal set of changes that | |
| // cause a failure when disabled. In this case, the search proceeds as normal except that | |
| // each pattern starts with a !. | |
| Disable bool | |
| // SkipDigits is the number of hex digits to use in skip messages. | |
| // If the set of available changes is the same in each run, as it should be, | |
| // then this doesn't matter: we'll only exclude suffixes that uniquely identify | |
| // a given change. But for some programs, especially bisecting runtime | |
| // behaviors, sometimes enabling one change unlocks questions about other | |
| // changes. Strictly speaking this is a misuse of bisect, but just to make | |
| // bisect more robust, we use the y and n runs to create an estimate of the | |
| // number of bits needed for a unique suffix, and then we round it up to | |
| // a number of hex digits, with one extra digit for good measure, and then | |
| // we always use that many hex digits for skips. | |
| SkipHexDigits int | |
| // Add is a list of suffixes to add to every trial, because they | |
| // contain changes that are necessary for a group we are assembling. | |
| Add []string | |
| // Skip is a list of suffixes that uniquely identify changes to exclude from every trial, | |
| // because they have already been used in failing change sets. | |
| // Suffixes later in the list may only be unique after removing | |
| // the ones earlier in the list. | |
| // Skip applies after Add. | |
| Skip []string | |
| } | |
| // A Result holds the result of a single target trial. | |
| type Result struct { | |
| Success bool // whether the target succeeded (exited with zero status) | |
| Cmd string // full target command line | |
| Out string // full target output (stdout and stderr combined) | |
| Suffix string // the suffix used for collecting MatchIDs, MatchText, and MatchFull | |
| MatchIDs []uint64 // match IDs enabled during this trial | |
| MatchText []string // match reports for the IDs, with match markers removed | |
| MatchFull []string // full match lines for the IDs, with match markers kept | |
| } | |
| // &searchFatal is a special panic value to signal that Search failed. | |
| // This lets us unwind the search recursion on a fatal error | |
| // but have Search return normally. | |
| var searchFatal int | |
| // Search runs a bisect search according to the configuration in b. | |
| // It reports whether any failing change sets were found. | |
| func (b *Bisect) Search() bool { | |
| defer func() { | |
| // Recover from panic(&searchFatal), implicitly returning false from Search. | |
| // Re-panic on any other panic. | |
| if e := recover(); e != nil && e != &searchFatal { | |
| panic(e) | |
| } | |
| }() | |
| // Run with no changes and all changes, to figure out which direction we're searching. | |
| // The goal is to find the minimal set of changes to toggle | |
| // starting with the state where everything works. | |
| // If "no changes" succeeds and "all changes" fails, | |
| // we're looking for a minimal set of changes to enable to provoke the failure | |
| // (broken = runY, b.Negate = false) | |
| // If "no changes" fails and "all changes" succeeds, | |
| // we're looking for a minimal set of changes to disable to provoke the failure | |
| // (broken = runN, b.Negate = true). | |
| b.Logf("checking target with all changes disabled") | |
| runN := b.Run("n") | |
| b.Logf("checking target with all changes enabled") | |
| runY := b.Run("y") | |
| var broken *Result | |
| switch { | |
| case runN.Success && !runY.Success: | |
| b.Logf("target succeeds with no changes, fails with all changes") | |
| b.Logf("searching for minimal set of enabled changes causing failure") | |
| broken = runY | |
| b.Disable = false | |
| case !runN.Success && runY.Success: | |
| b.Logf("target fails with no changes, succeeds with all changes") | |
| b.Logf("searching for minimal set of disabled changes causing failure") | |
| broken = runN | |
| b.Disable = true | |
| case runN.Success && runY.Success: | |
| b.Fatalf("target succeeds with no changes and all changes") | |
| case !runN.Success && !runY.Success: | |
| b.Fatalf("target fails with no changes and all changes") | |
| } | |
| // Compute minimum number of bits needed to distinguish | |
| // all the changes we saw during N and all the changes we saw during Y. | |
| b.SkipHexDigits = skipHexDigits(runN.MatchIDs, runY.MatchIDs) | |
| // Loop finding and printing change sets, until none remain. | |
| found := 0 | |
| for { | |
| // Find set. | |
| bad := b.search(broken) | |
| if bad == nil { | |
| if found == 0 { | |
| b.Fatalf("cannot find any failing change sets of size ≤ %d", b.MaxSet) | |
| } | |
| break | |
| } | |
| // Confirm that set really does fail, to avoid false accusations. | |
| // Also asking for user-visible output; earlier runs did not. | |
| b.Logf("confirming failing change set") | |
| b.Add = append(b.Add[:0], bad...) | |
| broken = b.Run("v") | |
| if broken.Success { | |
| b.Logf("confirmation run succeeded unexpectedly") | |
| } | |
| b.Add = b.Add[:0] | |
| // Print confirmed change set. | |
| found++ | |
| b.Logf("FOUND failing change set") | |
| desc := "(enabling changes causes failure)" | |
| if b.Disable { | |
| desc = "(disabling changes causes failure)" | |
| } | |
| fmt.Fprintf(b.Stdout, "--- change set #%d %s\n%s\n---\n", found, desc, strings.Join(broken.MatchText, "\n")) | |
| // Stop if we've found enough change sets. | |
| if b.Max > 0 && found >= b.Max { | |
| break | |
| } | |
| // If running bisect target | tee bad.txt, prints to stdout and stderr | |
| // both appear on the terminal, but the ones to stdout go through tee | |
| // and can take a little bit of extra time. Sleep 1 millisecond to give | |
| // tee time to catch up, so that its stdout print does not get interlaced | |
| // with the stderr print from the next b.Log message. | |
| time.Sleep(1 * time.Millisecond) | |
| // Disable the now-known-bad changes and see if any failures remain. | |
| b.Logf("checking for more failures") | |
| b.Skip = append(bad, b.Skip...) | |
| broken = b.Run("") | |
| if broken.Success { | |
| what := "enabled" | |
| if b.Disable { | |
| what = "disabled" | |
| } | |
| b.Logf("target succeeds with all remaining changes %s", what) | |
| break | |
| } | |
| b.Logf("target still fails; searching for more bad changes") | |
| } | |
| return true | |
| } | |
| // Fatalf prints a message to standard error and then panics, | |
| // causing Search to return false. | |
| func (b *Bisect) Fatalf(format string, args ...any) { | |
| s := fmt.Sprintf("bisect: fatal error: "+format, args...) | |
| if !strings.HasSuffix(s, "\n") { | |
| s += "\n" | |
| } | |
| b.Stderr.Write([]byte(s)) | |
| panic(&searchFatal) | |
| } | |
| // Logf prints a message to standard error. | |
| func (b *Bisect) Logf(format string, args ...any) { | |
| s := fmt.Sprintf("bisect: "+format, args...) | |
| if !strings.HasSuffix(s, "\n") { | |
| s += "\n" | |
| } | |
| b.Stderr.Write([]byte(s)) | |
| } | |
| func skipHexDigits(idY, idN []uint64) int { | |
| var all []uint64 | |
| seen := make(map[uint64]bool) | |
| for _, x := range idY { | |
| seen[x] = true | |
| all = append(all, x) | |
| } | |
| for _, x := range idN { | |
| if !seen[x] { | |
| seen[x] = true | |
| all = append(all, x) | |
| } | |
| } | |
| sort.Slice(all, func(i, j int) bool { return bits.Reverse64(all[i]) < bits.Reverse64(all[j]) }) | |
| digits := sort.Search(64/4, func(digits int) bool { | |
| mask := uint64(1)<<(4*digits) - 1 | |
| for i := 0; i+1 < len(all); i++ { | |
| if all[i]&mask == all[i+1]&mask { | |
| return false | |
| } | |
| } | |
| return true | |
| }) | |
| if digits < 64/4 { | |
| digits++ | |
| } | |
| return digits | |
| } | |
| // search searches for a single locally minimal change set. | |
| // | |
| // Invariant: r describes the result of r.Suffix + b.Add, which failed. | |
| // (There's an implicit -b.Skip everywhere here. b.Skip does not change.) | |
| // We want to extend r.Suffix to preserve the failure, working toward | |
| // a suffix that identifies a single change. | |
| func (b *Bisect) search(r *Result) []string { | |
| // The caller should be passing in a failure result that we diagnose. | |
| if r.Success { | |
| b.Fatalf("internal error: unexpected success") // mistake by caller | |
| } | |
| // If the failure reported no changes, the target is misbehaving. | |
| if len(r.MatchIDs) == 0 { | |
| b.Fatalf("failure with no reported changes:\n\n$ %s\n%s\n", r.Cmd, r.Out) | |
| } | |
| // If there's one matching change, that's the one we're looking for. | |
| if len(r.MatchIDs) == 1 { | |
| return []string{fmt.Sprintf("x%0*x", b.SkipHexDigits, r.MatchIDs[0]&(1<<(4*b.SkipHexDigits)-1))} | |
| } | |
| // If the suffix we were tracking in the trial is already 64 bits, | |
| // either the target is bad or bisect itself is buggy. | |
| if len(r.Suffix) >= 64 { | |
| b.Fatalf("failed to isolate a single change with very long suffix") | |
| } | |
| // We want to split the current matchIDs by left-extending the suffix with 0 and 1. | |
| // If all the matches have the same next bit, that won't cause a split, which doesn't | |
| // break the algorithm but does waste time. Avoid wasting time by left-extending | |
| // the suffix to the longest suffix shared by all the current match IDs | |
| // before adding 0 or 1. | |
| suffix := commonSuffix(r.MatchIDs) | |
| if !strings.HasSuffix(suffix, r.Suffix) { | |
| b.Fatalf("internal error: invalid common suffix") // bug in commonSuffix | |
| } | |
| // Run 0suffix and 1suffix. If one fails, chase down the failure in that half. | |
| r0 := b.Run("0" + suffix) | |
| if !r0.Success { | |
| return b.search(r0) | |
| } | |
| r1 := b.Run("1" + suffix) | |
| if !r1.Success { | |
| return b.search(r1) | |
| } | |
| // suffix failed, but 0suffix and 1suffix succeeded. | |
| // Assuming the target isn't flaky, this means we need | |
| // at least one change from 0suffix AND at least one from 1suffix. | |
| // We are already tracking N = len(b.Add) other changes and are | |
| // allowed to build sets of size at least 1+N (or we shouldn't be here at all). | |
| // If we aren't allowed to build sets of size 2+N, give up this branch. | |
| if b.MaxSet > 0 && 2+len(b.Add) > b.MaxSet { | |
| return nil | |
| } | |
| // Adding all matches for 1suffix, recurse to narrow down 0suffix. | |
| old := len(b.Add) | |
| b.Add = append(b.Add, "1"+suffix) | |
| r0 = b.Run("0" + suffix) | |
| if r0.Success { | |
| // 0suffix + b.Add + 1suffix = suffix + b.Add is what r describes, and it failed. | |
| b.Fatalf("target fails inconsistently") | |
| } | |
| bad0 := b.search(r0) | |
| if bad0 == nil { | |
| // Search failed due to MaxSet limit. | |
| return nil | |
| } | |
| b.Add = b.Add[:old] | |
| // Adding the specific match we found in 0suffix, recurse to narrow down 1suffix. | |
| b.Add = append(b.Add[:old], bad0...) | |
| r1 = b.Run("1" + suffix) | |
| if r1.Success { | |
| // 1suffix + b.Add + bad0 = bad0 + b.Add + 1suffix is what b.search(r0) reported as a failure. | |
| b.Fatalf("target fails inconsistently") | |
| } | |
| bad1 := b.search(r1) | |
| if bad1 == nil { | |
| // Search failed due to MaxSet limit. | |
| return nil | |
| } | |
| b.Add = b.Add[:old] | |
| // bad0 and bad1 together provoke the failure. | |
| return append(bad0, bad1...) | |
| } | |
| // Run runs a set of trials selecting changes with the given suffix, | |
| // plus the ones in b.Add and not the ones in b.Skip. | |
| // The returned result's MatchIDs, MatchText, and MatchFull | |
| // only list the changes that match suffix. | |
| // When b.Count > 1, Run runs b.Count trials and requires | |
| // that they all succeed or they all fail. If not, it calls b.Fatalf. | |
| func (b *Bisect) Run(suffix string) *Result { | |
| out := b.run(suffix) | |
| for i := 1; i < b.Count; i++ { | |
| r := b.run(suffix) | |
| if r.Success != out.Success { | |
| b.Fatalf("target fails inconsistently") | |
| } | |
| } | |
| return out | |
| } | |
| // run runs a single trial for Run. | |
| func (b *Bisect) run(suffix string) *Result { | |
| random := fmt.Sprint(rand.Uint64()) | |
| // Accept suffix == "v" to mean we need user-visible output. | |
| visible := "" | |
| if suffix == "v" { | |
| visible = "v" | |
| suffix = "" | |
| } | |
| // Construct change ID pattern. | |
| var pattern string | |
| if suffix == "y" || suffix == "n" { | |
| pattern = suffix | |
| suffix = "" | |
| } else { | |
| var elem []string | |
| if suffix != "" { | |
| elem = append(elem, "+", suffix) | |
| } | |
| for _, x := range b.Add { | |
| elem = append(elem, "+", x) | |
| } | |
| for _, x := range b.Skip { | |
| elem = append(elem, "-", x) | |
| } | |
| pattern = strings.Join(elem, "") | |
| if pattern == "" { | |
| pattern = "y" | |
| } | |
| } | |
| if b.Disable { | |
| pattern = "!" + pattern | |
| } | |
| pattern = visible + pattern | |
| // Construct substituted env and args. | |
| env := make([]string, len(b.Env)) | |
| for i, x := range b.Env { | |
| k, v, _ := strings.Cut(x, "=") | |
| env[i] = k + "=" + replace(v, pattern, random) | |
| } | |
| args := make([]string, len(b.Args)) | |
| for i, x := range b.Args { | |
| args[i] = replace(x, pattern, random) | |
| } | |
| // Construct and log command line. | |
| // There is no newline in the log print. | |
| // The line will be completed when the command finishes. | |
| cmdText := strings.Join(append(append(env, b.Cmd), args...), " ") | |
| fmt.Fprintf(b.Stderr, "bisect: run: %s...", cmdText) | |
| // Run command with args and env. | |
| var out []byte | |
| var err error | |
| if b.TestRun != nil { | |
| out, err = b.TestRun(env, b.Cmd, args) | |
| } else { | |
| ctx := context.Background() | |
| if b.Timeout != 0 { | |
| var cancel context.CancelFunc | |
| ctx, cancel = context.WithTimeout(ctx, b.Timeout) | |
| defer cancel() | |
| } | |
| cmd := exec.CommandContext(ctx, b.Cmd, args...) | |
| cmd.Env = append(os.Environ(), env...) | |
| // Set up cmd.Cancel, cmd.WaitDelay on Go 1.20 and later | |
| // TODO(rsc): Inline go120.go's cmdInterrupt once we stop supporting Go 1.19. | |
| cmdInterrupt(cmd) | |
| out, err = cmd.CombinedOutput() | |
| } | |
| // Parse output to construct result. | |
| r := &Result{ | |
| Suffix: suffix, | |
| Success: err == nil, | |
| Cmd: cmdText, | |
| Out: string(out), | |
| } | |
| // Calculate bits, mask to identify suffix matches. | |
| var bits, mask uint64 | |
| if suffix != "" && suffix != "y" && suffix != "n" && suffix != "v" { | |
| var err error | |
| bits, err = strconv.ParseUint(suffix, 2, 64) | |
| if err != nil { | |
| b.Fatalf("internal error: bad suffix") | |
| } | |
| mask = uint64(1<<len(suffix)) - 1 | |
| } | |
| // Process output, collecting match reports for suffix. | |
| have := make(map[uint64]bool) | |
| all := r.Out | |
| for all != "" { | |
| var line string | |
| line, all, _ = strings.Cut(all, "\n") | |
| short, id, ok := bisect.CutMarker(line) | |
| if !ok || (id&mask) != bits { | |
| continue | |
| } | |
| if !have[id] { | |
| have[id] = true | |
| r.MatchIDs = append(r.MatchIDs, id) | |
| } | |
| r.MatchText = append(r.MatchText, short) | |
| r.MatchFull = append(r.MatchFull, line) | |
| } | |
| // Finish log print from above, describing the command's completion. | |
| if err == nil { | |
| fmt.Fprintf(b.Stderr, " ok (%d matches)\n", len(r.MatchIDs)) | |
| } else { | |
| fmt.Fprintf(b.Stderr, " FAIL (%d matches)\n", len(r.MatchIDs)) | |
| } | |
| if err != nil && len(r.MatchIDs) == 0 { | |
| b.Fatalf("target failed without printing any matches\n%s", r.Out) | |
| } | |
| // In verbose mode, print extra debugging: all the lines with match markers. | |
| if b.Verbose { | |
| b.Logf("matches:\n%s", strings.Join(r.MatchFull, "\n\t")) | |
| } | |
| return r | |
| } | |
| // replace returns x with literal text PATTERN and RANDOM replaced by pattern and random. | |
| func replace(x, pattern, random string) string { | |
| x = strings.ReplaceAll(x, "PATTERN", pattern) | |
| x = strings.ReplaceAll(x, "RANDOM", random) | |
| return x | |
| } | |
| // commonSuffix returns the longest common binary suffix shared by all uint64s in list. | |
| // If list is empty, commonSuffix returns an empty string. | |
| func commonSuffix(list []uint64) string { | |
| if len(list) == 0 { | |
| return "" | |
| } | |
| b := list[0] | |
| n := 64 | |
| for _, x := range list { | |
| for x&((1<<n)-1) != b { | |
| n-- | |
| b &= (1 << n) - 1 | |
| } | |
| } | |
| s := make([]byte, n) | |
| for i := n - 1; i >= 0; i-- { | |
| s[i] = '0' + byte(b&1) | |
| b >>= 1 | |
| } | |
| return string(s[:]) | |
| } |