← Back to index

lib/BDD/Behave/DryRun.rakumod

128 / 131 lines covered (97.7%)

1unit module BDD::Behave::DryRun;23use BDD::Behave::Benchmark::Format;4use BDD::Behave::SpecTree;56constant Suite = BDD::Behave::SpecTree::Suite;7constant ExampleGroup = BDD::Behave::SpecTree::ExampleGroup;8constant Example = BDD::Behave::SpecTree::Example;910sub example-matches(11  Example $ex,12  @include-tags, @exclude-tags, @example-patterns, @only-locations,13  Bool :$focus-mode,14  --> Bool15) {16  my @tags = $ex.effective-tags;1718  if @exclude-tags.elems19     && @tags.first({ $_ ∈ @exclude-tags }).defined {20    return False;21  }2223  if $focus-mode24     && !$ex.effective-focused25     && !$ex.effective-skipped {26    return False;27  }2829  if @include-tags.elems30     && !@tags.first({ $_ ∈ @include-tags }).defined {31    return False;32  }3334  if @example-patterns.elems {35    my $description = full-description($ex);36    my $matched = False;37    for @example-patterns -> $pattern {38      if matches-pattern($description, $pattern) {39        $matched = True;40        last;41      }42    }43    return False unless $matched;44  }4546  if @only-locations.elems {47    my $matched = False;48    for @only-locations -> $loc {49      if location-matches-node($ex, $loc) {50        $matched = True;51        last;52      }53      for $ex.ancestry -> $node {54        next unless $node ~~ ExampleGroup;55        if location-matches-node($node, $loc) {56          $matched = True;57          last;58        }59      }60      last if $matched;61    }62    return False unless $matched;63  }6465  True;66}6768sub full-description(Example $ex --> Str) {69  my @parts = $ex.ancestry.grep(ExampleGroup).map(*.description);70  @parts.push: $ex.description;71  @parts.join(' ');72}7374sub matches-pattern(Str $description, Str $pattern --> Bool) {75  if $pattern.chars > 276     && $pattern.starts-with('/')77     && $pattern.ends-with('/') {78    my $body = $pattern.substr(1, $pattern.chars - 2);79    my $rx = / <{ $body }> /;80    return so $description.match($rx);81  }82  $description.contains($pattern);83}8485sub location-matches-node($node, Str $loc --> Bool) {86  return False unless $loc.contains(':');87  my $idx = $loc.rindex(':');88  my $pattern-path = $loc.substr(0, $idx);89  my $pattern-line = $loc.substr($idx + 1);90  return False unless $node.line.Str eq $pattern-line;91  path-matches($node.file.Str, $pattern-path);92}9394sub path-matches(Str $node-path, Str $pattern-path --> Bool) {95  return True if $node-path eq $pattern-path;96  return True if $node-path.IO.absolute eq $pattern-path.IO.absolute;97  return True if $node-path.ends-with('/' ~ $pattern-path);98  False;99}100101sub has-focus(@suites --> Bool) {102  for @suites -> $s {103    return True if has-focus-node($s);104  }105  False;106}107108sub has-focus-node($node --> Bool) {109  given $node {110    when Example {111      return True if $node.focused;112    }113    default {114      return True if $node.focused;115      for $node.children.list -> $child {116        return True if has-focus-node($child);117      }118    }119  }120  False;121}122123our class FilterOptions {124  has @.include-tags;125  has @.exclude-tags;126  has @.example-patterns;127  has @.only-locations;128}129130our sub matching-examples(@suites, FilterOptions $opts --> List) {131  my $focus-mode = has-focus(@suites);132  my @results;133  for @suites -> $suite {134    walk-collect($suite, $opts, $focus-mode, @results);135  }136  @results.List;137}138139sub walk-collect($node, $opts, $focus-mode, @out) {140  given $node {141    when Example {142      if example-matches(143        $node,144        $opts.include-tags, $opts.exclude-tags,145        $opts.example-patterns, $opts.only-locations,146        :$focus-mode,147      ) {148        @out.push: $node;149      }150    }151    default {152      for $node.children.list -> $child {153        walk-collect($child, $opts, $focus-mode, @out);154      }155    }156  }157}158159sub status-label(Example $ex --> Str) {160  return 'pending' if $ex.pending;161  return 'skipped' if $ex.effective-skipped;162  return 'focused' if $ex.effective-focused;163  '';164}165166our sub render-text(@suites, FilterOptions $opts, Bool :$verbose = False --> Str) {167  my $focus-mode = has-focus(@suites);168  my @lines;169  my $multi = @suites.elems > 1;170  for @suites -> $suite {171    next unless suite-has-match($suite, $opts, $focus-mode);172    if $multi {173      @lines.push: "# {$suite.description}";174      @lines.push: '';175    }176    render-text-children(177      $suite, $opts, $focus-mode, $multi ?? 1 !! 0, @lines, :$verbose,178    );179    @lines.push: '' if @lines.elems && @lines[*-1] ne '';180  }181  my $count = matching-examples(@suites, $opts).elems;182  @lines.push: "$count {$count == 1 ?? 'example' !! 'examples'}";183  @lines.join("\n") ~ "\n";184}185186sub suite-has-match($container, FilterOptions $opts, Bool $focus-mode --> Bool) {187  for $container.children.list -> $child {188    given $child {189      when Example {190        return True if example-matches(191          $child,192          $opts.include-tags, $opts.exclude-tags,193          $opts.example-patterns, $opts.only-locations,194          :$focus-mode,195        );196      }197      default {198        return True if suite-has-match($child, $opts, $focus-mode);199      }200    }201  }202  False;203}204205sub render-text-children(206  $container, $opts, $focus-mode, Int $depth, @lines, Bool :$verbose,207) {208  for $container.children.list -> $child {209    given $child {210      when ExampleGroup {211        next unless suite-has-match($child, $opts, $focus-mode);212        @lines.push: ('  ' x $depth) ~ $child.description;213        render-text-children(214          $child, $opts, $focus-mode, $depth + 1, @lines, :$verbose,215        );216      }217      when Example {218        next unless example-matches(219          $child,220          $opts.include-tags, $opts.exclude-tags,221          $opts.example-patterns, $opts.only-locations,222          :$focus-mode,223        );224        my $status = status-label($child);225        my $suffix = $status.chars ?? " ({$status.uc})" !! '';226        @lines.push: ('  ' x $depth) ~ $child.description ~ $suffix;227        if $verbose {228          my $indent = '  ' x ($depth + 1);229          @lines.push: $indent ~ $child.file ~ ':' ~ $child.line;230          my @tags = $child.effective-tags;231          @lines.push: $indent ~ 'tags: ' ~ @tags.join(', ') if @tags.elems;232        }233      }234    }235  }236}237238our sub render-json(@suites, FilterOptions $opts, :@load-errors --> Str) {239  my $focus-mode = has-focus(@suites);240  my @examples;241  for matching-examples(@suites, $opts) -> $ex {242    my %m = $ex.metadata.clone;243    %m<lets>:delete;244    @examples.push: %(245      description       => $ex.description,246      full-description  => full-description($ex),247      file              => $ex.file.Str,248      line              => $ex.line,249      tags              => $ex.effective-tags.List,250      metadata          => %m,251      pending           => $ex.pending.so,252      focused           => $ex.effective-focused.so,253      skipped           => $ex.effective-skipped.so,254    );255  }256257  my @suite-tree = @suites.map(&serialize-suite-node);258259  my @load-error-records = @load-errors.map(-> %e {260    %(261      file    => (%e<file> // '').Str,262      message => (%e<message> // '').Str,263    );264  });265266  BDD::Behave::Benchmark::Format::to-json(%(267    version      => 1,268    count        => @examples.elems,269    examples     => @examples,270    suites       => @suite-tree,271    'load-errors' => @load-error-records,272  ));273}274275our sub serialize-suite-node($node --> Hash) {276  given $node {277    when Suite {278      %(279        type        => 'suite',280        description => $node.description,281        file        => $node.file.Str,282        line        => $node.line,283        metadata    => sanitize-metadata($node.metadata),284        children    => $node.children.map(&serialize-suite-node).List,285      );286    }287    when ExampleGroup {288      %(289        type        => 'group',290        description => $node.description,291        file        => $node.file.Str,292        line        => $node.line,293        metadata    => sanitize-metadata($node.metadata),294        children    => $node.children.map(&serialize-suite-node).List,295      );296    }297    when Example {298      %(299        type        => 'example',300        description => $node.description,301        file        => $node.file.Str,302        line        => $node.line,303        metadata    => sanitize-metadata($node.metadata),304        pending     => $node.pending.so,305      );306    }307    default {308      %();309    }310  }311}312313sub sanitize-metadata(%metadata --> Hash) {314  my %m = %metadata.clone;315  %m<lets>:delete;316  %m;317}