← Back to index

lib/BDD/Behave/SpecRegistry.rakumod

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}