135 / 137 lines covered (98.5%)
1unit module BDD::Behave::SpecRegistry;23need BDD::Behave::SpecTree;45sub suite-type() { ::('BDD::Behave::SpecTree::Suite') }6sub example-group-type() { ::('BDD::Behave::SpecTree::ExampleGroup') }7sub example-type() { ::('BDD::Behave::SpecTree::Example') }89class Entry {10 has $.suite;11 has @.stack;12}1314our class ExampleQueryResult {15 has Str $.description is required;16 has Str $.full-description is required;17 has IO::Path $.file is required;18 has Int $.line is required;19 has IO::Path $.suite-file;20 has Str $.suite-description;21 has Str @.group-descriptions;22 has Str @.tags;23 has %.metadata;24 has Bool $.pending = False;25 has Bool $.focused = False;26 has Bool $.skipped = False;2728 method location(--> Str) { "{$!file}:{$!line}" }2930 method to-hash(--> Hash) {31 %(32 description => $!description,33 full-description => $!full-description,34 file => $!file.Str,35 line => $!line,36 suite-file => ($!suite-file.defined ?? $!suite-file.Str !! Str),37 suite-description => ($!suite-description // Str),38 group-descriptions => @!group-descriptions.List,39 tags => @!tags.List,40 metadata => %!metadata.Hash,41 pending => $!pending,42 focused => $!focused,43 skipped => $!skipped,44 );45 }46}4748sub matches-pattern(Str $description, Str $pattern --> Bool) {49 if $pattern.chars > 250 && $pattern.starts-with('/')51 && $pattern.ends-with('/') {52 my $body = $pattern.substr(1, $pattern.chars - 2);53 my $rx = / <{ $body }> /;54 return so $description.match($rx);55 }56 $description.contains($pattern);57}5859sub path-matches(IO::Path $node-path, Str $pattern-path --> Bool) {60 return True if $node-path.Str eq $pattern-path;61 return True if $node-path.absolute eq $pattern-path.IO.absolute;62 return True if $node-path.Str.ends-with('/' ~ $pattern-path);63 return True if $node-path.basename eq $pattern-path;64 False;65}6667sub meta-value-equals($actual, $expected --> Bool) {68 return False unless $actual.defined;69 if $expected ~~ Bool {70 return $expected ?? ?$actual !! !$actual;71 }72 if $expected ~~ Positional {73 return False unless $actual ~~ Positional;74 my @a = $actual.list;75 my @e = $expected.list;76 return False unless @a.elems == @e.elems;77 for @e.kv -> $i, $v {78 return False unless @a[$i] eq $v;79 }80 return True;81 }82 if $actual ~~ Positional {83 return $actual.list.first(* eq $expected).defined;84 }85 $actual eq $expected;86}8788class SpecRegistry {89 has %.entries;90 has $.current-entry is rw;9192 method entry-for(IO::Path $file --> Entry) {93 my IO::Path $path = $file.absolute.IO;94 my $key = $path.absolute.Str;95 %.entries{$key} //= Entry.new(96 suite => suite-type().create(:description($path.basename), :file($path)),97 stack => [],98 );99 %.entries{$key};100 }101102 method suite-for(IO::Path $file) {103 self.entry-for($file).suite;104 }105106 method register-group(:$description!, :&block!, :$file!, :$line!, :@tags = [], :%extra-meta = %(), :$skipped, :$focused) {107 my IO::Path $path = ($file ~~ IO::Path ?? $file !! $file.IO).absolute.IO;108 my $entry = self.entry-for($path);109 $!current-entry = $entry;110 my $parent = $entry.stack.elems ?? $entry.stack[*-1] !! $entry.suite;111 my $group = example-group-type().new(:$description, :file($path), :$line);112 $group.set-metadata(:tags(@tags.list)) if @tags.elems;113 $group.set-metadata(:skipped(True)) if $skipped;114 $group.set-metadata(:focused(True)) if $focused;115 for %extra-meta.kv -> $key, $value {116 $group.set-metadata(|%($key => $value));117 }118 $parent.add-group($group);119 $entry.stack.push($group);120 LEAVE {121 $entry.stack.pop;122 $!current-entry = $entry.stack.elems ?? $entry !! Nil;123 }124 block();125 $group;126 }127128 method register-example(:$description!, :&block!, :$file!, :$line!, :@tags = [], :%extra-meta = %(), :$skipped, :$focused) {129 my IO::Path $path = ($file ~~ IO::Path ?? $file !! $file.IO).absolute.IO;130 my $entry = self.entry-for($path);131 my $parent = $entry.stack.elems ?? $entry.stack[*-1] !! $entry.suite;132 my $example = example-type().new(:$description, :file($path), :$line, :block(&block));133134 my @lets;135 @lets.append: $entry.suite.let-definitions.list;136 for $entry.stack.grep(example-group-type()) -> $group {137 @lets.append: $group.let-definitions.list;138 }139 $example.set-metadata(:lets(@lets));140 $example.set-metadata(:tags(@tags.list)) if @tags.elems;141 $example.set-metadata(:skipped(True)) if $skipped;142 $example.set-metadata(:focused(True)) if $focused;143 for %extra-meta.kv -> $key, $value {144 $example.set-metadata(|%($key => $value));145 }146 $parent.add-example($example);147 $example;148 }149150 method current-group-for(IO::Path $file) {151 my IO::Path $path = ($file ~~ IO::Path ?? $file !! $file.IO).absolute.IO;152 my $entry = self.entry-for($path);153 $entry.stack.elems ?? $entry.stack[*-1] !! Nil;154 }155156 method suites() {157 %.entries.values.map(*.suite).List;158 }159160 method suite-for-file(IO::Path $file) {161 my $key = $file.absolute.Str;162 %.entries{$key}:exists ?? %.entries{$key}.suite !! Nil;163 }164165 method clear {166 %.entries = ();167 }168169 method all-examples(--> List) {170 my @results;171 for self.suites.list -> $suite {172 self!collect-examples($suite, $suite, [], @results);173 }174 @results.List;175 }176177 method !collect-examples($node, $suite, @group-descriptions, @results) {178 given $node {179 when example-type() {180 @results.push: self!to-query-result($node, $suite, @group-descriptions);181 }182 when example-group-type() {183 my @next-groups = (|@group-descriptions, $node.description);184 for $node.children.list -> $child {185 self!collect-examples($child, $suite, @next-groups, @results);186 }187 }188 when suite-type() {189 for $node.children.list -> $child {190 self!collect-examples($child, $suite, @group-descriptions, @results);191 }192 }193 }194 }195196 method !to-query-result($example, $suite, @group-descriptions --> ExampleQueryResult) {197 my %metadata = $example.metadata.clone;198 %metadata<lets>:delete;199 my $full-description = (|@group-descriptions, $example.description).join(' ');200201 ExampleQueryResult.new(202 description => $example.description,203 :$full-description,204 file => $example.file,205 line => $example.line,206 suite-file => $suite.file,207 suite-description => $suite.description,208 group-descriptions => @group-descriptions.List,209 tags => $example.effective-tags.List,210 :%metadata,211 pending => $example.pending.so,212 focused => $example.effective-focused.so,213 skipped => $example.effective-skipped.so,214 );215 }216217 method query-examples(218 :$description-pattern,219 :$file,220 :$line,221 :@include-tags = [],222 :@exclude-tags = [],223 :%metadata = %(),224 :%metadata-exclude = %(),225 :$pending,226 :$focused,227 :$skipped,228 --> List229 ) {230 my @all = self.all-examples;231 my @kept;232 for @all -> $r {233 if $description-pattern.defined && $description-pattern.chars {234 next unless matches-pattern($r.full-description, $description-pattern);235 }236237 if $file.defined {238 my $pattern-path = $file ~~ IO::Path ?? $file.Str !! $file.Str;239 next unless path-matches($r.file, $pattern-path);240 }241242 if $line.defined {243 next unless $r.line == $line;244 }245246 if @include-tags.elems {247 next unless @include-tags.first({ $r.tags.first(* eq $_).defined }).defined;248 }249250 if @exclude-tags.elems {251 next if @exclude-tags.first({ $r.tags.first(* eq $_).defined }).defined;252 }253254 my $matches-meta = True;255 for %metadata.kv -> $k, $v {256 unless meta-value-equals($r.metadata{$k}, $v) {257 $matches-meta = False;258 last;259 }260 }261 next unless $matches-meta;262263 my $excluded = False;264 for %metadata-exclude.kv -> $k, $v {265 if meta-value-equals($r.metadata{$k}, $v) {266 $excluded = True;267 last;268 }269 }270 next if $excluded;271272 if $pending.defined {273 next unless ?$r.pending == ?$pending;274 }275276 if $focused.defined {277 next unless ?$r.focused == ?$focused;278 }279280 if $skipped.defined {281 next unless ?$r.skipped == ?$skipped;282 }283284 @kept.push: $r;285 }286 @kept.List;287 }288289 method count-examples(|args --> Int) {290 self.query-examples(|args).elems;291 }292}293294my SpecRegistry $REGISTRY .= new;295296our sub registry() is export(:DEFAULT) {297 $REGISTRY;298}299300our sub suites() is export(:DEFAULT) {301 $REGISTRY.suites;302}