diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e23272..278c53c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.12.0] - 2024-02-03 +### Added +- Added ability to use matchers for case equality. [#55](https://github.com/icy-arctic-fox/spectator/issues/55) +- Added support for nested case equality when checking arguments with Array, Tuple, Hash, and NamedTuple. + +### Fixed +- Fixed some issues with the `be_within` matcher when used with expected and union types. + ## [0.11.7] - 2023-10-16 ### Fixed - Fix memoized value (`let`) with a union type causing segfault. [#81](https://gitlab.com/arctic-fox/spectator/-/issues/81) @@ -450,7 +458,8 @@ This has been changed so that it compiles and raises an error at runtime with a First version ready for public use. -[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.7...master +[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.12.0...master +[0.12.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.7...v0.12.0 [0.11.7]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.6...v0.11.7 [0.11.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.5...v0.11.6 [0.11.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.4...v0.11.5 diff --git a/README.md b/README.md index 323b4ca..00b5eb9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Add this to your application's `shard.yml`: development_dependencies: spectator: gitlab: arctic-fox/spectator - version: ~> 0.11.0 + version: ~> 0.12.0 ``` Usage diff --git a/shard.yml b/shard.yml index 048953c..f364f0f 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.11.7 +version: 0.12.0 description: | Feature-rich testing framework for Crystal inspired by RSpec. diff --git a/spec/issues/github_issue_55_spec.cr b/spec/issues/github_issue_55_spec.cr new file mode 100644 index 0000000..92c2b42 --- /dev/null +++ b/spec/issues/github_issue_55_spec.cr @@ -0,0 +1,48 @@ +require "../spec_helper" + +Spectator.describe "GitHub Issue #55" do + GROUP_NAME = "CallCenter" + + let(name) { "TimeTravel" } + let(source) { "my.time.travel.experiment" } + + class Analytics(T) + property start_time = Time.local + property end_time = Time.local + + def initialize(@brain_talker : T) + end + + def instrument(*, name, source, &) + @brain_talker.send(payload: { + :group => GROUP_NAME, + :name => name, + :source => source, + :start => start_time, + :end => end_time, + }, action: "analytics") + end + end + + double(:brain_talker, send: nil) + + let(brain_talker) { double(:brain_talker) } + let(analytics) { Analytics.new(brain_talker) } + + it "tracks the time it takes to run the block" do + analytics.start_time = expected_start_time = Time.local + expected_end_time = expected_start_time + 10.seconds + analytics.end_time = expected_end_time + 0.5.seconds # Offset to ensure non-exact match. + + analytics.instrument(name: name, source: source) do + end + + expect(brain_talker).to have_received(:send).with(payload: { + :group => GROUP_NAME, + :name => name, + :source => source, + :start => expected_start_time, + :end => be_within(1.second).of(expected_end_time), + }, action: "analytics") + end +end diff --git a/src/spectator/matchers/matcher.cr b/src/spectator/matchers/matcher.cr index e54e55e..05adb81 100644 --- a/src/spectator/matchers/matcher.cr +++ b/src/spectator/matchers/matcher.cr @@ -1,3 +1,4 @@ +require "../value" require "./match_data" module Spectator::Matchers @@ -22,6 +23,19 @@ module Spectator::Matchers # A successful match with `#match` should normally fail for this method, and vice-versa. abstract def negated_match(actual : Expression(T)) : MatchData forall T + # Compares a matcher against a value. + # Enables composable matchers. + def ===(actual : Expression(T)) : Bool + match(actual).matched? + end + + # Compares a matcher against a value. + # Enables composable matchers. + def ===(other) : Bool + expression = Value.new(other) + match(expression).matched? + end + private def match_data_description(actual : Expression(T)) : String forall T match_data_description(actual.label) end diff --git a/src/spectator/matchers/range_matcher.cr b/src/spectator/matchers/range_matcher.cr index 8c31810..8a9a307 100644 --- a/src/spectator/matchers/range_matcher.cr +++ b/src/spectator/matchers/range_matcher.cr @@ -29,7 +29,26 @@ module Spectator::Matchers # Checks whether the matcher is satisfied with the expression given to it. private def match?(actual : Expression(T)) : Bool forall T - expected.value.includes?(actual.value) + actual_value = actual.value + expected_value = expected.value + if expected_value.is_a?(Range) && actual_value.is_a?(Comparable) + return match_impl?(expected_value, actual_value) + end + return false unless actual_value.is_a?(Comparable(typeof(expected_value.begin))) + expected_value.includes?(actual_value) + end + + private def match_impl?(expected_value : Range(B, E), actual_value : Comparable(B)) : Bool forall B, E + expected_value.includes?(actual_value) + end + + private def match_impl?(expected_value : Range(B, E), actual_value : T) : Bool forall B, E, T + return false unless actual_value.is_a?(B) || actual_value.is_a?(Comparable(B)) + expected_value.includes?(actual_value) + end + + private def match_impl?(expected_value : Range(Number, Number), actual_value : Number) : Bool + expected_value.includes?(actual_value) end # Message displayed when the matcher isn't satisfied.