diff --git a/README.md b/README.md index 0f07377..a828f2a 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ Items not marked as completed may have partial implementations. - [X] `be_empty` - [X] `have_key` - [X] `have_value` - - [ ] `all` + - [X] `all` - [ ] `all_satisfy` - [X] Truthy matchers - `be`, `be_true`, `be_truthy`, `be_false`, `be_falsey`, `be_nil` - [X] Error matchers - `raise_error` diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 043ece4..bcfbec1 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -526,6 +526,19 @@ module Spectator::DSL ::Spectator::Matchers::AttributesMatcher.new(%test_value) end + # Verifies that all elements of a collection satisfy some matcher. + # The collection should implement `Enumerable`. + # + # Examples: + # ``` + # array = [1, 2, 3, 4] + # expect(array).to all(be_even) # Fails. + # expect(array).to all(be_lt(5)) # Passes. + # ``` + macro all(matcher) + ::Spectator::Matchers::AllMatcher.new({{matcher}}) + end + # Indicates that some expression's value should change after taking an action. # # Examples: diff --git a/src/spectator/matchers/all_matcher.cr b/src/spectator/matchers/all_matcher.cr new file mode 100644 index 0000000..d43bda8 --- /dev/null +++ b/src/spectator/matchers/all_matcher.cr @@ -0,0 +1,57 @@ +require "../test_value" +require "./failed_match_data" +require "./matcher" +require "./successful_match_data" + +module Spectator::Matchers + # Matcher that checks if all elements of a collection apply to some other matcher. + struct AllMatcher(TMatcher) < Matcher + # Other matcher that all elements must match successfully. + private getter matcher + + # Creates the matcher with an expected successful matcher. + def initialize(@matcher : TMatcher) + end + + # Short text about the matcher's purpose. + # This explains what condition satisfies the matcher. + # The description is used when the one-liner syntax is used. + def description + "all #{matcher.description}" + end + + # Actually performs the test against the expression. + def match(actual : TestExpression(T)) : MatchData forall T + found = test_values(actual).each do |element| + match_data = matcher.match(element) + break match_data unless match_data.matched? + end + found ? found : SuccessfulMatchData.new + end + + # Negated matching for this matcher is not supported. + # Attempting to call this method will result in a compilation error. + # + # This syntax has a logical problem. + # "All values do not satisfy some condition." + # Does this mean that all values don't satisfy the matcher? + # What if only one doesn't? + # What if the collection is empty? + # + # RSpec doesn't support this syntax either. + def negated_match(actual : TestExpression(T)) : MatchData forall T + {% raise "The `expect { }.to_not all()` syntax is not supported (ambiguous)." %} + end + + # Maps all values in the test collection to their own test values. + # Each value is given their own label, + # which is the original label with an index appended. + private def test_values(actual) + label_prefix = actual.label + actual.value.map_with_index do |value, index| + label = "#{label_prefix}[#{index}]" + TestValue.new(value, label) + end + end + end +end diff --git a/src/spectator/test_block.cr b/src/spectator/test_block.cr index b0dc53d..a6af675 100644 --- a/src/spectator/test_block.cr +++ b/src/spectator/test_block.cr @@ -16,7 +16,7 @@ module Spectator def self.create(proc : -> T, label : String) forall T {% if T.id == "ReturnType".id %} - wrapper = -> { proc.call; nil } + wrapper = ->{ proc.call; nil } TestBlock(Nil).new(wrapper, label) {% else %} TestBlock(T).new(proc, label) @@ -31,7 +31,7 @@ module Spectator def self.create(proc : -> T) forall T {% if T.id == "ReturnType".id %} - wrapper = -> { proc.call; nil } + wrapper = ->{ proc.call; nil } TestBlock(Nil).new(wrapper) {% else %} TestBlock(T).new(proc)