← Back to index

lib/BDD/Behave/Bisect.rakumod

75 / 91 lines covered (82.4%)

1unit module BDD::Behave::Bisect;23use BDD::Behave::Colors;45our class BisectResult {6  has Str @.initial-failing;7  has %.minimal-deps;8  has Int $.iterations = 0;9  has Bool $.had-failures = False;10  has Str $.message;11}1213our class Bisector {14  has @.spec-files;15  has Str $.behave-bin = 'bin/behave';16  has @.extra-args;17  has Bool $.verbose = False;18  has Bool $.quiet = False;1920  has Int $!iterations = 0;2122  method iterations(--> Int) { $!iterations }2324  method run(--> BisectResult) {25    self!log: "==> Bisect: initial pass";26    my %initial = self!subprocess-run(:no-only);27    my @executed = %initial<executed>.list.cache;28    my @failed = %initial<failed>.list.cache;2930    unless @failed.elems {31      self!log: "Bisect: no failing examples in the initial run. Nothing to do.";32      return BisectResult.new(33        :initial-failing(()),34        :had-failures(False),35        :iterations($!iterations),36        :message('no failures'),37      );38    }3940    self!log: "Bisect: {@failed.elems} failing example(s) found across {@executed.elems} executed";41    for @failed -> $f { self!log: "  " ~ red("✗ ") ~ $f }4243    my %minimal-deps;44    for @failed -> $failing {45      self!log: "";46      self!log: "==> Bisecting $failing";4748      my $idx = @executed.first(* eq $failing, :k);49      unless $idx.defined {50        self!log: "  WARN: $failing not present in execution order, skipping";51        %minimal-deps{$failing} = [];52        next;53      }5455      my @prior = @executed[0 ..^ $idx].grep(* ne $failing).List;5657      if @prior.elems == 0 {58        self!log: "  No prior examples — failure is independent of order";59        %minimal-deps{$failing} = [];60        next;61      }6263      my %isolation = self!subprocess-run(:only-locations([$failing]));64      if %isolation<failed>.first(* eq $failing).defined {65        self!log: "  Failure reproduces in isolation — not order-dependent";66        %minimal-deps{$failing} = [];67        next;68      }6970      my %full = self!subprocess-run(:only-locations([|@prior, $failing]));71      unless %full<failed>.first(* eq $failing).defined {72        self!log: "  Failure does not reproduce under --only-example replay; cannot bisect";73        %minimal-deps{$failing} = @prior;74        next;75      }7677      my @minimal = self!minimize($failing, @prior);78      %minimal-deps{$failing} = @minimal;7980      self!log: "";81      self!log: "  Minimal reproduction ({@minimal.elems} prior + 1 failing):";82      for @minimal -> $m { self!log: "    " ~ light-blue($m) }83      self!log: "    " ~ red($failing) ~ "  (failing)";84      self!log: "";85      self!log: "  Reproduce with:";86      my $cmd = "    bin/behave"87        ~ @minimal.map({ " --only-example $_" }).join88        ~ " --only-example $failing"89        ~ " --order defined"90        ~ @!spec-files.map({ " $_" }).join;91      self!log: $cmd;92    }9394    self!log: "";95    self!log: "Bisect complete: {$!iterations} iteration(s)";9697    BisectResult.new(98      :initial-failing(@failed),99      :%minimal-deps,100      :had-failures(True),101      :iterations($!iterations),102    );103  }104105  method !minimize(Str $failing, @prior --> List) {106    my @current = @prior.List;107108    loop {109      last if @current.elems <= 1;110      my $mid = @current.elems div 2;111      my @left = @current[0 ..^ $mid].List;112      my @right = @current[$mid .. *].List;113114      my %right-result = self!subprocess-run(115        :only-locations([|@right, $failing]),116      );117      if %right-result<failed>.first(* eq $failing).defined {118        @current = @right;119        self!log: "  shrunk to {@current.elems} prior";120        next;121      }122123      my %left-result = self!subprocess-run(124        :only-locations([|@left, $failing]),125      );126      if %left-result<failed>.first(* eq $failing).defined {127        @current = @left;128        self!log: "  shrunk to {@current.elems} prior";129        next;130      }131132      last;133    }134135    my Bool $changed = True;136    while $changed && @current.elems > 1 {137      $changed = False;138      for ^@current.elems -> $i {139        my @candidate;140        @candidate.append: @current[0 ..^ $i];141        @candidate.append: @current[$i + 1 .. *];142        my %result = self!subprocess-run(143          :only-locations([|@candidate, $failing]),144        );145        if %result<failed>.first(* eq $failing).defined {146          @current = @candidate;147          $changed = True;148          self!log: "  pruned one; {@current.elems} prior remain";149          last;150        }151      }152    }153154    @current.List;155  }156157  method !subprocess-run(:@only-locations = [], :$no-only = False --> Hash) {158    $!iterations++;159160    my @args = '--bisect-data', '--order', 'defined';161    unless $no-only {162      for @only-locations -> $loc {163        @args.append: '--only-example', $loc;164      }165    }166    @args.append: @!extra-args;167    @args.append: @!spec-files;168169    if $!verbose {170      note "  [iter $!iterations] raku -Ilib $!behave-bin {@args.join(' ')}";171    }172173    my $proc = run('raku', '-Ilib', $!behave-bin, |@args, :out, :err);174    my $out = $proc.out.slurp(:close);175    my $err = $proc.err.slurp(:close);176177    my @executed;178    my @failed;179    for $out.lines -> $line {180      if $line.starts-with('behave-executed: ') {181        @executed.push: $line.substr('behave-executed: '.chars);182      } elsif $line.starts-with('behave-failed: ') {183        @failed.push: $line.substr('behave-failed: '.chars);184      }185    }186187    %( :@executed, :@failed, :exit($proc.exitcode), :$out, :$err );188  }189190  method !log(Str $msg) {191    say $msg unless $!quiet;192  }193}