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}