From 10a0ccc8bd3215370ffa8a2dc1ff4091761526c6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 6 Jun 2024 16:25:39 -0600 Subject: [PATCH] Fresh start --- .gitignore | 9 +- .gitlab-ci.yml | 89 -- .guardian.yml | 11 - ARCHITECTURE.md | 273 ---- CHANGELOG.md | 530 ------- CONTRIBUTING.md | 204 --- LICENSE | 12 +- README.md | 426 +----- shard.yml | 9 +- spec/docs/anonymous_doubles_spec.cr | 15 - spec/docs/custom_matchers_spec.cr | 91 -- spec/docs/doubles_spec.cr | 79 - spec/docs/getting_started_spec.cr | 23 - spec/docs/helper_methods_spec.cr | 60 - spec/docs/mocks_spec.cr | 257 ---- spec/docs/null_objects_spec.cr | 16 - spec/docs/readme_spec.cr | 36 - spec/docs/structure_spec.cr | 34 - spec/docs/stubs_spec.cr | 130 -- spec/docs/subject_spec.cr | 32 - spec/features/aggregate_failures_spec.cr | 41 - spec/features/concise_spec.cr | 55 - spec/features/custom_message_spec.cr | 31 - spec/features/expect_type_spec.cr | 70 - spec/features/interpolated_label_spec.cr | 22 - spec/features/line_number_spec.cr | 32 - spec/features/metadata_spec.cr | 29 - spec/features/subject_spec.cr | 23 - spec/helpers/.gitkeep | 0 spec/issues/github_issue_28_spec.cr | 17 - spec/issues/github_issue_29_spec.cr | 29 - spec/issues/github_issue_30_spec.cr | 9 - spec/issues/github_issue_32_spec.cr | 34 - spec/issues/github_issue_33_spec.cr | 26 - spec/issues/github_issue_41_spec.cr | 30 - spec/issues/github_issue_42_spec.cr | 41 - spec/issues/github_issue_43_spec.cr | 51 - spec/issues/github_issue_44_spec.cr | 40 - spec/issues/github_issue_47_spec.cr | 18 - spec/issues/github_issue_48_spec.cr | 135 -- spec/issues/github_issue_49_spec.cr | 6 - spec/issues/github_issue_55_spec.cr | 48 - spec/issues/gitlab_issue_51_spec.cr | 109 -- spec/issues/gitlab_issue_76_spec.cr | 6 - spec/issues/gitlab_issue_77_spec.cr | 10 - spec/issues/gitlab_issue_80_spec.cr | 30 - spec/matchers/equality_matcher_spec.cr | 34 - spec/matchers/receive_matcher_spec.cr | 515 ------- spec/matchers/type_matcher_spec.cr | 26 - .../rspec/core/before_and_after_hooks_spec.cr | 112 -- spec/rspec/core/explicit_subject_spec.cr | 138 -- spec/rspec/core/helper_methods_spec.cr | 29 - spec/rspec/core/implicit_subject_spec.cr | 43 - spec/rspec/core/let_spec.cr | 45 - spec/rspec/core/one_liner_subject_spec.cr | 31 - spec/rspec/expectations/all_matcher_spec.cr | 35 - .../expectations/be_between_matcher_spec.cr | 17 - spec/rspec/expectations/be_matchers_spec.cr | 66 - .../expectations/be_within_matcher_spec.cr | 24 - .../rspec/expectations/change_matcher_spec.cr | 47 - .../expectations/comparison_matchers_spec.cr | 44 - .../contain_exactly_matcher_spec.cr | 30 - .../expectations/contain_matcher_spec.cr | 90 -- spec/rspec/expectations/cover_matcher_spec.cr | 29 - .../expectations/end_with_matcher_spec.cr | 30 - .../expectations/equality_matchers_spec.cr | 64 - .../have_attributes_matcher_spec.cr | 36 - spec/rspec/expectations/match_matcher_spec.cr | 28 - .../expectations/predicate_matchers_spec.cr | 81 -- .../expectations/raise_error_matcher_spec.cr | 93 -- .../expectations/respond_to_matcher_spec.cr | 28 - .../expectations/start_with_matcher_spec.cr | 30 - spec/rspec/expectations/type_matchers_spec.cr | 100 -- spec/spec_helper.cr | 17 +- spec/spectator/anything_spec.cr | 24 - spec/spectator/block_spec.cr | 34 - .../spectator/dsl/mocks/allow_receive_spec.cr | 188 --- spec/spectator/dsl/mocks/double_spec.cr | 385 ----- .../dsl/mocks/expect_receive_spec.cr | 226 --- .../spectator/dsl/mocks/have_received_spec.cr | 339 ----- spec/spectator/dsl/mocks/lazy_double_spec.cr | 83 -- spec/spectator/dsl/mocks/mock_spec.cr | 1288 ----------------- spec/spectator/dsl/mocks/null_double_spec.cr | 189 --- spec/spectator/dsl/mocks/stub_spec.cr | 116 -- spec/spectator/lazy_spec.cr | 15 - spec/spectator/lazy_wrapper_spec.cr | 28 - spec/spectator/mocks/allow_spec.cr | 39 - spec/spectator/mocks/arguments_spec.cr | 284 ---- spec/spectator/mocks/double_spec.cr | 542 ------- spec/spectator/mocks/exception_stub_spec.cr | 166 --- spec/spectator/mocks/formal_arguments_spec.cr | 325 ----- spec/spectator/mocks/lazy_double_spec.cr | 352 ----- spec/spectator/mocks/method_call_spec.cr | 26 - spec/spectator/mocks/mock_spec.cr | 1062 -------------- spec/spectator/mocks/multi_value_stub_spec.cr | 173 --- spec/spectator/mocks/null_double_spec.cr | 503 ------- spec/spectator/mocks/null_stub_spec.cr | 165 --- spec/spectator/mocks/proc_stub_spec.cr | 182 --- .../mocks/reference_mock_registry_spec.cr | 93 -- .../mocks/value_mock_registry_spec.cr | 93 -- spec/spectator/mocks/value_stub_spec.cr | 165 --- spec/spectator/system_exit_spec.cr | 13 - spec/spectator/value_spec.cr | 36 - spec/spectator/wrapper_spec.cr | 13 - spec/spectator_spec.cr | 9 + src/spectator.cr | 117 +- src/spectator/abstract_expression.cr | 53 - src/spectator/anything.cr | 25 - src/spectator/block.cr | 34 - src/spectator/composite_node_filter.cr | 15 - src/spectator/config.cr | 120 -- src/spectator/config/builder.cr | 317 ---- .../config/cli_arguments_applicator.cr | 240 --- src/spectator/context.cr | 37 - src/spectator/context_delegate.cr | 27 - src/spectator/context_method.cr | 10 - src/spectator/dsl.cr | 11 - src/spectator/dsl/builder.cr | 120 -- src/spectator/dsl/concise.cr | 56 - src/spectator/dsl/examples.cr | 153 -- src/spectator/dsl/expectations.cr | 191 --- src/spectator/dsl/groups.cr | 245 ---- src/spectator/dsl/hooks.cr | 167 --- src/spectator/dsl/matchers.cr | 879 ----------- src/spectator/dsl/memoize.cr | 108 -- src/spectator/dsl/metadata.cr | 29 - src/spectator/dsl/mocks.cr | 507 ------- src/spectator/dsl/reserved.cr | 7 - src/spectator/dsl/top.cr | 32 - src/spectator/error_result.cr | 28 - src/spectator/example.cr | 297 ---- src/spectator/example_builder.cr | 38 - src/spectator/example_context_delegate.cr | 33 - src/spectator/example_context_method.cr | 10 - src/spectator/example_failed.cr | 14 - src/spectator/example_group.cr | 151 -- src/spectator/example_group_builder.cr | 62 - src/spectator/example_group_hook.cr | 57 - src/spectator/example_group_iteration.cr | 25 - src/spectator/example_hook.cr | 52 - src/spectator/example_iterator.cr | 52 - src/spectator/example_pending.cr | 14 - src/spectator/example_procsy_hook.cr | 54 - src/spectator/expectation.cr | 281 ---- src/spectator/expectation_failed.cr | 16 - src/spectator/expression.cr | 18 - src/spectator/fail_result.cr | 81 -- src/spectator/filtered_example_iterator.cr | 85 -- src/spectator/formatting.cr | 7 - .../formatting/broadcast_formatter.cr | 87 -- src/spectator/formatting/components.cr | 8 - src/spectator/formatting/components/block.cr | 32 - .../formatting/components/comment.cr | 23 - .../components/error_result_block.cr | 83 -- .../formatting/components/example_command.cr | 26 - .../components/fail_result_block.cr | 66 - .../components/failure_command_list.cr | 21 - .../formatting/components/junit/root.cr | 39 - .../formatting/components/junit/test_case.cr | 102 -- .../formatting/components/junit/test_suite.cr | 104 -- .../components/pending_result_block.cr | 29 - .../formatting/components/profile.cr | 41 - .../formatting/components/result_block.cr | 95 -- .../formatting/components/runtime.cr | 66 - src/spectator/formatting/components/stats.cr | 38 - .../formatting/components/tap_profile.cr | 42 - src/spectator/formatting/components/totals.cr | 46 - .../formatting/document_formatter.cr | 99 -- src/spectator/formatting/formatter.cr | 128 -- src/spectator/formatting/html/body.ecr | 86 -- src/spectator/formatting/html/foot.ecr | 2 - src/spectator/formatting/html/head.ecr | 213 --- src/spectator/formatting/html_formatter.cr | 69 - src/spectator/formatting/json_formatter.cr | 66 - src/spectator/formatting/junit_formatter.cr | 51 - src/spectator/formatting/notifications.cr | 23 - .../formatting/progress_formatter.cr | 48 - src/spectator/formatting/summary.cr | 93 -- src/spectator/formatting/tap_formatter.cr | 71 - src/spectator/harness.cr | 192 --- src/spectator/hooks.cr | 83 -- src/spectator/includes.cr | 58 - .../iterative_example_group_builder.cr | 58 - src/spectator/label.cr | 7 - src/spectator/lazy.cr | 34 - src/spectator/lazy_wrapper.cr | 36 - src/spectator/line_node_filter.cr | 19 - src/spectator/location.cr | 66 - src/spectator/location_node_filter.cr | 17 - src/spectator/matchers.cr | 8 - src/spectator/matchers/all_matcher.cr | 57 - src/spectator/matchers/array_matcher.cr | 122 -- src/spectator/matchers/attributes_matcher.cr | 105 -- src/spectator/matchers/case_matcher.cr | 46 - .../matchers/change_exact_matcher.cr | 87 -- src/spectator/matchers/change_from_matcher.cr | 91 -- src/spectator/matchers/change_matcher.cr | 85 -- .../matchers/change_relative_matcher.cr | 57 - src/spectator/matchers/change_to_matcher.cr | 76 - src/spectator/matchers/collection_matcher.cr | 62 - .../matchers/compiled_type_matcher.cr | 59 - src/spectator/matchers/contain_matcher.cr | 69 - src/spectator/matchers/empty_matcher.cr | 47 - src/spectator/matchers/end_with_matcher.cr | 105 -- src/spectator/matchers/equality_matcher.cr | 40 - src/spectator/matchers/exception_matcher.cr | 127 -- src/spectator/matchers/failed_match_data.cr | 28 - .../matchers/greater_than_equal_matcher.cr | 58 - .../matchers/greater_than_matcher.cr | 58 - src/spectator/matchers/have_key_matcher.cr | 58 - src/spectator/matchers/have_matcher.cr | 120 -- .../matchers/have_predicate_matcher.cr | 98 -- src/spectator/matchers/have_value_matcher.cr | 58 - src/spectator/matchers/inequality_matcher.cr | 58 - src/spectator/matchers/instance_matcher.cr | 57 - .../matchers/less_than_equal_matcher.cr | 58 - src/spectator/matchers/less_than_matcher.cr | 58 - src/spectator/matchers/match_data.cr | 12 - src/spectator/matchers/matcher.cr | 51 - src/spectator/matchers/nil_matcher.cr | 40 - src/spectator/matchers/pattern_matcher.cr | 41 - src/spectator/matchers/predicate_matcher.cr | 99 -- src/spectator/matchers/range_matcher.cr | 110 -- src/spectator/matchers/receive_matcher.cr | 159 -- src/spectator/matchers/reference_matcher.cr | 48 - src/spectator/matchers/regex_matcher.cr | 40 - src/spectator/matchers/respond_matcher.cr | 67 - src/spectator/matchers/size_matcher.cr | 65 - src/spectator/matchers/size_of_matcher.cr | 65 - src/spectator/matchers/standard_matcher.cr | 130 -- src/spectator/matchers/start_with_matcher.cr | 104 -- .../matchers/successful_match_data.cr | 11 - src/spectator/matchers/truthy_matcher.cr | 161 --- src/spectator/matchers/type_matcher.cr | 63 - .../matchers/unordered_array_matcher.cr | 85 -- src/spectator/matchers/value_matcher.cr | 69 - src/spectator/metadata.cr | 11 - src/spectator/mocks.cr | 7 - src/spectator/mocks/abstract_arguments.cr | 61 - src/spectator/mocks/allow.cr | 28 - src/spectator/mocks/arguments.cr | 110 -- src/spectator/mocks/double.cr | 214 --- src/spectator/mocks/exception_stub.cr | 55 - src/spectator/mocks/formal_arguments.cr | 133 -- src/spectator/mocks/lazy_double.cr | 91 -- src/spectator/mocks/method_call.cr | 42 - src/spectator/mocks/mock.cr | 227 --- src/spectator/mocks/mock_registry.cr | 43 - src/spectator/mocks/mock_registry_entry.cr | 13 - src/spectator/mocks/mocked.cr | 127 -- src/spectator/mocks/multi_value_stub.cr | 35 - src/spectator/mocks/null_double.cr | 64 - src/spectator/mocks/null_stub.cr | 16 - src/spectator/mocks/proc_stub.cr | 36 - .../mocks/reference_mock_registry.cr | 53 - src/spectator/mocks/stub.cr | 50 - src/spectator/mocks/stub_modifiers.cr | 21 - src/spectator/mocks/stubbable.cr | 567 -------- src/spectator/mocks/stubbed_name.cr | 9 - src/spectator/mocks/stubbed_type.cr | 68 - src/spectator/mocks/typed_stub.cr | 19 - src/spectator/mocks/unexpected_message.cr | 5 - src/spectator/mocks/value_mock_registry.cr | 70 - src/spectator/mocks/value_stub.cr | 38 - src/spectator/multiple_expectations_failed.cr | 16 - src/spectator/name_node_filter.cr | 15 - src/spectator/node.cr | 94 -- src/spectator/node_builder.cr | 6 - src/spectator/node_filter.cr | 14 - src/spectator/node_iterator.cr | 44 - src/spectator/null_context.cr | 6 - src/spectator/null_node_filter.cr | 16 - src/spectator/pass_result.cr | 37 - src/spectator/pending_example_builder.cr | 24 - src/spectator/pending_result.cr | 57 - src/spectator/profile.cr | 61 - src/spectator/report.cr | 132 -- src/spectator/result.cr | 44 - src/spectator/run_flags.cr | 21 - src/spectator/runner.cr | 98 -- src/spectator/runner_events.cr | 93 -- src/spectator/should.cr | 145 -- src/spectator/spec.cr | 30 - src/spectator/spec_builder.cr | 212 --- src/spectator/system_exit.cr | 28 - src/spectator/tag_node_filter.cr | 18 - src/spectator/test_context.cr | 40 - src/spectator/value.cr | 29 - src/spectator/wrapper.cr | 53 - util/mirror-wiki.sh | 11 - util/nightly.sh | 7 - util/test-all-individually.sh | 5 - 292 files changed, 43 insertions(+), 26536 deletions(-) delete mode 100644 .gitlab-ci.yml delete mode 100644 .guardian.yml delete mode 100644 ARCHITECTURE.md delete mode 100644 CHANGELOG.md delete mode 100644 CONTRIBUTING.md delete mode 100644 spec/docs/anonymous_doubles_spec.cr delete mode 100644 spec/docs/custom_matchers_spec.cr delete mode 100644 spec/docs/doubles_spec.cr delete mode 100644 spec/docs/getting_started_spec.cr delete mode 100644 spec/docs/helper_methods_spec.cr delete mode 100644 spec/docs/mocks_spec.cr delete mode 100644 spec/docs/null_objects_spec.cr delete mode 100644 spec/docs/readme_spec.cr delete mode 100644 spec/docs/structure_spec.cr delete mode 100644 spec/docs/stubs_spec.cr delete mode 100644 spec/docs/subject_spec.cr delete mode 100644 spec/features/aggregate_failures_spec.cr delete mode 100644 spec/features/concise_spec.cr delete mode 100644 spec/features/custom_message_spec.cr delete mode 100644 spec/features/expect_type_spec.cr delete mode 100644 spec/features/interpolated_label_spec.cr delete mode 100644 spec/features/line_number_spec.cr delete mode 100644 spec/features/metadata_spec.cr delete mode 100644 spec/features/subject_spec.cr delete mode 100644 spec/helpers/.gitkeep delete mode 100644 spec/issues/github_issue_28_spec.cr delete mode 100644 spec/issues/github_issue_29_spec.cr delete mode 100644 spec/issues/github_issue_30_spec.cr delete mode 100644 spec/issues/github_issue_32_spec.cr delete mode 100644 spec/issues/github_issue_33_spec.cr delete mode 100644 spec/issues/github_issue_41_spec.cr delete mode 100644 spec/issues/github_issue_42_spec.cr delete mode 100644 spec/issues/github_issue_43_spec.cr delete mode 100644 spec/issues/github_issue_44_spec.cr delete mode 100644 spec/issues/github_issue_47_spec.cr delete mode 100644 spec/issues/github_issue_48_spec.cr delete mode 100644 spec/issues/github_issue_49_spec.cr delete mode 100644 spec/issues/github_issue_55_spec.cr delete mode 100644 spec/issues/gitlab_issue_51_spec.cr delete mode 100644 spec/issues/gitlab_issue_76_spec.cr delete mode 100644 spec/issues/gitlab_issue_77_spec.cr delete mode 100644 spec/issues/gitlab_issue_80_spec.cr delete mode 100644 spec/matchers/equality_matcher_spec.cr delete mode 100644 spec/matchers/receive_matcher_spec.cr delete mode 100644 spec/matchers/type_matcher_spec.cr delete mode 100644 spec/rspec/core/before_and_after_hooks_spec.cr delete mode 100644 spec/rspec/core/explicit_subject_spec.cr delete mode 100644 spec/rspec/core/helper_methods_spec.cr delete mode 100644 spec/rspec/core/implicit_subject_spec.cr delete mode 100644 spec/rspec/core/let_spec.cr delete mode 100644 spec/rspec/core/one_liner_subject_spec.cr delete mode 100644 spec/rspec/expectations/all_matcher_spec.cr delete mode 100644 spec/rspec/expectations/be_between_matcher_spec.cr delete mode 100644 spec/rspec/expectations/be_matchers_spec.cr delete mode 100644 spec/rspec/expectations/be_within_matcher_spec.cr delete mode 100644 spec/rspec/expectations/change_matcher_spec.cr delete mode 100644 spec/rspec/expectations/comparison_matchers_spec.cr delete mode 100644 spec/rspec/expectations/contain_exactly_matcher_spec.cr delete mode 100644 spec/rspec/expectations/contain_matcher_spec.cr delete mode 100644 spec/rspec/expectations/cover_matcher_spec.cr delete mode 100644 spec/rspec/expectations/end_with_matcher_spec.cr delete mode 100644 spec/rspec/expectations/equality_matchers_spec.cr delete mode 100644 spec/rspec/expectations/have_attributes_matcher_spec.cr delete mode 100644 spec/rspec/expectations/match_matcher_spec.cr delete mode 100644 spec/rspec/expectations/predicate_matchers_spec.cr delete mode 100644 spec/rspec/expectations/raise_error_matcher_spec.cr delete mode 100644 spec/rspec/expectations/respond_to_matcher_spec.cr delete mode 100644 spec/rspec/expectations/start_with_matcher_spec.cr delete mode 100644 spec/rspec/expectations/type_matchers_spec.cr delete mode 100644 spec/spectator/anything_spec.cr delete mode 100644 spec/spectator/block_spec.cr delete mode 100644 spec/spectator/dsl/mocks/allow_receive_spec.cr delete mode 100644 spec/spectator/dsl/mocks/double_spec.cr delete mode 100644 spec/spectator/dsl/mocks/expect_receive_spec.cr delete mode 100644 spec/spectator/dsl/mocks/have_received_spec.cr delete mode 100644 spec/spectator/dsl/mocks/lazy_double_spec.cr delete mode 100644 spec/spectator/dsl/mocks/mock_spec.cr delete mode 100644 spec/spectator/dsl/mocks/null_double_spec.cr delete mode 100644 spec/spectator/dsl/mocks/stub_spec.cr delete mode 100644 spec/spectator/lazy_spec.cr delete mode 100644 spec/spectator/lazy_wrapper_spec.cr delete mode 100644 spec/spectator/mocks/allow_spec.cr delete mode 100644 spec/spectator/mocks/arguments_spec.cr delete mode 100644 spec/spectator/mocks/double_spec.cr delete mode 100644 spec/spectator/mocks/exception_stub_spec.cr delete mode 100644 spec/spectator/mocks/formal_arguments_spec.cr delete mode 100644 spec/spectator/mocks/lazy_double_spec.cr delete mode 100644 spec/spectator/mocks/method_call_spec.cr delete mode 100644 spec/spectator/mocks/mock_spec.cr delete mode 100644 spec/spectator/mocks/multi_value_stub_spec.cr delete mode 100644 spec/spectator/mocks/null_double_spec.cr delete mode 100644 spec/spectator/mocks/null_stub_spec.cr delete mode 100644 spec/spectator/mocks/proc_stub_spec.cr delete mode 100644 spec/spectator/mocks/reference_mock_registry_spec.cr delete mode 100644 spec/spectator/mocks/value_mock_registry_spec.cr delete mode 100644 spec/spectator/mocks/value_stub_spec.cr delete mode 100644 spec/spectator/system_exit_spec.cr delete mode 100644 spec/spectator/value_spec.cr delete mode 100644 spec/spectator/wrapper_spec.cr create mode 100644 spec/spectator_spec.cr delete mode 100644 src/spectator/abstract_expression.cr delete mode 100644 src/spectator/anything.cr delete mode 100644 src/spectator/block.cr delete mode 100644 src/spectator/composite_node_filter.cr delete mode 100644 src/spectator/config.cr delete mode 100644 src/spectator/config/builder.cr delete mode 100644 src/spectator/config/cli_arguments_applicator.cr delete mode 100644 src/spectator/context.cr delete mode 100644 src/spectator/context_delegate.cr delete mode 100644 src/spectator/context_method.cr delete mode 100644 src/spectator/dsl.cr delete mode 100644 src/spectator/dsl/builder.cr delete mode 100644 src/spectator/dsl/concise.cr delete mode 100644 src/spectator/dsl/examples.cr delete mode 100644 src/spectator/dsl/expectations.cr delete mode 100644 src/spectator/dsl/groups.cr delete mode 100644 src/spectator/dsl/hooks.cr delete mode 100644 src/spectator/dsl/matchers.cr delete mode 100644 src/spectator/dsl/memoize.cr delete mode 100644 src/spectator/dsl/metadata.cr delete mode 100644 src/spectator/dsl/mocks.cr delete mode 100644 src/spectator/dsl/reserved.cr delete mode 100644 src/spectator/dsl/top.cr delete mode 100644 src/spectator/error_result.cr delete mode 100644 src/spectator/example.cr delete mode 100644 src/spectator/example_builder.cr delete mode 100644 src/spectator/example_context_delegate.cr delete mode 100644 src/spectator/example_context_method.cr delete mode 100644 src/spectator/example_failed.cr delete mode 100644 src/spectator/example_group.cr delete mode 100644 src/spectator/example_group_builder.cr delete mode 100644 src/spectator/example_group_hook.cr delete mode 100644 src/spectator/example_group_iteration.cr delete mode 100644 src/spectator/example_hook.cr delete mode 100644 src/spectator/example_iterator.cr delete mode 100644 src/spectator/example_pending.cr delete mode 100644 src/spectator/example_procsy_hook.cr delete mode 100644 src/spectator/expectation.cr delete mode 100644 src/spectator/expectation_failed.cr delete mode 100644 src/spectator/expression.cr delete mode 100644 src/spectator/fail_result.cr delete mode 100644 src/spectator/filtered_example_iterator.cr delete mode 100644 src/spectator/formatting.cr delete mode 100644 src/spectator/formatting/broadcast_formatter.cr delete mode 100644 src/spectator/formatting/components.cr delete mode 100644 src/spectator/formatting/components/block.cr delete mode 100644 src/spectator/formatting/components/comment.cr delete mode 100644 src/spectator/formatting/components/error_result_block.cr delete mode 100644 src/spectator/formatting/components/example_command.cr delete mode 100644 src/spectator/formatting/components/fail_result_block.cr delete mode 100644 src/spectator/formatting/components/failure_command_list.cr delete mode 100644 src/spectator/formatting/components/junit/root.cr delete mode 100644 src/spectator/formatting/components/junit/test_case.cr delete mode 100644 src/spectator/formatting/components/junit/test_suite.cr delete mode 100644 src/spectator/formatting/components/pending_result_block.cr delete mode 100644 src/spectator/formatting/components/profile.cr delete mode 100644 src/spectator/formatting/components/result_block.cr delete mode 100644 src/spectator/formatting/components/runtime.cr delete mode 100644 src/spectator/formatting/components/stats.cr delete mode 100644 src/spectator/formatting/components/tap_profile.cr delete mode 100644 src/spectator/formatting/components/totals.cr delete mode 100644 src/spectator/formatting/document_formatter.cr delete mode 100644 src/spectator/formatting/formatter.cr delete mode 100644 src/spectator/formatting/html/body.ecr delete mode 100644 src/spectator/formatting/html/foot.ecr delete mode 100644 src/spectator/formatting/html/head.ecr delete mode 100644 src/spectator/formatting/html_formatter.cr delete mode 100644 src/spectator/formatting/json_formatter.cr delete mode 100644 src/spectator/formatting/junit_formatter.cr delete mode 100644 src/spectator/formatting/notifications.cr delete mode 100644 src/spectator/formatting/progress_formatter.cr delete mode 100644 src/spectator/formatting/summary.cr delete mode 100644 src/spectator/formatting/tap_formatter.cr delete mode 100644 src/spectator/harness.cr delete mode 100644 src/spectator/hooks.cr delete mode 100644 src/spectator/includes.cr delete mode 100644 src/spectator/iterative_example_group_builder.cr delete mode 100644 src/spectator/label.cr delete mode 100644 src/spectator/lazy.cr delete mode 100644 src/spectator/lazy_wrapper.cr delete mode 100644 src/spectator/line_node_filter.cr delete mode 100644 src/spectator/location.cr delete mode 100644 src/spectator/location_node_filter.cr delete mode 100644 src/spectator/matchers.cr delete mode 100644 src/spectator/matchers/all_matcher.cr delete mode 100644 src/spectator/matchers/array_matcher.cr delete mode 100644 src/spectator/matchers/attributes_matcher.cr delete mode 100644 src/spectator/matchers/case_matcher.cr delete mode 100644 src/spectator/matchers/change_exact_matcher.cr delete mode 100644 src/spectator/matchers/change_from_matcher.cr delete mode 100644 src/spectator/matchers/change_matcher.cr delete mode 100644 src/spectator/matchers/change_relative_matcher.cr delete mode 100644 src/spectator/matchers/change_to_matcher.cr delete mode 100644 src/spectator/matchers/collection_matcher.cr delete mode 100644 src/spectator/matchers/compiled_type_matcher.cr delete mode 100644 src/spectator/matchers/contain_matcher.cr delete mode 100644 src/spectator/matchers/empty_matcher.cr delete mode 100644 src/spectator/matchers/end_with_matcher.cr delete mode 100644 src/spectator/matchers/equality_matcher.cr delete mode 100644 src/spectator/matchers/exception_matcher.cr delete mode 100644 src/spectator/matchers/failed_match_data.cr delete mode 100644 src/spectator/matchers/greater_than_equal_matcher.cr delete mode 100644 src/spectator/matchers/greater_than_matcher.cr delete mode 100644 src/spectator/matchers/have_key_matcher.cr delete mode 100644 src/spectator/matchers/have_matcher.cr delete mode 100644 src/spectator/matchers/have_predicate_matcher.cr delete mode 100644 src/spectator/matchers/have_value_matcher.cr delete mode 100644 src/spectator/matchers/inequality_matcher.cr delete mode 100644 src/spectator/matchers/instance_matcher.cr delete mode 100644 src/spectator/matchers/less_than_equal_matcher.cr delete mode 100644 src/spectator/matchers/less_than_matcher.cr delete mode 100644 src/spectator/matchers/match_data.cr delete mode 100644 src/spectator/matchers/matcher.cr delete mode 100644 src/spectator/matchers/nil_matcher.cr delete mode 100644 src/spectator/matchers/pattern_matcher.cr delete mode 100644 src/spectator/matchers/predicate_matcher.cr delete mode 100644 src/spectator/matchers/range_matcher.cr delete mode 100644 src/spectator/matchers/receive_matcher.cr delete mode 100644 src/spectator/matchers/reference_matcher.cr delete mode 100644 src/spectator/matchers/regex_matcher.cr delete mode 100644 src/spectator/matchers/respond_matcher.cr delete mode 100644 src/spectator/matchers/size_matcher.cr delete mode 100644 src/spectator/matchers/size_of_matcher.cr delete mode 100644 src/spectator/matchers/standard_matcher.cr delete mode 100644 src/spectator/matchers/start_with_matcher.cr delete mode 100644 src/spectator/matchers/successful_match_data.cr delete mode 100644 src/spectator/matchers/truthy_matcher.cr delete mode 100644 src/spectator/matchers/type_matcher.cr delete mode 100644 src/spectator/matchers/unordered_array_matcher.cr delete mode 100644 src/spectator/matchers/value_matcher.cr delete mode 100644 src/spectator/metadata.cr delete mode 100644 src/spectator/mocks.cr delete mode 100644 src/spectator/mocks/abstract_arguments.cr delete mode 100644 src/spectator/mocks/allow.cr delete mode 100644 src/spectator/mocks/arguments.cr delete mode 100644 src/spectator/mocks/double.cr delete mode 100644 src/spectator/mocks/exception_stub.cr delete mode 100644 src/spectator/mocks/formal_arguments.cr delete mode 100644 src/spectator/mocks/lazy_double.cr delete mode 100644 src/spectator/mocks/method_call.cr delete mode 100644 src/spectator/mocks/mock.cr delete mode 100644 src/spectator/mocks/mock_registry.cr delete mode 100644 src/spectator/mocks/mock_registry_entry.cr delete mode 100644 src/spectator/mocks/mocked.cr delete mode 100644 src/spectator/mocks/multi_value_stub.cr delete mode 100644 src/spectator/mocks/null_double.cr delete mode 100644 src/spectator/mocks/null_stub.cr delete mode 100644 src/spectator/mocks/proc_stub.cr delete mode 100644 src/spectator/mocks/reference_mock_registry.cr delete mode 100644 src/spectator/mocks/stub.cr delete mode 100644 src/spectator/mocks/stub_modifiers.cr delete mode 100644 src/spectator/mocks/stubbable.cr delete mode 100644 src/spectator/mocks/stubbed_name.cr delete mode 100644 src/spectator/mocks/stubbed_type.cr delete mode 100644 src/spectator/mocks/typed_stub.cr delete mode 100644 src/spectator/mocks/unexpected_message.cr delete mode 100644 src/spectator/mocks/value_mock_registry.cr delete mode 100644 src/spectator/mocks/value_stub.cr delete mode 100644 src/spectator/multiple_expectations_failed.cr delete mode 100644 src/spectator/name_node_filter.cr delete mode 100644 src/spectator/node.cr delete mode 100644 src/spectator/node_builder.cr delete mode 100644 src/spectator/node_filter.cr delete mode 100644 src/spectator/node_iterator.cr delete mode 100644 src/spectator/null_context.cr delete mode 100644 src/spectator/null_node_filter.cr delete mode 100644 src/spectator/pass_result.cr delete mode 100644 src/spectator/pending_example_builder.cr delete mode 100644 src/spectator/pending_result.cr delete mode 100644 src/spectator/profile.cr delete mode 100644 src/spectator/report.cr delete mode 100644 src/spectator/result.cr delete mode 100644 src/spectator/run_flags.cr delete mode 100644 src/spectator/runner.cr delete mode 100644 src/spectator/runner_events.cr delete mode 100644 src/spectator/should.cr delete mode 100644 src/spectator/spec.cr delete mode 100644 src/spectator/spec_builder.cr delete mode 100644 src/spectator/system_exit.cr delete mode 100644 src/spectator/tag_node_filter.cr delete mode 100644 src/spectator/test_context.cr delete mode 100644 src/spectator/value.cr delete mode 100644 src/spectator/wrapper.cr delete mode 100755 util/mirror-wiki.sh delete mode 100755 util/nightly.sh delete mode 100755 util/test-all-individually.sh diff --git a/.gitignore b/.gitignore index f76b510..75f549d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,12 @@ /.shards/ *.dwarf -# Libraries don't need dependency lock -# Dependencies will be locked in application that uses them +# Libraries don't need dependency lock. +# Dependencies will be locked in applications that use them. /shard.lock -# Ignore JUnit output +# Ignore JUnit output. output.xml -/test.cr +# Ignore ad-hoc test files. +/*test*.cr diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index d627d27..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,89 +0,0 @@ -# Official language image. Look for the different tagged releases at: -# https://hub.docker.com/r/crystallang/crystal/ -image: "crystallang/crystal:latest" - -# Cache shards in between builds -cache: - paths: - - lib - - bin - -before_script: - - crystal -v # Print out Crystal version for debugging - -spec: - script: - - crystal spec --error-on-warnings --junit_output=. spec/matchers/ spec/spectator/*.cr - artifacts: - when: always - paths: - - output.xml - reports: - junit: output.xml - -spec docs: - extends: spec - script: - - crystal spec --error-on-warnings --junit_output=. spec/docs/ - -spec features: - extends: spec - script: - - crystal spec --error-on-warnings --junit_output=. spec/features/ - -spec regression: - extends: spec - script: - - crystal spec --error-on-warnings --junit_output=. spec/issues/ - -spec rspec: - extends: spec - script: - - crystal spec --error-on-warnings --junit_output=. spec/rspec/ - -spec dsl: - extends: spec - script: - - crystal spec --error-on-warnings --junit_output=. spec/spectator/dsl/ - -spec mocks: - extends: spec - script: - - crystal spec --error-on-warnings --junit_output=. spec/spectator/mocks/ - -format: - script: - - shards - - crystal tool format --check - -style: - script: - - shards - - bin/ameba - -nightly: - image: "crystallang/crystal:nightly" - allow_failure: true - script: - - shards --ignore-crystal-version - - crystal spec --error-on-warnings --junit_output=. --tag smoke spec/spectator/dsl/ - - crystal tool format --check - artifacts: - when: always - paths: - - output.xml - reports: - junit: output.xml - -pages: - stage: deploy - dependencies: - - spec - script: - - crystal docs - - mv docs/ public/ - artifacts: - paths: - - public - only: - - master diff --git a/.guardian.yml b/.guardian.yml deleted file mode 100644 index f283d96..0000000 --- a/.guardian.yml +++ /dev/null @@ -1,11 +0,0 @@ -files: ./src/**/*.cr -run: time crystal spec --error-trace ---- -files: ./src/**/*.cr -run: bin/ameba %file% ---- -files: ./spec/**/*.cr -run: time crystal spec --error-trace %file% ---- -files: ./shard.yml -run: shards diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 90bcaba..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,273 +0,0 @@ -# Architecture and Design of Spectator - -This document explains the structure and design decisions behind Spectator. -It is broken up into the logical components of Spectator: - -- [Terms](#terms) -- [DSL](#dsl) - Domain Specific Language. Macros and methods that build a spec. -- [Matchers](#matchers) -- [Examples and groups](#examples-and-groups) -- [Runner and harness](#runner-and-harness) - - [Hooks](#hooks) -- [Mocks and doubles](#mocks-and-doubles) - - [Stubs](#stubs) - - [Doubles](#doubles) -- [Formatting](#formatting) - -## Terms - -The following are terms and concepts frequently used in the project. -They are listed in alphabetical order, -but you may find it useful to jump around when learning what they mean. - -**Assertion** - -An *assertion* is a fundamental piece of a test. -It checks that a condition is satisfied. -If that condition isn't met, then an exception is raised. - -**Builder** - -A *builder* is a type that incrementally constructs a complex object. -Builders are primarily used to create *specs* and *example groups*. -See: https://sourcemaking.com/design_patterns/builder - -**Config** - -Short for *configuration*, a *config* stores information about how to run the *spec*. -The configuration includes parsed command-line options, settings from `.spectator` and `Spectator.configure`. - -**Context** - -A *context* is the scope or "environment" a test runs in. -It is a part of an *example group* that provides methods, memoized values, and more to an example block. -From a technical standpoint, it is typically an instance of the class defined by an `example_group` block. -It can thought of as a closure. - -**Double** - -Stand-in for another type. -*Doubles* can be passed to methods under test instead of a real object. -They can be configured to respond to methods with *stubs*. -*Doubles* also track calls made to them. -An important note: a *double* is _not_ the same type (nor does it inherit) the replaced type. - -**DSL** - -*DSL* stands for **D**omain **S**pecific **L**anguage. -It is the human-like language that comprises a *spec*. -Keywords in the *DSL*, such as `describe`, `it`, and `expect`, are macros or methods. -Those macros and methods make calls to Spectator to describe the structure of a *spec*. -They are combined in such a way that makes it easy to read. - -**Example** - -An *example* is essentially a *test* and metadata. -Spectator makes a distinction between *test* and *example*. -An *example* can have a description, *group*, *context*, *result*, and *tags*. -That is to say: an *example* is the *test* and information for execution. -An *example* is a type of *node*. - -In the *DSL*, an *example* is created with `example` and `it` blocks and their variants. - -**Example Group** - -An *example group* (or *group* for short), is a collection of *examples*. -*Groups* can be nested in other *groups*, but can only have one parent. -*Groups* can have *hooks*. -*Groups* have extra properties like a name and metadata. -A *group* is a type of *node*. - -In the *DSL*, an *example group* is created with `example_group`, `describe`, and `context` blocks and their variants. - -**Expectation** - -An *expectation* captures a value or behavior and whether it satisfies a condition. -*Expectations* contain *match data*. -They are bubbled up from the *harness* to the runner. -*Expectations* can be thought of as wrappers for *assertions*. - -In the *DSL*, an *expectation* is the code: `expect(THIS).to eq(THAT)` -An *expectation target* is just the `expect(THIS)` portion. - -**Formatter** - -A *formatter* takes *results* and reports them to the user in a specific format. -Examples of *formatters* are XML, HTML, JSON, dots, and documentation. -The runner will call methods on the *formatter*. -The methods called depend on the type of *result* and the state of the runner. -For instance, `#example_started` is called before a an example runs, -and `#dump_summary` is called at the end when all *results* are available. - -**Harness** - -A *harness* is used to safely wrap *test* code. -It captures *expectations* and creates a *result* based on the outcome. - -**Filter** - -A *filter* selects *nodes* to be included in a running *spec*. -There are multiple types of *filters*. - -**Hook** - -A *hook* is a piece of code to execute at a key time. -For instance, before a *test* starts, or after everything in a *group* completes. -*Hooks* can be run in the same *context* as a *test*. -These are known as "example hooks." -*Hooks* that don't run in a *context*, and instead run independent of *examples*, are called "example group hooks." -*Hooks* are attached to *groups*. - -**Label** - -A *label* is a string from the *spec* that identifies a expression. -*Labels* are captured to improve the readability of *results* and *match data*. - -In the following code, the labels are: `does something useful`, `the_answer`, and `42`. - -```crystal -it "does something useful" do - expect(the_answer).to eq(42) -end -``` - -**Matcher** - -A *matcher* defines an expected value or behavior. -*Matchers* are given an "actual" value from a test and produce *match data*. -The *match data* contains information regarding whether the value or behavior was expected (satisfies a condition). -They behave similarly to an instance of a `Regex`. - -In the following code, the `eq(42)` portion returns an instance of a *matcher* expecting the value 42. - -```crystal -expect(the_answer).to eq(42) -``` - -**Match Data** - -*Match data* is produced by *matchers*. -It contains information regarding whether an *expectation* is satisfied and values from the match. -The values are key-value pairs identifying things such as "expected value" and "actual value." -*Match data* is similar in concept to `Regex::MatchData`. - -**Mock** - -A *mock* is a type that can have its original functionality "swapped out" for a *stub*. -This allows complex types to be "mocked" so that other types can be unit tested. -*Mocks* can have any number of *stubs* defined. -They are similar to *doubles*, but use a real type. - -**Node** - -A *node* refers to any component in a *spec*. -*Nodes* are typically *examples* and *example groups*. -A *node* can have metadata associated with it, such as a *label*, location, and *tags*. - -**Procsy** - -A *procsy* is a glorified `Proc`. -It is used to wrap an underlying proc in some way. -Typically used to wrap an *example* when passed to a *hook*. - -**Profile** - -A *profile* includes timing information for *examples*. -It tracks how long each *example* took and sorts them. - -**Report** - -A *report* is a collection of *results* generated by running *examples*. -It provides easy access to various metrics and types of *results*. - -**Result** - -A *result* summarizes the outcome of running an *example*. -A *result* can be passing, failing, or pending. -*Results* contain timing information, *expectations* processed in the *example*, and an error for failing *results*. - -**Spec** - -A *spec* is a collection of *examples*. -Conceptually, a *spec* defines the behavior of a system. -A *spec* consists of a single, root *example group* that provides a tree structure of *examples*. -A *spec* also contains some *config* describing how to run it. - -**Stub** - -A *stub* is a method in a *double* or *mock* that replaces the original functionality. -*Stubs* can be attached to a single instance or all instances of a type. - -**Tag** - -A *tag* is an identifier with optional value. -*Tags* can be used to group and filter *examples* and *example groups*. -Some *tags* have special meaning, like `skip` indicating an *example* or *group* should be skipped. - -**Test** - -The word "test" is overused, especially when using a testing framework. -We make an effort to avoid using the word "test" for everything. -However, *test* has a technical meaning in Spectator. -It refers to the code (block) executed in an *example*. - -```crystal -it "does a thing" do - # Test code starts here. Everything inside this `it` block is considered a test. - expect(the_answer).to eq(42) - # Test code ends here. -end -``` - -## DSL - -The DSL is made up of methods and macros. -What look like keywords (`describe`, `it`, `expect`, `eq`, etc.) are just macros and methods provided by the DSL. -Those macros and methods are defined in multiple modules in the `Spectator::DSL` namespace. -They are logically grouped by their functionality. - -Each module is included (as a mix-in) to the base Spectator context that all tests use. -The `SpectatorTestContext` class includes all of the DSL modules. - -The DSL methods and macros should be kept simple. -Their functionality should be off-loaded to internal Spectator "APIs." -For instance, when the DSL creates an example, -it defines a method for the test code and calls another method to register it. -While Crystal macros are powerful, excessive use of them makes maintenance harder and compilation slower. -Additionally, by keeping logic out of the DSL, testing of internals becomes less dependent on the DSL. - -*TODO:* Builders... - -*TODO:* Tricks... - -## Matchers - -*TODO:* Base types - -## Examples and groups - -*TODO* - -## Runner and harness - -*TODO* - -### Hooks - -*TODO* - -## Mocks and doubles - -*TODO* - -### Stubs - -*TODO* - -### Doubles - -*TODO* - -## Formatting - -*TODO* diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 278c53c..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,530 +0,0 @@ -# Changelog -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) - -## [0.11.6] - 2023-01-26 -### Added -- Added ability to cast types using the return value from expect/should statements with a type matcher. -- Added support for string interpolation in context names/labels. - -### Fixed -- Fix invalid syntax (unterminated call) when recording calls to stubs with an un-named splat. [#51](https://github.com/icy-arctic-fox/spectator/issues/51) -- Fix malformed method signature when using named splat with keyword arguments in mocked type. [#49](https://github.com/icy-arctic-fox/spectator/issues/49) - -### Changed -- Expectations using 'should' syntax report file and line where the 'should' keyword is instead of the test start. -- Add non-captured block argument in preparation for Crystal 1.8.0. - -## [0.11.5] - 2022-12-18 -### Added -- Added support for mock modules and types that include mocked modules. - -### Fixed -- Fix macro logic to support free variables, 'self', and variants on stubbed methods. [#48](https://github.com/icy-arctic-fox/spectator/issues/48) -- Fix method stubs used on methods that capture blocks. -- Fix type name resolution for when using custom types in a mocked typed. -- Prevent comparing range arguments with non-compatible types in stubs. [#48](https://github.com/icy-arctic-fox/spectator/issues/48) - -### Changed -- Simplify string representation of mock-related types. -- Remove unnecessary redefinitions of methods when adding stub functionality to a type. -- Allow metadata to be stored as nil to reduce overhead when tracking nodes without tags. -- Use normal equality (==) instead of case-equality (===) with proc arguments in stubs. -- Change stub value cast logic to avoid compiler bug. [#80](https://gitlab.com/arctic-fox/spectator/-/issues/80) - -## [0.11.4] - 2022-11-27 -### Added -- Add support for using named (keyword) arguments in place of positional arguments in stubs. [#47](https://github.com/icy-arctic-fox/spectator/issues/47) -- Add `before`, `after`, and `around` as aliases for `before_each`, `after_each`, and `around_each` respectively. - -### Fixed -- Clear stubs defined with `expect().to receive()` syntax after test finishes to prevent leakage between tests. -- Ensure stubs defined with `allow().to receive()` syntax are cleared after test finishes when used inside a test (another leakage). -- Fix crash caused when logging is enabled after running an example that attempts to exit. - -### Removed -- Removed support for stubbing undefined (untyped) methods in lazy doubles. Avoids possible segfault. - -## [0.11.3] - 2022-09-03 -### Fixed -- Display error block (failure message and stack trace) when using `fail`. [#78](https://gitlab.com/arctic-fox/spectator/-/issues/78) -- Defining a custom matcher outside of the `Spectator` namespace no longer produces a compilation error. [#46](https://github.com/icy-arctic-fox/spectator/issues/46) - -## [0.11.2] - 2022-08-07 -### Fixed -- `expect_raises` with block and no arguments produces compilation error. [#77](https://gitlab.com/arctic-fox/spectator/-/issues/77) - -### Changed -- `-e` (`--example`) CLI option performs a partial match instead of exact match. [#71](https://gitlab.com/arctic-fox/spectator/-/issues/71) [#45](https://github.com/icy-arctic-fox/spectator/issues/45) - -## [0.11.1] - 2022-07-18 -### Fixed -- Workaround nilable type issue with memoized value. [#76](https://gitlab.com/arctic-fox/spectator/-/issues/76) - -## [0.11.0] - 2022-07-14 -### Changed -- Overhauled mock and double system. [#63](https://gitlab.com/arctic-fox/spectator/-/issues/63) -- Testing if `exit` is called no longer is done with stubs and catching the `Spectator::SystemExit` exception should be caught. [#29](https://github.com/icy-arctic-fox/spectator/issues/29) -- Adjust evaluation order of `change` matcher expressions. - -### Removed -- Removed support for stubbing top-level methods (such as `exit`). - -## [0.10.6] - 2022-07-07 -### Fixed -- Fixed compiler warnings generated by positional arguments with different names. - -### Changed -- Forward example procsy `to_s` to underlying example. [#70](https://gitlab.com/arctic-fox/spectator/-/issues/70) - -## [0.10.5] - 2022-01-27 -### Fixed -- Fixed usage of `sample` with single block argument. [#41](https://github.com/icy-arctic-fox/spectator/issues/41#issuecomment-1022525702) - -## [0.10.4] - 2022-01-11 -### Added -- Support string interpolation for example name/description. [#41](https://github.com/icy-arctic-fox/spectator/issues/41) -- Support multiple block arguments in `sample` block (`Hash#each`). [#41](https://github.com/icy-arctic-fox/spectator/issues/41#issuecomment-1010192486) - -### Changed -- Source line reported by failure list changed to line containing `expect` instead of example start line. -- Better compiler error when using string interpolation in group name/description. [#41](https://github.com/icy-arctic-fox/spectator/issues/41) - -## [0.10.3] - 2021-10-13 -### Fixed -- Fixed runtime error with `expect` outside of test block - now gives compilation error. - -### Added -- Description of a `provided` example can be set by using `it` as an argument. [#69](https://gitlab.com/arctic-fox/spectator/-/issues/69) - -## [0.10.2] - 2021-10-22 -### Fixed -- Fix usage of `be ===` and `be =~` [#34](https://github.com/icy-arctic-fox/spectator/issues/34) -- Better handling of the `be(nil)` when used with value types. [#37](https://github.com/icy-arctic-fox/spectator/issues/37) -- Fix missing arguments for stubbed top-level methods (`system`, `exit`, etc.). [#36](https://github.com/icy-arctic-fox/spectator/issues/36) -- Fix outdated naming when using `expect_any_instance_of`. -- Fix adding stubs to class methods on mocked types. - -### Changed -- Elegantly handle missing/undefined methods with `have_attributes` matcher. - -## [0.10.1] - 2021-09-16 -### Fixed -- Fix `Spectator.configure` block calls to `filter_run_excluding` and `filter_run_including`. [#61](https://gitlab.com/arctic-fox/spectator/-/issues/61) -- Fix shard version constant creation when lib is in a directory with spaces in the path. [#33](https://gitlab.com/arctic-fox/spectator/-/merge_requests/33) Thanks @toddsundsted ! -- Re-add pre- and post-condition hooks. [#62](https://gitlab.com/arctic-fox/spectator/-/issues/62) - -## [0.10.0] - 2021-08-19 -### Fixed -- Fix resolution of types with the same name in nested scopes. [#31](https://github.com/icy-arctic-fox/spectator/issues/31) -- `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) -- Hook execution order has been tweaked to match RSpec. - -### Added -- `before_each`, `after_each`, and `around_each` hooks are yielded the current example as a block argument. -- The `let` and `subject` blocks are yielded the current example as a block argument. -- Add internal logging that uses Crystal's `Log` utility. Provide the `LOG_LEVEL` environment variable to enable. -- Support dynamic creation of examples. -- Capture and log information for hooks. -- Tags can be added to examples and example groups. -- Add matcher to check compiled type of values. -- Examples can be skipped by using a `:pending` tag. A reason method can be specified: `pending: "Some excuse"` -- Examples without a test block are marked as pending. [#37](https://gitlab.com/arctic-fox/spectator/-/issues/37) -- Examples can be skipped during execution by using `skip` or `pending` in the example block. [#17](https://gitlab.com/arctic-fox/spectator/-/issues/17) -- Sample blocks can be temporarily skipped by using `xsample` or `xrandom_sample`. -- Add `before_suite` and `after_suite` hooks. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) -- Support defining hooks in `Spectator.configure` block. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) -- Examples with failures or skipped during execution will report the location of that result. [#57](https://gitlab.com/arctic-fox/spectator/-/issues/57) -- Support custom messages for failed expectations. [#28](https://gitlab.com/arctic-fox/spectator/-/issues/28) -- Allow named arguments and assignments for `provided` (`given`) block. -- Add `aggregate_failures` to capture and report multiple failed expectations. [#24](https://gitlab.com/arctic-fox/spectator/-/issues/24) -- Supports matching groups. [#25](https://gitlab.com/arctic-fox/spectator/-/issues/25) [#24](https://github.com/icy-arctic-fox/spectator/issues/24) -- Add `filter_run_including`, `filter_run_excluding`, and `filter_run_when_matching` to config block. -- By default, only run tests when any are marked with `focus: true`. -- Add "f-prefix" blocks for examples and groups (`fit`, `fdescribe`, etc.) as a short-hand for specifying `focus: true`. -- Add HTML formatter. Operates the same as the JUnit formatter. Specify `--html_output=DIR` to use. [#22](https://gitlab.com/arctic-fox/spectator/-/issues/22) [#3](https://github.com/icy-arctic-fox/spectator/issues/3) - -### Changed -- `given` (now `provided`) blocks changed to produce a single example. `it` can no longer be nested in a `provided` block. -- The "should" syntax no longer reports the source as inside Spectator. -- Short-hand "should" syntax must be included by using `require "spectator/should"` - `it { should eq("foo") }` -- Better error messages and detection when DSL methods are used when they shouldn't (i.e. `describe` inside `it`). -- Prevent usage of reserved keywords in DSL (such as `initialize`). -- The count argument for `sample` and `random_sample` groups must be named (use `count: 5` instead of just `5`). -- Helper methods used as arguments for `sample` and `random_sample` must be class methods. -- Simplify and reduce instanced types and generics. Should speed up compilation times. -- Overhaul example creation and handling. -- Overhaul storage of test values. -- Overhaul reporting and formatting. Cleaner output for failures and pending tests. -- Cleanup and simplify DSL implementation. -- Other minor internal improvements and cleanup. - -### Deprecated -- `pending` blocks will behave differently in v0.11.0. They will mimic RSpec in that they _compile and run_ the block expecting it to fail. Use a `skip` (or `xit`) block instead to prevent compiling the example. -- `given` has been renamed to `provided`. The `given` keyword may be reused later for memoization. - -### Removed -- Removed one-liner `it`-syntax without braces (block). - -## [0.9.40] - 2021-07-10 -### Fixed -- Fix stubbing of class methods. -- Fix handling of `no_args` in some cases. - -### Changed -- Better handling and stubbing of `Process.exit`. - -## [0.9.39] - 2021-07-02 -### Fixed -- Fix `expect().to receive()` syntax not implicitly stubbing the method. -- Avoid calling `NoReturn` methods from stubs. [#29](https://github.com/icy-arctic-fox/spectator/issues/29) - -### Added -- Added support for `with(no_args)` for method stubs. [#28](https://github.com/icy-arctic-fox/spectator/issues/28) -- Allow creation of doubles without definition block. [#30](https://github.com/icy-arctic-fox/spectator/issues/30) - -## [0.9.38] - 2021-05-27 -### Fixed -- Fix `Channel::ClosedError` when using default Crystal Logger. [#27](https://github.com/icy-arctic-fox/spectator/issues/27) - -## [0.9.37] - 2021-05-19 -### Added -- Added support for `be ===` and `be =~`. [#26](https://github.com/icy-arctic-fox/spectator/issues/26) - -## [0.9.36] - 2021-04-22 -### Fixed -- Remove old workaround that prevented compilation on Windows. [#58](https://gitlab.com/arctic-fox/spectator/-/issues/58) - -## [0.9.35] - 2021-04-18 -### Fixed -- Allow types stored in variables or returned by methods in `be_a` (and variants), not just type literals. [#25](https://github.com/icy-arctic-fox/spectator/issues/25) - -## [0.9.34] - 2021-03-31 -### Changed -- Allow filtering examples by using any line in the example block. [#19](https://github.com/icy-arctic-fox/spectator/issues/19) Thanks @matthewmcgarvey ! - -## [0.9.33] - 2021-03-22 -### Changed -- Target Crystal 1.0 - -## [0.9.32] - 2021-02-03 -### Fixed -- Fix source reference with brace-less example syntax. [#20](https://github.com/icy-arctic-fox/spectator/issues/20) - -## [0.9.31] - 2021-01-08 -### Fixed -- Fix misaligned line numbers when referencing examples and groups. - -## [0.9.30] - 2020-12-23 -### Fixed -- Fix issue caused by additions from 0.9.29. - -### Changed -- Improve the `contain`, `contain_elements`, `have`, and `have_elements` to show missing items in output. - -## [0.9.29] - 2020-12-23 -### Added -- Add variants `contain_elements` and `have_elements`, which behave like `contain` and `have` matchers except that they take an array (or any enumerable type) instead of a parameter list or splat. - -## [0.9.28] - 2020-11-07 -### Added -- Add `return_type` option to method stubs. - -## [0.9.27] - 2020-10-01 -### Added -- Add syntax for stubbing operator-style methods, such as `[]`. - -## [0.9.26] - 2020-09-27 -### Fixed -- Fix issue with yielding in stubbed mocks. - -## [0.9.25] - 2020-09-26 -### Fixed -- Fix issue with splatting values for failed match data. This prevented the use of "description" and "failure_message" in some matches like `respond_to`. - -## [0.9.24] - 2020-09-17 -### Changed -- Allow some forms of string interpolation in group and example descriptions. - -## [0.9.23] - 2020-08-30 -### Fixed -- Allow the use of `object_id` and other possibly conflicting method names via `let`. [#53](https://gitlab.com/arctic-fox/spectator/-/issues/53) - -## [0.9.22] - 2020-08-11 -### Changed -- Handle splat in macro for matcher DSL. [#8](https://github.com/icy-arctic-fox/spectator/issues/8) - -## [0.9.21] - 2020-07-27 -### Added -- Display random seed when using `-r` or `--seed` options. [#7](https://github.com/icy-arctic-fox/spectator/issues/7) - -## [0.9.20] - 2020-05-29 -### Fixed -- Fix bug when using multiple short-hand block expects in one test. - -## [0.9.19] - 2020-05-28 -### Fixed -- Fix issue with `match_array` and `contain_exactly` matchers not working with immutable collections. - -## [0.9.18] - 2020-04-26 -### Fixed -- Fix `describe_class.new` when using a generic type. - -## [0.9.17] - 2020-04-23 -### Fixed -- Fix issue when using deferred syntax with `receive` matcher. [#48](https://gitlab.com/arctic-fox/spectator/-/issues/48) - -## [0.9.16] - 2020-04-06 -### Fixed -- Silence warnings from Crystal 0.34 - -## [0.9.15] - 2020-04-03 -### Fixed -- Fix issues with `match_array().in_any_order` and `contain_exactly().in_any_order`. [#47](https://gitlab.com/arctic-fox/spectator/-/issues/47) - -### Changed -- Improve usability when actual value does not respond to methods needed to verify it. -For instance, `expect(nil).to contain_exactly(:foo)` would not compile. -This has been changed so that it compiles and raises an error at runtime with a useful message. - -## [0.9.14] - 2020-04-01 -### Fixed -- Fix using nil with `be` matcher. [#45](https://gitlab.com/arctic-fox/spectator/-/issues/45) - -## [0.9.13] - 2020-03-28 -### Fixed -- Fix arguments not found in default stubs for mocks. [#44](https://gitlab.com/arctic-fox/spectator/-/issues/44) - -## [0.9.12] - 2020-03-20 -### Fixed -- Fix issue when mocking modules. Thanks @watzon ! - -## [0.9.11] - 2020-03-04 -### Fixed -- Fix issue when describing constants. [#40](https://gitlab.com/arctic-fox/spectator/-/issues/40) [#41](https://gitlab.com/arctic-fox/spectator/-/issues/41) - -## [0.9.10] - 2020-03-03 -### Changed -- Smarter behavior when omitting the block argument to the `around_each` hook. - -## [0.9.9] - 2020-02-22 -### Fixed -- Fix implicit subject when used with a module. [#6](https://github.com/icy-arctic-fox/spectator/issues/6) - -## [0.9.8] - 2020-02-21 -### Fixed -- Fix `be_between` matcher. Thanks @davidepaolotua / @jinn999 ! - -## [0.9.7] - 2020-02-16 -### Fixed -- Fix memoization of subject when using a type name for the context. -- Fix some cases when mocking a class method. - -## [0.9.6] - 2020-02-10 -### Added -- Add short-hand "should" syntax - `it { should eq("foo") }` -- The `be` matcher can be used on value types. -- Add more tests cases from RSpec docs. - -### Fixed -- Fix an issue with stubbed class methods on mocked types. Sometimes `previous_def` was used when `super` should have been used instead. -- Fix deferred expectations not running after all hooks. - -## [0.9.5] - 2020-01-19 -### Changed -- Described type is now considered an explicit subject. - -## [0.9.4] - 2020-01-19 -### Added -- Add more test cases from RSpec docs. -- Add `it_fails` utility to test expected failures. - -### Fixed -- Fix negated case for `respond_to` matcher. - -## [0.9.3] - 2020-01-17 -### Fixed -- Fix implicit subject overwriting explicit subject. [#25](https://gitlab.com/arctic-fox/spectator/-/merge_requests/25) - -## [0.9.2] - 2020-01-14 -### Added -- Add tests from RSpec docs. -- Add `with_message` modifier for `raise_error` matcher. -- Support omitted description on `it` and `specify` blocks. Use matcher description by default. - -### Fixed -- Fix `let!` not inferring return type. [#4](https://github.com/icy-arctic-fox/spectator/issues/4) - -### Changed -- Modified some matchers to behave more closely to their RSpec counterparts. - -## [0.9.1] - 2019-12-13 -### Fixed -- Fix default stub with type. -- Fix verifying double on self argument type. -- Pass stub instead of stub name to mock registry. - -### Removed -- Remove unnecessary type from stub class hierarchy. - -## [0.9.0] - 2019-12-08 -### Added -- Implement initial mocks and doubles (stubbing) support. [#16](https://gitlab.com/arctic-fox/spectator/-/merge_requests/16) [#6](https://gitlab.com/arctic-fox/spectator/-/issues/6) -- Deferred expectations (`to_eventually` and `to_never`). - -### Changed -- Test cases no longer define an entire class, but rather a method in a class belonging to the group. - -## [0.8.3] - 2019-09-23 -### Fixed -- Fix and address warnings with Crystal 0.31.0. - -## [0.8.2] - 2019-08-21 -### Fixed -- Workaround for Crystal compiler bug [#7060](https://github.com/crystal-lang/crystal/issues/7060). [#1](https://github.com/icy-arctic-fox/spectator/issues/1) - -## [0.8.1] - 2019-08-17 -### Fixed -- Fix nested `sample_value` blocks giving cryptic error. [#20](https://gitlab.com/arctic-fox/spectator/-/issues/20) - -## [0.8.0] - 2019-08-12 -### Added -- Add "any order" modifier for `contains_exactly` and `match_array`. -- Add `change` matcher and its variations. -- Add `all` matcher. -- Variation of `let` syntax that takes an assignment. - -### Changed -- Rewrote matcher class structure. -- Improved tracking of actual and expected values and their labels. -- Matcher values are only produced when the match fails, instead of always. - -### Fixed -- Fix malformed code generated by macros not working in latest Crystal version. - -## [0.7.2] - 2019-06-01 -### Fixed -- Reference types used in `subject` and `let` were recreated between hooks and the test block. [#11](https://gitlab.com/arctic-fox/spectator/-/issues/11) - -## [0.7.1] - 2019-05-21 -### Fixed -- Fixed an issue where named subjects could crash the compiler. - -## [0.7.0] - 2019-05-16 -### Added -- Added `be_between` matcher. - -### Changed -- The `be_within` matcher behaves like RSpec's. - -## [0.6.0] - 2019-05-08 -### Changed -- Introduced reference matcher and changed `be` matcher to use it instead of the case matcher. - -### Removed -- Removed regex matcher, the case matcher is used instead. - -## [0.5.3] - 2019-05-08 -### Fixed -- Updated the `expect_raises` matcher to accept an optional second argument to mimic `raise_error`. [#4](https://gitlab.com/arctic-fox/spectator/-/issues/4) - -## [0.5.2] - 2019-04-22 -### Fixed -- Fix `after_all` hooks not running with fail-fast enabled. [#2](https://gitlab.com/arctic-fox/spectator/-/issues/2) - -## [0.5.1] - 2019-04-18 -### Added -- Note in README regarding repository mirror. - -### Fixed -- Change protection on expectation partial to work with Crystal 0.28 and "should" syntax. -- Change references to `Time.now` to `Time.utc` in docs. - -## [0.5.0] - 2019-04-07 -First version ready for public use. - - -[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 -[0.11.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.3...v0.11.4 -[0.11.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.2...v0.11.3 -[0.11.2]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.1...v0.11.2 -[0.11.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.0...v0.11.1 -[0.11.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.6...v0.11.0 -[0.10.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.5...v0.10.6 -[0.10.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.4...v0.10.5 -[0.10.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.3...v0.10.4 -[0.10.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.2...v0.10.3 -[0.10.2]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.1...v0.10.2 -[0.10.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.0...v0.10.1 -[0.10.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.40...v0.10.0 -[0.9.40]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.39...v0.9.40 -[0.9.39]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.38...v0.9.39 -[0.9.38]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.37...v0.9.38 -[0.9.37]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.36...v0.9.37 -[0.9.36]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.35...v0.9.36 -[0.9.35]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.34...v0.9.35 -[0.9.34]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.33...v0.9.34 -[0.9.33]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.32...v0.9.33 -[0.9.32]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.31...v0.9.32 -[0.9.31]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.30...v0.9.31 -[0.9.30]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.29...v0.9.30 -[0.9.29]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.28...v0.9.29 -[0.9.28]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.27...v0.9.28 -[0.9.27]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.26...v0.9.27 -[0.9.26]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.25...v0.9.26 -[0.9.25]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.24...v0.9.25 -[0.9.24]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.23...v0.9.24 -[0.9.23]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.22...v0.9.23 -[0.9.22]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.21...v0.9.22 -[0.9.21]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.20...v0.9.21 -[0.9.20]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.19...v0.9.20 -[0.9.19]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.18...v0.9.19 -[0.9.18]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.17...v0.9.18 -[0.9.17]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.16...v0.9.17 -[0.9.16]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.15...v0.9.16 -[0.9.15]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.14...v0.9.15 -[0.9.14]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.13...v0.9.14 -[0.9.13]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.12...v0.9.13 -[0.9.12]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.11...v0.9.12 -[0.9.11]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.10...v0.9.11 -[0.9.10]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.9...v0.9.10 -[0.9.9]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.8...v0.9.9 -[0.9.8]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.7...v0.9.8 -[0.9.7]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.6...v0.9.7 -[0.9.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.5...v0.9.6 -[0.9.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.4...v0.9.5 -[0.9.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.3...v0.9.4 -[0.9.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.2...v0.9.3 -[0.9.2]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.1...v0.9.2 -[0.9.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.0...v0.9.1 -[0.9.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.8.3...v0.9.0 -[0.8.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.8.2...v0.8.3 -[0.8.2]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.8.1...v0.8.2 -[0.8.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.8.0...v0.8.1 -[0.8.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.7.2...v0.8.0 -[0.7.2]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.7.1...v0.7.2 -[0.7.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.7.0...v0.7.1 -[0.7.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.6.0...v0.7.0 -[0.6.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.5.2...v0.6.0 -[0.5.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.5.2...v0.5.3 -[0.5.2]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.5.1...v0.5.2 -[0.5.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.5.0...v0.5.1 -[0.5.0]: https://gitlab.com/arctic-fox/spectator/-/releases/v0.5.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 0dbf778..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,204 +0,0 @@ -# Contributing to Spectator - -Welcome to Spectator! We're glad you're here! - -Spectator strives to be an easy-to-use, batteries included testing framework for Crystal shards and applications. -The goal of Spectator is to: - -- Provide an easy-to-understand syntax for tests. Reading and writing tests should feel natural. -- Lower the bar to entry. Simplify non-trivial use cases make testing easier. -- Remove boilerplate. Reduce the amount of code necessary to get the job done. Provide common utilities. - -Spectator is heavily inspired by [RSpec](https://rspec.info/). -It tries to maintain compatibility with RSpec, but this isn't always possible. -Some language differences between [Ruby and Crystal](https://www.crystalforrubyists.com/) prohibit this. -Spectator also maintains feature parity with Crystal's [Spec](https://crystal-lang.org/reference/guides/testing.html). - -**Table of Contents** - -- [Useful links](#useful-links) -- [Repository hosting](#repository-hosting) -- [How can I contribute?](#how-can-i-contribute) - - [Upcoming](#upcoming) - - [How to submit changes](#how-to-submit-changes) - - [How to report a bug](#how-to-report-a-bug) - - [How to request an enhancement](#how-to-request-an-enhancement) - - [Where can I get help?](#where-can-i-get-help) -- [Development](#development) - - [Testing](#testing) - - [Style guide and conventions](#style-guide-and-conventions) - - [Branching](#branching) - -## Useful links - -Here are some useful links for Spectator: - -- [README](README.md) -- [Change log](CHANGELOG.md) -- [Architecture](ARCHITECTURE.md) -- Wiki [GitLab](https://gitlab.com/arctic-fox/spectator/-/wikis/home) | [GitHub](https://github.com/icy-arctic-fox/spectator/wiki) - - Big List of Matchers [GitLab](https://gitlab.com/arctic-fox/spectator/-/wikis/Big-List-of-Matchers) | [GitHub](https://github.com/icy-arctic-fox/spectator/wiki/Big-List-of-Matchers) -- [Documentation (Crystal docs)](https://arctic-fox.gitlab.io/spectator) -- Issue tracker [GitLab](https://gitlab.com/arctic-fox/spectator/-/issues) | [GitHub](https://github.com/icy-arctic-fox/spectator/issues) -- [Builds](https://gitlab.com/arctic-fox/spectator/-/pipelines) -- [Crystal Macros](https://crystal-lang.org/reference/syntax_and_semantics/macros/index.html) - - [API](https://crystal-lang.org/api/latest/Crystal/Macros.html) - -## Repository hosting - -Spectator is available on [GitHub](https://github.com/icy-arctic-fox/spectator) and [GitLab](https://gitlab.com/arctic-fox/spectator). -The primary housing for Spectator is [GitLab](https://gitlab.com/arctic-fox/spectator). -This is a preference of the primary developer ([arctic-fox](https://gitlab.com/arctic-fox)). -That doesn't mean GitLab is the only place to contribute! -The repository and wiki are mirrored and maintainers will ensure contributions get into the project. -Issues, pull requests, and other contributions are accepted on both. -Use whichever you prefer! - -## How can I contribute? - -You can contribute in a variety of ways. -One of the easiest ways to contribute is to use Spectator! -Provide us feedback and report and bugs or issues you find. -Being a testing framework, Spectator should be rock solid. - -If you want to contribute to the codebase, take a look at the open issues. -[GitLab](https://gitlab.com/arctic-fox/spectator/-/issues) | [GitHub](https://github.com/icy-arctic-fox/spectator/issues) -Any issue not assigned can be picked up. -Even if one is already assigned, you may still be able to help out, just ask! - -If there isn't an issue for something you want to add or change, please create one first. -That way, we can discuss it before possibly wasting time on a misunderstanding. -Check out the [upcoming](#upcoming) section for a list of upcoming changes. -Anyone submitting code to Spectator will get a call-out in the [change log](CHANGELOG.md). - -You can also help by writing tests or documentation for the wiki. - -### Upcoming - -These are the projects that Spectator maintainers are looking to tackle next. -These issues are quite substantial, but assistance would be greatly appreciated! - -- [Overhaul of the mock and stubbing system](https://gitlab.com/arctic-fox/spectator/-/issues/63) -- [Errors masked in DSL by `method_missing` (predicate matcher)](https://gitlab.com/arctic-fox/spectator/-/issues/64) -- [Custom matcher DSL](https://gitlab.com/arctic-fox/spectator/-/issues/65) -- [Compiler optimizations](https://gitlab.com/arctic-fox/spectator/-/issues/66) -- More tests! - -### How to submit changes - -Submit your changes as a pull request (either GitLab or GitHub). -Please keep change requests focused on a single feature or bug. -Larger pull requests may be broken up into logical pieces. -This helps reviewers digest the changes. - -Describe the purpose of the change, what it addresses. -Provide links to related issues, bugs the changes fix, or external resources (i.e. RSpec docs). -Note any significant items or breaking changes. -Include a snippet of code demonstrating the code, if applicable. - -Please include tests for new code and bug fixes. -Some parts of Spectator are harder to test than others. -Check out the [testing](#testing) section for more information. - -### How to report a bug - -Be sure you can reproduce the issue. -Try to reduce the amount of code and complexity needed to reproduce the issue. -Check of outstanding issues, it might already be reported. - -Open an issue (either [GitLab](https://gitlab.com/arctic-fox/spectator/-/issues/new) or [GitHub](https://github.com/icy-arctic-fox/spectator/issues/new)). -Provide information on what you're trying to do. -Include source code that reproduces the issue. -Add any specific details or specifics about your usage and environment. -For instance: using Windows, compiling with specific flags, dependencies on other shards, custom helper code, etc. - -Maintainers generally won't close an issue until they're certain it is resolved. -We may ask that you verify the fix on your end before closing the issue. - -### How to request an enhancement - -Check for existing feature requests, someone might already have the same idea. -Create a new issue (either [GitLab](https://gitlab.com/arctic-fox/spectator/-/issues/new) or [GitHub](https://github.com/icy-arctic-fox/spectator/issues/new)). -Explain what you're trying to do or would like improved. -Include reasoning - why do you want this feature, what would it help with? -Provide code snippets if it helps illustrate the idea. - -## Where can I get help? - -First checkout the [README](README.md) and wiki ([GitLab](https://gitlab.com/arctic-fox/spectator/-/wikis/home) | [GitHub](https://github.com/icy-arctic-fox/spectator/wiki)). -These two locations are official sources of information for the project. - -Look for existing issues before creating a new one. -We use issues for bugs, features, and general help/support. -You might find something that addresses your issue or points you in the right direction. -Adding a :+1: to the original issue is appreciated! - -If you can't find anything already out there, submit an issue. -Explain what you're trying to do and maybe an explanation of why. -Sometimes we might discover a better solution for what you're attempting to do. -Provide code snippets, command-line, and examples if possible. - -## Development - -Spectator is cross-platform and should work on any OS that [Crystal supports](https://crystal-lang.org/install/). -Development on Spectator is possible with a default Crystal installation (with Shards). - -To get started: - -1. Fork the repository [GitLab](https://gitlab.com/arctic-fox/spectator/fork/new) | [GitHub](https://github.com/icy-arctic-fox/spectator/fork) -2. Clone the forked git repository. -3. Run `shards` in the repository to pull developer dependencies. -4. Verify everything works by running `crystal spec`. - -At this point, dive into the code or check out the [architecture](ARCHITECTURE.md). - -### Testing - -Spectator uses itself for testing. -Please try to write tests for any new features that are added. -Bugs should have tests created for them to combat regression. - -The `spec/` directory contains all tests. -The feature tests are grouped into sub directories based on their type, they are: - -- `docs/` - Example snippets from Spectator's documentation. -- `features/` - Tests for Spectator's DSL and headline features. -- `issues/` - Tests for reported bugs. -- `matchers/` - Exhaustive testing of matchers. -- `rspec/` - Examples from RSpec's documentation modified slightly to work with Spectator. See: https://relishapp.com/rspec/ -- `spectator/` - Unit tests for Spectator internals. - -The `helpers/` directory contains utilities to aid tests. - -### Style guide and conventions - -General Crystal styling should be used. -To ensure everything is formatted correctly, run `crystal tool format` prior to committing. - -Additionally, [Ameba](https://crystal-ameba.github.io/) is used to encourage best coding practices. -To run Ameba, run `bin/ameba` in the root of the repository. -This requires that shards have been installed. - -Formatting checks and Ameba will be run as part of the CI pipeline. -Both are required to pass before a pull request will be accepted and merged. -Exceptions can be made for some Ameba issues. -Adding `#ameba:disable` to a line will [disable Ameba](https://crystal-ameba.github.io/ameba/#inline-disabling) for a particular issue. -However, please prefer to fix the issue instead of ignoring them. - -Please attempt to document every class and public method. -Documentation isn't required on private methods and trivial code. -Please add comments explaining algorithms and complex code. -DSL methods and macros should be heavily documented. -This helps developers using Spectator. - -HTML documentation is automatically generated (by `crystal docs`) and published to [GitLab pages](https://arctic-fox.gitlab.io/spectator). - -### Branching - -The `master` branch contains the latest stable code. -Branches are used for features, fixes, and release preparation. - -A new minor release is made whenever there is enough functionality to warrant one or some time has passed since the last one and there's pending fixes. -A new major release occurs when there are substantial changes to Spectator. -Known breaking changes are always in a major release. -Tags are made for each release. diff --git a/LICENSE b/LICENSE index e4174f9..426de9c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) 2018 Michael Miller +Copyright (c) 2024 Michael Miller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 00b5eb9..68af570 100644 --- a/README.md +++ b/README.md @@ -1,427 +1,39 @@ -Spectator -========= +# spectator -Spectator is a fully-featured spec-based test framework for Crystal. -It mimics features from [RSpec](http://rspec.info/). -Developers coming from Ruby and RSpec will feel right at home. -Spectator provides additional functionality to make testing easier and more fluent. +TODO: Write a description here -**Goal:** +## Installation -Spectator is designed to: +1. Add the dependency to your `shard.yml`: -- Reduce complexity of test code. -- Remove boilerplate from tests. -- Lower the difficulty of writing non-trivial tests. -- Provide an elegant syntax that is easy to read and understand. -- Provide common utilities that the end-user would otherwise need to write. + ```yaml + dependencies: + spectator: + github: icy-arctic-fox/spectator + ``` -Installation ------------- +2. Run `shards install` -Add this to your application's `shard.yml`: - -```yaml -development_dependencies: - spectator: - gitlab: arctic-fox/spectator - version: ~> 0.12.0 -``` - -Usage ------ - -If it doesn't exist already, create a `spec/spec_helper.cr` file. -In it, place the following: +## Usage ```crystal -require "../src/*" require "spectator" ``` -This will include Spectator and the source code for your shard. -Now you can start writing your specs. -The syntax is the same as what you would expect from modern RSpec. -The "expect" syntax is recommended and the default, however the "should" syntax is also available. -Your specs must be wrapped in a `Spectator.describe` block. -All other blocks inside the top-level block may use `describe` and `context` without the `Spectator.` prefix. +TODO: Write usage instructions here -Here's a minimal spec to demonstrate: +## Development -```crystal -require "./spec_helper" +TODO: Write development instructions here -Spectator.describe String do - subject { "foo" } +## Contributing - describe "#==" do - context "with the same value" do - let(value) { subject.dup } - - it "is true" do - is_expected.to eq(value) - end - end - - context "with a different value" do - let(value) { "bar" } - - it "is false" do - is_expected.to_not eq(value) - end - end - end -end -``` - -If you find yourself trying to shoehorn in functionality -or unsure how to write a test, please create an [issue](https://gitlab.com/arctic-fox/spectator/issues/new) for it. -The goal is to make it as easy as possible to write specs and keep your code clean. -We may come up with a solution or even introduce a feature to support your needs. - -**NOTE:** Due to the way this shard uses macros, -you may find that some code you would expect to work, or works in other spec libraries, creates syntax errors. -If you run into this, please create an issue so that we may try to resolve it. - -Features --------- - -Spectator has all of the basic functionality for BDD. -For full documentation on what it can do, please visit the [wiki](https://gitlab.com/arctic-fox/spectator/wikis/home). - -### Contexts - -The DSL supports arbitrarily nested contexts. -Contexts can have values defined for multiple tests (`let` and `subject`). -Additionally, hooks can be used to ensure any initialization or cleanup is done (`before`, `after`, and `around`). -Pre- and post-conditions can be used to ensure code contracts are kept. - -```crystal -# Initialize the database before running the tests in this context. -before_all { Database.init } - -# Teardown the database and cleanup after tests in the is context finish. -after_all { Database.cleanup } - -# Before each test, add some rows to the database. -let(row_count) { 5 } -before_each do - row_count.times { Database.insert_row } -end - -# Remove the rows after the test to get a clean slate. -after_each { Database.clear } - -describe "#row_count" do - it "returns the number of rows" do - expect(Database.row_count).to eq(row_count) - end -end -``` - -Spectator has different types of contexts to reduce boilerplate. -One is the `sample` context. -This context type repeats all tests (and contexts within) for a set of values. -For instance, some feature should behave the same for different input. -However, some inputs might cause problems, but should behave the same. -An example is various strings (empty strings, quoted strings, strings with non-ASCII, etc), -and numbers (positive, negative, zero, NaN, infinity). - -```crystal -# List of integers to test against. -def various_integers - [-7, -1, 0, 1, 42] -end - -# Repeat nested tests for every value in `#various_integers`. -sample various_integers do |int| - # Example that checks if a fictitious method `#format` converts to strings. - it "formats correctly" do - expect(format(int)).to eq(int.to_s) - end -end -``` - -Another context type is `provided`. -This context drastically reduces the amount of code needed in some scenarios. -It can be used where one (or more inputs) changes the output of multiple methods. -The `provided` context gives a concise syntax for this use case. - -```crystal -subject(user) { User.new(age) } - -# Each expression in the `provided` block is its own test. -provided age = 10 do - expect(user.can_drive?).to be_false - expect(user.can_vote?).to be_false -end - -provided age = 16 do - expect(user.can_drive?).to be_true - expect(user.can_vote?).to be_false -end - -provided age = 18 do - expect(user.can_drive?).to be_true - expect(user.can_vote?).to be_true -end -``` - -### Assertions - -Spectator supports two formats for assertions (expectations). -The preferred format is the "expect syntax". -This takes the form: - -```crystal -expect(THIS).to eq(THAT) -``` - -The other format, "should syntax" is used by Crystal's default Spec. - -``` -THIS.should eq(THAT) -``` - -The first format doesn't monkey-patch the `Object` type. -And as a bonus, it captures the expression or variable passed to `expect()`. -For instance, compare these two tests: - -```crystal -foo = "Hello world" -foo.size.should eq(12) # Wrong on purpose! -``` - -Produces this error output: - -```text -Failure: 11 does not equal 12 - - expected: 11 - actual: 12 -``` - -Which is reasonable, but where did 11 come from? -Alternatively, with the "expect syntax": - -```crystal -foo = "Hello world" -expect(foo.size).to eq(12) # Wrong on purpose! -``` - -Produces this error output: - -```text -Failure: foo.size does not equal 12 - - expected: 12 - actual: 11 -``` - -This makes it clearer what was being tested and failed. - -### Matchers - -Spectator has a variety of matchers for assertions. -These are named in such a way to help tests read as plain English. -Matchers can be used on any value or block. - -There are typical matchers for testing equality: `eq` and `ne`. -And matchers for comparison: `<`, `<=`, `>`, `>=`, `be_within`. -There are matchers for checking contents of collections: -`contain`, `have`, `start_with`, `end_with`, `be_empty`, `have_key`, and more. -See the [wiki](https://gitlab.com/arctic-fox/spectator/wikis/Matchers) for a full list of matchers. - -### Running - -Spectator supports multiple options for running tests. -"Fail fast" aborts on the first test failure. -"Fail blank" fails if there are no tests. -Tests can be filtered by their location and name. -Additionally, tests can be randomized. -Spectator can be configured with command-line arguments, -a configure block in a `spec_helper.cr` file, and `.spectator` configuration file. - -```crystal -Spectator.configure do |config| - config.fail_blank # Fail on no tests. - config.randomize # Randomize test order. - config.profile # Display slowest tests. -end -``` - -### Mocks and Doubles - -Spectator supports an extensive mocking feature set via two types - mocks and doubles. -Mocks are used to override behavior in existing types. -Doubles are objects that stand-in when there are no type restrictions. -Stubs can be defined on both which control how methods behave. - -```crystal -abstract class Interface - abstract def invoke(thing) : String -end - -# Type being tested. -class Driver - def do_something(interface : Interface, thing) - interface.invoke(thing) - end -end - -Spectator.describe Driver do - # Define a mock for Interface. - mock Interface - - # Define a double that the interface will use. - double(:my_double, foo: 42) - - it "does a thing" do - # Create an instance of the mock interface. - interface = mock(Interface) - # Indicate that `#invoke` should return "test" when called. - allow(interface).to receive(:invoke).and_return("test") - - # Create an instance of the double. - dbl = double(:my_double) - # Call the mock method. - subject.do_something(interface, dbl) - # Verify everything went okay. - expect(interface).to have_received(:invoke).with(dbl) - end -end -``` - -For details on mocks and doubles, see the [wiki](https://gitlab.com/arctic-fox/spectator/-/wikis/Mocks-and-Doubles). - -### Output - -Spectator matches Crystal's default Spec output with some minor changes. -JUnit and TAP are also supported output formats. -There are also highly detailed JSON and HTML outputs. - -Development ------------ - -This shard is still in active development. -New features are being added and existing functionality improved. -Spectator is well-tested, but may have some yet-to-be-found bugs. - -### Feature Progress - -In no particular order, features that have been implemented and are planned. -Items not marked as completed may have partial implementations. - -- [ ] DSL - - [X] `describe` and `context` blocks - - [X] Contextual values with `let`, `let!`, `subject`, `described_class` - - [X] Test multiple and generated values - `sample`, `random_sample` - - [X] Concise syntax - `provided` (was the now deprecated `given`) - - [X] Before and after hooks - `before_each`, `before_all`, `after_each`, `after_all`, `around_each` - - [X] Pre- and post-conditions - `pre_condition`, `post_condition` - - [ ] Other hooks - `on_success`, `on_failure`, `on_error` - - [X] One-liner syntax - - [X] Should syntax - `should`, `should_not` - - [X] Helper methods and modules - - [ ] Aliasing - custom example group types with preset attributes - - [X] Pending tests - `pending` - - [ ] Shared examples - `behaves_like`, `include_examples` - - [X] Deferred expectations - `to_eventually`, `to_never` -- [ ] Matchers - - [X] Equality matchers - `eq`, `ne`, `be ==`, `be !=` - - [X] Comparison matchers - `be <`, `be <=`, `be >`, `be >=`, `be_within[.of]`, `be_close` - - [X] Type matchers - `be_a`, `respond_to` - - [ ] Collection matchers - - [X] `contain` - - [X] `have` - - [X] `contain_exactly` - - [X] `contain_exactly.in_any_order` - - [X] `match_array` - - [X] `match_array.in_any_order` - - [X] `start_with` - - [X] `end_with` - - [X] `be_empty` - - [X] `have_key` - - [X] `have_value` - - [X] `all` - - [ ] `all_satisfy` - - [X] Truthy matchers - `be`, `be_true`, `be_truthy`, `be_false`, `be_falsey`, `be_nil` - - [X] Error matchers - `raise_error` - - [ ] Yield matchers - `yield_control[.times]`, `yield_with_args[.times]`, `yield_with_no_args[.times]`, `yield_successive_args` - - [ ] Output matchers - `output[.to_stdout|.to_stderr]` - - [X] Predicate matchers - `be_x`, `have_x` - - [ ] Misc. matchers - - [X] `match` - - [ ] `satisfy` - - [X] `change[.by|.from[.to]|.to|.by_at_least|.by_at_most]` - - [X] `have_attributes` - - [ ] Compound - `and`, `or` -- [ ] Mocks and Doubles - - [X] Mocks (Stub real types) - `mock TYPE { }` - - [X] Doubles (Stand-ins for real types) - `double NAME { }` - - [X] Method stubs - `allow().to receive()`, `allow().to receive().and_return()` - - [X] Spies - `expect().to have_received()` - - [X] Message expectations - `expect().to have_received().at_least()` - - [X] Argument expectations - `expect().to have_received().with()` - - [ ] Message ordering - `expect().to have_received().ordered` - - [X] Null doubles -- [X] Runner - - [X] Fail fast - - [X] Test filtering - by name, context, and tags - - [X] Fail on no tests - - [X] Randomize test order - - [X] Dry run - for validation and checking formatted output - - [X] Config block in `spec_helper.cr` - - [X] Config file - `.spectator` -- [X] Reporter and formatting - - [X] RSpec/Crystal Spec default - - [X] JSON - - [X] JUnit - - [X] TAP - - [X] HTML - -### How it Works (in a nutshell) - -This shard makes extensive use of the Crystal macro system to build classes and modules. -Each `describe` and `context` block creates a new class that inherits its parent. -The `it` block creates an method. -An instance of the group class is created to run the test. -Each group class includes all test values and hooks. - -Contributing ------------- - -1. Fork it (GitHub or GitLab ) +1. Fork it () 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) -5. Create a new Pull/Merge Request +5. Create a new Pull Request -Please make sure to run `crystal tool format` before submitting. -The CI build checks for properly formatted code. -[Ameba](https://crystal-ameba.github.io/) is run to check for code style. +## Contributors -Documentation is automatically generated and published to GitLab pages. -It can be found here: https://arctic-fox.gitlab.io/spectator - -This project's home is (and primarily developed) on [GitLab](https://gitlab.com/arctic-fox/spectator). -A mirror is maintained to [GitHub](https://github.com/icy-arctic-fox/spectator). -Issues, pull requests (merge requests), and discussion are welcome on both. -Maintainers will ensure your contributions make it in. - -For more information, see: [CONTRIBUTING.md](CONTRIBUTING.md) - -### Testing - -Tests must be written for any new functionality. - -The `spec/` directory contains feature tests as well as unit tests. -These demonstrate small bits of functionality. -The feature tests are grouped into sub directories based on their type, they are: - -- docs/ - Example snippets from Spectator's documentation. -- rspec/ - Examples from RSpec's documentation modified slightly to work with Spectator. - See: https://relishapp.com/rspec/ - Additional sub directories in this directory represent the modules/projects of RSpec. - -The other directories are for unit testing various parts of Spectator. +- [Michael Miller](https://github.com/icy-arctic-fox) - creator and maintainer diff --git a/shard.yml b/shard.yml index 559754f..3b2dc8c 100644 --- a/shard.yml +++ b/shard.yml @@ -1,16 +1,11 @@ name: spectator -version: 0.12.0 +version: 0.14.0 description: | Feature-rich testing framework for Crystal inspired by RSpec. authors: - Michael Miller -crystal: 1.6.0 +crystal: ">= 1.6.0, < 2.0.0" license: MIT - -development_dependencies: - ameba: - github: crystal-ameba/ameba - version: ~> 1.2.0 diff --git a/spec/docs/anonymous_doubles_spec.cr b/spec/docs/anonymous_doubles_spec.cr deleted file mode 100644 index fb22dee..0000000 --- a/spec/docs/anonymous_doubles_spec.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/wikis/Anonymous-Doubles -Spectator.describe "Anonymous Doubles Docs" do - it "does something" do - dbl = double(foo: 42) - expect(dbl.foo).to eq(42) - end - - it "does something" do - dbl = double(foo: 42) - allow(dbl).to receive(:foo).and_return(123) - expect(dbl.foo).to eq(123) - end -end diff --git a/spec/docs/custom_matchers_spec.cr b/spec/docs/custom_matchers_spec.cr deleted file mode 100644 index d3ee565..0000000 --- a/spec/docs/custom_matchers_spec.cr +++ /dev/null @@ -1,91 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/wikis/Custom-Matchers -Spectator.describe "Custom Matchers Docs" do - context "value matcher" do - # Sub-type of Matcher to suit our needs. - # Notice this is a struct. - struct MultipleOfMatcher(ExpectedType) < Spectator::Matchers::ValueMatcher(ExpectedType) - # 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 : String - "is a multiple of #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Spectator::Expression(T)) : Bool forall T - actual.value % expected.value == 0 - end - - # Message displayed when the matcher isn't satisfied. - # The message should typically only contain the test expression labels. - private def failure_message(actual : Spectator::Expression(T)) : String forall T - "#{actual.label} is not a multiple of #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # The message should typically only contain the test expression labels. - private def failure_message_when_negated(actual : Spectator::Expression(T)) : String forall T - "#{actual.label} is a multiple of #{expected.label}" - end - end - - # The DSL portion of the matcher. - # This captures the test expression and creates an instance of the matcher. - macro be_a_multiple_of(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - MultipleOfMatcher.new(%value) - end - - specify do - expect(9).to be_a_multiple_of(3) - # or negated: - expect(5).to_not be_a_multiple_of(2) - end - - specify "failure messages" do - expect { expect(9).to be_a_multiple_of(5) }.to raise_error(Spectator::ExpectationFailed, "9 is not a multiple of 5") - expect { expect(6).to_not be_a_multiple_of(3) }.to raise_error(Spectator::ExpectationFailed, "6 is a multiple of 3") - end - end - - context "standard matcher" do - struct OddMatcher < Spectator::Matchers::StandardMatcher - def description : String - "is odd" - end - - private def match?(actual : Spectator::Expression(T)) : Bool forall T - actual.value % 2 == 1 - end - - private def failure_message(actual : Spectator::Expression(T)) : String forall T - "#{actual.label} is not odd" - end - - private def failure_message_when_negated(actual : Spectator::Expression(T)) : String forall T - "#{actual.label} is odd" - end - - private def does_not_match?(actual : Spectator::Expression(T)) : Bool forall T - actual.value % 2 == 0 - end - end - - macro be_odd - OddMatcher.new - end - - specify do - expect(9).to be_odd - expect(2).to_not be_odd - end - - specify "failure messages" do - expect { expect(2).to be_odd }.to raise_error(Spectator::ExpectationFailed, "2 is not odd") - expect { expect(3).to_not be_odd }.to raise_error(Spectator::ExpectationFailed, "3 is odd") - end - end -end diff --git a/spec/docs/doubles_spec.cr b/spec/docs/doubles_spec.cr deleted file mode 100644 index be28c3d..0000000 --- a/spec/docs/doubles_spec.cr +++ /dev/null @@ -1,79 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/wikis/Doubles -Spectator.describe "Doubles Docs" do - let(answer) { 42 } - - double :my_double, answer: 5 do - def answer(arg1, arg2) - arg1 + arg2 - end - end - - it "does something" do - dbl = double(:my_double, answer: answer) - expect(dbl.answer).to eq(42) - expect(dbl.answer(1, 2)).to eq(3) - end - - class Emitter - def initialize(@value : Int32) - end - - def emit(target) - target.call(@value) - end - end - - context "Expecting Behavior" do - describe Emitter do - subject { Emitter.new(42) } - - double :target, call: nil - - describe "#emit" do - it "invokes #call on the target" do - target = double(:target) - subject.emit(target) - expect(target).to have_received(:call).with(42) - end - end - end - - it "does something" do - dbl = double(:my_double) - allow(dbl).to receive(:answer).and_return(42) # Merge this line... - dbl.answer - expect(dbl).to have_received(:answer) # and this line. - end - - it "does something" do - dbl = double(:my_double) - expect(dbl).to receive(:answer).and_return(42) - dbl.answer - end - end - - context "Class Doubles" do - double :my_double do - # Define class methods with `self.` prefix. - stub def self.something - 42 - end - end - - it "does something" do - # Default stubs can be defined with key-value pairs (keyword arguments). - dbl = class_double(:my_double, something: 3) - expect(dbl.something).to eq(3) - - # Stubs can be changed with `allow`. - allow(dbl).to receive(:something).and_return(5) - expect(dbl.something).to eq(5) - - # Even the expect-receive syntax works. - expect(dbl).to receive(:something).and_return(7) - dbl.something - end - end -end diff --git a/spec/docs/getting_started_spec.cr b/spec/docs/getting_started_spec.cr deleted file mode 100644 index e73dda5..0000000 --- a/spec/docs/getting_started_spec.cr +++ /dev/null @@ -1,23 +0,0 @@ -require "../spec_helper" - -Spectator.describe String do - subject { "foo" } - - describe "#==" do - context "with the same value" do - let(value) { subject.dup } - - it "is true" do - is_expected.to eq(value) - end - end - - context "with a different value" do - let(value) { "bar" } - - it "is false" do - is_expected.to_not eq(value) - end - end - end -end diff --git a/spec/docs/helper_methods_spec.cr b/spec/docs/helper_methods_spec.cr deleted file mode 100644 index 26d85d2..0000000 --- a/spec/docs/helper_methods_spec.cr +++ /dev/null @@ -1,60 +0,0 @@ -require "../spec_helper" - -Spectator.describe String do - # This is a helper method. - def random_string(length) - chars = ('a'..'z').to_a - String.build(length) do |builder| - length.times { builder << chars.sample } - end - end - - describe "#size" do - subject { random_string(10).size } - - it "is the length of the string" do - is_expected.to eq(10) - end - end -end - -Spectator.describe String do - # length is now pulled from value defined by `let`. - def random_string - chars = ('a'..'z').to_a - String.build(length) do |builder| - length.times { builder << chars.sample } - end - end - - describe "#size" do - let(length) { 10 } # random_string uses this. - subject { random_string.size } - - it "is the length of the string" do - is_expected.to eq(length) - end - end -end - -module StringHelpers - def random_string - chars = ('a'..'z').to_a - String.build(length) do |builder| - length.times { builder << chars.sample } - end - end -end - -Spectator.describe String do - include StringHelpers - - describe "#size" do - let(length) { 10 } - subject { random_string.size } - - it "is the length of the string" do - is_expected.to eq(length) - end - end -end diff --git a/spec/docs/mocks_spec.cr b/spec/docs/mocks_spec.cr deleted file mode 100644 index 58c16f8..0000000 --- a/spec/docs/mocks_spec.cr +++ /dev/null @@ -1,257 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/wikis/Mocks -Spectator.describe "Mocks Docs" do - context "Abstract Types" do - abstract class MyClass - abstract def something : String - end - - mock MyClass - - it "does something" do - mock = mock(MyClass) - allow(mock).to receive(:something).and_return("test") # Uncomment this line to fix. - mock.something - end - end - - abstract class MyClass - abstract def answer : Int32 - abstract def answer(arg1, arg2) : Int32 - end - - mock MyClass, answer: 5 do - def answer(arg1, arg2) : Int32 - arg1 + arg2 - end - end - - let(answer) { 42 } - - it "does something" do - mock = mock(MyClass, answer: answer) - expect(mock.answer).to eq(42) - expect(mock.answer(1, 2)).to eq(3) - end - - context "Instance Variables and Initializers" do - class MyClass - def initialize(@value : Int32) - end - end - - mock MyClass do - def initialize(@value : Int32 = 0) # Note the lack of `stub` here. - end - end - - it "can create a mock" do - mock = mock(MyClass) - expect(mock).to_not be_nil - end - end - - context "Expecting Behavior" do - abstract class Target - abstract def call(value) : Nil - end - - class Emitter - def initialize(@value : Int32) - end - - def emit(target : Target) - target.call(@value) - end - end - - describe Emitter do - subject { Emitter.new(42) } - - mock Target, call: nil - - describe "#emit" do - it "invokes #call on the target" do - target = mock(Target) - subject.emit(target) - expect(target).to have_received(:call).with(42) - end - end - end - - it "does something" do - mock = mock(MyClass) - allow(mock).to receive(:answer).and_return(42) # Merge this line... - mock.answer - expect(mock).to have_received(:answer) # and this line. - end - - it "does something" do - mock = mock(MyClass) - expect(mock).to receive(:answer).and_return(42) - mock.answer - end - end - - context "Class Mocks" do - class MyClass - def self.something - 0 - end - end - - mock MyClass do - # Define class methods with `self.` prefix. - stub def self.something - 42 - end - end - - it "does something" do - # Default stubs can be defined with key-value pairs (keyword arguments). - mock = class_mock(MyClass, something: 3) - expect(mock.something).to eq(3) - - # Stubs can be changed with `allow`. - allow(mock).to receive(:something).and_return(5) - expect(mock.something).to eq(5) - - # Even the expect-receive syntax works. - expect(mock).to receive(:something).and_return(7) - mock.something - end - end - - context "Mock Modules" do - module MyModule - def something - # ... - end - end - - describe "#something" do - # Define a mock for MyModule. - mock MyClass - - it "does something" do - # Use mock here. - end - end - - module MyFileUtils - def self.rm_rf(path) - # ... - end - end - - mock MyFileUtils - - it "deletes all of my files" do - utils = class_mock(MyFileUtils) - allow(utils).to receive(:rm_rf) - utils.rm_rf("/") - expect(utils).to have_received(:rm_rf).with("/") - end - - module MyFileUtils2 - extend self - - def rm_rf(path) - # ... - end - end - - mock(MyFileUtils2) do - # Define a default stub for the method. - stub def self.rm_rf(path) - # ... - end - end - - it "deletes all of my files part 2" do - utils = class_mock(MyFileUtils2) - allow(utils).to receive(:rm_rf) - utils.rm_rf("/") - expect(utils).to have_received(:rm_rf).with("/") - end - - module Runnable - def run - # ... - end - end - - mock Runnable - - specify do - runnable = mock(Runnable) # or new_mock(Runnable) - runnable.run - end - - module Runnable2 - abstract def command : String - - def run_one - "Running #{command}" - end - end - - mock Runnable2, command: "ls -l" - - specify do - runnable = mock(Runnable2) - expect(runnable.run_one).to eq("Running ls -l") - runnable = mock(Runnable2, command: "echo foo") - expect(runnable.run_one).to eq("Running echo foo") - end - - context "Injecting Mocks" do - module MyFileUtils - def self.rm_rf(path) - true - end - end - - inject_mock MyFileUtils do - stub def self.rm_rf(path) - "Simulating deletion of #{path}" - false - end - end - - specify do - expect(MyFileUtils.rm_rf("/")).to be_false - end - end - end - - context "Injecting Mocks" do - struct MyStruct - def something - 42 - end - - def something_else(arg1, arg2) - "#{arg1} #{arg2}" - end - end - - inject_mock MyStruct, something: 5 do - stub def something_else(arg1, arg2) - "foo bar" - end - end - - specify "creating a mocked type without `mock`" do - inst = MyStruct.new - expect(inst).to receive(:something).and_return(7) - inst.something - end - - it "reverts to default stub for other examples" do - inst = mock(MyStruct) - expect(inst.something).to eq(5) # Default stub used instead of original behavior. - end - end -end diff --git a/spec/docs/null_objects_spec.cr b/spec/docs/null_objects_spec.cr deleted file mode 100644 index 36da434..0000000 --- a/spec/docs/null_objects_spec.cr +++ /dev/null @@ -1,16 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/wikis/Null-Objects -Spectator.describe "Null Objects Docs" do - double :my_double - - it "returns itself for undefined methods" do - dbl = double(:my_double).as_null_object - expect(dbl.some_undefined_method).to be(dbl) - end - - it "can be used to chain methods" do - dbl = double(:my_double).as_null_object - expect(dbl.foo.bar.baz).to be(dbl) - end -end diff --git a/spec/docs/readme_spec.cr b/spec/docs/readme_spec.cr deleted file mode 100644 index 2ed7fd5..0000000 --- a/spec/docs/readme_spec.cr +++ /dev/null @@ -1,36 +0,0 @@ -require "../spec_helper" - -module Readme - abstract class Interface - abstract def invoke(thing) : String - end - - # Type being tested. - class Driver - def do_something(interface : Interface, thing) - interface.invoke(thing) - end - end -end - -Spectator.describe Readme::Driver do - # Define a mock for Interface. - mock Readme::Interface - - # Define a double that the interface will use. - double(:my_double, foo: 42) - - it "does a thing" do - # Create an instance of the mock interface. - interface = mock(Readme::Interface) - # Indicate that `#invoke` should return "test" when called. - allow(interface).to receive(:invoke).and_return("test") - - # Create an instance of the double. - dbl = double(:my_double) - # Call the mock method. - subject.do_something(interface, dbl) - # Verify everything went okay. - expect(interface).to have_received(:invoke).with(dbl) - end -end diff --git a/spec/docs/structure_spec.cr b/spec/docs/structure_spec.cr deleted file mode 100644 index d007bf1..0000000 --- a/spec/docs/structure_spec.cr +++ /dev/null @@ -1,34 +0,0 @@ -require "../spec_helper" - -Spectator.describe String do - let(normal_string) { "foobar" } - let(empty_string) { "" } - - describe "#empty?" do - subject { string.empty? } - - context "when empty" do - let(string) { empty_string } - - it "is true" do - is_expected.to be_true - end - end - - context "when not empty" do - let(string) { normal_string } - - it "is false" do - is_expected.to be_false - end - end - end -end - -Spectator.describe Bytes do - it "stores an array of bytes" do - bytes = Bytes.new(32) - bytes[0] = 42 - expect(bytes[0]).to eq(42) - end -end diff --git a/spec/docs/stubs_spec.cr b/spec/docs/stubs_spec.cr deleted file mode 100644 index fa3a30f..0000000 --- a/spec/docs/stubs_spec.cr +++ /dev/null @@ -1,130 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/wikis/Stubs -Spectator.describe "Stubs Docs" do - double :time_double, time_in: Time.utc(2016, 2, 15, 10, 20, 30) - double :my_double, something: 42, answer?: false - - let(dbl) { double(:my_double) } - - def receive_time_in_utc - receive(:time_in).with(:utc).and_return(Time.utc) - end - - it "returns the time in UTC" do - dbl = double(:time_double) - allow(dbl).to receive_time_in_utc - expect(dbl.time_in(:utc).zone.name).to eq("UTC") - end - - context "Modifiers" do - double :my_double, something: 42, answer?: false - - let(dbl) { double(:my_double) } - - context "and_return" do - specify do - allow(dbl).to receive(:something).and_return(42) - expect(dbl.something).to eq(42) - end - - specify do - allow(dbl).to receive(:something).and_return(1, 2, 3) - expect(dbl.something).to eq(1) - expect(dbl.something).to eq(2) - expect(dbl.something).to eq(3) - expect(dbl.something).to eq(3) - end - end - - context "and_raise" do - specify do - allow(dbl).to receive(:something).and_raise # Raise `Exception` with no message. - expect { dbl.something }.to raise_error(Exception) - - allow(dbl).to receive(:something).and_raise(IO::Error) # Raise `IO::Error` with no message. - expect { dbl.something }.to raise_error(IO::Error) - - allow(dbl).to receive(:something).and_raise(KeyError, "Missing key: :foo") # Raise `KeyError` with the specified message. - expect { dbl.something }.to raise_error(KeyError, "Missing key: :foo") - - exception = ArgumentError.new("Malformed") - allow(dbl).to receive(:something).and_raise(exception) # Raise `exception`. - expect { dbl.something }.to raise_error(ArgumentError, "Malformed") - end - end - - context "with" do - specify do - allow(dbl).to receive(:answer?).and_return(false) - allow(dbl).to receive(:answer?).with(42).and_return(true) - expect(dbl.answer?(42)).to be_true - expect(dbl.answer?(5)).to be_false - end - - specify do - allow(dbl).to receive(:answer?).with(Int, key: /foo/).and_return(true) - expect(dbl.answer?(42, key: "foobar")).to be_true - end - end - end - - context "Expect-Receive Syntax" do - class Driver - def doit(thing) - thing.call - end - end - - describe Driver do - describe "#doit" do - double :thing, call: 5 - - it "calls thing.call (1)" do - thing = double(:thing) - allow(thing).to receive(:call).and_return(42) - subject.doit(thing) - expect(thing).to have_received(:call) - end - - it "calls thing.call (2)" do - thing = double(:thing) - expect(thing).to receive(:call).and_return(42) - subject.doit(thing) - end - - it "calls thing.call (3)" do - thing = double(:thing) - allow(thing).to receive(:call).and_return(42) - expect(thing).to_eventually have_received(:call) - subject.doit(thing) - end - end - end - - specify do - expect(dbl).to receive(:answer?).with(42).and_return(true) - dbl.answer?(42) - end - end - - context "Default Stubs" do - double :my_double, foo: "foo" do # Default stub for #foo - # Default stub for #bar - stub def bar - "bar" - end - end - - it "does something" do - dbl = double(:my_double) - expect(dbl.foo).to eq("foo") - expect(dbl.bar).to eq("bar") - - # Overriding initial defaults. - dbl = double(:my_double, foo: "FOO", bar: "BAR") - expect(dbl.foo).to eq("FOO") - expect(dbl.bar).to eq("BAR") - end - end -end diff --git a/spec/docs/subject_spec.cr b/spec/docs/subject_spec.cr deleted file mode 100644 index 459da35..0000000 --- a/spec/docs/subject_spec.cr +++ /dev/null @@ -1,32 +0,0 @@ -require "../spec_helper" - -Spectator.describe "subject" do - subject(array1) { [1, 2, 3] } - subject(array2) { [4, 5, 6] } - - it "has different elements" do - expect(array1).to_not eq(subject) # array2 would also work here. - end - - let(string) { "foobar" } - - it "isn't empty" do - expect(string.empty?).to be_false - end - - it "is six characters" do - expect(string.size).to eq(6) - end - - let(array) { [0, 1, 2] } - - it "modifies the array" do - array[0] = 42 - expect(array).to eq([42, 1, 2]) - end - - it "doesn't carry across tests" do - array[1] = 777 - expect(array).to eq([0, 777, 2]) - end -end diff --git a/spec/features/aggregate_failures_spec.cr b/spec/features/aggregate_failures_spec.cr deleted file mode 100644 index 8779dd2..0000000 --- a/spec/features/aggregate_failures_spec.cr +++ /dev/null @@ -1,41 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator, :smoke do - describe "aggregate_failures" do - it "captures multiple failed expectations" do - expect do - aggregate_failures do - expect(true).to be_false - expect(false).to be_true - end - end.to raise_error(Spectator::MultipleExpectationsFailed, /2 failures/) - end - - it "raises normally for one failed expectation" do - expect do - aggregate_failures do - expect(true).to be_false - expect(true).to be_true - end - end.to raise_error(Spectator::ExpectationFailed) - end - - it "doesn't raise when there are no failed expectations" do - expect do - aggregate_failures do - expect(false).to be_false - expect(true).to be_true - end - end.to_not raise_error(Spectator::ExpectationFailed) - end - - it "supports naming the block" do - expect do - aggregate_failures "contradiction" do - expect(true).to be_false - expect(false).to be_true - end - end.to raise_error(Spectator::MultipleExpectationsFailed, /contradiction/) - end - end -end diff --git a/spec/features/concise_spec.cr b/spec/features/concise_spec.cr deleted file mode 100644 index 64360a3..0000000 --- a/spec/features/concise_spec.cr +++ /dev/null @@ -1,55 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator, :smoke do - context "consice syntax" do - describe "provided group with a single assignment" do - provided x = 42 do - expect(x).to eq(42) - end - end - - describe "provided group with multiple assignments" do - provided x = 42, y = 123 do - expect(x).to eq(42) - expect(y).to eq(123) - end - end - - describe "provided group with a single named argument" do - provided x: 42 do - expect(x).to eq(42) - end - end - - describe "provided group with multiple named arguments" do - provided x: 42, y: 123 do - expect(x).to eq(42) - expect(y).to eq(123) - end - end - - describe "provided group with mix of assignments and named arguments" do - provided x = 42, y: 123 do - expect(x).to eq(42) - expect(y).to eq(123) - end - - provided x = 42, y = 123, z: 0, foo: "bar" do - expect(x).to eq(42) - expect(y).to eq(123) - expect(z).to eq(0) - expect(foo).to eq("bar") - end - end - - describe "provided group with references to other arguments" do - let(foo) { "bar" } - - provided x = 3, y: x * 5, baz: foo.sub('r', 'z') do - expect(x).to eq(3) - expect(y).to eq(15) - expect(baz).to eq("baz") - end - end - end -end diff --git a/spec/features/custom_message_spec.cr b/spec/features/custom_message_spec.cr deleted file mode 100644 index da7e06c..0000000 --- a/spec/features/custom_message_spec.cr +++ /dev/null @@ -1,31 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator, :smoke do - it "supports custom expectation messages" do - expect do - expect(false).to be_true, "paradox!" - end.to raise_error(Spectator::ExampleFailed, "paradox!") - end - - it "supports custom expectation messages with a proc" do - count = 0 - expect do - expect(false).to be_true, ->{ count += 1; "Failed #{count} times" } - end.to raise_error(Spectator::ExampleFailed, "Failed 1 times") - end - - context "not_to" do - it "supports custom expectation messages" do - expect do - expect(true).not_to be_true, "paradox!" - end.to raise_error(Spectator::ExampleFailed, "paradox!") - end - - it "supports custom expectation messages with a proc" do - count = 0 - expect do - expect(true).not_to be_true, ->{ count += 1; "Failed #{count} times" } - end.to raise_error(Spectator::ExampleFailed, "Failed 1 times") - end - end -end diff --git a/spec/features/expect_type_spec.cr b/spec/features/expect_type_spec.cr deleted file mode 100644 index 6ed9949..0000000 --- a/spec/features/expect_type_spec.cr +++ /dev/null @@ -1,70 +0,0 @@ -require "../spec_helper" - -Spectator.describe "Expect Type", :smoke do - context "with expect syntax" do - it "ensures a type is cast" do - value = 42.as(String | Int32) - expect(value).to be_a(String | Int32) - expect(value).to compile_as(String | Int32) - value = expect(value).to be_a(Int32) - expect(value).to eq(42) - expect(value).to be_a(Int32) - expect(value).to compile_as(Int32) - expect(value).to_not respond_to(:downcase) - end - - it "ensures a type is not nil" do - value = 42.as(Int32?) - expect(value).to be_a(Int32?) - expect(value).to compile_as(Int32?) - value = expect(value).to_not be_nil - expect(value).to eq(42) - expect(value).to be_a(Int32) - expect(value).to compile_as(Int32) - expect { value.not_nil! }.to_not raise_error(NilAssertionError) - end - - it "removes types from a union" do - value = 42.as(String | Int32) - expect(value).to be_a(String | Int32) - expect(value).to compile_as(String | Int32) - value = expect(value).to_not be_a(String) - expect(value).to eq(42) - expect(value).to be_a(Int32) - expect(value).to compile_as(Int32) - expect(value).to_not respond_to(:downcase) - end - end - - context "with should syntax" do - it "ensures a type is cast" do - value = 42.as(String | Int32) - value.should be_a(String | Int32) - value = value.should be_a(Int32) - value.should eq(42) - value.should be_a(Int32) - value.should compile_as(Int32) - value.should_not respond_to(:downcase) - end - - it "ensures a type is not nil" do - value = 42.as(Int32?) - value.should be_a(Int32?) - value = value.should_not be_nil - value.should eq(42) - value.should be_a(Int32) - value.should compile_as(Int32) - expect { value.not_nil! }.to_not raise_error(NilAssertionError) - end - - it "removes types from a union" do - value = 42.as(String | Int32) - value.should be_a(String | Int32) - value = value.should_not be_a(String) - value.should eq(42) - value.should be_a(Int32) - value.should compile_as(Int32) - value.should_not respond_to(:downcase) - end - end -end diff --git a/spec/features/interpolated_label_spec.cr b/spec/features/interpolated_label_spec.cr deleted file mode 100644 index add8e93..0000000 --- a/spec/features/interpolated_label_spec.cr +++ /dev/null @@ -1,22 +0,0 @@ -require "../spec_helper" - -Spectator.describe "Interpolated Label", :smoke do - let(foo) { "example" } - let(bar) { "context" } - - it "interpolates #{foo} labels" do |example| - expect(example.name).to eq("interpolates example labels") - end - - context "within a #{bar}" do - let(foo) { "multiple" } - - it "interpolates context labels" do |example| - expect(example.group.name).to eq("within a context") - end - - it "interpolates #{foo} levels" do |example| - expect(example.name).to eq("interpolates multiple levels") - end - end -end diff --git a/spec/features/line_number_spec.cr b/spec/features/line_number_spec.cr deleted file mode 100644 index c847985..0000000 --- a/spec/features/line_number_spec.cr +++ /dev/null @@ -1,32 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator, :smoke do - let(current_example) { ::Spectator::Example.current } - subject(location) { current_example.location } - - context "line numbers" do - it "contains starting line of spec" do - expect(location.line).to eq(__LINE__ - 1) - end - - it "contains ending line of spec" do - expect(location.end_line).to eq(__LINE__ + 1) - end - - it "handles multiple lines and examples" do - # Offset is important. - expect(location.line).to eq(__LINE__ - 2) - # This line fails, refer to https://github.com/crystal-lang/crystal/issues/10562 - # expect(location.end_line).to eq(__LINE__ + 2) - # Offset is still important. - end - end - - context "file names" do - subject { location.file } - - it "match source code" do - is_expected.to eq(__FILE__) - end - end -end diff --git a/spec/features/metadata_spec.cr b/spec/features/metadata_spec.cr deleted file mode 100644 index 33f3f50..0000000 --- a/spec/features/metadata_spec.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "../spec_helper" - -Spectator.describe "Spec metadata", :smoke do - let(interpolation) { "string interpolation" } - - it "supports #{interpolation}" do |example| - expect(example.name).to eq("supports string interpolation") - end - - def self.various_strings - %w[foo bar baz] - end - - sample various_strings do |string| - it "works with #{string}" do |example| - expect(example.name).to eq("works with #{string}") - end - end - - def self.a_hash - {"foo" => 42, "bar" => 123, "baz" => 7} - end - - sample a_hash do |key, value| - it "works with #{key} = #{value}" do |example| - expect(example.name).to eq("works with #{key} = #{value}") - end - end -end diff --git a/spec/features/subject_spec.cr b/spec/features/subject_spec.cr deleted file mode 100644 index a4380b2..0000000 --- a/spec/features/subject_spec.cr +++ /dev/null @@ -1,23 +0,0 @@ -require "../spec_helper" - -class Base; end - -module SomeModule; end - -Spectator.describe "Subject", :smoke do - subject { Base.new } - - context "nested" do - it "inherits the parent explicit subject" do - expect(subject).to be_a(Base) - end - end - - context "module" do - describe SomeModule do - it "sets the implicit subject to the module" do - expect(subject).to be(SomeModule) - end - end - end -end diff --git a/spec/helpers/.gitkeep b/spec/helpers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/spec/issues/github_issue_28_spec.cr b/spec/issues/github_issue_28_spec.cr deleted file mode 100644 index e6b6924..0000000 --- a/spec/issues/github_issue_28_spec.cr +++ /dev/null @@ -1,17 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #28" do - class Test - def foo - 42 - end - end - - mock Test - - it "matches method stubs with no_args" do - test = mock(Test) - expect(test).to receive(:foo).with(no_args).and_return(42) - test.foo - end -end diff --git a/spec/issues/github_issue_29_spec.cr b/spec/issues/github_issue_29_spec.cr deleted file mode 100644 index 8458783..0000000 --- a/spec/issues/github_issue_29_spec.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #29" do - class SomeClass - def goodbye - exit 0 - end - end - - describe SomeClass do - it "captures exit" do - expect { subject.goodbye }.to raise_error(Spectator::SystemExit) - end - end - - describe "class method" do - class Foo - def self.test - exit 0 - end - end - - subject { Foo } - - it "must capture exit" do - expect { subject.test }.to raise_error(Spectator::SystemExit) - end - end -end diff --git a/spec/issues/github_issue_30_spec.cr b/spec/issues/github_issue_30_spec.cr deleted file mode 100644 index 2720803..0000000 --- a/spec/issues/github_issue_30_spec.cr +++ /dev/null @@ -1,9 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #30" do - let(dbl) { double(:foo) } - - it "supports block-less symbol doubles" do - expect(dbl).to_not be_nil - end -end diff --git a/spec/issues/github_issue_32_spec.cr b/spec/issues/github_issue_32_spec.cr deleted file mode 100644 index 3500cda..0000000 --- a/spec/issues/github_issue_32_spec.cr +++ /dev/null @@ -1,34 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #32" do - module TestFoo - class TestClass - def initialize - end - - # the method we are testing - def self.test - new().test - end - - # the method we want to ensure gets called - def test - end - end - end - - let(test_class) { TestFoo::TestClass } - let(test_instance) { test_class.new } - - describe "something else" do - inject_mock TestFoo::TestClass - - it "must test when new is called" do - expect(test_class).to receive(:new).with(no_args).and_return(test_instance) - expect(test_instance).to receive(:test) - expect(test_class.new).to be(test_instance) - - test_class.test - end - end -end diff --git a/spec/issues/github_issue_33_spec.cr b/spec/issues/github_issue_33_spec.cr deleted file mode 100644 index 656a273..0000000 --- a/spec/issues/github_issue_33_spec.cr +++ /dev/null @@ -1,26 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #33" do - class Test - def method2 - end - - def method1 - method2 - end - end - - mock Test - - describe Test do - subject { mock(Test) } - - describe "#method1" do - it do - expect(subject).to receive(:method2) - - subject.method1 - end - end - end -end diff --git a/spec/issues/github_issue_41_spec.cr b/spec/issues/github_issue_41_spec.cr deleted file mode 100644 index 1b75e40..0000000 --- a/spec/issues/github_issue_41_spec.cr +++ /dev/null @@ -1,30 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #41" do - sample [1, 2, 3] do |i| - it "is itself" do - expect(i).to eq i - end - end - - def self.an_array - [1, 2, 3] - end - - sample an_array do |i| - it "is itself" do - expect(i).to eq(i) - end - end - - # NOTE: NamedTuple does not work, must be Enumerable(T) for `sample`. - def self.a_hash - {:a => "a", :b => "b", :c => "c"} - end - - sample a_hash do |k, v| - it "works on hashes" do - expect(v).to eq(k.to_s) - end - end -end diff --git a/spec/issues/github_issue_42_spec.cr b/spec/issues/github_issue_42_spec.cr deleted file mode 100644 index 58ff2b2..0000000 --- a/spec/issues/github_issue_42_spec.cr +++ /dev/null @@ -1,41 +0,0 @@ -require "../spec_helper" - -abstract class SdkInterface - abstract def register_hook(name, &block) -end - -class Example - def initialize(@sdk : Sdk) - end - - def configure - @sdk.register_hook("name") do - nil - end - end -end - -class Sdk < SdkInterface - def initialize - end - - def register_hook(name, &block) - nil - end -end - -Spectator.describe Example do - mock Sdk - - describe "#configure" do - it "registers a block on configure" do - sdk = mock(Sdk) - example_class = Example.new(sdk) - allow(sdk).to receive(register_hook()) - - example_class.configure - - expect(sdk).to have_received(register_hook()).with("name") - end - end -end diff --git a/spec/issues/github_issue_43_spec.cr b/spec/issues/github_issue_43_spec.cr deleted file mode 100644 index 45e2eb0..0000000 --- a/spec/issues/github_issue_43_spec.cr +++ /dev/null @@ -1,51 +0,0 @@ -require "../spec_helper" - -class Person - def initialize(@dog = Dog.new) - end - - def pet - @dog.pet - end - - def pet_more - @dog.pet(5) - end -end - -class Dog - def initialize - end - - def pet(times = 2) - "woof" * times - end -end - -Spectator.describe Person do - mock Dog - - describe "#pet" do - it "pets the persons dog" do - dog = mock(Dog) - person = Person.new(dog) - allow(dog).to receive(pet()).and_return("woof") - - person.pet - - expect(dog).to have_received(pet()).with(2) - end - end - - describe "#pet_more" do - it "pets the persons dog alot" do - dog = mock(Dog) - person = Person.new(dog) - allow(dog).to receive(pet()).and_return("woof") - - person.pet_more - - expect(dog).to have_received(pet()).with(5) - end - end -end diff --git a/spec/issues/github_issue_44_spec.cr b/spec/issues/github_issue_44_spec.cr deleted file mode 100644 index da6cbf3..0000000 --- a/spec/issues/github_issue_44_spec.cr +++ /dev/null @@ -1,40 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #44" do - inject_mock Process do - # Instance variable that can be nil, provide a default. - @process_info = Crystal::System::Process.new(0) - end - - let(command) { "ls -l" } - let(exception) { File::NotFoundError.new("File not found", file: "test.file") } - - context "with positional arguments" do - before_each do - pipe = Process::Redirect::Pipe - expect(Process).to receive(:run).with(command, nil, nil, false, true, pipe, pipe, pipe, nil).and_raise(exception) - end - - it "must stub Process.run" do - expect do - Process.run(command, shell: true, output: :pipe) do |_process| - end - end.to raise_error(File::NotFoundError, "File not found") - end - end - - # Original issue uses keyword arguments in place of positional arguments. - context "keyword arguments in place of positional arguments" do - before_each do - pipe = Process::Redirect::Pipe - expect(Process).to receive(:run).with(command, shell: true, output: pipe).and_raise(exception) - end - - it "must stub Process.run" do - expect do - Process.run(command, shell: true, output: :pipe) do |_process| - end - end.to raise_error(File::NotFoundError, "File not found") - end - end -end diff --git a/spec/issues/github_issue_47_spec.cr b/spec/issues/github_issue_47_spec.cr deleted file mode 100644 index 3576a2d..0000000 --- a/spec/issues/github_issue_47_spec.cr +++ /dev/null @@ -1,18 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #47" do - class Original - def foo(arg1, arg2) - # ... - end - end - - mock Original - - let(fake) { mock(Original) } - - specify do - expect(fake).to receive(:foo).with("arg1", arg2: "arg2") - fake.foo("arg1", "arg2") - end -end diff --git a/spec/issues/github_issue_48_spec.cr b/spec/issues/github_issue_48_spec.cr deleted file mode 100644 index b958c1b..0000000 --- a/spec/issues/github_issue_48_spec.cr +++ /dev/null @@ -1,135 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #48" do - class Test - def return_this(thing : T) : T forall T - thing - end - - def map(thing : T, & : T -> U) : U forall T, U - yield thing - end - - def make_nilable(thing : T) : T? forall T - thing.as(T?) - end - - def itself : self - self - end - - def itself? : self? - self.as(self?) - end - - def generic(thing : T) : Array(T) forall T - Array.new(100) { thing } - end - - def union : Int32 | String - 42.as(Int32 | String) - end - - def capture(&block : -> T) forall T - block - end - - def capture(thing : T, &block : T -> T) forall T - block.call(thing) - block - end - - def range(r : Range) - r - end - end - - mock Test, make_nilable: nil - - let(fake) { mock(Test) } - - it "handles free variables" do - allow(fake).to receive(:return_this).and_return("different") - expect(fake.return_this("test")).to eq("different") - end - - it "raises on type cast error with free variables" do - allow(fake).to receive(:return_this).and_return(42) - expect { fake.return_this("test") }.to raise_error(TypeCastError, /String/) - end - - it "handles free variables with a block" do - allow(fake).to receive(:map).and_return("stub") - expect(fake.map(:mapped, &.to_s)).to eq("stub") - end - - it "raises on type cast error with a block and free variables" do - allow(fake).to receive(:map).and_return(42) - expect { fake.map(:mapped, &.to_s) }.to raise_error(TypeCastError, /String/) - end - - it "handles nilable free variables" do - expect(fake.make_nilable("foo")).to be_nil - end - - it "handles 'self' return type" do - not_self = mock(Test) - allow(fake).to receive(:itself).and_return(not_self) - expect(fake.itself).to be(not_self) - end - - it "raises on type cast error with 'self' return type" do - allow(fake).to receive(:itself).and_return(42) - expect { fake.itself }.to raise_error(TypeCastError, /#{class_mock(Test)}/) - end - - it "handles nilable 'self' return type" do - not_self = mock(Test) - allow(fake).to receive(:itself?).and_return(not_self) - expect(fake.itself?).to be(not_self) - end - - it "handles generic return type" do - allow(fake).to receive(:generic).and_return([42]) - expect(fake.generic(42)).to eq([42]) - end - - it "raises on type cast error with generic return type" do - allow(fake).to receive(:generic).and_return("test") - expect { fake.generic(42) }.to raise_error(TypeCastError, /Array\(Int32\)/) - end - - it "handles union return types" do - allow(fake).to receive(:union).and_return("test") - expect(fake.union).to eq("test") - end - - it "raises on type cast error with union return type" do - allow(fake).to receive(:union).and_return(:test) - expect { fake.union }.to raise_error(TypeCastError, /Symbol/) - end - - it "handles captured blocks" do - proc = ->{} - allow(fake).to receive(:capture).and_return(proc) - expect(fake.capture { nil }).to be(proc) - end - - it "raises on type cast error with captured blocks" do - proc = ->{ 42 } - allow(fake).to receive(:capture).and_return(proc) - expect { fake.capture { "other" } }.to raise_error(TypeCastError, /Proc\(String\)/) - end - - it "handles captured blocks with arguments" do - proc = ->(x : Int32) { x * 2 } - allow(fake).to receive(:capture).and_return(proc) - expect(fake.capture(5) { 5 }).to be(proc) - end - - it "handles range comparisons against non-comparable types" do - range = 1..10 - allow(fake).to receive(:range).and_return(range) - expect(fake.range(1..3)).to eq(range) - end -end diff --git a/spec/issues/github_issue_49_spec.cr b/spec/issues/github_issue_49_spec.cr deleted file mode 100644 index 6161a57..0000000 --- a/spec/issues/github_issue_49_spec.cr +++ /dev/null @@ -1,6 +0,0 @@ -require "../spec_helper" - -# https://github.com/icy-arctic-fox/spectator/issues/49 -Spectator.describe "GitHub Issue #49" do - # mock File -end diff --git a/spec/issues/github_issue_55_spec.cr b/spec/issues/github_issue_55_spec.cr deleted file mode 100644 index 92c2b42..0000000 --- a/spec/issues/github_issue_55_spec.cr +++ /dev/null @@ -1,48 +0,0 @@ -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/spec/issues/gitlab_issue_51_spec.cr b/spec/issues/gitlab_issue_51_spec.cr deleted file mode 100644 index 996af80..0000000 --- a/spec/issues/gitlab_issue_51_spec.cr +++ /dev/null @@ -1,109 +0,0 @@ -require "../spec_helper" - -module GitLabIssue51 - class Foo - def call(str : String) : String? - "" - end - - def alt1_call(str : String) : String? - nil - end - - def alt2_call(str : String) : String? - [str, nil].sample - end - end - - class Bar - def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call. - a_foo.call("") - a_foo.alt1_call("") - a_foo.alt2_call("") - end - end -end - -Spectator.describe GitLabIssue51::Bar do - mock GitLabIssue51::Foo, call: "", alt1_call: "", alt2_call: "" - - let(:foo) { mock(GitLabIssue51::Foo) } - subject(:call) { described_class.new.call(foo) } - - describe "#call" do - it "invokes Foo#call" do - call - expect(foo).to have_received(:call) - end - - it "invokes Foo#alt1_call" do - call - expect(foo).to have_received(:alt1_call) - end - - it "invokes Foo#alt2_call" do - call - expect(foo).to have_received(:alt2_call) - end - - describe "with an explicit return of nil" do - it "should invoke Foo#call?" do - allow(foo).to receive(:call).and_return(nil) - call - expect(foo).to have_received(:call) - end - - it "invokes Foo#alt1_call" do - allow(foo).to receive(:alt1_call).and_return(nil) - call - expect(foo).to have_received(:alt1_call) - end - - it "invokes Foo#alt2_call" do - allow(foo).to receive(:alt2_call).and_return(nil) - call - expect(foo).to have_received(:alt2_call) - end - end - - describe "with returns set in before_each for all calls" do - before_each do - allow(foo).to receive(:call).and_return(nil) - allow(foo).to receive(:alt1_call).and_return(nil) - allow(foo).to receive(:alt2_call).and_return(nil) - end - - it "should invoke Foo#call?" do - call - expect(foo).to have_received(:call) - end - - it "should invoke Foo#alt1_call?" do - call - expect(foo).to have_received(:alt1_call) - end - - it "should invoke Foo#alt2_call?" do - call - expect(foo).to have_received(:alt2_call) - end - end - - describe "with returns set in before_each for alt calls only" do - before_each do - allow(foo).to receive(:alt1_call).and_return(nil) - allow(foo).to receive(:alt2_call).and_return(nil) - end - - it "invokes Foo#alt1_call" do - call - expect(foo).to have_received(:alt1_call) - end - - it "invokes Foo#alt2_call" do - call - expect(foo).to have_received(:alt2_call) - end - end - end -end diff --git a/spec/issues/gitlab_issue_76_spec.cr b/spec/issues/gitlab_issue_76_spec.cr deleted file mode 100644 index 3427af8..0000000 --- a/spec/issues/gitlab_issue_76_spec.cr +++ /dev/null @@ -1,6 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitLab Issue #76" do - let(:value) { nil.as(Int32?) } - specify { expect(value).to be_nil } -end diff --git a/spec/issues/gitlab_issue_77_spec.cr b/spec/issues/gitlab_issue_77_spec.cr deleted file mode 100644 index f13c1b7..0000000 --- a/spec/issues/gitlab_issue_77_spec.cr +++ /dev/null @@ -1,10 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/issues/77 -Spectator.describe "GitLab Issue #77" do - it "fails" do - expect_raises do - raise "Error!" - end - end -end diff --git a/spec/issues/gitlab_issue_80_spec.cr b/spec/issues/gitlab_issue_80_spec.cr deleted file mode 100644 index 9090130..0000000 --- a/spec/issues/gitlab_issue_80_spec.cr +++ /dev/null @@ -1,30 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/issues/80 - -class Item -end - -class ItemUser - @item = Item.new - - def item - @item - end -end - -Spectator.describe "test1" do - it "without mock" do - item_user = ItemUser.new - item = item_user.item - item == item - end -end - -Spectator.describe "test2" do - mock Item do - end - - it "without mock" do - end -end diff --git a/spec/matchers/equality_matcher_spec.cr b/spec/matchers/equality_matcher_spec.cr deleted file mode 100644 index 8e8a35f..0000000 --- a/spec/matchers/equality_matcher_spec.cr +++ /dev/null @@ -1,34 +0,0 @@ -require "../spec_helper" - -Spectator.describe "eq matcher" do - it "is true for equal values" do - expect(42).to eq(42) - end - - it "is false for unequal values" do - expect(42).to_not eq(24) - end - - it "is true for identical references" do - string = "foobar" - expect(string).to eq(string) - end - - it "is false for different references" do - string1 = "foo" - string2 = "bar" - expect(string1).to_not eq(string2) - end - - double(:fake) do - stub def ==(other) - true - end - end - - it "uses the == operator" do - dbl = double(:fake) - expect(42).to eq(dbl) - expect(dbl).to have_received(:==).with(42) - end -end diff --git a/spec/matchers/receive_matcher_spec.cr b/spec/matchers/receive_matcher_spec.cr deleted file mode 100644 index b5addc5..0000000 --- a/spec/matchers/receive_matcher_spec.cr +++ /dev/null @@ -1,515 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator::Matchers::ReceiveMatcher do - let(stub) { Spectator::NullStub.new(:test_method) } - subject(matcher) { described_class.new(stub) } - - let(args) { Spectator::Arguments.capture(1, "test", Symbol, foo: /bar/) } - let(args_stub) { Spectator::NullStub.new(:test_method, args) } - let(args_matcher) { described_class.new(args_stub) } - - let(no_args_stub) { Spectator::NullStub.new(:test_method, Spectator::Arguments.none) } - let(no_args_matcher) { described_class.new(no_args_stub) } - - double(:dbl, test_method: nil, irrelevant: nil) - let(dbl) { double(:dbl) } - let(actual) { Spectator::Value.new(dbl, "dbl") } - - def successful_match - Spectator::Matchers::SuccessfulMatchData - end - - def failed_match - Spectator::Matchers::FailedMatchData - end - - describe "#description" do - subject { matcher.description } - - it "includes the method name" do - is_expected.to contain("test_method") - end - - context "without an argument constraint" do - it "mentions it accepts any arguments" do - is_expected.to contain("any args") - end - end - - context "with no arguments" do - let(matcher) { no_args_matcher } - - it "mentions there are none" do - is_expected.to contain("no args") - end - end - - context "with arguments" do - let(matcher) { args_matcher } - - it "lists the arguments" do - is_expected.to contain("1, \"test\", Symbol, foo: #{/bar/.inspect}") - end - end - end - - describe "#with" do - subject { matcher.with(1, 2, 3, bar: /baz/) } - - it "applies a constraint on arguments" do - dbl.test_method - expect(&.match(actual)).to be_a(failed_match) - dbl.test_method(1, 2, 3, bar: "foobarbaz") - expect(&.match(actual)).to be_a(successful_match) - end - end - - describe "#match" do - subject(match_data) { matcher.match(actual) } - - post_condition { expect(match_data.description).to contain("dbl received #test_method") } - - let(failure_message) { match_data.as(Spectator::Matchers::FailedMatchData).failure_message } - - context "with no argument constraint" do - post_condition { expect(&.description).to contain("(any args)") } - - it "matches with no arguments" do - dbl.test_method - is_expected.to be_a(successful_match) - end - - it "matches with any arguments" do - dbl.test_method("foo") - is_expected.to be_a(successful_match) - end - - it "doesn't match with no calls" do - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl did not receive #test_method(any args)") - end - - it "doesn't match with different calls" do - dbl.irrelevant("foo") - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl did not receive #test_method(any args)") - end - end - - context "with a \"no arguments\" constraint" do - let(matcher) { no_args_matcher } - - post_condition { expect(&.description).to contain("(no args)") } - - it "matches with no arguments" do - dbl.test_method - is_expected.to be_a(successful_match) - end - - it "doesn't match with arguments" do - dbl.test_method("foo") - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl did not receive #test_method(no args)") - end - - it "doesn't match with no calls" do - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl did not receive #test_method(no args)") - end - - it "doesn't match with different calls" do - dbl.irrelevant("foo") - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl did not receive #test_method(no args)") - end - end - - context "with an arguments constraint" do - let(matcher) { args_matcher } - - post_condition { expect(&.description).to contain("(1, \"test\", Symbol, foo: #{/bar/.inspect})") } - - it "doesn't match with no arguments" do - dbl.test_method - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl did not receive #test_method(1, \"test\", Symbol, foo: #{/bar/.inspect})") - end - - it "matches with matching arguments" do - dbl.test_method(1, "test", :xyz, foo: "foobarbaz") - is_expected.to be_a(successful_match) - end - - it "doesn't match with differing arguments" do - dbl.test_method(1, "wrong", 42, foo: "wrong") - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl did not receive #test_method(1, \"test\", Symbol, foo: #{/bar/.inspect})") - end - - it "doesn't match with no calls" do - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl did not receive #test_method(1, \"test\", Symbol, foo: #{/bar/.inspect})") - end - - it "doesn't match with different calls" do - dbl.irrelevant("foo") - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl did not receive #test_method(1, \"test\", Symbol, foo: #{/bar/.inspect})") - end - end - - describe "the match data values" do - let(matcher) { args_matcher } - subject(values) { match_data.as(Spectator::Matchers::FailedMatchData).values } - - pre_condition { expect(match_data).to be_a(failed_match) } - - it "has the expected call listed" do - is_expected.to contain({:expected, "#test_method(1, \"test\", Symbol, foo: #{/bar/.inspect})"}) - end - - context "with method calls" do - before do - dbl.test_method - dbl.test_method(1, "wrong", :xyz, foo: "foobarbaz") - dbl.irrelevant("foo") - end - - it "has the list of called methods" do - is_expected.to contain({ - :actual, - <<-SIGNATURES - #test_method(no args) - #test_method(1, "wrong", :xyz, foo: "foobarbaz") - #irrelevant("foo") - SIGNATURES - }) - end - end - - context "with no method calls" do - it "reports \"None\" for the actual value" do - is_expected.to contain({:actual, "None"}) - end - end - end - end - - describe "#negated_match" do - subject(match_data) { matcher.negated_match(actual) } - - post_condition { expect(match_data.description).to contain("dbl did not receive #test_method") } - - let(failure_message) { match_data.as(Spectator::Matchers::FailedMatchData).failure_message } - - context "with no argument constraint" do - post_condition { expect(&.description).to contain("(any args)") } - - it "doesn't match with no arguments" do - dbl.test_method - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl received #test_method(any args)") - end - - it "doesn't match with any arguments" do - dbl.test_method("foo") - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl received #test_method(any args)") - end - - it "matches with no calls" do - is_expected.to be_a(successful_match) - end - - it "matches with different calls" do - dbl.irrelevant("foo") - is_expected.to be_a(successful_match) - end - end - - context "with a \"no arguments\" constraint" do - let(matcher) { no_args_matcher } - - post_condition { expect(&.description).to contain("(no args)") } - - it "doesn't match with no arguments" do - dbl.test_method - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl received #test_method(no args)") - end - - it "matches with arguments" do - dbl.test_method("foo") - is_expected.to be_a(successful_match) - end - - it "matches with no calls" do - is_expected.to be_a(successful_match) - end - - it "matches with different calls" do - dbl.irrelevant("foo") - is_expected.to be_a(successful_match) - end - end - - context "with an arguments constraint" do - let(matcher) { args_matcher } - - post_condition { expect(&.description).to contain("(1, \"test\", Symbol, foo: #{/bar/.inspect})") } - - it "matches with no arguments" do - dbl.test_method - is_expected.to be_a(successful_match) - end - - it "doesn't match with matching arguments" do - dbl.test_method(1, "test", :xyz, foo: "foobarbaz") - is_expected.to be_a(failed_match) - expect(failure_message).to eq("dbl received #test_method(1, \"test\", Symbol, foo: #{/bar/.inspect})") - end - - it "matches with differing arguments" do - dbl.test_method(1, "wrong", 42, foo: "wrong") - is_expected.to be_a(successful_match) - end - - it "matches with no calls" do - is_expected.to be_a(successful_match) - end - - it "matches with different calls" do - dbl.irrelevant("foo") - is_expected.to be_a(successful_match) - end - end - - describe "the match data values" do - subject(values) { match_data.as(Spectator::Matchers::FailedMatchData).values } - - pre_condition { expect(match_data).to be_a(failed_match) } - - before do - dbl.test_method - dbl.test_method(1, "test", :xyz, foo: "foobarbaz") - dbl.irrelevant("foo") - end - - it "has the expected call listed" do - is_expected.to contain({:expected, "Not #{stub.message}"}) - end - - it "has the list of called methods" do - is_expected.to contain({ - :actual, - <<-SIGNATURES - #test_method(no args) - #test_method(1, "test", :xyz, foo: "foobarbaz") - #irrelevant("foo") - SIGNATURES - }) - end - end - end - - describe "#once" do - let(matcher) { super.once } - subject(match_data) { matcher.match(actual) } - - it "matches when the stub is called once" do - dbl.test_method - is_expected.to be_a(successful_match) - end - - it "doesn't match when the stub isn't called" do - is_expected.to be_a(failed_match) - end - - it "doesn't match when the stub is called twice" do - 2.times { dbl.test_method } - is_expected.to be_a(failed_match) - end - end - - describe "#twice" do - let(matcher) { super.twice } - subject(match_data) { matcher.match(actual) } - - it "matches when the stub is called twice" do - 2.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - - it "doesn't match when the stub isn't called" do - is_expected.to be_a(failed_match) - end - - it "doesn't match when the stub is called once" do - dbl.test_method - is_expected.to be_a(failed_match) - end - - it "doesn't match when the stub is called thrice" do - 3.times { dbl.test_method } - is_expected.to be_a(failed_match) - end - end - - describe "#exactly" do - let(matcher) { super.exactly(3) } - subject(match_data) { matcher.match(actual) } - - it "matches when the stub is called the exact amount" do - 3.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - - it "doesn't match when the stub isn't called" do - is_expected.to be_a(failed_match) - end - - it "doesn't match when the stub is called less than the amount" do - 2.times { dbl.test_method } - is_expected.to be_a(failed_match) - end - - it "doesn't match when the stub is called more than the amount" do - 4.times { dbl.test_method } - is_expected.to be_a(failed_match) - end - end - - describe "#at_least" do - let(matcher) { super.at_least(3) } - subject(match_data) { matcher.match(actual) } - - it "matches when the stub is called the exact amount" do - 3.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - - it "doesn't match when the stub isn't called" do - is_expected.to be_a(failed_match) - end - - it "doesn't match when the stub is called less than the amount" do - 2.times { dbl.test_method } - is_expected.to be_a(failed_match) - end - - it "matches when the stub is called more than the amount" do - 4.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - end - - describe "#at_most" do - let(matcher) { super.at_most(3) } - subject(match_data) { matcher.match(actual) } - - it "matches when the stub is called the exact amount" do - 3.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - - it "matches when the stub isn't called" do - is_expected.to be_a(successful_match) - end - - it "matches when the stub is called less than the amount" do - 2.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - - it "doesn't match when the stub is called more than the amount" do - 4.times { dbl.test_method } - is_expected.to be_a(failed_match) - end - end - - describe "#at_least_once" do - let(matcher) { super.at_least_once } - subject(match_data) { matcher.match(actual) } - - it "matches when the stub is called once" do - dbl.test_method - is_expected.to be_a(successful_match) - end - - it "doesn't match when the stub isn't called" do - is_expected.to be_a(failed_match) - end - - it "matches when the stub is called more than once" do - 2.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - end - - describe "#at_least_twice" do - let(matcher) { super.at_least_twice } - subject(match_data) { matcher.match(actual) } - - it "doesn't match when the stub is called once" do - dbl.test_method - is_expected.to be_a(failed_match) - end - - it "doesn't match when the stub isn't called" do - is_expected.to be_a(failed_match) - end - - it "matches when the stub is called twice" do - 2.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - - it "matches when the stub is called more than twice" do - 3.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - end - - describe "#at_most_once" do - let(matcher) { super.at_most_once } - subject(match_data) { matcher.match(actual) } - - it "matches when the stub is called once" do - dbl.test_method - is_expected.to be_a(successful_match) - end - - it "matches when the stub isn't called" do - is_expected.to be_a(successful_match) - end - - it "doesn't match when the stub is called more than once" do - 2.times { dbl.test_method } - is_expected.to be_a(failed_match) - end - end - - describe "#at_most_twice" do - let(matcher) { super.at_most_twice } - subject(match_data) { matcher.match(actual) } - - it "matches when the stub is called once" do - dbl.test_method - is_expected.to be_a(successful_match) - end - - it "matches when the stub isn't called" do - is_expected.to be_a(successful_match) - end - - it "matches when the stub is called twice" do - 2.times { dbl.test_method } - is_expected.to be_a(successful_match) - end - - it "doesn't match when the stub is called more than twice" do - 3.times { dbl.test_method } - is_expected.to be_a(failed_match) - end - end -end diff --git a/spec/matchers/type_matcher_spec.cr b/spec/matchers/type_matcher_spec.cr deleted file mode 100644 index 22d218e..0000000 --- a/spec/matchers/type_matcher_spec.cr +++ /dev/null @@ -1,26 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator::Matchers::TypeMatcher do - context String do # Sets `described_class` to String - def other_type - Int32 - end - - describe "#|" do - it "works on sets" do - super_set = (described_class | other_type) - - expect(42).to be_kind_of(super_set) - expect("foo").to be_a(super_set) - end - end - - it "works on described_class" do - expect("foo").to be_a_kind_of(described_class) - end - - it "works on plain types" do - expect(42).to be_a(Int32) - end - end -end diff --git a/spec/rspec/core/before_and_after_hooks_spec.cr b/spec/rspec/core/before_and_after_hooks_spec.cr deleted file mode 100644 index 6bebe4d..0000000 --- a/spec/rspec/core/before_and_after_hooks_spec.cr +++ /dev/null @@ -1,112 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-core/v/3-8/docs/hooks/before-and-after-hooks -# and modified to fit Spectator and Crystal. -Spectator.describe "`before` and `after` hooks" do - context "Define `before_each` block" do - class Thing - def widgets - @widgets ||= [] of Symbol # Must specify array element type. - end - end - - describe Thing do - before_each do - @thing = Thing.new - end - - describe "initialize in before_each" do - it "has 0 widgets" do - widgets = @thing.as(Thing).widgets # Must cast since compile type is Thing? - expect(widgets.size).to eq(0) # Use size instead of count. - end - - it "can accept new widgets" do - widgets = @thing.as(Thing).widgets # Must cast since compile type is Thing? - widgets << :foo - end - - it "does not share state across examples" do - widgets = @thing.as(Thing).widgets # Must cast since compile type is Thing? - expect(widgets.size).to eq(0) # Use size instead of count. - end - end - end - end - - context "Define `before_all` block in example group" do - class Thing - def widgets - @widgets ||= [] of Symbol # Must specify array element type. - end - end - - describe Thing do - # Moved before_all into the same example group. - # Unlike Ruby, inherited class variables don't share the same value. - # See: https://crystal-lang.org/reference/syntax_and_semantics/class_variables.html - describe "initialized in before_all" do - @@thing : Thing? - - before_all do - @@thing = Thing.new # Must use class variables. - end - - it "has 0 widgets" do - widgets = @@thing.as(Thing).widgets # Must cast since compile type is Thing? - expect(widgets.size).to eq(0) # Use size instead of count. - end - - it "can accept new widgets" do - widgets = @@thing.as(Thing).widgets # Must cast since compile type is Thing? - widgets << :foo - end - - it "shares state across examples" do - widgets = @@thing.as(Thing).widgets # Must cast since compile type is Thing? - expect(widgets.size).to eq(1) # Use size instead of count. - end - end - end - end - - context "Failure in `before_each` block" do - # TODO - end - - context "Failure in `after_each` block" do - # TODO - end - - context "Define `before` and `after` blocks in configuration" do - # TODO - end - - context "`before`/`after` blocks are run in order" do - # Examples changed from using puts to appending to an array. - describe "before and after callbacks" do - @@order = [] of Symbol - - before_all do - @@order << :before_all - end - - before_each do - @@order << :before_each - end - - after_each do - @@order << :after_each - end - - after_all do - @@order << :after_all - end - - it "gets run in order" do - expect(@@order).to_eventually eq(%i[before_all before_each after_each after_all]) - end - end - end -end diff --git a/spec/rspec/core/explicit_subject_spec.cr b/spec/rspec/core/explicit_subject_spec.cr deleted file mode 100644 index 075bad7..0000000 --- a/spec/rspec/core/explicit_subject_spec.cr +++ /dev/null @@ -1,138 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-core/v/3-8/docs/subject/explicit-subject -# and modified to fit Spectator and Crystal. -Spectator.describe "Explicit Subject" do - context "A `subject` can be defined and used in the top level group scope" do - describe Array(Int32) do # TODO: Multiple arguments to describe/context. - subject { [1, 2, 3] } - - it "has the prescribed elements" do - expect(subject).to eq([1, 2, 3]) - end - end - end - - context "The `subject` define in an outer group is available to inner groups" do - describe Array(Int32) do - subject { [1, 2, 3] } - - describe "has some elements" do - it "which are the prescribed elements" do - expect(subject).to eq([1, 2, 3]) - end - end - end - end - - context "The `subject` is memoized within an example but not across examples" do - describe Array(Int32) do - # Changed to class variable to get around compiler error/crash. - # Unhandled exception: Negative argument (ArgumentError) - @@element_list = [1, 2, 3] - - subject { @@element_list.pop } - - it "is memoized across calls (i.e. the block is invoked once)" do - expect do - 3.times { subject } - end.to change { @@element_list }.from([1, 2, 3]).to([1, 2]) - expect(subject).to eq(3) - end - - it "is not memoized across examples" do - expect { subject }.to change { @@element_list }.from([1, 2]).to([1]) - expect(subject).to eq(2) - end - end - end - - context "The `subject` is available in `before` blocks" do - describe Array(Int32) do # TODO: Multiple arguments to describe/context. - subject { [] of Int32 } - - before { subject.push(1, 2, 3) } - - it "has the prescribed elements" do - expect(subject).to eq([1, 2, 3]) - end - end - end - - context "Helper methods can be invoked from a `subject` definition block" do - describe Array(Int32) do # TODO: Multiple arguments to describe/context. - def prepared_array - [1, 2, 3] - end - - subject { prepared_array } - - it "has the prescribed elements" do - expect(subject).to eq([1, 2, 3]) - end - end - end - - context "Use the `subject!` bang method to call the definition block before the example" do - describe "eager loading with subject!" do - subject! { element_list.push(99) } - - let(:element_list) { [1, 2, 3] } - - it "calls the definition block before the example" do - element_list.push(5) - expect(element_list).to eq([1, 2, 3, 99, 5]) - end - end - end - - context "Use `subject(:name)` to define a memoized helper method" do - # Globals not supported, using class variable instead. - @@count = 0 - - describe "named subject" do - subject(:global_count) { @@count += 1 } - - it "is memoized across calls (i.e. the block is invoked once)" do - expect do - 2.times { global_count } - end.not_to change { global_count }.from(1) - end - - it "is not cached across examples" do - expect(global_count).to eq(2) - end - - it "is still available using the subject method" do - expect(subject).to eq(3) - end - - it "works with the one-liner syntax" do - is_expected.to eq(4) - end - - it "the subject and named helpers return the same object" do - expect(global_count).to be(subject) - end - - it "is set to the block return value (i.e. the global $count)" do - expect(global_count).to be(@@count) - end - end - end - - context "Use `subject!(:name)` to define a helper method called before the example" do - describe "eager loading using a named subject!" do - subject!(:updated_list) { element_list.push(99) } - - let(:element_list) { [1, 2, 3] } - - it "calls the definition block before the example" do - element_list.push(5) - expect(element_list).to eq([1, 2, 3, 99, 5]) - expect(updated_list).to be(element_list) - end - end - end -end diff --git a/spec/rspec/core/helper_methods_spec.cr b/spec/rspec/core/helper_methods_spec.cr deleted file mode 100644 index 3fc6802..0000000 --- a/spec/rspec/core/helper_methods_spec.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "../../spec_helper" - -Spectator.describe "Arbitrary helper methods" do - context "Use a method define in the same group" do - describe "an example" do - def help - :available - end - - it "has access to methods define in its group" do - expect(help).to be(:available) - end - end - end - - context "Use a method defined in a parent group" do - describe "an example" do - def help - :available - end - - describe "in a nested group" do - it "has access to methods defined in its parent group" do - expect(help).to be(:available) - end - end - end - end -end diff --git a/spec/rspec/core/implicit_subject_spec.cr b/spec/rspec/core/implicit_subject_spec.cr deleted file mode 100644 index f6e1d40..0000000 --- a/spec/rspec/core/implicit_subject_spec.cr +++ /dev/null @@ -1,43 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-core/v/3-8/docs/subject/implicitly-defined-subject -# and modified to fit Spectator and Crystal. -Spectator.describe "Implicitly defined subject" do - context "`subject` exposed in top-level group" do - describe Array(String) do - it "should be empty when first created" do - expect(subject).to be_empty - end - end - end - - context "`subject` in a nested group" do - describe Array(String) do - describe "when first created" do - it "should be empty" do - expect(subject).to be_empty - end - end - end - end - - context "`subject` in a nested group with a different class (innermost wins)" do - class ArrayWithOneElement < Array(String) - def initialize(*_args) - super - unshift "first element" - end - end - - describe Array(String) do - describe ArrayWithOneElement do - context "referenced as subject" do - it "contains one element" do - expect(subject).to contain("first element") - end - end - end - end - end -end diff --git a/spec/rspec/core/let_spec.cr b/spec/rspec/core/let_spec.cr deleted file mode 100644 index a711fa9..0000000 --- a/spec/rspec/core/let_spec.cr +++ /dev/null @@ -1,45 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-core/v/3-8/docs/helper-methods/let-and-let -# and modified to fit Spectator and Crystal. -Spectator.describe "Let and let!" do - context "Use `let` to define memoized helper method" do - # Globals aren't supported, use class variables instead. - @@count = 0 - - describe "let" do - let(:count) { @@count += 1 } - - it "memoizes the value" do - expect(count).to eq(1) - expect(count).to eq(1) - end - - it "is not cached across examples" do - expect(count).to eq(2) - end - end - end - - context "Use `let!` to define a memoized helper method that is called in a `before` hook" do - # Globals aren't supported, use class variables instead. - @@count = 0 - - describe "let!" do - # Use class variable here. - @@invocation_order = [] of Symbol - - let!(:count) do - @@invocation_order << :let! - @@count += 1 - end - - it "calls the helper method in a before hook" do - @@invocation_order << :example - expect(@@invocation_order).to eq([:let!, :example]) - expect(count).to eq(1) - end - end - end -end diff --git a/spec/rspec/core/one_liner_subject_spec.cr b/spec/rspec/core/one_liner_subject_spec.cr deleted file mode 100644 index c1e124d..0000000 --- a/spec/rspec/core/one_liner_subject_spec.cr +++ /dev/null @@ -1,31 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-core/v/3-8/docs/subject/one-liner-syntax -# and modified to fit Spectator and Crystal. -Spectator.describe "One-liner syntax" do - context "Implicit subject" do - describe Array(Int32) do - # Rather than: - # it "should be empty" do - # subject.should be_empty - # end - - it { should be_empty } - # or - it { is_expected.to be_empty } - end - end - - context "Explicit subject" do - describe Array(Int32) do - describe "with 3 items" do - subject { [1, 2, 3] } - - it { should_not be_empty } - # or - it { is_expected.not_to be_empty } - end - end - end -end diff --git a/spec/rspec/expectations/all_matcher_spec.cr b/spec/rspec/expectations/all_matcher_spec.cr deleted file mode 100644 index 92d31d3..0000000 --- a/spec/rspec/expectations/all_matcher_spec.cr +++ /dev/null @@ -1,35 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/all-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`all` matcher" do - context "array usage" do - describe [1, 3, 5] do - it { is_expected.to all(be_odd) } - it { is_expected.to all(be_an(Int32)) } # Changed to Int32 to satisfy compiler. - it { is_expected.to all(be < 10) } - - # deliberate failures - it_fails { is_expected.to all(be_even) } - it_fails { is_expected.to all(be_a(String)) } - it_fails { is_expected.to all(be > 2) } - end - end - - context "compound matcher usage" do - # Changed `include` to `contain` to match our own. - # `include` is a keyword and can't be used as a method name in Crystal. - - describe ["anything", "everything", "something"] do - skip reason: "Add support for compound matchers." { is_expected.to all(be_a(String).and contain("thing")) } - skip reason: "Add support for compound matchers." { is_expected.to all(be_a(String).and end_with("g")) } - skip reason: "Add support for compound matchers." { is_expected.to all(start_with("s").or contain("y")) } - - # deliberate failures - skip reason: "Add support for compound matchers." { is_expected.to all(contain("foo").and contain("bar")) } - skip reason: "Add support for compound matchers." { is_expected.to all(be_a(String).and start_with("a")) } - skip reason: "Add support for compound matchers." { is_expected.to all(start_with("a").or contain("z")) } - end - end -end diff --git a/spec/rspec/expectations/be_between_matcher_spec.cr b/spec/rspec/expectations/be_between_matcher_spec.cr deleted file mode 100644 index 6a840e5..0000000 --- a/spec/rspec/expectations/be_between_matcher_spec.cr +++ /dev/null @@ -1,17 +0,0 @@ -require "../../spec_helper" - -Spectator.describe "`be_between` matcher" do - context "basic usage" do - describe 7 do - it { is_expected.to be_between(1, 10) } - it { is_expected.to be_between(0.2, 27.1) } - it { is_expected.not_to be_between(1.5, 4) } - it { is_expected.not_to be_between(8, 9) } - - # boundaries check - it { is_expected.to be_between(0, 7) } - it { is_expected.to be_between(7, 10) } - it { is_expected.not_to(be_between(0, 7).exclusive) } - end - end -end diff --git a/spec/rspec/expectations/be_matchers_spec.cr b/spec/rspec/expectations/be_matchers_spec.cr deleted file mode 100644 index 1e4ee63..0000000 --- a/spec/rspec/expectations/be_matchers_spec.cr +++ /dev/null @@ -1,66 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/be-matchers -# and modified to fit Spectator and Crystal. -Spectator.describe "`be` matchers" do - context "be_truthy matcher" do - specify { expect(true).to be_truthy } - specify { expect(7).to be_truthy } - specify { expect("foo").to be_truthy } - specify { expect(nil).not_to be_truthy } - specify { expect(false).not_to be_truthy } - - # deliberate failures - specify_fails { expect(true).not_to be_truthy } - specify_fails { expect(7).not_to be_truthy } - specify_fails { expect("foo").not_to be_truthy } - specify_fails { expect(nil).to be_truthy } - specify_fails { expect(false).to be_truthy } - end - - context "be_falsey matcher" do - specify { expect(nil).to be_falsey } - specify { expect(false).to be_falsey } - specify { expect(true).not_to be_falsey } - specify { expect(7).not_to be_falsey } - specify { expect("foo").not_to be_falsey } - - # deliberate failures - specify_fails { expect(nil).not_to be_falsey } - specify_fails { expect(false).not_to be_falsey } - specify_fails { expect(true).to be_falsey } - specify_fails { expect(7).to be_falsey } - specify_fails { expect("foo").to be_falsey } - end - - context "be_nil matcher" do - specify { expect(nil).to be_nil } - specify { expect(false).not_to be_nil } - specify { expect(true).not_to be_nil } - specify { expect(7).not_to be_nil } - specify { expect("foo").not_to be_nil } - - # deliberate failures - specify_fails { expect(nil).not_to be_nil } - specify_fails { expect(false).to be_nil } - specify_fails { expect(true).to be_nil } - specify_fails { expect(7).to be_nil } - specify_fails { expect("foo").to be_nil } - end - - context "be matcher" do - specify { expect(true).to be } - specify { expect(7).to be } - specify { expect("foo").to be } - specify { expect(nil).not_to be } - specify { expect(false).not_to be } - - # deliberate failures - specify_fails { expect(true).not_to be } - specify_fails { expect(7).not_to be } - specify_fails { expect("foo").not_to be } - specify_fails { expect(nil).to be } - specify_fails { expect(false).to be } - end -end diff --git a/spec/rspec/expectations/be_within_matcher_spec.cr b/spec/rspec/expectations/be_within_matcher_spec.cr deleted file mode 100644 index 2a99b51..0000000 --- a/spec/rspec/expectations/be_within_matcher_spec.cr +++ /dev/null @@ -1,24 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/be-within-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`be_within` matcher" do - context "basic usage" do - describe 27.5 do - it { is_expected.to be_within(0.5).of(27.9) } - it { is_expected.to be_within(0.5).of(28.0) } - it { is_expected.to be_within(0.5).of(27.1) } - it { is_expected.to be_within(0.5).of(27.0) } - - it { is_expected.not_to be_within(0.5).of(28.1) } - it { is_expected.not_to be_within(0.5).of(26.9) } - - # deliberate failures - it_fails { is_expected.not_to be_within(0.5).of(28) } - it_fails { is_expected.not_to be_within(0.5).of(27) } - it_fails { is_expected.to be_within(0.5).of(28.1) } - it_fails { is_expected.to be_within(0.5).of(26.9) } - end - end -end diff --git a/spec/rspec/expectations/change_matcher_spec.cr b/spec/rspec/expectations/change_matcher_spec.cr deleted file mode 100644 index 174ca57..0000000 --- a/spec/rspec/expectations/change_matcher_spec.cr +++ /dev/null @@ -1,47 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/change-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`change` matcher" do - # Modified this example type to work in Crystal. - module Counter - extend self - - @@count = 0 - - def increment - @@count += 1 - end - - def count - @@count - end - end - - context "expect change" do - describe "Counter#increment" do # TODO: Allow multiple arguments to context/describe. - it "should increment the count" do - expect { Counter.increment }.to change { Counter.count }.from(0).to(1) - end - - # deliberate failure - it_fails "should increment the count by 2" do - expect { Counter.increment }.to change { Counter.count }.by(2) - end - end - end - - context "expect no change" do - describe "Counter#increment" do # TODO: Allow multiple arguments to context/describe. - # deliberate failures - it_fails "should not increment the count by 1 (using not_to)" do - expect { Counter.increment }.not_to change { Counter.count } - end - - it_fails "should not increment the count by 1 (using to_not)" do - expect { Counter.increment }.to_not change { Counter.count } - end - end - end -end diff --git a/spec/rspec/expectations/comparison_matchers_spec.cr b/spec/rspec/expectations/comparison_matchers_spec.cr deleted file mode 100644 index bb201d7..0000000 --- a/spec/rspec/expectations/comparison_matchers_spec.cr +++ /dev/null @@ -1,44 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/comparison-matchers -# and modified to fit Spectator and Crystal. -Spectator.describe "Comparison matchers" do - context "numeric operator matchers" do - describe 18 do - it { is_expected.to be < 20 } - it { is_expected.to be > 15 } - it { is_expected.to be <= 19 } - it { is_expected.to be >= 17 } - - # deliberate failures - it_fails { is_expected.to be < 15 } - it_fails { is_expected.to be > 20 } - it_fails { is_expected.to be <= 17 } - it_fails { is_expected.to be >= 19 } - # it { is_expected.to be < 'a' } # Removed because Crystal doesn't support Int32#<(Char) - end - - describe 'a' do - it { is_expected.to be < 'b' } - - # deliberate failures - # it { is_expected.to be < 18 } # Removed because Crystal doesn't support Char#<(Int32) - end - end - - context "string operator matchers" do - describe "Strawberry" do - it { is_expected.to be < "Tomato" } - it { is_expected.to be > "Apple" } - it { is_expected.to be <= "Turnip" } - it { is_expected.to be >= "Banana" } - - # deliberate failures - it_fails { is_expected.to be < "Cranberry" } - it_fails { is_expected.to be > "Zuchini" } - it_fails { is_expected.to be <= "Potato" } - it_fails { is_expected.to be >= "Tomato" } - end - end -end diff --git a/spec/rspec/expectations/contain_exactly_matcher_spec.cr b/spec/rspec/expectations/contain_exactly_matcher_spec.cr deleted file mode 100644 index c4d7d3c..0000000 --- a/spec/rspec/expectations/contain_exactly_matcher_spec.cr +++ /dev/null @@ -1,30 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/contain-exactly-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`contain_exactly` matcher" do - context "Array is expected to contain every value" do - describe [1, 2, 3] do - it { is_expected.to contain_exactly(1, 2, 3) } - it { is_expected.to contain_exactly(1, 3, 2) } - it { is_expected.to contain_exactly(2, 1, 3) } - it { is_expected.to contain_exactly(2, 3, 1) } - it { is_expected.to contain_exactly(3, 1, 2) } - it { is_expected.to contain_exactly(3, 2, 1) } - - # deliberate failures - it_fails { is_expected.to contain_exactly(1, 2, 1) } - end - end - - context "Array is not expected to contain every value" do - describe [1, 2, 3] do - it { is_expected.to_not contain_exactly(1, 2, 3, 4) } - it { is_expected.to_not contain_exactly(1, 2) } - - # deliberate failures - it_fails { is_expected.to_not contain_exactly(1, 3, 2) } - end - end -end diff --git a/spec/rspec/expectations/contain_matcher_spec.cr b/spec/rspec/expectations/contain_matcher_spec.cr deleted file mode 100644 index b95d961..0000000 --- a/spec/rspec/expectations/contain_matcher_spec.cr +++ /dev/null @@ -1,90 +0,0 @@ -require "../../spec_helper" - -# In Ruby, this is the `include` matcher. -# However, `include` is a reserved keyword in Crystal. -# So instead, it is `contain` in Spectator. -Spectator.describe "`contain` matcher" do - context "array usage" do - describe [1, 3, 7] do - it { is_expected.to contain(1) } - it { is_expected.to contain(3) } - it { is_expected.to contain(7) } - it { is_expected.to contain(1, 7) } - it { is_expected.to contain(1, 3, 7) } - - skip reason: "Utility matcher method `a_kind_of` is not supported." { is_expected.to contain(a_kind_of(Int)) } - - skip reason: "Compound matchers aren't supported." { is_expected.to contain(be_odd.and be < 10) } - - # TODO: Fix behavior and cleanup output. - # This syntax is allowed, but produces a wrong result and bad output. - skip reason: "Fix behavior and cleanup output." { is_expected.to contain(be_odd) } - skip reason: "Fix behavior and cleanup output." { is_expected.not_to contain(be_even) } - - it { is_expected.not_to contain(17) } - it { is_expected.not_to contain(43, 100) } - - # deliberate failures - it_fails { is_expected.to contain(4) } - it_fails { is_expected.to contain(be_even) } - it_fails { is_expected.not_to contain(1) } - it_fails { is_expected.not_to contain(3) } - it_fails { is_expected.not_to contain(7) } - it_fails { is_expected.not_to contain(1, 3, 7) } - - # both of these should fail since it contains 1 but not 9 - it_fails { is_expected.to contain(1, 9) } - it_fails { is_expected.not_to contain(1, 9) } - end - end - - context "string usage" do - describe "a string" do - it { is_expected.to contain("str") } - it { is_expected.to contain("a", "str", "ng") } - it { is_expected.not_to contain("foo") } - it { is_expected.not_to contain("foo", "bar") } - - # deliberate failures - it_fails { is_expected.to contain("foo") } - it_fails { is_expected.not_to contain("str") } - it_fails { is_expected.to contain("str", "foo") } - it_fails { is_expected.not_to contain("str", "foo") } - end - end - - context "hash usage" do - # A hash can't be described inline here for some reason. - # So it is placed in the subject instead. - describe ":a => 7, :b => 5" do - subject { {:a => 7, :b => 5} } - - # Hash syntax is changed here from `:a => 7` to `a: 7`. - skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(a: 7) } - skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(b: 5, a: 7) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:c) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:c, :d) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(d: 2) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(a: 5) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(b: 7, a: 5) } - - # deliberate failures - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:a) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:b, :a) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(a: 7) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(a: 7, b: 5) } - skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(:c) } - skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(:c, :d) } - skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(d: 2) } - skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(a: 5) } - skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(a: 5, b: 7) } - - # Mixed cases--the hash contains one but not the other. - # All 4 of these cases should fail. - skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(:a, :d) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:a, :d) } - skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(a: 7, d: 3) } - skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(a: 7, d: 3) } - end - end -end diff --git a/spec/rspec/expectations/cover_matcher_spec.cr b/spec/rspec/expectations/cover_matcher_spec.cr deleted file mode 100644 index cf32929..0000000 --- a/spec/rspec/expectations/cover_matcher_spec.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/cover-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`cover` matcher" do - context "range usage" do - describe (1..10) do - it { is_expected.to cover(4) } - it { is_expected.to cover(6) } - it { is_expected.to cover(8) } - it { is_expected.to cover(4, 6) } - it { is_expected.to cover(4, 6, 8) } - it { is_expected.not_to cover(11) } - it { is_expected.not_to cover(11, 12) } - - # deliberate failures - it_fails { is_expected.to cover(11) } - it_fails { is_expected.not_to cover(4) } - it_fails { is_expected.not_to cover(6) } - it_fails { is_expected.not_to cover(8) } - it_fails { is_expected.not_to cover(4, 6, 8) } - - # both of these should fail since it covers 5 but not 11 - it_fails { is_expected.to cover(5, 11) } - it_fails { is_expected.not_to cover(5, 11) } - end - end -end diff --git a/spec/rspec/expectations/end_with_matcher_spec.cr b/spec/rspec/expectations/end_with_matcher_spec.cr deleted file mode 100644 index 92d927a..0000000 --- a/spec/rspec/expectations/end_with_matcher_spec.cr +++ /dev/null @@ -1,30 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/end-with-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`end_with` matcher" do - context "string usage" do - describe "this string" do - it { is_expected.to end_with "string" } - it { is_expected.not_to end_with "stringy" } - - # deliberate failures - it_fails { is_expected.not_to end_with "string" } - it_fails { is_expected.to end_with "stringy" } - end - end - - context "array usage" do - describe [0, 1, 2, 3, 4] do - it { is_expected.to end_with 4 } - skip reason: "Add support for multiple items at the end of an array." { is_expected.to end_with 3, 4 } - it { is_expected.not_to end_with 3 } - skip reason: "Add support for multiple items at the end of an array." { is_expected.not_to end_with 0, 1, 2, 3, 4, 5 } - - # deliberate failures - it_fails { is_expected.not_to end_with 4 } - it_fails { is_expected.to end_with 3 } - end - end -end diff --git a/spec/rspec/expectations/equality_matchers_spec.cr b/spec/rspec/expectations/equality_matchers_spec.cr deleted file mode 100644 index 9aa923c..0000000 --- a/spec/rspec/expectations/equality_matchers_spec.cr +++ /dev/null @@ -1,64 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/equality-matchers -# and modified to fit Spectator and Crystal. -Spectator.describe "Equality matchers" do - context "compare using eq (==)" do - describe "a string" do - it "is equal to another string of the same value" do - expect("this string").to eq("this string") - end - - it "is not equal to another string of a different value" do - expect("this string").not_to eq("a different string") - end - end - - describe "an integer" do - it "is equal to a float for the same value" do - expect(5).to eq(5.0) - end - end - end - - context "compare using ==" do - describe "a string" do - it "is equal to another string of the same value" do - expect("this string").to be == "this string" - end - - it "is not equal to another string of a different value" do - expect("this string").not_to be == "a different string" - end - end - - describe "an integer" do - it "is equal to a float of the same value" do - expect(5).to be == 5.0 - end - end - end - - # There are no #eql? and #equal? methods in Crystal, so these tests are skipped. - - context "compare using be (same?)" do - it "is equal to itself" do - string = "this string" - expect(string).to be(string) - end - - it "is not equal to another reference of the same value" do - # Strings with identical contents are the same reference in Crystal. - # This test is modified to reflect that. - # expect("this string").not_to be("this string") - box1 = Box.new("this string") - box2 = Box.new("this string") - expect(box1).not_to be(box2) - end - - it "is not equal to another string of a different value" do - expect("this string").not_to be("a different string") - end - end -end diff --git a/spec/rspec/expectations/have_attributes_matcher_spec.cr b/spec/rspec/expectations/have_attributes_matcher_spec.cr deleted file mode 100644 index 7bcb441..0000000 --- a/spec/rspec/expectations/have_attributes_matcher_spec.cr +++ /dev/null @@ -1,36 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/have-attributes-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`have_attributes` matcher" do - context "basic usage" do - # Use `record` instead of `Struct.new`. - record Person, name : String, age : Int32 - - describe Person.new("Jim", 32) do - # Changed some syntax for Ruby hashes to Crystal named tuples. - - # Spectator doesn't support helper matchers like `a_string_starting_with` and `a_value <`. - # But maybe in the future it will. - it { is_expected.to have_attributes(name: "Jim") } - skip reason: "Add support for fuzzy matchers." { is_expected.to have_attributes(name: a_string_starting_with("J")) } - it { is_expected.to have_attributes(age: 32) } - skip reason: "Add support for fuzzy matchers." { is_expected.to have_attributes(age: (a_value > 30)) } - it { is_expected.to have_attributes(name: "Jim", age: 32) } - skip reason: "Add support for fuzzy matchers." { is_expected.to have_attributes(name: a_string_starting_with("J"), age: (a_value > 30)) } - it { is_expected.not_to have_attributes(name: "Bob") } - it { is_expected.not_to have_attributes(age: 10) } - skip reason: "Add support for fuzzy matchers." { is_expected.not_to have_attributes(age: (a_value < 30)) } - - # deliberate failures - it_fails { is_expected.to have_attributes(name: "Bob") } - it_fails { is_expected.to have_attributes(name: 10) } - - # fails if any of the attributes don't match - it_fails { is_expected.to have_attributes(name: "Bob", age: 32) } - it_fails { is_expected.to have_attributes(name: "Jim", age: 10) } - it_fails { is_expected.to have_attributes(name: "Bob", age: 10) } - end - end -end diff --git a/spec/rspec/expectations/match_matcher_spec.cr b/spec/rspec/expectations/match_matcher_spec.cr deleted file mode 100644 index dac2a0c..0000000 --- a/spec/rspec/expectations/match_matcher_spec.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/match-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`match` matcher" do - context "string usage" do - describe "a string" do - it { is_expected.to match(/str/) } - it { is_expected.not_to match(/foo/) } - - # deliberate failures - it_fails { is_expected.not_to match(/str/) } - it_fails { is_expected.to match(/foo/) } - end - end - - context "regular expression usage" do - describe /foo/ do - it { is_expected.to match("food") } - it { is_expected.not_to match("drinks") } - - # deliberate failures - it_fails { is_expected.not_to match("food") } - it_fails { is_expected.to match("drinks") } - end - end -end diff --git a/spec/rspec/expectations/predicate_matchers_spec.cr b/spec/rspec/expectations/predicate_matchers_spec.cr deleted file mode 100644 index 8a1cbda..0000000 --- a/spec/rspec/expectations/predicate_matchers_spec.cr +++ /dev/null @@ -1,81 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/predicate-matchers -# and modified to fit Spectator and Crystal. -Spectator.describe "Predicate matchers" do - context "should be_zero (based on Int#zero?)" do - describe 0 do - it { is_expected.to be_zero } - end - - describe 7 do - # deliberate failure - it_fails { is_expected.to be_zero } - end - end - - context "should_not be_empty (based on Array#empty?)" do - describe [1, 2, 3] do - it { is_expected.not_to be_empty } - end - - describe [] of Int32 do - # deliberate failure - it_fails { is_expected.not_to be_empty } - end - end - - context "should have_key (based on Hash#has_key?)" do - describe Hash do - subject { {:foo => 7} } - - it { is_expected.to have_key(:foo) } - - # deliberate failure - it_fails { is_expected.to have_key(:bar) } - end - end - - context "should_not have_all_string_keys (based on custom #has_all_string_keys? method)" do - class ::Hash(K, V) - def has_all_string_keys? - keys.all? { |k| String === k } - end - end - - describe Hash do - context "with symbol keys" do - subject { {:foo => 7, :bar => 5} } - - it { is_expected.not_to have_all_string_keys } - end - - context "with string keys" do - subject { {"foo" => 7, "bar" => 5} } - - # deliberate failure - it_fails { is_expected.not_to have_all_string_keys } - end - end - end - - context "matcher arguments are passed on to the predicate method" do - struct ::Int - def multiple_of?(x) - (self % x).zero? - end - end - - describe 12 do - it { is_expected.to be_multiple_of(3) } - it { is_expected.not_to be_multiple_of(7) } - - # deliberate failures - it_fails { is_expected.not_to be_multiple_of(4) } - it_fails { is_expected.to be_multiple_of(5) } - end - end - - # The examples using private methods cause a compilation error in Crystal, and can't be used here. -end diff --git a/spec/rspec/expectations/raise_error_matcher_spec.cr b/spec/rspec/expectations/raise_error_matcher_spec.cr deleted file mode 100644 index 1d262ce..0000000 --- a/spec/rspec/expectations/raise_error_matcher_spec.cr +++ /dev/null @@ -1,93 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/raise-error-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`raise_error` matcher" do - context "expect any error" do - # This example originally calls a non-existent method. - # That isn't allowed in Crystal. - # The example has been changed to just raise a runtime error. - describe "dividing by zero" do - it "raises" do - expect { 42 // 0 }.to raise_error - end - end - end - - context "expect specific error" do - # Again, can't even compile if a method doesn't exist. - # So using a different exception here. - describe "dividing by zero" do - it "raises" do - expect { 42 // 0 }.to raise_error(DivisionByZeroError) - end - end - end - - # The following examples are changed slightly. - # `raise Type.new(message)` is the syntax in Crystal, - # whereas it is `raise Type, message` in Ruby. - # Additionally, `StandardError` doesn't exist in Crystal, - # so `Exception` is used instead. - context "match message with a string" do - describe "matching error message with string" do - it "matches the error message" do - expect { raise Exception.new("this message exactly") } - .to raise_error("this message exactly") - end - end - end - - context "match message with a regexp" do - describe "matching error message with regex" do - it "matches the error message" do - expect { raise Exception.new("my message") } - .to raise_error(/my mess/) - end - end - end - - context "matching message with `with_message`" do - describe "matching error message with regex" do - it "matches the error message" do - expect { raise Exception.new("my message") } - .to raise_error.with_message(/my mess/) - end - end - end - - context "match class + message with string" do - describe "matching error message with string" do - it "matches the error message" do - expect { raise Exception.new("this message exactly") } - .to raise_error(Exception, "this message exactly") - end - end - end - - context "match class + message with regexp" do - describe "matching error message with regex" do - it "matches the error message" do - expect { raise Exception.new("my message") } - .to raise_error(Exception, /my mess/) - end - end - end - - context "set expectations on error object passed to block" do - skip "raises DivisionByZeroError", reason: "Support passing a block to `raise_error` matcher." do - expect { 42 // 0 }.to raise_error do |error| - expect(error).to be_a(DivisionByZeroError) - end - end - end - - context "expect no error at all" do - describe "#to_s" do - it "does not raise" do - expect { 42.to_s }.not_to raise_error - end - end - end -end diff --git a/spec/rspec/expectations/respond_to_matcher_spec.cr b/spec/rspec/expectations/respond_to_matcher_spec.cr deleted file mode 100644 index ff52285..0000000 --- a/spec/rspec/expectations/respond_to_matcher_spec.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/respond-to-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`respond_to` matcher" do - context "basic usage" do - describe "a string" do - it { is_expected.to respond_to(:size) } # It's size in Crystal, not length. - it { is_expected.to respond_to(:hash, :class, :to_s) } - it { is_expected.not_to respond_to(:to_model) } - it { is_expected.not_to respond_to(:compact, :flatten) } - - # deliberate failures - it_fails { is_expected.to respond_to(:to_model) } - it_fails { is_expected.to respond_to(:compact, :flatten) } - it_fails { is_expected.not_to respond_to(:size) } - it_fails { is_expected.not_to respond_to(:hash, :class, :to_s) } - - # mixed examples--String responds to :length but not :flatten - # both specs should fail - it_fails { is_expected.to respond_to(:size, :flatten) } - it_fails { is_expected.not_to respond_to(:size, :flatten) } - end - end - - # Spectator doesn't support argument matching with respond_to. -end diff --git a/spec/rspec/expectations/start_with_matcher_spec.cr b/spec/rspec/expectations/start_with_matcher_spec.cr deleted file mode 100644 index 74f8f8a..0000000 --- a/spec/rspec/expectations/start_with_matcher_spec.cr +++ /dev/null @@ -1,30 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/start-with-matcher -# and modified to fit Spectator and Crystal. -Spectator.describe "`start_with` matcher" do - context "with a string" do - describe "this string" do - it { is_expected.to start_with "this" } - it { is_expected.not_to start_with "that" } - - # deliberate failures - it_fails { is_expected.not_to start_with "this" } - it_fails { is_expected.to start_with "that" } - end - end - - context "with an array" do - describe [0, 1, 2, 3, 4] do - it { is_expected.to start_with 0 } - skip reason: "Add support for multiple items at the beginning of an array." { is_expected.to start_with(0, 1) } - it { is_expected.not_to start_with(2) } - skip reason: "Add support for multiple items at the beginning of an array." { is_expected.not_to start_with(0, 1, 2, 3, 4, 5) } - - # deliberate failures - it_fails { is_expected.not_to start_with 0 } - it_fails { is_expected.to start_with 3 } - end - end -end diff --git a/spec/rspec/expectations/type_matchers_spec.cr b/spec/rspec/expectations/type_matchers_spec.cr deleted file mode 100644 index 35bec18..0000000 --- a/spec/rspec/expectations/type_matchers_spec.cr +++ /dev/null @@ -1,100 +0,0 @@ -require "../../spec_helper" - -# Examples taken from: -# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/type-matchers -# and modified to fit Spectator and Crystal. -Spectator.describe "Type matchers" do - context "be_(a_)kind_of matcher" do - # The docs use Float as an example. - # This doesn't work with the Crystal compiler, - # so a custom hierarchy is used instead. - # "Error: can't use Number as generic type argument yet, use a more specific type" - - module MyModule; end - - class Base; end - - class Derived < Base - include MyModule - end - - describe Derived do - # the actual class - it { is_expected.to be_kind_of(Derived) } - it { is_expected.to be_a_kind_of(Derived) } - it { is_expected.to be_a(Derived) } - - # the superclass - it { is_expected.to be_kind_of(Base) } - it { is_expected.to be_a_kind_of(Base) } - it { is_expected.to be_an(Base) } - - # an included module - it { is_expected.to be_kind_of(MyModule) } - it { is_expected.to be_a_kind_of(MyModule) } - it { is_expected.to be_a(MyModule) } - - # negative passing case - it { is_expected.not_to be_kind_of(String) } - it { is_expected.not_to be_a_kind_of(String) } - it { is_expected.not_to be_a(String) } - - # deliberate failures - it_fails { is_expected.not_to be_kind_of(Derived) } - it_fails { is_expected.not_to be_a_kind_of(Derived) } - it_fails { is_expected.not_to be_a(Derived) } - it_fails { is_expected.not_to be_kind_of(Base) } - it_fails { is_expected.not_to be_a_kind_of(Base) } - it_fails { is_expected.not_to be_an(Base) } - it_fails { is_expected.not_to be_kind_of(MyModule) } - it_fails { is_expected.not_to be_a_kind_of(MyModule) } - it_fails { is_expected.not_to be_a(MyModule) } - it_fails { is_expected.to be_kind_of(String) } - it_fails { is_expected.to be_a_kind_of(String) } - it_fails { is_expected.to be_a(String) } - end - - context "be_(an_)instance_of matcher" do - # The docs use Float as an example. - # This doesn't work with the Crystal compiler, - # so a custom hierarchy is used instead. - # "Error: can't use Number as generic type argument yet, use a more specific type" - - module MyModule; end - - class Base; end - - class Derived < Base - include MyModule - end - - describe Derived do - # the actual class - it { is_expected.to be_instance_of(Derived) } - it { is_expected.to be_an_instance_of(Derived) } - - # the superclass - it { is_expected.not_to be_instance_of(Base) } - it { is_expected.not_to be_an_instance_of(Base) } - - # an included module - it { is_expected.not_to be_instance_of(MyModule) } - it { is_expected.not_to be_an_instance_of(MyModule) } - - # another class with no relation to the subject's hierarchy - it { is_expected.not_to be_instance_of(String) } - it { is_expected.not_to be_an_instance_of(String) } - - # deliberate failures - it_fails { is_expected.not_to be_instance_of(Derived) } - it_fails { is_expected.not_to be_an_instance_of(Derived) } - it_fails { is_expected.to be_instance_of(Base) } - it_fails { is_expected.to be_an_instance_of(Base) } - it_fails { is_expected.to be_instance_of(MyModule) } - it_fails { is_expected.to be_an_instance_of(MyModule) } - it_fails { is_expected.to be_instance_of(String) } - it_fails { is_expected.to be_an_instance_of(String) } - end - end - end -end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index e2f9578..e0c29df 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,17 +1,2 @@ -require "json" # Needed to test masking Object#to_json in doubles. -require "yaml" # Needed to test masking Object#to_yaml in doubles. +require "spec" require "../src/spectator" -require "../src/spectator/should" -require "./helpers/**" - -macro it_fails(description = nil, &block) - it {{description}} do - expect do - {{block.body}} - end.to raise_error(Spectator::ExampleFailed) - end -end - -macro specify_fails(description = nil, &block) - it_fails {{description}} {{block}} -end diff --git a/spec/spectator/anything_spec.cr b/spec/spectator/anything_spec.cr deleted file mode 100644 index 3140f7e..0000000 --- a/spec/spectator/anything_spec.cr +++ /dev/null @@ -1,24 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator::Anything do - it "matches everything" do - expect(true).to match(subject) - expect(false).to match(subject) - expect(nil).to match(subject) - expect(42).to match(subject) - expect(42.as(Int32 | String)).to match(subject) - expect(["foo", "bar"]).to match(subject) - end - - describe "#to_s" do - subject { super.to_s } - - it { is_expected.to contain("anything") } - end - - describe "#inspect" do - subject { super.inspect } - - it { is_expected.to contain("anything") } - end -end diff --git a/spec/spectator/block_spec.cr b/spec/spectator/block_spec.cr deleted file mode 100644 index a459b02..0000000 --- a/spec/spectator/block_spec.cr +++ /dev/null @@ -1,34 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator::Block do - describe "#value" do - it "calls the block" do - called = false - block = described_class.new { called = true } - expect { block.value }.to change { called }.to(true) - end - - it "can be called multiple times (doesn't cache the value)" do - count = 0 - block = described_class.new { count += 1 } - block.value # Call once, count should be 1. - expect { block.value }.to change { count }.from(1).to(2) - end - end - - describe "#to_s" do - let(block) do - described_class.new("Test Label") { 42 } - end - - subject { block.to_s } - - it "contains the label" do - is_expected.to contain("Test Label") - end - - it "contains the value" do - is_expected.to contain("42") - end - end -end diff --git a/spec/spectator/dsl/mocks/allow_receive_spec.cr b/spec/spectator/dsl/mocks/allow_receive_spec.cr deleted file mode 100644 index 473c74b..0000000 --- a/spec/spectator/dsl/mocks/allow_receive_spec.cr +++ /dev/null @@ -1,188 +0,0 @@ -require "../../../spec_helper" - -Spectator.describe "Allow stub DSL" do - context "with a double" do - double(:dbl) do - # Ensure the original is never called. - stub abstract def foo : Nil - stub abstract def foo(arg) : Nil - stub abstract def value : Int32 - end - - let(dbl) { double(:dbl) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage) - end - - it "matches when a message is received" do - allow(dbl).to receive(:foo) - expect { dbl.foo }.to_not raise_error - end - - it "returns the correct value" do - allow(dbl).to receive(:value).and_return(42) - expect(dbl.value).to eq(42) - end - - it "matches when a message is received with matching arguments" do - allow(dbl).to receive(:foo).with(:bar) - expect { dbl.foo(:bar) }.to_not raise_error - end - - it "raises when a message without arguments is received" do - allow(dbl).to receive(:foo).with(:bar) - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - - it "raises when a message with different arguments is received" do - allow(dbl).to receive(:foo).with(:baz) - expect { dbl.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - end - - context "with a class double" do - double(:dbl) do - # Ensure the original is never called. - abstract_stub def self.foo : Nil - end - - abstract_stub def self.foo(arg) : Nil - end - - abstract_stub def self.value : Int32 - 42 - end - end - - let(dbl) { class_double(:dbl) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage) - end - - it "matches when a message is received" do - allow(dbl).to receive(:foo) - expect { dbl.foo }.to_not raise_error - end - - it "returns the correct value" do - allow(dbl).to receive(:value).and_return(42) - expect(dbl.value).to eq(42) - end - - it "matches when a message is received with matching arguments" do - allow(dbl).to receive(:foo).with(:bar) - expect { dbl.foo(:bar) }.to_not raise_error - end - - it "raises when a message without arguments is received" do - allow(dbl).to receive(:foo).with(:bar) - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - - it "raises when a message with different arguments is received" do - allow(dbl).to receive(:foo).with(:baz) - expect { dbl.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - end - - context "with a mock" do - abstract class MyClass - abstract def foo : Int32 - abstract def foo(arg) : Int32 - end - - mock(MyClass) - - let(fake) { mock(MyClass) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage) - end - - it "matches when a message is received" do - allow(fake).to receive(:foo).and_return(42) - expect(fake.foo).to eq(42) - end - - it "returns the correct value" do - allow(fake).to receive(:foo).and_return(42) - expect(fake.foo).to eq(42) - end - - it "matches when a message is received with matching arguments" do - allow(fake).to receive(:foo).with(:bar).and_return(42) - expect(fake.foo(:bar)).to eq(42) - end - - it "raises when a message without arguments is received" do - allow(fake).to receive(:foo).with(:bar) - expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - - it "raises when a message with different arguments is received" do - allow(fake).to receive(:foo).with(:baz) - expect { fake.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - end - - context "with a class mock" do - class MyClass - def self.foo : Int32 - 42 - end - - def self.foo(arg) : Int32 - 42 - end - end - - mock(MyClass) - - let(fake) { class_mock(MyClass) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition { expect(fake.foo).to eq(42) } - - it "matches when a message is received" do - allow(fake).to receive(:foo).and_return(0) - expect(fake.foo).to eq(0) - end - - it "returns the correct value" do - allow(fake).to receive(:foo).and_return(0) - expect(fake.foo).to eq(0) - end - - it "matches when a message is received with matching arguments" do - allow(fake).to receive(:foo).with(:bar).and_return(0) - expect(fake.foo(:bar)).to eq(0) - end - - it "calls the original when a message without arguments is received" do - allow(fake).to receive(:foo).with(:bar) - expect(fake.foo).to eq(42) - end - - it "calls the original when a message with different arguments is received" do - allow(fake).to receive(:foo).with(:baz) - expect(fake.foo(:bar)).to eq(42) - end - end -end diff --git a/spec/spectator/dsl/mocks/double_spec.cr b/spec/spectator/dsl/mocks/double_spec.cr deleted file mode 100644 index 5547ae0..0000000 --- a/spec/spectator/dsl/mocks/double_spec.cr +++ /dev/null @@ -1,385 +0,0 @@ -require "../../../spec_helper" - -Spectator.describe "Double DSL", :smoke do - context "specifying methods as keyword args" do - double(:test, foo: "foobar", bar: 42) - subject(dbl) { double(:test) } - - it "defines a double with methods" do - aggregate_failures do - expect(dbl.foo).to eq("foobar") - expect(dbl.bar).to eq(42) - end - end - - it "compiles types without unions" do - aggregate_failures do - expect(dbl.foo).to compile_as(String) - expect(dbl.bar).to compile_as(Int32) - end - end - - context "with an unexpected message" do - it "raises an error" do - expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) - end - - it "reports the double name" do - expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /:test/) - end - - it "reports the arguments" do - expect { dbl.baz(:xyz, 123, a: "XYZ") }.to raise_error(Spectator::UnexpectedMessage, /\(:xyz, 123, a: "XYZ"\)/) - end - end - - context "blocks" do - it "supports blocks" do - aggregate_failures do - expect(dbl.foo { nil }).to eq("foobar") - expect(dbl.bar { nil }).to eq(42) - end - end - - it "supports blocks and has non-union return types" do - aggregate_failures do - expect(dbl.foo { nil }).to compile_as(String) - expect(dbl.bar { nil }).to compile_as(Int32) - end - end - - it "fails on undefined messages" do - expect do - dbl.baz { nil } - end.to raise_error(Spectator::UnexpectedMessage, /baz/) - end - end - end - - context "block with stubs" do - context "one method" do - double(:test2) do - stub def foo - "one method" - end - end - - subject(dbl) { double(:test2) } - - it "defines a double with methods" do - expect(dbl.foo).to eq("one method") - end - - it "compiles types without unions" do - expect(dbl.foo).to compile_as(String) - end - end - - context "two methods" do - double(:test3) do - stub def foo - "two methods" - end - - stub def bar - 42 - end - end - - subject(dbl) { double(:test3) } - - it "defines a double with methods" do - aggregate_failures do - expect(dbl.foo).to eq("two methods") - expect(dbl.bar).to eq(42) - end - end - - it "compiles types without unions" do - aggregate_failures do - expect(dbl.foo).to compile_as(String) - expect(dbl.bar).to compile_as(Int32) - end - end - end - - context "empty block" do - double(:test4) do - end - - subject(dbl) { double(:test4) } - - it "defines a double" do - expect(dbl).to be_a(Spectator::Double) - end - end - - context "stub-less method" do - double(:test5) do - def foo - "no stub" - end - end - - subject(dbl) { double(:test5) } - - it "defines a double with methods" do - expect(dbl.foo).to eq("no stub") - end - end - - context "mixing keyword arguments" do - double(:test6, foo: "kwargs", bar: 42) do - stub def foo - "block" - end - - stub def baz - "block" - end - - stub def baz(value) - "block2" - end - end - - subject(dbl) { double(:test6) } - - it "overrides the keyword arguments with the block methods" do - expect(dbl.foo).to eq("block") - end - - it "falls back to the keyword argument value for mismatched arguments" do - expect(dbl.foo(42)).to eq("kwargs") - end - - it "can call methods defined only by keyword arguments" do - expect(dbl.bar).to eq(42) - end - - it "can call methods defined only by the block" do - expect(dbl.baz).to eq("block") - end - - it "can call methods defined by the block with different signatures" do - expect(dbl.baz(42)).to eq("block2") - end - end - - context "methods accepting blocks" do - double(:test7) do - stub def foo(&) - yield - end - - stub def bar(& : Int32 -> String) - yield 42 - end - end - - subject(dbl) { double(:test7) } - - it "defines the method and yields" do - expect(dbl.foo { :xyz }).to eq(:xyz) - end - - it "matches methods with block argument type restrictions" do - expect(dbl.bar &.to_s).to eq("42") - end - end - end - - describe "double naming" do - double(:Name, type: :symbol) - - it "accepts a symbolic double name" do - dbl = double(:Name) - expect(dbl.type).to eq(:symbol) - end - - it "accepts a string double name" do - dbl = double("Name") - expect(dbl.type).to eq(:symbol) - end - - it "accepts a constant double name" do - dbl = double(Name) - expect(dbl.type).to eq(:symbol) - end - end - - describe "predefined method stubs" do - double(:test8, foo: 42) - - let(dbl) { double(:test8, foo: 7) } - - it "overrides the original value" do - expect(dbl.foo).to eq(7) - end - end - - describe "scope" do - double(:outer, scope: :outer) - double(:scope, scope: :outer) - - it "finds a double in the same scope" do - dbl = double(:outer) - expect(dbl.scope).to eq(:outer) - end - - it "uses an identically named double from the same scope" do - dbl = double(:scope) - expect(dbl.scope).to eq(:outer) - end - - context "inner1" do - double(:inner, scope: :inner1) - double(:scope, scope: :inner1) - - it "finds a double in the same scope" do - dbl = double(:inner) - expect(dbl.scope).to eq(:inner1) - end - - it "uses an identically named double from the same scope" do - dbl = double(:scope) - expect(dbl.scope).to eq(:inner1) - end - - context "nested" do - it "finds a double from a parent scope" do - aggregate_failures do - dbl = double(:inner) - expect(dbl.scope).to eq(:inner1) - dbl = double(:outer) - expect(dbl.scope).to eq(:outer) - end - end - - it "uses the inner-most identically named double" do - dbl = double(:inner) - expect(dbl.scope).to eq(:inner1) - end - end - end - - context "inner2" do - double(:inner, scope: :inner2) - double(:scope, scope: :inner2) - - it "finds a double in the same scope" do - dbl = double(:inner) - expect(dbl.scope).to eq(:inner2) - end - - it "uses an identically named double from the same scope" do - dbl = double(:scope) - expect(dbl.scope).to eq(:inner2) - end - - context "nested" do - it "finds a double from a parent scope" do - aggregate_failures do - dbl = double(:inner) - expect(dbl.scope).to eq(:inner2) - dbl = double(:outer) - expect(dbl.scope).to eq(:outer) - end - end - - it "uses the inner-most identically named double" do - dbl = double(:inner) - expect(dbl.scope).to eq(:inner2) - end - end - end - end - - describe "context" do - double(:context_double, predefined: :predefined, override: :predefined) do - stub abstract def memoize : Symbol - - stub def inline : Symbol - :inline # Memoized values can't be used here. - end - - stub def reference : String - memoize.to_s - end - end - - let(memoize) { :memoize } - let(override) { :override } - let(dbl) { double(:context_double, override: override) } - - before { allow(dbl).to receive(:memoize).and_return(memoize) } - - it "doesn't change predefined values" do - expect(dbl.predefined).to eq(:predefined) - end - - it "can use memoized values for overrides" do - expect(dbl.override).to eq(:override) - end - - it "can use memoized values for stubs" do - expect(dbl.memoize).to eq(:memoize) - end - - it "can override inline stubs" do - expect { allow(dbl).to receive(:inline).and_return(override) }.to change { dbl.inline }.from(:inline).to(:override) - end - - it "can reference memoized values with indirection" do - expect { allow(dbl).to receive(:memoize).and_return(override) }.to change { dbl.reference }.from("memoize").to("override") - end - end - - describe "class doubles" do - double(:class_double) do - abstract_stub def self.abstract_method - :abstract - end - - stub def self.default_method - :default - end - - stub def self.args(arg) - arg - end - - stub def self.method1 - :method1 - end - - stub def self.reference - method1.to_s - end - end - - let(dbl) { class_double(:class_double) } - - it "raises on abstract stubs" do - expect { dbl.abstract_method }.to raise_error(Spectator::UnexpectedMessage, /abstract_method/) - end - - it "can define default stubs" do - expect(dbl.default_method).to eq(:default) - end - - it "can define new stubs" do - expect { allow(dbl).to receive(:args).and_return(42) }.to change { dbl.args(5) }.from(5).to(42) - end - - it "can override class method stubs" do - allow(dbl).to receive(:method1).and_return(:override) - expect(dbl.method1).to eq(:override) - end - - it "can reference stubs" do - allow(dbl).to receive(:method1).and_return(:reference) - expect(dbl.reference).to eq("reference") - end - end -end diff --git a/spec/spectator/dsl/mocks/expect_receive_spec.cr b/spec/spectator/dsl/mocks/expect_receive_spec.cr deleted file mode 100644 index a249ad3..0000000 --- a/spec/spectator/dsl/mocks/expect_receive_spec.cr +++ /dev/null @@ -1,226 +0,0 @@ -require "../../../spec_helper" - -Spectator.describe "Deferred stub expectation DSL" do - context "with a double" do - double(:dbl) do - # Ensure the original is never called. - stub abstract def foo : Nil - stub abstract def foo(arg) : Nil - stub abstract def value : Int32 - end - - let(dbl) { double(:dbl) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage) - dbl._spectator_clear_calls # Don't include previous call in results. - end - - it "matches when a message is received" do - expect(dbl).to receive(:foo) - dbl.foo - end - - it "returns the correct value" do - expect(dbl).to receive(:value).and_return(42) - expect(dbl.value).to eq(42) - end - - it "matches when a message isn't received" do - expect(dbl).to_not receive(:foo) - end - - it "matches when a message is received with matching arguments" do - expect(dbl).to receive(:foo).with(:bar) - dbl.foo(:bar) - end - - it "matches when a message without arguments is received" do - expect(dbl).to_not receive(:foo).with(:bar) - dbl.foo - end - - it "matches when a message without arguments isn't received" do - expect(dbl).to_not receive(:foo).with(:bar) - end - - it "matches when a message with arguments isn't received" do - expect(dbl).to_not receive(:foo).with(:baz) - dbl.foo(:bar) - end - end - - context "with a class double" do - double(:dbl) do - # Ensure the original is never called. - abstract_stub def self.foo : Nil - end - - abstract_stub def self.foo(arg) : Nil - end - - abstract_stub def self.value : Int32 - 42 - end - end - - let(dbl) { class_double(:dbl) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage) - dbl._spectator_clear_calls # Don't include previous call in results. - end - - it "matches when a message is received" do - expect(dbl).to receive(:foo) - dbl.foo - end - - it "returns the correct value" do - expect(dbl).to receive(:value).and_return(42) - expect(dbl.value).to eq(42) - end - - it "matches when a message isn't received" do - expect(dbl).to_not receive(:foo) - end - - it "matches when a message is received with matching arguments" do - expect(dbl).to receive(:foo).with(:bar) - dbl.foo(:bar) - end - - it "matches when a message without arguments is received" do - expect(dbl).to_not receive(:foo).with(:bar) - dbl.foo - end - - it "matches when a message without arguments isn't received" do - expect(dbl).to_not receive(:foo).with(:bar) - end - - it "matches when a message with arguments isn't received" do - expect(dbl).to_not receive(:foo).with(:baz) - dbl.foo(:bar) - end - end - - context "with a mock" do - abstract class MyClass - abstract def foo : Int32 - abstract def foo(arg) : Int32 - end - - mock(MyClass) - - let(fake) { mock(MyClass) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage) - fake._spectator_clear_calls # Don't include previous call in results. - end - - it "matches when a message is received" do - expect(fake).to receive(:foo).and_return(42) - fake.foo(:bar) - end - - it "returns the correct value" do - expect(fake).to receive(:foo).and_return(42) - expect(fake.foo).to eq(42) - end - - it "matches when a message isn't received" do - expect(fake).to_not receive(:foo) - end - - it "matches when a message is received with matching arguments" do - expect(fake).to receive(:foo).with(:bar).and_return(42) - fake.foo(:bar) - end - - it "matches when a message without arguments is received" do - expect(fake).to_not receive(:foo).with(:bar).and_return(42) - fake.foo - end - - it "matches when a message without arguments is received" do - expect(fake).to_not receive(:foo).with(:bar) - end - - it "matches when a message with arguments isn't received" do - expect(fake).to_not receive(:foo).with(:baz).and_return(42) - fake.foo(:bar) - end - end - - context "with a class mock" do - class MyClass - def self.foo : Int32 - 42 - end - - def self.foo(arg) : Int32 - 42 - end - end - - mock(MyClass) - - let(fake) { class_mock(MyClass) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect(fake.foo).to eq(42) - fake._spectator_clear_calls # Don't include previous call in results. - end - - it "matches when a message is received" do - expect(fake).to receive(:foo).and_return(0) - fake.foo(:bar) - end - - it "returns the correct value" do - expect(fake).to receive(:foo).and_return(0) - expect(fake.foo).to eq(0) - end - - it "matches when a message isn't received" do - expect(fake).to_not receive(:foo) - end - - it "matches when a message is received with matching arguments" do - expect(fake).to receive(:foo).with(:bar).and_return(0) - fake.foo(:bar) - end - - it "matches when a message without arguments is received" do - expect(fake).to_not receive(:foo).with(:bar).and_return(0) - fake.foo - end - - it "matches when a message without arguments is received" do - expect(fake).to_not receive(:foo).with(:bar) - end - - it "matches when a message with arguments isn't received" do - expect(fake).to_not receive(:foo).with(:baz).and_return(0) - fake.foo(:bar) - end - end -end diff --git a/spec/spectator/dsl/mocks/have_received_spec.cr b/spec/spectator/dsl/mocks/have_received_spec.cr deleted file mode 100644 index 57fc0dd..0000000 --- a/spec/spectator/dsl/mocks/have_received_spec.cr +++ /dev/null @@ -1,339 +0,0 @@ -require "../../../spec_helper" - -Spectator.describe "Stubbable receiver DSL" do - context "with a double" do - double(:dbl, foo: 42) - - let(dbl) { double(:dbl) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - - it "matches when a message is received" do - dbl.foo - expect(dbl).to have_received(:foo) - end - - it "matches when a message isn't received" do - expect(dbl).to_not have_received(:foo) - end - - it "matches when a message is received with matching arguments" do - dbl.foo(:bar) - expect(dbl).to have_received(:foo).with(:bar) - end - - it "matches when a message without arguments is received" do - dbl.foo - expect(dbl).to_not have_received(:foo).with(:bar) - end - - it "matches when a message without arguments isn't received" do - expect(dbl).to_not have_received(:foo).with(:bar) - end - - it "matches when a message with arguments isn't received" do - dbl.foo(:bar) - expect(dbl).to_not have_received(:foo).with(:baz) - end - end - - context "with a class double" do - double(:dbl) do - stub def self.foo - 42 - end - - stub def self.foo(arg) - 42 - end - end - - let(dbl) { class_double(:dbl) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - - it "matches when a message is received" do - dbl.foo - expect(dbl).to have_received(:foo) - end - - it "matches when a message isn't received" do - expect(dbl).to_not have_received(:foo) - end - - it "matches when a message is received with matching arguments" do - dbl.foo(:bar) - expect(dbl).to have_received(:foo).with(:bar) - end - - it "matches when a message without arguments is received" do - dbl.foo - expect(dbl).to_not have_received(:foo).with(:bar) - end - - it "matches when a message without arguments isn't received" do - expect(dbl).to_not have_received(:foo).with(:bar) - end - - it "matches when a message with arguments isn't received" do - dbl.foo(:bar) - expect(dbl).to_not have_received(:foo).with(:baz) - end - end - - context "with a mock" do - abstract class MyClass - abstract def foo(arg) : Int32 - end - - mock(MyClass, foo: 42) - - let(fake) { mock(MyClass) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - - it "matches when a message is received" do - fake.foo(:bar) - expect(fake).to have_received(:foo) - end - - it "matches when a message isn't received" do - expect(fake).to_not have_received(:foo) - end - - it "matches when a message is received with matching arguments" do - fake.foo(:bar) - expect(fake).to have_received(:foo).with(:bar) - end - - it "matches when a message without arguments is received" do - expect(fake).to_not have_received(:foo).with(:bar) - end - - it "matches when a message with arguments isn't received" do - fake.foo(:bar) - expect(fake).to_not have_received(:foo).with(:baz) - end - end - - context "with a class mock" do - class MyClass - def self.foo(arg) : Int32 - 42 - end - end - - mock(MyClass) - - let(fake) { class_mock(MyClass) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - - it "matches when a message is received" do - fake.foo(:bar) - expect(fake).to have_received(:foo) - end - - it "matches when a message isn't received" do - expect(fake).to_not have_received(:foo) - end - - it "matches when a message is received with matching arguments" do - fake.foo(:bar) - expect(fake).to have_received(:foo).with(:bar) - end - - it "matches when a message without arguments is received" do - expect(fake).to_not have_received(:foo).with(:bar) - end - - it "matches when a message with arguments isn't received" do - fake.foo(:bar) - expect(fake).to_not have_received(:foo).with(:baz) - end - end - - context "count modifiers" do - double(:dbl, foo: 42) - - let(dbl) { double(:dbl) } - - describe "#once" do - it "matches when the stub is called once" do - dbl.foo - expect(dbl).to have_received(:foo).once - end - - it "doesn't match when the stub isn't called" do - expect(dbl).to_not have_received(:foo).once - end - - it "doesn't match when the stub is called twice" do - 2.times { dbl.foo } - expect(dbl).to_not have_received(:foo).once - end - end - - describe "#twice" do - it "matches when the stub is called twice" do - 2.times { dbl.foo } - expect(dbl).to have_received(:foo).twice - end - - it "doesn't match when the stub isn't called" do - expect(dbl).to_not have_received(:foo).twice - end - - it "doesn't match when the stub is called once" do - dbl.foo - expect(dbl).to_not have_received(:foo).twice - end - - it "doesn't match when the stub is called thrice" do - 3.times { dbl.foo } - expect(dbl).to_not have_received(:foo).twice - end - end - - describe "#exactly" do - it "matches when the stub is called the exact amount" do - 3.times { dbl.foo } - expect(dbl).to have_received(:foo).exactly(3).times - end - - it "doesn't match when the stub isn't called" do - expect(dbl).to_not have_received(:foo).exactly(3).times - end - - it "doesn't match when the stub is called less than the amount" do - 2.times { dbl.foo } - expect(dbl).to_not have_received(:foo).exactly(3).times - end - - it "doesn't match when the stub is called more than the amount" do - 4.times { dbl.foo } - expect(dbl).to_not have_received(:foo).exactly(3).times - end - end - - describe "#at_least" do - it "matches when the stub is called the exact amount" do - 3.times { dbl.foo } - expect(dbl).to have_received(:foo).at_least(3).times - end - - it "doesn't match when the stub isn't called" do - expect(dbl).to_not have_received(:foo).at_least(3).times - end - - it "doesn't match when the stub is called less than the amount" do - 2.times { dbl.foo } - expect(dbl).to_not have_received(:foo).at_least(3).times - end - - it "matches when the stub is called more than the amount" do - 4.times { dbl.foo } - expect(dbl).to have_received(:foo).at_least(3).times - end - end - - describe "#at_most" do - it "matches when the stub is called the exact amount" do - 3.times { dbl.foo } - expect(dbl).to have_received(:foo).at_most(3).times - end - - it "matches when the stub isn't called" do - expect(dbl).to have_received(:foo).at_most(3).times - end - - it "matches when the stub is called less than the amount" do - 2.times { dbl.foo } - expect(dbl).to have_received(:foo).at_most(3).times - end - - it "doesn't match when the stub is called more than the amount" do - 4.times { dbl.foo } - expect(dbl).to_not have_received(:foo).at_most(3).times - end - end - - describe "#at_least_once" do - it "matches when the stub is called once" do - dbl.foo - expect(dbl).to have_received(:foo).at_least_once - end - - it "doesn't match when the stub isn't called" do - expect(dbl).to_not have_received(:foo).at_least_once - end - - it "matches when the stub is called more than once" do - 2.times { dbl.foo } - expect(dbl).to have_received(:foo).at_least_once - end - end - - describe "#at_least_twice" do - it "doesn't match when the stub is called once" do - dbl.foo - expect(dbl).to_not have_received(:foo).at_least_twice - end - - it "doesn't match when the stub isn't called" do - expect(dbl).to_not have_received(:foo).at_least_twice - end - - it "matches when the stub is called twice" do - 2.times { dbl.foo } - expect(dbl).to have_received(:foo).at_least_twice - end - - it "matches when the stub is called more than twice" do - 3.times { dbl.foo } - expect(dbl).to have_received(:foo).at_least_twice - end - end - - describe "#at_most_once" do - it "matches when the stub is called once" do - dbl.foo - expect(dbl).to have_received(:foo).at_most_once - end - - it "matches when the stub isn't called" do - expect(dbl).to have_received(:foo).at_most_once - end - - it "doesn't match when the stub is called more than once" do - 2.times { dbl.foo } - expect(dbl).to_not have_received(:foo).at_most_once - end - end - - describe "#at_most_twice" do - it "matches when the stub is called once" do - dbl.foo - expect(dbl).to have_received(:foo).at_most_twice - end - - it "matches when the stub isn't called" do - expect(dbl).to have_received(:foo).at_most_twice - end - - it "matches when the stub is called twice" do - 2.times { dbl.foo } - expect(dbl).to have_received(:foo).at_most_twice - end - - it "doesn't match when the stub is called more than twice" do - 3.times { dbl.foo } - expect(dbl).to_not have_received(:foo).at_most_twice - end - end - end -end diff --git a/spec/spectator/dsl/mocks/lazy_double_spec.cr b/spec/spectator/dsl/mocks/lazy_double_spec.cr deleted file mode 100644 index db0e11d..0000000 --- a/spec/spectator/dsl/mocks/lazy_double_spec.cr +++ /dev/null @@ -1,83 +0,0 @@ -require "../../../spec_helper" - -Spectator.describe "Lazy double DSL" do - context "specifying methods as keyword args" do - subject(dbl) { double(:test, foo: "foobar", bar: 42) } - - it "defines a double with methods" do - aggregate_failures do - expect(dbl.foo).to eq("foobar") - expect(dbl.bar).to eq(42) - end - end - - context "with an unexpected message" do - it "raises an error" do - expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) - end - - it "reports the double name" do - expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /:test/) - end - - it "reports the arguments" do - expect { dbl.baz(:xyz, 123, a: "XYZ") }.to raise_error(Spectator::UnexpectedMessage, /\(:xyz, 123, a: "XYZ"\)/) - end - end - - context "blocks" do - it "supports blocks" do - aggregate_failures do - expect(dbl.foo { nil }).to eq("foobar") - expect(dbl.bar { nil }).to eq(42) - end - end - - it "fails on undefined messages" do - expect do - dbl.baz { nil } - end.to raise_error(Spectator::UnexpectedMessage, /baz/) - end - end - end - - describe "double naming" do - it "accepts a symbolic double name" do - dbl = double(:name) - expect { dbl.oops }.to raise_error(Spectator::UnexpectedMessage, /:name/) - end - - it "accepts a string double name" do - dbl = double("Name") - expect { dbl.oops }.to raise_error(Spectator::UnexpectedMessage, /"Name"/) - end - - it "accepts no name" do - dbl = double - expect { dbl.oops }.to raise_error(Spectator::UnexpectedMessage, /anonymous/i) - end - - it "accepts no name and predefined responses" do - dbl = double(foo: 42) - expect(dbl.foo).to eq(42) - end - end - - describe "context" do - let(memoize) { :memoize } - let(override) { :override } - let(dbl) { double(predefined: :predefined, memoize: memoize) } - - it "doesn't change predefined values" do - expect(dbl.predefined).to eq(:predefined) - end - - it "can use memoized values for stubs" do - expect(dbl.memoize).to eq(:memoize) - end - - it "can stub methods with memoized values" do - expect { allow(dbl).to receive(:memoize).and_return(override) }.to change { dbl.memoize }.from(:memoize).to(:override) - end - end -end diff --git a/spec/spectator/dsl/mocks/mock_spec.cr b/spec/spectator/dsl/mocks/mock_spec.cr deleted file mode 100644 index cd57cdc..0000000 --- a/spec/spectator/dsl/mocks/mock_spec.cr +++ /dev/null @@ -1,1288 +0,0 @@ -require "../../../spec_helper" - -Spectator.describe "Mock DSL", :smoke do - alias CapturedArguments = Tuple(Int32, Tuple(Int32, Int32), Int32, NamedTuple(x: Int32, y: Int32, z: Int32)) - - let(args_proc) do - ->(args : Spectator::AbstractArguments) do - { - args[0].as(Int32), - { - args[1].as(Int32), - args[2].as(Int32), - }, - args[:kwarg].as(Int32), - { - x: args[:x].as(Int32), - y: args[:y].as(Int32), - z: args[:z].as(Int32), - }, - } - end - end - - context "with a concrete class" do - class ConcreteClass - getter _spectator_invocations = [] of Symbol - - def method1 - @_spectator_invocations << :method1 - "original" - end - - def method2 : Symbol - @_spectator_invocations << :method2 - :original - end - - def method3(arg) - @_spectator_invocations << :method3 - arg - end - - def method4(&) : Symbol - @_spectator_invocations << :method4 - yield - end - - def method5(&) - @_spectator_invocations << :method5 - yield.to_i - end - - def method6(&) - @_spectator_invocations << :method6 - yield - end - - def method7(arg, *args, kwarg, **kwargs) - @_spectator_invocations << :method7 - {arg, args, kwarg, kwargs} - end - - def method8(arg, *args, kwarg, **kwargs, &) - @_spectator_invocations << :method8 - yield - {arg, args, kwarg, kwargs} - end - end - - # method1 stubbed via mock block - # method2 stubbed via keyword args - # method3 not stubbed (calls original) - # method4 stubbed via mock block (yields) - # method5 stubbed via keyword args (yields) - # method6 not stubbed (calls original and yields) - # method7 not stubbed (calls original) testing args - # method8 not stubbed (calls original and yields) testing args - mock(ConcreteClass, method2: :stubbed, method5: 42) do - stub def method1 - "stubbed" - end - - stub def method4(&) : Symbol - yield - :block - end - end - - subject(fake) { mock(ConcreteClass) } - - it "defines a subclass" do - expect(fake).to be_a(ConcreteClass) - end - - it "defines stubs in the block" do - expect(fake.method1).to eq("stubbed") - end - - it "can stub methods defined in the block" do - stub = Spectator::ValueStub.new(:method1, "override") - expect { fake._spectator_define_stub(stub) }.to change { fake.method1 }.from("stubbed").to("override") - end - - it "defines stubs from keyword arguments" do - expect(fake.method2).to eq(:stubbed) - end - - it "can stub methods from keyword arguments" do - stub = Spectator::ValueStub.new(:method2, :override) - expect { fake._spectator_define_stub(stub) }.to change { fake.method2 }.from(:stubbed).to(:override) - end - - it "calls the original implementation for methods not provided a stub" do - expect(fake.method3(:xyz)).to eq(:xyz) - end - - it "can stub methods after declaration" do - stub = Spectator::ValueStub.new(:method3, :abc) - expect { fake._spectator_define_stub(stub) }.to change { fake.method3(:xyz) }.from(:xyz).to(:abc) - end - - it "defines stubs with yield in the block" do - expect(fake.method4 { :wrong }).to eq(:block) - end - - it "can stub methods with yield in the block" do - stub = Spectator::ValueStub.new(:method4, :override) - expect { fake._spectator_define_stub(stub) }.to change { fake.method4 { :wrong } }.from(:block).to(:override) - end - - it "defines stubs with yield from keyword arguments" do - expect(fake.method5 { :wrong }).to eq(42) - end - - it "can stub methods with yield from keyword arguments" do - stub = Spectator::ValueStub.new(:method5, 123) - expect { fake._spectator_define_stub(stub) }.to change { fake.method5 { "0" } }.from(42).to(123) - end - - it "can stub yielding methods after declaration" do - stub = Spectator::ValueStub.new(:method6, :abc) - expect { fake._spectator_define_stub(stub) }.to change { fake.method6 { :xyz } }.from(:xyz).to(:abc) - end - - it "handles arguments correctly" do - args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) - args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block } - aggregate_failures do - expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - end - end - - it "handles arguments correctly with stubs" do - stub1 = Spectator::ProcStub.new(:method7, args_proc) - stub2 = Spectator::ProcStub.new(:method8, args_proc) - fake._spectator_define_stub(stub1) - fake._spectator_define_stub(stub2) - args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) - args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block } - aggregate_failures do - expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - end - end - - it "compiles types without unions" do - aggregate_failures do - expect(fake.method1).to compile_as(String) - expect(fake.method2).to compile_as(Symbol) - expect(fake.method3(42)).to compile_as(Int32) - expect(fake.method4 { :foo }).to compile_as(Symbol) - expect(fake.method5 { "123" }).to compile_as(Int32) - expect(fake.method6 { "123" }).to compile_as(String) - end - end - - def restricted(thing : ConcreteClass) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(fake)).to eq("stubbed") - end - - it "does not call the original method when stubbed" do - fake.method1 - fake.method2 - fake.method3("foo") - fake.method4 { :foo } - fake.method5 { "42" } - fake.method6 { 42 } - fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) - fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block } - expect(fake._spectator_invocations).to contain_exactly(:method3, :method6, :method7, :method8) - end - - # Cannot test unexpected messages - will not compile due to missing methods. - - describe "deferred default stubs" do - mock(ConcreteClass) - - let(fake2) do - mock(ConcreteClass, - method1: "stubbed", - method3: 123, - method4: :xyz) - end - - it "uses the keyword arguments as stubs" do - aggregate_failures do - expect(fake2.method1).to eq("stubbed") - expect(fake2.method2).to eq(:original) - expect(fake2.method3(42)).to eq(123) - expect(fake2.method4 { :foo }).to eq(:xyz) - end - end - end - end - - context "with an abstract class" do - abstract class AbstractClass - abstract def method1 - - abstract def method2 : Symbol - - abstract def method3 : Int32 - - abstract def method4 - - abstract def method5(&) - - abstract def method6(&) : Symbol - - abstract def method7(arg, *args, kwarg, **kwargs) - - abstract def method8(arg, *args, kwarg, **kwargs, &) - end - - # method1 stubbed via mock block - # method2 stubbed via keyword args - # method3 not stubbed (raises) - # method4 not stubbed, but type defined via mock block - # method5 stubbed via mock block (yields) - # method6 stubbed via keyword args (yields) - # method7 not stubbed (calls original) testing args - # method8 not stubbed (calls original and yields) testing args - mock(AbstractClass, method2: :stubbed, method6: :kwargs) do - # NOTE: Abstract methods without a type restriction on the return value - # must be implemented with a type restriction. - stub def method1 : String - "stubbed" - end - - # NOTE: Defining the stub here with a return type restriction, but no default implementation. - abstract_stub abstract def method4 : Symbol - - # NOTE: Abstract methods that yield must have yield functionality defined in the method. - # This requires that yielding methods have a default implementation. - # Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition. - stub def method5(&) - yield - end - - # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - stub def method6(&) : Symbol - yield - end - - # NOTE: Defining the stub here with a return type restriction, but no default implementation. - abstract_stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments - - # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - abstract_stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments - end - - subject(fake) { mock(AbstractClass) } - - it "defines a subclass" do - expect(fake).to be_a(AbstractClass) - end - - it "defines stubs in the block" do - expect(fake.method1).to eq("stubbed") - end - - it "can stub methods defined in the block" do - stub = Spectator::ValueStub.new(:method1, "override") - expect { fake._spectator_define_stub(stub) }.to change { fake.method1 }.from("stubbed").to("override") - end - - it "defines stubs from keyword arguments" do - expect(fake.method2).to eq(:stubbed) - end - - it "can stub methods from keyword arguments" do - stub = Spectator::ValueStub.new(:method2, :override) - expect { fake._spectator_define_stub(stub) }.to change { fake.method2 }.from(:stubbed).to(:override) - end - - it "raises on undefined stubs" do - expect { fake.method3 }.to raise_error(Spectator::UnexpectedMessage, /method3/) - end - - it "can defer stubs on previously undefined stubs" do - stub = Spectator::ValueStub.new(:method3, 42) - fake._spectator_define_stub(stub) - expect(fake.method3).to eq(42) - end - - it "raises on abstract stubs" do - expect { fake.method4 }.to raise_error(Spectator::UnexpectedMessage, /method4/) - end - - it "can defer stubs on abstract stubs" do - stub = Spectator::ValueStub.new(:method4, :abstract) - fake._spectator_define_stub(stub) - expect(fake.method4).to eq(:abstract) - end - - it "defines stubs with yield in the block" do - stub = Spectator::ValueStub.new(:method5, :block) - fake._spectator_define_stub(stub) - expect(fake.method5 { :wrong }).to eq(:block) - end - - it "defines stubs with yield from keyword arguments" do - expect(fake.method6 { :wrong }).to eq(:kwargs) - end - - it "can stub methods with yield from keyword arguments" do - stub = Spectator::ValueStub.new(:method6, :override) - expect { fake._spectator_define_stub(stub) }.to change { fake.method6 { :wrong } }.from(:kwargs).to(:override) - end - - it "handles arguments correctly with stubs" do - stub1 = Spectator::ProcStub.new(:method7, args_proc) - stub2 = Spectator::ProcStub.new(:method8, args_proc) - fake._spectator_define_stub(stub1) - fake._spectator_define_stub(stub2) - args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) - args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) # { :block } - aggregate_failures do - expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - end - end - - it "compiles types without unions" do - stub = Spectator::ValueStub.new(:method3, 42) - fake._spectator_define_stub(stub) - - aggregate_failures do - expect(fake.method1).to compile_as(String) - expect(fake.method2).to compile_as(Symbol) - expect(fake.method3).to compile_as(Int32) - expect(fake.method5 { :foo }).to compile_as(Symbol) - expect(fake.method6 { :foo }).to compile_as(Symbol) - end - end - - def restricted(thing : AbstractClass) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(fake)).to eq("stubbed") - end - - # Cannot test unexpected messages - will not compile due to missing methods. - - describe "deferred default stubs" do - mock(AbstractClass) do - # NOTE: Abstract methods without a type restriction on the return value - # must be implemented with a type restriction. - abstract_stub abstract def method1 : String - - # NOTE: Defining the stub here with a return type restriction, but no default implementation. - abstract_stub abstract def method4 : Symbol - - # NOTE: Abstract methods that yield must have yield functionality defined in the method. - # This requires that yielding methods have a default implementation. - # Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition. - stub def method5(&) - yield - end - - # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - stub def method6(&) : Symbol - yield - end - end - - let(fake2) do - mock(AbstractClass, - method1: "stubbed", - method2: :stubbed, - method3: 123, - method4: :xyz, - method5: :abc, - method6: :bar) - end - - it "uses the keyword arguments as stubs" do - aggregate_failures do - expect(fake2.method1).to eq("stubbed") - expect(fake2.method2).to eq(:stubbed) - expect(fake2.method3).to eq(123) - expect(fake2.method4).to eq(:xyz) - expect(fake2.method5 { :foo }).to eq(:abc) - expect(fake2.method6 { :foo }).to eq(:bar) - end - end - end - end - - context "with an abstract struct" do - abstract struct AbstractStruct - abstract def method1 - - abstract def method2 : Symbol - - abstract def method3 : Int32 - - abstract def method4 - - abstract def method5(&) - - abstract def method6(&) : Symbol - - abstract def method7(arg, *args, kwarg, **kwargs) - - abstract def method8(arg, *args, kwarg, **kwargs, &) - end - - # method1 stubbed via mock block - # method2 stubbed via keyword args - # method3 not stubbed (raises) - # method4 not stubbed, but type defined via mock block - # method5 stubbed via mock block (yields) - # method6 stubbed via keyword args (yields) - # method7 not stubbed (calls original) testing args - # method8 not stubbed (calls original and yields) testing args - mock(AbstractStruct, method2: :stubbed, method6: :kwargs) do - # NOTE: Abstract methods without a type restriction on the return value - # must be implemented with a type restriction. - stub def method1 : String - "stubbed" - end - - # NOTE: Defining the stub here with a return type restriction, but no default implementation. - abstract_stub abstract def method4 : Symbol - - # NOTE: Abstract methods that yield must have yield functionality defined in the method. - # This requires that yielding methods have a default implementation. - # Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition. - stub def method5(&) - yield - end - - # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - stub def method6(&) : Symbol - yield - end - - # NOTE: Defining the stub here with a return type restriction, but no default implementation. - abstract_stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments - - # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - abstract_stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments - end - - subject(fake) { mock(AbstractStruct) } - - it "defines a subtype" do - expect(fake).to be_a(AbstractStruct) - end - - it "defines stubs in the block" do - expect(fake.method1).to eq("stubbed") - end - - it "can stub methods defined in the block" do - stub = Spectator::ValueStub.new(:method1, "override") - expect { fake._spectator_define_stub(stub) }.to change { fake.method1 }.from("stubbed").to("override") - end - - it "defines stubs from keyword arguments" do - expect(fake.method2).to eq(:stubbed) - end - - it "can stub methods from keyword arguments" do - stub = Spectator::ValueStub.new(:method2, :override) - expect { fake._spectator_define_stub(stub) }.to change { fake.method2 }.from(:stubbed).to(:override) - end - - it "raises on undefined stubs" do - expect { fake.method3 }.to raise_error(Spectator::UnexpectedMessage, /method3/) - end - - it "can defer stubs on previously undefined stubs" do - stub = Spectator::ValueStub.new(:method3, 42) - fake._spectator_define_stub(stub) - expect(fake.method3).to eq(42) - end - - it "raises on abstract stubs" do - expect { fake.method4 }.to raise_error(Spectator::UnexpectedMessage, /method4/) - end - - it "can defer stubs on abstract stubs" do - stub = Spectator::ValueStub.new(:method4, :abstract) - fake._spectator_define_stub(stub) - expect(fake.method4).to eq(:abstract) - end - - it "defines stubs with yield in the block" do - stub = Spectator::ValueStub.new(:method5, :block) - fake._spectator_define_stub(stub) - expect(fake.method5 { :wrong }).to eq(:block) - end - - it "defines stubs with yield from keyword arguments" do - expect(fake.method6 { :wrong }).to eq(:kwargs) - end - - it "can stub methods with yield from keyword arguments" do - stub = Spectator::ValueStub.new(:method6, :override) - expect { fake._spectator_define_stub(stub) }.to change { fake.method6 { :wrong } }.from(:kwargs).to(:override) - end - - it "handles arguments correctly with stubs" do - stub1 = Spectator::ProcStub.new(:method7, args_proc) - stub2 = Spectator::ProcStub.new(:method8, args_proc) - fake._spectator_define_stub(stub1) - fake._spectator_define_stub(stub2) - args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) - args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) # { :block } - aggregate_failures do - expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - end - end - - it "compiles types without unions" do - stub = Spectator::ValueStub.new(:method3, 42) - fake._spectator_define_stub(stub) - - aggregate_failures do - expect(fake.method1).to compile_as(String) - expect(fake.method2).to compile_as(Symbol) - expect(fake.method3).to compile_as(Int32) - expect(fake.method5 { :foo }).to compile_as(Symbol) - expect(fake.method6 { :foo }).to compile_as(Symbol) - end - end - - def restricted(thing : AbstractStruct) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(fake)).to eq("stubbed") - end - - # Cannot test unexpected messages - will not compile due to missing methods. - - describe "deferred default stubs" do - mock(AbstractStruct) do - # NOTE: Abstract methods without a type restriction on the return value - # must be implemented with a type restriction. - abstract_stub abstract def method1 : String - - # NOTE: Defining the stub here with a return type restriction, but no default implementation. - abstract_stub abstract def method4 : Symbol - - # NOTE: Abstract methods that yield must have yield functionality defined in the method. - # This requires that yielding methods have a default implementation. - # Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition. - stub def method5(&) - yield - end - - # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - stub def method6(&) : Symbol - yield - end - end - - let(fake2) do - mock(AbstractStruct, - method1: "stubbed", - method2: :stubbed, - method3: 123, - method4: :xyz, - method5: :abc, - method6: :bar) - end - - it "uses the keyword arguments as stubs" do - aggregate_failures do - expect(fake2.method1).to eq("stubbed") - expect(fake2.method2).to eq(:stubbed) - expect(fake2.method3).to eq(123) - expect(fake2.method4).to eq(:xyz) - expect(fake2.method5 { :foo }).to eq(:abc) - expect(fake2.method6 { :foo }).to eq(:bar) - end - end - end - end - - context "with a semi-abstract struct" do - abstract struct SemiAbstractStruct - def method1 - "original" - end - - def method2 : Symbol - :original - end - - def method3(&) - yield - end - - def method4(&) : Int32 - yield.to_i - end - - def method5 - 42 - end - end - - # method1 stubbed via mock block - # method2 stubbed via keyword args - # method3 not stubbed (calls original and yields) - # method4 stubbed via keyword args (yields) - # method5 not stubbed (calls original) - mock(SemiAbstractStruct, method2: :stubbed, method4: 123) do - stub def method1 - "stubbed" - end - end - - subject(fake) { mock(SemiAbstractStruct) } - - it "defines a subtype" do - expect(fake).to be_a(SemiAbstractStruct) - end - - it "defines stubs in the block" do - expect(fake.method1).to eq("stubbed") - end - - it "can stub methods defined in the block" do - stub = Spectator::ValueStub.new(:method1, "override") - expect { fake._spectator_define_stub(stub) }.to change { fake.method1 }.from("stubbed").to("override") - end - - it "defines stubs from keyword arguments" do - expect(fake.method2).to eq(:stubbed) - end - - it "can stub methods from keyword arguments" do - stub = Spectator::ValueStub.new(:method2, :override) - expect { fake._spectator_define_stub(stub) }.to change { fake.method2 }.from(:stubbed).to(:override) - end - - it "calls the original method with yielding methods" do - expect(fake.method3 { :block }).to eq(:block) - end - - it "can defer defining stubs with yielding methods" do - stub = Spectator::ValueStub.new(:method3, :new) - expect { fake._spectator_define_stub(stub) }.to change { fake.method3 { :old } }.from(:old).to(:new) - end - - it "defines stubs with yield from keyword arguments" do - expect(fake.method4 { "42" }).to eq(123) - end - - it "defines stubs with yield in the block" do - stub = Spectator::ValueStub.new(:method4, 5) - expect { fake._spectator_define_stub(stub) }.to change { fake.method4 { "42" } }.from(123).to(5) - end - - it "calls the original method" do - expect(fake.method5).to eq(42) - end - - it "can defer defining stubs" do - stub = Spectator::ValueStub.new(:method5, 123) - expect { fake._spectator_define_stub(stub) }.to change { fake.method5 }.from(42).to(123) - end - - it "compiles types without unions" do - aggregate_failures do - expect(fake.method1).to compile_as(String) - expect(fake.method2).to compile_as(Symbol) - expect(fake.method3 { :foo }).to compile_as(Symbol) - expect(fake.method4 { "42" }).to compile_as(Int32) - expect(fake.method5).to compile_as(Int32) - end - end - - def restricted(thing : SemiAbstractStruct) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(fake)).to eq("stubbed") - end - - # Cannot test unexpected messages - will not compile due to missing methods. - - describe "deferred default stubs" do - mock(SemiAbstractStruct) - - let(fake2) do - mock(SemiAbstractStruct, - method1: "stubbed", - method2: :stubbed, - method3: :kwargs, - method4: 123, - method5: 0) - end - - it "uses the keyword arguments as stubs" do - aggregate_failures do - expect(fake2.method1).to eq("stubbed") - expect(fake2.method2).to eq(:stubbed) - expect(fake2.method3 { :foo }).to eq(:kwargs) - expect(fake2.method4 { "42" }).to eq(123) - expect(fake2.method5).to eq(0) - end - end - end - end - - context "with a concrete struct" do - struct ConcreteStruct - def method1 - "original" - end - - def method2 : Symbol - :original - end - - def method3(&) - yield - end - - def method4(&) : Int32 - yield.to_i - end - - def method5 - 42 - end - end - - # method1 stubbed via mock block - # method2 stubbed via keyword args - # method3 not stubbed (calls original and yields) - # method4 stubbed via keyword args (yields) - # method5 not stubbed (calls original) - inject_mock(ConcreteStruct, method2: :stubbed, method4: 123) do - stub def method1 - "stubbed" - end - end - - subject(real) { mock(ConcreteStruct) } - - it "defines a subtype" do - expect(real).to be_a(ConcreteStruct) - end - - it "defines stubs in the block" do - expect(real.method1).to eq("stubbed") - end - - it "can stub methods defined in the block" do - stub = Spectator::ValueStub.new(:method1, "override") - expect { real._spectator_define_stub(stub) }.to change { real.method1 }.from("stubbed").to("override") - end - - it "defines stubs from keyword arguments" do - expect(real.method2).to eq(:stubbed) - end - - it "can stub methods from keyword arguments" do - stub = Spectator::ValueStub.new(:method2, :override) - expect { real._spectator_define_stub(stub) }.to change { real.method2 }.from(:stubbed).to(:override) - end - - it "calls the original method with yielding methods" do - expect(real.method3 { :block }).to eq(:block) - end - - it "can defer defining stubs with yielding methods" do - stub = Spectator::ValueStub.new(:method3, :new) - expect { real._spectator_define_stub(stub) }.to change { real.method3 { :old } }.from(:old).to(:new) - end - - it "defines stubs with yield from keyword arguments" do - expect(real.method4 { "42" }).to eq(123) - end - - it "defines stubs with yield in the block" do - stub = Spectator::ValueStub.new(:method4, 5) - expect { real._spectator_define_stub(stub) }.to change { real.method4 { "42" } }.from(123).to(5) - end - - it "calls the original method" do - expect(real.method5).to eq(42) - end - - it "can defer defining stubs" do - stub = Spectator::ValueStub.new(:method5, 123) - expect { real._spectator_define_stub(stub) }.to change { real.method5 }.from(42).to(123) - end - - it "compiles types without unions" do - aggregate_failures do - expect(real.method1).to compile_as(String) - expect(real.method2).to compile_as(Symbol) - expect(real.method3 { :foo }).to compile_as(Symbol) - expect(real.method4 { "42" }).to compile_as(Int32) - expect(real.method5).to compile_as(Int32) - end - end - - def restricted(thing : ConcreteStruct) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(real)).to eq("stubbed") - end - - # Cannot test unexpected messages - will not compile due to missing methods. - - describe "deferred default stubs" do - let(real) do - mock(ConcreteStruct, - method1: "stubbed", - method2: :stubbed, - method3: :kwargs, - method4: 123, - method5: 0) - end - - it "uses the keyword arguments as stubs" do - aggregate_failures do - expect(real.method1).to eq("stubbed") - expect(real.method2).to eq(:stubbed) - expect(real.method3 { :foo }).to eq(:kwargs) - expect(real.method4 { "42" }).to eq(123) - expect(real.method5).to eq(0) - end - end - end - end - - describe "scope" do - class Scope - def scope - :original - end - end - - mock(Scope, scope: :outer) - - it "finds a mock in the same scope" do - fake = mock(Scope) - expect(fake.scope).to eq(:outer) - end - - context "inner1" do - mock(Scope, scope: :inner) - - it "uses the innermost defined mock" do - fake = mock(Scope) - expect(fake.scope).to eq(:inner) - end - - context "nested1" do - mock(Scope, scope: :nested) - - it "uses the nested defined mock" do - fake = mock(Scope) - expect(fake.scope).to eq(:nested) - end - end - - context "nested2" do - it "finds a mock from a parent scope" do - fake = mock(Scope) - expect(fake.scope).to eq(:inner) - end - end - end - - context "inner2" do - it "finds a mock from a parent scope" do - fake = mock(Scope) - expect(fake.scope).to eq(:outer) - end - - context "nested3" do - it "finds a mock from a grandparent scope" do - fake = mock(Scope) - expect(fake.scope).to eq(:outer) - end - end - end - end - - describe "context" do - abstract class Dummy - abstract def predefined : Symbol - - abstract def override : Symbol - - abstract def memoize : Symbol - - def inline : Symbol - :original - end - - def reference : String - memoize.to_s - end - end - - mock(Dummy, predefined: :predefined, override: :predefined) do - stub def inline : Symbol - :inline # Memoized values can't be used here. - end - end - - let(memoize) { :memoize } - let(override) { :override } - let(fake) { mock(Dummy, override: override) } - - before { allow(fake).to receive(:memoize).and_return(memoize) } - - it "doesn't change predefined values" do - expect(fake.predefined).to eq(:predefined) - end - - it "can use memoized values for overrides" do - expect(fake.override).to eq(:override) - end - - it "can use memoized values for stubs" do - expect(fake.memoize).to eq(:memoize) - end - - it "can override inline stubs" do - expect { allow(fake).to receive(:inline).and_return(override) }.to change { fake.inline }.from(:inline).to(:override) - end - - it "can reference memoized values with indirection" do - expect { allow(fake).to receive(:memoize).and_return(override) }.to change { fake.reference }.from("memoize").to("override") - end - end - - describe "class mock" do - abstract class Dummy - def self.abstract_method - :not_really_abstract - end - - def self.default_method - :original - end - - def self.args(arg) - arg - end - - def self.method1 - :original - end - - def self.reference - method1.to_s - end - end - - mock(Dummy) do - abstract_stub def self.abstract_method - :abstract - end - - stub def self.default_method - :default - end - end - - let(fake) { class_mock(Dummy) } - - it "raises on abstract stubs" do - expect { fake.abstract_method }.to raise_error(Spectator::UnexpectedMessage, /abstract_method/) - end - - it "can define default stubs" do - expect(fake.default_method).to eq(:default) - end - - it "can define new stubs" do - expect { allow(fake).to receive(:args).and_return(42) }.to change { fake.args(5) }.from(5).to(42) - end - - it "can override class method stubs" do - allow(fake).to receive(:method1).and_return(:override) - expect(fake.method1).to eq(:override) - end - - it "can reference stubs" do - allow(fake).to receive(:method1).and_return(:reference) - expect(fake.reference).to eq("reference") - end - end - - describe "mock module" do - module Dummy - # `extend self` cannot be used. - # The Crystal compiler doesn't report the methods as class methods when doing so. - - def self.abstract_method - :not_really_abstract - end - - def self.default_method - :original - end - - def self.args(arg) - arg - end - - def self.method1 - :original - end - - def self.reference - method1.to_s - end - end - - mock(Dummy) do - abstract_stub def self.abstract_method - :abstract - end - - stub def self.default_method - :default - end - end - - let(fake) { class_mock(Dummy) } - - it "raises on abstract stubs" do - expect { fake.abstract_method }.to raise_error(Spectator::UnexpectedMessage, /abstract_method/) - end - - it "can define default stubs" do - expect(fake.default_method).to eq(:default) - end - - it "can define new stubs" do - expect { allow(fake).to receive(:args).and_return(42) }.to change { fake.args(5) }.from(5).to(42) - end - - it "can override class method stubs" do - allow(fake).to receive(:method1).and_return(:override) - expect(fake.method1).to eq(:override) - end - - xit "can reference stubs", pending: "Default stub of module class methods always refer to original" do - allow(fake).to receive(:method1).and_return(:reference) - expect(fake.reference).to eq("reference") - end - end - - context "with a class including a mocked module" do - module Dummy - getter _spectator_invocations = [] of Symbol - - def method1 - @_spectator_invocations << :method1 - "original" - end - - def method2 : Symbol - @_spectator_invocations << :method2 - :original - end - - def method3(arg) - @_spectator_invocations << :method3 - arg - end - - def method4(&) : Symbol - @_spectator_invocations << :method4 - yield - end - - def method5(&) - @_spectator_invocations << :method5 - yield.to_i - end - - def method6(&) - @_spectator_invocations << :method6 - yield - end - - def method7(arg, *args, kwarg, **kwargs) - @_spectator_invocations << :method7 - {arg, args, kwarg, kwargs} - end - - def method8(arg, *args, kwarg, **kwargs, &) - @_spectator_invocations << :method8 - yield - {arg, args, kwarg, kwargs} - end - end - - # method1 stubbed via mock block - # method2 stubbed via keyword args - # method3 not stubbed (calls original) - # method4 stubbed via mock block (yields) - # method5 stubbed via keyword args (yields) - # method6 not stubbed (calls original and yields) - # method7 not stubbed (calls original) testing args - # method8 not stubbed (calls original and yields) testing args - mock(Dummy, method2: :stubbed, method5: 42) do - stub def method1 - "stubbed" - end - - stub def method4(&) : Symbol - yield - :block - end - end - - subject(fake) { mock(Dummy) } - - it "defines a subclass" do - expect(fake).to be_a(Dummy) - end - - it "defines stubs in the block" do - expect(fake.method1).to eq("stubbed") - end - - it "can stub methods defined in the block" do - stub = Spectator::ValueStub.new(:method1, "override") - expect { fake._spectator_define_stub(stub) }.to change { fake.method1 }.from("stubbed").to("override") - end - - it "defines stubs from keyword arguments" do - expect(fake.method2).to eq(:stubbed) - end - - it "can stub methods from keyword arguments" do - stub = Spectator::ValueStub.new(:method2, :override) - expect { fake._spectator_define_stub(stub) }.to change { fake.method2 }.from(:stubbed).to(:override) - end - - it "calls the original implementation for methods not provided a stub" do - expect(fake.method3(:xyz)).to eq(:xyz) - end - - it "can stub methods after declaration" do - stub = Spectator::ValueStub.new(:method3, :abc) - expect { fake._spectator_define_stub(stub) }.to change { fake.method3(:xyz) }.from(:xyz).to(:abc) - end - - it "defines stubs with yield in the block" do - expect(fake.method4 { :wrong }).to eq(:block) - end - - it "can stub methods with yield in the block" do - stub = Spectator::ValueStub.new(:method4, :override) - expect { fake._spectator_define_stub(stub) }.to change { fake.method4 { :wrong } }.from(:block).to(:override) - end - - it "defines stubs with yield from keyword arguments" do - expect(fake.method5 { :wrong }).to eq(42) - end - - it "can stub methods with yield from keyword arguments" do - stub = Spectator::ValueStub.new(:method5, 123) - expect { fake._spectator_define_stub(stub) }.to change { fake.method5 { "0" } }.from(42).to(123) - end - - it "can stub yielding methods after declaration" do - stub = Spectator::ValueStub.new(:method6, :abc) - expect { fake._spectator_define_stub(stub) }.to change { fake.method6 { :xyz } }.from(:xyz).to(:abc) - end - - it "handles arguments correctly" do - args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) - args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block } - aggregate_failures do - expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - end - end - - it "handles arguments correctly with stubs" do - stub1 = Spectator::ProcStub.new(:method7, args_proc) - stub2 = Spectator::ProcStub.new(:method8, args_proc) - fake._spectator_define_stub(stub1) - fake._spectator_define_stub(stub2) - args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) - args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block } - aggregate_failures do - expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}}) - end - end - - it "compiles types without unions" do - aggregate_failures do - expect(fake.method1).to compile_as(String) - expect(fake.method2).to compile_as(Symbol) - expect(fake.method3(42)).to compile_as(Int32) - expect(fake.method4 { :foo }).to compile_as(Symbol) - expect(fake.method5 { "123" }).to compile_as(Int32) - expect(fake.method6 { "123" }).to compile_as(String) - end - end - - def restricted(thing : Dummy) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(fake)).to eq("stubbed") - end - - it "does not call the original method when stubbed" do - fake.method1 - fake.method2 - fake.method3("foo") - fake.method4 { :foo } - fake.method5 { "42" } - fake.method6 { 42 } - fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) - fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block } - expect(fake._spectator_invocations).to contain_exactly(:method3, :method6, :method7, :method8) - end - - # Cannot test unexpected messages - will not compile due to missing methods. - - describe "deferred default stubs" do - mock(Dummy) - - let(fake2) do - mock(Dummy, - method1: "stubbed", - method3: 123, - method4: :xyz) - end - - it "uses the keyword arguments as stubs" do - aggregate_failures do - expect(fake2.method1).to eq("stubbed") - expect(fake2.method2).to eq(:original) - expect(fake2.method3(42)).to eq(123) - expect(fake2.method4 { :foo }).to eq(:xyz) - end - end - end - end -end diff --git a/spec/spectator/dsl/mocks/null_double_spec.cr b/spec/spectator/dsl/mocks/null_double_spec.cr deleted file mode 100644 index 06d35ee..0000000 --- a/spec/spectator/dsl/mocks/null_double_spec.cr +++ /dev/null @@ -1,189 +0,0 @@ -require "../../../spec_helper" - -Spectator.describe "Null double DSL" do - context "specifying methods as keyword args" do - double(:test, foo: "foobar", bar: 42) - subject(dbl) { double(:test).as_null_object } - - it "defines a double with methods" do - aggregate_failures do - expect(dbl.foo).to eq("foobar") - expect(dbl.bar).to eq(42) - end - end - - it "compiles types without unions" do - aggregate_failures do - expect(dbl.foo).to compile_as(String) - expect(dbl.bar).to compile_as(Int32) - end - end - - it "returns self for unexpected messages" do - expect(dbl.baz).to be(dbl) - end - - context "blocks" do - it "supports blocks" do - aggregate_failures do - expect(dbl.foo { nil }).to eq("foobar") - expect(dbl.bar { nil }).to eq(42) - end - end - - it "supports blocks and has non-union return types" do - aggregate_failures do - expect(dbl.foo { nil }).to compile_as(String) - expect(dbl.bar { nil }).to compile_as(Int32) - end - end - - it "returns self on undefined messages" do - expect(dbl.baz { nil }).to be(dbl) - end - end - end - - context "block with stubs" do - context "one method" do - double(:test2) do - stub def foo - "one method" - end - end - - subject(dbl) { double(:test2).as_null_object } - - it "defines a double with methods" do - expect(dbl.foo).to eq("one method") - end - - it "compiles types without unions" do - expect(dbl.foo).to compile_as(String) - end - end - - context "two methods" do - double(:test3) do - stub def foo - "two methods" - end - - stub def bar - 42 - end - end - - subject(dbl) { double(:test3).as_null_object } - - it "defines a double with methods" do - aggregate_failures do - expect(dbl.foo).to eq("two methods") - expect(dbl.bar).to eq(42) - end - end - - it "compiles types without unions" do - aggregate_failures do - expect(dbl.foo).to compile_as(String) - expect(dbl.bar).to compile_as(Int32) - end - end - end - - context "empty block" do - double(:test4) do - end - - subject(dbl) { double(:test4).as_null_object } - - it "defines a double" do - expect(dbl).to be_a(Spectator::Double) - end - end - - context "stub-less method" do - double(:test5) do - def foo - "no stub" - end - end - - subject(dbl) { double(:test5).as_null_object } - - it "defines a double with methods" do - expect(dbl.foo).to eq("no stub") - end - end - - context "mixing keyword arguments" do - double(:test6, foo: "kwargs", bar: 42) do - stub def foo - "block" - end - - stub def baz - "block" - end - - stub def baz(value) - "block2" - end - end - - subject(dbl) { double(:test6).as_null_object } - - it "overrides the keyword arguments with the block methods" do - expect(dbl.foo).to eq("block") - end - - it "falls back to the keyword argument value for mismatched arguments" do - expect(dbl.foo(42)).to eq("kwargs") - end - - it "can call methods defined only by keyword arguments" do - expect(dbl.bar).to eq(42) - end - - it "can call methods defined only by the block" do - expect(dbl.baz).to eq("block") - end - - it "can call methods defined by the block with different signatures" do - expect(dbl.baz(42)).to eq("block2") - end - end - - context "methods accepting blocks" do - double(:test7) do - stub def foo(&) - yield - end - - stub def bar(& : Int32 -> String) - yield 42 - end - end - - subject(dbl) { double(:test7).as_null_object } - - it "defines the method and yields" do - expect(dbl.foo { :xyz }).to eq(:xyz) - end - - it "matches methods with block argument type restrictions" do - expect(dbl.bar &.to_s).to eq("42") - end - end - end - - describe "predefined method stubs" do - double(:test8, foo: 42) - - let(dbl) { double(:test8, foo: 7).as_null_object } - - it "overrides the original value" do - expect(dbl.foo).to eq(7) - end - end -end diff --git a/spec/spectator/dsl/mocks/stub_spec.cr b/spec/spectator/dsl/mocks/stub_spec.cr deleted file mode 100644 index a2e0c8d..0000000 --- a/spec/spectator/dsl/mocks/stub_spec.cr +++ /dev/null @@ -1,116 +0,0 @@ -require "../../../spec_helper" - -Spectator.describe "Stub DSL", :smoke do - double(:foobar, foo: 42, bar: "baz") do - stub abstract def other : String - stub abstract def null : Nil - end - - let(dbl) { double(:foobar) } - - it "overrides default stubs" do - allow(dbl).to receive(:foo).and_return(123) - expect(dbl.foo).to eq(123) - end - - it "overrides abstract stubs" do - allow(dbl).to receive(:other).and_return("test") - expect(dbl.other).to eq("test") - end - - it "returns nil by default" do - allow(dbl).to receive(:null) - expect(dbl.null).to be_nil - end - - it "raises on cast errors" do - allow(dbl).to receive(:foo).and_return(:xyz) - expect { dbl.foo }.to raise_error(TypeCastError, /Int32/) - end - - describe "#receive" do - it "returns the value from the block" do - allow(dbl).to receive(:foo) { 5 } - expect(dbl.foo).to eq(5) - end - - it "accepts and calls block" do - count = 0 - allow(dbl).to receive(:foo) { count += 1 } - expect { dbl.foo }.to change { count }.from(0).to(1) - end - - it "passes the arguments to the block" do - captured = nil.as(Spectator::AbstractArguments?) - allow(dbl).to receive(:foo) { |a| captured = a; 0 } - dbl.foo(3, 5, 7, bar: "baz") - args = Spectator::Arguments.capture(3, 5, 7, bar: "baz") - expect(captured).to eq(args) - end - end - - describe "#with" do - context Spectator::MultiValueStub do - it "applies the stub with matching arguments" do - allow(dbl).to receive(:foo).and_return(1, 2, 3).with(Int32, bar: /baz/) - aggregate_failures do - expect(dbl.foo(3, bar: "foobarbaz")).to eq(1) - expect(dbl.foo).to eq(42) - expect(dbl.foo(5, bar: "barbaz")).to eq(2) - expect(dbl.foo(7, bar: "foobaz")).to eq(3) - expect(dbl.foo(11, bar: "baz")).to eq(3) - end - end - end - - context Spectator::NullStub do - it "applies the stub with matching arguments" do - allow(dbl).to receive(:foo).with(Int32, bar: /baz/).and_return(1) - aggregate_failures do - expect(dbl.foo(3, bar: "foobarbaz")).to eq(1) - expect(dbl.foo).to eq(42) - end - end - - it "changes to a proc stub" do - called = 0 - allow(dbl).to receive(:foo).with(Int32, bar: /baz/) { called += 1 } - aggregate_failures do - expect { dbl.foo(3, bar: "foobarbaz") }.to change { called }.from(0).to(1) - expect(dbl.foo(5, bar: "baz")).to eq(2) - expect(dbl.foo).to eq(42) - end - end - end - - context Spectator::ValueStub do - it "applies the stub with matching arguments" do - allow(dbl).to receive(:foo).and_return(1).with(Int32, bar: /baz/) - aggregate_failures do - expect(dbl.foo(3, bar: "foobarbaz")).to eq(1) - expect(dbl.foo).to eq(42) - end - end - end - end - - describe "#no_args" do - it "defines a stub with a no arguments constraint" do - allow(dbl).to receive(:foo).with(no_args).and_return(5) - aggregate_failures do - expect(dbl.foo).to eq(5) - expect(dbl.foo(0)).to eq(42) - end - end - end - - describe "#any_args" do - it "defines a stub with no arguments constraint" do - allow(dbl).to receive(:foo).with(any_args).and_return(5) - aggregate_failures do - expect(dbl.foo).to eq(5) - expect(dbl.foo(0)).to eq(5) - end - end - end -end diff --git a/spec/spectator/lazy_spec.cr b/spec/spectator/lazy_spec.cr deleted file mode 100644 index 0f138bd..0000000 --- a/spec/spectator/lazy_spec.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator::Lazy do - it "returns the value of the block" do - lazy = Spectator::Lazy(Int32).new - expect { lazy.get { 42 } }.to eq(42) - end - - it "caches the value" do - lazy = Spectator::Lazy(Int32).new - count = 0 - expect { lazy.get { count += 1 } }.to change { count }.from(0).to(1) - expect { lazy.get { count += 1 } }.to_not change { count } - end -end diff --git a/spec/spectator/lazy_wrapper_spec.cr b/spec/spectator/lazy_wrapper_spec.cr deleted file mode 100644 index 94782b4..0000000 --- a/spec/spectator/lazy_wrapper_spec.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator::LazyWrapper do - it "returns the value of the block" do - expect { subject.get { 42 } }.to eq(42) - end - - it "caches the value" do - wrapper = described_class.new - count = 0 - expect { wrapper.get { count += 1 } }.to change { count }.from(0).to(1) - expect { wrapper.get { count += 1 } }.to_not change { count } - end - - # This type of nesting is used when `super` is called in a subject block. - # ``` - # subject { super.to_s } - # ``` - it "works with nested wrappers" do - outer = described_class.new - inner = described_class.new - value = outer.get do - inner.get { 42 }.to_s - end - expect(value).to eq("42") - expect(value).to be_a(String) - end -end diff --git a/spec/spectator/mocks/allow_spec.cr b/spec/spectator/mocks/allow_spec.cr deleted file mode 100644 index 090014e..0000000 --- a/spec/spectator/mocks/allow_spec.cr +++ /dev/null @@ -1,39 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::Allow do - let(dbl) { Spectator::LazyDouble.new(foo: 42) } - let(stub) { Spectator::ValueStub.new(:foo, 123) } - subject(alw) { Spectator::Allow.new(dbl) } - - describe "#to" do - it "applies a stub" do - expect { alw.to(stub) }.to change { dbl.foo }.from(42).to(123) - end - - context "leak" do - class Thing - def foo - 42 - end - end - - mock Thing - - getter(thing : Thing) { mock(Thing) } - - # Workaround type restrictions requiring a constant. - def fake - class_mock(Thing).cast(thing) - end - - specify do - expect { allow(fake).to(stub) }.to change { fake.foo }.from(42).to(123) - end - - # This example must be run after the previous (random order may break this). - it "clears the stub after the example completes" do - expect { fake.foo }.to eq(42) - end - end - end -end diff --git a/spec/spectator/mocks/arguments_spec.cr b/spec/spectator/mocks/arguments_spec.cr deleted file mode 100644 index f6a09b7..0000000 --- a/spec/spectator/mocks/arguments_spec.cr +++ /dev/null @@ -1,284 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::Arguments do - subject(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) } - - it "stores the arguments" do - expect(arguments).to have_attributes( - args: {42, "foo"}, - kwargs: {bar: "baz", qux: 123} - ) - end - - describe ".capture" do - subject { Spectator::Arguments.capture(42, "foo", bar: "baz", qux: 123) } - - it "stores the arguments and keyword arguments" do - is_expected.to have_attributes(args: {42, "foo"}, kwargs: {bar: "baz", qux: 123}) - end - end - - describe "#[](index)" do - it "returns a positional argument" do - aggregate_failures do - expect(arguments[0]).to eq(42) - expect(arguments[1]).to eq("foo") - end - end - end - - describe "#[](symbol)" do - it "returns a keyword argument" do - aggregate_failures do - expect(arguments[:bar]).to eq("baz") - expect(arguments[:qux]).to eq(123) - end - end - end - - describe "#to_s" do - subject { arguments.to_s } - - it "formats the arguments" do - is_expected.to eq("(42, \"foo\", bar: \"baz\", qux: 123)") - end - - context "when empty" do - let(arguments) { Spectator::Arguments.none } - - it "returns (no args)" do - is_expected.to eq("(no args)") - end - end - end - - describe "#==" do - subject { arguments == other } - - context "with Arguments" do - context "with equal arguments" do - let(other) { arguments } - - it { is_expected.to be_true } - end - - context "with different arguments" do - let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(other) { Spectator::Arguments.new(arguments.args, {qux: 123, bar: "baz"}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz"}) } - - it { is_expected.to be_false } - end - - context "with an extra kwarg" do - let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz", qux: 123, extra: 0}) } - - it { is_expected.to be_false } - end - end - - context "with FormalArguments" do - context "with equal arguments" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } - end - - context "with different arguments" do - let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {qux: 123, bar: "baz"}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz"}) } - - it { is_expected.to be_false } - end - - context "with an extra kwarg" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123, extra: 0}) } - - it { is_expected.to be_false } - end - - context "with different splat arguments" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_false } - end - - context "with mixed positional tuple types" do - let(other) { Spectator::FormalArguments.new({arg1: 42}, :splat, {"foo"}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } - end - end - end - - describe "#===" do - subject { pattern === arguments } - - context "with Arguments" do - context "with equal arguments" do - let(pattern) { arguments } - - it { is_expected.to be_true } - end - - context "with matching arguments" do - let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) } - - it { is_expected.to be_true } - end - - context "with non-matching arguments" do - let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) } - - it { is_expected.to be_false } - end - - context "with different arguments" do - let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(pattern) { Spectator::Arguments.new(arguments.args, {qux: Int32, bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with an additional kwarg" do - let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/, qux: Int32, extra: 0}) } - - it { is_expected.to be_false } - end - end - - context "with FormalArguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) } - - context "with equal arguments" do - let(pattern) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } - end - - context "with matching arguments" do - let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) } - - it { is_expected.to be_true } - end - - context "with non-matching arguments" do - let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) } - - it { is_expected.to be_false } - end - - context "with different arguments" do - let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(pattern) { Spectator::Arguments.new(arguments.positional, {qux: Int32, bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with an additional kwarg" do - let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/, qux: Int32, extra: 0}) } - - it { is_expected.to be_false } - end - - context "with different splat arguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) } - let(pattern) { Spectator::Arguments.new({Int32, /foo/, 5}, arguments.kwargs) } - - it { is_expected.to be_false } - end - - context "with matching mixed positional tuple types" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) } - let(pattern) { Spectator::Arguments.new({Int32, /foo/, 1, 2, 3}, arguments.kwargs) } - - it { is_expected.to be_true } - end - - context "with non-matching mixed positional tuple types" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) } - let(pattern) { Spectator::Arguments.new({Float64, /bar/, 3, 2, Symbol}, arguments.kwargs) } - - it { is_expected.to be_false } - end - - context "with matching args spilling over into splat and mixed positional tuple types" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(Int32, /foo/, Symbol, Symbol, :z, bar: /baz/, qux: Int32) } - - it { is_expected.to be_true } - end - - context "with non-matching args spilling over into splat and mixed positional tuple types" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(Float64, /bar/, Symbol, String, :z, bar: /foo/, qux: Int32) } - - it { is_expected.to be_false } - end - - context "with matching mixed named positional and keyword arguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(/foo/, Symbol, :y, Symbol, arg1: Int32, bar: /baz/, qux: 123) } - - it { is_expected.to be_true } - end - - context "with non-matching mixed named positional and keyword arguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(5, Symbol, :z, Symbol, arg2: /foo/, bar: /baz/, qux: Int32) } - - it { is_expected.to be_false } - end - - context "with non-matching mixed named positional and keyword arguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(/bar/, String, :y, Symbol, arg1: 0, bar: /foo/, qux: Float64) } - - it { is_expected.to be_false } - end - end - end -end diff --git a/spec/spectator/mocks/double_spec.cr b/spec/spectator/mocks/double_spec.cr deleted file mode 100644 index e55c549..0000000 --- a/spec/spectator/mocks/double_spec.cr +++ /dev/null @@ -1,542 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::Double do - Spectator::Double.define(EmptyDouble) - Spectator::Double.define(FooBarDouble, "dbl-name", foo: 42, bar: "baz") - - # The subject `dbl` must be carefully used in sub-contexts, otherwise it pollutes parent scopes. - # This changes the type of `dbl` to `Double+`, which produces a union of methods and their return types. - context "plain double" do - subject(dbl) { FooBarDouble.new } - - it "responds to defined messages" do - aggregate_failures do - expect(dbl.foo).to eq(42) - expect(dbl.bar).to eq("baz") - end - end - - it "fails on undefined messages" do - expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) - end - - it "reports the name in errors" do - expect { dbl.baz }.to raise_error(/"dbl-name"/) - end - - it "reports arguments" do - expect { dbl.baz(123, "qux", field: :value) }.to raise_error(Spectator::UnexpectedMessage, /\(123, "qux", field: :value\)/) - end - - it "has a non-union return type" do - aggregate_failures do - expect(dbl.foo).to compile_as(Int32) - expect(dbl.bar).to compile_as(String) - end - end - - it "uses nil for undefined messages" do - expect { dbl.baz }.to compile_as(Nil) - end - - context "blocks" do - it "supports blocks" do - aggregate_failures do - expect(dbl.foo { nil }).to eq(42) - expect(dbl.bar { nil }).to eq("baz") - end - end - - it "supports blocks and has non-union return types" do - aggregate_failures do - expect(dbl.foo { nil }).to compile_as(Int32) - expect(dbl.bar { nil }).to compile_as(String) - end - end - - it "fails on undefined messages" do - expect do - dbl.baz { nil } - end.to raise_error(Spectator::UnexpectedMessage, /baz/) - end - end - end - - context "without a double name" do - Spectator::Double.define(NamelessDouble, foo: 42) - - subject(dbl) { NamelessDouble.new } - - it "reports as anonymous" do - expect { dbl.baz }.to raise_error(/anonymous/i) - end - end - - context "with abstract stubs and return type annotations" do - Spectator::Double.define(TestDouble) do - abstract_stub abstract def foo(value) : String - end - - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) } - subject(dbl) { TestDouble.new([stub]) } - - it "enforces the return type" do - expect(dbl.foo("foobar")).to compile_as(String) - end - - it "raises on non-matching arguments" do - expect { dbl.foo("bar") }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - - it "raises on non-matching stub" do - stub = Spectator::ValueStub.new(:foo, 42, arguments).as(Spectator::Stub) - dbl._spectator_define_stub(stub) - expect { dbl.foo("foobar") }.to raise_error(TypeCastError, /String/) - end - end - - context "with nillable return type annotations" do - Spectator::Double.define(TestDouble) do - abstract_stub abstract def foo : String? - abstract_stub abstract def bar : Nil - end - - let(foo_stub) { Spectator::ValueStub.new(:foo, nil).as(Spectator::Stub) } - let(bar_stub) { Spectator::ValueStub.new(:bar, nil).as(Spectator::Stub) } - subject(dbl) { TestDouble.new([foo_stub, bar_stub]) } - - it "doesn't raise on nil" do - aggregate_failures do - expect(dbl.foo).to be_nil - expect(dbl.bar).to be_nil - end - end - end - - context "with a method that uses NoReturn" do - Spectator::Double.define(NoReturnDouble) do - abstract_stub abstract def oops : NoReturn - end - - subject(dbl) { NoReturnDouble.new } - - it "raises a TypeCastError when using a value-based stub" do - stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub) - dbl._spectator_define_stub(stub) - expect { dbl.oops }.to raise_error(TypeCastError, /NoReturn/) - end - - it "raises when using an exception stub" do - exception = ArgumentError.new("bogus") - stub = Spectator::ExceptionStub.new(:oops, exception).as(Spectator::Stub) - dbl._spectator_define_stub(stub) - expect { dbl.oops }.to raise_error(ArgumentError, "bogus") - end - end - - context "with common object methods" do - subject(dbl) do - EmptyDouble.new([ - Spectator::ValueStub.new(:"!=", false), - Spectator::ValueStub.new(:"!~", false), - Spectator::ValueStub.new(:"==", true), - Spectator::ValueStub.new(:"===", true), - Spectator::ValueStub.new(:"=~", nil), - Spectator::ValueStub.new(:class, EmptyDouble), - Spectator::ValueStub.new(:dup, EmptyDouble.new), - Spectator::ValueStub.new(:"in?", true), - Spectator::ValueStub.new(:inspect, "inspect"), - Spectator::ValueStub.new(:itself, EmptyDouble.new), - Spectator::ValueStub.new(:"not_nil!", EmptyDouble.new), - Spectator::ValueStub.new(:pretty_inspect, "pretty_inspect"), - Spectator::ValueStub.new(:tap, EmptyDouble.new), - Spectator::ValueStub.new(:to_json, "to_json"), - Spectator::ValueStub.new(:to_pretty_json, "to_pretty_json"), - Spectator::ValueStub.new(:to_s, "to_s"), - Spectator::ValueStub.new(:to_yaml, "to_yaml"), - Spectator::ValueStub.new(:try, nil), - Spectator::ValueStub.new(:object_id, 42_u64), - Spectator::ValueStub.new(:"same?", true), - ] of Spectator::Stub) - end - - it "responds with defined messages" do - aggregate_failures do - expect(dbl.!=(42)).to be_false - expect(dbl.!~(42)).to be_false - expect(dbl.==(42)).to be_true - expect(dbl.===(42)).to be_true - expect(dbl.=~(42)).to be_nil - expect(dbl.class).to eq(EmptyDouble) - expect(dbl.dup).to be_a(EmptyDouble) - expect(dbl.in?([42])).to eq(true) - expect(dbl.in?(1, 2, 3)).to eq(true) - expect(dbl.inspect).to eq("inspect") - expect(dbl.itself).to be_a(EmptyDouble) - expect(dbl.not_nil!).to be_a(EmptyDouble) - expect(dbl.pretty_inspect).to eq("pretty_inspect") - expect(dbl.tap { nil }).to be_a(EmptyDouble) - expect(dbl.to_json).to eq("to_json") - expect(dbl.to_pretty_json).to eq("to_pretty_json") - expect(dbl.to_s).to eq("to_s") - expect(dbl.to_yaml).to eq("to_yaml") - expect(dbl.try { nil }).to be_nil - expect(dbl.object_id).to eq(42_u64) - expect(dbl.same?(dbl)).to be_true - expect(dbl.same?(nil)).to be_true - end - end - - it "has a non-union return type" do - expect(dbl.inspect).to compile_as(String) - end - end - - context "without common object methods" do - subject(dbl) { EmptyDouble.new } - - it "returns original implementation with undefined messages" do - io = IO::Memory.new - pp = PrettyPrint.new(io) - hasher = Crystal::Hasher.new - aggregate_failures do - expect(dbl.!=(42)).to be_true - expect(dbl.!~(42)).to be_true - expect(dbl.==(42)).to be_false - expect(dbl.===(42)).to be_false - expect(dbl.=~(42)).to be_nil - expect(dbl.class).to eq(EmptyDouble) - expect(dbl.dup).to be_a(EmptyDouble) - expect(dbl.hash(hasher)).to be_a(Crystal::Hasher) - expect(dbl.hash).to be_a(UInt64) - expect(dbl.in?([42])).to be_false - expect(dbl.in?(1, 2, 3)).to be_false - expect(dbl.itself).to be(dbl) - expect(dbl.not_nil!).to be(dbl) - expect(dbl.pretty_print(pp)).to be_nil - expect(dbl.tap { nil }).to be(dbl) - expect(dbl.try { nil }).to be_nil - expect(dbl.object_id).to be_a(UInt64) - expect(dbl.same?(dbl)).to be_true - expect(dbl.same?(nil)).to be_false - end - end - - it "reports arguments when they don't match" do - expect { dbl.same?(123, :xyz) }.to raise_error(Spectator::UnexpectedMessage, /\(123, :xyz\)/) - end - end - - context "with arguments constraints" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - - context "without common object methods" do - Spectator::Double.define(TestDouble) do - abstract_stub abstract def foo(value) : String - abstract_stub abstract def foo(value, & : -> _) : String - end - - let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) } - subject(dbl) { TestDouble.new([stub]) } - - it "returns the response when constraint satisfied" do - expect(dbl.foo("foobar")).to eq("bar") - end - - it "raises an error when constraint unsatisfied" do - expect { dbl.foo("baz") }.to raise_error(Spectator::UnexpectedMessage) - end - - it "raises an error when argument count doesn't match" do - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage) - end - - it "has a non-union return type" do - expect(dbl.foo("foobar")).to compile_as(String) - end - - it "ignores the block argument if not in the constraint" do - expect(dbl.foo("foobar") { nil }).to eq("bar") - end - end - - context "with common object methods" do - Spectator::Double.define(TestDouble) do - stub abstract def same?(other : Reference) : Bool - end - - let(stub) { Spectator::ValueStub.new(:"same?", true, arguments).as(Spectator::Stub) } - subject(dbl) { TestDouble.new([stub]) } - - it "returns the response when constraint satisfied" do - expect(dbl.same?("foobar")).to eq(true) - end - - it "raises an error when constraint unsatisfied" do - expect { dbl.same?("baz") }.to raise_error(Spectator::UnexpectedMessage) - end - - it "raises an error when argument count doesn't match" do - expect { dbl.same? }.to raise_error(Spectator::UnexpectedMessage) - end - - it "has a non-union return type" do - expect(dbl.same?("foobar")).to compile_as(Bool) - end - end - end - - context "class method stubs" do - Spectator::Double.define(ClassDouble) do - stub def self.foo - :stub - end - - stub def self.bar(arg) - arg - end - - stub def self.baz(arg, &) - yield - end - end - - subject(dbl) { ClassDouble } - let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } - - after { dbl._spectator_clear_stubs } - - it "overrides an existing method" do - expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override) - end - - it "doesn't affect other methods" do - expect { dbl._spectator_define_stub(foo_stub) }.to_not change { dbl.bar(42) } - end - - it "replaces an existing stub" do - dbl._spectator_define_stub(foo_stub) - stub = Spectator::ValueStub.new(:foo, :replacement) - expect { dbl._spectator_define_stub(stub) }.to change { dbl.foo }.to(:replacement) - end - - it "picks the correct stub based on arguments" do - stub1 = Spectator::ValueStub.new(:bar, :fallback) - stub2 = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) - dbl._spectator_define_stub(stub1) - dbl._spectator_define_stub(stub2) - aggregate_failures do - expect(dbl.bar(:wrong)).to eq(:fallback) - expect(dbl.bar(:match)).to eq(:override) - end - end - - it "only uses a stub if an argument constraint is met" do - stub = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) - dbl._spectator_define_stub(stub) - aggregate_failures do - expect(dbl.bar(:original)).to eq(:original) - expect(dbl.bar(:match)).to eq(:override) - end - end - - it "ignores the block argument if not in the constraint" do - stub1 = Spectator::ValueStub.new(:baz, 1) - stub2 = Spectator::ValueStub.new(:baz, 2, Spectator::Arguments.capture(3)) - dbl._spectator_define_stub(stub1) - dbl._spectator_define_stub(stub2) - aggregate_failures do - expect(dbl.baz(5) { 42 }).to eq(1) - expect(dbl.baz(3) { 42 }).to eq(2) - end - end - - describe "._spectator_clear_stubs" do - before { dbl._spectator_define_stub(foo_stub) } - - it "removes previously defined stubs" do - expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub) - end - end - - describe "._spectator_calls" do - before { dbl._spectator_clear_calls } - - # Retrieves symbolic names of methods called on a double. - def called_method_names(dbl) - dbl._spectator_calls.map(&.method) - end - - it "stores calls to stubbed methods" do - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[]).to(%i[foo]) - end - - it "stores multiple calls to the same stub" do - dbl.foo - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[foo]).to(%i[foo foo]) - end - - it "stores arguments for a call" do - dbl.bar(42) - args = Spectator::Arguments.capture(42) - call = dbl._spectator_calls.first - expect(call.arguments).to eq(args) - end - end - end - - describe "#_spectator_define_stub" do - subject(dbl) { FooBarDouble.new } - let(stub3) { Spectator::ValueStub.new(:foo, 3) } - let(stub5) { Spectator::ValueStub.new(:foo, 5) } - let(stub7) { Spectator::ValueStub.new(:foo, 7, Spectator::Arguments.capture(:lucky)) } - - it "overrides an existing method" do - expect { dbl._spectator_define_stub(stub3) }.to change { dbl.foo }.from(42).to(3) - end - - it "replaces an existing stub" do - dbl._spectator_define_stub(stub3) - expect { dbl._spectator_define_stub(stub5) }.to change { dbl.foo }.from(3).to(5) - end - - it "doesn't affect other methods" do - expect { dbl._spectator_define_stub(stub5) }.to_not change { dbl.bar } - end - - it "picks the correct stub based on arguments" do - dbl._spectator_define_stub(stub5) - dbl._spectator_define_stub(stub7) - aggregate_failures do - expect(dbl.foo).to eq(5) - expect(dbl.foo(:lucky)).to eq(7) - end - end - - it "only uses a stub if an argument constraint is met" do - dbl._spectator_define_stub(stub7) - aggregate_failures do - expect(dbl.foo).to eq(42) - expect(dbl.foo(:lucky)).to eq(7) - end - end - - it "ignores the block argument if not in the constraint" do - dbl._spectator_define_stub(stub5) - dbl._spectator_define_stub(stub7) - aggregate_failures do - expect(dbl.foo { nil }).to eq(5) - expect(dbl.foo(:lucky) { nil }).to eq(7) - end - end - end - - describe "#_spectator_clear_stubs" do - subject(dbl) { FooBarDouble.new } - let(stub) { Spectator::ValueStub.new(:foo, 5) } - - before { dbl._spectator_define_stub(stub) } - - it "removes previously defined stubs" do - expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42) - end - end - - describe "#_spectator_calls" do - subject(dbl) { FooBarDouble.new } - let(stub) { Spectator::ValueStub.new(:foo, 5) } - - before { dbl._spectator_define_stub(stub) } - - # Retrieves symbolic names of methods called on a double. - def called_method_names(dbl) - dbl._spectator_calls.map(&.method) - end - - it "stores calls to stubbed methods" do - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[]).to(%i[foo]) - end - - it "stores multiple calls to the same stub" do - dbl.foo - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[foo]).to(%i[foo foo]) - end - - it "stores calls to non-stubbed methods" do - expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) - expect(called_method_names(dbl)).to contain(:baz) - end - - it "stores arguments for a call" do - dbl.foo(42) - args = Spectator::Arguments.capture(42) - call = dbl._spectator_calls.first - expect(call.arguments).to eq(args) - end - end - - describe "#to_s" do - subject(string) { dbl.to_s } - - context "with a name" do - let(dbl) { FooBarDouble.new } - - it "indicates it's a double" do - expect(string).to contain("Double") - end - - it "contains the double name" do - expect(string).to contain("dbl-name") - end - end - - context "without a name" do - let(dbl) { EmptyDouble.new } - - it "indicates it's a double" do - expect(string).to contain("Double") - end - - it "contains \"Anonymous\"" do - expect(string).to contain("Anonymous") - end - end - end - - describe "#inspect" do - subject(string) { dbl.inspect } - - context "with a name" do - let(dbl) { FooBarDouble.new } - - it "indicates it's a double" do - expect(string).to contain("Double") - end - - it "contains the double name" do - expect(string).to contain("dbl-name") - end - - it "contains the object ID" do - expect(string).to contain(dbl.object_id.to_s(16)) - end - end - - context "without a name" do - let(dbl) { EmptyDouble.new } - - it "indicates it's a double" do - expect(string).to contain("Double") - end - - it "contains \"Anonymous\"" do - expect(string).to contain("Anonymous") - end - - it "contains the object ID" do - expect(string).to contain(dbl.object_id.to_s(16)) - end - end - end -end diff --git a/spec/spectator/mocks/exception_stub_spec.cr b/spec/spectator/mocks/exception_stub_spec.cr deleted file mode 100644 index 7f45455..0000000 --- a/spec/spectator/mocks/exception_stub_spec.cr +++ /dev/null @@ -1,166 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::ExceptionStub do - let(method_call) { Spectator::MethodCall.capture(:foo) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(exception) { RuntimeError.new("Test exception") } - subject(stub) { described_class.new(:foo, exception, location: location) } - - it "stores the method name" do - expect(stub.method).to eq(:foo) - end - - it "stores the location" do - expect(stub.location).to eq(location) - end - - it "raises the specified exception" do - expect { stub.call(method_call) }.to raise_error(RuntimeError, "Test exception") - end - - context Spectator::StubModifiers do - describe "#and_return(value)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::ExceptionStub.new(:foo, exception, arguments, location) } - subject(stub) { original.and_return(123) } - - it "produces a stub that returns a value" do - expect(stub.call(method_call)).to eq(123) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_return(*values)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::ExceptionStub.new(:foo, exception, arguments, location) } - subject(stub) { original.and_return(3, 2, 1, 0) } - - it "produces a stub that returns values" do - values = Array.new(5) { stub.call(method_call) } - expect(values).to eq([3, 2, 1, 0, 0]) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_raise" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::ExceptionStub.new(:foo, exception, arguments, location) } - let(new_exception) { ArgumentError.new("Test argument error") } - subject(stub) { original.and_raise(new_exception) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - - context "with a class and message" do - subject(stub) { original.and_raise(ArgumentError, "Test argument error") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - end - - context "with a message" do - subject(stub) { original.and_raise("Test exception") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(Exception, "Test exception") - end - end - - context "with a class" do - subject(stub) { original.and_raise(ArgumentError) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError) - end - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - end - - describe "#===" do - subject { stub === call } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - - context "with a constraint" do - let(constraint) { Spectator::Arguments.capture(/foo/) } - let(stub) { Spectator::ValueStub.new(:foo, 42, constraint) } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - - context "with a non-matching arguments" do - let(call) { Spectator::MethodCall.capture(:foo, "baz") } - - it "returns false" do - is_expected.to be_false - end - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - end - end -end diff --git a/spec/spectator/mocks/formal_arguments_spec.cr b/spec/spectator/mocks/formal_arguments_spec.cr deleted file mode 100644 index 963b6eb..0000000 --- a/spec/spectator/mocks/formal_arguments_spec.cr +++ /dev/null @@ -1,325 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::FormalArguments do - subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it "stores the arguments" do - expect(arguments).to have_attributes( - args: {arg1: 42, arg2: "foo"}, - splat_name: :splat, - splat: {:x, :y, :z}, - kwargs: {bar: "baz", qux: 123} - ) - end - - describe ".build" do - subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) } - - it "stores the arguments and keyword arguments" do - is_expected.to have_attributes( - args: {arg1: 42, arg2: "foo"}, - splat_name: :splat, - splat: {1, 2, 3}, - kwargs: {bar: "baz", qux: 123} - ) - end - - context "without a splat" do - subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) } - - it "stores the arguments and keyword arguments" do - is_expected.to have_attributes( - args: {arg1: 42, arg2: "foo"}, - splat: nil, - kwargs: {bar: "baz", qux: 123} - ) - end - end - end - - describe "#[](index)" do - it "returns a positional argument" do - aggregate_failures do - expect(arguments[0]).to eq(42) - expect(arguments[1]).to eq("foo") - end - end - - it "returns splat arguments" do - aggregate_failures do - expect(arguments[2]).to eq(:x) - expect(arguments[3]).to eq(:y) - expect(arguments[4]).to eq(:z) - end - end - - context "with named positional arguments" do - subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it "returns a positional argument" do - aggregate_failures do - expect(arguments[0]).to eq(42) - expect(arguments[1]).to eq("foo") - end - end - - it "returns splat arguments" do - aggregate_failures do - expect(arguments[2]).to eq(:x) - expect(arguments[3]).to eq(:y) - expect(arguments[4]).to eq(:z) - end - end - end - end - - describe "#[](symbol)" do - it "returns a keyword argument" do - aggregate_failures do - expect(arguments[:bar]).to eq("baz") - expect(arguments[:qux]).to eq(123) - end - end - - context "with named positional arguments" do - subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it "returns a positional argument" do - aggregate_failures do - expect(arguments[:arg1]).to eq(42) - expect(arguments[:arg2]).to eq("foo") - end - end - - it "returns a keyword argument" do - aggregate_failures do - expect(arguments[:bar]).to eq("baz") - expect(arguments[:qux]).to eq(123) - end - end - end - end - - describe "#to_s" do - subject { arguments.to_s } - - it "formats the arguments" do - is_expected.to eq("(arg1: 42, arg2: \"foo\", *splat: {:x, :y, :z}, bar: \"baz\", qux: 123)") - end - - context "when empty" do - let(arguments) { Spectator::FormalArguments.none } - - it "returns (no args)" do - is_expected.to eq("(no args)") - end - end - - context "with a splat and no arguments" do - let(arguments) { Spectator::FormalArguments.build(NamedTuple.new, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it "omits the splat name" do - is_expected.to eq("(:x, :y, :z, bar: \"baz\", qux: 123)") - end - end - end - - describe "#==" do - subject { arguments == other } - - context "with Arguments" do - context "with equal arguments" do - let(other) { Spectator::Arguments.new(arguments.positional, arguments.kwargs) } - - it { is_expected.to be_true } - end - - context "with different arguments" do - let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(other) { Spectator::Arguments.new(arguments.positional, {qux: 123, bar: "baz"}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz"}) } - - it { is_expected.to be_false } - end - - context "with an extra kwarg" do - let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz", qux: 123, extra: 0}) } - - it { is_expected.to be_false } - end - end - - context "with FormalArguments" do - context "with equal arguments" do - let(other) { arguments } - - it { is_expected.to be_true } - end - - context "with different arguments" do - let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: 123, bar: "baz"}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz"}) } - - it { is_expected.to be_false } - end - - context "with an extra kwarg" do - let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz", qux: 123, extra: 0}) } - - it { is_expected.to be_false } - end - - context "with different splat arguments" do - let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) } - - it { is_expected.to be_false } - end - - context "with mixed positional tuple types" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, arguments.splat_name, arguments.splat, arguments.kwargs) } - - it { is_expected.to be_true } - end - - context "with mixed positional tuple types (flipped)" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } - end - end - end - - describe "#===" do - subject { pattern === arguments } - - context "with Arguments" do - let(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) } - - context "with equal arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } - end - - context "with matching arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32}) } - - it { is_expected.to be_true } - end - - context "with non-matching arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, {bar: /foo/, qux: "123"}) } - - it { is_expected.to be_false } - end - - context "with different arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {qux: Int32, bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with an additional kwarg" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32, extra: 0}) } - - it { is_expected.to be_false } - end - end - - context "with FormalArguments" do - context "with equal arguments" do - let(pattern) { arguments } - - it { is_expected.to be_true } - end - - context "with matching arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, :splat, {Symbol, Symbol, :z}, {bar: /baz/, qux: Int32}) } - - it { is_expected.to be_true } - end - - context "with non-matching arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, :splat, {String, Int32, :x}, {bar: /foo/, qux: "123"}) } - - it { is_expected.to be_false } - end - - context "with different arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: Int32, bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with an additional kwarg" do - let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/, qux: Int32, extra: 0}) } - - it { is_expected.to be_false } - end - - context "with different splat arguments" do - let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) } - - it { is_expected.to be_false } - end - - context "with matching mixed positional tuple types" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, arguments.splat_name, arguments.splat, arguments.kwargs) } - - it { is_expected.to be_true } - end - - context "with non-matching mixed positional tuple types" do - let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, arguments.splat_name, arguments.splat, arguments.kwargs) } - - it { is_expected.to be_false } - end - end - end -end diff --git a/spec/spectator/mocks/lazy_double_spec.cr b/spec/spectator/mocks/lazy_double_spec.cr deleted file mode 100644 index 8ea5a5d..0000000 --- a/spec/spectator/mocks/lazy_double_spec.cr +++ /dev/null @@ -1,352 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::LazyDouble do - context "plain double" do - subject(dbl) { Spectator::LazyDouble.new("dbl-name", foo: 42, bar: "baz") } - - it "responds to defined messages" do - aggregate_failures do - expect(dbl.foo).to eq(42) - expect(dbl.bar).to eq("baz") - end - end - - it "fails on undefined messages" do - expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) - end - - it "reports the name in errors" do - expect { dbl.baz }.to raise_error(/"dbl-name"/) - end - - it "reports arguments" do - expect { dbl.baz(123, "qux", field: :value) }.to raise_error(Spectator::UnexpectedMessage, /\(123, "qux", field: :value\)/) - end - - context "blocks" do - it "supports blocks" do - aggregate_failures do - expect(dbl.foo { nil }).to eq(42) - expect(dbl.bar { nil }).to eq("baz") - end - end - - it "fails on undefined messages" do - expect do - dbl.baz { nil } - end.to raise_error(Spectator::UnexpectedMessage, /baz/) - end - end - end - - context "without a double name" do - subject(dbl) { Spectator::LazyDouble.new } - - it "reports as anonymous" do - expect { dbl.baz }.to raise_error(/anonymous/i) - end - end - - context "with nillable values" do - subject(dbl) { Spectator::LazyDouble.new(foo: nil.as(String?), bar: nil) } - - it "doesn't raise on nil" do - aggregate_failures do - expect(dbl.foo).to be_nil - expect(dbl.bar).to be_nil - end - end - end - - context "with common object methods" do - let(dup) { double(:dup) } - - subject(dbl) do - Spectator::LazyDouble.new(nil, [ - Spectator::ValueStub.new(:"!=", false), - Spectator::ValueStub.new(:"!~", false), - Spectator::ValueStub.new(:"==", true), - Spectator::ValueStub.new(:"===", true), - Spectator::ValueStub.new(:"=~", nil), - Spectator::ValueStub.new(:dup, dup), - Spectator::ValueStub.new(:hash, 42_u64), - Spectator::ValueStub.new(:"in?", true), - Spectator::ValueStub.new(:inspect, "inspect"), - Spectator::ValueStub.new(:itself, dup), - Spectator::ValueStub.new(:"not_nil!", dup), - Spectator::ValueStub.new(:pretty_inspect, "pretty_inspect"), - Spectator::ValueStub.new(:tap, dup), - Spectator::ValueStub.new(:to_json, "to_json"), - Spectator::ValueStub.new(:to_pretty_json, "to_pretty_json"), - Spectator::ValueStub.new(:to_s, "to_s"), - Spectator::ValueStub.new(:to_yaml, "to_yaml"), - Spectator::ValueStub.new(:try, nil), - Spectator::ValueStub.new(:object_id, 42_u64), - Spectator::ValueStub.new(:"same?", true), - ] of Spectator::Stub) - end - - it "responds with defined messages" do - aggregate_failures do - expect(dbl.!=(42)).to eq(false) - expect(dbl.!~(42)).to eq(false) - expect(dbl.==(42)).to eq(true) - expect(dbl.===(42)).to eq(true) - expect(dbl.=~(42)).to be_nil - expect(dbl.dup).to be(dup) - expect(dbl.hash).to eq(42_u64) - expect(dbl.in?([42])).to eq(true) - expect(dbl.in?(1, 2, 3)).to eq(true) - expect(dbl.inspect).to eq("inspect") - expect(dbl.itself).to be(dup) - expect(dbl.not_nil!).to be(dup) - expect(dbl.pretty_inspect).to eq("pretty_inspect") - expect(dbl.tap { nil }).to be(dup) - expect(dbl.to_json).to eq("to_json") - expect(dbl.to_pretty_json).to eq("to_pretty_json") - expect(dbl.to_s).to eq("to_s") - expect(dbl.to_yaml).to eq("to_yaml") - expect(dbl.try { nil }).to be_nil - expect(dbl.object_id).to eq(42_u64) - expect(dbl.same?(dbl)).to eq(true) - expect(dbl.same?(nil)).to eq(true) - end - end - - it "has a non-union return type" do - expect(dbl.inspect).to compile_as(String) - end - end - - context "without common object methods" do - subject(dbl) { Spectator::LazyDouble.new } - - it "returns the original value" do - io = IO::Memory.new - aggregate_failures do - expect(dbl.!=(42)).to be_true - expect(dbl.!~(42)).to be_true - expect(dbl.==(42)).to be_false - expect(dbl.===(42)).to be_false - expect(dbl.=~(42)).to be_nil - expect(dbl.class).to be_lt(Spectator::LazyDouble) - expect(dbl.in?([42])).to be_false - expect(dbl.in?(1, 2, 3)).to be_false - expect(dbl.itself).to be(dbl) - expect(dbl.not_nil!).to be(dbl) - expect(dbl.tap { nil }).to be(dbl) - expect(dbl.to_s(io)).to be_nil - expect(dbl.try { nil }).to be_nil - expect(dbl.same?(dbl)).to be_true - expect(dbl.same?(nil)).to be_false - end - end - end - - context "with arguments constraints" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - - context "without common object methods" do - let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) } - subject(dbl) { Spectator::LazyDouble.new(nil, [stub], foo: "fallback") } - - it "returns the response when constraint satisfied" do - expect(dbl.foo("foobar")).to eq("bar") - end - - it "returns the fallback value when constraint unsatisfied" do - expect { dbl.foo("baz") }.to eq("fallback") - end - - it "returns the fallback value when argument count doesn't match" do - expect { dbl.foo }.to eq("fallback") - end - end - - context "with common object methods" do - let(stub) { Spectator::ValueStub.new(:"same?", true, arguments).as(Spectator::Stub) } - subject(dbl) { Spectator::LazyDouble.new(nil, [stub]) } - - it "returns the response when constraint satisfied" do - expect(dbl.same?("foobar")).to eq(true) - end - - it "raises an error when constraint unsatisfied" do - expect { dbl.same?("baz") }.to raise_error(Spectator::UnexpectedMessage) - end - - it "raises an error when argument count doesn't match" do - expect { dbl.same? }.to raise_error(Spectator::UnexpectedMessage) - end - - context "with a fallback defined" do - subject(dbl) { Spectator::LazyDouble.new(nil, [stub], "same?": true) } - - it "returns the fallback when constraint unsatisfied" do - expect(dbl.same?("baz")).to be_true - end - end - end - end - - describe "#_spectator_define_stub" do - subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") } - let(stub3) { Spectator::ValueStub.new(:foo, 3) } - let(stub5) { Spectator::ValueStub.new(:foo, 5) } - let(stub7) { Spectator::ValueStub.new(:foo, 7, Spectator::Arguments.capture(:lucky)) } - - it "overrides an existing method" do - expect { dbl._spectator_define_stub(stub3) }.to change { dbl.foo }.from(42).to(3) - end - - it "replaces an existing stub" do - dbl._spectator_define_stub(stub3) - expect { dbl._spectator_define_stub(stub5) }.to change { dbl.foo }.from(3).to(5) - end - - it "doesn't affect other methods" do - expect { dbl._spectator_define_stub(stub5) }.to_not change { dbl.bar } - end - - it "picks the correct stub based on arguments" do - dbl._spectator_define_stub(stub5) - dbl._spectator_define_stub(stub7) - aggregate_failures do - expect(dbl.foo).to eq(5) - expect(dbl.foo(:lucky)).to eq(7) - end - end - - it "only uses a stub if an argument constraint is met" do - dbl._spectator_define_stub(stub7) - aggregate_failures do - expect(dbl.foo).to eq(42) - expect(dbl.foo(:lucky)).to eq(7) - end - end - - it "ignores the block argument if not in the constraint" do - dbl._spectator_define_stub(stub5) - dbl._spectator_define_stub(stub7) - aggregate_failures do - expect(dbl.foo { nil }).to eq(5) - expect(dbl.foo(:lucky) { nil }).to eq(7) - end - end - - context "with previously undefined methods" do - it "raises an error" do - stub = Spectator::ValueStub.new(:baz, :xyz) - expect { dbl._spectator_define_stub(stub) }.to raise_error(/stub/) - end - end - end - - describe "#_spectator_clear_stubs" do - subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") } - let(stub) { Spectator::ValueStub.new(:foo, 5) } - - before { dbl._spectator_define_stub(stub) } - - it "removes previously defined stubs" do - expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42) - end - end - - describe "#_spectator_calls" do - subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") } - let(stub) { Spectator::ValueStub.new(:foo, 5) } - - before { dbl._spectator_define_stub(stub) } - - # Retrieves symbolic names of methods called on a double. - def called_method_names(dbl) - dbl._spectator_calls.map(&.method) - end - - it "stores calls to stubbed methods" do - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[]).to(%i[foo]) - end - - it "stores multiple calls to the same stub" do - dbl.foo - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[foo]).to(%i[foo foo]) - end - - it "stores calls to non-stubbed methods" do - expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) - expect(called_method_names(dbl)).to contain(:baz) - end - - it "stores arguments for a call" do - dbl.foo(42) - args = Spectator::Arguments.capture(42) - call = dbl._spectator_calls.first - expect(call.arguments).to eq(args) - end - end - - describe "#to_s" do - subject(string) { dbl.to_s } - - context "with a name" do - let(dbl) { Spectator::LazyDouble.new("dbl-name") } - - it "indicates it's a double" do - expect(string).to contain("LazyDouble") - end - - it "contains the double name" do - expect(string).to contain("dbl-name") - end - end - - context "without a name" do - let(dbl) { Spectator::LazyDouble.new } - - it "contains the double type" do - expect(string).to contain("LazyDouble") - end - - it "contains \"Anonymous\"" do - expect(string).to contain("Anonymous") - end - end - end - - describe "#inspect" do - subject(string) { dbl.inspect } - - context "with a name" do - let(dbl) { Spectator::LazyDouble.new("dbl-name") } - - it "contains the double type" do - expect(string).to contain("LazyDouble") - end - - it "contains the double name" do - expect(string).to contain("dbl-name") - end - - it "contains the object ID" do - expect(string).to contain(dbl.object_id.to_s(16)) - end - end - - context "without a name" do - let(dbl) { Spectator::LazyDouble.new } - - it "contains the double type" do - expect(string).to contain("LazyDouble") - end - - it "contains \"Anonymous\"" do - expect(string).to contain("Anonymous") - end - - it "contains the object ID" do - expect(string).to contain(dbl.object_id.to_s(16)) - end - end - end -end diff --git a/spec/spectator/mocks/method_call_spec.cr b/spec/spectator/mocks/method_call_spec.cr deleted file mode 100644 index 52a9e4f..0000000 --- a/spec/spectator/mocks/method_call_spec.cr +++ /dev/null @@ -1,26 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::MethodCall do - let(arguments) { Spectator::Arguments.capture(42, "foobar", foo: :bar) } - subject(call) { Spectator::MethodCall.new(:foo, arguments) } - - it "stores the method name" do - expect(&.method).to eq(:foo) - end - - it "stores arguments" do - expect(&.arguments).to eq(arguments) - end - - describe ".capture" do - subject(call) { Spectator::MethodCall.capture(:foo, 42, "foobar", foo: :bar) } - - it "stores the method name" do - expect(&.method).to eq(:foo) - end - - it "stores arguments" do - expect(&.arguments).to eq(arguments) - end - end -end diff --git a/spec/spectator/mocks/mock_spec.cr b/spec/spectator/mocks/mock_spec.cr deleted file mode 100644 index 3ddd0fe..0000000 --- a/spec/spectator/mocks/mock_spec.cr +++ /dev/null @@ -1,1062 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::Mock do - let(stub1) { Spectator::ValueStub.new(:method1, 777) } - let(stub2) { Spectator::ValueStub.new(:method2, :override) } - let(stub3) { Spectator::ValueStub.new(:method3, "stubbed") } - - # Retrieves symbolic names of methods called on a mock. - def called_method_names(mock) - mock._spectator_calls.map(&.method) - end - - describe "#define_subtype" do - context "with a concrete class" do - class Thing - getter _spectator_invocations = [] of Symbol - - def method1 - @_spectator_invocations << :method1 - 42 - end - - def method2 - @_spectator_invocations << :method2 - :original - end - - def method3 - @_spectator_invocations << :method3 - "original" - end - - def method4 : Thing - self - end - - def method5 : OtherThing - OtherThing.new - end - end - - class OtherThing; end - - Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method1: 123) do - stub def method2 - :stubbed - end - end - - let(mock) { MockThing.new } - - it "defines a subclass of the mocked type" do - expect(mock).to be_a(Thing) - end - - it "overrides responses from methods with keyword arguments" do - expect(mock.method1).to eq(123) - end - - it "overrides responses from methods defined in the block" do - expect(mock.method2).to eq(:stubbed) - end - - it "allows methods to be stubbed" do - aggregate_failures do - expect { mock._spectator_define_stub(stub1) }.to change { mock.method1 }.to(777) - expect { mock._spectator_define_stub(stub2) }.to change { mock.method2 }.to(:override) - expect { mock._spectator_define_stub(stub3) }.to change { mock.method3 }.from("original").to("stubbed") - end - end - - it "can clear stubs" do - mock._spectator_define_stub(stub1) - mock._spectator_define_stub(stub2) - mock._spectator_define_stub(stub3) - - mock._spectator_clear_stubs - aggregate_failures do - expect(mock.method1).to eq(123) - expect(mock.method2).to eq(:stubbed) - expect(mock.method3).to eq("original") - end - end - - it "sets the mock name" do - args = Spectator::Arguments.capture("foo") - stub = Spectator::ValueStub.new(:method3, 0, args) - mock._spectator_define_stub(stub) - expect { mock.method3 }.to raise_error(Spectator::UnexpectedMessage, /mock_name/), "Raised error doesn't contain the mocked name." - end - - it "records invoked stubs" do - expect { mock.method2 }.to change { called_method_names(mock) }.from(%i[]).to(%i[method2]) - expect { mock.method1 }.to change { called_method_names(mock) }.from(%i[method2]).to(%i[method2 method1]) - expect { mock.method3 }.to change { called_method_names(mock) }.from(%i[method2 method1]).to(%i[method2 method1 method3]) - end - - it "records multiple invocations of the same stub" do - mock.method2 - expect { mock.method2 }.to change { called_method_names(mock) }.from(%i[method2]).to(%i[method2 method2]) - end - - def restricted(thing : Thing) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(mock)).to eq(123) - end - - it "does not call the original method when stubbed" do - mock.method1 - mock.method2 - mock.method3 - expect(mock._spectator_invocations).to contain_exactly(:method3) - end - - it "can reference its own type" do - new_mock = MockThing.new - stub = Spectator::ValueStub.new(:method4, new_mock) - mock._spectator_define_stub(stub) - expect(mock.method4).to be(new_mock) - end - - it "can reference other types in the original namespace" do - other = OtherThing.new - stub = Spectator::ValueStub.new(:method5, other) - mock._spectator_define_stub(stub) - expect(mock.method5).to be(other) - end - end - - context "with an abstract class" do - abstract class Thing - getter _spectator_invocations = [] of Symbol - - abstract def method1 - - abstract def method2 : Symbol - - def method3 - @_spectator_invocations << :method3 - "original" - end - - abstract def method4 - - abstract def method4 : Thing - - abstract def method5 : OtherThing - end - - class OtherThing; end - - Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method2: :stubbed) do - stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent. - 123 - end - end - - let(mock) { MockThing.new } - - it "defines a subclass of the mocked type" do - expect(mock).to be_a(Thing) - end - - it "overrides responses from methods defined in the block" do - expect(mock.method1).to eq(123) - end - - it "overrides responses from methods with keyword arguments" do - expect(mock.method2).to eq(:stubbed) - end - - it "allows methods to be stubbed" do - aggregate_failures do - expect { mock._spectator_define_stub(stub1) }.to change { mock.method1 }.to(777) - expect { mock._spectator_define_stub(stub2) }.to change { mock.method2 }.to(:override) - expect { mock._spectator_define_stub(stub3) }.to change { mock.method3 }.from("original").to("stubbed") - end - end - - it "can clear stubs" do - mock._spectator_define_stub(stub1) - mock._spectator_define_stub(stub2) - mock._spectator_define_stub(stub3) - - mock._spectator_clear_stubs - aggregate_failures do - expect(mock.method1).to eq(123) - expect(mock.method2).to eq(:stubbed) - expect(mock.method3).to eq("original") - end - end - - it "raises when calling an abstract method that isn't stubbed" do - expect { mock.method4 }.to raise_error(Spectator::UnexpectedMessage, /method4/) - end - - it "sets the mock name" do - args = Spectator::Arguments.capture("foo") - stub = Spectator::ValueStub.new(:method3, 0, args) - mock._spectator_define_stub(stub) - expect { mock.method3 }.to raise_error(Spectator::UnexpectedMessage, /mock_name/), "Raised error doesn't contain the mocked name." - end - - it "records invoked stubs" do - expect { mock.method2 }.to change { called_method_names(mock) }.from(%i[]).to(%i[method2]) - expect { mock.method1 }.to change { called_method_names(mock) }.from(%i[method2]).to(%i[method2 method1]) - expect { mock.method3 }.to change { called_method_names(mock) }.from(%i[method2 method1]).to(%i[method2 method1 method3]) - end - - it "records multiple invocations of the same stub" do - mock.method2 - expect { mock.method2 }.to change { called_method_names(mock) }.from(%i[method2]).to(%i[method2 method2]) - end - - def restricted(thing : Thing) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(mock)).to eq(123) - end - - it "does not call the original method when stubbed" do - mock.method1 - mock.method2 - mock.method3 - expect(mock._spectator_invocations).to contain_exactly(:method3) - end - - it "can reference its own type" do - new_mock = MockThing.new - stub = Spectator::ValueStub.new(:method4, new_mock) - mock._spectator_define_stub(stub) - expect(mock.method4).to be(new_mock) - end - - it "can reference other types in the original namespace" do - other = OtherThing.new - stub = Spectator::ValueStub.new(:method5, other) - mock._spectator_define_stub(stub) - expect(mock.method5).to be(other) - end - end - - context "with an abstract struct" do - abstract struct Thing - getter _spectator_invocations = [] of Symbol - - abstract def method1 - - abstract def method2 : Symbol - - def method3 - @_spectator_invocations << :method3 - "original" - end - - abstract def method4 - - abstract def method4 : Thing - - abstract def method5 : OtherThing - end - - class OtherThing; end - - Spectator::Mock.define_subtype(:struct, Thing, MockThing, :mock_name, method2: :stubbed) do - stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent. - 123 - end - end - - let(mock) { MockThing.new } - - it "defines a sub-type of the mocked type" do - expect(mock).to be_a(Thing) - end - - it "overrides responses from methods defined in the block" do - expect(mock.method1).to eq(123) - end - - it "overrides responses from methods with keyword arguments" do - expect(mock.method2).to eq(:stubbed) - end - - it "allows methods to be stubbed" do - mock = self.mock # FIXME: Workaround for passing by value messing with stubs. - aggregate_failures do - expect { mock._spectator_define_stub(stub1) }.to change { mock.method1 }.to(777) - expect { mock._spectator_define_stub(stub2) }.to change { mock.method2 }.to(:override) - expect { mock._spectator_define_stub(stub3) }.to change { mock.method3 }.from("original").to("stubbed") - end - end - - it "can clear stubs" do - mock._spectator_define_stub(stub1) - mock._spectator_define_stub(stub2) - mock._spectator_define_stub(stub3) - - mock._spectator_clear_stubs - aggregate_failures do - expect(mock.method1).to eq(123) - expect(mock.method2).to eq(:stubbed) - expect(mock.method3).to eq("original") - end - end - - it "raises when calling an abstract method that isn't stubbed" do - expect { mock.method4 }.to raise_error(Spectator::UnexpectedMessage, /method4/) - end - - it "sets the mock name" do - mock = self.mock # FIXME: Workaround for passing by value messing with stubs. - args = Spectator::Arguments.capture("foo") - stub = Spectator::ValueStub.new(:method3, 0, args) - mock._spectator_define_stub(stub) - expect { mock.method3 }.to raise_error(Spectator::UnexpectedMessage, /mock_name/), "Raised error doesn't contain the mocked name." - end - - def restricted(thing : Thing) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(mock)).to eq(123) - end - - it "does not call the original method when stubbed" do - mock = self.mock # FIXME: Workaround for passing by value messing with stubs. - mock.method1 - mock.method2 - mock.method3 - expect(mock._spectator_invocations).to contain_exactly(:method3) - end - - it "can reference its own type" do - mock = self.mock # FIXME: Workaround for passing by value messing with stubs. - new_mock = MockThing.new - stub = Spectator::ValueStub.new(:method4, new_mock) - mock._spectator_define_stub(stub) - expect(mock.method4).to be_a(Thing) - end - - it "can reference other types in the original namespace" do - mock = self.mock # FIXME: Workaround for passing by value messing with stubs. - other = OtherThing.new - stub = Spectator::ValueStub.new(:method5, other) - mock._spectator_define_stub(stub) - expect(mock.method5).to be(other) - end - end - - context "class method stubs" do - class Thing - def self.foo - :original - end - - def self.bar(arg) - arg - end - - def self.baz(arg, &) - yield - end - - def self.thing : Thing - new - end - - def self.other : OtherThing - OtherThing.new - end - end - - class OtherThing; end - - Spectator::Mock.define_subtype(:class, Thing, MockThing) do - stub def self.foo - :stub - end - end - - let(mock) { MockThing } - let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } - - after { mock._spectator_clear_stubs } - - it "overrides an existing method" do - expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override) - end - - it "doesn't affect other methods" do - expect { mock._spectator_define_stub(foo_stub) }.to_not change { mock.bar(42) } - end - - it "replaces an existing stub" do - mock._spectator_define_stub(foo_stub) - stub = Spectator::ValueStub.new(:foo, :replacement) - expect { mock._spectator_define_stub(stub) }.to change { mock.foo }.to(:replacement) - end - - it "picks the correct stub based on arguments" do - stub1 = Spectator::ValueStub.new(:bar, :fallback) - stub2 = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) - mock._spectator_define_stub(stub1) - mock._spectator_define_stub(stub2) - aggregate_failures do - expect(mock.bar(:wrong)).to eq(:fallback) - expect(mock.bar(:match)).to eq(:override) - end - end - - it "only uses a stub if an argument constraint is met" do - stub = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) - mock._spectator_define_stub(stub) - aggregate_failures do - expect(mock.bar(:original)).to eq(:original) - expect(mock.bar(:match)).to eq(:override) - end - end - - it "ignores the block argument if not in the constraint" do - stub1 = Spectator::ValueStub.new(:baz, 1) - stub2 = Spectator::ValueStub.new(:baz, 2, Spectator::Arguments.capture(3)) - mock._spectator_define_stub(stub1) - mock._spectator_define_stub(stub2) - aggregate_failures do - expect(mock.baz(5) { 42 }).to eq(1) - expect(mock.baz(3) { 42 }).to eq(2) - end - end - - def restricted(thing : Thing.class) - thing.foo - end - - it "can be used in type restricted methods" do - expect(restricted(mock)).to eq(:stub) - end - - it "can reference its own type" do - new_mock = MockThing.new - stub = Spectator::ValueStub.new(:thing, new_mock) - mock._spectator_define_stub(stub) - expect(mock.thing).to be(new_mock) - end - - it "can reference other types in the original namespace" do - other = OtherThing.new - stub = Spectator::ValueStub.new(:other, other) - mock._spectator_define_stub(stub) - expect(mock.other).to be(other) - end - - describe "._spectator_clear_stubs" do - before { mock._spectator_define_stub(foo_stub) } - - it "removes previously defined stubs" do - expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub) - end - end - - describe "._spectator_calls" do - before { mock._spectator_clear_calls } - - # Retrieves symbolic names of methods called on a mock. - def called_method_names(mock) - mock._spectator_calls.map(&.method) - end - - it "stores calls to stubbed methods" do - expect { mock.foo }.to change { called_method_names(mock) }.from(%i[]).to(%i[foo]) - end - - it "stores multiple calls to the same stub" do - mock.foo - expect { mock.foo }.to change { called_method_names(mock) }.from(%i[foo]).to(%i[foo foo]) - end - - it "stores arguments for a call" do - mock.bar(42) - args = Spectator::Arguments.capture(42) - call = mock._spectator_calls.first - expect(call.arguments).to eq(args) - end - end - end - - context "with a module" do - module Thing - # `extend self` cannot be used. - # The Crystal compiler doesn't report the methods as class methods when doing so. - - def self.original_method - :original - end - - def self.default_method - :original - end - - def self.stubbed_method(_value = 42) - :original - end - end - - Spectator::Mock.define_subtype(:module, Thing, MockThing) do - stub def self.stubbed_method(_value = 42) - :stubbed - end - end - - let(mock) { MockThing } - - after { mock._spectator_clear_stubs } - - it "overrides an existing method" do - stub = Spectator::ValueStub.new(:original_method, :override) - expect { mock._spectator_define_stub(stub) }.to change { mock.original_method }.from(:original).to(:override) - end - - it "doesn't affect other methods" do - stub = Spectator::ValueStub.new(:stubbed_method, :override) - expect { mock._spectator_define_stub(stub) }.to_not change { mock.original_method } - end - - it "replaces an existing default stub" do - stub = Spectator::ValueStub.new(:default_method, :override) - expect { mock._spectator_define_stub(stub) }.to change { mock.default_method }.to(:override) - end - - it "replaces an existing stubbed method" do - stub = Spectator::ValueStub.new(:stubbed_method, :override) - expect { mock._spectator_define_stub(stub) }.to change { mock.stubbed_method }.to(:override) - end - - def restricted(thing : Thing.class) - thing.stubbed_method - end - - it "can be used in type restricted methods" do - expect(restricted(mock)).to eq(:stubbed) - end - - describe "._spectator_clear_stubs" do - before do - stub = Spectator::ValueStub.new(:original_method, :override) - mock._spectator_define_stub(stub) - end - - it "removes previously defined stubs" do - expect { mock._spectator_clear_stubs }.to change { mock.original_method }.from(:override).to(:original) - end - end - - describe "._spectator_calls" do - before { mock._spectator_clear_calls } - - # Retrieves symbolic names of methods called on a mock. - def called_method_names(mock) - mock._spectator_calls.map(&.method) - end - - it "stores calls to original methods" do - expect { mock.original_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[original_method]) - end - - it "stores calls to default methods" do - expect { mock.default_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[default_method]) - end - - it "stores calls to stubbed methods" do - expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[stubbed_method]) - end - - it "stores multiple calls to the same stub" do - mock.stubbed_method - expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[stubbed_method]).to(%i[stubbed_method stubbed_method]) - end - - it "stores arguments for a call" do - mock.stubbed_method(5) - args = Spectator::Arguments.capture(5) - call = mock._spectator_calls.first - expect(call.arguments).to eq(args) - end - end - end - - context "with a mocked module included in a class" do - module Thing - def original_method - :original - end - - def default_method - :original - end - - def stubbed_method(_value = 42) - :original - end - end - - Spectator::Mock.define_subtype(:module, Thing, MockThing, default_method: :default) do - stub def stubbed_method(_value = 42) - :stubbed - end - end - - class IncludedMock - include MockThing - end - - let(mock) { IncludedMock.new } - - it "overrides an existing method" do - stub = Spectator::ValueStub.new(:original_method, :override) - expect { mock._spectator_define_stub(stub) }.to change { mock.original_method }.from(:original).to(:override) - end - - it "doesn't affect other methods" do - stub = Spectator::ValueStub.new(:stubbed_method, :override) - expect { mock._spectator_define_stub(stub) }.to_not change { mock.original_method } - end - - it "replaces an existing default stub" do - stub = Spectator::ValueStub.new(:default_method, :override) - expect { mock._spectator_define_stub(stub) }.to change { mock.default_method }.to(:override) - end - - it "replaces an existing stubbed method" do - stub = Spectator::ValueStub.new(:stubbed_method, :override) - expect { mock._spectator_define_stub(stub) }.to change { mock.stubbed_method }.to(:override) - end - - def restricted(thing : Thing.class) - thing.default_method - end - - describe "#_spectator_clear_stubs" do - before do - stub = Spectator::ValueStub.new(:original_method, :override) - mock._spectator_define_stub(stub) - end - - it "removes previously defined stubs" do - expect { mock._spectator_clear_stubs }.to change { mock.original_method }.from(:override).to(:original) - end - end - - describe "#_spectator_calls" do - before { mock._spectator_clear_calls } - - # Retrieves symbolic names of methods called on a mock. - def called_method_names(mock) - mock._spectator_calls.map(&.method) - end - - it "stores calls to original methods" do - expect { mock.original_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[original_method]) - end - - it "stores calls to default methods" do - expect { mock.default_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[default_method]) - end - - it "stores calls to stubbed methods" do - expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[stubbed_method]) - end - - it "stores multiple calls to the same stub" do - mock.stubbed_method - expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[stubbed_method]).to(%i[stubbed_method stubbed_method]) - end - - it "stores arguments for a call" do - mock.stubbed_method(5) - args = Spectator::Arguments.capture(5) - call = mock._spectator_calls.first - expect(call.arguments).to eq(args) - end - end - end - - context "with a method that uses NoReturn" do - abstract class Thing - abstract def oops : NoReturn - end - - Spectator::Mock.define_subtype(:class, Thing, MockThing) - - let(mock) { MockThing.new } - - after { mock._spectator_clear_stubs } - - it "raises a TypeCastError when using a value-based stub" do - stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub) - mock._spectator_define_stub(stub) - expect { mock.oops }.to raise_error(TypeCastError, /NoReturn/) - end - - it "raises when using an exception stub" do - exception = ArgumentError.new("bogus") - stub = Spectator::ExceptionStub.new(:oops, exception).as(Spectator::Stub) - mock._spectator_define_stub(stub) - expect { mock.oops }.to raise_error(ArgumentError, "bogus") - end - end - end - - describe "#inject" do - context "with a class" do - class ::MockedClass - getter _spectator_invocations = [] of Symbol - - getter method1 do - @_spectator_invocations << :method1 - 42 - end - - def method2 - @_spectator_invocations << :method2 - :original - end - - def method3 - @_spectator_invocations << :method3 - "original" - end - - def instance_variables - {% begin %}[{{@type.instance_vars.map(&.name.symbolize).splat}}]{% end %} - end - end - - Spectator::Mock.inject(:class, ::MockedClass, :mock_name, method1: 123) do - stub def method2 - :stubbed - end - end - - let(mock) { MockedClass.new } - - # Necessary to clear stubs to prevent leakages between tests. - after { mock._spectator_clear_stubs } - - it "overrides responses from methods with keyword arguments" do - expect(mock.method1).to eq(123) - end - - it "overrides responses from methods defined in the block" do - expect(mock.method2).to eq(:stubbed) - end - - it "allows methods to be stubbed" do - aggregate_failures do - expect { mock._spectator_define_stub(stub1) }.to change { mock.method1 }.to(777) - expect { mock._spectator_define_stub(stub2) }.to change { mock.method2 }.to(:override) - expect { mock._spectator_define_stub(stub3) }.to change { mock.method3 }.from("original").to("stubbed") - end - end - - it "can clear stubs" do - mock._spectator_define_stub(stub1) - mock._spectator_define_stub(stub2) - mock._spectator_define_stub(stub3) - - mock._spectator_clear_stubs - aggregate_failures do - expect(mock.method1).to eq(123) - expect(mock.method2).to eq(:stubbed) - expect(mock.method3).to eq("original") - end - end - - it "doesn't change the size of an instance" do - size = sizeof(Int64) + sizeof(Int32?) + sizeof(Array(Symbol)) # TypeID + Int32? + _spectator_invocations - expect(instance_sizeof(MockedClass)).to eq(size) - end - - it "doesn't affect instance variables" do - expect(mock.instance_variables).to contain_exactly(:method1, :_spectator_invocations) - end - - it "sets the mock name" do - args = Spectator::Arguments.capture("foo") - stub = Spectator::ValueStub.new(:method3, 0, args) - mock._spectator_define_stub(stub) - expect { mock.method3 }.to raise_error(Spectator::UnexpectedMessage, /mock_name/), "Raised error doesn't contain the mocked name." - end - - it "records invoked stubs" do - expect { mock.method2 }.to change { called_method_names(mock) }.from(%i[]).to(%i[method2]) - expect { mock.method1 }.to change { called_method_names(mock) }.from(%i[method2]).to(%i[method2 method1]) - expect { mock.method3 }.to change { called_method_names(mock) }.from(%i[method2 method1]).to(%i[method2 method1 method3]) - end - - it "records multiple invocations of the same stub" do - mock.method2 - expect { mock.method2 }.to change { called_method_names(mock) }.from(%i[method2]).to(%i[method2 method2]) - end - - def restricted(thing : MockedClass) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(mock)).to eq(123) - end - - it "does not call the original method when stubbed" do - mock.method1 - mock.method2 - mock.method3 - expect(mock._spectator_invocations).to contain_exactly(:method3) - end - end - - context "with a struct" do - struct ::MockedStruct - # Using a class variable instead of an instance variable to prevent mutability problems with stub lookup. - class_getter _spectator_invocations = [] of Symbol - - @method1 = 42 - - def method1 - @@_spectator_invocations << :method1 - @method1 - end - - def method2 - @@_spectator_invocations << :method2 - :original - end - - def method3 - @@_spectator_invocations << :method3 - "original" - end - - def instance_variables - {% begin %}[{{@type.instance_vars.map(&.name.symbolize).splat}}]{% end %} - end - end - - Spectator::Mock.inject(:struct, ::MockedStruct, :mock_name, method1: 123) do - stub def method2 - :stubbed - end - end - - let(mock) { MockedStruct.new } - - # Necessary to clear stubs to prevent leakages between tests. - after { mock._spectator_clear_stubs } - after { MockedStruct._spectator_invocations.clear } - - it "overrides responses from methods with keyword arguments" do - expect(mock.method1).to eq(123) - end - - it "overrides responses from methods defined in the block" do - expect(mock.method2).to eq(:stubbed) - end - - it "allows methods to be stubbed" do - aggregate_failures do - expect { mock._spectator_define_stub(stub1) }.to change { mock.method1 }.to(777) - expect { mock._spectator_define_stub(stub2) }.to change { mock.method2 }.to(:override) - expect { mock._spectator_define_stub(stub3) }.to change { mock.method3 }.from("original").to("stubbed") - end - end - - it "doesn't change the size of an instance" do - expect(sizeof(MockedStruct)).to eq(sizeof(Int32)) - end - - it "doesn't affect instance variables" do - expect(mock.instance_variables).to contain_exactly(:method1) - end - - it "sets the mock name" do - args = Spectator::Arguments.capture("foo") - stub = Spectator::ValueStub.new(:method3, 0, args) - mock._spectator_define_stub(stub) - expect { mock.method3 }.to raise_error(Spectator::UnexpectedMessage, /mock_name/), "Raised error doesn't contain the mocked name." - end - - it "records invoked stubs" do - expect { mock.method2 }.to change { called_method_names(mock) }.from(%i[]).to(%i[method2]) - expect { mock.method1 }.to change { called_method_names(mock) }.from(%i[method2]).to(%i[method2 method1]) - expect { mock.method3 }.to change { called_method_names(mock) }.from(%i[method2 method1]).to(%i[method2 method1 method3]) - end - - it "records multiple invocations of the same stub" do - mock.method2 - expect { mock.method2 }.to change { called_method_names(mock) }.from(%i[method2]).to(%i[method2 method2]) - end - - def restricted(thing : MockedStruct) - thing.method1 - end - - it "can be used in type restricted methods" do - expect(restricted(mock)).to eq(123) - end - - it "does not call the original method when stubbed" do - mock.method1 - mock.method2 - mock.method3 - expect(MockedStruct._spectator_invocations).to contain_exactly(:method3) - end - end - - context "class method stubs" do - class ::Thing - def self.foo - :original - end - - def self.bar(arg) - arg - end - - def self.baz(arg, &) - yield - end - end - - Spectator::Mock.inject(:class, ::Thing) do - stub def self.foo - :stub - end - end - - let(mock) { Thing } - let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } - - after { mock._spectator_clear_stubs } - - it "overrides an existing method" do - expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override) - end - - it "doesn't affect other methods" do - expect { mock._spectator_define_stub(foo_stub) }.to_not change { mock.bar(42) } - end - - it "replaces an existing stub" do - mock._spectator_define_stub(foo_stub) - stub = Spectator::ValueStub.new(:foo, :replacement) - expect { mock._spectator_define_stub(stub) }.to change { mock.foo }.to(:replacement) - end - - it "picks the correct stub based on arguments" do - stub1 = Spectator::ValueStub.new(:bar, :fallback) - stub2 = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) - mock._spectator_define_stub(stub1) - mock._spectator_define_stub(stub2) - aggregate_failures do - expect(mock.bar(:wrong)).to eq(:fallback) - expect(mock.bar(:match)).to eq(:override) - end - end - - it "only uses a stub if an argument constraint is met" do - stub = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) - mock._spectator_define_stub(stub) - aggregate_failures do - expect(mock.bar(:original)).to eq(:original) - expect(mock.bar(:match)).to eq(:override) - end - end - - it "ignores the block argument if not in the constraint" do - stub1 = Spectator::ValueStub.new(:baz, 1) - stub2 = Spectator::ValueStub.new(:baz, 2, Spectator::Arguments.capture(3)) - mock._spectator_define_stub(stub1) - mock._spectator_define_stub(stub2) - aggregate_failures do - expect(mock.baz(5) { 42 }).to eq(1) - expect(mock.baz(3) { 42 }).to eq(2) - end - end - - def restricted(thing : Thing.class) - thing.foo - end - - it "can be used in type restricted methods" do - expect(restricted(mock)).to eq(:stub) - end - - describe "._spectator_clear_stubs" do - before { mock._spectator_define_stub(foo_stub) } - - it "removes previously defined stubs" do - expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub) - end - end - - describe "._spectator_calls" do - before { mock._spectator_clear_calls } - - # Retrieves symbolic names of methods called on a mock. - def called_method_names(mock) - mock._spectator_calls.map(&.method) - end - - it "stores calls to stubbed methods" do - expect { mock.foo }.to change { called_method_names(mock) }.from(%i[]).to(%i[foo]) - end - - it "stores multiple calls to the same stub" do - mock.foo - expect { mock.foo }.to change { called_method_names(mock) }.from(%i[foo]).to(%i[foo foo]) - end - - it "stores arguments for a call" do - mock.bar(42) - args = Spectator::Arguments.capture(42) - call = mock._spectator_calls.first - expect(call.arguments).to eq(args) - end - end - end - - context "with a method that uses NoReturn" do - struct ::NoReturnThing - def oops : NoReturn - raise "oops" - end - end - - Spectator::Mock.inject(:struct, ::NoReturnThing) - - let(mock) { NoReturnThing.new } - - after { mock._spectator_clear_stubs } - - it "raises a TypeCastError when using a value-based stub" do - stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub) - mock._spectator_define_stub(stub) - expect { mock.oops }.to raise_error(TypeCastError, /NoReturn/) - end - - it "raises when using an exception stub" do - exception = ArgumentError.new("bogus") - stub = Spectator::ExceptionStub.new(:oops, exception).as(Spectator::Stub) - mock._spectator_define_stub(stub) - expect { mock.oops }.to raise_error(ArgumentError, "bogus") - end - end - end -end diff --git a/spec/spectator/mocks/multi_value_stub_spec.cr b/spec/spectator/mocks/multi_value_stub_spec.cr deleted file mode 100644 index a68a273..0000000 --- a/spec/spectator/mocks/multi_value_stub_spec.cr +++ /dev/null @@ -1,173 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::MultiValueStub do - let(method_call) { Spectator::MethodCall.capture(:foo) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - subject(stub) { described_class.new(:foo, [3, 5, 7], location: location) } - - it "stores the method name" do - expect(stub.method).to eq(:foo) - end - - it "stores the location" do - expect(stub.location).to eq(location) - end - - describe "#call" do - it "returns the values in order" do - values = Array.new(3) { stub.call(method_call) } - expect(values).to eq([3, 5, 7]) - end - - it "returns the final value after exhausting other values" do - values = Array.new(5) { stub.call(method_call) } - expect(values).to eq([3, 5, 7, 7, 7]) - end - end - - context Spectator::StubModifiers do - describe "#and_return(value)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::MultiValueStub.new(:foo, [3, 5, 7], arguments, location) } - subject(stub) { original.and_return(123) } - - it "produces a stub that returns a value" do - expect(stub.call(method_call)).to eq(123) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_return(*values)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::MultiValueStub.new(:foo, [3, 5, 7], arguments, location) } - subject(stub) { original.and_return(3, 2, 1, 0) } - - it "produces a stub that returns values" do - values = Array.new(5) { stub.call(method_call) } - expect(values).to eq([3, 2, 1, 0, 0]) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_raise" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::MultiValueStub.new(:foo, [3, 5, 7], arguments, location) } - let(new_exception) { ArgumentError.new("Test argument error") } - subject(stub) { original.and_raise(new_exception) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - - context "with a class and message" do - subject(stub) { original.and_raise(ArgumentError, "Test argument error") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - end - - context "with a message" do - subject(stub) { original.and_raise("Test exception") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(Exception, "Test exception") - end - end - - context "with a class" do - subject(stub) { original.and_raise(ArgumentError) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError) - end - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - end - - describe "#===" do - subject { stub === call } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - - context "with a constraint" do - let(constraint) { Spectator::Arguments.capture(/foo/) } - let(stub) { Spectator::ValueStub.new(:foo, 42, constraint) } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - - context "with a non-matching arguments" do - let(call) { Spectator::MethodCall.capture(:foo, "baz") } - - it "returns false" do - is_expected.to be_false - end - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - end - end -end diff --git a/spec/spectator/mocks/null_double_spec.cr b/spec/spectator/mocks/null_double_spec.cr deleted file mode 100644 index a6fc7d2..0000000 --- a/spec/spectator/mocks/null_double_spec.cr +++ /dev/null @@ -1,503 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::NullDouble do - Spectator::NullDouble.define(EmptyDouble) - Spectator::NullDouble.define(FooBarDouble, "dbl-name", foo: 42, bar: "baz") - - # The subject `dbl` must be carefully used in sub-contexts, otherwise it pollutes parent scopes. - # This changes the type of `dbl` to `Double`, which produces a union of methods and their return types. - context "plain double" do - subject(dbl) { FooBarDouble.new } - - it "responds to defined messages" do - aggregate_failures do - expect(dbl.foo).to eq(42) - expect(dbl.bar).to eq("baz") - end - end - - it "returns self on undefined messages" do - expect(dbl.baz).to be(dbl) - end - - it "has a non-union return type" do - aggregate_failures do - expect(dbl.foo).to compile_as(Int32) - expect(dbl.bar).to compile_as(String) - end - end - - context "blocks" do - it "supports blocks" do - aggregate_failures do - expect(dbl.foo { nil }).to eq(42) - expect(dbl.bar { nil }).to eq("baz") - end - end - - it "supports blocks and has non-union return types" do - aggregate_failures do - expect(dbl.foo { nil }).to compile_as(Int32) - expect(dbl.bar { nil }).to compile_as(String) - end - end - - it "returns self on undefined messages" do - expect(dbl.baz { nil }).to be(dbl) - end - end - end - - context "with abstract stubs and return type annotations" do - Spectator::NullDouble.define(TestDouble2) do - abstract_stub abstract def foo(value) : String - end - - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) } - subject(dbl) { TestDouble2.new([stub]) } - - it "enforces the return type" do - expect(dbl.foo("foobar")).to compile_as(String) - end - - it "raises on non-matching arguments" do - expect { dbl.foo("bar") }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - - it "raises on non-matching stub" do - stub = Spectator::ValueStub.new(:foo, 42, arguments).as(Spectator::Stub) - dbl._spectator_define_stub(stub) - expect { dbl.foo("foobar") }.to raise_error(TypeCastError, /String/) - end - end - - context "with nillable return type annotations" do - Spectator::NullDouble.define(TestDouble) do - abstract_stub abstract def foo : String? - abstract_stub abstract def bar : Nil - end - - let(foo_stub) { Spectator::ValueStub.new(:foo, nil).as(Spectator::Stub) } - let(bar_stub) { Spectator::ValueStub.new(:bar, nil).as(Spectator::Stub) } - subject(dbl) { TestDouble.new([foo_stub, bar_stub]) } - - it "doesn't raise on nil" do - aggregate_failures do - expect(dbl.foo).to be_nil - expect(dbl.bar).to be_nil - end - end - end - - context "with a method that uses NoReturn" do - Spectator::NullDouble.define(NoReturnDouble) do - abstract_stub abstract def oops : NoReturn - end - - subject(dbl) { NoReturnDouble.new } - - it "raises a TypeCastError when using a value-based stub" do - stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub) - dbl._spectator_define_stub(stub) - expect { dbl.oops }.to raise_error(TypeCastError, /NoReturn/) - end - - it "raises when using an exception stub" do - exception = ArgumentError.new("bogus") - stub = Spectator::ExceptionStub.new(:oops, exception).as(Spectator::Stub) - dbl._spectator_define_stub(stub) - expect { dbl.oops }.to raise_error(ArgumentError, "bogus") - end - end - - context "with common object methods" do - subject(dbl) do - EmptyDouble.new([ - Spectator::ValueStub.new(:"!=", false), - Spectator::ValueStub.new(:"!~", false), - Spectator::ValueStub.new(:"==", true), - Spectator::ValueStub.new(:"===", true), - Spectator::ValueStub.new(:"=~", nil), - Spectator::ValueStub.new(:class, EmptyDouble), - Spectator::ValueStub.new(:dup, EmptyDouble.new), - Spectator::ValueStub.new(:"in?", true), - Spectator::ValueStub.new(:inspect, "inspect"), - Spectator::ValueStub.new(:itself, EmptyDouble.new), - Spectator::ValueStub.new(:"not_nil!", EmptyDouble.new), - Spectator::ValueStub.new(:pretty_inspect, "pretty_inspect"), - Spectator::ValueStub.new(:tap, EmptyDouble.new), - Spectator::ValueStub.new(:to_json, "to_json"), - Spectator::ValueStub.new(:to_pretty_json, "to_pretty_json"), - Spectator::ValueStub.new(:to_s, "to_s"), - Spectator::ValueStub.new(:to_yaml, "to_yaml"), - Spectator::ValueStub.new(:try, nil), - Spectator::ValueStub.new(:object_id, 42_u64), - Spectator::ValueStub.new(:"same?", true), - ] of Spectator::Stub) - end - - it "responds with defined messages" do - aggregate_failures do - expect(dbl.!=(42)).to be_false - expect(dbl.!~(42)).to be_false - expect(dbl.==(42)).to be_true - expect(dbl.===(42)).to be_true - expect(dbl.=~(42)).to be_nil - expect(dbl.class).to eq(EmptyDouble) - expect(dbl.dup).to be_a(EmptyDouble) - expect(dbl.in?([42])).to eq(true) - expect(dbl.in?(1, 2, 3)).to eq(true) - expect(dbl.inspect).to eq("inspect") - expect(dbl.itself).to be_a(EmptyDouble) - expect(dbl.not_nil!).to be_a(EmptyDouble) - expect(dbl.pretty_inspect).to eq("pretty_inspect") - expect(dbl.tap { nil }).to be_a(EmptyDouble) - expect(dbl.to_json).to eq("to_json") - expect(dbl.to_pretty_json).to eq("to_pretty_json") - expect(dbl.to_s).to eq("to_s") - expect(dbl.to_yaml).to eq("to_yaml") - expect(dbl.try { nil }).to be_nil - expect(dbl.object_id).to eq(42_u64) - expect(dbl.same?(dbl)).to be_true - expect(dbl.same?(nil)).to be_true - end - end - - it "has a non-union return type" do - expect(dbl.inspect).to compile_as(String) - end - end - - context "without common object methods" do - subject(dbl) { EmptyDouble.new } - - it "returns original implementation with undefined messages" do - hasher = Crystal::Hasher.new - aggregate_failures do - expect(dbl.!=(42)).to be_true - expect(dbl.!~(42)).to be_true - expect(dbl.==(42)).to be_false - expect(dbl.===(42)).to be_false - expect(dbl.=~(42)).to be_nil - expect(dbl.class).to eq(EmptyDouble) - expect(dbl.dup).to be_a(EmptyDouble) - expect(dbl.hash(hasher)).to be_a(Crystal::Hasher) - expect(dbl.hash).to be_a(UInt64) - expect(dbl.in?([42])).to be_false - expect(dbl.in?(1, 2, 3)).to be_false - expect(dbl.itself).to be(dbl) - expect(dbl.not_nil!).to be(dbl) - expect(dbl.tap { nil }).to be(dbl) - expect(dbl.try { nil }).to be_nil - expect(dbl.object_id).to be_a(UInt64) - expect(dbl.same?(dbl)).to be_true - expect(dbl.same?(nil)).to be_false - end - end - end - - context "with arguments constraints" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - - context "without common object methods" do - Spectator::NullDouble.define(TestDouble) do - abstract_stub abstract def foo(value) : String - abstract_stub abstract def foo(value, & : -> _) : String - end - - let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) } - subject(dbl) { TestDouble.new([stub]) } - - it "returns the response when constraint satisfied" do - expect(dbl.foo("foobar")).to eq("bar") - end - - it "raises when constraint unsatisfied" do - expect { dbl.foo("baz") }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - - it "returns self when argument count doesn't match" do - expect(dbl.foo).to be(dbl) - end - - it "ignores the block argument if not in the constraint" do - expect(dbl.foo("foobar") { nil }).to eq("bar") - end - end - - context "with common object methods" do - Spectator::NullDouble.define(TestDouble) do - stub abstract def hash(hasher) : Crystal::Hasher - end - - let(hasher) { Crystal::Hasher.new } - let(stub) { Spectator::ValueStub.new(:hash, hasher, arguments).as(Spectator::Stub) } - subject(dbl) { TestDouble.new([stub]) } - - it "returns the response when constraint satisfied" do - expect(dbl.hash("foobar")).to be(hasher) - end - - it "raises when constraint unsatisfied" do - expect { dbl.hash("baz") }.to raise_error(Spectator::UnexpectedMessage, /hash/) - end - - it "raises when argument count doesn't match" do - expect { dbl.hash }.to raise_error(Spectator::UnexpectedMessage, /hash/) - end - end - end - - context "class method stubs" do - Spectator::NullDouble.define(ClassDouble) do - stub def self.foo - :stub - end - - stub def self.bar(arg) - arg - end - - stub def self.baz(arg, &) - yield - end - end - - subject(dbl) { ClassDouble } - let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } - - after { dbl._spectator_clear_stubs } - - it "overrides an existing method" do - expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override) - end - - it "doesn't affect other methods" do - expect { dbl._spectator_define_stub(foo_stub) }.to_not change { dbl.bar(42) } - end - - it "replaces an existing stub" do - dbl._spectator_define_stub(foo_stub) - stub = Spectator::ValueStub.new(:foo, :replacement) - expect { dbl._spectator_define_stub(stub) }.to change { dbl.foo }.to(:replacement) - end - - it "picks the correct stub based on arguments" do - stub1 = Spectator::ValueStub.new(:bar, :fallback) - stub2 = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) - dbl._spectator_define_stub(stub1) - dbl._spectator_define_stub(stub2) - aggregate_failures do - expect(dbl.bar(:wrong)).to eq(:fallback) - expect(dbl.bar(:match)).to eq(:override) - end - end - - it "only uses a stub if an argument constraint is met" do - stub = Spectator::ValueStub.new(:bar, :override, Spectator::Arguments.capture(:match)) - dbl._spectator_define_stub(stub) - aggregate_failures do - expect(dbl.bar(:original)).to eq(:original) - expect(dbl.bar(:match)).to eq(:override) - end - end - - it "ignores the block argument if not in the constraint" do - stub1 = Spectator::ValueStub.new(:baz, 1) - stub2 = Spectator::ValueStub.new(:baz, 2, Spectator::Arguments.capture(3)) - dbl._spectator_define_stub(stub1) - dbl._spectator_define_stub(stub2) - aggregate_failures do - expect(dbl.baz(5) { 42 }).to eq(1) - expect(dbl.baz(3) { 42 }).to eq(2) - end - end - - describe "._spectator_clear_stubs" do - before { dbl._spectator_define_stub(foo_stub) } - - it "removes previously defined stubs" do - expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub) - end - end - - describe "._spectator_calls" do - before { dbl._spectator_clear_calls } - - # Retrieves symbolic names of methods called on a double. - def called_method_names(dbl) - dbl._spectator_calls.map(&.method) - end - - it "stores calls to stubbed methods" do - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[]).to(%i[foo]) - end - - it "stores multiple calls to the same stub" do - dbl.foo - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[foo]).to(%i[foo foo]) - end - - it "stores arguments for a call" do - dbl.bar(42) - args = Spectator::Arguments.capture(42) - call = dbl._spectator_calls.first - expect(call.arguments).to eq(args) - end - end - end - - describe "#_spectator_define_stub" do - subject(dbl) { FooBarDouble.new } - let(stub3) { Spectator::ValueStub.new(:foo, 3) } - let(stub5) { Spectator::ValueStub.new(:foo, 5) } - let(stub7) { Spectator::ValueStub.new(:foo, 7, Spectator::Arguments.capture(:lucky)) } - - it "overrides an existing method" do - expect { dbl._spectator_define_stub(stub3) }.to change { dbl.foo }.from(42).to(3) - end - - it "replaces an existing stub" do - dbl._spectator_define_stub(stub3) - expect { dbl._spectator_define_stub(stub5) }.to change { dbl.foo }.from(3).to(5) - end - - it "doesn't affect other methods" do - expect { dbl._spectator_define_stub(stub5) }.to_not change { dbl.bar } - end - - it "picks the correct stub based on arguments" do - dbl._spectator_define_stub(stub5) - dbl._spectator_define_stub(stub7) - aggregate_failures do - expect(dbl.foo).to eq(5) - expect(dbl.foo(:lucky)).to eq(7) - end - end - - it "only uses a stub if an argument constraint is met" do - dbl._spectator_define_stub(stub7) - aggregate_failures do - expect(dbl.foo).to eq(42) - expect(dbl.foo(:lucky)).to eq(7) - end - end - - it "ignores the block argument if not in the constraint" do - dbl._spectator_define_stub(stub5) - dbl._spectator_define_stub(stub7) - aggregate_failures do - expect(dbl.foo { nil }).to eq(5) - expect(dbl.foo(:lucky) { nil }).to eq(7) - end - end - end - - describe "#_spectator_clear_stubs" do - subject(dbl) { FooBarDouble.new } - let(stub) { Spectator::ValueStub.new(:foo, 5) } - - before { dbl._spectator_define_stub(stub) } - - it "removes previously defined stubs" do - expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42) - end - end - - describe "#_spectator_calls" do - subject(dbl) { FooBarDouble.new } - let(stub) { Spectator::ValueStub.new(:foo, 5) } - - before { dbl._spectator_define_stub(stub) } - - # Retrieves symbolic names of methods called on a double. - def called_method_names(dbl) - dbl._spectator_calls.map(&.method) - end - - it "stores calls to stubbed methods" do - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[]).to(%i[foo]) - end - - it "stores multiple calls to the same stub" do - dbl.foo - expect { dbl.foo }.to change { called_method_names(dbl) }.from(%i[foo]).to(%i[foo foo]) - end - - it "stores calls to non-stubbed methods" do - expect { dbl.baz }.to change { called_method_names(dbl) }.from(%i[]).to(%i[baz]) - end - - it "stores arguments for a call" do - dbl.foo(42) - args = Spectator::Arguments.capture(42) - call = dbl._spectator_calls.first - expect(call.arguments).to eq(args) - end - end - - describe "#to_s" do - subject(string) { dbl.to_s } - - context "with a name" do - let(dbl) { FooBarDouble.new } - - it "indicates it's a double" do - expect(string).to contain("NullDouble") - end - - it "contains the double name" do - expect(string).to contain("dbl-name") - end - end - - context "without a name" do - let(dbl) { EmptyDouble.new } - - it "contains the double type" do - expect(string).to contain("NullDouble") - end - - it "contains \"Anonymous\"" do - expect(string).to contain("Anonymous") - end - end - end - - describe "#inspect" do - subject(string) { dbl.inspect } - - context "with a name" do - let(dbl) { FooBarDouble.new } - - it "contains the double type" do - expect(string).to contain("NullDouble") - end - - it "contains the double name" do - expect(string).to contain("dbl-name") - end - - it "contains the object ID" do - expect(string).to contain(dbl.object_id.to_s(16)) - end - end - - context "without a name" do - let(dbl) { EmptyDouble.new } - - it "contains the double type" do - expect(string).to contain("NullDouble") - end - - it "contains \"Anonymous\"" do - expect(string).to contain("Anonymous") - end - - it "contains the object ID" do - expect(string).to contain(dbl.object_id.to_s(16)) - end - end - end -end diff --git a/spec/spectator/mocks/null_stub_spec.cr b/spec/spectator/mocks/null_stub_spec.cr deleted file mode 100644 index 662fd23..0000000 --- a/spec/spectator/mocks/null_stub_spec.cr +++ /dev/null @@ -1,165 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::NullStub do - let(method_call) { Spectator::MethodCall.capture(:foo) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - subject(stub) { described_class.new(:foo, location: location) } - - it "stores the method name" do - expect(stub.method).to eq(:foo) - end - - it "stores the location" do - expect(stub.location).to eq(location) - end - - it "returns nil" do - expect(stub.call(method_call)).to be_nil - end - - context Spectator::StubModifiers do - describe "#and_return(value)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::NullStub.new(:foo, arguments, location) } - subject(stub) { original.and_return(42) } - - it "produces a stub that returns a value" do - expect(stub.call(method_call)).to eq(42) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_return(*values)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::NullStub.new(:foo, arguments, location) } - subject(stub) { original.and_return(3, 2, 1, 0) } - - it "produces a stub that returns values" do - values = Array.new(5) { stub.call(method_call) } - expect(values).to eq([3, 2, 1, 0, 0]) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_raise" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::NullStub.new(:foo, arguments, location) } - let(new_exception) { ArgumentError.new("Test argument error") } - subject(stub) { original.and_raise(new_exception) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - - context "with a class and message" do - subject(stub) { original.and_raise(ArgumentError, "Test argument error") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - end - - context "with a message" do - subject(stub) { original.and_raise("Test exception") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(Exception, "Test exception") - end - end - - context "with a class" do - subject(stub) { original.and_raise(ArgumentError) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError) - end - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - end - - describe "#===" do - subject { stub === call } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - - context "with a constraint" do - let(constraint) { Spectator::Arguments.capture(/foo/) } - let(stub) { described_class.new(:foo, constraint) } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - - context "with a non-matching arguments" do - let(call) { Spectator::MethodCall.capture(:foo, "baz") } - - it "returns false" do - is_expected.to be_false - end - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - end - end -end diff --git a/spec/spectator/mocks/proc_stub_spec.cr b/spec/spectator/mocks/proc_stub_spec.cr deleted file mode 100644 index bca4a05..0000000 --- a/spec/spectator/mocks/proc_stub_spec.cr +++ /dev/null @@ -1,182 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::ProcStub do - let(method_call) { Spectator::MethodCall.capture(:foo) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(proc) { Proc(Spectator::AbstractArguments, Int32).new { @call_count += 1 } } - subject(stub) { described_class.new(:foo, proc, location: location) } - - @call_count = 0 - - it "stores the method name" do - expect(stub.method).to eq(:foo) - end - - it "stores the location" do - expect(stub.location).to eq(location) - end - - it "calls the proc" do - expect(stub.call(method_call)).to eq(1) - end - - it "calls the proc for each invocation" do - stub.call(method_call) - expect { stub.call(method_call) }.to change { @call_count }.from(1).to(2) - end - - it "passed the original arguments" do - proc = Proc(Spectator::AbstractArguments, Spectator::AbstractArguments).new { |a| a } - stub = described_class.new(:foo, proc) - args = Spectator::Arguments.capture(42, bar: "baz") - call = Spectator::MethodCall.new(:foo, args) - captured = stub.call(call) - expect(captured).to eq(args) - end - - context Spectator::StubModifiers do - describe "#and_return(value)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::ProcStub.new(:foo, proc, arguments, location) } - subject(stub) { original.and_return(123) } - - it "produces a stub that returns a value" do - expect(stub.call(method_call)).to eq(123) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_return(*values)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::ProcStub.new(:foo, proc, arguments, location) } - subject(stub) { original.and_return(3, 2, 1, 0) } - - it "produces a stub that returns values" do - values = Array.new(5) { stub.call(method_call) } - expect(values).to eq([3, 2, 1, 0, 0]) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_raise" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::ProcStub.new(:foo, proc, arguments, location) } - let(new_exception) { ArgumentError.new("Test argument error") } - subject(stub) { original.and_raise(new_exception) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - - context "with a class and message" do - subject(stub) { original.and_raise(ArgumentError, "Test argument error") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - end - - context "with a message" do - subject(stub) { original.and_raise("Test exception") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(Exception, "Test exception") - end - end - - context "with a class" do - subject(stub) { original.and_raise(ArgumentError) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError) - end - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - end - - describe "#===" do - subject { stub === call } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - - context "with a constraint" do - let(constraint) { Spectator::Arguments.capture(/foo/) } - let(stub) { Spectator::ValueStub.new(:foo, 42, constraint) } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - - context "with a non-matching arguments" do - let(call) { Spectator::MethodCall.capture(:foo, "baz") } - - it "returns false" do - is_expected.to be_false - end - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - end - end -end diff --git a/spec/spectator/mocks/reference_mock_registry_spec.cr b/spec/spectator/mocks/reference_mock_registry_spec.cr deleted file mode 100644 index a872367..0000000 --- a/spec/spectator/mocks/reference_mock_registry_spec.cr +++ /dev/null @@ -1,93 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::ReferenceMockRegistry do - subject(registry) { described_class.new } - let(obj) { "foobar" } - let(stub) { Spectator::ValueStub.new(:test, 42) } - let(stubs) { [stub] of Spectator::Stub } - let(no_stubs) { [] of Spectator::Stub } - let(call) { Spectator::MethodCall.capture(:method2, 5) } - let(calls) { [call] } - let(no_calls) { [] of Spectator::MethodCall } - - it "initially has no stubs" do - expect(registry[obj].stubs).to be_empty - end - - it "initially has no calls" do - expect(registry[obj].calls).to be_empty - end - - it "stores stubs for an object" do - expect { registry[obj].stubs << stub }.to change { registry[obj].stubs }.from(no_stubs).to(stubs) - end - - it "stores calls for an object" do - expect { registry[obj].calls << call }.to change { registry[obj].calls }.from(no_calls).to(calls) - end - - it "isolates stubs between different objects" do - obj1 = "foo" - obj2 = "bar" - registry[obj2].stubs << Spectator::ValueStub.new(:obj2, 42) - expect { registry[obj1].stubs << stub }.to_not change { registry[obj2].stubs } - end - - it "isolates calls between different objects" do - obj1 = "foo" - obj2 = "bar" - registry[obj2].calls << Spectator::MethodCall.capture(:method1, 42) - expect { registry[obj1].calls << call }.to_not change { registry[obj2].calls } - end - - describe "#fetch" do - it "retrieves existing stubs" do - registry[obj].stubs << stub - expect(registry.fetch(obj) { no_stubs }.stubs).to eq(stubs) - end - - it "stores stubs on the first retrieval" do - expect(registry.fetch(obj) { stubs }.stubs).to eq(stubs) - end - - it "isolates stubs between different objects" do - obj1 = "foo" - obj2 = "bar" - registry[obj2].stubs << Spectator::ValueStub.new(:obj2, 42) - expect { registry.fetch(obj1) { no_stubs }.stubs }.to_not change { registry[obj2].stubs } - end - - it "isolates calls between different objects" do - obj1 = "foo" - obj2 = "bar" - registry[obj2].calls << Spectator::MethodCall.capture(:method1, 42) - expect { registry.fetch(obj1) { no_stubs }.calls }.to_not change { registry[obj2].calls } - end - end - - describe "#delete" do - it "clears stubs for an object" do - registry[obj].stubs << stub - expect { registry.delete(obj) }.to change { registry[obj].stubs }.from(stubs).to(no_stubs) - end - - it "doesn't clear initial stubs provided with #fetch" do - registry[obj].stubs << Spectator::ValueStub.new(:stub2, 42) - expect { registry.delete(obj) }.to change { registry.fetch(obj) { stubs }.stubs }.to(stubs) - end - - it "isolates stubs between different objects" do - obj1 = "foo" - obj2 = "bar" - registry[obj2].stubs << Spectator::ValueStub.new(:obj2, 42) - expect { registry.delete(obj1) }.to_not change { registry[obj2].stubs } - end - - it "isolates calls between different objects" do - obj1 = "foo" - obj2 = "bar" - registry[obj2].calls << Spectator::MethodCall.capture(:method1, 42) - expect { registry.delete(obj1) }.to_not change { registry[obj2].calls } - end - end -end diff --git a/spec/spectator/mocks/value_mock_registry_spec.cr b/spec/spectator/mocks/value_mock_registry_spec.cr deleted file mode 100644 index de2a1cc..0000000 --- a/spec/spectator/mocks/value_mock_registry_spec.cr +++ /dev/null @@ -1,93 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::ValueMockRegistry do - subject(registry) { Spectator::ValueMockRegistry(Int32).new } - let(obj) { 42 } - let(stub) { Spectator::ValueStub.new(:test, 5) } - let(stubs) { [stub] of Spectator::Stub } - let(no_stubs) { [] of Spectator::Stub } - let(call) { Spectator::MethodCall.capture(:method2, 5) } - let(calls) { [call] } - let(no_calls) { [] of Spectator::MethodCall } - - it "initially has no stubs" do - expect(registry[obj].stubs).to be_empty - end - - it "initially has no calls" do - expect(registry[obj].calls).to be_empty - end - - it "stores stubs for an object" do - expect { registry[obj].stubs << stub }.to change { registry[obj].stubs }.from(no_stubs).to(stubs) - end - - it "stores calls for an object" do - expect { registry[obj].calls << call }.to change { registry[obj].calls }.from(no_calls).to(calls) - end - - it "isolates stubs between different objects" do - obj1 = 1 - obj2 = 2 - registry[obj2].stubs << Spectator::ValueStub.new(:obj2, 42) - expect { registry[obj1].stubs << stub }.to_not change { registry[obj2].stubs } - end - - it "isolates calls between different objects" do - obj1 = 1 - obj2 = 2 - registry[obj2].calls << Spectator::MethodCall.capture(:method1, 42) - expect { registry[obj1].calls << call }.to_not change { registry[obj2].calls } - end - - describe "#fetch" do - it "retrieves existing stubs" do - registry[obj].stubs << stub - expect(registry.fetch(obj) { no_stubs }.stubs).to eq(stubs) - end - - it "stores stubs on the first retrieval" do - expect(registry.fetch(obj) { stubs }.stubs).to eq(stubs) - end - - it "isolates stubs between different objects" do - obj1 = 1 - obj2 = 2 - registry[obj2].stubs << Spectator::ValueStub.new(:obj2, 42) - expect { registry.fetch(obj1) { no_stubs }.stubs }.to_not change { registry[obj2].stubs } - end - - it "isolates calls between different objects" do - obj1 = 1 - obj2 = 2 - registry[obj2].calls << Spectator::MethodCall.capture(:method1, 42) - expect { registry.fetch(obj1) { no_stubs }.calls }.to_not change { registry[obj2].calls } - end - end - - describe "#delete" do - it "clears stubs for an object" do - registry[obj].stubs << stub - expect { registry.delete(obj) }.to change { registry[obj].stubs }.from(stubs).to(no_stubs) - end - - it "doesn't clear initial stubs provided with #fetch" do - registry[obj].stubs << Spectator::ValueStub.new(:stub2, 42) - expect { registry.delete(obj) }.to change { registry.fetch(obj) { stubs }.stubs }.to(stubs) - end - - it "isolates stubs between different objects" do - obj1 = 1 - obj2 = 2 - registry[obj2].stubs << Spectator::ValueStub.new(:obj2, 42) - expect { registry.delete(obj1) }.to_not change { registry[obj2].stubs } - end - - it "isolates calls between different objects" do - obj1 = 1 - obj2 = 2 - registry[obj2].calls << Spectator::MethodCall.capture(:method1, 42) - expect { registry.delete(obj1) }.to_not change { registry[obj2].calls } - end - end -end diff --git a/spec/spectator/mocks/value_stub_spec.cr b/spec/spectator/mocks/value_stub_spec.cr deleted file mode 100644 index 6f71619..0000000 --- a/spec/spectator/mocks/value_stub_spec.cr +++ /dev/null @@ -1,165 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::ValueStub do - let(method_call) { Spectator::MethodCall.capture(:foo) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - subject(stub) { described_class.new(:foo, 42, location: location) } - - it "stores the method name" do - expect(stub.method).to eq(:foo) - end - - it "stores the location" do - expect(stub.location).to eq(location) - end - - it "stores the return value" do - expect(stub.call(method_call)).to eq(42) - end - - context Spectator::StubModifiers do - describe "#and_return(value)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::ValueStub.new(:foo, 42, arguments, location) } - subject(stub) { original.and_return(123) } - - it "produces a stub that returns a value" do - expect(stub.call(method_call)).to eq(123) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_return(*values)" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::ValueStub.new(:foo, 42, arguments, location) } - subject(stub) { original.and_return(3, 2, 1, 0) } - - it "produces a stub that returns values" do - values = Array.new(5) { stub.call(method_call) } - expect(values).to eq([3, 2, 1, 0, 0]) - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - - describe "#and_raise" do - let(arguments) { Spectator::Arguments.capture(/foo/) } - let(location) { Spectator::Location.new(__FILE__, __LINE__) } - let(original) { Spectator::ValueStub.new(:foo, 42, arguments, location) } - let(new_exception) { ArgumentError.new("Test argument error") } - subject(stub) { original.and_raise(new_exception) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - - context "with a class and message" do - subject(stub) { original.and_raise(ArgumentError, "Test argument error") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError, "Test argument error") - end - end - - context "with a message" do - subject(stub) { original.and_raise("Test exception") } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(Exception, "Test exception") - end - end - - context "with a class" do - subject(stub) { original.and_raise(ArgumentError) } - - it "produces a stub that raises" do - expect { stub.call(method_call) }.to raise_error(ArgumentError) - end - end - - it "retains the method name" do - expect(stub.method).to eq(:foo) - end - - it "retains the arguments constraint" do - expect(stub.constraint).to eq(arguments) - end - - it "retains the location" do - expect(stub.location).to eq(location) - end - end - end - - describe "#===" do - subject { stub === call } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - - context "with a constraint" do - let(constraint) { Spectator::Arguments.capture(/foo/) } - let(stub) { Spectator::ValueStub.new(:foo, 42, constraint) } - - context "with a matching method name" do - let(call) { Spectator::MethodCall.capture(:foo, "foobar") } - - it "returns true" do - is_expected.to be_true - end - - context "with a non-matching arguments" do - let(call) { Spectator::MethodCall.capture(:foo, "baz") } - - it "returns false" do - is_expected.to be_false - end - end - end - - context "with a different method name" do - let(call) { Spectator::MethodCall.capture(:bar, "foobar") } - - it "returns false" do - is_expected.to be_false - end - end - end - end -end diff --git a/spec/spectator/system_exit_spec.cr b/spec/spectator/system_exit_spec.cr deleted file mode 100644 index 49cc4f6..0000000 --- a/spec/spectator/system_exit_spec.cr +++ /dev/null @@ -1,13 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator::SystemExit do - it "is raised when an attempt is made to exit the application" do - expect { exit }.to raise_error(described_class) - end - - it "has the status code passed to an exit call" do - exit 5 - rescue error : Spectator::SystemExit - expect(error.status).to eq(5) - end -end diff --git a/spec/spectator/value_spec.cr b/spec/spectator/value_spec.cr deleted file mode 100644 index 246e453..0000000 --- a/spec/spectator/value_spec.cr +++ /dev/null @@ -1,36 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator::Value do - subject { described_class.new(42, "Test Label") } - - it "stores the value" do - # NOTE: This cast is a workaround for [issue #55](https://gitlab.com/arctic-fox/spectator/-/issues/55) - value = subject.as(Spectator::Value(Int32)).value - expect(value).to eq(42) - end - - describe "#to_s" do - subject { super.to_s } - - it "contains the label" do - is_expected.to contain("Test Label") - end - - it "contains the value" do - is_expected.to contain("42") - end - end - - describe "#inspect" do - let(value) { described_class.new([42], "Test Label") } - subject { value.inspect } - - it "contains the label" do - is_expected.to contain("Test Label") - end - - it "contains the value" do - is_expected.to contain("[42]") - end - end -end diff --git a/spec/spectator/wrapper_spec.cr b/spec/spectator/wrapper_spec.cr deleted file mode 100644 index 9920adc..0000000 --- a/spec/spectator/wrapper_spec.cr +++ /dev/null @@ -1,13 +0,0 @@ -require "../spec_helper" - -Spectator.describe Spectator::Wrapper do - it "stores a value" do - wrapper = described_class.new(42) - expect(wrapper.get(Int32)).to eq(42) - end - - it "retrieves a value using the block trick" do - wrapper = described_class.new(Int32) - expect(wrapper.get { Int32 }).to eq(Int32) - end -end diff --git a/spec/spectator_spec.cr b/spec/spectator_spec.cr new file mode 100644 index 0000000..67b53a7 --- /dev/null +++ b/spec/spectator_spec.cr @@ -0,0 +1,9 @@ +require "./spec_helper" + +describe Spectator do + # TODO: Write tests + + it "works" do + false.should eq(true) + end +end diff --git a/src/spectator.cr b/src/spectator.cr index a05a7e2..f08083c 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -1,120 +1,5 @@ -require "colorize" -require "log" -require "./spectator/includes" - -# Module that contains all functionality related to Spectator. +# Feature-rich testing framework for Crystal inspired by RSpec. module Spectator - extend self - include DSL::Top - # Current version of the Spectator library. VERSION = {{ `shards version "#{__DIR__}"`.stringify.chomp }} - - # Logger for Spectator internals. - ::Log.setup_from_env - Log = ::Log.for(self) - - # Flag indicating whether Spectator should automatically run tests. - # This should be left alone (set to true) in typical usage. - # There are times when Spectator shouldn't run tests. - # One of those is testing Spectator. - class_property? autorun = true - - # All tests are ran just before the executable exits. - # Tests will be skipped, however, if `#autorun?` is set to false. - # There are a couple of reasons for this. - # - # First is that we want a clean interface for the end-user. - # They shouldn't need to call a "run" method. - # That adds the burden on the developer to ensure the tests are run after they are created. - # And that gets complicated when there are multiple files that could run in any order. - # - # Second is to allow all of the tests and framework to be constructed. - # We know that all of the instances and DSL builders have finished - # after the main part of the executable has run. - # - # By doing this, we provide a clean interface and safely run after everything is constructed. - # The downside, if something bad happens, like an exception is raised, - # Crystal doesn't display much information about what happened. - # That issue is handled by putting a begin/rescue block to show a custom error message. - at_exit do - # Run only if `#autorun?` is true. - # Return 1 on failure. - exit(1) if autorun? && !run - end - - @@config_builder = Config::Builder.new - @@config : Config? - - # Provides a means to configure how Spectator will run and report tests. - # A `ConfigBuilder` is yielded to allow changing the configuration. - # NOTE: The configuration set here can be overridden - # with a `.spectator` file and command-line arguments. - def configure(& : Config::Builder -> _) : Nil - yield @@config_builder - end - - # Random number generator for the test suite. - # All randomly generated values should be pulled from this. - # This provides re-producible results even though random values are used. - # The seed for this random generator is controlled by `ConfigBuilder.seed=`. - def random - config.random - end - - # Builds the tests and runs the framework. - private def run - # Silence default logger. - ::Log.setup_from_env(default_level: :none) - - # Build the spec and run it. - spec = DSL::Builder.build - spec.run - rescue ex - # Re-enable logger for fatal error. - ::Log.setup_from_env - - # Catch all unhandled exceptions here. - # Examples are already wrapped, so any exceptions they throw are caught. - # But if an exception occurs outside an example, - # it's likely the fault of the test framework (Spectator). - # So we display a helpful error that could be reported and return non-zero. - Log.fatal(exception: ex) { "Spectator encountered an unexpected error" } - false - end - - # Global configuration used by Spectator for running tests. - class_getter(config) { build_config } - - # Builds the configuration. - private def build_config - # Build up the configuration from various sources. - # The sources that take priority are later in the list. - apply_config_file - apply_command_line_args - - @@config_builder.build - end - - # Path to the Spectator configuration file. - # The contents of this file should contain command-line arguments. - # Those arguments are automatically applied when Spectator starts. - # Arguments should be placed with one per line. - CONFIG_FILE_PATH = ".spectator" - - # Loads configuration arguments from a file. - # The file is expected to be new-line delimited, - # one argument per line. - # The arguments are identical to those - # that would be passed on the command-line. - private def apply_config_file(file_path = CONFIG_FILE_PATH) : Nil - return unless File.exists?(file_path) - args = File.read(file_path).lines - Config::CLIArgumentsApplicator.new(args).apply(@@config_builder) - end - - # Applies configuration options from the command-line arguments - private def apply_command_line_args : Nil - Config::CLIArgumentsApplicator.new.apply(@@config_builder) - end end diff --git a/src/spectator/abstract_expression.cr b/src/spectator/abstract_expression.cr deleted file mode 100644 index 6ab4cbf..0000000 --- a/src/spectator/abstract_expression.cr +++ /dev/null @@ -1,53 +0,0 @@ -require "./label" - -module Spectator - # Represents an expression from a test. - # This is typically captured by an `expect` macro. - # It consists of a label and the value of the expression. - # The label should be a string recognizable by the user, - # or nil if one isn't available. - # - # This base class is provided so that all generic sub-classes can be stored as this one type. - # The value of the expression can be retrieved by down-casting to the expected type with `#cast`. - # - # NOTE: This is intentionally a class and not a struct. - # If it were a struct, changes made to the value held by an instance may not be kept when passing it around. - # See commit ca564619ad2ae45f832a058d514298c868fdf699. - abstract class AbstractExpression - # User recognizable string for the expression. - # This can be something like a variable name or a snippet of Crystal code. - getter label : Label - - # Creates the expression. - # The *label* is usually the Crystal code evaluating to the `#raw_value`. - # It can be nil if it isn't available. - def initialize(@label : Label) - end - - # Retrieves the evaluated value of the expression. - abstract def raw_value - - # Attempts to cast `#raw_value` to the type *T* and return it. - def cast(type : T.class) : T forall T - raw_value.as(T) - end - - # Produces a string representation of the expression. - # This consists of the label (if one is available) and the value. - def to_s(io : IO) : Nil - if (label = @label) - io << label << ": " - end - raw_value.to_s(io) - end - - # Produces a detailed string representation of the expression. - # This consists of the label (if one is available) and the value. - def inspect(io : IO) : Nil - if (label = @label) - io << label << ": " - end - raw_value.inspect(io) - end - end -end diff --git a/src/spectator/anything.cr b/src/spectator/anything.cr deleted file mode 100644 index aa25e3c..0000000 --- a/src/spectator/anything.cr +++ /dev/null @@ -1,25 +0,0 @@ -module Spectator - # Type dedicated to matching everything. - # This is intended to be used as a value to compare against when the value doesn't matter. - # Can be used like so: - # ``` - # anything = Spectator::Anything.new - # expect("foo").to match(anything) - # ``` - struct Anything - # Always returns true. - def ===(other) - true - end - - # Displays "anything". - def to_s(io : IO) : Nil - io << "anything" - end - - # Displays "". - def inspect(io : IO) : Nil - io << "" - end - end -end diff --git a/src/spectator/block.cr b/src/spectator/block.cr deleted file mode 100644 index 3e861ff..0000000 --- a/src/spectator/block.cr +++ /dev/null @@ -1,34 +0,0 @@ -require "./expression" -require "./label" - -module Spectator - # Represents a block from a test. - # This is typically captured by an `expect` macro. - # It consists of a label and parameter-less block. - # The label should be a string recognizable by the user, - # or nil if one isn't available. - class Block(T) < Expression(T) - # Creates the block expression from a proc. - # The *proc* will be called to evaluate the value of the expression. - # The *label* is usually the Crystal code for the *proc*. - # It can be nil if it isn't available. - def initialize(@block : -> T, label : Label = nil) - super(label) - end - - # Creates the block expression by capturing a block as a proc. - # The block will be called to evaluate the value of the expression. - # The *label* is usually the Crystal code for the *block*. - # It can be nil if it isn't available. - def initialize(label : Label = nil, &@block : -> T) - super(label) - end - - # Evaluates the block and returns the value from it. - # This method _does not_ cache the resulting value like `#value` does. - # Successive calls to this method may return different values. - def value : T - @block.call - end - end -end diff --git a/src/spectator/composite_node_filter.cr b/src/spectator/composite_node_filter.cr deleted file mode 100644 index 1450bcc..0000000 --- a/src/spectator/composite_node_filter.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "./node_filter" - -module Spectator - # Filter that combines multiple other filters. - class CompositeNodeFilter < NodeFilter - # Creates the example filter. - def initialize(@filters : Array(NodeFilter)) - end - - # Checks whether the node satisfies the filter. - def includes?(node) : Bool - @filters.any?(&.includes?(node)) - end - end -end diff --git a/src/spectator/config.cr b/src/spectator/config.cr deleted file mode 100644 index 66e6501..0000000 --- a/src/spectator/config.cr +++ /dev/null @@ -1,120 +0,0 @@ -require "./config/*" -require "./node_filter" -require "./example_group" -require "./filtered_example_iterator" -require "./formatting/formatter" -require "./node_iterator" -require "./run_flags" - -module Spectator - # Provides customization and describes specifics for how Spectator will run and report tests. - class Config - # Primary formatter all events will be sent to. - getter formatter : Formatting::Formatter - - # Flags indicating how the spec should run. - getter run_flags : RunFlags - - # Seed used for random number generation. - getter random_seed : UInt64 - - # Filter used to select which examples to run. - getter node_filter : NodeFilter - - # Filter used to select which examples to _not_ run. - getter node_reject : NodeFilter - - # Tags to filter on if they're present in a spec. - protected getter match_filters : Metadata - - # List of hooks to run before all examples in the test suite. - protected getter before_suite_hooks : Deque(ExampleGroupHook) - - # List of hooks to run before each top-level example group. - protected getter before_all_hooks : Deque(ExampleGroupHook) - - # List of hooks to run before every example. - protected getter before_each_hooks : Deque(ExampleHook) - - # List of hooks to run after all examples in the test suite. - protected getter after_suite_hooks : Deque(ExampleGroupHook) - - # List of hooks to run after each top-level example group. - protected getter after_all_hooks : Deque(ExampleGroupHook) - - # List of hooks to run after every example. - protected getter after_each_hooks : Deque(ExampleHook) - - # List of hooks to run around every example. - protected getter around_each_hooks : Deque(ExampleProcsyHook) - - # Creates a new configuration. - # Properties are pulled from *source*. - # Typically, *source* is a `Config::Builder`. - def initialize(source) - @formatter = source.formatter - @run_flags = source.run_flags - @random_seed = source.random_seed - @node_filter = source.node_filter - @node_reject = source.node_reject - @match_filters = source.match_filters - - @before_suite_hooks = source.before_suite_hooks - @before_all_hooks = source.before_all_hooks - @before_each_hooks = source.before_each_hooks - @after_suite_hooks = source.after_suite_hooks - @after_all_hooks = source.after_all_hooks - @after_each_hooks = source.after_each_hooks - @around_each_hooks = source.around_each_hooks - end - - # Produces the default configuration. - def self.default : self - Builder.new.build - end - - # Shuffles the items in an array using the configured random settings. - # If `#randomize?` is true, the *items* are shuffled and returned as a new array. - # Otherwise, the items are left alone and returned as-is. - # The array of *items* is never modified. - def shuffle(items) - return items unless run_flags.randomize? - - items.shuffle(random) - end - - # Shuffles the items in an array using the configured random settings. - # If `#randomize?` is true, the *items* are shuffled and returned. - # Otherwise, the items are left alone and returned as-is. - # The array of *items* is modified, the items are shuffled in-place. - def shuffle!(items) - return items unless run_flags.randomize? - - items.shuffle!(random) - end - - # Creates an iterator configured to select the filtered examples. - def iterator(group : ExampleGroup) - match_filter = match_filter(group) - iterator = FilteredExampleIterator.new(group, @node_filter) - iterator = iterator.select(match_filter) if match_filter - iterator.reject(@node_reject) - end - - # Creates a node filter if any conditionally matching filters apply to an example group. - private def match_filter(group : ExampleGroup) : NodeFilter? - iterator = NodeIterator.new(group) - filters = @match_filters.compact_map do |key, value| - filter = TagNodeFilter.new(key.to_s, value) - filter.as(NodeFilter) if iterator.rewind.any?(filter) - end - CompositeNodeFilter.new(filters) unless filters.empty? - end - - # Retrieves the configured random number generator. - # This will produce the same generator with the same seed every time. - def random - Random.new(random_seed) - end - end -end diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr deleted file mode 100644 index c7bdfa7..0000000 --- a/src/spectator/config/builder.cr +++ /dev/null @@ -1,317 +0,0 @@ -require "../composite_node_filter" -require "../node_filter" -require "../formatting" -require "../metadata" -require "../null_node_filter" -require "../run_flags" -require "../tag_node_filter" - -module Spectator - class Config - # Mutable configuration used to produce a final configuration. - # Use the setters in this class to incrementally build a configuration. - # Then call `#build` to create the final configuration. - class Builder - # Seed used for random number generation. - property random_seed : UInt64 = Random.rand(100000_u64) - - # Toggles indicating how the test spec should execute. - property run_flags = RunFlags::None - - protected getter match_filters : Metadata = {:focus => nil.as(String?)} - - @primary_formatter : Formatting::Formatter? - @additional_formatters = [] of Formatting::Formatter - @filters = [] of NodeFilter - @rejects = [] of NodeFilter - - # List of hooks to run before all examples in the test suite. - protected getter before_suite_hooks = Deque(ExampleGroupHook).new - - # List of hooks to run before each top-level example group. - protected getter before_all_hooks = Deque(ExampleGroupHook).new - - # List of hooks to run before every example. - protected getter before_each_hooks = Deque(ExampleHook).new - - # List of hooks to run after all examples in the test suite. - protected getter after_suite_hooks = Deque(ExampleGroupHook).new - - # List of hooks to run after each top-level example group. - protected getter after_all_hooks = Deque(ExampleGroupHook).new - - # List of hooks to run after every example. - protected getter after_each_hooks = Deque(ExampleHook).new - - # List of hooks to run around every example. - protected getter around_each_hooks = Deque(ExampleProcsyHook).new - - # Attaches a hook to be invoked before all examples in the test suite. - def add_before_suite_hook(hook) - @before_suite_hooks.push(hook) - end - - # Defines a block of code to execute before all examples in the test suite. - def before_suite(&block) - hook = ExampleGroupHook.new(&block) - add_before_suite_hook(hook) - end - - # Attaches a hook to be invoked before each top-level example group. - def add_before_all_hook(hook) - @before_all_hooks.push(hook) - end - - # Defines a block of code to execute before each top-level example group. - def before_all(&block) - hook = ExampleGroupHook.new(&block) - add_before_all_hook(hook) - end - - # Attaches a hook to be invoked before every example. - # The current example is provided as a block argument. - def add_before_each_hook(hook) - @before_each_hooks.push(hook) - end - - # Defines a block of code to execute before every. - # The current example is provided as a block argument. - def before_each(&block : Example -> _) - hook = ExampleHook.new(&block) - add_before_each_hook(hook) - end - - # Attaches a hook to be invoked after all examples in the test suite. - def add_after_suite_hook(hook) - @after_suite_hooks.unshift(hook) - end - - # Defines a block of code to execute after all examples in the test suite. - def after_suite(&block) - hook = ExampleGroupHook.new(&block) - add_after_suite_hook(hook) - end - - # Attaches a hook to be invoked after each top-level example group. - def add_after_all_hook(hook) - @after_all_hooks.unshift(hook) - end - - # Defines a block of code to execute after each top-level example group. - def after_all(&block) - hook = ExampleGroupHook.new(&block) - add_after_all_hook(hook) - end - - # Attaches a hook to be invoked after every example. - # The current example is provided as a block argument. - def add_after_each_hook(hook) - @after_each_hooks.unshift(hook) - end - - # Defines a block of code to execute after every example. - # The current example is provided as a block argument. - def after_each(&block : Example -> _) - hook = ExampleHook.new(&block) - add_after_each_hook(hook) - end - - # Attaches a hook to be invoked around every example. - # The current example in procsy form is provided as a block argument. - def add_around_each_hook(hook) - @around_each_hooks.push(hook) - end - - # Defines a block of code to execute around every example. - # The current example in procsy form is provided as a block argument. - def around_each(&block : Example::Procsy -> _) - hook = ExampleProcsyHook.new(label: "around_each", &block) - add_around_each_hook(hook) - end - - # Creates a configuration. - def build : Config - Config.new(self) - end - - # Sets the primary formatter to use for reporting test progress and results. - def formatter=(formatter : Formatting::Formatter) - @primary_formatter = formatter - end - - # Adds an extra formatter to use for reporting test progress and results. - def add_formatter(formatter : Formatting::Formatter) - @additional_formatters << formatter - end - - # Retrieves the formatters to use. - # If one wasn't specified by the user, - # then `#default_formatter` is returned. - private def formatters - @additional_formatters + [(@primary_formatter || default_formatter)] - end - - # The formatter that should be used if one wasn't provided. - private def default_formatter - Formatting::ProgressFormatter.new - end - - # A single formatter that will satisfy the configured output. - # If one formatter was configured, then it is returned. - # Otherwise, a `Formatting::BroadcastFormatter` is returned. - protected def formatter - case (formatters = self.formatters) - when .one? then formatters.first - else Formatting::BroadcastFormatter.new(formatters) - end - end - - # Enables fail-fast mode. - def fail_fast - @run_flags |= RunFlags::FailFast - end - - # Sets the fail-fast flag. - def fail_fast=(flag) - if flag - @run_flags |= RunFlags::FailFast - else - @run_flags &= ~RunFlags::FailFast - end - end - - # Indicates whether fail-fast mode is enabled. - protected def fail_fast? - @run_flags.fail_fast? - end - - # Enables fail-blank mode (fail on no tests). - def fail_blank - @run_flags |= RunFlags::FailBlank - end - - # Enables or disables fail-blank mode. - def fail_blank=(flag) - if flag - @run_flags |= RunFlags::FailBlank - else - @run_flags &= ~RunFlags::FailBlank - end - end - - # Indicates whether fail-fast mode is enabled. - # That is, it is a failure if there are no tests. - protected def fail_blank? - @run_flags.fail_blank? - end - - # Enables dry-run mode. - def dry_run - @run_flags |= RunFlags::DryRun - end - - # Enables or disables dry-run mode. - def dry_run=(flag) - if flag - @run_flags |= RunFlags::DryRun - else - @run_flags &= ~RunFlags::DryRun - end - end - - # Indicates whether dry-run mode is enabled. - # In this mode, no tests are run, but output acts like they were. - protected def dry_run? - @run_flags.dry_run? - end - - # Randomizes test execution order. - def randomize - @run_flags |= RunFlags::Randomize - end - - # Enables or disables running tests in a random order. - def randomize=(flag) - if flag - @run_flags |= RunFlags::Randomize - else - @run_flags &= ~RunFlags::Randomize - end - end - - # Indicates whether tests are run in a random order. - protected def randomize? - @run_flags.randomize? - end - - # Displays profiling information - def profile - @run_flags |= RunFlags::Profile - end - - # Enables or disables displaying profiling information. - def profile=(flag) - if flag - @run_flags |= RunFlags::Profile - else - @run_flags &= ~RunFlags::Profile - end - end - - # Indicates whether profiling information should be displayed. - protected def profile? - @run_flags.profile? - end - - # Adds a filter to determine which examples can run. - def add_node_filter(filter : NodeFilter) - @filters << filter - end - - # Specifies one or more tags to constrain running examples to. - def filter_run_including(*tags : Symbol, **values) - tags.each { |tag| @filters << TagNodeFilter.new(tag.to_s) } - values.each { |tag, value| @filters << TagNodeFilter.new(tag.to_s, value.to_s) } - end - - # Adds a filter to prevent examples from running. - def add_node_reject(filter : NodeFilter) - @rejects << filter - end - - # Specifies one or more tags to exclude from running examples. - def filter_run_excluding(*tags : Symbol, **values) - tags.each { |tag| @rejects << TagNodeFilter.new(tag.to_s) } - values.each { |tag, value| @rejects << TagNodeFilter.new(tag.to_s, value.to_s) } - end - - # Specifies one or more tags to filter on only if they're present in the spec. - def filter_run_when_matching(*tags : Symbol, **values) - tags.each { |tag| @match_filters[tag] = nil } - values.each { |tag, value| @match_filters[tag] = value.to_s } - end - - # Retrieves a filter that determines which examples can run. - # If no filters were added with `#add_node_filter`, - # then the returned filter will allow all examples to be run. - protected def node_filter - case (filters = @filters) - when .empty? then NullNodeFilter.new - when .one? then filters.first - else CompositeNodeFilter.new(filters) - end - end - - # Retrieves a filter that prevents examples from running. - # If no filters were added with `#add_node_reject`, - # then the returned filter will allow all examples to be run. - protected def node_reject - case (filters = @rejects) - when .empty? then NullNodeFilter.new(false) - when .one? then filters.first - else CompositeNodeFilter.new(filters) - end - end - end - end -end diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr deleted file mode 100644 index 15c9f94..0000000 --- a/src/spectator/config/cli_arguments_applicator.cr +++ /dev/null @@ -1,240 +0,0 @@ -require "colorize" -require "option_parser" -require "../formatting" -require "../line_node_filter" -require "../location" -require "../location_node_filter" -require "../name_node_filter" -require "../tag_node_filter" - -module Spectator - class Config - # Applies command-line arguments to a configuration. - class CLIArgumentsApplicator - # Logger for this class. - Log = Spectator::Log.for("config") - - # Creates the configuration source. - # By default, the command-line arguments (ARGV) are used. - # But custom arguments can be passed in. - def initialize(@args : Array(String) = ARGV) - end - - # Applies the specified configuration to a builder. - # Calling this method from multiple sources builds up the final configuration. - def apply(builder) : Nil - OptionParser.parse(@args) do |parser| - control_parser_options(parser, builder) - filter_parser_options(parser, builder) - output_parser_options(parser, builder) - end - end - - # Adds options to the parser for controlling the test execution. - private def control_parser_options(parser, builder) - fail_fast_option(parser, builder) - fail_blank_option(parser, builder) - dry_run_option(parser, builder) - random_option(parser, builder) - seed_option(parser, builder) - order_option(parser, builder) - end - - # Adds the fail-fast option to the parser. - private def fail_fast_option(parser, builder) - parser.on("-f", "--fail-fast", "Stop testing on first failure") do - Log.debug { "Enabling fail-fast (-f)" } - builder.fail_fast - end - end - - # Adds the fail-blank option to the parser. - private def fail_blank_option(parser, builder) - parser.on("-b", "--fail-blank", "Fail if there are no examples") do - Log.debug { "Enabling fail-blank (-b)" } - builder.fail_blank - end - end - - # Adds the dry-run option to the parser. - private def dry_run_option(parser, builder) - parser.on("-d", "--dry-run", "Don't run any tests, output what would have run") do - Log.debug { "Enabling dry-run (-d)" } - builder.dry_run - end - end - - # Adds the randomize examples option to the parser. - private def random_option(parser, builder) - parser.on("-r", "--rand", "Randomize the execution order of tests") do - Log.debug { "Randomizing test order (-r)" } - builder.randomize - end - end - - # Adds the random seed option to the parser. - private def seed_option(parser, builder) - parser.on("--seed INTEGER", "Set the seed for the random number generator (implies -r)") do |seed| - Log.debug { "Randomizing test order and setting RNG seed to #{seed}" } - builder.randomize - builder.random_seed = seed.to_u64 - end - end - - # Adds the example order option to the parser. - private def order_option(parser, builder) - parser.on("--order ORDER", "Set the test execution order. ORDER should be one of: defined, rand, or rand:SEED") do |method| - case method.downcase - when "defined" - Log.debug { "Disabling randomized tests (--order defined)" } - builder.randomize = false - when /^rand/ - builder.randomize - parts = method.split(':', 2) - if (seed = parts[1]?) - Log.debug { "Randomizing test order and setting RNG seed to #{seed} (--order rand:#{seed})" } - builder.random_seed = seed.to_u64 - else - Log.debug { "Randomizing test order (--order rand)" } - end - end - end - end - - # Adds options to the parser for filtering examples. - private def filter_parser_options(parser, builder) - example_option(parser, builder) - line_option(parser, builder) - location_option(parser, builder) - tag_option(parser, builder) - end - - # Adds the example filter option to the parser. - private def example_option(parser, builder) - parser.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |pattern| - Log.debug { "Filtering for examples containing '#{pattern}' (-e '#{pattern}')" } - filter = NameNodeFilter.new(pattern) - builder.add_node_filter(filter) - end - end - - # Adds the line filter option to the parser. - private def line_option(parser, builder) - parser.on("-l", "--line LINE", "Run examples whose line matches LINE") do |line| - Log.debug { "Filtering for examples on line #{line} (-l #{line})" } - filter = LineNodeFilter.new(line.to_i) - builder.add_node_filter(filter) - end - end - - # Adds the location filter option to the parser. - private def location_option(parser, builder) - parser.on("--location FILE:LINE", "Run the example at line 'LINE' in the file 'FILE', multiple allowed") do |location| - Log.debug { "Filtering for examples at #{location} (--location '#{location}')" } - location = Location.parse(location) - filter = LocationNodeFilter.new(location) - builder.add_node_filter(filter) - end - end - - # Adds the tag filter option to the parser. - private def tag_option(parser, builder) - parser.on("--tag TAG[:VALUE]", "Run examples with the specified TAG, or exclude examples by adding ~ before the TAG.") do |tag| - negated = tag.starts_with?('~') - tag = tag.lchop('~') - Log.debug { "Filtering for example with tag #{tag}" } - parts = tag.split(':', 2, remove_empty: true) - if parts.size > 1 - tag = parts.first - value = parts.last - end - - filter = TagNodeFilter.new(tag, value) - if negated - builder.add_node_reject(filter) - else - builder.add_node_filter(filter) - end - end - end - - # Adds options to the parser for changing output. - private def output_parser_options(parser, builder) - verbose_option(parser, builder) - help_option(parser, builder) - profile_option(parser, builder) - json_option(parser, builder) - tap_option(parser, builder) - junit_option(parser, builder) - html_option(parser, builder) - no_color_option(parser, builder) - end - - # Adds the verbose output option to the parser. - private def verbose_option(parser, builder) - parser.on("-v", "--verbose", "Verbose output using document formatter") do - Log.debug { "Setting output format to document (-v)" } - builder.formatter = Formatting::DocumentFormatter.new - end - end - - # Adds the help output option to the parser. - private def help_option(parser, builder) - parser.on("-h", "--help", "Show this help") do - puts parser - exit - end - end - - # Adds the profile output option to the parser. - private def profile_option(parser, builder) - parser.on("-p", "--profile", "Display the 10 slowest specs") do - Log.debug { "Enabling timing information (-p)" } - builder.profile - end - end - - # Adds the JSON output option to the parser. - private def json_option(parser, builder) - parser.on("--json", "Generate JSON output") do - Log.debug { "Setting output format to JSON (--json)" } - builder.formatter = Formatting::JSONFormatter.new - end - end - - # Adds the TAP output option to the parser. - private def tap_option(parser, builder) - parser.on("--tap", "Generate TAP output (Test Anything Protocol)") do - Log.debug { "Setting output format to TAP (--tap)" } - builder.formatter = Formatting::TAPFormatter.new - end - end - - # Adds the JUnit output option to the parser. - private def junit_option(parser, builder) - parser.on("--junit_output OUTPUT_DIR", "Generate JUnit XML output") do |output_dir| - Log.debug { "Setting output format to JUnit XML (--junit_output '#{output_dir}')" } - formatter = Formatting::JUnitFormatter.new(output_dir) - builder.add_formatter(formatter) - end - end - - # Adds the HTML output option to the parser. - private def html_option(parser, builder) - parser.on("--html_output OUTPUT_DIR", "Generate HTML output") do |output_dir| - Log.debug { "Setting output format to HTML (--html_output '#{output_dir}')" } - formatter = Formatting::HTMLFormatter.new(output_dir) - builder.add_formatter(formatter) - end - end - - # Adds the "no color" output option to the parser. - private def no_color_option(parser, builder) - parser.on("--no-color", "Disable colored output") do - Log.debug { "Disabling color output (--no-color)" } - Colorize.enabled = false - end - end - end - end -end diff --git a/src/spectator/context.cr b/src/spectator/context.cr deleted file mode 100644 index 15b9335..0000000 --- a/src/spectator/context.cr +++ /dev/null @@ -1,37 +0,0 @@ -# Base class that all test cases run in. -# This type is used to store all test case contexts as a single type. -# The instance must be downcast to the correct type before calling a context method. -# This type is intentionally outside the `Spectator` module. -# The reason for this is to prevent name collision when using the DSL to define a spec. -abstract class SpectatorContext - # Evaluates the contents of a block within the scope of the context. - def eval(&) - with self yield - end - - # Produces a dummy string to represent the context as a string. - # This prevents the default behavior, which normally stringifies instance variables. - # Due to the sheer amount of types Spectator can create - # and that the Crystal compiler instantiates a `#to_s` and/or `#inspect` for each of those types, - # an explosion in method instances can be created. - # The compile time is drastically reduced by using a dummy string instead. - def to_s(io : IO) : Nil - io << "Context" - end - - # :ditto: - def inspect(io : IO) : Nil - io << "Context<" << self.class << '>' - end -end - -module Spectator - # Base class that all test cases run in. - # This type is used to store all test case contexts as a single type. - # The instance must be downcast to the correct type before calling a context method. - # - # Nested contexts, such as those defined by `context` and `describe` in the DSL, can define their own methods. - # The intent is that a proc will downcast to the correct type and call one of those methods. - # This is how methods that contain test cases, hooks, and other context-specific code blocks get invoked. - alias Context = ::SpectatorContext -end diff --git a/src/spectator/context_delegate.cr b/src/spectator/context_delegate.cr deleted file mode 100644 index ac927b9..0000000 --- a/src/spectator/context_delegate.cr +++ /dev/null @@ -1,27 +0,0 @@ -require "./context" -require "./context_method" -require "./null_context" - -module Spectator - # Stores a test context and a method to call within it. - struct ContextDelegate - # Creates the delegate. - # The *context* is the instance of the test context. - # The *method* is proc that downcasts *context* and calls a method on it. - def initialize(@context : Context, @method : ContextMethod) - end - - # Creates a delegate with a null context. - # The context will be ignored and the block will be executed in its original scope. - def self.null(&block : -> _) - context = NullContext.new - method = ContextMethod.new { block.call } - new(context, method) - end - - # Invokes a method in the test context. - def call - @method.call(@context) - end - end -end diff --git a/src/spectator/context_method.cr b/src/spectator/context_method.cr deleted file mode 100644 index 30cce6d..0000000 --- a/src/spectator/context_method.cr +++ /dev/null @@ -1,10 +0,0 @@ -require "./context" - -module Spectator - # Encapsulates a method in a test context. - # This could be used to invoke a test case or hook method. - # The context is passed as an argument. - # The proc should downcast the context instance to the desired type - # and call a method on that context. - alias ContextMethod = Context -> -end diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr deleted file mode 100644 index a7d89fb..0000000 --- a/src/spectator/dsl.cr +++ /dev/null @@ -1,11 +0,0 @@ -require "./dsl/*" - -module Spectator - # Namespace containing methods representing the spec domain specific language. - # - # Note: Documentation inside macros is kept to a minimum to reduce generated code. - # This also helps keep error traces small. - # Documentation only useful for debugging is included in generated code. - module DSL - end -end diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr deleted file mode 100644 index ed1c6a4..0000000 --- a/src/spectator/dsl/builder.cr +++ /dev/null @@ -1,120 +0,0 @@ -require "../example_group_hook" -require "../example_hook" -require "../example_procsy_hook" -require "../spec_builder" - -module Spectator::DSL - # Incrementally builds up a test spec from the DSL. - # This is intended to be used only by the Spectator DSL. - module Builder - extend self - - # Underlying spec builder. - private class_getter(builder) { SpecBuilder.new(Spectator.config) } - - # Defines a new example group and pushes it onto the group stack. - # Examples and groups defined after calling this method will be nested under the new group. - # The group will be finished and popped off the stack when `#end_example` is called. - # - # See `Spec::Builder#start_group` for usage details. - def start_group(*args) - builder.start_group(*args) - end - - # Defines a new iterative example group and pushes it onto the group stack. - # Examples and groups defined after calling this method will be nested under the new group. - # The group will be finished and popped off the stack when `#end_example` is called. - # - # See `Spec::Builder#start_iterative_group` for usage details. - def start_iterative_group(*args) - builder.start_iterative_group(*args) - end - - # Completes a previously defined example group and pops it off the group stack. - # Be sure to call `#start_group` and `#end_group` symmetrically. - # - # See `Spec::Builder#end_group` for usage details. - def end_group(*args) - builder.end_group(*args) - end - - # Defines a new example. - # The example is added to the group currently on the top of the stack. - # - # See `Spec::Builder#add_example` for usage details. - def add_example(*args, &block : Example ->) - builder.add_example(*args, &block) - end - - # Defines a new pending example. - # The example is added to the group currently on the top of the stack. - # - # See `Spec::Builder#add_pending_example` for usage details. - def add_pending_example(*args) - builder.add_pending_example(*args) - end - - # Defines a block of code to execute before any and all examples in the test suite. - def before_suite(location = nil, label = "before_suite", &block) - hook = ExampleGroupHook.new(location: location, label: label, &block) - builder.before_suite(hook) - end - - # Defines a block of code to execute before any and all examples in the current group. - def before_all(location = nil, label = "before_all", &block) - hook = ExampleGroupHook.new(location: location, label: label, &block) - builder.before_all(hook) - end - - # Defines a block of code to execute before every example in the current group - def before_each(location = nil, label = "before_each", &block : Example -> _) - hook = ExampleHook.new(location: location, label: label, &block) - builder.before_each(hook) - end - - # Defines a block of code to execute after any and all examples in the test suite. - def after_suite(location = nil, label = "after_suite", &block) - hook = ExampleGroupHook.new(location: location, label: label, &block) - builder.after_suite(hook) - end - - # Defines a block of code to execute after any and all examples in the current group. - def after_all(location = nil, label = "after_all", &block) - hook = ExampleGroupHook.new(location: location, label: label, &block) - builder.after_all(hook) - end - - # Defines a block of code to execute after every example in the current group. - def after_each(location = nil, label = "after_each", &block : Example ->) - hook = ExampleHook.new(location: location, label: label, &block) - builder.after_each(hook) - end - - # Defines a block of code to execute around every example in the current group. - def around_each(location = nil, label = "around_each", &block : Example::Procsy ->) - hook = ExampleProcsyHook.new(location: location, label: label, &block) - builder.around_each(hook) - end - - # Defines a block of code to execute before every example in the current group - def pre_condition(location = nil, label = "pre_condition", &block : Example -> _) - hook = ExampleHook.new(location: location, label: label, &block) - builder.pre_condition(hook) - end - - # Defines a block of code to execute after every example in the current group. - def post_condition(location = nil, label = "post_condition", &block : Example ->) - hook = ExampleHook.new(location: location, label: label, &block) - builder.post_condition(hook) - end - - # Constructs the test spec. - # Returns the spec instance. - # - # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. - # This would indicate a logical error somewhere in Spectator or an extension of it. - def build : Spec - builder.build - end - end -end diff --git a/src/spectator/dsl/concise.cr b/src/spectator/dsl/concise.cr deleted file mode 100644 index bb65a42..0000000 --- a/src/spectator/dsl/concise.cr +++ /dev/null @@ -1,56 +0,0 @@ -require "./examples" -require "./groups" -require "./memoize" - -module Spectator::DSL - # DSL methods and macros for shorter syntax. - module Concise - # Defines an example and input values in a shorter syntax. - # The only arguments given to this macro are one or more assignments. - # The names in the assignments will be available in the example code. - # - # If the code block is omitted, then the example is skipped (marked as not implemented). - # - # Tags and metadata cannot be used with this macro. - # - # ``` - # provided x = 42, y: 123 do - # expect(x).to eq(42) - # expect(y).to eq(123) - # end - # ``` - macro provided(*assignments, it description = nil, **kwargs, &block) - {% raise "Cannot use 'provided' inside of a test block" if @def %} - - class Given%given < {{@type.id}} - {% for assignment in assignments %} - let({{assignment.target}}) { {{assignment.value}} } - {% end %} - {% for name, value in kwargs %} - let({{name}}) { {{value}} } - {% end %} - - {% if block %} - {% if description %} - example {{description}} {{block}} - {% else %} - example {{block}} - {% end %} - {% else %} - {% if description %} - example {{description}} {{block}} - {% else %} - example {{assignments.splat.stringify}} - {% end %} - {% end %} - end - end - - # :ditto: - @[Deprecated("Use `provided` instead.")] - macro given(*assignments, **kwargs, &block) - {% raise "Cannot use 'given' inside of a test block" if @def %} - provided({{assignments.splat(",")}} {{kwargs.double_splat}}) {{block}} - end - end -end diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr deleted file mode 100644 index 703f2d3..0000000 --- a/src/spectator/dsl/examples.cr +++ /dev/null @@ -1,153 +0,0 @@ -require "../context" -require "../location" -require "./builder" -require "./metadata" - -module Spectator::DSL - # DSL methods for defining examples and test code. - module Examples - include Metadata - - # Defines a macro to generate code for an example. - # The *name* is the name given to the macro. - # - # In addition, another macro is defined that marks the example as pending. - # The pending macro is prefixed with 'x'. - # For instance, `define_example :it` defines `it` and `xit`. - # - # Default tags can be provided with *tags* and *metadata*. - # The tags are merged with parent groups. - # Any items with falsey values from *metadata* remove the corresponding tag. - macro define_example(name, *tags, **metadata) - # Defines an example. - # - # If a block is given, it is treated as the code to test. - # The block is provided the current example instance as an argument. - # - # The first argument names the example (test). - # Typically, this specifies what is being tested. - # It has no effect on the test and is purely used for output. - # If omitted, a name is generated from the first assertion in the test. - # - # The example will be marked as pending if the block is omitted. - # A block or name must be provided. - # - # Tags can be specified by adding symbols (keywords) after the first argument. - # Key-value pairs can also be specified. - # Any falsey items will remove a previously defined tag. - macro {{name.id}}(what = nil, *tags, **metadata, &block) - \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} - \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} - - _spectator_metadata(%metadata, :metadata, {{tags.splat(",")}} {{metadata.double_splat}}) - _spectator_metadata(\%metadata, %metadata, \{{tags.splat(",")}} \{{metadata.double_splat}}) - - \{% if block %} - \{% raise "Block argument count '{{name.id}}' must be 0..1" if block.args.size > 1 %} - - private def \%test(\{{block.args.splat}}) : Nil - \{{block.body}} - end - - ::Spectator::DSL::Builder.add_example( - _spectator_example_name(\{{what}}), - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), - -> { new.as(::Spectator::Context) }, - \%metadata - ) do |example| - example.with_context(\{{@type.name}}) do - \{% if block.args.empty? %} - \%test - \{% else %} - \%test(example) - \{% end %} - end - end - - \{% else %} - ::Spectator::DSL::Builder.add_pending_example( - _spectator_example_name(\{{what}}), - ::Spectator::Location.new(\{{what.filename}}, \{{what.line_number}}), - \%metadata, - "Not yet implemented" - ) - \{% end %} - end - - define_pending_example :x{{name.id}}, skip: "Temporarily skipped with x{{name.id}}" - end - - # Defines a macro to generate code for a pending example. - # The *name* is the name given to the macro. - # - # The block for the example's content is discarded at compilation time. - # This prevents issues with undefined methods, signature differences, etc. - # - # Default tags can be provided with *tags* and *metadata*. - # The tags are merged with parent groups. - # Any items with falsey values from *metadata* remove the corresponding tag. - macro define_pending_example(name, *tags, **metadata) - # Defines a pending example. - # - # If a block is given, it is treated as the code to test. - # The block is provided the current example instance as an argument. - # - # The first argument names the example (test). - # Typically, this specifies what is being tested. - # It has no effect on the test and is purely used for output. - # If omitted, a name is generated from the first assertion in the test. - # - # Tags can be specified by adding symbols (keywords) after the first argument. - # Key-value pairs can also be specified. - # Any falsey items will remove a previously defined tag. - macro {{name.id}}(what = nil, *tags, **metadata, &block) - \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} - \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} - \{% raise "Block argument count '{{name.id}}' must be 0..1" if block && block.args.size > 1 %} - - _spectator_metadata(%metadata, :metadata, {{tags.splat(",")}} {{metadata.double_splat}}) - _spectator_metadata(\%metadata, %metadata, \{{tags.splat(",")}} \{{metadata.double_splat}}) - - ::Spectator::DSL::Builder.add_pending_example( - _spectator_example_name(\{{what}}), - ::Spectator::Location.new(\{{(what || block).filename}}, \{{(what || block).line_number}}, \{{(what || block).end_line_number}}), - \%metadata, - \{% if !block %}"Not yet implemented"\{% end %} - ) - end - end - - # Inserts the correct representation of a example's name. - # If *what* is a string, then it is dropped in as-is. - # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `Node#name`. - private macro _spectator_example_name(what) - {% if what.is_a?(StringLiteral) || what.is_a?(NilLiteral) %} - {{what}} - {% elsif what.is_a?(StringInterpolation) %} - ->(example : ::Spectator::Example) do - example.with_context(\{{@type.name}}) { {{what}} } - end - {% else %} - {{what.stringify}} - {% end %} - end - - define_example :example - - define_example :it - - define_example :specify - - define_example :fexample, focus: true - - define_example :fit, focus: true - - define_example :fspecify, focus: true - - @[Deprecated("Behavior of pending blocks will change in Spectator v0.11.0. Use `skip` instead.")] - define_pending_example :pending - - define_pending_example :skip - end -end diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr deleted file mode 100644 index dba2e9b..0000000 --- a/src/spectator/dsl/expectations.cr +++ /dev/null @@ -1,191 +0,0 @@ -require "../block" -require "../example_pending" -require "../expectation" -require "../expectation_failed" -require "../location" -require "../pending_result" -require "../value" - -module Spectator::DSL - # Methods and macros for asserting that conditions are met. - module Expectations - # Immediately fail the current test. - # A reason can be specified with *message*. - def fail(message = "Example failed", *, _file = __FILE__, _line = __LINE__) - raise ExampleFailed.new(Location.new(_file, _line), message) - end - - # Mark the current test as pending and immediately abort. - # A reason can be specified with *message*. - def pending(message = PendingResult::DEFAULT_REASON, *, _file = __FILE__, _line = __LINE__) - raise ExamplePending.new(Location.new(_file, _line), message) - end - - # Mark the current test as skipped and immediately abort. - # A reason can be specified with *message*. - def skip(message = PendingResult::DEFAULT_REASON, *, _file = __FILE__, _line = __LINE__) - raise ExamplePending.new(Location.new(_file, _line), message) - end - - # Starts an expectation. - # This should be followed up with `Assertion::Target#to` or `Assertion::Target#to_not`. - # The value passed in will be checked to see if it satisfies the conditions of the specified matcher. - # - # This macro should be used like so: - # ``` - # expect(actual).to eq(expected) - # ``` - # - # Where the actual value is returned by the system under test, - # and the expected value is what the actual value should be to satisfy the condition. - macro expect(actual) - {% raise "Cannot use 'expect' outside of a test block" unless @def %} - - %actual = begin - {{actual}} - end - - %expression = ::Spectator::Value.new(%actual, {{actual.stringify}}) - %location = ::Spectator::Location.new({{actual.filename}}, {{actual.line_number}}) - ::Spectator::Expectation::Target.new(%expression, %location) - end - - # Starts an expectation. - # This should be followed up with `Assertion::Target#to` or `Assertion::Target#not_to`. - # The value passed in will be checked to see if it satisfies the conditions of the specified matcher. - # - # This macro should be used like so: - # ``` - # expect { raise "foo" }.to raise_error - # ``` - # - # The block of code is passed along for validation to the matchers. - # - # The short, one argument syntax used for passing methods to blocks can be used. - # So instead of doing this: - # ``` - # expect(subject.size).to eq(5) - # ``` - # - # The following syntax can be used instead: - # ``` - # expect(&.size).to eq(5) - # ``` - # - # The method passed will always be evaluated on the subject. - # - # TECHNICAL NOTE: - # This macro uses an ugly hack to detect the short-hand syntax. - # - # The Crystal compiler will translate: - # ``` - # &.foo - # ``` - # - # effectively to: - # ``` - # { |__arg0| __arg0.foo } - # ``` - macro expect(&block) - {% raise "Cannot use 'expect' outside of a test block" unless @def %} - - {% if block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %} - {% method_name = block.body.id.split('.')[1..-1].join('.') %} - %block = ::Spectator::Block.new({{"#" + method_name}}) do - subject.{{method_name.id}} - end - {% elsif block.args.empty? %} - %block = ::Spectator::Block.new({{"`" + block.body.stringify + "`"}}) {{block}} - {% else %} - {% raise "Unexpected block arguments in 'expect' call" %} - {% end %} - - %location = ::Spectator::Location.new({{block.filename}}, {{block.line_number}}) - ::Spectator::Expectation::Target.new(%block, %location) - end - - # Short-hand for expecting something of the subject. - # - # These two are functionally equivalent: - # ``` - # expect(subject).to eq("foo") - # is_expected.to eq("foo") - # ``` - macro is_expected - {% raise "Cannot use 'is_expected' outside of a test block" unless @def %} - - expect(subject) - end - - # Short-hand form of `#is_expected` that can be used for one-liner syntax. - # - # For instance: - # ``` - # it "is 42" do - # expect(subject).to eq(42) - # end - # ``` - # - # Can be shortened to: - # ``` - # it { is(42) } - # ``` - # - # These three are functionally equivalent: - # ``` - # expect(subject).to eq("foo") - # is_expected.to eq("foo") - # is("foo") - # ``` - # - # See also: `#is_not` - macro is(expected) - {% raise "Cannot use 'is' outside of a test block" unless @def %} - - expect(subject).to(eq({{expected}})) - end - - # Short-hand, negated form of `#is_expected` that can be used for one-liner syntax. - # - # For instance: - # ``` - # it "is not 42" do - # expect(subject).to_not eq(42) - # end - # ``` - # - # Can be shortened to: - # ``` - # it { is_not(42) } - # ``` - # - # These three are functionally equivalent: - # ``` - # expect(subject).not_to eq("foo") - # is_expected.not_to eq("foo") - # is_not("foo") - # ``` - # - # See also: `#is` - macro is_not(expected) - {% raise "Cannot use 'is_not' outside of a test block" unless @def %} - - expect(subject).not_to(eq({{expected}})) - end - - # Captures multiple possible failures. - # Aborts after the block completes if there were any failed expectations in the block. - # - # ``` - # aggregate_failures do - # expect(true).to be_false - # expect(false).to be_true - # end - # ``` - def aggregate_failures(label = nil, &) - ::Spectator::Harness.current.aggregate_failures(label) do - yield - end - end - end -end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr deleted file mode 100644 index da06906..0000000 --- a/src/spectator/dsl/groups.cr +++ /dev/null @@ -1,245 +0,0 @@ -require "../location" -require "./builder" -require "./memoize" -require "./metadata" - -module Spectator::DSL - # DSL methods and macros for creating example groups. - # This module should be included as a mix-in. - module Groups - include Metadata - - # Defines a macro to generate code for an example group. - # The *name* is the name given to the macro. - # - # Default tags can be provided with *tags* and *metadata*. - # The tags are merged with parent groups. - # Any items with falsey values from *metadata* remove the corresponding tag. - macro define_example_group(name, *tags, **metadata) - # Defines a new example group. - # The *what* argument is a name or description of the group. - # - # The first argument names the example (test). - # Typically, this specifies what is being tested. - # This argument is also used as the subject. - # When it is a type name, it becomes an explicit, which overrides any previous subjects. - # Otherwise it becomes an implicit subject, which doesn't override explicitly defined subjects. - # - # Tags can be specified by adding symbols (keywords) after the first argument. - # Key-value pairs can also be specified. - # Any falsey items will remove a previously defined tag. - # - # TODO: Handle string interpolation in example and group names. - macro {{name.id}}(what, *tags, **metadata, &block) - \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} - - class Group\%group < \{{@type.id}} - _spectator_group_subject(\{{what}}) - - _spectator_metadata(:metadata, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) - _spectator_metadata(:metadata, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) - - ::Spectator::DSL::Builder.start_group( - _spectator_group_name(\{{what}}), - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), - metadata - ) - - \{{block.body if block}} - - ::Spectator::DSL::Builder.end_group - end - end - end - - # Defines a macro to generate code for an iterative example group. - # The *name* is the name given to the macro. - # - # Default tags can be provided with *tags* and *metadata*. - # The tags are merged with parent groups. - # Any items with falsey values from *metadata* remove the corresponding tag. - # - # If provided, a block can be used to modify collection that will be iterated. - # It takes a single argument - the original collection from the user. - # The modified collection should be returned. - # - # TODO: Handle string interpolation in example and group names. - macro define_iterative_group(name, *tags, **metadata, &block) - macro {{name.id}}(collection, *tags, count = nil, **metadata, &block) - \{% raise "Cannot use 'sample' inside of a test block" if @def %} - - class Group\%group < \{{@type.id}} - _spectator_metadata(:metadata, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) - _spectator_metadata(:metadata, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) - - def self.\%collection - \{{collection}} - end - - {% if block %} - def self.%mutate({{block.args.splat}}) - {{block.body}} - end - - def self.\%collection - %mutate(previous_def) - end - {% end %} - - \{% if count %} - def self.\%collection - previous_def.first(\{{count}}) - end - \{% end %} - - ::Spectator::DSL::Builder.start_iterative_group( - \%collection, - \{{collection.stringify}}, - [\{{block.args.empty? ? "".id : block.args.map(&.stringify).splat}}] of String, - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), - metadata - ) - - \{% if block %} - \{% if block.args.size > 1 %} - \{% for arg, i in block.args %} - let(\{{arg}}) do |example| - example.group.as(::Spectator::ExampleGroupIteration(typeof(Group\%group.\%collection.first))).item[\{{i}}] - end - \{% end %} - \{% else %} - let(\{{block.args[0]}}) do |example| - example.group.as(::Spectator::ExampleGroupIteration(typeof(Group\%group.\%collection.first))).item - end - \{% end %} - - \{{block.body}} - \{% end %} - - ::Spectator::DSL::Builder.end_group - end - end - end - - # Inserts the correct representation of a group's name. - # If *what* appears to be a type name, it will be symbolized. - # If it's a string, then it is dropped in as-is. - # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `Node#name`. - private macro _spectator_group_name(what) - {% if (what.is_a?(Generic) || - what.is_a?(Path) || - what.is_a?(TypeNode) || - what.is_a?(Union)) && - what.resolve?.is_a?(TypeNode) %} - {{what.symbolize}} - {% elsif what.is_a?(StringLiteral) || - what.is_a?(NilLiteral) %} - {{what}} - {% elsif what.is_a?(StringInterpolation) %} - {{@type.name}}.new.eval do - {{what}} - rescue e - "" - end - {% else %} - {{what.stringify}} - {% end %} - end - - # Defines the implicit subject for the test context. - # If *what* is a type, then the `described_class` method will be defined. - # Additionally, the implicit subject is set to an instance of *what* if it's not a module. - # - # There is no common macro type that has the `#resolve?` method. - # Also, `#responds_to?` can't be used in macros. - # So the large if statement in this macro is used to look for type signatures. - private macro _spectator_group_subject(what) - {% if (what.is_a?(Generic) || - what.is_a?(Path) || - what.is_a?(TypeNode) || - what.is_a?(Union)) && - (described_type = what.resolve?).is_a?(TypeNode) %} - private macro described_class - {{what}} - end - - subject do - {% if described_type.class? || described_type.struct? %} - described_class.new - {% else %} - described_class - {% end %} - end - {% else %} - private def _spectator_implicit_subject - {{what}} - end - {% end %} - end - - define_example_group :example_group - - define_example_group :describe - - define_example_group :context - - define_example_group :xexample_group, skip: "Temporarily skipped with xexample_group" - - define_example_group :xdescribe, skip: "Temporarily skipped with xdescribe" - - define_example_group :xcontext, skip: "Temporarily skipped with xcontext" - - define_example_group :fexample_group, focus: true - - define_example_group :fdescribe, focus: true - - define_example_group :fcontext, focus: true - - # Defines a new iterative example group. - # This type of group duplicates its contents for each element in *collection*. - # - # The first argument is the collection of elements to iterate over. - # - # Tags can be specified by adding symbols (keywords) after the first argument. - # Key-value pairs can also be specified. - # Any falsey items will remove a previously defined tag. - # - # The number of items iterated can be restricted by specifying a *count* argument. - # The first *count* items will be used if specified, otherwise all items will be used. - define_iterative_group :sample - - # :ditto: - define_iterative_group :xsample, skip: "Temporarily skipped with xsample" - - # :ditto: - define_iterative_group :fsample, focus: true - - # Defines a new iterative example group. - # This type of group duplicates its contents for each element in *collection*. - # This is the same as `#sample` except that the items are shuffled. - # The items are selected with a RNG based on the seed. - # - # The first argument is the collection of elements to iterate over. - # - # Tags can be specified by adding symbols (keywords) after the first argument. - # Key-value pairs can also be specified. - # Any falsey items will remove a previously defined tag. - # - # The number of items iterated can be restricted by specifying a *count* argument. - # The first *count* items will be used if specified, otherwise all items will be used. - define_iterative_group :random_sample do |collection| - collection.to_a.shuffle(::Spectator.random) - end - - # :ditto: - define_iterative_group :xrandom_sample, skip: "Temporarily skipped with xrandom_sample" do |collection| - collection.to_a.shuffle(::Spectator.random) - end - - # :ditto: - define_iterative_group :frandom_sample, focus: true do |collection| - collection.to_a.shuffle(::Spectator.random) - end - end -end diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr deleted file mode 100644 index 6672e59..0000000 --- a/src/spectator/dsl/hooks.cr +++ /dev/null @@ -1,167 +0,0 @@ -require "../location" -require "./builder" - -module Spectator::DSL - # DSL methods for adding custom logic to key times of the spec execution. - module Hooks - # Defines a macro to create an example group hook. - # The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`. - # A custom *name* can be used for the hook method. - # If not provided, *type* will be used instead. - # Additionally, a block can be provided. - # The block can perform any operations necessary and yield to invoke the end-user hook. - macro define_example_group_hook(type, name = nil, &block) - macro {{(name ||= type).id}}(&block) - \{% raise "Missing block for '{{name.id}}' hook" unless block %} - \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} - - private def self.\%hook : Nil - \{{block.body}} - end - - {% if block %} - private def self.%wrapper : Nil - {{block.body}} - end - {% end %} - - ::Spectator::DSL::Builder.{{type.id}}( - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}) - ) do - {% if block %} - %wrapper do |*args| - \{% if block.args.empty? %} - \%hook - \{% else %} - \%hook(*args) - \{% end %} - end - {% else %} - \%hook - {% end %} - end - end - end - - # Defines a macro to create an example hook. - # The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`. - # A custom *name* can be used for the hook method. - # If not provided, *type* will be used instead. - # Additionally, a block can be provided that takes the current example as an argument. - # The block can perform any operations necessary and yield to invoke the end-user hook. - macro define_example_hook(type, name = nil, &block) - macro {{(name ||= type).id}}(&block) - \{% raise "Missing block for '{{name.id}}' hook" unless block %} - \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} - \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} - - private def \%hook(\{{block.args.splat}}) : Nil - \{{block.body}} - end - - {% if block %} - private def %wrapper({{block.args.splat}}) : Nil - {{block.body}} - end - {% end %} - - ::Spectator::DSL::Builder.{{type.id}}( - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}) - ) do |example| - example.with_context(\{{@type.name}}) do - {% if block %} - {% if block.args.empty? %} - %wrapper do |*args| - \{% if block.args.empty? %} - \%hook - \{% else %} - \%hook(*args) - \{% end %} - end - {% else %} - %wrapper(example) do |*args| - \{% if block.args.empty? %} - \%hook - \{% else %} - \%hook(*args) - \{% end %} - end - {% end %} - {% else %} - \{% if block.args.empty? %} - \%hook - \{% else %} - \%hook(example) - \{% end %} - {% end %} - end - end - end - end - - # Defines a block of code that will be invoked once before any examples in the suite. - # The block will not run in the context of the current running example. - # This means that values defined by `let` and `subject` are not available. - define_example_group_hook :before_suite - - # Defines a block of code that will be invoked once after all examples in the suite. - # The block will not run in the context of the current running example. - # This means that values defined by `let` and `subject` are not available. - define_example_group_hook :after_suite - - # Defines a block of code that will be invoked once before any examples in the group. - # The block will not run in the context of the current running example. - # This means that values defined by `let` and `subject` are not available. - define_example_group_hook :before_all - - # Defines a block of code that will be invoked once after all examples in the group. - # The block will not run in the context of the current running example. - # This means that values defined by `let` and `subject` are not available. - define_example_group_hook :after_all - - # Defines a block of code that will be invoked before every example in the group. - # The block will be run in the context of the current running example. - # This means that values defined by `let` and `subject` are available. - define_example_hook :before_each - - # :ditto: - macro before(&block) - before_each {{block}} - end - - # Defines a block of code that will be invoked after every example in the group. - # The block will be run in the context of the current running example. - # This means that values defined by `let` and `subject` are available. - define_example_hook :after_each - - # :ditto: - macro after(&block) - after_each {{block}} - end - - # Defines a block of code that will be invoked around every example in the group. - # The block will be run in the context of the current running example. - # This means that values defined by `let` and `subject` are available. - # - # The block will execute before the example. - # An `Example::Procsy` is passed to the block. - # The `Example::Procsy#run` method should be called to ensure the example runs. - # More code can run afterwards (in the block). - define_example_hook :around_each - - # :ditto: - macro around(&block) - around_each {{block}} - end - - # Defines a block of code that will be invoked before every example in the group. - # The block will be run in the context of the current running example. - # This means that values defined by `let` and `subject` are available. - define_example_hook :pre_condition - - # Defines a block of code that will be invoked after every example in the group. - # The block will be run in the context of the current running example. - # This means that values defined by `let` and `subject` are available. - define_example_hook :post_condition - end -end diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr deleted file mode 100644 index 95e6c5d..0000000 --- a/src/spectator/dsl/matchers.cr +++ /dev/null @@ -1,879 +0,0 @@ -require "../block" -require "../matchers" -require "../value" - -module Spectator::DSL - module Matchers - # Indicates that some value should equal another. - # The == operator is used for this check. - # The value passed to this method is the expected value. - # - # Example: - # ``` - # expect(1 + 2).to eq(3) - # ``` - macro eq(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::EqualityMatcher.new(%value) - end - - # Indicates that some value should not equal another. - # The != operator is used for this check. - # The value passed to this method is the unexpected value. - # - # Example: - # ``` - # expect(1 + 2).to ne(5) - # ``` - macro ne(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::InequalityMatcher.new(%value) - end - - # Indicates that some value when compared to another satisfies an operator. - # An operator can follow, such as: <, <=, >, or >=. - # See `Spectator::Matchers::TruthyMatcher` for a full list of operators. - # - # Examples: - # ``` - # expect(1 + 1).to be > 1 - # expect(5).to be >= 3 - # ``` - # - # Additionally, a value can just "be" truthy by omitting an operator. - # ``` - # expect("foo").to be - # # is the same as: - # expect("foo").to be_truthy - # ``` - macro be - ::Spectator::Matchers::TruthyMatcher.new - end - - # Indicates that some object should be the same as another. - # This checks if two references are the same. - # The `Reference#same?` method is used for this check. - # - # Examples: - # ``` - # obj = "foobar" - # expect(obj).to be(obj) - # expect(obj.dup).to_not be(obj) - # ``` - macro be(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::ReferenceMatcher.new(%value) - end - - # Indicates that some value should be of a specified type. - # The `Object#is_a?` method is used for this check. - # A type name or type union should be used for *expected*. - # - # Examples: - # ``` - # expect("foo").to be_a(String) - # - # x = Random.rand(2) == 0 ? "foobar" : 5 - # expect(x).to be_a(Int32 | String) - # ``` - macro be_a(expected) - ::Spectator::Matchers::TypeMatcher.create({{expected}}) - end - - # Indicates that some value should be of a specified type. - # The `Object#is_a?` method is used for this check. - # A type name or type union should be used for *expected*. - # This method is identical to `#be_a`, - # and exists just to improve grammar. - # - # Examples: - # ``` - # expect(123).to be_an(Int32) - # ``` - macro be_an(expected) - be_a({{expected}}) - end - - # Indicates that some value should be of a specified type. - # The `Object#is_a?` method is used for this check. - # A type name or type union should be used for *expected*. - # This method is identical to `#be_a`, - # and exists just to improve grammar. - # - # Examples: - # ``` - # expect(123).to be_kind_of(Int) - # ``` - macro be_kind_of(expected) - be_a({{expected}}) - end - - # Indicates that some value should be of a specified type. - # The `Object#is_a?` method is used for this check. - # A type name or type union should be used for *expected*. - # This method is identical to `#be_a`, - # and exists just to improve grammar. - # - # Examples: - # ``` - # expect(123).to be_a_kind_of(Int) - # ``` - macro be_a_kind_of(expected) - be_a({{expected}}) - end - - # Indicates that some value should be of a specified type. - # The value's runtime type is checked. - # A type name or type union should be used for *expected*. - # - # Examples: - # ``` - # expect(123).to be_instance_of(Int32) - # ``` - macro be_instance_of(expected) - ::Spectator::Matchers::InstanceMatcher({{expected}}).new - end - - # Indicates that some value should be of a specified type. - # The value's runtime type is checked. - # A type name or type union should be used for *expected*. - # This method is identical to `#be_an_instance_of`, - # and exists just to improve grammar. - # - # Examples: - # ``` - # expect(123).to be_an_instance_of(Int32) - # ``` - macro be_an_instance_of(expected) - be_instance_of({{expected}}) - end - - # Indicates that some value should be of a specified type at compile time. - # The value's compile time type is checked. - # This can test is a variable or value returned by a method is inferred to the expected type. - # - # Examples: - # ``` - # value = 42 || "foobar" - # expect(value).to compile_as(Int32 | String) - # ``` - macro compile_as(expected) - ::Spectator::Matchers::CompiledTypeMatcher({{expected}}).new - end - - # Indicates that some value should respond to a method call. - # One or more method names can be provided. - # - # Examples: - # ``` - # expect("foobar").to respond_to(:downcase) - # expect(%i[a b c]).to respond_to(:size, :first) - # ``` - macro respond_to(*expected) - ::Spectator::Matchers::RespondMatcher({% begin %}NamedTuple( - {% for method in expected %} - {{method.id.stringify}}: Nil, - {% end %} - ){% end %}).new - end - - # Indicates that some value should be less than another. - # The < operator is used for this check. - # The value passed to this method is the value expected to be larger. - # - # Example: - # ``` - # expect(3 - 1).to be_lt(3) - # ``` - macro be_lt(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::LessThanMatcher.new(%value) - end - - # Indicates that some value should be less than or equal to another. - # The <= operator is used for this check. - # The value passed to this method is the value expected to be larger or equal. - # - # Example: - # ``` - # expect(3 - 1).to be_le(3) - # ``` - macro be_le(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::LessThanEqualMatcher.new(%value) - end - - # Indicates that some value should be greater than another. - # The > operator is used for this check. - # The value passed to this method is the value expected to be smaller. - # - # Example: - # ``` - # expect(3 + 1).to be_gt(3) - # ``` - macro be_gt(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::GreaterThanMatcher.new(%value) - end - - # Indicates that some value should be greater than or equal to another. - # The >= operator is used for this check. - # The value passed to this method is the value expected to be smaller or equal. - # - # Example: - # ``` - # expect(3 + 1).to be_ge(3) - # ``` - macro be_ge(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::GreaterThanEqualMatcher.new(%value) - end - - # Indicates that some value should match another. - # The === (case equality) operator is used for this check. - # Typically a regular expression is used. - # This has identical behavior as a "when" condition in a case block. - # - # Examples: - # ``` - # expect("foo").to match(/foo|bar/) - # expect("BAR").to match(/foo|bar/i) - # expect(1 + 2).to match(3) - # expect(5).to match(Int32) # Using `#be_a` instead is recommended here. - # expect({:foo, 5}).to match({Symbol, Int32}) - # ``` - macro match(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::CaseMatcher.new(%value) - end - - # Indicates that some value should be true. - # - # Examples: - # ``` - # expect(nil.nil?).to be_true - # expect(%i[a b c].any?).to be_true - # ``` - macro be_true - eq(true) - end - - # Indicates that some value should be false. - # - # Examples: - # ``` - # expect("foo".nil?).to be_false - # expect(%i[a b c].empty?).to be_false - # ``` - macro be_false - eq(false) - end - - # Indicates that some value should be truthy. - # This means that the value is not false and not nil. - # - # Examples: - # ``` - # expect(123).to be_truthy - # expect(true).to be_truthy - # ``` - macro be_truthy - ::Spectator::Matchers::TruthyMatcher.new - end - - # Indicates that some value should be falsey. - # This means that the value is either false or nil. - # - # Examples: - # ``` - # expect(false).to be_falsey - # expect(nil).to be_falsey - # ``` - macro be_falsey - ::Spectator::Matchers::TruthyMatcher.new(false) - end - - # Indicates that some value should be contained within another. - # This checker can be used in one of two ways. - # - # The first: the *expected* argument can be anything - # that implements the `includes?` method. - # This is typically a `Range`, but can also be `Enumerable`. - # - # Examples: - # ``` - # expect(:foo).to be_within(%i[foo bar baz]) - # expect(7).to be_within(1..10) - # ``` - # - # The other way is to use this is with the "of" keyword. - # This creates a lower and upper bound - # centered around the value of the *expected* argument. - # This usage is helpful for comparisons on floating-point numbers. - # - # Examples: - # ``` - # expect(50.0).to be_within(0.01).of(50.0) - # expect(speed).to be_within(5).of(speed_limit) - # ``` - # - # NOTE: The of suffix must be used - # if the *expected* argument does not implement an `includes?` method. - # - # Additionally, for this second usage, - # an "inclusive" or "exclusive" suffix can be added. - # These modify the upper-bound on the range being checked against. - # By default, the range is inclusive. - # - # Examples: - # ``` - # expect(days).to be_within(1).of(30).inclusive # 29, 30, or 31 - # expect(100).to be_within(2).of(99).exclusive # 97, 98, 99, or 100 (not 101) - # ``` - # - # NOTE: Do not attempt to mix the two use cases. - # It likely won't work and will result in a compilation error. - macro be_within(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::CollectionMatcher.new(%value) - end - - # Indicates that some value should be between a lower and upper-bound. - # - # Example: - # ``` - # expect(7).to be_between(1, 10) - # ``` - # - # Additionally, an "inclusive" or "exclusive" suffix can be added. - # These modify the upper-bound on the range being checked against. - # By default, the range is inclusive. - # - # Examples: - # ``` - # expect(days).to be_between(28, 31).inclusive # 28, 29, 30, or 31 - # expect(100).to be_between(97, 101).exclusive # 97, 98, 99, or 100 (not 101) - # ``` - macro be_between(min, max) - %range = Range.new({{min}}, {{max}}) - %label = [{{min.stringify}}, {{max.stringify}}].join(" to ") - %value = ::Spectator::Value.new(%range, %label) - ::Spectator::Matchers::RangeMatcher.new(%value) - end - - # Indicates that some value should be within a delta of an expected value. - # - # Example: - # ``` - # expect(pi).to be_close(3.14159265359, 0.0000001) - # ``` - # - # This is functionally equivalent to: - # ``` - # be_within(expected).of(delta) - # ``` - macro be_close(expected, delta) - be_within({{delta}}).of({{expected}}) - end - - # Indicates that some value should or should not be nil. - # - # Examples: - # ``` - # expect(error).to be_nil - # expect(input).to_not be_nil - # ``` - macro be_nil - ::Spectator::Matchers::NilMatcher.new - end - - # Indicates that some collection should be empty. - # - # Example: - # ``` - # expect([]).to be_empty - # ``` - macro be_empty - ::Spectator::Matchers::EmptyMatcher.new - end - - # Indicates that some value or set should start with another value. - # This is typically used on a `String` or `Array` (any `Enumerable` works). - # The *expected* argument can be a `String`, `Char`, or `Regex` - # when the actual type (being compared against) is a `String`. - # For `Enumerable` types, only the first item is inspected. - # It is compared with the === operator, - # so that values, types, regular expressions, and others can be tested. - # - # Examples: - # ``` - # expect("foobar").to start_with("foo") - # expect("foobar").to start_with('f') - # expect("FOOBAR").to start_with(/foo/i) - # - # expect(%i[a b c]).to start_with(:a) - # expect(%i[a b c]).to start_with(Symbol) - # expect(%w[foo bar]).to start_with(/foo/) - # ``` - macro start_with(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::StartWithMatcher.new(%value) - end - - # Indicates that some value or set should end with another value. - # This is typically used on a `String` or `Array` (any `Indexable` works). - # The *expected* argument can be a `String`, `Char`, or `Regex` - # when the actual type (being compared against) is a `String`. - # For `Indexable` types, only the last item is inspected. - # It is compared with the === operator, - # so that values, types, regular expressions, and others can be tested. - # - # Examples: - # ``` - # expect("foobar").to end_with("bar") - # expect("foobar").to end_with('r') - # expect("FOOBAR").to end_with(/bar/i) - # - # expect(%i[a b c]).to end_with(:c) - # expect(%i[a b c]).to end_with(Symbol) - # expect(%w[foo bar]).to end_with(/bar/) - # ``` - macro end_with(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::EndWithMatcher.new(%value) - end - - # Indicates that some value or set should contain another value. - # This is typically used on a `String` or `Array` (any `Enumerable` works). - # The *expected* argument can be a `String` or `Char` - # when the actual type (being compared against) is a `String`. - # For `Enumerable` types, items are compared using the underlying implementation. - # In both cases, the `includes?` method is used. - # - # Examples: - # ``` - # expect("foobar").to contain("foo") - # expect("foobar").to contain('o') - # expect(%i[a b c]).to contain(:b) - # ``` - # - # Additionally, multiple arguments can be specified. - # ``` - # expect("foobarbaz").to contain("foo", "bar") - # expect(%i[a b c]).to contain(:a, :b) - # ``` - macro contain(*expected) - {% if expected.id.starts_with?("{*") %} - %value = ::Spectator::Value.new({{expected.id[2...-1]}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%value) - {% else %} - %value = ::Spectator::Value.new({{expected}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%value) - {% end %} - end - - # Indicates that some value or set should contain specific items. - # This is typically used on a `String` or `Array` (any `Enumerable` works). - # The *expected* argument can be a `String` or `Char` - # when the actual type (being compared against) is a `String`. - # For `Enumerable` types, items are compared using the underlying implementation. - # In both cases, the `includes?` method is used. - # - # This is identical to `#contain`, but accepts an array (or enumerable type) instead of multiple arguments. - # - # Examples: - # ``` - # expect("foobar").to contain_elements(["foo", "bar"]) - # expect("foobar").to contain_elements(['a', 'b']) - # expect(%i[a b c]).to contain_elements(%i[a b]) - # ``` - macro contain_elements(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%value) - end - - # Indicates that some range (or collection) should contain another value. - # This is typically used on a `Range` (although any `Enumerable` works). - # The `includes?` method is used. - # - # Examples: - # ``` - # expect(1..10).to contain(5) - # expect((1..)).to contain(100) - # expect(..100).to contain(50) - # ``` - # - # Additionally, multiple arguments can be specified. - # ``` - # expect(1..10).to contain(2, 3) - # expect(..100).to contain(0, 50) - # ``` - macro cover(*expected) - {% if expected.id.starts_with?("{*") %} - %value = ::Spectator::Value.new({{expected.id[2...-1]}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%value) - {% else %} - %value = ::Spectator::Value.new({{expected}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%value) - {% end %} - end - - # Indicates that some value or set should contain another value. - # This is similar to `#contain`, but uses a different method for matching. - # Typically a `String` or `Array` (any `Enumerable` works) is checked against. - # The *expected* argument can be a `String` or `Char` - # when the actual type (being compared against) is a `String`. - # The `includes?` method is used for this case. - # For `Enumerable` types, each item is inspected until one matches. - # The === operator is used for this case, which allows for equality, type, regex, and other matches. - # - # Examples: - # ``` - # expect("foobar").to have("foo") - # expect("foobar").to have('o') - # - # expect(%i[a b c]).to have(:b) - # expect(%w[FOO BAR BAZ]).to have(/bar/i) - # expect([1, 2, 3, :a, :b, :c]).to have(Int32) - # ``` - # - # Additionally, multiple arguments can be specified. - # ``` - # expect("foobarbaz").to have("foo", "bar") - # expect(%i[a b c]).to have(:a, :b) - # expect(%w[FOO BAR BAZ]).to have(/foo/i, String) - # ``` - macro have(*expected) - {% if expected.id.starts_with?("{*") %} - %value = ::Spectator::Value.new({{expected.id[2...-1]}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::HaveMatcher.new(%value) - {% else %} - %value = ::Spectator::Value.new({{expected}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::HaveMatcher.new(%value) - {% end %} - end - - # Indicates that some value or set should contain specific items. - # This is similar to `#contain_elements`, but uses a different method for matching. - # Typically a `String` or `Array` (any `Enumerable` works) is checked against. - # The *expected* argument can be a `String` or `Char` - # when the actual type (being compared against) is a `String`. - # The `includes?` method is used for this case. - # For `Enumerable` types, each item is inspected until one matches. - # The === operator is used for this case, which allows for equality, type, regex, and other matches. - # - # Examples: - # ``` - # expect("foobar").to have_elements(["foo", "bar"]) - # expect("foobar").to have_elements(['a', 'b']) - # - # expect(%i[a b c]).to have_elements(%i[b c]) - # expect(%w[FOO BAR BAZ]).to have_elements([/FOO/, /bar/i]) - # expect([1, 2, 3, :a, :b, :c]).to have_elements([Int32, Symbol]) - # ``` - macro have_elements(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::HaveMatcher.new(%value) - end - - # Indicates that some set, such as a `Hash`, has a given key. - # The `has_key?` method is used for this check. - # - # Examples: - # ``` - # expect({foo: "bar"}).to have_key(:foo) - # expect({"lucky" => 7}).to have_key("lucky") - # ``` - macro have_key(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::HaveKeyMatcher.new(%value) - end - - # :ditto: - macro has_key(expected) - have_key({{expected}}) - end - - # Indicates that some set, such as a `Hash`, has a given value. - # The `has_value?` method is used for this check. - # - # Examples: - # ``` - # expect({foo: "bar"}).to have_value("bar") - # expect({"lucky" => 7}).to have_value(7) - # ``` - macro have_value(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::HaveValueMatcher.new(%value) - end - - # :ditto: - macro has_value(expected) - have_value({{expected}}) - end - - # Indicates that some set should contain some values in any order. - # - # Example: - # ``` - # expect([1, 2, 3]).to contain_exactly(3, 2, 1) - # ``` - macro contain_exactly(*expected) - {% if expected.id.starts_with?("{*") %} - %value = ::Spectator::Value.new(({{expected.id[2...-1]}}).to_a, {{expected.stringify}}) - ::Spectator::Matchers::ArrayMatcher.new(%value) - {% else %} - %value = ::Spectator::Value.new(({{expected}}).to_a, {{expected.stringify}}) - ::Spectator::Matchers::ArrayMatcher.new(%value) - {% end %} - end - - # Indicates that some set should contain the same values in any order as another set. - # This is the same as `#contain_exactly`, but takes an array as an argument. - # - # Example: - # ``` - # expect([1, 2, 3]).to match_array([3, 2, 1]) - # ``` - macro match_array(expected) - %value = ::Spectator::Value.new(({{expected}}).to_a, {{expected.stringify}}) - ::Spectator::Matchers::ArrayMatcher.new(%value) - end - - # Indicates that some set should have a specified size. - # - # Example: - # ``` - # expect([1, 2, 3]).to have_size(3) - # ``` - macro have_size(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::SizeMatcher.new(%value) - end - - # Indicates that some set should have the same size (number of elements) as another set. - # - # Example: - # ``` - # expect([1, 2, 3]).to have_size_of(%i[x y z]) - # ``` - macro have_size_of(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::SizeOfMatcher.new(%value) - end - - # Indicates that some value should have a set of attributes matching some conditions. - # A list of named arguments are expected. - # The names correspond to the attributes in the instance to check. - # The values are conditions to check with the === operator against the attribute's value. - # - # Examples: - # ``` - # expect("foobar").to have_attributes(size: 6, upcase: "FOOBAR") - # expect(%i[a b c]).to have_attributes(size: 1..5, first: Symbol) - # ``` - macro have_attributes(**expected) - {% if expected.id.starts_with?("{**") %} - %value = ::Spectator::Value.new({{expected.id[3...-1]}}, {{expected.double_splat.stringify}}) - ::Spectator::Matchers::AttributesMatcher.new(%value) - {% else %} - %value = ::Spectator::Value.new({{expected}}, {{expected.double_splat.stringify}}) - ::Spectator::Matchers::AttributesMatcher.new(%value) - {% end %} - 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: - # ``` - # i = 0 - # expect { i += 1 }.to change { i } - # expect { i += 0 }.to_not change { i } - # ``` - # - # ``` - # i = 0 - # expect { i += 5 }.to change { i }.from(0).to(5) - # ``` - # - # ``` - # i = 0 - # expect { i += 5 }.to change { i }.to(5) - # ``` - # - # ``` - # i = 0 - # expect { i += 5 }.to change { i }.from(0) - # ``` - # - # ``` - # i = 0 - # expect { i += 42 }.to change { i }.by(42) - # ``` - # - # The block short-hand syntax can be used here. - # It will reference the current subject. - # - # ``` - # expect { subject << :foo }.to change(&.size).by(1) - # ``` - macro change(&expression) - {% if expression.args.size == 1 && expression.args[0] =~ /^__arg\d+$/ && expression.body.is_a?(Call) && expression.body.id =~ /^__arg\d+\./ %} - {% method_name = expression.body.id.split('.')[1..-1].join('.') %} - %block = ::Spectator::Block.new({{"#" + method_name}}) do - subject.{{method_name.id}} - end - {% elsif expression.args.empty? %} - %block = ::Spectator::Block.new({{"`" + expression.body.stringify + "`"}}) {{expression}} - {% else %} - {% raise "Unexpected block arguments in 'expect' call" %} - {% end %} - - ::Spectator::Matchers::ChangeMatcher.new(%block) - end - - # Indicates that some block should raise an error. - # - # Examples: - # ``` - # expect { raise "foobar" }.to raise_error - # ``` - macro raise_error - ::Spectator::Matchers::ExceptionMatcher(Exception, Nil).new - end - - # Indicates that some block should raise an error with a given message or type. - # The *type_or_message* parameter should be an exception type - # or a string or regex to match the exception's message against. - # - # Examples: - # ``` - # hash = {"foo" => "bar"} - # expect { hash["baz"] }.to raise_error(KeyError) - # expect { hash["baz"] }.to raise_error(/baz/) - # expect { raise "foobar" }.to raise_error("foobar") - # ``` - macro raise_error(type_or_message) - ::Spectator::Matchers::ExceptionMatcher.create({{type_or_message}}, {{type_or_message.stringify}}) - end - - # Indicates that some block should raise an error with a given message and type. - # The *type* is the exception type expected to be raised. - # The *message* is a string or regex to match to exception message against. - # - # Examples: - # ``` - # hash = {"foo" => "bar"} - # expect { hash["baz"] }.to raise_error(KeyError, /baz/) - # expect { raise ArgumentError.new("foobar") }.to raise_error(ArgumentError, "foobar") - # ``` - macro raise_error(type, message) - ::Spectator::Matchers::ExceptionMatcher.create({{type}}, {{message}}, {{message.stringify}}) - end - - # Indicates that some block should raise an error. - # - # Examples: - # ``` - # expect_raises { raise "foobar" } - # ``` - macro expect_raises(&block) - expect {{block}}.to raise_error - end - - # Indicates that some block should raise an error with a given type. - # The *type* parameter should be an exception type. - # - # Examples: - # ``` - # hash = {"foo" => "bar"} - # expect_raises(KeyError) { hash["baz"] }.to raise_error(KeyError) - # ``` - macro expect_raises(type, &block) - expect {{block}}.to raise_error({{type}}) - end - - # Indicates that some block should raise an error with a given message and type. - # The *type* is the exception type expected to be raised. - # The *message* is a string or regex to match to exception message against. - # This method is included for compatibility with Crystal's default spec. - # - # Examples: - # ``` - # hash = {"foo" => "bar"} - # expect_raises(KeyError, /baz/) { hash["baz"] } - # expect_raises(ArgumentError, "foobar") { raise ArgumentError.new("foobar") } - # ``` - macro expect_raises(type, message, &block) - expect {{block}}.to raise_error({{type}}, {{message}}) - end - - # Indicates that a mock or double (stubbable type) should receive a message (have a method called). - # The *method* is the name of the method expected to be called. - # - # ``` - # expect(dbl).to have_received(:foo) - # ``` - macro have_received(method) - %value = ::Spectator::Value.new(({{method.id.symbolize}}), {{method.id.stringify}}) - ::Spectator::Matchers::ReceiveMatcher.new(%value) - end - - # Used to create predicate matchers. - # Any missing method that starts with 'be_' or 'have_' will be handled. - # All other method names will be ignored and raise a compile-time error. - # - # This can be used to simply check a predicate method that ends in '?'. - # For instance: - # ``` - # expect("foobar").to be_ascii_only - # # Is equivalent to: - # expect("foobar".ascii_only?).to be_true - # - # expect("foobar").to_not have_back_references - # # Is equivalent to: - # expect("foobar".has_back_references?).to_not be_true - # ``` - macro method_missing(call) - {% if call.name.starts_with?("be_") %} - # Remove `be_` prefix. - {% method_name = call.name[3..-1] %} - {% matcher = "PredicateMatcher" %} - {% elsif call.name.starts_with?("have_") %} - # Remove `have_` prefix. - {% method_name = call.name[5..-1] %} - {% matcher = "HavePredicateMatcher" %} - {% else %} - {% raise "Undefined local variable or method '#{call}'" %} - {% end %} - - descriptor = { {{method_name}}: ::Tuple.new({{call.args.splat}}) } - label = ::String::Builder.new({{method_name.stringify}}) - {% unless call.args.empty? %} - label << '(' - {% for arg, index in call.args %} - label << {{arg}} - {% if index < call.args.size - 1 %} - label << ", " - {% end %} - {% end %} - label << ')' - {% end %} - value = ::Spectator::Value.new(descriptor, label.to_s) - ::Spectator::Matchers::{{matcher.id}}.new(value) - end - end -end diff --git a/src/spectator/dsl/memoize.cr b/src/spectator/dsl/memoize.cr deleted file mode 100644 index 788a610..0000000 --- a/src/spectator/dsl/memoize.cr +++ /dev/null @@ -1,108 +0,0 @@ -require "../lazy_wrapper" -require "./reserved" - -module Spectator::DSL - # DSL methods for defining test values (subjects). - # These values are stored and reused throughout the test. - module Memoize - # Defines a memoized getter. - # The *name* is the name of the getter method. - # The block is evaluated only on the first time the getter is used - # and the return value is saved for subsequent calls. - macro let(name, &block) - {% raise "Missing block for 'let'" unless block %} - {% raise "Expected zero or one arguments for 'let', but got #{block.args.size}" if block.args.size > 1 %} - {% raise "Cannot use 'let' inside of an example block" if @def %} - {% raise "Cannot use '#{name.id}' for 'let'" if name.id.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} - - @%value = ::Spectator::LazyWrapper.new - - private def {{name.id}} - {% if block.args.size > 0 %} - {{block.args.first}} = ::Spectator::Example.current - {% end %} - @%value.get do - {{block.body}} - end - end - end - - # Defines a memoized getter. - # The *name* is the name of the getter method. - # The block is evaluated once before the example runs - # and the return value is saved. - macro let!(name, &block) - {% raise "Missing block for 'let!'" unless block %} - {% raise "Expected zero or one arguments for 'let!', but got #{block.args.size}" if block.args.size > 1 %} - {% raise "Cannot use 'let!' inside of an example block" if @def %} - {% raise "Cannot use '#{name.id}' for 'let!'" if name.id.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} - - let({{name}}) {{block}} - before_each { {{name.id}} } - end - - # Explicitly defines the subject of the tests. - # Creates a memoized getter for the subject. - # The block is evaluated only the first time the subject is referenced - # and the return value is saved for subsequent calls. - macro subject(&block) - {% raise "Missing block for 'subject'" unless block %} - {% raise "Expected zero or one arguments for 'let!', but got #{block.args.size}" if block.args.size > 1 %} - {% raise "Cannot use 'subject' inside of an example block" if @def %} - - let(subject) {{block}} - end - - # Explicitly defines the subject of the tests. - # Creates a memoized getter for the subject. - # The subject can be referenced by using `subject` or *name*. - # The block is evaluated only the first time the subject is referenced - # and the return value is saved for subsequent calls. - macro subject(name, &block) - {% raise "Missing block for 'subject'" unless block %} - {% raise "Expected zero or one arguments for 'subject', but got #{block.args.size}" if block.args.size > 1 %} - {% raise "Cannot use 'subject' inside of an example block" if @def %} - {% raise "Cannot use '#{name.id}' for 'subject'" if name.id.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} - - let({{name.id}}) {{block}} - - {% if name.id != :subject.id %} - private def subject - {{name.id}} - end - {% end %} - end - - # Explicitly defines the subject of the tests. - # Creates a memoized getter for the subject. - # The block is evaluated once before the example runs - # and the return value is saved for subsequent calls. - macro subject!(&block) - {% raise "Missing block for 'subject'" unless block %} - {% raise "Expected zero or one arguments for 'subject!', but got #{block.args.size}" if block.args.size > 1 %} - {% raise "Cannot use 'subject!' inside of an example block" if @def %} - - let!(subject) {{block}} - end - - # Explicitly defines the subject of the tests. - # Creates a memoized getter for the subject. - # The subject can be referenced by using `subject` or *name*. - # The block is evaluated once before the example runs - # and the return value is saved for subsequent calls. - macro subject!(name, &block) - {% raise "Missing block for 'subject'" unless block %} - {% raise "Expected zero or one arguments for 'subject!', but got #{block.args.size}" if block.args.size > 1 %} - {% raise "Cannot use 'subject!' inside of an example block" if @def %} - {% raise "Cannot use '#{name.id}' for 'subject!'" if name.id.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} - - let!({{name.id}}) {{block}} - - {% if name.id != :subject.id %} - private def subject - {{name.id}} - end - {% end %} - end - end -end diff --git a/src/spectator/dsl/metadata.cr b/src/spectator/dsl/metadata.cr deleted file mode 100644 index 04092b9..0000000 --- a/src/spectator/dsl/metadata.cr +++ /dev/null @@ -1,29 +0,0 @@ -module Spectator::DSL - module Metadata - # Defines a class method named *name* that combines metadata - # returned by *source* with *tags* and *metadata*. - # Any falsey items from *metadata* are removed. - private macro _spectator_metadata(name, source, *tags, **metadata) - private def self.{{name.id}} - %metadata = {{source.id}}.dup - {% unless tags.empty? && metadata.empty? %} - %metadata ||= ::Spectator::Metadata.new - {% end %} - {% for k in tags %} - %metadata[{{k.id.symbolize}}] = nil - {% end %} - {% for k, v in metadata %} - %cond = begin - {{v}} - end - if %cond - %metadata[{{k.id.symbolize}}] = %cond.to_s - else - %metadata.delete({{k.id.symbolize}}) - end - {% end %} - %metadata - end - end - end -end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr deleted file mode 100644 index ab05636..0000000 --- a/src/spectator/dsl/mocks.cr +++ /dev/null @@ -1,507 +0,0 @@ -require "../mocks" - -module Spectator::DSL - # Methods and macros for mocks and doubles. - module Mocks - # All defined double and mock types. - # Each tuple consists of the double name or mocked type, - # defined context (example group), and double type name relative to its context. - TYPES = [] of {Symbol, Symbol, Symbol} - - # Defines a new double type. - # - # This must be called from outside of a method (where classes can be defined). - # The *name* is the identifier used to reference the double, like when instantiating it. - # Simple stubbed methods returning a value can be defined by *value_methods*. - # More complex methods and stubs can be defined in a block passed to this macro. - # - # ``` - # def_double(:dbl, foo: 42, bar: "baz") do - # stub abstract def deferred : String - # end - # ``` - private macro def_double(name, **value_methods, &block) - {% # Construct a unique type name for the double by using the number of defined doubles. - index = ::Spectator::DSL::Mocks::TYPES.size - double_type_name = "Double#{index}".id - null_double_type_name = "NullDouble#{index}".id - - # Store information about how the double is defined and its context. - # This is important for constructing an instance of the double later. - ::Spectator::DSL::Mocks::TYPES << {name.id.symbolize, @type.name(generic_args: false).symbolize, double_type_name.symbolize} %} - - # Define the plain double type. - ::Spectator::Double.define({{double_type_name}}, {{name}}, {{**value_methods}}) do - # Returns a new double that responds to undefined methods with itself. - # See: `NullDouble` - def as_null_object - {{null_double_type_name}}.new(@stubs) - end - - {{block.body if block}} - end - - {% begin %} - # Define a matching null double type. - ::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{**value_methods}}) {{block}} - {% end %} - end - - # Instantiates a double. - # - # The *name* is an optional identifier for the double. - # If *name* was previously used to define a double (with `#def_double`), - # then this macro returns a new instance of that previously defined double type. - # Otherwise, a `LazyDouble` is created and returned. - # - # Initial stubbed values for methods can be provided with *value_methods*. - # - # ``` - # def_double(:dbl, foo: 42) - # - # specify do - # dbl = new_double(:dbl, foo: 7) - # expect(dbl.foo).to eq(7) - # lazy = new_double(:lazy, foo: 123) - # expect(lazy.foo).to eq(123) - # end - # ``` - private macro new_double(name = nil, **value_methods) - {% # Find tuples with the same name. - found_tuples = ::Spectator::DSL::Mocks::TYPES.select { |tuple| tuple[0] == name.id.symbolize } - - # Split the current context's type namespace into parts. - type_parts = @type.name(generic_args: false).split("::") - - # Find tuples in the same context or a parent of where the double was defined. - # This is done by comparing each part of their namespaces. - found_tuples = found_tuples.select do |tuple| - # Split the namespace of the context the double was defined in. - context_parts = tuple[1].id.split("::") - - # Compare namespace parts between the context the double was defined in and this context. - # This logic below is effectively comparing array elements, but with methods supported by macros. - matches = context_parts.map_with_index { |part, i| part == type_parts[i] } - matches.all? { |b| b } - end - - # Sort the results by the number of namespace parts. - # The last result will be the double type defined closest to the current context's type. - found_tuples = found_tuples.sort_by do |tuple| - tuple[1].id.split("::").size - end - found_tuple = found_tuples.last %} - - begin - %double = {% if found_tuple %} - {{found_tuple[2].id}}.new({{**value_methods}}) - {% else %} - ::Spectator::LazyDouble.new({{name}}, {{**value_methods}}) - {% end %} - ::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset }) - %double - end - end - - # Instantiates a class double. - # - # The *name* is an optional identifier for the double. - # If *name* was previously used to define a double (with `#def_double`), - # then this macro returns a previously defined double class. - # Otherwise, `LazyDouble` is created and returned. - # - # Initial stubbed values for methods can be provided with *value_methods*. - # - # ``` - # def_double(:dbl) do - # stub def self.foo - # 42 - # end - # end - # - # specify do - # dbl = class_double(:dbl) - # expect(dbl.foo).to eq(42) - # allow(dbl).to receive(:foo).and_return(123) - # expect(dbl.foo).to eq(123) - # end - # ``` - private macro class_double(name = nil, **value_methods) - {% # Find tuples with the same name. - found_tuples = ::Spectator::DSL::Mocks::TYPES.select { |tuple| tuple[0] == name.id.symbolize } - - # Split the current context's type namespace into parts. - type_parts = @type.name(generic_args: false).split("::") - - # Find tuples in the same context or a parent of where the double was defined. - # This is done by comparing each part of their namespaces. - found_tuples = found_tuples.select do |tuple| - # Split the namespace of the context the double was defined in. - context_parts = tuple[1].id.split("::") - - # Compare namespace parts between the context the double was defined in and this context. - # This logic below is effectively comparing array elements, but with methods supported by macros. - matches = context_parts.map_with_index { |part, i| part == type_parts[i] } - matches.all? { |b| b } - end - - # Sort the results by the number of namespace parts. - # The last result will be the double type defined closest to the current context's type. - found_tuples = found_tuples.sort_by do |tuple| - tuple[1].id.split("::").size - end - found_tuple = found_tuples.last %} - - begin - %double = {% if found_tuple %} - {{found_tuple[2].id}} - {% else %} - ::Spectator::LazyDouble - {% end %} - {% for key, value in value_methods %} - %stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}) - %double._spectator_define_stub(%stub{key}) - {% end %} - ::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset }) - %double - end - end - - # Defines or instantiates a double. - # - # When used inside of a method, instantiates a new double. - # See `#new_double`. - # - # When used outside of a method, defines a new double. - # See `#def_double`. - macro double(name, **value_methods, &block) - {% begin %} - {% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{**value_methods}}) {{block}} - {% end %} - end - - # Instantiates a new double with predefined responses. - # - # This constructs a `LazyDouble`. - # - # ``` - # dbl = double(foo: 42) - # expect(dbl.foo).to eq(42) - # ``` - macro double(**value_methods) - ::Spectator::LazyDouble.new({{**value_methods}}) - end - - # Defines a new mock type. - # - # This must be called from outside of a method (where classes can be defined). - # *type* is the type being mocked. - # The *name* is an optional identifier used in debug output. - # Simple stubbed methods returning a value can be defined by *value_methods*. - # More complex methods and stubs can be defined in a block passed to this macro. - # - # ``` - # abstract class MyClass - # def foo - # 42 - # end - # - # def bar - # Time.utc - # end - # end - # - # def_mock(MyClass, foo: 5) do - # stub def bar - # Time.utc(2022, 7, 10) - # end - # end - # ``` - private macro def_mock(type, name = nil, **value_methods, &block) - {% resolved = type.resolve - # Construct a unique type name for the mock by using the number of defined types. - index = ::Spectator::DSL::Mocks::TYPES.size - # The type is nested under the original so that any type names from the original can be resolved. - mock_type_name = "Mock#{index}".id - - # Store information about how the mock is defined and its context. - # This is important for constructing an instance of the mock later. - ::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, "::#{resolved.name}::#{mock_type_name}".id.symbolize} - - base = if resolved.class? - :class - elsif resolved.struct? - :struct - else - :module - end %} - - {% begin %} - {{base.id}} ::{{resolved.name}} - ::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}} - end - {% end %} - end - - # Instantiates a mock. - # - # *type* is the type being mocked. - # - # Initial stubbed values for methods can be provided with *value_methods*. - # - # ``` - # abstract class MyClass - # def foo - # 42 - # end - # - # def bar - # Time.utc - # end - # end - # - # def_mock(MyClass, foo: 5) do - # stub def bar - # Time.utc(2022, 7, 10) - # end - # end - # - # specify do - # dbl = new_mock(MyClass, foo: 7) - # expect(dbl.foo).to eq(7) - # expect(dbl.bar).to eq(Time.utc(2022, 7, 10)) - # end - # ``` - private macro new_mock(type, **value_methods) - {% # Find tuples with the same name. - found_tuples = ::Spectator::DSL::Mocks::TYPES.select { |tuple| tuple[0] == type.id.symbolize } - - # Split the current context's type namespace into parts. - type_parts = @type.name(generic_args: false).split("::") - - # Find tuples in the same context or a parent of where the mock was defined. - # This is done by comparing each part of their namespaces. - found_tuples = found_tuples.select do |tuple| - # Split the namespace of the context the double was defined in. - context_parts = tuple[1].id.split("::") - - # Compare namespace parts between the context the double was defined in and this context. - # This logic below is effectively comparing array elements, but with methods supported by macros. - matches = context_parts.map_with_index { |part, i| part == type_parts[i] } - matches.all? { |b| b } - end - - # Sort the results by the number of namespace parts. - # The last result will be the double type defined closest to the current context's type. - found_tuples = found_tuples.sort_by do |tuple| - tuple[1].id.split("::").size - end - found_tuple = found_tuples.last %} - - {% if found_tuple %} - {{found_tuple[2].id}}.new.tap do |%mock| - {% for key, value in value_methods %} - %stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}) - %mock._spectator_define_stub(%stub{key}) - {% end %} - ::Spectator::Harness.current?.try(&.cleanup { %mock._spectator_reset }) - end - {% else %} - {% raise "Type `#{type.id}` must be previously mocked before attempting to instantiate." %} - {% end %} - end - - # Defines or instantiates a mock. - # - # When used inside of a method, instantiates a new mock. - # See `#new_mock`. - # - # When used outside of a method, defines a new mock. - # See `#def_mock`. - macro mock(type, **value_methods, &block) - {% raise "First argument of `mock` must be a type name, not #{type}" unless type.is_a?(Path) || type.is_a?(Generic) || type.is_a?(Union) || type.is_a?(Metaclass) || type.is_a?(TypeNode) %} - {% begin %} - {% if @def %}new_mock{% else %}def_mock{% end %}({{type}}, {{**value_methods}}) {{block}} - {% end %} - end - - # Instantiates a class mock. - # - # *type* is the type being mocked. - # - # Initial stubbed values for methods can be provided with *value_methods*. - # - # ``` - # class MyClass - # def self.foo - # 42 - # end - # end - # - # def_mock(MyClass) - # - # specify do - # mock = class_mock(MyClass, foo: 5) - # expect(dbl.foo).to eq(5) - # allow(dbl).to receive(:foo).and_return(123) - # expect(dbl.foo).to eq(123) - # end - # ``` - private macro class_mock(type, **value_methods) - {% # Find tuples with the same name. - found_tuples = ::Spectator::DSL::Mocks::TYPES.select { |tuple| tuple[0] == type.id.symbolize } - - # Split the current context's type namespace into parts. - type_parts = @type.name(generic_args: false).split("::") - - # Find tuples in the same context or a parent of where the mock was defined. - # This is done by comparing each part of their namespaces. - found_tuples = found_tuples.select do |tuple| - # Split the namespace of the context the double was defined in. - context_parts = tuple[1].id.split("::") - - # Compare namespace parts between the context the double was defined in and this context. - # This logic below is effectively comparing array elements, but with methods supported by macros. - matches = context_parts.map_with_index { |part, i| part == type_parts[i] } - matches.all? { |b| b } - end - - # Sort the results by the number of namespace parts. - # The last result will be the double type defined closest to the current context's type. - found_tuples = found_tuples.sort_by do |tuple| - tuple[1].id.split("::").size - end - found_tuple = found_tuples.last %} - - {% if found_tuple %} - begin - %mock = {{found_tuple[2].id}} - {% for key, value in value_methods %} - %stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}) - %mock._spectator_define_stub(%stub{key}) - {% end %} - ::Spectator::Harness.current?.try(&.cleanup { %mock._spectator_reset }) - %mock - end - {% else %} - {% raise "Type `#{type.id}` must be previously mocked before attempting to instantiate." %} - {% end %} - end - - # Injects mock (stub) functionality into an existing type. - # - # Warning: Using this will modify the type being tested. - # This may result in different behavior between test and non-test code. - # - # This must be used instead of `def_mock` if a concrete struct is tested. - # The `mock` method is not necessary to create a type with an injected mock. - # The type can be used as it would normally instead. - # However, stub information may leak between examples. - # - # The *type* is the name of the type to inject mock functionality into. - # Initial stubbed values for methods can be provided with *value_methods*. - # - # ``` - # struct MyStruct - # def foo - # 42 - # end - # end - # - # inject_mock(MyStruct, foo: 5) - # - # specify do - # inst = MyStruct.new - # expect(inst.foo).to eq(5) - # allow(inst).to receive(:foo).and_return(123) - # expect(inst.foo).to eq(123) - # end - # ``` - macro inject_mock(type, **value_methods, &block) - {% resolved = type.resolve - base = if resolved.class? - :class - elsif resolved.struct? - :struct - else - :module - end - - # Store information about how the mock is defined and its context. - # This isn't required, but new_mock() should still find this type. - ::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, resolved.name.symbolize} %} - - ::Spectator::Mock.inject({{base}}, ::{{resolved.name}}, {{**value_methods}}) {{block}} - end - - # Targets a stubbable object (such as a mock or double) for operations. - # - # The *stubbable* must be a `Stubbable` or `StubbedType`. - # This method is expected to be followed up with `.to receive()`. - # - # ``` - # dbl = dbl(:foobar) - # allow(dbl).to receive(:foo).and_return(42) - # ``` - def allow(stubbable : Stubbable | StubbedType) - ::Spectator::Allow.new(stubbable) - end - - # Helper method producing a compilation error when attempting to stub a non-stubbable object. - # - # Triggered in cases like this: - # ``` - # allow(42).to receive(:to_s).and_return("123") - # ``` - def allow(stubbable) - {% raise "Target of `allow()` must be stubbable (mock or double)." %} - end - - # Begins the creation of a stub. - # - # The *method* is the name of the method being stubbed. - # It should not define any parameters, it should be just the method name as a literal symbol or string. - # - # Alone, this method returns a `NullStub`, which allows a stubbable object to return nil from a method. - # This macro is typically followed up with a method like `and_return` to change the stub's behavior. - # - # ``` - # dbl = dbl(:foobar) - # allow(dbl).to receive(:foo) - # expect(dbl.foo).to be_nil - # - # allow(dbl).to receive(:foo).and_return(42) - # expect(dbl.foo).to eq(42) - # ``` - # - # A block can be provided to be run every time the stub is invoked. - # The value returned by the block is returned by the stubbed method. - # - # ``` - # dbl = dbl(:foobar) - # allow(dbl).to receive(:foo) { 42 } - # expect(dbl.foo).to eq(42) - # ``` - macro receive(method, *, _file = __FILE__, _line = __LINE__, &block) - {% if block %} - %proc = ->(%args : ::Spectator::AbstractArguments) { - {% if !block.args.empty? %}{{*block.args}} = %args {% end %} - {{block.body}} - } - ::Spectator::ProcStub.new({{method.id.symbolize}}, %proc, location: ::Spectator::Location.new({{_file}}, {{_line}})) - {% else %} - ::Spectator::NullStub.new({{method.id.symbolize}}, location: ::Spectator::Location.new({{_file}}, {{_line}})) - {% end %} - end - - # Returns empty arguments. - def no_args - ::Spectator::Arguments.none - end - - # Indicates any arguments can be used (no constraint). - def any_args - ::Spectator::Arguments.any - end - end -end diff --git a/src/spectator/dsl/reserved.cr b/src/spectator/dsl/reserved.cr deleted file mode 100644 index 63e0026..0000000 --- a/src/spectator/dsl/reserved.cr +++ /dev/null @@ -1,7 +0,0 @@ -module Spectator - module DSL - # Keywords that cannot be used in specs using the DSL. - # These are either problematic or reserved for internal use. - RESERVED_KEYWORDS = %i[initialize finalize class allocate] - end -end diff --git a/src/spectator/dsl/top.cr b/src/spectator/dsl/top.cr deleted file mode 100644 index e70dd8f..0000000 --- a/src/spectator/dsl/top.cr +++ /dev/null @@ -1,32 +0,0 @@ -require "./groups" - -module Spectator::DSL - module Top - {% for method in %i[example_group describe context] %} - # Top-level describe method. - # All specs in a file must be wrapped in this call. - # This takes an argument and a block. - # The argument is what your spec is describing. - # It can be any Crystal expression, - # but is typically a class name or feature string. - # The block should contain all of the examples for what is being described. - # - # Example: - # ``` - # Spectator.describe Foo do - # # Your examples for `Foo` go here. - # end - # ``` - # - # Tags can be specified by adding symbols (keywords) after the first argument. - # Key-value pairs can also be specified. - # - # NOTE: Inside the block, the `Spectator` prefix _should not_ be used. - macro {{method.id}}(description, *tags, **metadata, &block) - class ::SpectatorTestContext - {{method.id}}(\{{description}}, \{{tags.splat(", ")}} \{{metadata.double_splat}}) \{{block}} - end - end - {% end %} - end -end diff --git a/src/spectator/error_result.cr b/src/spectator/error_result.cr deleted file mode 100644 index f58da20..0000000 --- a/src/spectator/error_result.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "./fail_result" - -module Spectator - # Outcome that indicates running an example generated an error. - # This occurs when an unexpected exception was raised while running an example. - # This is different from a "failed" result in that the error was not from a failed assertion. - class ErrorResult < FailResult - # Calls the `error` method on *visitor*. - def accept(visitor) - visitor.error(self) - end - - # Calls the `error` method on *visitor*. - def accept(visitor, &) - visitor.error(yield self) - end - - # One-word description of the result. - def to_s(io : IO) : Nil - io << "error" - end - - # String used for the JSON status field. - private def json_status - "error" - end - end -end diff --git a/src/spectator/example.cr b/src/spectator/example.cr deleted file mode 100644 index e18676c..0000000 --- a/src/spectator/example.cr +++ /dev/null @@ -1,297 +0,0 @@ -require "./example_context_delegate" -require "./example_group" -require "./harness" -require "./location" -require "./node" -require "./pending_result" -require "./result" -require "./metadata" - -module Spectator - # Standard example that runs a test case. - class Example < Node - # Currently running example. - class_getter! current : Example - - # Group the node belongs to. - getter! group : ExampleGroup - - # Assigns the node to the specified *group*. - # This is an internal method and should only be called from `ExampleGroup`. - # `ExampleGroup` manages the association of nodes to groups. - protected setter group : ExampleGroup? - - # Indicates whether the example already ran. - getter? finished : Bool = false - - # Result of the last time the example ran. - # Is pending if the example hasn't run. - getter result : Result = PendingResult.new("Example not run") - - @name_proc : Proc(Example, String)? - - # Creates the example. - # An instance to run the test code in is given by *context*. - # The *entrypoint* defines the test code (typically inside *context*). - # The *name* describes the purpose of the example. - # The *location* tracks where the example exists in source code. - # The example will be assigned to *group* if it is provided. - # A set of *metadata* can be used for filtering and modifying example behavior. - # Note: The metadata will not be merged with the parent metadata. - def initialize(@context : Context, @entrypoint : self ->, - name : String? = nil, location : Location? = nil, - @group : ExampleGroup? = nil, metadata = nil) - super(name, location, metadata) - - # Ensure group is linked. - group << self if group - end - - # Creates the example. - # An instance to run the test code in is given by *context*. - # The *entrypoint* defines the test code (typically inside *context*). - # The *name* describes the purpose of the example. - # It can be a proc to be evaluated in the context of the example. - # The *location* tracks where the example exists in source code. - # The example will be assigned to *group* if it is provided. - # A set of *metadata* can be used for filtering and modifying example behavior. - # Note: The metadata will not be merged with the parent metadata. - def initialize(@context : Context, @entrypoint : self ->, - @name_proc : Example -> String, location : Location? = nil, - @group : ExampleGroup? = nil, metadata = nil) - super(nil, location, metadata) - - # Ensure group is linked. - group << self if group - end - - # Creates a dynamic example. - # A block provided to this method will be called as-if it were the test code for the example. - # The block will be given this example instance as an argument. - # The *name* describes the purpose of the example. - # It can be a `Symbol` to describe a type. - # The *location* tracks where the example exists in source code. - # The example will be assigned to *group* if it is provided. - # A set of *metadata* can be used for filtering and modifying example behavior. - # Note: The metadata will not be merged with the parent metadata. - def initialize(name : String? = nil, location : Location? = nil, - @group : ExampleGroup? = nil, metadata = nil, &block : self ->) - super(name, location, metadata) - - @context = NullContext.new - @entrypoint = block - - # Ensure group is linked. - group << self if group - end - - # Creates a pending example. - # The *name* describes the purpose of the example. - # It can be a `Symbol` to describe a type. - # The *location* tracks where the example exists in source code. - # The example will be assigned to *group* if it is provided. - # A set of *metadata* can be used for filtering and modifying example behavior. - # Note: The metadata will not be merged with the parent metadata. - def self.pending(name : String? = nil, location : Location? = nil, - group : ExampleGroup? = nil, metadata = nil, reason = nil) - # Add pending tag and reason if they don't exist. - tags = {:pending => nil, :reason => reason} - metadata = metadata ? metadata.merge(tags) { |_, v, _| v } : tags - new(name, location, group, metadata) { nil } - end - - # Executes the test case. - # Returns the result of the execution. - # The result will also be stored in `#result`. - def run : Result - Log.debug { "Running example: #{self}" } - Log.warn { "Example already ran: #{self}" } if @finished - - if pending? - Log.debug { "Skipping example #{self} - marked pending" } - @finished = true - return @result = PendingResult.new(pending_reason) - end - - previous_example = @@current - @@current = self - - begin - @result = Harness.run do - if proc = @name_proc - self.name = proc.call(self) - end - - @group.try(&.call_before_all) - if (parent = @group) - parent.call_around_each(procsy).call - else - run_internal - end - if (parent = @group) - parent.call_after_all if parent.finished? - end - end - ensure - @@current = previous_example - @finished = true - end - end - - private def run_internal - if group = @group - group.call_before_each(self) - group.call_pre_condition(self) - end - Log.trace { "Running example code for: #{self}" } - @entrypoint.call(self) - @finished = true - Log.trace { "Finished running example code for: #{self}" } - if group = @group - group.call_post_condition(self) - group.call_after_each(self) - end - end - - # Executes code within the example's test context. - # This is an advanced method intended for internal usage only. - # - # The *klass* defines the type of the test context. - # This is typically only known by the code constructing the example. - # An error will be raised if *klass* doesn't match the test context's type. - # The block given to this method will be executed within the test context. - # - # The context casted to an instance of *klass* is provided as a block argument. - # - # TODO: Benchmark compiler performance using this method versus client-side casting in a proc. - protected def with_context(klass, &) - context = klass.cast(@context) - with context yield - end - - # Casts the example's test context to a specific type. - # This is an advanced method intended for internal usage only. - # - # The *klass* defines the type of the test context. - # This is typically only known by the code constructing the example. - # An error will be raised if *klass* doesn't match the test context's type. - # - # The context casted to an instance of *klass* is returned. - # - # TODO: Benchmark compiler performance using this method versus client-side casting in a proc. - protected def cast_context(klass) - klass.cast(@context) - end - - # Yields this example and all parent groups. - def ascend(&) - node = self - while node - yield node - node = node.group? - end - end - - # Constructs the full name or description of the example. - # This prepends names of groups this example is part of. - def to_s(io : IO) : Nil - name = @name - - # Prefix with group's full name if the node belongs to a group. - if (parent = @group) - parent.to_s(io) - - # Add padding between the node names - # only if the names don't appear to be symbolic. - # Skip blank group names (like the root group). - io << ' ' unless !parent.name? || # ameba:disable Style/NegatedConditionsInUnless - (parent.name?.is_a?(Symbol) && name.is_a?(String) && - (name.starts_with?('#') || name.starts_with?('.'))) - end - - super - end - - # Exposes information about the example useful for debugging. - def inspect(io : IO) : Nil - super - io << " - " << result - end - - # Creates the JSON representation of the example, - # which is just its name. - def to_json(json : JSON::Builder) - json.object do - json.field("description", name? || "") - json.field("full_description", to_s) - if location = location? - json.field("file_path", location.path) - json.field("line_number", location.line) - end - @result.to_json(json) if @finished - end - end - - # Creates a procsy from this example that runs the example. - def procsy - Procsy.new(self) { run_internal } - end - - # Creates a procsy from this example and the provided block. - def procsy(&block : ->) - Procsy.new(self, &block) - end - - # Wraps an example to behave like a `Proc`. - # This is typically used for an *around_each* hook. - # Invoking `#call` or `#run` will run the example. - struct Procsy - # Underlying example that will run. - getter example : Example - - # Creates the example proxy. - # The *example* should be run eventually. - # The *proc* defines the block of code to run when `#call` or `#run` is invoked. - def initialize(@example : Example, &@proc : ->) - end - - # Invokes the proc. - def call : Nil - @proc.call - end - - # Invokes the proc. - def run : Nil - @proc.call - end - - # Creates a new procsy for a block and the example from this instance. - def wrap(&block : ->) : self - self.class.new(@example, &block) - end - - # Executes code within the example's test context. - # This is an advanced method intended for internal usage only. - # - # The *klass* defines the type of the test context. - # This is typically only known by the code constructing the example. - # An error will be raised if *klass* doesn't match the test context's type. - # The block given to this method will be executed within the test context. - # - # TODO: Benchmark compiler performance using this method versus client-side casting in a proc. - protected def with_context(klass, &) - context = @example.cast_context(klass) - with context yield - end - - # Allow instance to behave like an example. - forward_missing_to @example - - # Constructs the full name or description of the example. - # This prepends names of groups this example is part of. - def to_s(io : IO) : Nil - @example.to_s(io) - end - end - end -end diff --git a/src/spectator/example_builder.cr b/src/spectator/example_builder.cr deleted file mode 100644 index 23398d2..0000000 --- a/src/spectator/example_builder.cr +++ /dev/null @@ -1,38 +0,0 @@ -require "./context" -require "./example" -require "./location" -require "./metadata" -require "./node_builder" - -module Spectator - # Constructs examples. - # Call `#build` to produce an `Example`. - class ExampleBuilder < NodeBuilder - @name : Proc(Example, String) | String? - - # Creates the builder. - # A proc provided by *context_builder* is used to create a unique `Context` for each example produced by `#build`. - # The *entrypoint* indicates the proc used to invoke the test code in the example. - # The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`. - def initialize(@context_builder : -> Context, @entrypoint : Example ->, - @name : String? = nil, @location : Location? = nil, @metadata : Metadata? = nil) - end - - # Creates the builder. - # A proc provided by *context_builder* is used to create a unique `Context` for each example produced by `#build`. - # The *entrypoint* indicates the proc used to invoke the test code in the example. - # The *name* is an interpolated string that runs in the context of the example. - # *location*, and *metadata* will be applied to the `Example` produced by `#build`. - def initialize(@context_builder : -> Context, @entrypoint : Example ->, - @name : Example -> String, @location : Location? = nil, @metadata : Metadata? = nil) - end - - # Constructs an example with previously defined attributes and context. - # The *parent* is an already constructed example group to nest the new example under. - # It can be nil if the new example won't have a parent. - def build(parent = nil) - context = @context_builder.call - Example.new(context, @entrypoint, @name, @location, parent, @metadata) - end - end -end diff --git a/src/spectator/example_context_delegate.cr b/src/spectator/example_context_delegate.cr deleted file mode 100644 index 0b61f57..0000000 --- a/src/spectator/example_context_delegate.cr +++ /dev/null @@ -1,33 +0,0 @@ -require "./context" -require "./example_context_method" -require "./null_context" - -module Spectator - # Stores a test context and a method to call within it. - # This is a variant of `ContextDelegate` that accepts the current running example. - struct ExampleContextDelegate - # Retrieves the underlying context. - protected getter context : Context - - # Creates the delegate. - # The *context* is the instance of the test context. - # The *method* is proc that downcasts *context* and calls a method on it. - def initialize(@context : Context, @method : ExampleContextMethod) - end - - # Creates a delegate with a null context. - # The context will be ignored and the block will be executed in its original scope. - # The example instance will be provided as an argument to the block. - def self.null(&block : Example -> _) - context = NullContext.new - method = ExampleContextMethod.new { |example| block.call(example) } - new(context, method) - end - - # Invokes a method in the test context. - # The *example* is the current running example. - def call(example : Example) - @method.call(example, @context) - end - end -end diff --git a/src/spectator/example_context_method.cr b/src/spectator/example_context_method.cr deleted file mode 100644 index c14d254..0000000 --- a/src/spectator/example_context_method.cr +++ /dev/null @@ -1,10 +0,0 @@ -require "./context" - -module Spectator - # Encapsulates a method in a test context. - # This could be used to invoke a test case or hook method. - # The context is passed as an argument. - # The proc should downcast the context instance to the desired type and call a method on that context. - # The current example is also passed as an argument. - alias ExampleContextMethod = Example, Context -> -end diff --git a/src/spectator/example_failed.cr b/src/spectator/example_failed.cr deleted file mode 100644 index 2170604..0000000 --- a/src/spectator/example_failed.cr +++ /dev/null @@ -1,14 +0,0 @@ -require "./location" - -module Spectator - # Exception that indicates an example failed. - # When raised within a test, the test should abort. - class ExampleFailed < Exception - getter! location : Location - - # Creates the exception. - def initialize(@location : Location?, message : String? = nil, cause : Exception? = nil) - super(message, cause) - end - end -end diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr deleted file mode 100644 index 55a3233..0000000 --- a/src/spectator/example_group.cr +++ /dev/null @@ -1,151 +0,0 @@ -require "./example_procsy_hook" -require "./hooks" -require "./node" - -module Spectator - # Collection of examples and sub-groups. - class ExampleGroup < Node - include Hooks - include Indexable(Node) - - @nodes = [] of Node - - # Parent group this group belongs to. - getter! group : ExampleGroup - - # Assigns this group to the specified *group*. - # This is an internal method and should only be called from `ExampleGroup`. - # `ExampleGroup` manages the association of nodes to groups. - protected setter group : ExampleGroup? - - define_hook before_all : ExampleGroupHook do - Log.trace { "Processing before_all hooks for: #{self}" } - - @group.try &.call_before_all - before_all_hooks.each &.call_once - end - - define_hook after_all : ExampleGroupHook, :prepend do - Log.trace { "Processing after_all hooks for: #{self}" } - - after_all_hooks.each &.call_once if finished? - if group = @group - group.call_after_all if group.finished? - end - end - - define_hook before_each : ExampleHook do |example| - Log.trace { "Processing before_each hooks for: #{self}" } - - @group.try &.call_before_each(example) - before_each_hooks.each &.call(example) - end - - define_hook after_each : ExampleHook, :prepend do |example| - Log.trace { "Processing after_each hooks for: #{self}" } - - after_each_hooks.each &.call(example) - @group.try &.call_after_each(example) - end - - define_hook around_each : ExampleProcsyHook do |procsy| - Log.trace { "Processing around_each hooks for: #{self}" } - - around_each_hooks.reverse_each { |hook| procsy = hook.wrap(procsy) } - if group = @group - procsy = group.call_around_each(procsy) - end - procsy - end - - define_hook pre_condition : ExampleHook do |example| - Log.trace { "Processing pre_condition hooks for: #{self}" } - - @group.try &.call_pre_condition(example) - pre_condition_hooks.each &.call(example) - end - - define_hook post_condition : ExampleHook, :prepend do |example| - Log.trace { "Processing post_condition hooks for: #{self}" } - - post_condition_hooks.each &.call(example) - @group.try &.call_post_condition(example) - end - - # Creates the example group. - # The *name* describes the purpose of the group. - # It can be a `Symbol` to describe a type. - # The *location* tracks where the group exists in source code. - # This group will be assigned to the parent *group* if it is provided. - # A set of *metadata* can be used for filtering and modifying example behavior. - def initialize(@name : Label = nil, @location : Location? = nil, - @group : ExampleGroup? = nil, @metadata : Metadata? = nil) - # Ensure group is linked. - group << self if group - end - - delegate size, unsafe_fetch, to: @nodes - - # Yields this group and all parent groups. - def ascend(&) - group = self - while group - yield group - group = group.group? - end - end - - # Removes the specified *node* from the group. - # The node will be unassigned from this group. - def delete(node : Node) - # Only remove from the group if it is associated with this group. - return unless node.group == self - - node.group = nil - @nodes.delete(node) - end - - # Checks if all examples and sub-groups have finished. - def finished? : Bool - @nodes.all?(&.finished?) - end - - # Constructs the full name or description of the example group. - # This prepends names of groups this group is part of. - def to_s(io : IO, *, nested = false) : Nil - unless parent = @group - # Display special string when called directly. - io << "" unless nested - return - end - - # Prefix with group's full name if the node belongs to a group. - parent.to_s(io, nested: true) - name = @name - - # Add padding between the node names - # only if the names don't appear to be symbolic. - # Skip blank group names (like the root group). - io << ' ' unless !parent.name? || # ameba:disable Style/NegatedConditionsInUnless - (parent.name?.is_a?(Symbol) && name.is_a?(String) && - (name.starts_with?('#') || name.starts_with?('.'))) - - super(io) - end - - # Adds the specified *node* to the group. - # Assigns the node to this group. - # If the node already belongs to a group, - # it will be removed from the previous group before adding it to this group. - def <<(node : Node) - # Remove from existing group if the node is part of one. - if (previous = node.group?) - previous.delete(node) - end - - # Add the node to this group and associate with it. - @nodes << node - node.group = self - end - end -end diff --git a/src/spectator/example_group_builder.cr b/src/spectator/example_group_builder.cr deleted file mode 100644 index 207cb6e..0000000 --- a/src/spectator/example_group_builder.cr +++ /dev/null @@ -1,62 +0,0 @@ -require "./example_group" -require "./example_group_hook" -require "./example_hook" -require "./example_procsy_hook" -require "./hooks" -require "./label" -require "./location" -require "./metadata" -require "./node_builder" - -module Spectator - # Progressively constructs an example group. - # Hooks and builders for child nodes can be added over time to this builder. - # When done, call `#build` to produce an `ExampleGroup`. - class ExampleGroupBuilder < NodeBuilder - include Hooks - - define_hook before_all : ExampleGroupHook - define_hook after_all : ExampleGroupHook, :prepend - define_hook before_each : ExampleHook - define_hook after_each : ExampleHook, :prepend - define_hook pre_condition : ExampleHook - define_hook post_condition : ExampleHook, :prepend - define_hook around_each : ExampleProcsyHook - - @children = [] of NodeBuilder - - # Creates the builder. - # Initially, the builder will have no children and no hooks. - # The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`. - def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata? = nil) - end - - # Constructs an example group with previously defined attributes, children, and hooks. - # The *parent* is an already constructed example group to nest the new example group under. - # It can be nil if the new example group won't have a parent. - def build(parent = nil) - ExampleGroup.new(@name, @location, parent, @metadata).tap do |group| - apply_hooks(group) - @children.each(&.build(group)) - end - end - - # Adds a child builder to the group. - # The *builder* will have `NodeBuilder#build` called on it from within `#build`. - # The new example group will be passed to it. - def <<(builder) - @children << builder - end - - # Adds all previously configured hooks to an example group. - private def apply_hooks(group) - before_all_hooks.each { |hook| group.before_all(hook) } - before_each_hooks.each { |hook| group.before_each(hook) } - after_all_hooks.reverse_each { |hook| group.after_all(hook) } - after_each_hooks.reverse_each { |hook| group.after_each(hook) } - around_each_hooks.each { |hook| group.around_each(hook) } - pre_condition_hooks.each { |hook| group.pre_condition(hook) } - post_condition_hooks.reverse_each { |hook| group.post_condition(hook) } - end - end -end diff --git a/src/spectator/example_group_hook.cr b/src/spectator/example_group_hook.cr deleted file mode 100644 index aee357f..0000000 --- a/src/spectator/example_group_hook.cr +++ /dev/null @@ -1,57 +0,0 @@ -require "./label" -require "./location" - -module Spectator - # Information about a hook tied to an example group and a proc to invoke it. - class ExampleGroupHook - # Location of the hook in source code. - getter! location : Location - - # User-defined description of the hook. - getter! label : Label - - @proc : -> - @called = Atomic::Flag.new - - # Creates the hook with a proc. - # The *proc* will be called when the hook is invoked. - # A *location* and *label* can be provided for debugging. - def initialize(@proc : (->), *, @location : Location? = nil, @label : Label = nil) - end - - # Creates the hook with a block. - # The block will be executed when the hook is invoked. - # A *location* and *label* can be provided for debugging. - def initialize(*, @location : Location? = nil, @label : Label = nil, &block : -> _) - @proc = block - end - - # Invokes the hook. - def call : Nil - @called.test_and_set - @proc.call - end - - # Invokes the hook if it hasn't already been invoked. - # Returns true if the hook was invoked (first time being called). - def call_once : Bool - first = @called.test_and_set - @proc.call if first - first - end - - # Produces the string representation of the hook. - # Includes the location and label if they're not nil. - def to_s(io : IO) : Nil - io << "example group hook" - - if (label = @label) - io << ' ' << label - end - - if (location = @location) - io << " @ " << location - end - end - end -end diff --git a/src/spectator/example_group_iteration.cr b/src/spectator/example_group_iteration.cr deleted file mode 100644 index 0d20a29..0000000 --- a/src/spectator/example_group_iteration.cr +++ /dev/null @@ -1,25 +0,0 @@ -require "./example_group" -require "./label" -require "./location" -require "./metadata" - -module Spectator - # Collection of examples and sub-groups for a single iteration of an iterative example group. - class ExampleGroupIteration(T) < ExampleGroup - # Item for this iteration of the example groups. - getter item : T - - # Creates the example group iteration. - # The element for the current iteration is provided by *item*. - # The *name* describes the purpose of the group. - # It can be a `Symbol` to describe a type. - # This is typically a stringified form of *item*. - # The *location* tracks where the group exists in source code. - # This group will be assigned to the parent *group* if it is provided. - # A set of *metadata* can be used for filtering and modifying example behavior. - def initialize(@item : T, name : Label = nil, location : Location? = nil, - group : ExampleGroup? = nil, metadata : Metadata? = nil) - super(name, location, group, metadata) - end - end -end diff --git a/src/spectator/example_hook.cr b/src/spectator/example_hook.cr deleted file mode 100644 index 6bc77a0..0000000 --- a/src/spectator/example_hook.cr +++ /dev/null @@ -1,52 +0,0 @@ -require "./label" -require "./location" - -module Spectator - # Information about a hook tied to an example and a proc to invoke it. - class ExampleHook - # Method signature for example hooks. - alias Proc = Example -> - - # Location of the hook in source code. - getter! location : Location - - # User-defined description of the hook. - getter! label : Label - - @proc : Proc - - # Creates the hook with a proc. - # The *proc* will be called when the hook is invoked. - # A *location* and *label* can be provided for debugging. - def initialize(@proc : Proc, *, @location : Location? = nil, @label : Label = nil) - end - - # Creates the hook with a block. - # The block must take a single argument - the current example. - # The block will be executed when the hook is invoked. - # A *location* and *label* can be provided for debugging. - def initialize(*, @location : Location? = nil, @label : Label = nil, &block : Proc) - @proc = block - end - - # Invokes the hook. - # The *example* refers to the current example. - def call(example : Example) : Nil - @proc.call(example) - end - - # Produces the string representation of the hook. - # Includes the location and label if they're not nil. - def to_s(io : IO) : Nil - io << "example hook" - - if (label = @label) - io << ' ' << label - end - - if (location = @location) - io << " @ " << location - end - end - end -end diff --git a/src/spectator/example_iterator.cr b/src/spectator/example_iterator.cr deleted file mode 100644 index 3a4ba30..0000000 --- a/src/spectator/example_iterator.cr +++ /dev/null @@ -1,52 +0,0 @@ -require "./example" -require "./node" - -module Spectator - # Iterates through all examples in a group and its nested groups. - # Nodes are iterated in pre-order. - class ExampleIterator - include Iterator(Example) - - # A stack is used to track where in the tree this iterator is. - @stack = Deque(Node).new(1) - - # Creates a new iterator. - # The *group* is the example group to iterate through. - def initialize(@group : Node) - @stack.push(@group) - end - - # Retrieves the next `Example`. - # If there are none left, then `Iterator::Stop` is returned. - def next - # Keep going until either: - # a. an example is found. - # b. the stack is empty. - until @stack.empty? - # Retrieve the next node. - # This could be an `Example` or a group. - node = @stack.pop - - # If the node is a group, add its direct children to the queue - # in reverse order so that the tree is traversed in pre-order. - if node.is_a?(Indexable(Node)) - node.reverse_each { |child| @stack.push(child) } - end - - # Return the node if it's an example. - # Otherwise, advance and check the next one. - return node if node.is_a?(Example) - end - - # Nothing left to iterate. - stop - end - - # Restart the iterator at the beginning. - def rewind - @stack.clear - @stack.push(@group) - self - end - end -end diff --git a/src/spectator/example_pending.cr b/src/spectator/example_pending.cr deleted file mode 100644 index 16c0709..0000000 --- a/src/spectator/example_pending.cr +++ /dev/null @@ -1,14 +0,0 @@ -module Spectator - # Exception that indicates an example is pending and should be skipped. - # When raised within a test, the test should abort. - class ExamplePending < Exception - # Location of where the example was aborted. - getter location : Location? - - # Creates the exception. - # Specify *location* to where the example was aborted. - def initialize(@location : Location? = nil, message : String? = nil, cause : Exception? = nil) - super(message, cause) - end - end -end diff --git a/src/spectator/example_procsy_hook.cr b/src/spectator/example_procsy_hook.cr deleted file mode 100644 index 16bc970..0000000 --- a/src/spectator/example_procsy_hook.cr +++ /dev/null @@ -1,54 +0,0 @@ -require "./label" -require "./location" - -module Spectator - # Information about a hook tied to an example and a proc to invoke it. - class ExampleProcsyHook - # Location of the hook in source code. - getter! location : Location - - # User-defined description of the hook. - getter! label : Label - - @proc : Example::Procsy -> - - # Creates the hook with a proc. - # The *proc* will be called when the hook is invoked. - # A *location* and *label* can be provided for debugging. - def initialize(@proc : (Example::Procsy ->), *, @location : Location? = nil, @label : Label = nil) - end - - # Creates the hook with a block. - # The block must take a single argument - the current example wrapped in a procsy. - # The block will be executed when the hook is invoked. - # A *location* and *label* can be provided for debugging. - def initialize(*, @location : Location? = nil, @label : Label = nil, &block : Example::Procsy -> _) - @proc = block - end - - # Invokes the hook. - # The *example* refers to the current example. - def call(procsy : Example::Procsy) : Nil - @proc.call(procsy) - end - - # Creates an example procsy that invokes this hook. - def wrap(procsy : Example::Procsy) : Example::Procsy - procsy.wrap { call(procsy) } - end - - # Produces the string representation of the hook. - # Includes the location and label if they're not nil. - def to_s(io : IO) : Nil - io << "example hook" - - if (label = @label) - io << ' ' << label - end - - if (location = @location) - io << " @ " << location - end - end - end -end diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr deleted file mode 100644 index 79d8473..0000000 --- a/src/spectator/expectation.cr +++ /dev/null @@ -1,281 +0,0 @@ -require "json" -require "./expression" -require "./location" - -module Spectator - # Result of evaluating a matcher on a target. - # Contains information about the match, - # such as whether it was successful and a description of the operation. - struct Expectation - # Location of the expectation in source code. - # This can be nil if the location can't be captured, - # for instance using the *should* syntax or dynamically created expectations. - getter! location : Location - - # Indicates whether the expectation was met. - def satisfied? - @match_data.matched? - end - - # Indicates whether the expectation was not met. - def failed? - !satisfied? - end - - # If nil, then the match was successful. - def failure_message? - return unless match_data = @match_data.as?(Matchers::FailedMatchData) - - case message = @message - when String then message - when Proc(String) then @message = message.call # Cache result of call. - else match_data.failure_message - end - end - - # Description of why the match failed. - def failure_message - failure_message?.not_nil! - end - - # Additional information about the match, useful for debug. - # If nil, then the match was successful. - def values? - @match_data.as?(Matchers::FailedMatchData).try(&.values) - end - - # Additional information about the match, useful for debug. - def values - values?.not_nil! - end - - def description - @match_data.description - end - - # Creates the expectation. - # The *match_data* comes from the result of calling `Matcher#match`. - # The *location* is the location of the expectation in source code, if available. - # A custom *message* can be used in case of a failure. - def initialize(@match_data : Matchers::MatchData, @location : Location? = nil, - @message : String? | Proc(String) = nil) - end - - # Creates the JSON representation of the expectation. - def to_json(json : JSON::Builder) - json.object do - if location = @location - json.field("file_path", location.path) - json.field("line_number", location.line) - end - json.field("satisfied", satisfied?) - if (failed = @match_data.as?(Matchers::FailedMatchData)) - failed_to_json(failed, json) - end - end - end - - # Adds failure information to a JSON structure. - private def failed_to_json(failed : Matchers::FailedMatchData, json : JSON::Builder) - json.field("failure", failed.failure_message) - json.field("values") do - json.object do - failed.values.each do |pair| - json.field(pair.first, pair.last) - end - end - end - end - - # Stores part of an expectation. - # This covers the actual value (or block) being inspected and its location. - # This is the type returned by an `expect` block in the DSL. - # It is not intended to be used directly, but instead by chaining methods. - # Typically `#to` and `#not_to` are used. - struct Target(T) - # Creates the expectation target. - # The *expression* is the actual value being tested and its label. - # The *location* is the location of where this expectation was defined. - def initialize(@expression : Expression(T), @location : Location) - end - - # Asserts that a method is called some point before the example completes. - @[AlwaysInline] - def to(stub : Stub, message = nil) : Nil - {% raise "The syntax `expect(...).to receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %} - - to_eventually(stub, message) - end - - # Asserts that some criteria defined by the matcher is satisfied. - # Allows a custom message to be used. - def to(matcher, message = nil) : Nil - match_data = matcher.match(@expression) - report(match_data, message) - end - - # Asserts that some criteria defined by the matcher is satisfied. - # Allows a custom message to be used. - # Returns the expected value cast as the expected type, if the matcher is satisfied. - def to(matcher : Matchers::TypeMatcher(U), message = nil) forall U - match_data = matcher.match(@expression) - value = @expression.value - if report(match_data, message) - return value if value.is_a?(U) - - raise "Spectator bug: expected value should have cast to #{U}" - else - raise TypeCastError.new("#{@expression.label} is expected to be a #{U}, but was actually #{value.class}") - end - end - - # Asserts that a method is not called before the example completes. - @[AlwaysInline] - def to_not(stub : Stub, message = nil) : Nil - {% raise "The syntax `expect(...).to_not receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %} - - to_never(stub, message) - end - - # :ditto: - @[AlwaysInline] - def not_to(stub : Stub, message = nil) : Nil - to_not(stub, message) - end - - # Asserts that some criteria defined by the matcher is not satisfied. - # This is effectively the opposite of `#to`. - # Allows a custom message to be used. - def to_not(matcher, message = nil) : Nil - match_data = matcher.negated_match(@expression) - report(match_data, message) - end - - # Asserts that some criteria defined by the matcher is not satisfied. - # Allows a custom message to be used. - # Returns the expected value cast without the unexpected type, if the matcher is satisfied. - def to_not(matcher : Matchers::TypeMatcher(U), message = nil) forall U - match_data = matcher.negated_match(@expression) - value = @expression.value - if report(match_data, message) - return value unless value.is_a?(U) - - raise "Spectator bug: expected value should not be #{U}" - else - raise TypeCastError.new("#{@expression.label} is not expected to be a #{U}, but was actually #{value.class}") - end - end - - # Asserts that some criteria defined by the matcher is not satisfied. - # Allows a custom message to be used. - # Returns the expected value cast as a non-nillable type, if the matcher is satisfied. - def to_not(matcher : Matchers::NilMatcher, message = nil) - match_data = matcher.negated_match(@expression) - if report(match_data, message) - value = @expression.value - return value unless value.nil? - - raise "Spectator bug: expected value should not be nil" - else - raise NilAssertionError.new("#{@expression.label} is not expected to be nil.") - end - end - - # :ditto: - @[AlwaysInline] - def not_to(matcher, message = nil) : Nil - to_not(matcher, message) - end - - # Asserts that a method is called some point before the example completes. - def to_eventually(stub : Stub, message = nil) : Nil - {% raise "The syntax `expect(...).to_eventually receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %} - - stubbable = @expression.value - unless stubbable._spectator_stub_for_method?(stub.method) - # Add stub without an argument constraint. - # Avoids confusing logic like this: - # ``` - # expect(dbl).to receive(:foo).with(:bar) - # dbl.foo(:baz) - # ``` - # Notice that `#foo` is called, but with different arguments. - # Normally this would raise an error, but that should be prevented. - unconstrained_stub = stub.with(Arguments.any) - stubbable._spectator_define_stub(unconstrained_stub) - end - - # Apply the stub that is expected to be called. - stubbable._spectator_define_stub(stub) - - # Check if the stub was invoked after the test completes. - matcher = Matchers::ReceiveMatcher.new(stub) - Harness.current.defer { to(matcher, message) } - - # Prevent leaking stubs between tests. - Harness.current.cleanup { stubbable._spectator_remove_stub(stub) } - end - - # Asserts that some criteria defined by the matcher is eventually satisfied. - # The expectation is checked after the example finishes and all hooks have run. - # Allows a custom message to be used. - def to_eventually(matcher, message = nil) : Nil - Harness.current.defer { to(matcher, message) } - end - - # Asserts that a method is not called before the example completes. - def to_never(stub : Stub, message = nil) : Nil - {% raise "The syntax `expect(...).to_never receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %} - - stubbable = @expression.value - unless stubbable._spectator_stub_for_method?(stub.method) - # Add stub without an argument constraint. - # Avoids confusing logic like this: - # ``` - # expect(dbl).to receive(:foo).with(:bar) - # dbl.foo(:baz) - # ``` - # Notice that `#foo` is called, but with different arguments. - # Normally this would raise an error, but that should be prevented. - unconstrained_stub = stub.with(Arguments.any) - stubbable._spectator_define_stub(unconstrained_stub) - end - - # Apply the stub that could be called in case it is. - stubbable._spectator_define_stub(stub) - - # Check if the stub was invoked after the test completes. - matcher = Matchers::ReceiveMatcher.new(stub) - Harness.current.defer { to_not(matcher, message) } - - # Prevent leaking stubs between tests. - Harness.current.cleanup { stubbable._spectator_remove_stub(stub) } - end - - # :ditto: - @[AlwaysInline] - def never_to(stub : Stub, message = nil) : Nil - to_never(stub, message) - end - - # Asserts that some criteria defined by the matcher is never satisfied. - # The expectation is checked after the example finishes and all hooks have run. - # Allows a custom message to be used. - def to_never(matcher, message = nil) : Nil - Harness.current.defer { to_not(matcher, message) } - end - - # :ditto: - @[AlwaysInline] - def never_to(matcher, message = nil) : Nil - to_never(matcher, message) - end - - # Reports an expectation to the current harness. - private def report(match_data : Matchers::MatchData, message : String? | Proc(String) = nil) - expectation = Expectation.new(match_data, @location, message) - Harness.current.report(expectation) - end - end - end -end diff --git a/src/spectator/expectation_failed.cr b/src/spectator/expectation_failed.cr deleted file mode 100644 index 2fe3941..0000000 --- a/src/spectator/expectation_failed.cr +++ /dev/null @@ -1,16 +0,0 @@ -require "./example_failed" -require "./expectation" - -module Spectator - # Exception that indicates an expectation from a test failed. - # When raised within a test, the test should abort. - class ExpectationFailed < ExampleFailed - # Expectation that failed. - getter expectation : Expectation - - # Creates the exception. - def initialize(@expectation : Expectation, message : String? = nil, cause : Exception? = nil) - super(expectation.location?, message, cause) - end - end -end diff --git a/src/spectator/expression.cr b/src/spectator/expression.cr deleted file mode 100644 index 0937b8e..0000000 --- a/src/spectator/expression.cr +++ /dev/null @@ -1,18 +0,0 @@ -require "./abstract_expression" - -module Spectator - # Represents an expression from a test. - # This is typically captured by an `expect` macro. - # It consists of a label and a typed expression. - # The label should be a string recognizable by the user, - # or nil if one isn't available. - abstract class Expression(T) < AbstractExpression - # Retrieves the underlying value of the expression. - abstract def value : T - - # Retrieves the evaluated value of the expression. - def raw_value - value - end - end -end diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr deleted file mode 100644 index 082a0d2..0000000 --- a/src/spectator/fail_result.cr +++ /dev/null @@ -1,81 +0,0 @@ -require "json" -require "./example_failed" -require "./location" -require "./result" - -module Spectator - # Outcome that indicates an example failed. - # This typically means an assertion did not pass. - class FailResult < Result - # Error that occurred while running the example. - # This describes the primary reason for the failure. - getter error : Exception - - # Creates a failure result. - # The *elapsed* argument is the length of time it took to run the example. - # The *error* is the exception raised that caused the failure. - def initialize(elapsed, @error, expectations = [] of Expectation) - super(elapsed, expectations) - end - - # Calls the `failure` method on *visitor*. - def accept(visitor) - visitor.fail(self) - end - - # Calls the `failure` method on *visitor*. - def accept(visitor, &) - visitor.fail(yield self) - end - - # Indicates whether the example passed. - def pass? : Bool - false - end - - # Indicates whether the example failed. - def fail? : Bool - true - end - - # Attempts to retrieve the location where the example failed. - # This only works if the location of the failed expectation was reported. - # If available, returns a `Location`, otherwise `nil`. - def location? : Location? - return unless error = @error.as?(ExampleFailed) - - error.location? - end - - # Attempts to retrieve the location where the example failed. - # This only works if the location of the failed expectation was reported. - # If available, returns a `Location`, otherwise raises `NilAssertionError`. - def location : Location - location? || raise(NilAssertionError.new("Source location of failure unavailable")) - end - - # One-word description of the result. - def to_s(io : IO) : Nil - io << "fail" - end - - # Creates a JSON object from the result information. - def to_json(json : JSON::Builder) - super - json.field("status", json_status) - json.field("exception") do - json.object do - json.field("class", @error.class.name) - json.field("message", @error.message) - json.field("backtrace", @error.backtrace) - end - end - end - - # String used for the JSON status field. - # Necessary for the error result to override the status, but nothing else from `#to_json`. - private def json_status - "failed" - end - end -end diff --git a/src/spectator/filtered_example_iterator.cr b/src/spectator/filtered_example_iterator.cr deleted file mode 100644 index 3286f3b..0000000 --- a/src/spectator/filtered_example_iterator.cr +++ /dev/null @@ -1,85 +0,0 @@ -require "./example" -require "./node" -require "./node_filter" -require "./node_iterator" - -module Spectator - # Iterates through selected nodes in a group and its nested groups. - # Nodes are iterated in pre-order. - class FilteredExampleIterator - include Iterator(Example) - - # A stack is used to track where in the tree this iterator is. - @stack = Deque(Node).new(1) - - # A queue stores forced examples that have been matched by the a parent group. - @queue = Deque(Example).new - - # Creates a new iterator. - # The *group* is the example group to iterate through. - # The *filter* selects which examples (and groups) to iterate through. - def initialize(@group : Node, @filter : NodeFilter) - @stack.push(@group) - end - - # Retrieves the next selected `Example`. - # If there are none left, then `Iterator::Stop` is returned. - def next - # Return items from the queue first before continuing to the stack. - return @queue.shift unless @queue.empty? - - # Keep going until either: - # a. a suitable example is found. - # b. the stack is empty. - until @stack.empty? - # Retrieve the next node. - node = @stack.pop - - # If the node is a group, conditionally traverse it. - if node.is_a?(Indexable(Node)) - # To traverse, a child node or the group itself must match the filter. - return node if node = next_group_match(node) - elsif node.is_a?(Example) && @filter.includes?(node) - return node - end - end - - # Nothing left to iterate. - stop - end - - # Restart the iterator at the beginning. - def rewind - @stack.clear - @stack.push(@group) - @queue.clear - self - end - - # Attempts to find the next matching example in a group. - # If any child in the group matches, then traversal on the stack (down the tree) continues. - # However, if no children match, but the group itself does, then all examples in the group match. - # In the latter scenario, the examples are added to the queue, and the next item from the queue returned. - # Stack iteration should continue if nil is returned. - private def next_group_match(group : Indexable(Node)) : Example? - # Look for any children that match. - iterator = NodeIterator.new(group) - - # Check if any children match. - # Skip first node because its the group being checked. - if iterator.skip(1).any?(@filter) - # Add the group's direct children to the queue - # in reverse order so that the tree is traversed in pre-order. - group.reverse_each { |node| @stack.push(node) } - - # Check if the group matches, but no children match. - elsif @filter.includes?(group) - # Add examples from the group to the queue. - # Return the next example from the queue. - iterator.rewind.select(Example).each { |node| @queue.push(node) } - @queue.shift unless @queue.empty? - # If the queue is empty (group has no examples), go to next loop iteration of the stack. - end - end - end -end diff --git a/src/spectator/formatting.cr b/src/spectator/formatting.cr deleted file mode 100644 index b5373d2..0000000 --- a/src/spectator/formatting.cr +++ /dev/null @@ -1,7 +0,0 @@ -require "./formatting/*" - -module Spectator - # Reports test results to the end-user in various formats. - module Formatting - end -end diff --git a/src/spectator/formatting/broadcast_formatter.cr b/src/spectator/formatting/broadcast_formatter.cr deleted file mode 100644 index 76ac814..0000000 --- a/src/spectator/formatting/broadcast_formatter.cr +++ /dev/null @@ -1,87 +0,0 @@ -require "./formatter" - -module Spectator::Formatting - # Reports events to multiple other formatters. - # Events received by this formatter will be sent to others. - class BroadcastFormatter < Formatter - # Creates the broadcast formatter. - # Takes a collection of formatters to pass events along to. - def initialize(@formatters : Enumerable(Formatter)) - end - - # Forwards the event to other formatters. - def start(notification) - @formatters.each(&.start(notification)) - end - - # :ditto: - def example_started(notification) - @formatters.each(&.example_started(notification)) - end - - # :ditto: - def example_finished(notification) - @formatters.each(&.example_finished(notification)) - end - - # :ditto: - def example_passed(notification) - @formatters.each(&.example_passed(notification)) - end - - # :ditto: - def example_pending(notification) - @formatters.each(&.example_pending(notification)) - end - - # :ditto: - def example_failed(notification) - @formatters.each(&.example_failed(notification)) - end - - # :ditto: - def example_error(notification) - @formatters.each(&.example_error(notification)) - end - - # :ditto: - def message(notification) - @formatters.each(&.message(notification)) - end - - # :ditto: - def stop - @formatters.each(&.stop) - end - - # :ditto: - def start_dump - @formatters.each(&.start_dump) - end - - # :ditto: - def dump_pending(notification) - @formatters.each(&.dump_pending(notification)) - end - - # :ditto: - def dump_failures(notification) - @formatters.each(&.dump_failures(notification)) - end - - # :ditto: - def dump_summary(notification) - @formatters.each(&.dump_summary(notification)) - end - - # :ditto: - def dump_profile(notification) - @formatters.each(&.dump_profile(notification)) - end - - # :ditto: - def close - @formatters.each(&.close) - end - end -end diff --git a/src/spectator/formatting/components.cr b/src/spectator/formatting/components.cr deleted file mode 100644 index 9d888f3..0000000 --- a/src/spectator/formatting/components.cr +++ /dev/null @@ -1,8 +0,0 @@ -require "./components/**" - -module Spectator::Formatting - # Namespace for snippets of text displayed in console output. - # These types are typically constructed and have `#to_s` called. - module Components - end -end diff --git a/src/spectator/formatting/components/block.cr b/src/spectator/formatting/components/block.cr deleted file mode 100644 index 22411a7..0000000 --- a/src/spectator/formatting/components/block.cr +++ /dev/null @@ -1,32 +0,0 @@ -module Spectator::Formatting::Components - # Base type for handling indented output. - # Indents are tracked and automatically printed. - # Use `#indent` to increase the indent for the duration of a block. - # Use `#line` to produce a line with an indentation prefixing it. - abstract struct Block - # Default indent amount. - private INDENT = 2 - - # Creates the block. - # A default *indent* size can be specified. - def initialize(*, @indent : Int32 = INDENT) - end - - # Increases the indent by the a specific *amount* for the duration of the block. - private def indent(amount = INDENT, &) - @indent += amount - yield - @indent -= amount - end - - # Produces a line of output with an indent before it. - # The contents of the line should be generated by a block provided to this method. - # Ensure that _only_ one line is produced by the block, - # otherwise the indent will be lost. - private def line(io, &) - @indent.times { io << ' ' } - yield - io.puts - end - end -end diff --git a/src/spectator/formatting/components/comment.cr b/src/spectator/formatting/components/comment.cr deleted file mode 100644 index 30f4293..0000000 --- a/src/spectator/formatting/components/comment.cr +++ /dev/null @@ -1,23 +0,0 @@ -require "colorize" - -module Spectator::Formatting::Components - # Object that can be stringified pre-pended with a comment mark (#). - struct Comment(T) - # Default color for a comment. - private COLOR = :cyan - - # Creates a comment with the specified content. - def initialize(@content : T) - end - - # Creates a colored comment. - def self.colorize(content) - new(content).colorize(COLOR) - end - - # Writes the comment to the output. - def to_s(io : IO) : Nil - io << "# " << @content - end - end -end diff --git a/src/spectator/formatting/components/error_result_block.cr b/src/spectator/formatting/components/error_result_block.cr deleted file mode 100644 index a24784a..0000000 --- a/src/spectator/formatting/components/error_result_block.cr +++ /dev/null @@ -1,83 +0,0 @@ -require "colorize" -require "../../example" -require "../../error_result" -require "./result_block" - -module Spectator::Formatting::Components - # Displays information about an error result. - struct ErrorResultBlock < ResultBlock - # Creates the component. - def initialize(example : Example, index : Int32, @error : Exception, subindex = 0) - super(example, index, subindex) - end - - # Content displayed on the second line of the block after the label. - private def subtitle - @error.message.try(&.each_line.first) - end - - # Prefix for the second line of the block. - private def subtitle_label - case @error - when ExampleFailed then "Failure: " - else "Error: " - end.colorize(:red) - end - - # Display error information. - private def content(io) - # Fetch the error and message. - lines = @error.message.try(&.lines) - - # Write the error and message if available. - case - when lines.nil? then write_error_class(io) - when lines.size == 1 then write_error_message(io, lines.first) - when lines.size > 1 then write_multiline_error_message(io, lines) - else write_error_class(io) - end - - # Display the backtrace if it's available. - if backtrace = @error.backtrace? - indent { write_backtrace(io, backtrace) } - end - - io.puts - end - - # Display just the error type. - private def write_error_class(io) - line(io) do - io << @error.class.colorize(:red) - end - end - - # Display the error type and first line of the message. - private def write_error_message(io, message) - line(io) do - io << "#{@error.class}: ".colorize(:red) - io << message - end - end - - # Display the error type and its multi-line message. - private def write_multiline_error_message(io, lines) - # Use the normal formatting for the first line. - write_error_message(io, lines.first) - - # Display additional lines after the first. - lines.skip(1).each do |entry| - line(io) { io << entry } - end - end - - # Writes the backtrace entries to the output. - private def write_backtrace(io, backtrace) - backtrace.each do |entry| - # Dim entries that are outside the shard. - entry = entry.colorize.dim unless entry.starts_with?(/(src|spec)\//) - line(io) { io << entry } - end - end - end -end diff --git a/src/spectator/formatting/components/example_command.cr b/src/spectator/formatting/components/example_command.cr deleted file mode 100644 index b1246db..0000000 --- a/src/spectator/formatting/components/example_command.cr +++ /dev/null @@ -1,26 +0,0 @@ -require "../../example" -require "./comment" - -module Spectator::Formatting::Components - # Provides syntax for running a specific example from the command-line. - struct ExampleCommand - # Creates the component with the specified example. - def initialize(@example : Example) - end - - # Produces output for running the previously specified example. - def to_s(io : IO) : Nil - io << "crystal spec " - - # Use location for argument if it's available, since it's simpler. - # Otherwise, use the example name filter argument. - if location = @example.location? - io << location - else - io << "-e " << @example - end - - io << ' ' << Comment.colorize(@example.to_s) - end - end -end diff --git a/src/spectator/formatting/components/fail_result_block.cr b/src/spectator/formatting/components/fail_result_block.cr deleted file mode 100644 index e93a94d..0000000 --- a/src/spectator/formatting/components/fail_result_block.cr +++ /dev/null @@ -1,66 +0,0 @@ -require "colorize" -require "../../example" -require "../../expectation" -require "../../fail_result" -require "./result_block" - -module Spectator::Formatting::Components - # Displays information about a fail result. - struct FailResultBlock < ResultBlock - @longest_key : Int32 - - # Creates the component. - def initialize(example : Example, index : Int32, @expectation : Expectation, subindex = 0) - super(example, index, subindex) - @longest_key = expectation.values.max_of { |(key, _value)| key.to_s.size } - end - - # Content displayed on the second line of the block after the label. - private def subtitle - @expectation.failure_message - end - - # Prefix for the second line of the block. - private def subtitle_label - "Failure: ".colorize(:red) - end - - # Display expectation match data. - private def content(io) - indent do - @expectation.values.each do |(key, value)| - value_line(io, key, value) - end - end - - io.puts - end - - # Display a single line for a match data value. - private def value_line(io, key, value) - key = key.to_s - padding = " " * (@longest_key - key.size) - lines = value.lines - - line(io) do - io << padding << key.colorize(:red) << ": ".colorize(:red) << lines.shift - end - - unless lines.empty? - indent(@longest_key + 2) do - lines.each do |line| - line(io) { io << line } - end - end - end - end - - # Produces the location line. - # This is where the result was determined. - private def location_line(io) - return unless location = @expectation.location? || @example.location? - - line(io) { io << Comment.colorize(location) } - end - end -end diff --git a/src/spectator/formatting/components/failure_command_list.cr b/src/spectator/formatting/components/failure_command_list.cr deleted file mode 100644 index 7eab4f5..0000000 --- a/src/spectator/formatting/components/failure_command_list.cr +++ /dev/null @@ -1,21 +0,0 @@ -require "../../example" -require "./example_command" - -module Spectator::Formatting::Components - # Produces a list of commands to run failed examples. - struct FailureCommandList - # Creates the component. - # Requires a set of *failures* to display commands for. - def initialize(@failures : Enumerable(Example)) - end - - # Produces the list of commands to run failed examples. - def to_s(io : IO) : Nil - io.puts "Failed examples:" - io.puts - @failures.each do |failure| - io.puts ExampleCommand.new(failure).colorize(:red) - end - end - end -end diff --git a/src/spectator/formatting/components/junit/root.cr b/src/spectator/formatting/components/junit/root.cr deleted file mode 100644 index 3c6f440..0000000 --- a/src/spectator/formatting/components/junit/root.cr +++ /dev/null @@ -1,39 +0,0 @@ -require "./test_suite" - -module Spectator::Formatting::Components::JUnit - # Root node of the JUnit XML document. - # This is the "testsuites" element and all of its children. - struct Root - # Creates the root element. - def initialize(@runtime : Time::Span, @suites : Array(TestSuite), *, - @total : Int32, @failures : Int32, @errors : Int32) - end - - # Constructs the element from a report. - def self.from_report(report) - hostname = System.hostname - counts = report.counts - suites = report.examples.group_by { |example| example.location?.try(&.path) || "anonymous" } - suites = suites.map do |file, examples| - TestSuite.from_examples(file, examples, hostname) - end - - new(report.runtime, suites, - total: counts.total, - failures: counts.fail, - errors: counts.error) - end - - # Produces the XML fragment. - def to_xml(xml) - xml.element("testsuites", - tests: @total, - failures: @failures, - errors: @errors, - time: @runtime.total_seconds, - name: "Spec") do - @suites.each(&.to_xml(xml)) - end - end - end -end diff --git a/src/spectator/formatting/components/junit/test_case.cr b/src/spectator/formatting/components/junit/test_case.cr deleted file mode 100644 index 796c97f..0000000 --- a/src/spectator/formatting/components/junit/test_case.cr +++ /dev/null @@ -1,102 +0,0 @@ -require "xml" -require "../../../example" - -module Spectator::Formatting::Components::JUnit - # Test case node of the JUnit XML document. - # This is the "testsuite" element and all of its children. - struct TestCase - # Creates the test case element. - def initialize(@class_name : String, @example : Example) - end - - # Produces the XML fragment. - def to_xml(xml) - result = @example.result - xml.element("testcase", - name: @example, - assertions: result.expectations.size, - classname: @class_name, - status: result.accept(StatusVisitor), - time: result.elapsed.total_seconds) do - visitor = ElementVisitor.new(xml) - result.accept(visitor) - end - end - - # Picks the status string for a result. - private module StatusVisitor - extend self - - # Returns "PASS". - def pass(_result) - "PASS" - end - - # Returns "FAIL". - def fail(_result) - "FAIL" - end - - # :ditto: - def error(result) - fail(result) - end - - # Returns "TODO". - def pending(_result) - "TODO" - end - end - - # Result visitor that adds elements to the test case node depending on the result. - private struct ElementVisitor - # Creates the visitor. - def initialize(@xml : XML::Builder) - end - - # Does nothing. - def pass(_result) - # ... - end - - # Adds a failure element to the test case node. - def fail(result) - error = result.error - result.expectations.each do |expectation| - next unless expectation.failed? - - @xml.element("failure", message: expectation.failure_message, type: error.class) do - match_data(expectation.values) - end - end - end - - # Adds an error element to the test case node. - def error(result) - error = result.error - fail(result) # Include failed expectations. - @xml.element("error", message: error.message, type: error.class) do - if backtrace = error.backtrace - @xml.text(backtrace.join("\n")) - end - end - end - - # Adds a skipped element to the test case node. - def pending(result) - @xml.element("skipped", message: result.reason) - end - - # Writes match data for a failed expectation. - private def match_data(values) - values.each do |(key, value)| - @xml.text("\n") - @xml.text(key.to_s) - @xml.text(": ") - @xml.text(value) - end - @xml.text("\n") - end - end - end -end diff --git a/src/spectator/formatting/components/junit/test_suite.cr b/src/spectator/formatting/components/junit/test_suite.cr deleted file mode 100644 index 28b5adc..0000000 --- a/src/spectator/formatting/components/junit/test_suite.cr +++ /dev/null @@ -1,104 +0,0 @@ -require "./test_case" - -module Spectator::Formatting::Components::JUnit - # Test suite node of the JUnit XML document. - # This is the "testsuite" element and all of its children. - struct TestSuite - # Amounts for each type of test result. - record Counts, total : Int32, failures : Int32, errors : Int32, skipped : Int32 - - # Creates the test suite element. - def initialize(@package : String, @name : String, @cases : Array(TestCase), - @time : Time::Span, @counts : Counts, @hostname : String) - end - - # Constructs the test suite element from a collection of tests. - # The *examples* should all come from the same *file*. - def self.from_examples(file, examples, hostname) - package, name = package_name_from_file(file) - counts = count_examples(examples) - time = examples.sum(&.result.elapsed) - cases = examples.map { |example| TestCase.new(name, example) } - new(package, name, cases, time, counts, hostname) - end - - # Constructs a package and suite name from a file path. - private def self.package_name_from_file(file) - path = Path.new(file.to_s) - name = path.stem - directory = path.dirname - package = directory.gsub(File::SEPARATOR, '.') - {package, name} - end - - # Counts the number of examples for each result type. - private def self.count_examples(examples) - visitor = CountVisitor.new - - # Iterate through each example and count the number of each type of result. - # Don't count examples that haven't run (indicated by `Node#finished?`). - # This typically happens in fail-fast mode. - examples.each do |example| - example.result.accept(visitor) if example.finished? - end - - visitor.counts - end - - # Produces the XML fragment. - def to_xml(xml) - xml.element("testsuite", - package: @package, - name: @name, - tests: @counts.total, - failures: @counts.failures, - errors: @counts.errors, - skipped: @counts.skipped, - time: @time.total_seconds, - hostname: @hostname) do - @cases.each(&.to_xml(xml)) - end - end - - # Totals up the number of each type of result. - # Defines methods for the different types of results. - # Call `#counts` to retrieve the `Counts` instance. - private class CountVisitor - @pass = 0 - @failures = 0 - @errors = 0 - @skipped = 0 - - # Increments the number of passing examples. - def pass(_result) - @pass += 1 - end - - # Increments the number of failing (non-error) examples. - def fail(_result) - @failures += 1 - end - - # Increments the number of error (and failed) examples. - def error(result) - fail(result) - @errors += 1 - end - - # Increments the number of pending (skipped) examples. - def pending(_result) - @skipped += 1 - end - - # Produces the total counts. - def counts - Counts.new( - total: @pass + @failures + @skipped, - failures: @failures, - errors: @errors, - skipped: @skipped - ) - end - end - end -end diff --git a/src/spectator/formatting/components/pending_result_block.cr b/src/spectator/formatting/components/pending_result_block.cr deleted file mode 100644 index 95c2481..0000000 --- a/src/spectator/formatting/components/pending_result_block.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "colorize" -require "../../example" -require "../../pending_result" -require "./result_block" - -module Spectator::Formatting::Components - # Displays information about a pending result. - struct PendingResultBlock < ResultBlock - # Creates the component. - def initialize(example : Example, index : Int32, @result : PendingResult) - super(example, index) - end - - # Content displayed on the second line of the block after the label. - private def subtitle - @result.reason - end - - # Prefix for the second line of the block. - private def subtitle_label - # TODO: Could be pending or skipped. - "Pending: ".colorize(:yellow) - end - - # No content for this type of block. - private def content(io) - end - end -end diff --git a/src/spectator/formatting/components/profile.cr b/src/spectator/formatting/components/profile.cr deleted file mode 100644 index b98d8f5..0000000 --- a/src/spectator/formatting/components/profile.cr +++ /dev/null @@ -1,41 +0,0 @@ -require "../../profile" -require "./runtime" - -module Spectator::Formatting::Components - # Displays profiling information for slow examples. - struct Profile - # Creates the component with the specified *profile*. - def initialize(@profile : Spectator::Profile) - end - - # Produces the output containing the profiling information. - def to_s(io : IO) : Nil - io << "Top " - io << @profile.size - io << " slowest examples (" - io << Runtime.new(@profile.time) - io << ", " - io << @profile.percentage.round(2) - io.puts "% of total time):" - - @profile.each do |example| - example_profile(io, example) - end - end - - # Writes a single example's timing to the output. - private def example_profile(io, example) - io << " " - io.puts example - io << " " - io << Runtime.new(example.result.elapsed).colorize.bold - - if location = example.location - io << ' ' - io.puts location - else - io.puts - end - end - end -end diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr deleted file mode 100644 index ddd9c47..0000000 --- a/src/spectator/formatting/components/result_block.cr +++ /dev/null @@ -1,95 +0,0 @@ -require "../../example" -require "./block" -require "./comment" - -module Spectator::Formatting::Components - # Base class that displayed indexed results in block form. - # These typically take the form: - # ```text - # 1) Title - # Label: Subtitle - # - # Content - # # Source - # ``` - abstract struct ResultBlock < Block - # Creates the block with the specified *index* and for the given *example*. - def initialize(@example : Example, @index : Int32, @subindex : Int32 = 0) - super() - end - - # Content displayed on the first line of the block. - # Will be stringified. - # By default, uses the example name. - # Can be overridden to use a different value. - private def title - @example - end - - # Content displayed on the second line of the block after the label. - # Will be stringified. - private abstract def subtitle - - # Prefix for the second line of the block. - # Will be stringified. - # This is typically something like "Error:" or "Failure:" - private abstract def subtitle_label - - # Produces the main content of the block. - # *io* is the stream to write to. - # `#line` and `#indent` (from `Block`) should be used to maintain spacing. - private abstract def content(io) - - # Writes the component's output to the specified stream. - def to_s(io : IO) : Nil - title_line(io) - # Ident over to align with the spacing used by the index. - indent(index_digit_count + 2) do - subtitle_line(io) - io.puts - content(io) - location_line(io) - end - end - - # Produces the title line. - private def title_line(io) - line(io) do - io << @index - io << '.' << @subindex if @subindex > 0 - io << ") " << title - end - end - - # Produces the subtitle line. - private def subtitle_line(io) - line(io) do - io << subtitle_label << subtitle - end - end - - # Produces the location line. - # This is where the result was determined. - private def location_line(io) - location = if (result = @example.result).responds_to?(:location?) - result.location? - end - location ||= @example.location? - return unless location - - line(io) { io << Comment.colorize(location) } - end - - # Computes the number of spaces the index takes - private def index_digit_count - count = digit_count(@index) - count += 1 + digit_count(@subindex) if @subindex > 0 - count - end - - # Computes the number of spaces an integer takes. - private def digit_count(integer) - (Math.log(integer.to_f + 1) / Math::LOG10).ceil.to_i - end - end -end diff --git a/src/spectator/formatting/components/runtime.cr b/src/spectator/formatting/components/runtime.cr deleted file mode 100644 index 9638d93..0000000 --- a/src/spectator/formatting/components/runtime.cr +++ /dev/null @@ -1,66 +0,0 @@ -module Spectator::Formatting::Components - # Presents a human readable time span. - struct Runtime - # Creates the component. - def initialize(@span : Time::Span) - end - - # Appends the elapsed time to the output. - # The text will be formatted as follows, depending on the magnitude: - # ```text - # ## microseconds - # ## milliseconds - # ## seconds - # #:## - # #:##:## - # # days #:##:## - # ``` - def to_s(io : IO) : Nil - millis = @span.total_milliseconds - return format_micro(io, millis * 1000) if millis < 1 - - seconds = @span.total_seconds - return format_millis(io, millis) if seconds < 1 - return format_seconds(io, seconds) if seconds < 60 - - minutes, seconds = seconds.divmod(60) - return format_minutes(io, minutes, seconds) if minutes < 60 - - hours, minutes = minutes.divmod(60) - return format_hours(io, hours, minutes, seconds) if hours < 24 - - days, hours = hours.divmod(24) - format_days(io, days, hours, minutes, seconds) - end - - # Formats for microseconds. - private def format_micro(io, micros) - io << micros.round.to_i << " microseconds" - end - - # Formats for milliseconds. - private def format_millis(io, millis) - io << millis.round(2) << " milliseconds" - end - - # Formats for seconds. - private def format_seconds(io, seconds) - io << seconds.round(2) << " seconds" - end - - # Formats for minutes. - private def format_minutes(io, minutes, seconds) - io.printf("%i:%02i", minutes, seconds) - end - - # Formats for hours. - private def format_hours(io, hours, minutes, seconds) - io.printf("%i:%02i:%02i", hours, minutes, seconds) - end - - # Formats for days. - private def format_days(io, days, hours, minutes, seconds) - io.printf("%i days %i:%02i:%02i", days, hours, minutes, seconds) - end - end -end diff --git a/src/spectator/formatting/components/stats.cr b/src/spectator/formatting/components/stats.cr deleted file mode 100644 index 47e1063..0000000 --- a/src/spectator/formatting/components/stats.cr +++ /dev/null @@ -1,38 +0,0 @@ -require "colorize" -require "../../report" -require "./runtime" -require "./totals" - -module Spectator::Formatting::Components - # Statistics information displayed at the end of a run. - struct Stats - # Creates the component with stats from *report*. - def initialize(@report : Report) - end - - # Displays the stats. - def to_s(io : IO) : Nil - runtime(io) - totals(io) - if seed = @report.random_seed? - random(io, seed) - end - end - - # Displays the time it took to run the suite. - private def runtime(io) - io << "Finished in " - io.puts Runtime.new(@report.runtime) - end - - # Displays the counts for each type of result. - private def totals(io) - io.puts Totals.colorize(@report.counts) - end - - # Displays the random seed. - private def random(io, seed) - io.puts "Randomized with seed: #{seed}".colorize(:cyan) - end - end -end diff --git a/src/spectator/formatting/components/tap_profile.cr b/src/spectator/formatting/components/tap_profile.cr deleted file mode 100644 index 4154e07..0000000 --- a/src/spectator/formatting/components/tap_profile.cr +++ /dev/null @@ -1,42 +0,0 @@ -require "../../profile" -require "./runtime" - -module Spectator::Formatting::Components - # Displays profiling information for slow examples in a TAP format. - # Produces output similar to `Profile`, but formatted for TAP. - struct TAPProfile - # Creates the component with the specified *profile*. - def initialize(@profile : Spectator::Profile) - end - - # Produces the output containing the profiling information. - def to_s(io : IO) : Nil - io << "# Top " - io << @profile.size - io << " slowest examples (" - io << Runtime.new(@profile.time) - io << ", " - io << @profile.percentage.round(2) - io.puts "% of total time):" - - @profile.each do |example| - example_profile(io, example) - end - end - - # Writes a single example's timing to the output. - private def example_profile(io, example) - io << "# " - io.puts example - io << "# " - io << Runtime.new(example.result.elapsed) - - if location = example.location? - io << ' ' - io.puts location - else - io.puts - end - end - end -end diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr deleted file mode 100644 index 4063cae..0000000 --- a/src/spectator/formatting/components/totals.cr +++ /dev/null @@ -1,46 +0,0 @@ -require "colorize" - -module Spectator::Formatting::Components - # Displays counts for each type of example result (pass, fail, error, pending). - struct Totals - # Creates the component with the specified counts. - def initialize(@examples : Int32, @failures : Int32, @errors : Int32, @pending : Int32) - end - - # Creates the component by pulling numbers from *counts*. - def initialize(counts) - @examples = counts.run - @failures = counts.fail - @errors = counts.error - @pending = counts.pending - end - - # Creates the component, but colors it whether there were pending or failed results. - # The component will be red if there were failures (or errors), - # yellow if there were pending/skipped tests, - # and green if everything passed. - def self.colorize(counts) - totals = new(counts) - if counts.fail > 0 - totals.colorize(:red) - elsif counts.pending > 0 - totals.colorize(:yellow) - else - totals.colorize(:green) - end - end - - # Writes the counts to the output. - def to_s(io : IO) : Nil - io << @examples << " examples, " << @failures << " failures" - - if @errors > 0 - io << " (" << @errors << " errors)" - end - - if @pending > 0 - io << ", " << @pending << " pending" - end - end - end -end diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr deleted file mode 100644 index 882e618..0000000 --- a/src/spectator/formatting/document_formatter.cr +++ /dev/null @@ -1,99 +0,0 @@ -require "../label" -require "./formatter" -require "./summary" - -module Spectator::Formatting - # Produces an indented document-style output. - # Each nested group of examples increases the indent. - # Example names are output in a color based on their result. - class DocumentFormatter < Formatter - include Summary - - # Indentation string. - private INDENT = " " - - # String used for groups and examples that don't have a name. - private NO_NAME = "" - - # Output stream to write results to. - private getter io - - @previous_hierarchy = [] of Label - - # Creates the formatter. - # By default, output is sent to STDOUT. - def initialize(@io : IO = STDOUT) - end - - # Invoked just before an example runs. - # Prints the example group hierarchy if it changed. - def example_started(notification) - hierarchy = group_hierarchy(notification.example) - tuple = hierarchy_diff(@previous_hierarchy, hierarchy) - print_sub_hierarchy(*tuple) - @previous_hierarchy = hierarchy - end - - # Invoked after an example completes successfully. - # Produces a successful example line. - def example_passed(notification) - name = (notification.example.name? || NO_NAME) - line(name.colorize(:green)) - end - - # Invoked after an example is skipped or marked as pending. - # Produces a pending example line. - def example_pending(notification) - name = (notification.example.name? || NO_NAME) - line(name.colorize(:yellow)) - end - - # Invoked after an example fails. - # Produces a failure example line. - def example_failed(notification) - name = (notification.example.name? || NO_NAME) - line(name.colorize(:red)) - end - - # Invoked after an example fails from an unexpected error. - # Produces a failure example line. - def example_error(notification) - example_failed(notification) - end - - # Produces a list of groups making up the hierarchy for an example. - private def group_hierarchy(example) - Array(Label).new.tap do |hierarchy| - group = example.group? - while group && (parent = group.group?) - hierarchy << (group.name? || NO_NAME) - group = parent - end - hierarchy.reverse! - end - end - - # Generates a difference between two hierarchies. - private def hierarchy_diff(first, second) - index = -1 - diff = second.skip_while do |group| - index += 1 - first.size > index && first[index] == group - end - {index, diff} - end - - # Displays an indented hierarchy starting partially into the whole hierarchy. - private def print_sub_hierarchy(start_index, hierarchy) - hierarchy.each_with_index(start_index) do |name, index| - line(name, index) - end - end - - # Displays an indented line of text. - private def line(text, level = @previous_hierarchy.size) - level.times { @io << INDENT } - @io.puts text - end - end -end diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr deleted file mode 100644 index d1e6956..0000000 --- a/src/spectator/formatting/formatter.cr +++ /dev/null @@ -1,128 +0,0 @@ -module Spectator::Formatting - # Base class and interface used to notify systems of events. - # This is typically used for producing output from test results, - # but can also be used to send data to external systems. - # - # All event methods are implemented as no-ops. - # To respond to an event, override its method. - # Every method receives a notification object containing information about the event. - # - # Methods are called in this order: - # 1. `#start` - # 2. `#example_started` - # 3. `#example_finished` - # 4. `#example_passed` - # 5. `#example_pending` - # 6. `#example_failed` - # 7. `#stop` - # 8. `#start_dump` - # 9. `#dump_pending` - # 10. `#dump_failures` - # 11. `#dump_profile` - # 12. `#dump_summary` - # 13. `#close` - # - # Only one of the `#example_passed`, `#example_pending`, or `#example_failed` methods - # will be called after `#example_finished`, depending on the outcome of the test. - # - # The "dump" methods are called after all tests that will run have run. - # They are provided summarized information. - abstract class Formatter - # This method is the first method to be invoked - # and will be called only once. - # It is called before any examples run. - # The *notification* will be a `StartNotification` type of object. - def start(_notification) - end - - # Invoked just before an example runs. - # This method is called once for every example. - # The *notification* will be an `ExampleNotification` type of object. - def example_started(_notification) - end - - # Invoked just after an example completes. - # This method is called once for every example. - # One of `#example_passed`, `#example_pending` or `#example_failed` - # will be called immediately after this method, depending on the example's result. - # The *notification* will be an `ExampleNotification` type of object. - def example_finished(_notification) - end - - # Invoked after an example completes successfully. - # This is called right after `#example_finished`. - # The *notification* will be an `ExampleNotification` type of object. - def example_passed(_notification) - end - - # Invoked after an example is skipped or marked as pending. - # This is called right after `#example_finished`. - # The *notification* will be an `ExampleNotification` type of object. - def example_pending(_notification) - end - - # Invoked after an example fails. - # This is called right after `#example_finished`. - # The *notification* will be an `ExampleNotification` type of object. - # - # NOTE: Errors are normally considered failures, - # however `#example_error` is called instead if one occurs in an example. - def example_failed(_notification) - end - - # Invoked after an example fails from an unexpected error. - # This is called right after `#example_finished`. - # The *notification* will be an `ExampleNotification` type of object. - def example_error(_notification) - end - - # Called whenever the example or framework produces a message. - # This is typically used for logging. - # The *notification* will be a `MessageNotification` type of object. - def message(_notification) - end - - # Invoked after all tests that will run have completed. - # When this method is called, it should be considered that the testing is done. - # Summary (dump) methods will be called after this. - def stop - end - - # Invoked after all examples finished. - # Indicates that summarized report data is about to be produced. - # This method is called after `#stop` and before `#dump_pending`. - def start_dump - end - - # Invoked after testing completes with a list of pending examples. - # This method will be called with an empty list if there were no pending (skipped) examples. - # Called after `#start_dump` and before `#dump_failures`. - # The *notification* will be an `ExampleSummaryNotification` type of object. - def dump_pending(_notification) - end - - # Invoked after testing completes with a list of failed examples. - # This method will be called with an empty list if there were no failures. - # Called after `#dump_pending` and before `#dump_summary`. - # The *notification* will be an `ExampleSummaryNotification` type of object. - def dump_failures(_notification) - end - - # Invoked after testing completes with profiling information. - # This method is only called if profiling is enabled. - # Called after `#dump_failures` and before `#dump_summary`. - def dump_profile(_notification) - end - - # Invoked after testing completes with summarized information from the test suite. - # Called after `#dump_profile` and before `#close`. - # The *notification* will be an `SummaryNotification` type of object. - def dump_summary(_notification) - end - - # Invoked at the end of the program. - # Allows the formatter to perform any cleanup and teardown. - def close - end - end -end diff --git a/src/spectator/formatting/html/body.ecr b/src/spectator/formatting/html/body.ecr deleted file mode 100644 index 9537921..0000000 --- a/src/spectator/formatting/html/body.ecr +++ /dev/null @@ -1,86 +0,0 @@ -
-

Test Results

- <% escape(totals(report)) %> - <% escape(runtime(report.runtime)) %> -
- -<%- if report.counts.fail > 0 -%> -

Failures (<%= report.counts.fail %>)

-
    - <%- report.failures.each do |example| -%> -
  1. - - <% escape(example) %> - -
  2. - <%- end -%> -
-<%- end -%> - -<%- if report.counts.pending > 0 -%> -

Pending (<%= report.counts.pending %>)

-
    - <%- report.pending.each do |example| -%> -
  1. - - <% escape(example) %> - -
  2. - <%- end -%> -
-<%- end -%> - -

Examples (<%= report.counts.total %>)

-
    - <%- report.examples.each do |example| -%> -
  • -

    <% escape(example) %>

    - <%= example.result %> - Took <% escape(runtime(example.result.elapsed)) %> - <% if location = example.location? %><% escape(location) %><% end %> - <% if result = example.result.as?(PendingResult) %>

    <% escape(result.reason) %>

    - - <%- elsif result = example.result.as?(ErrorResult) -%> -

    - <% escape(result.error.class) %> - <% escape(result.error.message) %> -

    - <%- if backtrace = result.error.backtrace? -%> -
    - <%- backtrace.each do |line| -%> - <% escape(line) %> - <%- end -%> -
    - <%- end -%> - - <%- elsif result = example.result.as?(FailResult) -%> -

    <% escape(result.error.message) %> - <%- end -%> - - <%- if example.result.expectations.empty? -%> - No expectations reported - <%- else -%> -

    Expectations

    -
      - <%- example.result.expectations.each do |expectation| -%> -
    1. "<% if location = expectation.location? %> title="<% escape(location) %>"<% end %>> - <% escape(expectation.description) %> - <%- if expectation.satisfied? -%> - pass - <%- else -%> - fail -

      <% escape(expectation.failure_message) %> -

      - <%- expectation.values.each do |key, value| -%> -
      <% escape(key) %>
      -
      <% escape(value) %>
      - <%- end -%> -
      - <%- end -%> -
    2. - <%- end -%> -
    - <%- end -%> -
  • - <%- end -%> -
diff --git a/src/spectator/formatting/html/foot.ecr b/src/spectator/formatting/html/foot.ecr deleted file mode 100644 index 308b1d0..0000000 --- a/src/spectator/formatting/html/foot.ecr +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/spectator/formatting/html/head.ecr b/src/spectator/formatting/html/head.ecr deleted file mode 100644 index f34fe42..0000000 --- a/src/spectator/formatting/html/head.ecr +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - - - - Test Results - - - - - diff --git a/src/spectator/formatting/html_formatter.cr b/src/spectator/formatting/html_formatter.cr deleted file mode 100644 index a4744c0..0000000 --- a/src/spectator/formatting/html_formatter.cr +++ /dev/null @@ -1,69 +0,0 @@ -require "ecr" -require "html" -require "./formatter" - -module Spectator::Formatting - # Produces an HTML document with results of the test suite. - class HTMLFormatter < Formatter - # Default HTML file name. - private OUTPUT_FILE = "output.html" - - # Output stream for the HTML file. - private getter! io : IO - - # Creates the formatter. - # The *output_path* can be a directory or path of an HTML file. - # If the former, then an "output.html" file will be generated in the specified directory. - def initialize(output_path = OUTPUT_FILE) - @output_path = if output_path.ends_with?(".html") - output_path - else - File.join(output_path, OUTPUT_FILE) - end - end - - # Prepares the formatter for writing. - def start(_notification) - @io = File.open(@output_path, "w") - ECR.embed(__DIR__ + "/html/head.ecr", io) - end - - # Invoked after testing completes with summarized information from the test suite. - # All results are gathered at the end, then the report is generated. - def dump_summary(notification) - report = notification.report # ameba:disable Lint/UselessAssign - ECR.embed(__DIR__ + "/html/body.ecr", io) - end - - # Invoked at the end of the program. - # Allows the formatter to perform any cleanup and teardown. - def close - ECR.embed(__DIR__ + "/html/foot.ecr", io) - io.flush - io.close - end - - private def escape(string) - HTML.escape(string.to_s, io) - end - - private def runtime(span) - Components::Runtime.new(span).to_s - end - - private def totals(report) - Components::Totals.new(report.counts) - end - - private def summary_result(report) - counts = report.counts - if counts.fail > 0 - "fail" - elsif counts.pending > 0 - "pending" - else - "pass" - end - end - end -end diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr deleted file mode 100644 index 90bac6a..0000000 --- a/src/spectator/formatting/json_formatter.cr +++ /dev/null @@ -1,66 +0,0 @@ -require "json" -require "./formatter" - -module Spectator::Formatting - # Produces a JSON document with results of the test suite. - class JSONFormatter < Formatter - # Creates the formatter. - # By default, output is sent to STDOUT. - def initialize(io = STDOUT) - @json = JSON::Builder.new(io) - end - - # Begins the JSON document output. - def start(_notification) - @json.start_document - @json.start_object - @json.field("version", Spectator::VERSION) - - # Start examples array. - @json.string("examples") - @json.start_array - end - - # Adds an object containing fields about the example. - def example_finished(notification) - notification.example.to_json(@json) - end - - # Marks the end of the examples array. - def stop - @json.end_array # Close examples array. - end - - # Adds the profiling information to the document. - def dump_profile(notification) - @json.field("profile") do - notification.profile.to_json(@json) - end - end - - # Adds the summary object to the document. - def dump_summary(notification) - report = notification.report - - @json.field("summary") do - @json.object do - @json.field("duration", report.runtime.total_seconds) - @json.field("example_count", report.counts.total) - @json.field("failure_count", report.counts.fail) - @json.field("error_count", report.counts.error) - @json.field("pending_count", report.counts.pending) - end - end - - totals = Components::Totals.new(report.counts) - @json.field("summary_line", totals.to_s) - end - - # Ends the JSON document and flushes output. - def close - @json.end_object - @json.end_document - @json.flush - end - end -end diff --git a/src/spectator/formatting/junit_formatter.cr b/src/spectator/formatting/junit_formatter.cr deleted file mode 100644 index a777371..0000000 --- a/src/spectator/formatting/junit_formatter.cr +++ /dev/null @@ -1,51 +0,0 @@ -require "xml" -require "./formatter" - -module Spectator::Formatting - # Produces a JUnit compatible XML file containing the test results. - class JUnitFormatter < Formatter - # Default XML file name. - private OUTPUT_FILE = "output.xml" - - # XML builder for the entire document. - private getter! xml : XML::Builder - - # Output stream for the XML file. - private getter! io : IO - - # Creates the formatter. - # The *output_path* can be a directory or path of an XML file. - # If the former, then an "output.xml" file will be generated in the specified directory. - def initialize(output_path = OUTPUT_FILE) - @output_path = if output_path.ends_with?(".xml") - output_path - else - File.join(output_path, OUTPUT_FILE) - end - end - - # Prepares the formatter for writing. - def start(_notification) - @io = io = File.open(@output_path, "w") - @xml = xml = XML::Builder.new(io) - xml.start_document("1.0", "UTF-8") - end - - # Invoked after testing completes with summarized information from the test suite. - # Unfortunately, the JUnit specification is not conducive to streaming data. - # All results are gathered at the end, then the report is generated. - def dump_summary(notification) - report = notification.report - root = Components::JUnit::Root.from_report(report) - root.to_xml(xml) - end - - # Invoked at the end of the program. - # Allows the formatter to perform any cleanup and teardown. - def close - xml.end_document - xml.flush - io.close - end - end -end diff --git a/src/spectator/formatting/notifications.cr b/src/spectator/formatting/notifications.cr deleted file mode 100644 index e789514..0000000 --- a/src/spectator/formatting/notifications.cr +++ /dev/null @@ -1,23 +0,0 @@ -require "../example" -require "../profile" -require "../report" - -module Spectator::Formatting - # Structure indicating the test suite has started. - record StartNotification, example_count : Int32 - - # Structure indicating an event occurred with an example. - record ExampleNotification, example : Example - - # Structure containing a subset of examples from the test suite. - record ExampleSummaryNotification, examples : Enumerable(Example) - - # Structure containing profiling information. - record ProfileNotification, profile : Profile - - # Structure containing summarized information from the outcome of the test suite. - record SummaryNotification, report : Report - - # Structure containing a debug or log message from the test suite. - record MessageNotification, message : String -end diff --git a/src/spectator/formatting/progress_formatter.cr b/src/spectator/formatting/progress_formatter.cr deleted file mode 100644 index 20975f4..0000000 --- a/src/spectator/formatting/progress_formatter.cr +++ /dev/null @@ -1,48 +0,0 @@ -require "colorize" -require "./formatter" -require "./summary" - -module Spectator::Formatting - # Output formatter that produces a single character for each test as it completes. - # A '.' indicates a pass, 'F' a failure, 'E' an error, and '*' a skipped or pending test. - class ProgressFormatter < Formatter - include Summary - - @pass_char : Colorize::Object(Char) = '.'.colorize(:green) - @fail_char : Colorize::Object(Char) = 'F'.colorize(:red) - @error_char : Colorize::Object(Char) = 'E'.colorize(:red) - @skip_char : Colorize::Object(Char) = '*'.colorize(:yellow) - - # Output stream to write results to. - private getter io - - # Creates the formatter. - def initialize(@io : IO = STDOUT) - end - - # Produces a pass character. - def example_passed(_notification) - @pass_char.to_s(@io) - end - - # Produces a fail character. - def example_failed(_notification) - @fail_char.to_s(@io) - end - - # Produces an error character. - def example_error(_notification) - @error_char.to_s(@io) - end - - # Produces a skip character. - def example_pending(_notification) - @skip_char.to_s(@io) - end - - # Produces a new line after the tests complete. - def stop - @io.puts - end - end -end diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr deleted file mode 100644 index b866307..0000000 --- a/src/spectator/formatting/summary.cr +++ /dev/null @@ -1,93 +0,0 @@ -require "../fail_result" -require "./components" - -module Spectator::Formatting - # Mix-in providing common output for summarized results. - # Implements the following methods: - # `Formatter#start_dump`, `Formatter#dump_pending`, `Formatter#dump_failures`, - # `Formatter#dump_summary`, and `Formatter#dump_profile`. - # Classes including this module must implement `#io`. - module Summary - # Stream to write results to. - private abstract def io - - def start_dump - io.puts - end - - # Invoked after testing completes with a list of pending examples. - # This method will be called with an empty list if there were no pending (skipped) examples. - # Called after `#start_dump` and before `#dump_failures`. - def dump_pending(notification) - return if (examples = notification.examples).empty? - - io.puts "Pending:" - io.puts - examples.each_with_index(1) do |example, index| - result = example.result.as(PendingResult) - io.puts Components::PendingResultBlock.new(example, index, result) - end - end - - # Invoked after testing completes with a list of failed examples. - # This method will be called with an empty list if there were no failures. - # Called after `#dump_pending` and before `#dump_summary`. - def dump_failures(notification) - return if (examples = notification.examples).empty? - - io.puts "Failures:" - io.puts - examples.each_with_index(1) do |example, index| - dump_failed_example(example, index) - end - end - - # Invoked after testing completes with profiling information. - # This method is only called if profiling is enabled. - # Called after `#dump_failures` and before `#dump_summary`. - def dump_profile(notification) - io.puts Components::Profile.new(notification.profile) - end - - # Invoked after testing completes with summarized information from the test suite. - # Called after `#dump_failures` and before `#dump_profile`. - def dump_summary(notification) - report = notification.report - io.puts Components::Stats.new(report) - - return if (failures = report.failures).empty? - - io.puts Components::FailureCommandList.new(failures) - end - - # Displays one or more blocks for a failed example. - # Each block is a failed expectation or error raised in the example. - private def dump_failed_example(example, index) - # Retrieve the ultimate reason for failing. - error = example.result.as?(FailResult).try(&.error) - - # Prevent displaying duplicated output from expectation. - # Display `ExampleFailed` but not `ExpectationFailed`. - error = nil if error.responds_to?(:expectation) - - # Gather all failed expectations. - failed_expectations = example.result.expectations.select(&.failed?) - block_count = failed_expectations.size - block_count += 1 if error # Add an extra block for final error if it's significant. - - # Don't use sub-index if there was only one problem. - if block_count == 1 - if error - io.puts Components::ErrorResultBlock.new(example, index, error) - else - io.puts Components::FailResultBlock.new(example, index, failed_expectations.first) - end - else - failed_expectations.each_with_index(1) do |expectation, subindex| - io.puts Components::FailResultBlock.new(example, index, expectation, subindex) - end - io.puts Components::ErrorResultBlock.new(example, index, error, block_count) if error - end - end - end -end diff --git a/src/spectator/formatting/tap_formatter.cr b/src/spectator/formatting/tap_formatter.cr deleted file mode 100644 index e5991a3..0000000 --- a/src/spectator/formatting/tap_formatter.cr +++ /dev/null @@ -1,71 +0,0 @@ -require "./formatter" - -module Spectator::Formatting - # Produces TAP output from test results. - # See: https://testanything.org/ - # Version 12 of the specification is used. - class TAPFormatter < Formatter - @counter = 0 - - # Creates the formatter. - def initialize(@io : IO = STDOUT) - end - - # Invoked when the test suite begins. - def start(notification) - @io << "1.." - @io.puts notification.example_count - end - - # Invoked just after an example completes. - def example_finished(_notification) - @counter += 1 - end - - # Invoked after an example completes successfully. - def example_passed(notification) - @io << "ok " << @counter << " - " - @io.puts notification.example - end - - # Invoked after an example is skipped or marked as pending. - def example_pending(notification) - # TODO: Skipped tests should report ok. - @io << "not ok " << @counter << " - " - @io << notification.example << " # TODO " - - # This should never be false. - if (result = notification.example.result).responds_to?(:reason) - @io.puts result.reason - end - end - - # Invoked after an example fails. - def example_failed(notification) - @io << "not ok " << @counter << " - " - @io.puts notification.example - end - - # Invoked after an example fails from an unexpected error. - def example_error(notification) - example_failed(notification) - end - - # Called whenever the example or framework produces a message. - # This is typically used for logging. - def message(notification) - @io << "# " - @io.puts notification.message - end - - # Invoked after testing completes with profiling information. - def dump_profile(notification) - @io << Components::TAPProfile.new(notification.profile) - end - - # Invoked after testing completes with summarized information from the test suite. - def dump_summary(notification) - @io.puts "Bail out!" if notification.report.counts.remaining? - end - end -end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr deleted file mode 100644 index 1f9fa09..0000000 --- a/src/spectator/harness.cr +++ /dev/null @@ -1,192 +0,0 @@ -require "./error_result" -require "./example_failed" -require "./example_pending" -require "./expectation" -require "./expectation_failed" -require "./multiple_expectations_failed" -require "./pass_result" -require "./result" - -module Spectator - # Helper class that acts as a gateway between test code and the framework. - # - # Test code should be wrapped with a call to `.run`. - # This class will catch all errors raised by the test code. - # Errors caused by failed assertions (`AssertionFailed`) are translated to failed results (`FailResult`). - # Errors not caused by assertions are translated to error results (`ErrorResult`). - # - # Every runnable example should invoke the test code by calling `.run`. - # This sets up the harness so that the test code can use it. - # The framework does the following: - # ``` - # result = Harness.run { run_example_code } - # # Do something with the result. - # ``` - # - # Then from the test code, the harness can be accessed via `.current` like so: - # ``` - # harness = ::Spectator::Harness.current - # # Do something with the harness. - # ``` - # - # Of course, the end-user shouldn't see this or work directly with the harness. - # Instead, methods the test calls can access it. - # For instance, an expectation reporting a result. - class Harness - Log = ::Spectator::Log.for(self) - - # Retrieves the harness for the current running example. - class_getter! current : self - - # Wraps an example with a harness and runs test code. - # A block provided to this method is considered to be the test code. - # The value of `.current` is set to the harness for the duration of the test. - # It will be reset after the test regardless of the outcome. - # The result of running the test code will be returned. - def self.run(&) : Result - with_harness do |harness| - harness.run { yield } - end - end - - # Instantiates a new harness and yields it. - # The `.current` harness is set to the new harness for the duration of the block. - # `.current` is reset to the previous value (probably nil) afterwards, even if the block raises. - # The result of the block is returned. - private def self.with_harness(&) - previous = @@current - begin - @@current = harness = new - yield harness - ensure - @@current = previous - end - end - - @deferred = Deque(->).new - @cleanup = Deque(->).new - @expectations = [] of Expectation - @aggregate : Array(Expectation)? = nil - - # Runs test code and produces a result based on the outcome. - # The test code should be called from within the block given to this method. - def run(&) : Result - elapsed, error = capture { yield } - elapsed2, error2 = capture { run_deferred } - run_cleanup - translate(elapsed + elapsed2, error || error2) - end - - def report(expectation : Expectation) : Bool - Log.debug { "Reporting expectation #{expectation}" } - @expectations << expectation - - # TODO: Move this out of harness, maybe to `Example`. - Example.current.name = expectation.description unless Example.current.name? - - if expectation.failed? - raise ExpectationFailed.new(expectation, expectation.failure_message) unless (aggregate = @aggregate) - aggregate << expectation - false - else - true - end - end - - # Stores a block of code to be executed later. - # All deferred blocks run just before the `#run` method completes. - def defer(&block) : Nil - @deferred << block - end - - # Stores a block of code to be executed at cleanup. - # Cleanup is run after everything else, even deferred blocks. - # Each cleanup step is wrapped in error handling so that one failure doesn't block the next ones. - def cleanup(&block) : Nil - @cleanup << block - end - - def aggregate_failures(label = nil, &) - previous = @aggregate - @aggregate = aggregate = [] of Expectation - begin - yield.tap do - # If there's an nested aggregate (for some reason), allow the top-level one to handle things. - check_aggregate(aggregate, label) unless previous - end - ensure - @aggregate = previous - end - end - - private def check_aggregate(aggregate, label) - failures = aggregate.select(&.failed?) - case failures.size - when 0 then return - when 1 - expectation = failures.first - raise ExpectationFailed.new(expectation, expectation.failure_message) - else - message = "Got #{failures.size} failures from failure aggregation block" - message += " \"#{label}\"" if label - raise MultipleExpectationsFailed.new(failures, message) - end - end - - # Yields to run the test code and returns information about the outcome. - # Returns a tuple with the elapsed time and an error if one occurred (otherwise nil). - private def capture(&) : Tuple(Time::Span, Exception?) - error = nil - elapsed = Time.measure do - error = catch { yield } - end - {elapsed, error} - end - - # Yields to run a block of code and captures exceptions. - # If the block of code raises an error, the error is caught and returned. - # If the block doesn't raise an error, then nil is returned. - private def catch(&) : Exception? - yield - rescue e - e - else - nil - end - - # Translates the outcome of running a test to a result. - # Takes the *elapsed* time and a possible *error* from the test. - # Returns a type of `Result`. - private def translate(elapsed, error) : Result - case error - when nil - PassResult.new(elapsed, @expectations) - when ExampleFailed - FailResult.new(elapsed, error, @expectations) - when ExamplePending - PendingResult.new(error.message || PendingResult::DEFAULT_REASON, error.location, elapsed, @expectations) - else - ErrorResult.new(elapsed, error, @expectations) - end - end - - # Runs all deferred blocks. - # This method executes code from tests and may raise an error. - # It should be wrapped in a call to `#capture`. - private def run_deferred - Log.debug { "Running deferred operations" } - @deferred.each(&.call) - end - - # Invokes all cleanup callbacks. - # Each callback is wrapped with error handling. - private def run_cleanup - Log.debug { "Running cleanup" } - @cleanup.each do |callback| - callback.call - rescue e - Log.error(exception: e) { "Encountered error during cleanup" } - end - end - end -end diff --git a/src/spectator/hooks.cr b/src/spectator/hooks.cr deleted file mode 100644 index f06b0e1..0000000 --- a/src/spectator/hooks.cr +++ /dev/null @@ -1,83 +0,0 @@ -module Spectator - # Mix-in for defining hook methods. - module Hooks - # Defines various methods for adding hooks of a specific type. - # - # The *declaration* defines the name and type of hook. - # It should be a type declaration in the form: `some_hook : ExampleHook`, - # where `some_hook` is the name of the hook, and `ExampleHook` is type type. - # - # A default order can be specified by *order*. - # The *order* argument must be *append* or *prepend*. - # This indicates the order hooks are added by default when called by client code. - # - # Multiple methods are generated. - # The primary methods will be named the same as the hook (from *declaration*). - # These take a pre-built hook instance, or arguments to pass to the hook type's initializer. - # The new hook is added a collection in the order specified by *order*. - # - # A private getter method is created so that the hooks can be accessed if needed. - # The getter method has `_hooks` appended to the hook name. - # For instance, if the *declaration* contains `important_thing`, then the getter is `important_thing_hooks`. - # - # Lastly, an optional block can be provided. - # If given, a protected method will be defined with the block's contents. - # This method typically operates on (calls) the hooks. - # The private getter method mentioned above can be used to access the hooks. - # Any block arguments will be used as argument in the method. - # The method name has the prefix `call_` followed by the hook name. - # - # ``` - # define_hook important_event : ImportantHook do |example| - # important_event_hooks.each &.call(example) - # end - # - # # ... - # - # important_event do |example| - # puts "An important event occurred for #{example}" - # end - # ``` - macro define_hook(declaration, order = :append, &block) - {% if order.id == :append.id - method = :push.id - elsif order.id == :prepend.id - method = :unshift.id - else - raise "Unknown hook order type - #{order}" - end %} - - # Retrieves all registered hooks for {{declaration.var}}. - protected getter {{declaration.var}}_hooks = Deque({{declaration.type}}).new - - # Registers a new "{{declaration.var}}" hook. - # The hook will be {{order.id}}ed to the list. - def {{declaration.var}}(hook : {{declaration.type}}) : Nil - @{{declaration.var}}_hooks.{{method}}(hook) - end - - # Registers a new "{{declaration.var}}" hook. - # The hook will be {{order.id}}ed to the list. - # A new hook will be created by passing args to `{{declaration.type}}.new`. - def {{declaration.var}}(*args, **kwargs) : Nil - hook = {{declaration.type}}.new(*args, **kwargs) - {{declaration.var}}(hook) - end - - # Registers a new "{{declaration.var}}" hook. - # The hook will be {{order.id}}ed to the list. - # A new hook will be created by passing args to `{{declaration.type}}.new`. - def {{declaration.var}}(*args, **kwargs, &block) : Nil - hook = {{declaration.type}}.new(*args, **kwargs, &block) - {{declaration.var}}(hook) - end - - {% if block %} - # Handles calling all "{{declaration.var}}" hooks. - protected def call_{{declaration.var}}({{block.args.splat}}) - {{block.body}} - end - {% end %} - end - end -end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr deleted file mode 100644 index 10c1698..0000000 --- a/src/spectator/includes.cr +++ /dev/null @@ -1,58 +0,0 @@ -# This file includes all source files *except* `should.cr`. -# The `should.cr` file contains the optional feature for using should-syntax. -# Since this is disabled by default, we don't include all files. -# Including all files with a wildcard would accidentally enable should-syntax. -# Unfortunately, that leads to the existence of this file to include everything but that file. - -require "./abstract_expression" -require "./anything" -require "./block" -require "./composite_node_filter" -require "./config" -require "./context" -require "./context_delegate" -require "./context_method" -require "./dsl" -require "./error_result" -require "./example_context_delegate" -require "./example_context_method" -require "./example" -require "./node_filter" -require "./example_group" -require "./example_group_hook" -require "./example_hook" -require "./example_iterator" -require "./example_procsy_hook" -require "./expectation" -require "./expectation_failed" -require "./expression" -require "./fail_result" -require "./formatting" -require "./harness" -require "./hooks" -require "./label" -require "./lazy" -require "./lazy_wrapper" -require "./line_node_filter" -require "./location" -require "./location_node_filter" -require "./matchers" -require "./metadata" -require "./mocks" -require "./name_node_filter" -require "./null_context" -require "./null_node_filter" -require "./pass_result" -require "./pending_result" -require "./profile" -require "./report" -require "./result" -require "./runner_events" -require "./runner" -require "./spec_builder" -require "./spec" -require "./system_exit" -require "./tag_node_filter" -require "./test_context" -require "./value" -require "./wrapper" diff --git a/src/spectator/iterative_example_group_builder.cr b/src/spectator/iterative_example_group_builder.cr deleted file mode 100644 index fe67c7a..0000000 --- a/src/spectator/iterative_example_group_builder.cr +++ /dev/null @@ -1,58 +0,0 @@ -require "./example_group" -require "./example_group_builder" -require "./example_group_iteration" -require "./location" -require "./metadata" - -module Spectator - # Progressively constructs an iterative example group. - # Hooks and builders for child nodes can be added over time to this builder. - # When done, call `#build` to produce an `ExampleGroup` with nested `ExampleGroupIteration` instances. - class IterativeExampleGroupBuilder(T) < ExampleGroupBuilder - # Creates the builder. - # Initially, the builder will have no children and no hooks. - # The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`. - # The *collection* is the set of items to create sub-nodes for. - # The *iterators* is a list of optional names given to items in the collection. - def initialize(@collection : Enumerable(T), name : String? = nil, @iterators : Array(String) = [] of String, - location : Location? = nil, metadata : Metadata? = nil) - super(name, location, metadata) - end - - # Constructs an iterative example group with previously defined attributes, children, and hooks. - # The *parent* is an already constructed example group to nest the new example group under. - # It can be nil if the new example group won't have a parent. - def build(parent = nil) - ExampleGroup.new(@name, @location, parent, @metadata).tap do |group| - # Hooks are applied once to the outer group, - # instead of multiple times for each inner group (iteration). - apply_hooks(group) - - @collection.each do |item| - ExampleGroupIteration.new(item, iteration_name(item), @location, group).tap do |iteration| - @children.each(&.build(iteration)) - end - end - end - end - - # Constructs the name of an example group iteration. - private def iteration_name(item) - if item.is_a?(Tuple) && @iterators.size > 1 - item.zip?(@iterators).map do |(subitem, iterator)| - if iterator - "#{iterator}: #{subitem.inspect}" - else - subitem.inspect - end - end.join("; ") - else - if iterator = @iterators.first? - "#{iterator}: #{item.inspect}" - else - item.inspect - end - end - end - end -end diff --git a/src/spectator/label.cr b/src/spectator/label.cr deleted file mode 100644 index 9a09f26..0000000 --- a/src/spectator/label.cr +++ /dev/null @@ -1,7 +0,0 @@ -module Spectator - # Identifier used in the spec. - # Significant to the user. - # When a label is a symbol, then it is referencing a type or method. - # A label is nil when one can't be provided or captured. - alias Label = String | Symbol | Nil -end diff --git a/src/spectator/lazy.cr b/src/spectator/lazy.cr deleted file mode 100644 index 7a477b3..0000000 --- a/src/spectator/lazy.cr +++ /dev/null @@ -1,34 +0,0 @@ -module Spectator - # Lazily stores a value. - struct Lazy(T) - @value : Value(T)? - - # Retrieves the value, if it was previously fetched. - # On the first invocation of this method, it will yield. - # The block should return the value to store. - # Subsequent calls will return the same value and not yield. - def get(&block : -> T) - if (existing = @value) - existing.get - else - yield.tap do |value| - @value = Value.new(value) - end - end - end - - # Wrapper for a value. - # This is intended to be used as a union with nil. - # It allows storing (caching) a nillable value. - private struct Value(T) - # Creates the wrapper. - def initialize(@value : T) - end - - # Retrieves the value. - def get : T - @value - end - end - end -end diff --git a/src/spectator/lazy_wrapper.cr b/src/spectator/lazy_wrapper.cr deleted file mode 100644 index 9bc7125..0000000 --- a/src/spectator/lazy_wrapper.cr +++ /dev/null @@ -1,36 +0,0 @@ -require "./lazy" -require "./wrapper" - -module Spectator - # Lazily stores a value of any type. - # Combines `Lazy` and `Wrapper`. - # - # Contains no value until the first call to `#get` is made. - # Any type can be stored in this wrapper. - # However, the type must always be known when retrieving it via `#get`. - # The type is inferred from the block, and all blocks must return the same type. - # Because of this, it is recommended to only have `#get` called in one location. - # - # This type is expected to be used like so: - # ``` - # @wrapper : LazyWrapper - # - # # ... - # - # def lazy_load - # @wrapper.get { some_expensive_operation } - # end - # ``` - struct LazyWrapper - @lazy = Lazy(Wrapper).new - - # Retrieves the value, if it was previously fetched. - # On the first invocation of this method, it will yield. - # The block should return the value to store. - # Subsequent calls will return the same value and not yield. - def get(& : -> T) : T forall T - wrapper = @lazy.get { Wrapper.new(yield) } - wrapper.get { yield } - end - end -end diff --git a/src/spectator/line_node_filter.cr b/src/spectator/line_node_filter.cr deleted file mode 100644 index 52213e4..0000000 --- a/src/spectator/line_node_filter.cr +++ /dev/null @@ -1,19 +0,0 @@ -require "./node_filter" - -module Spectator - # Filter that matches nodes on a given line. - class LineNodeFilter < NodeFilter - # Creates the node filter. - def initialize(@line : Int32) - end - - # Checks whether the node satisfies the filter. - def includes?(node) : Bool - return false unless location = node.location? - - start_line = location.line - end_line = location.end_line - (start_line..end_line).covers?(@line) - end - end -end diff --git a/src/spectator/location.cr b/src/spectator/location.cr deleted file mode 100644 index 7b06eb9..0000000 --- a/src/spectator/location.cr +++ /dev/null @@ -1,66 +0,0 @@ -module Spectator - # Defines the file and line number a piece of code originated from. - struct Location - # Absolute file path. - getter file : String - - # Starting line number in the file. - getter line : Int32 - - # Ending line number in the file. - getter end_line : Int32 - - # Creates the location. - def initialize(@file, @line, end_line = nil) - # if an end line is not provided, - # make the end line the same as the start line - @end_line = end_line || @line - end - - # Parses a location from a string. - # The *string* should be in the form: - # ```text - # FILE:LINE - # ``` - # This matches the output of the `#to_s` method. - def self.parse(string) - # Make sure to handle multiple colons. - # If this ran on Windows, there's a possibility of a colon in the path. - # The last element should always be the line number. - parts = string.split(':') - path = parts[0...-1].join(':') - line = parts.last - file = File.expand_path(path) - self.new(file, line.to_i) - end - - # The relative path to the file from the current directory. - # If the file isn't in the current directory or a sub-directory, - # then the absolute path is provided. - def path - # Add the path separator here. - # Otherwise, things like: - # `spectator/foo.cr` and `spectator.cr` overlap. - # It also makes the substring easier. - cwd = Dir.current + File::SEPARATOR - if file.starts_with?(cwd) - # Relative to the current directory. - # Trim the current directory path from the beginning. - file[cwd.size..-1] - else - # Not trivial to find the file. - # Return the absolute path. - file - end - end - - # String representation of the location. - # This is formatted as: - # ```text - # FILE:LINE - # ``` - def to_s(io : IO) : Nil - io << path << ':' << line - end - end -end diff --git a/src/spectator/location_node_filter.cr b/src/spectator/location_node_filter.cr deleted file mode 100644 index 3774db2..0000000 --- a/src/spectator/location_node_filter.cr +++ /dev/null @@ -1,17 +0,0 @@ -require "./location" -require "./node_filter" - -module Spectator - # Filter that matches nodes in a given file and line. - class LocationNodeFilter < NodeFilter - # Creates the filter. - # The *location* indicates which file and line the node must contain. - def initialize(@location : Location) - end - - # Checks whether the node satisfies the filter. - def includes?(node) : Bool - @location === node.location? - end - end -end diff --git a/src/spectator/matchers.cr b/src/spectator/matchers.cr deleted file mode 100644 index 76f9035..0000000 --- a/src/spectator/matchers.cr +++ /dev/null @@ -1,8 +0,0 @@ -require "./matchers/*" - -module Spectator - # Evaluates conditions on values and objects - # to determine whether they satisfy test criteria. - module Matchers - end -end diff --git a/src/spectator/matchers/all_matcher.cr b/src/spectator/matchers/all_matcher.cr deleted file mode 100644 index 404b66d..0000000 --- a/src/spectator/matchers/all_matcher.cr +++ /dev/null @@ -1,57 +0,0 @@ -require "../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 : String - "all #{matcher.description}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - found = values(actual).each do |element| - match_data = matcher.match(element) - break match_data unless match_data.matched? - end - found || SuccessfulMatchData.new(match_data_description(actual)) - 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 : Expression(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 values(actual) - label_prefix = actual.label - actual.value.map_with_index do |value, index| - label = "#{label_prefix}[#{index}]" - Value.new(value, label) - end - end - end -end diff --git a/src/spectator/matchers/array_matcher.cr b/src/spectator/matchers/array_matcher.cr deleted file mode 100644 index eea9353..0000000 --- a/src/spectator/matchers/array_matcher.cr +++ /dev/null @@ -1,122 +0,0 @@ -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" -require "./unordered_array_matcher" - -module Spectator::Matchers - # Matcher for checking that the contents of one array (or similar type) - # has the exact same contents as another but may be in any order. - struct ArrayMatcher(ExpectedType) < Matcher - # Expected value and label. - private getter expected - - # Creates the matcher with an expected value. - def initialize(@expected : Value(Array(ExpectedType))) - 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 : String - "contains exactly #{expected.label}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:to_a) - - actual_elements = actual_value.to_a - expected_elements = expected.value - missing, extra = compare_arrays(expected_elements, actual_elements) - - if missing.empty? && extra.empty? - # Contents are identical. - SuccessfulMatchData.new(match_data_description(actual)) - else - # Content differs. - FailedMatchData.new(match_data_description(actual), "#{actual.label} does not contain exactly #{expected.label}", - expected: expected_elements.inspect, - actual: actual_elements.inspect, - missing: missing.empty? ? "None" : missing.inspect, - extra: extra.empty? ? "None" : extra.inspect - ) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:to_a) - - actual_elements = actual_value.to_a - expected_elements = expected.value - missing, extra = compare_arrays(expected_elements, actual_elements) - - if missing.empty? && extra.empty? - # Contents are identical. - FailedMatchData.new(match_data_description(actual), "#{actual.label} contains exactly #{expected.label}", - expected: "Not #{expected_elements.inspect}", - actual: actual_elements.inspect - ) - else - # Content differs. - SuccessfulMatchData.new(match_data_description(actual)) - end - end - - # Ensures the arrays elements are compared in order. - # This is the default behavior for the matcher. - def in_order - self - end - - # Specifies that the arrays elements can be compared in any order. - # The elements can be in a different order, but both arrays must have the same elements. - def in_any_order - UnorderedArrayMatcher.new(expected) - end - - # Compares two arrays to determine whether they contain the same elements, but in any order. - # A tuple of two arrays is returned. - # The first array is the missing elements (present in expected, missing in actual). - # The second array array is the extra elements (not present in expected, present in actual). - private def compare_arrays(expected_elements, actual_elements) - # Produce hashes where the array elements are the keys, and the values are the number of occurrences. - expected_hash = expected_elements.group_by(&.itself).map { |k, v| {k, v.size} }.to_h - actual_hash = actual_elements.group_by(&.itself).map { |k, v| {k, v.size} }.to_h - - { - hash_count_difference(expected_hash, actual_hash), - hash_count_difference(actual_hash, expected_hash), - } - end - - # Expects two hashes, with values as counts for keys. - # Produces an array of differences with elements repeated if needed. - private def hash_count_difference(first, second) - # Subtract the number of occurrences from the other array. - # A duplicate hash is used here because the original can't be modified, - # since it there's a two-way comparison. - # - # Then reject elements that have zero (or less) occurrences. - # Lastly, expand to the correct number of elements. - first.map do |element, count| - if second_count = second[element]? - {element, count - second_count} - else - {element, count} - end - end.reject do |(_, count)| - count <= 0 - end.flat_map do |(element, count)| - Array.new(count, element) - end - end - - private def unexpected(value, label) - raise "#{label} is not a collection (must respond to `#to_a`). #{label}: #{value.inspect}" - end - end -end diff --git a/src/spectator/matchers/attributes_matcher.cr b/src/spectator/matchers/attributes_matcher.cr deleted file mode 100644 index c66ecc4..0000000 --- a/src/spectator/matchers/attributes_matcher.cr +++ /dev/null @@ -1,105 +0,0 @@ -require "../value" -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" - -module Spectator::Matchers - # Matcher that tests that multiple attributes match specified conditions. - # The attributes are tested with the === operator. - # The `ExpectedType` type param should be a `NamedTuple`. - # Each key in the tuple is the attribute/method name, - # and the corresponding value is the expected value to match against. - struct AttributesMatcher(ExpectedType) < Matcher - # Stand-in for undefined methods on types. - private module Undefined - extend self - - # Text displayed when a method is undefined. - def inspect(io : IO) : Nil - io << "" - end - end - - # Expected value and label. - private getter expected - - # Creates the matcher with an expected value. - def initialize(@expected : Value(ExpectedType)) - 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 : String - "has attributes #{expected.label}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - snapshot = snapshot_values(actual.value) - if match?(snapshot) - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} does not have attributes #{expected.label}", values(snapshot).to_a) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - snapshot = snapshot_values(actual.value) - if match?(snapshot) - FailedMatchData.new(match_data_description(actual), "#{actual.label} has attributes #{expected.label}", negated_values(snapshot).to_a) - else - SuccessfulMatchData.new(match_data_description(actual)) - end - end - - # Captures all of the actual values. - # A `NamedTuple` is returned, with each key being the attribute. - private def snapshot_values(object) - {% begin %} - { - {% for attribute in ExpectedType.keys %} - {{attribute}}: object.responds_to?({{attribute.symbolize}}) ? object.{{attribute}} : Undefined, - {% end %} - } - {% end %} - end - - # Checks if all attributes from the snapshot of them are satisfied. - private def match?(snapshot) - # Test that every attribute has the expected value. - {% for attribute in ExpectedType.keys %} - return false unless expected.value[{{attribute.symbolize}}] === snapshot[{{attribute.symbolize}}] - {% end %} - - # At this point, none of the checks failed, so the match was successful. - true - end - - # Produces the tuple for the failed match data from a snapshot of the attributes. - private def values(snapshot) - {% begin %} - { - {% for attribute in ExpectedType.keys %} - {{"expected " + attribute.stringify}}: expected.value[{{attribute.symbolize}}].inspect, - {{"actual " + attribute.stringify}}: snapshot[{{attribute.symbolize}}].inspect, - {% end %} - } - {% end %} - end - - # Produces the tuple for the failed negated match data from a snapshot of the attributes. - private def negated_values(snapshot) - {% begin %} - { - {% for attribute in ExpectedType.keys %} - {{"expected " + attribute.stringify}}: "Not #{expected.value[{{attribute.symbolize}}].inspect}", - {{"actual " + attribute.stringify}}: snapshot[{{attribute.symbolize}}].inspect, - {% end %} - } - {% end %} - end - end -end diff --git a/src/spectator/matchers/case_matcher.cr b/src/spectator/matchers/case_matcher.cr deleted file mode 100644 index 7c59a54..0000000 --- a/src/spectator/matchers/case_matcher.cr +++ /dev/null @@ -1,46 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Common matcher that tests whether two values semantically equal each other. - # The values are compared with the === operator. - struct CaseMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "matches #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - expected.value === actual.value - end - - # Overload that takes a regex so that the operands are flipped. - # This mimics RSpec's behavior. - private def match?(actual : Expression(Regex)) : Bool forall T - actual.value === expected.value - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} does not match #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} matched #{expected.label}" - end - end -end diff --git a/src/spectator/matchers/change_exact_matcher.cr b/src/spectator/matchers/change_exact_matcher.cr deleted file mode 100644 index 09d1a83..0000000 --- a/src/spectator/matchers/change_exact_matcher.cr +++ /dev/null @@ -1,87 +0,0 @@ -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" - -module Spectator::Matchers - # Matcher that tests whether an expression changed from and to specific values. - struct ChangeExactMatcher(ExpressionType, FromType, ToType) < Matcher - # The expression that is expected to (not) change. - private getter expression - - # The expected value of the expression before the change. - private getter expected_before - - # The expected value of the expression after the change. - private getter expected_after - - # Creates a new change matcher. - def initialize(@expression : Block(ExpressionType), @expected_before : FromType, @expected_after : ToType) - 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 : String - "changes #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - before = expression.value - before_inspect = before.inspect - - if expected_before == before - actual.value # Trigger block that might cause a change. - after = expression.value - after_inspect = after.inspect - - if expected_after == after - SuccessfulMatchData.new(match_data_description(actual)) - elsif before == after - FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}", - before: before_inspect, - after: after_inspect - ) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} to #{expected_after.inspect}", - before: before_inspect, - after: after_inspect, - expected: expected_after.inspect - ) - end - else - FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected_before.inspect}", - expected: expected_before.inspect, - actual: before_inspect, - ) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - before = expression.value - before_inspect = before.inspect - - if expected_before == before - actual.value # Trigger block that might cause a change. - after = expression.value - after_inspect = after.inspect - - if expected_after == after - FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}", - before: before_inspect, - after: after_inspect - ) - else - SuccessfulMatchData.new(match_data_description(actual)) - end - else - FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected_before.inspect}", - expected: expected_before.inspect, - actual: before_inspect, - ) - end - end - end -end diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr deleted file mode 100644 index 0ae3d13..0000000 --- a/src/spectator/matchers/change_from_matcher.cr +++ /dev/null @@ -1,91 +0,0 @@ -require "./change_exact_matcher" -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" - -module Spectator::Matchers - # Matcher that tests whether an expression changed from a specific value. - struct ChangeFromMatcher(ExpressionType, FromType) < Matcher - # The expression that is expected to (not) change. - private getter expression - - # The expected value of the expression before the change. - private getter expected - - # Creates a new change matcher. - def initialize(@expression : Block(ExpressionType), @expected : FromType) - 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 : String - "changes #{expression.label} from #{expected}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - before = expression.value - before_inspect = before.inspect - - if expected != before - return FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected}", - expected: expected.inspect, - actual: before_inspect, - ) - end - - actual.value # Trigger block that might change the expression. - after = expression.value - after_inspect = after.inspect - - if expected == after - FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} from #{expected}", - before: before_inspect, - after: after_inspect, - expected: "Not #{expected.inspect}" - ) - else - SuccessfulMatchData.new(match_data_description(actual)) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - before = expression.value - before_inspect = before.inspect - - if expected != before - return FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected}", - expected: expected.inspect, - actual: before_inspect - ) - end - - actual.value # Trigger block that might change the expression. - after = expression.value - after_inspect = after.inspect - - if expected == after - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label} from #{expected}", - before: before_inspect, - after: after_inspect, - expected: expected.inspect - ) - end - end - - # Specifies what the resulting value of the expression must be. - def to(value) - ChangeExactMatcher.new(@expression, @expected, value) - end - - # Specifies what the resulting value of the expression should change by. - def by(amount) - ChangeExactMatcher.new(@expression, @expected, @expected + value) - end - end -end diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr deleted file mode 100644 index 3be8111..0000000 --- a/src/spectator/matchers/change_matcher.cr +++ /dev/null @@ -1,85 +0,0 @@ -require "./change_from_matcher" -require "./change_to_matcher" -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" - -module Spectator::Matchers - # Matcher that tests whether an expression changed. - struct ChangeMatcher(ExpressionType) < Matcher - # The expression that is expected to (not) change. - private getter expression - - # Creates a new change matcher. - def initialize(@expression : Block(ExpressionType)) - 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 : String - "changes #{expression.label}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - before, after = change(actual) - if before == after - FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}", - before: before.inspect, - after: after.inspect - ) - else - SuccessfulMatchData.new(match_data_description(actual)) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - before, after = change(actual) - if before == after - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label}", - before: before.inspect, - after: after.inspect - ) - end - end - - # Specifies what the initial value of the expression must be. - def from(value) - ChangeFromMatcher.new(@expression, value) - end - - # Specifies what the resulting value of the expression must be. - def to(value) - ChangeToMatcher.new(@expression, value) - end - - # Specifies that t he resulting value must be some amount different. - def by(amount) - ChangeRelativeMatcher.new(@expression, "by #{amount}") { |before, after| amount == after - before } - end - - # Specifies that the resulting value must be at least some amount different. - def by_at_least(minimum) - ChangeRelativeMatcher.new(@expression, "by at least #{minimum}") { |before, after| minimum <= after - before } - end - - # Specifies that the resulting value must be at most some amount different. - def by_at_most(maximum) - ChangeRelativeMatcher.new(@expression, "by at most #{maximum}") { |before, after| maximum >= after - before } - end - - # Performs the change and reports the before and after values. - private def change(actual) - before = expression.value # Retrieve the expression's initial value. - actual.value # Invoke action that might change the expression's value. - after = expression.value # Retrieve the expression's value again. - - {before, after} - end - end -end diff --git a/src/spectator/matchers/change_relative_matcher.cr b/src/spectator/matchers/change_relative_matcher.cr deleted file mode 100644 index 8511dd4..0000000 --- a/src/spectator/matchers/change_relative_matcher.cr +++ /dev/null @@ -1,57 +0,0 @@ -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" - -module Spectator::Matchers - # Matcher that tests whether an expression changed by an amount. - struct ChangeRelativeMatcher(ExpressionType) < Matcher - # The expression that is expected to (not) change. - private getter expression - - # Creates a new change matcher. - def initialize(@expression : Block(ExpressionType), @relativity : String, - &evaluator : ExpressionType, ExpressionType -> Bool) - @evaluator = evaluator - 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 : String - "changes #{expression.label} #{@relativity}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - before, after = change(actual) - if before == after - FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}", - before: before.inspect, - after: after.inspect - ) - elsif @evaluator.call(before, after) - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} #{@relativity}", - before: before.inspect, - after: after.inspect - ) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - {% raise "The `expect { }.to_not change { }.by_...()` syntax is not supported (ambiguous)." %} - end - - # Performs the change and reports the before and after values. - private def change(actual) - before = expression.value # Retrieve the expression's initial value. - actual.value # Invoke action that might change the expression's value. - after = expression.value # Retrieve the expression's value again. - - {before, after} - end - end -end diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr deleted file mode 100644 index 884b4b0..0000000 --- a/src/spectator/matchers/change_to_matcher.cr +++ /dev/null @@ -1,76 +0,0 @@ -require "./change_exact_matcher" -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" - -module Spectator::Matchers - # Matcher that tests whether an expression changed to a specific value. - struct ChangeToMatcher(ExpressionType, ToType) < Matcher - # The expression that is expected to (not) change. - private getter expression - - # The expected value of the expression after the change. - private getter expected - - # Creates a new change matcher. - def initialize(@expression : Block(ExpressionType), @expected : ToType) - 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 : String - "changes #{expression.label} to #{expected}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - before = expression.value - before_inspect = before.inspect - - if expected == before - return FailedMatchData.new(match_data_description(actual), "#{expression.label} was already #{expected}", - before: before_inspect, - expected: "Not #{expected.inspect}" - ) - end - - actual.value # Trigger block that could change the expression. - after = expression.value - after_inspect = after.inspect - - if expected == after - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} to #{expected}", - before: before_inspect, - after: after_inspect, - expected: expected.inspect - ) - end - 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. - # "The action does not change the expression to some value." - # Is it a failure if the value is not changed, - # but it is the expected value? - # - # RSpec doesn't support this syntax either. - def negated_match(actual : Expression(T)) : MatchData forall T - {% raise "The `expect { }.to_not change { }.to()` syntax is not supported (ambiguous)." %} - end - - # Specifies what the initial value of the expression must be. - def from(value) - ChangeExactMatcher.new(@expression, value, @expected) - end - - # Specifies how much the initial value should change by. - def by(amount) - ChangeExactMatcher.new(@expression, @expected - amount, @expected) - end - end -end diff --git a/src/spectator/matchers/collection_matcher.cr b/src/spectator/matchers/collection_matcher.cr deleted file mode 100644 index 2b7c028..0000000 --- a/src/spectator/matchers/collection_matcher.cr +++ /dev/null @@ -1,62 +0,0 @@ -require "../value" -require "./range_matcher" -require "./value_matcher" - -module Spectator::Matchers - # Matcher for checking that a value is in a collection of other values. - struct CollectionMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "is in #{expected.label}" - end - - # 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) - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is not in #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is in #{expected.label}" - end - - # Creates a new range matcher with bounds based off of *center*. - # - # This method expects that the original matcher was created with a "difference" value. - # That is: - # ``` - # CollectionMatcher.new(diff).of(center) - # ``` - # This implies that the `#match` method would not work on the original matcher. - # - # The new range will be centered at *center* - # and have upper and lower bounds equal to *center* plus and minus diff. - # The range will be inclusive. - def of(center) - diff = @expected.value - lower = center - diff - upper = center + diff - range = Range.new(lower, upper) - value = Value.new(range, "#{center} ± #{expected.label}") - RangeMatcher.new(value) - end - end -end diff --git a/src/spectator/matchers/compiled_type_matcher.cr b/src/spectator/matchers/compiled_type_matcher.cr deleted file mode 100644 index d185aff..0000000 --- a/src/spectator/matchers/compiled_type_matcher.cr +++ /dev/null @@ -1,59 +0,0 @@ -require "./matcher" - -module Spectator::Matchers - # Matcher that tests a value is of a specified type at compile time. - # The values are compared with the `typeof` method. - # This can be used to inspect the inferred type of methods and variables. - struct CompiledTypeMatcher(Expected) < StandardMatcher - # 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 : String - "compiles as #{Expected}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - Expected == typeof(actual.value) - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} does not compile as #{Expected}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} compiles as #{Expected}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: Expected.to_s, - actual: typeof(actual.value).inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: "Not #{Expected}", - actual: typeof(actual.value).inspect, - } - end - end -end diff --git a/src/spectator/matchers/contain_matcher.cr b/src/spectator/matchers/contain_matcher.cr deleted file mode 100644 index 7ebe221..0000000 --- a/src/spectator/matchers/contain_matcher.cr +++ /dev/null @@ -1,69 +0,0 @@ -require "./matcher" - -module Spectator::Matchers - # Matcher that tests whether a value, such as a `String` or `Array`, contains one or more values. - # The values are checked with the `includes?` method. - struct ContainMatcher(ExpectedType) < Matcher - # Expected value and label. - private getter expected - - # Creates the matcher with an expected value. - def initialize(@expected : Value(ExpectedType)) - 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 : String - "contains #{expected.label}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:includes?) - - missing = expected.value.reject do |item| - actual_value.includes?(item) - end - - if missing.empty? - # Contents are present. - SuccessfulMatchData.new(match_data_description(actual)) - else - # Content is missing. - FailedMatchData.new(match_data_description(actual), "#{actual.label} does not contain #{expected.label}", - expected: expected.value.inspect, - actual: actual_value.inspect, - missing: missing.inspect, - ) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:includes?) - - satisfied = expected.value.any? do |item| - actual_value.includes?(item) - end - - if satisfied - # Contents are present. - FailedMatchData.new(match_data_description(actual), "#{actual.label} contains #{expected.label}", - expected: "Not #{expected.value.inspect}", - actual: actual_value.inspect - ) - else - # Content is missing. - SuccessfulMatchData.new(match_data_description(actual)) - end - end - - private def unexpected(value, label) - raise "#{label} is not a collection (must respond to `#includes?`). #{label}: #{value.inspect}" - end - end -end diff --git a/src/spectator/matchers/empty_matcher.cr b/src/spectator/matchers/empty_matcher.cr deleted file mode 100644 index 7ed61ed..0000000 --- a/src/spectator/matchers/empty_matcher.cr +++ /dev/null @@ -1,47 +0,0 @@ -require "./matcher" - -module Spectator::Matchers - # Matcher that tests whether a collection is empty. - # The values are checked with the `empty?` method. - struct EmptyMatcher < StandardMatcher - # 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 : String - "is empty" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:empty?) - - actual_value.empty? - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is not empty" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is empty" - end - - private def unexpected(value, label) - raise "#{label} is not a collection (must respond to `#empty?`). #{label}: #{value.inspect}" - end - end -end diff --git a/src/spectator/matchers/end_with_matcher.cr b/src/spectator/matchers/end_with_matcher.cr deleted file mode 100644 index 039d182..0000000 --- a/src/spectator/matchers/end_with_matcher.cr +++ /dev/null @@ -1,105 +0,0 @@ -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" - -module Spectator::Matchers - # Matcher that tests whether a value, such as a `String` or `Array`, ends with a value. - # The `ends_with?` method is used if it's defined on the actual type. - # Otherwise, it is treated as an `Indexable` and the `last` value is compared against. - struct EndWithMatcher(ExpectedType) < Matcher - # Expected value and label. - private getter expected - - # Creates the matcher with an expected value. - def initialize(@expected : Value(ExpectedType)) - 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 : String - "ends with #{expected.label}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - value = actual.value - if value.is_a?(String) || value.responds_to?(:ends_with?) - match_ends_with(value, actual.label) - else - match_last(value, actual.label) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - value = actual.value - if value.is_a?(String) || value.responds_to?(:ends_with?) - negated_match_ends_with(value, actual.label) - else - negated_match_last(value, actual.label) - end - end - - # Checks whether the actual value ends with the expected value. - # This method expects (and uses) the `#ends_with?` method on the value. - private def match_ends_with(actual_value, actual_label) - if actual_value.ends_with?(expected.value) - SuccessfulMatchData.new(match_data_description(actual_label)) - else - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not end with #{expected.label} (using #ends_with?)", - expected: expected.value.inspect, - actual: actual_value.inspect - ) - end - end - - # Checks whether the last element of the value is the expected value. - # This method expects that the actual value is a set (enumerable). - private def match_last(actual_value, actual_label) - list = actual_value.to_a - last = list.last - - if expected.value === last - SuccessfulMatchData.new(match_data_description(actual_label)) - else - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not end with #{expected.label} (using expected === last)", - expected: expected.value.inspect, - actual: last.inspect, - list: list.inspect - ) - end - end - - # Checks whether the actual value does not end with the expected value. - # This method expects (and uses) the `#ends_with?` method on the value. - private def negated_match_ends_with(actual_value, actual_label) - if actual_value.ends_with?(expected.value) - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} ends with #{expected.label} (using #ends_with?)", - expected: "Not #{expected.value.inspect}", - actual: actual_value.inspect - ) - else - SuccessfulMatchData.new(match_data_description(actual_label)) - end - end - - # Checks whether the last element of the value is not the expected value. - # This method expects that the actual value is a set (enumerable). - private def negated_match_last(actual_value, actual_label) - list = actual_value.to_a - last = list.last - - if expected.value === last - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} ends with #{expected.label} (using expected === last)", - expected: "Not #{expected.value.inspect}", - actual: last.inspect, - list: list.inspect - ) - else - SuccessfulMatchData.new(match_data_description(actual_label)) - end - end - end -end diff --git a/src/spectator/matchers/equality_matcher.cr b/src/spectator/matchers/equality_matcher.cr deleted file mode 100644 index ebd725d..0000000 --- a/src/spectator/matchers/equality_matcher.cr +++ /dev/null @@ -1,40 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Common matcher that tests whether two values equal each other. - # The values are compared with the == operator. - struct EqualityMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "equals #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - expected.value == actual.value - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} does not equal #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} equals #{expected.label}" - end - end -end diff --git a/src/spectator/matchers/exception_matcher.cr b/src/spectator/matchers/exception_matcher.cr deleted file mode 100644 index b26d390..0000000 --- a/src/spectator/matchers/exception_matcher.cr +++ /dev/null @@ -1,127 +0,0 @@ -require "../value" -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" - -module Spectator::Matchers - # Matcher that tests whether an exception is raised. - struct ExceptionMatcher(ExceptionType, ExpectedType) < Matcher - # Expected value and label. - private getter expected - - # Creates the matcher with no expectation of the message. - def initialize - @expected = Value.new(nil, ExceptionType.to_s) - end - - # Creates the matcher with an expected message. - def initialize(@expected : Value(ExpectedType)) - 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 : String - if (message = @expected) - "raises #{ExceptionType} with message #{message}" - else - "raises #{ExceptionType}" - end - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - exception = capture_exception { actual.value } - if exception.nil? - FailedMatchData.new(match_data_description(actual), "#{actual.label} did not raise", expected: ExceptionType.inspect) - else - if exception.is_a?(ExceptionType) - if (value = expected.value).nil? - SuccessfulMatchData.new(match_data_description(actual)) - else - if value === exception.message - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} raised #{exception.class}, but the message is not #{expected.label}", - "expected type": ExceptionType.inspect, - "actual type": exception.class.inspect, - "expected message": value.inspect, - "actual message": exception.message.to_s - ) - end - end - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} did not raise #{ExceptionType}", - expected: ExceptionType.inspect, - actual: exception.class.inspect - ) - end - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - exception = capture_exception { actual.value } - if exception.nil? - SuccessfulMatchData.new(match_data_description(actual)) - else - if exception.is_a?(ExceptionType) - if (value = expected.value).nil? - FailedMatchData.new(match_data_description(actual), "#{actual.label} raised #{exception.class}", - expected: "Not #{ExceptionType}", - actual: exception.class.inspect - ) - else - if value === exception.message - FailedMatchData.new(match_data_description(actual), "#{actual.label} raised #{exception.class} with message matching #{expected.label}", - "expected type": ExceptionType.inspect, - "actual type": exception.class.inspect, - "expected message": value.inspect, - "actual message": exception.message.to_s - ) - else - SuccessfulMatchData.new(match_data_description(actual)) - end - end - else - SuccessfulMatchData.new(match_data_description(actual)) - end - end - end - - def with_message(message : T) forall T - value = Value.new(message) - ExceptionMatcher(ExceptionType, T).new(value) - end - - # Runs a block of code and returns the exception it threw. - # If no exception was thrown, *nil* is returned. - private def capture_exception(&) - exception = nil - begin - yield - rescue ex - exception = ex - end - exception - end - - # Creates a new exception matcher with no message check. - def self.create(exception_type : T.class, label : String) forall T - ExceptionMatcher(T, Nil).new - end - - # Creates a new exception matcher with a message check. - def self.create(value, label : String) - expected = Value.new(value, label) - ExceptionMatcher(Exception, typeof(value)).new(expected) - end - - # Creates a new exception matcher with a type and message check. - def self.create(exception_type : T.class, value, label : String) forall T - expected = Value.new(value, label) - ExceptionMatcher(T, typeof(value)).new(expected) - end - end -end diff --git a/src/spectator/matchers/failed_match_data.cr b/src/spectator/matchers/failed_match_data.cr deleted file mode 100644 index 537cf95..0000000 --- a/src/spectator/matchers/failed_match_data.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "./match_data" - -module Spectator::Matchers - # Information about a failed match. - struct FailedMatchData < MatchData - # Indicates that the match failed. - def matched? : Bool - false - end - - # Description from the matcher as to why it failed. - getter failure_message : String - - # Additional information from the match that can be used to debug the problem. - getter values : Array(Tuple(Symbol, String)) - - # Creates the match data. - def initialize(description, @failure_message, @values) - super(description) - end - - # Creates the match data. - def initialize(description, @failure_message, **values) - super(description) - @values = values.to_a - end - end -end diff --git a/src/spectator/matchers/greater_than_equal_matcher.cr b/src/spectator/matchers/greater_than_equal_matcher.cr deleted file mode 100644 index 194591c..0000000 --- a/src/spectator/matchers/greater_than_equal_matcher.cr +++ /dev/null @@ -1,58 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether one value is greater than or equal to another. - # The values are compared with the >= operator. - struct GreaterThanEqualMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "greater than or equal to #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual.value >= expected.value - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is less than #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is greater than or equal to #{expected.label}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: ">= #{expected.value.inspect}", - actual: actual.value.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: "< #{expected.value.inspect}", - actual: actual.value.inspect, - } - end - end -end diff --git a/src/spectator/matchers/greater_than_matcher.cr b/src/spectator/matchers/greater_than_matcher.cr deleted file mode 100644 index a2bcdc6..0000000 --- a/src/spectator/matchers/greater_than_matcher.cr +++ /dev/null @@ -1,58 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether one value is greater than another. - # The values are compared with the > operator. - struct GreaterThanMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "greater than #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual.value > expected.value - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is less than or equal to #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is greater than #{expected.label}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: "> #{expected.value.inspect}", - actual: actual.value.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: "<= #{expected.value.inspect}", - actual: actual.value.inspect, - } - end - end -end diff --git a/src/spectator/matchers/have_key_matcher.cr b/src/spectator/matchers/have_key_matcher.cr deleted file mode 100644 index 36c5395..0000000 --- a/src/spectator/matchers/have_key_matcher.cr +++ /dev/null @@ -1,58 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether a `Hash` (or similar type) has a given key. - # The set is checked with the `has_key?` method. - struct HaveKeyMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "has key #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:has_key?) - - actual_value.has_key?(expected.value) - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} does not have key #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} has key #{expected.label}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - actual_value = actual.value - set = actual_value.responds_to?(:keys) ? actual_value.keys : actual_value - { - key: expected.value.inspect, - actual: set.inspect, - } - end - - private def unexpected(value, label) - raise "#{label} is not hash-like (must respond to `#has_key?`). #{label}: #{value.inspect}" - end - end -end diff --git a/src/spectator/matchers/have_matcher.cr b/src/spectator/matchers/have_matcher.cr deleted file mode 100644 index 9c16c9f..0000000 --- a/src/spectator/matchers/have_matcher.cr +++ /dev/null @@ -1,120 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether a value, such as a `String` or `Array`, matches one or more values. - # For a `String`, the `includes?` method is used. - # Otherwise, it expects an `Enumerable` and iterates over each item until === is true. - struct HaveMatcher(ExpectedType) < Matcher - # Expected value and label. - private getter expected - - # Creates the matcher with an expected value. - def initialize(@expected : Value(ExpectedType)) - 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 : String - "has #{expected.label}" - end - - # Entrypoint for the matcher, forwards to the correct method for string or enumerable. - def match(actual : Expression(T)) : MatchData forall T - if (value = actual.value).is_a?(String) - match_string(value, actual.label) - else - match_enumerable(value, actual.label) - end - end - - # Actually performs the test against the expression. - private def match_enumerable(actual_value, actual_label) - array = actual_value.to_a - missing = expected.value.reject do |item| - array.any? do |element| - item === element - end - end - - if missing.empty? - # Contents are present. - SuccessfulMatchData.new(match_data_description(actual_label)) - else - # Content is missing. - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not have #{expected.label}", - expected: expected.value.inspect, - actual: actual_value.inspect, - missing: missing.inspect, - ) - end - end - - # Checks if a `String` matches the expected values. - # The `includes?` method is used for this check. - private def match_string(actual_value, actual_label) - missing = expected.value.reject do |item| - actual_value.includes?(item) - end - - if missing.empty? - SuccessfulMatchData.new(match_data_description(actual_label)) - else - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not have #{expected.label}", - expected: expected.value.inspect, - actual: actual_value.inspect, - missing: missing.inspect, - ) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - if (value = actual.value).is_a?(String) - negated_match_string(value, actual.label) - else - negated_match_enumerable(value, actual.label) - end - end - - # Actually performs the negated test against the expression. - private def negated_match_enumerable(actual_value, actual_label) - array = actual_value.to_a - satisfied = expected.value.any? do |item| - array.any? do |element| - item === element - end - end - - if satisfied - # Contents are present. - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} has #{expected.label}", - expected: "Not #{expected.value.inspect}", - actual: actual_value.inspect - ) - else - # Content is missing. - SuccessfulMatchData.new(match_data_description(actual_label)) - end - end - - # Checks if a `String` doesn't match the expected values. - # The `includes?` method is used for this check. - private def negated_match_string(actual_value, actual_label) - satisfied = expected.value.any? do |item| - actual_value.includes?(item) - end - - if satisfied - SuccessfulMatchData.new(match_data_description(actual_label)) - else - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not have #{expected.label}", - expected: expected.value.inspect, - actual: actual_value.inspect, - missing: missing.inspect, - ) - end - end - end -end diff --git a/src/spectator/matchers/have_predicate_matcher.cr b/src/spectator/matchers/have_predicate_matcher.cr deleted file mode 100644 index 55416b5..0000000 --- a/src/spectator/matchers/have_predicate_matcher.cr +++ /dev/null @@ -1,98 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests one or more "has" predicates - # (methods ending in '?' and starting with 'has_'). - # The `ExpectedType` type param should be a `NamedTuple`. - # Each key in the tuple is a predicate (without the '?' and 'has_' prefix) to test. - # Each value is a a `Tuple` of arguments to pass to the predicate method. - struct HavePredicateMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "has #{expected.label}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - snapshot = snapshot_values(actual.value) - if values_match?(snapshot) - SuccessfulMatchData.new(description) - else - FailedMatchData.new(description, "#{actual.label} does not have #{expected.label}", values(snapshot).to_a) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - snapshot = snapshot_values(actual.value) - if values_match?(snapshot) - FailedMatchData.new(description, "#{actual.label} has #{expected.label}", values(snapshot).to_a) - else - SuccessfulMatchData.new(description) - end - end - - def match?(actual : Expression(T)) : Bool forall T - snapshot = snapshot_values(actual.value) - values_match?(snapshot) - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} does not have #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} has #{expected.label}" - end - - # Captures all of the actual values. - # A `NamedTuple` is returned, with each key being the attribute. - private def snapshot_values(object) - {% begin %} - { - {% for attribute in ExpectedType.keys %} - {{attribute}}: object.has_{{attribute}}?(*@expected.value[{{attribute.symbolize}}]), - {% end %} - } - {% end %} - end - - # Checks if all predicate methods from the snapshot of them are satisfied. - private def values_match?(snapshot) : Bool - # Test each predicate and immediately return false if one is false. - {% for attribute in ExpectedType.keys %} - return false unless snapshot[{{attribute.symbolize}}] - {% end %} - - # All checks passed if this point is reached. - true - end - - # Produces the tuple for the failed match data from a snapshot of the predicate methods. - private def values(snapshot) - {% begin %} - { - {% for attribute in ExpectedType.keys %} - {{attribute}}: snapshot[{{attribute.symbolize}}].inspect, - {% end %} - } - {% end %} - end - end -end diff --git a/src/spectator/matchers/have_value_matcher.cr b/src/spectator/matchers/have_value_matcher.cr deleted file mode 100644 index 462fa15..0000000 --- a/src/spectator/matchers/have_value_matcher.cr +++ /dev/null @@ -1,58 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether a `Hash` (or similar type) has a given value. - # The set is checked with the `has_value?` method. - struct HaveValueMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "has value #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:has_value?) - - actual_value.has_value?(expected.value) - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} does not have value #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} has value #{expected.label}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - actual_value = actual.value - set = actual_value.responds_to?(:values) ? actual_value.values : actual_value - { - value: expected.value.inspect, - actual: set.inspect, - } - end - - private def unexpected(value, label) - raise "#{label} is not hash-like (must respond to `#has_value?`). #{label}: #{value.inspect}" - end - end -end diff --git a/src/spectator/matchers/inequality_matcher.cr b/src/spectator/matchers/inequality_matcher.cr deleted file mode 100644 index 4ba68bc..0000000 --- a/src/spectator/matchers/inequality_matcher.cr +++ /dev/null @@ -1,58 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether two values do not equal each other. - # The values are compared with the != operator. - struct InequalityMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "is not equal to #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - expected.value != actual.value - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is equal to #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is not equal to #{expected.label}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: "Not #{expected.value.inspect}", - actual: actual.value.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: expected.value.inspect, - actual: actual.value.inspect, - } - end - end -end diff --git a/src/spectator/matchers/instance_matcher.cr b/src/spectator/matchers/instance_matcher.cr deleted file mode 100644 index 2c8823f..0000000 --- a/src/spectator/matchers/instance_matcher.cr +++ /dev/null @@ -1,57 +0,0 @@ -require "./matcher" - -module Spectator::Matchers - # Matcher that tests a value is of a specified type. - struct InstanceMatcher(Expected) < StandardMatcher - # 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 : String - "is an instance of #{Expected}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual.value.class == Expected - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is not an instance of #{Expected}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is an instance of #{Expected}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: Expected.to_s, - actual: actual.value.class.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: "Not #{Expected}", - actual: actual.value.class.inspect, - } - end - end -end diff --git a/src/spectator/matchers/less_than_equal_matcher.cr b/src/spectator/matchers/less_than_equal_matcher.cr deleted file mode 100644 index 4bd1f2c..0000000 --- a/src/spectator/matchers/less_than_equal_matcher.cr +++ /dev/null @@ -1,58 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether one value is less than or equal to another. - # The values are compared with the <= operator. - struct LessThanEqualMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "less than or equal to #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual.value <= expected.value - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is greater than #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is less than or equal to #{expected.label}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: "<= #{expected.value.inspect}", - actual: actual.value.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: "> #{expected.value.inspect}", - actual: actual.value.inspect, - } - end - end -end diff --git a/src/spectator/matchers/less_than_matcher.cr b/src/spectator/matchers/less_than_matcher.cr deleted file mode 100644 index 1a09ad8..0000000 --- a/src/spectator/matchers/less_than_matcher.cr +++ /dev/null @@ -1,58 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether one value is less than another. - # The values are compared with the < operator. - struct LessThanMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "less than #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual.value < expected.value - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is greater than or equal to #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is less than #{expected.label}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: "< #{expected.value.inspect}", - actual: actual.value.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: ">= #{expected.value.inspect}", - actual: actual.value.inspect, - } - end - end -end diff --git a/src/spectator/matchers/match_data.cr b/src/spectator/matchers/match_data.cr deleted file mode 100644 index 31454f5..0000000 --- a/src/spectator/matchers/match_data.cr +++ /dev/null @@ -1,12 +0,0 @@ -module Spectator::Matchers - # Information about the outcome of a match. - abstract struct MatchData - # Indicates whether the match as successful or not. - abstract def matched? : Bool - - getter description : String - - def initialize(@description : String) - end - end -end diff --git a/src/spectator/matchers/matcher.cr b/src/spectator/matchers/matcher.cr deleted file mode 100644 index 05adb81..0000000 --- a/src/spectator/matchers/matcher.cr +++ /dev/null @@ -1,51 +0,0 @@ -require "../value" -require "./match_data" - -module Spectator::Matchers - # Common base class for all expectation conditions. - # A matcher looks at something produced by the SUT - # and evaluates whether it is correct or not. - abstract struct Matcher - # 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. - # ``` - # it { is_expected.to do_something } - # ``` - # The phrasing should be such that it reads "it ___." - # where the blank is what is returned by this method. - abstract def description : String - - # Actually performs the test against the expression (value or block). - abstract def match(actual : Expression(T)) : MatchData forall T - - # Performs the test against the expression (value or block), but inverted. - # 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 - - private def match_data_description(actual_label : String | Symbol) : String - "#{actual_label} #{description}" - end - - private def match_data_description(actual_label : Nil) : String - description - end - end -end diff --git a/src/spectator/matchers/nil_matcher.cr b/src/spectator/matchers/nil_matcher.cr deleted file mode 100644 index 38b3d03..0000000 --- a/src/spectator/matchers/nil_matcher.cr +++ /dev/null @@ -1,40 +0,0 @@ -require "./matcher" - -module Spectator::Matchers - # Common matcher that tests whether a value is nil. - # The `Object#nil?` method is used for this. - struct NilMatcher < StandardMatcher - # 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 : String - "is nil" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual.value.nil? - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is not nil" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is nil" - end - end -end diff --git a/src/spectator/matchers/pattern_matcher.cr b/src/spectator/matchers/pattern_matcher.cr deleted file mode 100644 index 4f8cab6..0000000 --- a/src/spectator/matchers/pattern_matcher.cr +++ /dev/null @@ -1,41 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Common matcher that tests whether a value matches another. - # The values are compared with the === operator. - # This is the same as `CaseMatcher`, but the operands are flipped. - struct PatternMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "matches #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual.value === expected.value - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} does not match #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} matched #{expected.label}" - end - end -end diff --git a/src/spectator/matchers/predicate_matcher.cr b/src/spectator/matchers/predicate_matcher.cr deleted file mode 100644 index 08bb2f6..0000000 --- a/src/spectator/matchers/predicate_matcher.cr +++ /dev/null @@ -1,99 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests one or more predicates (methods ending in '?'). - # The `ExpectedType` type param should be a `NamedTuple`. - # Each key in the tuple is a predicate (without the '?') to test. - # Each value is a a `Tuple` of arguments to pass to the predicate method. - struct PredicateMatcher(ExpectedType) < Matcher - # Expected value and label. - private getter expected - - # Creates the matcher with a expected values. - def initialize(@expected : Value(ExpectedType)) - 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 : String - "is #{expected.label}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - snapshot = snapshot_values(actual.value) - if match?(snapshot) - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} is not #{expected.label}", values(snapshot).to_a) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - snapshot = snapshot_values(actual.value) - if match?(snapshot) - FailedMatchData.new(match_data_description(actual), "#{actual.label} is #{expected.label}", values(snapshot).to_a) - else - SuccessfulMatchData.new(match_data_description(actual)) - end - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is not #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is #{expected.label}" - end - - # Captures all of the actual values. - # A `NamedTuple` is returned, with each key being the attribute. - private def snapshot_values(object) - {% begin %} - { - {% for attribute in ExpectedType.keys %} - {{attribute}}: object.{{attribute}}?(*@expected.value[{{attribute.symbolize}}]), - {% end %} - } - {% end %} - end - - # Checks if all predicate methods from the snapshot of them are satisfied. - private def match?(snapshot) - # Test each predicate and immediately return false if one is false. - {% for attribute in ExpectedType.keys %} - return false unless snapshot[{{attribute.symbolize}}] - {% end %} - - # All checks passed if this point is reached. - true - end - - # Produces the tuple for the failed match data from a snapshot of the predicate methods. - private def values(snapshot) - {% begin %} - { - {% for attribute in ExpectedType.keys %} - {{attribute}}: snapshot[{{attribute.symbolize}}].inspect, - {% end %} - } - {% end %} - end - end -end diff --git a/src/spectator/matchers/range_matcher.cr b/src/spectator/matchers/range_matcher.cr deleted file mode 100644 index 8a9a307..0000000 --- a/src/spectator/matchers/range_matcher.cr +++ /dev/null @@ -1,110 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether a value is in a given range. - # The `Range#includes?` method is used for this check. - struct RangeMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "is in #{expected.label}" - end - - # Returns a new matcher, with the same bounds, but uses an inclusive range. - def inclusive - label = expected.label - new_range = Range.new(range.begin, range.end, exclusive: false) - expected = Value.new(new_range, label) - RangeMatcher.new(expected) - end - - # Returns a new matcher, with the same bounds, but uses an exclusive range. - def exclusive - label = expected.label - new_range = Range.new(range.begin, range.end, exclusive: true) - expected = Value.new(new_range, label) - RangeMatcher.new(expected) - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - 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. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is not in #{expected.label} (#{exclusivity})" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is in #{expected.label} (#{exclusivity})" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - lower: ">= #{range.begin.inspect}", - upper: "#{exclusive? ? "<" : "<="} #{range.end.inspect}", - actual: actual.value.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - lower: "< #{range.begin.inspect}", - upper: "#{exclusive? ? ">=" : ">"} #{range.end.inspect}", - actual: actual.value.inspect, - } - end - - # Gets the expected range. - private def range - expected.value - end - - # Indicates whether the range is inclusive or exclusive. - private def exclusive? - range.exclusive? - end - - # Produces a string "inclusive" or "exclusive" based on the range. - private def exclusivity - exclusive? ? "exclusive" : "inclusive" - end - end -end diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr deleted file mode 100644 index 560cabd..0000000 --- a/src/spectator/matchers/receive_matcher.cr +++ /dev/null @@ -1,159 +0,0 @@ -require "../mocks/stub" -require "../mocks/stubbable" -require "../mocks/stubbed_type" -require "./matcher" - -module Spectator::Matchers - # Matcher that inspects stubbable objects for method calls. - struct ReceiveMatcher < Matcher - alias Count = Range(Int32?, Int32?) - - # Creates the matcher for expecting a method call matching a stub. - def initialize(@stub : Stub, @count : Count = Count.new(1, nil)) - end - - # Creates the matcher for expecting a method call with any arguments. - # *expected* is an expression evaluating to the method name as a symbol. - def initialize(expected : Expression(Symbol)) - stub = NullStub.new(expected.value).as(Stub) - initialize(stub) - end - - # Returns a new matcher with an argument constraint. - def with(*args, **kwargs) : self - stub = @stub.with(*args, **kwargs) - self.class.new(stub, @count) - end - - # Returns a new matcher that checks that the stub was invoked once. - def once : self - self.class.new(@stub, Count.new(1, 1)) - end - - # Returns a new matcher that checks that the stub was invoked twice. - def twice : self - self.class.new(@stub, Count.new(2, 2)) - end - - # Returns a new matcher that checks that the stub was invoked an exact number of times. - def exactly(count : Int) : self - self.class.new(@stub, Count.new(count, count)) - end - - # Returns a new matcher that checks that the stub was invoked at least a set amount of times. - def at_least(count : Int) : self - self.class.new(@stub, Count.new(count, nil)) - end - - # Returns a new matcher that checks that the stub was invoked at most a set amount of times. - def at_most(count : Int) : self - self.class.new(@stub, Count.new(nil, count)) - end - - # Returns a new matcher that checks that the stub was invoked at least once. - def at_least_once : self - self.class.new(@stub, Count.new(1, nil)) - end - - # Returns a new matcher that checks that the stub was invoked at least twice. - def at_least_twice : self - self.class.new(@stub, Count.new(2, nil)) - end - - # Returns a new matcher that checks that the stub was invoked at most once. - def at_most_once : self - self.class.new(@stub, Count.new(nil, 1)) - end - - # Returns a new matcher that checks that the stub was invoked at most twice. - def at_most_twice : self - self.class.new(@stub, Count.new(nil, 2)) - end - - # Returns self - used for fluent interface. - # - # ``` - # expect(dbl).to have_received(:foo).exactly(5).times - # ``` - def times : self - self - end - - # Short text about the matcher's purpose. - def description : String - "received #{@stub.message} #{humanize_count}" - end - - # Actually performs the test against the expression (value or block). - def match(actual : Expression(Stubbable) | Expression(StubbedType)) : MatchData - stubbed = actual.value - calls = relevant_calls(stubbed) - if @count.includes?(calls.size) - SuccessfulMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}") - else - FailedMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}", - "#{actual.label} did not receive #{@stub.message}", values(actual).to_a) - end - end - - # Actually performs the test against the expression (value or block). - def match(actual : Expression(T)) : MatchData forall T - {% raise "Value being checked with `have_received` must be stubbable (mock or double)." %} - end - - # Performs the test against the expression (value or block), but inverted. - def negated_match(actual : Expression(Stubbable) | Expression(StubbedType)) : MatchData - stubbed = actual.value - calls = relevant_calls(stubbed) - if @count.includes?(calls.size) - FailedMatchData.new("#{actual.label} did not receive #{@stub.message}", "#{actual.label} received #{@stub.message}", negated_values(actual).to_a) - else - SuccessfulMatchData.new("#{actual.label} did not receive #{@stub.message} #{humanize_count}") - end - end - - # Performs the test against the expression (value or block), but inverted. - def negated_match(actual : Expression(T)) : MatchData forall T - {% raise "Value being checked with `have_received` must be stubbable (mock or double)." %} - end - - # Additional information about the match failure. - private def values(actual : Expression(T)) forall T - { - expected: @stub.message, - actual: method_call_list(actual.value), - } - end - - # Additional information about the match failure when negated. - private def negated_values(actual : Expression(T)) forall T - { - expected: "Not #{@stub.message}", - actual: method_call_list(actual.value), - } - end - - # Filtered list of method calls relevant to the matcher. - private def relevant_calls(stubbable) - stubbable._spectator_calls.select { |call| @stub === call } - end - - private def humanize_count - case {@count.begin, @count.end} - when {Int32, nil} then "at least #{@count.begin} times" - when {nil, Int32} then "at most #{@count.end} times" - else "any number of times" - end - end - - # Formatted list of method calls. - private def method_call_list(stubbable) - calls = stubbable._spectator_calls - if calls.empty? - "None" - else - calls.join("\n") - end - end - end -end diff --git a/src/spectator/matchers/reference_matcher.cr b/src/spectator/matchers/reference_matcher.cr deleted file mode 100644 index 5a3f330..0000000 --- a/src/spectator/matchers/reference_matcher.cr +++ /dev/null @@ -1,48 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether two references are the same. - # The values are compared with the `Reference#same?` method. - struct ReferenceMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "is #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - value = expected.value - if value.nil? - actual.value.nil? - elsif value.responds_to?(:same?) - value.same?(actual.value) - else - # Value type (struct) comparison. - actual.value.class == value.class && actual.value == value - end - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is not #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is #{expected.label}" - end - end -end diff --git a/src/spectator/matchers/regex_matcher.cr b/src/spectator/matchers/regex_matcher.cr deleted file mode 100644 index b9b2ce3..0000000 --- a/src/spectator/matchers/regex_matcher.cr +++ /dev/null @@ -1,40 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Common matcher that tests whether a value matches another. - # The values are compared with the =~ operator. - struct RegexMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "matches #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - !!(actual.value =~ expected.value) - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} does not match #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} matched #{expected.label}" - end - end -end diff --git a/src/spectator/matchers/respond_matcher.cr b/src/spectator/matchers/respond_matcher.cr deleted file mode 100644 index 9712da5..0000000 --- a/src/spectator/matchers/respond_matcher.cr +++ /dev/null @@ -1,67 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests that a type responds to a method call. - # The instance is tested with the `responds_to?` method. - # The `ExpectedType` type param should be a `NamedTuple`, - # with each key being the method to check and the value is ignored. - struct RespondMatcher(ExpectedType) < Matcher - # 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 : String - "responds to #{label}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - snapshot = snapshot_values(actual.value) - if snapshot.values.all? - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} does not respond to #{label}", values(snapshot).to_a) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - snapshot = snapshot_values(actual.value) - # Intentionally check truthiness of each value. - if snapshot.values.any? # ameba:disable Performance/AnyInsteadOfEmpty - FailedMatchData.new(match_data_description(actual), "#{actual.label} responds to #{label}", values(snapshot).to_a) - else - SuccessfulMatchData.new(match_data_description(actual)) - end - end - - # Captures all of the actual values. - # A `NamedTuple` is returned, with each key being the attribute. - private def snapshot_values(object) - {% begin %} - { - {% for attribute in ExpectedType.keys %} - {{attribute}}: object.responds_to?({{attribute.symbolize}}), - {% end %} - } - {% end %} - end - - # Produces the tuple for the failed match data from a snapshot of the results. - private def values(snapshot) - {% begin %} - { - {% for attribute in ExpectedType.keys %} - {{attribute}}: snapshot[{{attribute.symbolize}}].inspect, - {% end %} - } - {% end %} - end - - # Generated, user-friendly, string for the expected value. - private def label - # Prefix every method name with # and join them with commas. - {{ExpectedType.keys.map { |e| "##{e}".id }.splat.stringify}} - end - end -end diff --git a/src/spectator/matchers/size_matcher.cr b/src/spectator/matchers/size_matcher.cr deleted file mode 100644 index 74362a5..0000000 --- a/src/spectator/matchers/size_matcher.cr +++ /dev/null @@ -1,65 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether a set has a specified number of elements. - # The set's `#size` method is used for this check. - struct SizeMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "has size #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:size?) - - expected.value == actual_value.size - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} does not have #{expected.label} elements" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} has #{expected.label} elements" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: expected.value.inspect, - actual: actual.value.size.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: "Not #{expected.value.inspect}", - actual: actual.value.size.inspect, - } - end - - private def unexpected(value, label) - raise "#{label} must respond to `#size`. #{label}: #{value.inspect}" - end - end -end diff --git a/src/spectator/matchers/size_of_matcher.cr b/src/spectator/matchers/size_of_matcher.cr deleted file mode 100644 index f84aedf..0000000 --- a/src/spectator/matchers/size_of_matcher.cr +++ /dev/null @@ -1,65 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether a set has the same number of elements as another set. - # The set's `#size` method is used for this check. - struct SizeOfMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # 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 : String - "is the same size as #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:size?) - - expected.value.size == actual_value.size - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is not the same size as #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is the same size as #{expected.label}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: expected.value.size.inspect, - actual: actual.value.size.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: "Not #{expected.value.size.inspect}", - actual: actual.value.size.inspect, - } - end - - private def unexpected(value, label) - raise "#{label} must respond to `#size`. #{label}: #{value.inspect}" - end - end -end diff --git a/src/spectator/matchers/standard_matcher.cr b/src/spectator/matchers/standard_matcher.cr deleted file mode 100644 index 13dee4c..0000000 --- a/src/spectator/matchers/standard_matcher.cr +++ /dev/null @@ -1,130 +0,0 @@ -require "../expression" -require "./failed_match_data" -require "./matcher" -require "./successful_match_data" - -module Spectator::Matchers - # Provides common methods for matchers. - # - # The `#match` and `#negated_match` methods have an implementation - # that is suitable for most matchers. - # Matchers based on this class need to define `#match?` and `#failure_message`. - # If the matcher can be negated, - # the `#failure_message_when_negated` method needs to be overridden. - # Additionally, the `#does_not_match?` method can be specified - # if there's custom behavior for negated matches. - # If the matcher operates on or has extra data that is useful for debug, - # then the `#values` and `#negated_values` methods can be overridden. - # Finally, define a `#description` message that can be used for the one-liner "it" syntax. - abstract struct StandardMatcher < Matcher - # Actually performs the test against the expression (value or block). - # - # This method calls the abstract `#match?` method. - # If it returns true, then a `SuccessfulMatchData` instance is returned. - # Otherwise, a `FailedMatchData` instance is returned. - # Additionally, `#failure_message` and `#values` are called for a failed match. - def match(actual : Expression(T)) : MatchData forall T - if match?(actual) - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), failure_message(actual), values(actual).to_a) - end - end - - # Performs the test against the expression (value or block), but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - # - # This method calls the abstract `#does_not_match?` method. - # If it returns true, then a `SuccessfulMatchData` instance is returned. - # Otherwise, a `FailedMatchData` instance is returned. - # Additionally, `#failure_message_when_negated` and `#negated_values` are called for a failed match. - def negated_match(actual : Expression(T)) : MatchData forall T - # TODO: Invert description. - if does_not_match?(actual) - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), failure_message_when_negated(actual), negated_values(actual).to_a) - end - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private abstract def failure_message(actual : Expression(T)) : String forall T - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # A default implementation of this method is provided, - # which causes compilation to fail. - # If the matcher supports negation, it must override this method. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual : Expression(T)) : String forall T - raise "Negation with #{self.class} is not supported." - end - - # Checks whether the matcher is satisfied with the expression given to it. - private abstract def match?(actual : Expression(T)) : Bool forall T - - # If the expectation is negated, then this method is called instead of `#match?`. - # - # The default implementation of this method is to invert the result of `#match?`. - # If the matcher requires custom handling of negated matches, - # then this method should be overridden. - # Remember to override `#failure_message_when_negated` as well. - private def does_not_match?(actual : Expression(T)) : Bool forall T - !match?(actual) - end - - # Additional information about the match failure. - # - # By default, just the actual value is produced. - # The return value must be a NamedTuple with Strings for each value. - # The tuple can be of any size, - # but the keys must be known at compile-time (as Symbols), - # and the values must be strings. - # Generally, the string values are produced by calling `#inspect` on the relevant object. - # It should look like this: - # ``` - # { - # expected: "foo", - # actual: "bar", - # } - # ``` - # - # The values should typically only contain the test expression values, not the labels. - # Labeled should be returned by `#failure_message`. - private def values(actual : Expression(T)) forall T - {actual: actual.value.inspect} - end - - # Additional information about the match failure when negated. - # - # By default, just the actual value is produced (same as `#values`). - # The return value must be a NamedTuple with Strings for each value. - # The tuple can be of any size, - # but the keys must be known at compile-time (as Symbols), - # and the values must be strings. - # Generally, the string values are produced by calling `#inspect` on the relevant object. - # It should look like this: - # ``` - # { - # expected: "Not foo", - # actual: "bar", - # } - # ``` - # - # The values should typically only contain the test expression values, not the labels. - # Labeled should be returned by `#failure_message_when_negated`. - private def negated_values(actual : Expression(T)) forall T - values(actual) - end - end -end diff --git a/src/spectator/matchers/start_with_matcher.cr b/src/spectator/matchers/start_with_matcher.cr deleted file mode 100644 index 4bfc6c8..0000000 --- a/src/spectator/matchers/start_with_matcher.cr +++ /dev/null @@ -1,104 +0,0 @@ -# Checks whether the last element of the value is the expected value. -# This method expects that the actual value is a set (enumerable).require "./value_matcher" - -module Spectator::Matchers - # Matcher that tests whether a value, such as a `String` or `Array`, starts with a value. - # The `starts_with?` method is used if it's defined on the actual type. - # Otherwise, it is treated as an `Enumerable` and the `first` value is compared against. - struct StartWithMatcher(ExpectedType) < Matcher - # Expected value and label. - private getter expected - - # Creates the matcher with an expected value. - def initialize(@expected : Value(ExpectedType)) - 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 : String - "starts with #{expected.label}" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - value = actual.value - if value.is_a?(String) || value.responds_to?(:starts_with?) - match_starts_with(value, actual.label) - else - match_first(value, actual.label) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - value = actual.value - if value.is_a?(String) || value.responds_to?(:starts_with?) - negated_match_starts_with(value, actual.label) - else - negated_match_first(value, actual.label) - end - end - - # Checks whether the actual value starts with the expected value. - # This method expects (and uses) the `#starts_with?` method on the value. - private def match_starts_with(actual_value, actual_label) - if actual_value.starts_with?(expected.value) - SuccessfulMatchData.new(match_data_description(actual_label)) - else - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not start with #{expected.label} (using #starts_with?)", - expected: expected.value.inspect, - actual: actual_value.inspect - ) - end - end - - # Checks whether the first element of the value is the expected value. - # This method expects that the actual value is a set (enumerable). - private def match_first(actual_value, actual_label) - list = actual_value.to_a - first = list.first - - if expected.value === first - SuccessfulMatchData.new(match_data_description(actual_label)) - else - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not start with #{expected.label} (using expected === first)", - expected: expected.value.inspect, - actual: first.inspect, - list: list.inspect - ) - end - end - - # Checks whether the actual value does not start with the expected value. - # This method expects (and uses) the `#starts_with?` method on the value. - private def negated_match_starts_with(actual_value, actual_label) - if actual_value.starts_with?(expected.value) - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} starts with #{expected.label} (using #starts_with?)", - expected: "Not #{expected.value.inspect}", - actual: actual_value.inspect - ) - else - SuccessfulMatchData.new(match_data_description(actual_label)) - end - end - - # Checks whether the first element of the value is not the expected value. - # This method expects that the actual value is a set (enumerable). - private def negated_match_first(actual_value, actual_label) - list = actual_value.to_a - first = list.first - - if expected.value === first - FailedMatchData.new(match_data_description(actual_label), "#{actual_label} starts with #{expected.label} (using expected === first)", - expected: "Not #{expected.value.inspect}", - actual: first.inspect, - list: list.inspect - ) - else - SuccessfulMatchData.new(match_data_description(actual_label)) - end - end - end -end diff --git a/src/spectator/matchers/successful_match_data.cr b/src/spectator/matchers/successful_match_data.cr deleted file mode 100644 index 29b11d0..0000000 --- a/src/spectator/matchers/successful_match_data.cr +++ /dev/null @@ -1,11 +0,0 @@ -require "./match_data" - -module Spectator::Matchers - # Information about a successful match. - struct SuccessfulMatchData < MatchData - # Indicates that the match succeeded. - def matched? : Bool - true - end - end -end diff --git a/src/spectator/matchers/truthy_matcher.cr b/src/spectator/matchers/truthy_matcher.cr deleted file mode 100644 index b784fb7..0000000 --- a/src/spectator/matchers/truthy_matcher.cr +++ /dev/null @@ -1,161 +0,0 @@ -require "./standard_matcher" - -module Spectator::Matchers - # Matcher that tests whether a value is truthy or falsey. - # Falsey means a value is considered false by an if-statement, - # which are false and nil in Crystal. - # Truthy is the opposite of falsey. - # - # Additionally, different matchers can be created - # by using the `#<`, `#<=`, `#>`, `#>=`, `#==`, and `#!=` operators. - struct TruthyMatcher < StandardMatcher - # Creates the truthy matcher. - # The *truthy* argument should be true to match "truthy" values, - # and false to match "falsey" values. - def initialize(@truthy : Bool = true) - 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 : String - "is #{label}" - end - - # Creates a matcher that checks if a value is less than an expected value. - # The spec would look like: - # ``` - # expect(0).to be < 1 - # ``` - def <(value) - expected = Value.new(value) - LessThanMatcher.new(expected) - end - - # Creates a matcher that checks if a value is less than or equal to an expected value. - # The spec would look like: - # ``` - # expect(0).to be <= 1 - # ``` - def <=(value) - expected = Value.new(value) - LessThanEqualMatcher.new(expected) - end - - # Creates a matcher that checks if a value is greater than an expected value. - # The spec would look like: - # ``` - # expect(2).to be > 1 - # ``` - def >(value) - expected = Value.new(value) - GreaterThanMatcher.new(expected) - end - - # Creates a matcher that checks if a value is greater than or equal to an expected value. - # The spec would look like: - # ``` - # expect(2).to be >= 1 - # ``` - def >=(value) - expected = Value.new(value) - GreaterThanEqualMatcher.new(expected) - end - - # Creates a matcher that checks if a value is equal to an expected value. - # The spec would look like: - # ``` - # expect(0).to be == 0 - # ``` - def ==(value) - expected = Value.new(value) - EqualityMatcher.new(expected) - end - - # Creates a matcher that checks if a value is not equal to an expected value. - # The spec would look like: - # ``` - # expect(0).to be != 1 - # ``` - def !=(value) - expected = Value.new(value) - InequalityMatcher.new(expected) - end - - # Creates a matcher that checks if a value is semantically equal to an expected value. - # The spec would look like: - # ``` - # expect("foobar").to be === /foo/ - # ``` - def ===(value) - expected = Value.new(value) - PatternMatcher.new(expected) - end - - # Creates a matcher that checks if a value matches the pattern of an expected value. - # The spec would look like: - # ``` - # expect("foobar").to be =~ /foo/ - # ``` - def =~(value) - expected = Value.new(value) - RegexMatcher.new(expected) - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - @truthy == !!actual.value - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is #{negated_label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is #{label}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: @truthy ? "Not false or nil" : "false or nil", - actual: actual.value.inspect, - truthy: (!!actual.value).inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: @truthy ? "false or nil" : "Not false or nil", - actual: actual.value.inspect, - truthy: (!!actual.value).inspect, - } - end - - # Generated, user-friendly, string for the expected value. - private def label - @truthy ? "truthy" : "falsey" - end - - # Generated, user-friendly, string for the unexpected value. - private def negated_label - @truthy ? "falsey" : "truthy" - end - end -end diff --git a/src/spectator/matchers/type_matcher.cr b/src/spectator/matchers/type_matcher.cr deleted file mode 100644 index 65980bb..0000000 --- a/src/spectator/matchers/type_matcher.cr +++ /dev/null @@ -1,63 +0,0 @@ -require "./matcher" - -module Spectator::Matchers - # Matcher that tests a value is of a specified type. - # The values are compared with the `Object#is_a?` method. - struct TypeMatcher(Expected) < StandardMatcher - # Alternate constructor that works with constant types and types from variables. - def self.create(type : T.class) forall T - TypeMatcher(T).new - 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 : String - "is a #{Expected}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Expression(T)) : Bool forall T - actual.value.is_a?(Expected) - end - - # Message displayed when the matcher isn't satisfied. - # - # This is only called when `#match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message(actual) : String - "#{actual.label} is not a #{Expected}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # - # This is only called when `#does_not_match?` returns false. - # - # The message should typically only contain the test expression labels. - # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual) : String - "#{actual.label} is a #{Expected}" - end - - # Additional information about the match failure. - # The return value is a NamedTuple with Strings for each value. - private def values(actual) - { - expected: Expected.to_s, - actual: actual.value.class.inspect, - } - end - - # Additional information about the match failure when negated. - # The return value is a NamedTuple with Strings for each value. - private def negated_values(actual) - { - expected: "Not #{Expected}", - actual: actual.value.class.inspect, - } - end - end -end diff --git a/src/spectator/matchers/unordered_array_matcher.cr b/src/spectator/matchers/unordered_array_matcher.cr deleted file mode 100644 index 510465f..0000000 --- a/src/spectator/matchers/unordered_array_matcher.cr +++ /dev/null @@ -1,85 +0,0 @@ -require "./value_matcher" - -module Spectator::Matchers - # Matcher for checking that the contents of one array (or similar type) - # has the exact same contents as another, but in any order. - struct UnorderedArrayMatcher(ExpectedType) < Matcher - # Expected value and label. - private getter expected - - # Creates the matcher with an expected value. - def initialize(@expected : Value(Array(ExpectedType))) - 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 : String - "contains #{expected.label} in any order" - end - - # Actually performs the test against the expression. - def match(actual : Expression(T)) : MatchData forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:to_a) - - actual_elements = actual_value.to_a - expected_elements = expected.value - missing, extra = array_diff(expected_elements, actual_elements) - - if missing.empty? && extra.empty? - SuccessfulMatchData.new(match_data_description(actual)) - else - FailedMatchData.new(match_data_description(actual), "#{actual.label} does not contain #{expected.label} (unordered)", - expected: expected_elements.inspect, - actual: actual_elements.inspect, - missing: missing.inspect, - extra: extra.inspect, - ) - end - end - - # Performs the test against the expression, but inverted. - # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : Expression(T)) : MatchData forall T - actual_value = actual.value - return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:to_a) - - actual_elements = actual_value.to_a - expected_elements = expected.value - missing, extra = array_diff(expected_elements, actual_elements) - - if missing.empty? && extra.empty? - FailedMatchData.new(match_data_description(actual), "#{actual.label} contains #{expected.label} (unordered)", - expected: "Not #{expected_elements.inspect}", - actual: actual_elements.inspect, - ) - else - SuccessfulMatchData.new(match_data_description(actual)) - end - end - - # Finds the difference of two unordered arrays. - # Returns a tuple of arrays - missing from *actual* and extra in *actual*. - private def array_diff(expected, actual) - extra = actual.dup - missing = expected.class.new # Creates an empty array of the expected element type. - - # OPTIMIZE: Not very efficient at finding the difference. - expected.each do |item| - index = extra.index(item) - if index - extra.delete_at(index) - else - missing << item - end - end - - {missing, extra} - end - - private def unexpected(value, label) - raise "#{label} is not a collection (must respond to `#to_a`). #{label}: #{value.inspect}" - end - end -end diff --git a/src/spectator/matchers/value_matcher.cr b/src/spectator/matchers/value_matcher.cr deleted file mode 100644 index a88f457..0000000 --- a/src/spectator/matchers/value_matcher.cr +++ /dev/null @@ -1,69 +0,0 @@ -require "../expression" -require "../value" -require "./standard_matcher" - -module Spectator::Matchers - # Category of matcher that uses a value. - # Matchers of this type expect that a SUT applies to the value in some way. - # - # Matchers based on this class need to define `#match?` and `#failure_message`. - # If the matcher can be negated, - # the `#failure_message_when_negated` method needs to be overridden. - # Additionally, the `#does_not_match?` method can be specified - # if there's custom behavior for negated matches. - # If the matcher operates on or has extra data that is useful for debug, - # then the `#values` and `#negated_values` methods can be overridden. - # Finally, define a `#description` message that can be used for the one-liner "it" syntax. - # - # The failure messages should typically only contain the test expression labels. - # Actual values should be returned by `#values` and `#negated_values`. - abstract struct ValueMatcher(ExpectedType) < StandardMatcher - # Expected value. - # Sub-types may use this value to test the expectation and generate message strings. - private getter expected - - # Creates the value matcher. - # The expected value is stored for later use. - def initialize(@expected : ::Spectator::Value(ExpectedType)) - end - - # Additional information about the match failure. - # - # By default, just the actual and expected values are produced. - # The return value must be a NamedTuple with Strings for each value. - # The tuple can be of any size, - # but the keys must be known at compile-time (as Symbols), - # and the values must be strings. - # Generally, the string values are produced by calling `#inspect` on the relevant object. - # It should look like this: - # ``` - # { - # expected: "foo", - # actual: "bar", - # } - # ``` - private def values(actual : Expression(T)) forall T - super.merge(expected: expected.value.inspect) - end - - # Additional information about the match failure when negated. - # - # By default, just the actual and expected values are produced (same as `#values`). - # However, the expected value is prefixed with the word "Not". - # The return value must be a NamedTuple with Strings for each value. - # The tuple can be of any size, - # but the keys must be known at compile-time (as Symbols), - # and the values must be strings. - # Generally, the string values are produced by calling `#inspect` on the relevant object. - # It should look like this: - # ``` - # { - # expected: "Not foo", - # actual: "bar", - # } - # ``` - private def negated_values(actual : Expression(T)) forall T - super.merge(expected: "Not #{expected.value.inspect}") - end - end -end diff --git a/src/spectator/metadata.cr b/src/spectator/metadata.cr deleted file mode 100644 index 9baaab6..0000000 --- a/src/spectator/metadata.cr +++ /dev/null @@ -1,11 +0,0 @@ -module Spectator - # User-defined keywords used for filtering and behavior modification. - alias Tags = Set(Symbol) - - # User-defined keywords used for filtering and behavior modification. - # The value of a tag is optional, but may contain useful information. - # If the value is nil, the tag exists, but has no data. - # However, when tags are given on examples and example groups, - # if the value is falsey (false or nil), then the tag should be removed from the overall collection. - alias Metadata = Hash(Symbol, String?) -end diff --git a/src/spectator/mocks.cr b/src/spectator/mocks.cr deleted file mode 100644 index f50bf9e..0000000 --- a/src/spectator/mocks.cr +++ /dev/null @@ -1,7 +0,0 @@ -require "./mocks/*" - -module Spectator - # Functionality for mocking existing types. - module Mocks - end -end diff --git a/src/spectator/mocks/abstract_arguments.cr b/src/spectator/mocks/abstract_arguments.cr deleted file mode 100644 index 4a6f75f..0000000 --- a/src/spectator/mocks/abstract_arguments.cr +++ /dev/null @@ -1,61 +0,0 @@ -module Spectator - # Untyped arguments to a method call (message). - abstract class AbstractArguments - # Use the string representation to avoid over complicating debug output. - def inspect(io : IO) : Nil - to_s(io) - end - - # Utility method for comparing two tuples considering special types. - private def compare_tuples(a : Tuple | Array, b : Tuple | Array) - return false if a.size != b.size - - a.zip(b) do |a_value, b_value| - return false unless compare_values(a_value, b_value) - end - true - end - - # Utility method for comparing two tuples considering special types. - # Supports nilable tuples (ideal for splats). - private def compare_tuples(a : Tuple? | Array?, b : Tuple? | Array?) - return false if a.nil? ^ b.nil? - - compare_tuples(a.not_nil!, b.not_nil!) - end - - # Utility method for comparing two named tuples ignoring order. - private def compare_named_tuples(a : NamedTuple | Hash, b : NamedTuple | Hash) - a.each do |k, v1| - v2 = b.fetch(k) { return false } - return false unless compare_values(v1, v2) - end - true - end - - # Utility method for comparing two arguments considering special types. - # Some types used for case-equality don't work well with unexpected right-hand types. - # This can happen when the right side is a massive union of types. - private def compare_values(a, b) - case a - when Proc - # Using procs as argument matchers isn't supported currently. - # Compare directly instead. - a == b - when Range - # Ranges can only be matched against if their right side is comparable. - # Ensure the right side is comparable, otherwise compare directly. - return a === b if b.is_a?(Comparable(typeof(b))) - a == b - when Tuple, Array - return compare_tuples(a, b) if b.is_a?(Tuple) || b.is_a?(Array) - a === b - when NamedTuple, Hash - return compare_named_tuples(a, b) if b.is_a?(NamedTuple) || b.is_a?(Hash) - a === b - else - a === b - end - end - end -end diff --git a/src/spectator/mocks/allow.cr b/src/spectator/mocks/allow.cr deleted file mode 100644 index 4754690..0000000 --- a/src/spectator/mocks/allow.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "../harness" -require "./stub" -require "./stubbable" -require "./stubbed_type" - -module Spectator - # Targets a stubbable object. - # - # This type is effectively part of the mock DSL. - # It is primarily used in the mock DSL to provide this syntax: - # ``` - # allow(dbl).to - # ``` - struct Allow(T) - # Creates the stub target. - # - # The *target* must be a kind of `Stubbable` or `StubbedType`. - def initialize(@target : T) - {% raise "Target of `allow` must be stubbable (a mock or double)." unless T < Stubbable || T < StubbedType %} - end - - # Applies a stub to the targeted stubbable object. - def to(stub : Stub) : Nil - @target._spectator_define_stub(stub) - Harness.current?.try &.cleanup { @target._spectator_remove_stub(stub) } - end - end -end diff --git a/src/spectator/mocks/arguments.cr b/src/spectator/mocks/arguments.cr deleted file mode 100644 index f15a0ba..0000000 --- a/src/spectator/mocks/arguments.cr +++ /dev/null @@ -1,110 +0,0 @@ -require "./abstract_arguments" - -module Spectator - # Arguments used in a method call. - # - # Can also be used to match arguments. - # *Args* must be a `Tuple` representing the standard arguments. - # *KWArgs* must be a `NamedTuple` type representing extra keyword arguments. - class Arguments(Args, KWArgs) < AbstractArguments - # Positional arguments. - getter args : Args - - # Keyword arguments. - getter kwargs : KWArgs - - # Creates arguments used in a method call. - def initialize(@args : Args, @kwargs : KWArgs) - {% raise "Positional arguments (generic type Args) must be a Tuple" unless Args <= Tuple %} - {% raise "Keyword arguments (generic type KWArgs) must be a NamedTuple" unless KWArgs <= NamedTuple %} - end - - # Instance of empty arguments. - class_getter none : AbstractArguments = capture - - # Returns unconstrained arguments. - def self.any : AbstractArguments? - nil.as(AbstractArguments?) - end - - # Friendlier constructor for capturing arguments. - def self.capture(*args, **kwargs) - new(args, kwargs) - end - - # Returns the positional argument at the specified index. - def [](index : Int) - args[index] - end - - # Returns the specified named argument. - def [](arg : Symbol) - @kwargs[arg] - end - - # Returns all arguments and splatted arguments as a tuple. - def positional : Tuple - args - end - - # Returns all named positional and keyword arguments as a named tuple. - def named : NamedTuple - kwargs - end - - # Constructs a string representation of the arguments. - def to_s(io : IO) : Nil - return io << "(no args)" if args.empty? && kwargs.empty? - - io << '(' - - # Add the positional arguments. - args.each_with_index do |arg, i| - io << ", " if i > 0 - arg.inspect(io) - end - - # Add the keyword arguments. - kwargs.each_with_index(args.size) do |key, value, i| - io << ", " if i > 0 - io << key << ": " - value.inspect(io) - end - - io << ')' - end - - # Checks if this set of arguments and another are equal. - def ==(other : AbstractArguments) - positional == other.positional && kwargs == other.kwargs - end - - # Checks if another set of arguments matches this set of arguments. - def ===(other : Arguments) - compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs) - end - - # :ditto: - def ===(other : FormalArguments) - return false unless compare_named_tuples(kwargs, other.named) - - i = 0 - other.args.each do |k, v2| - break if i >= positional.size - next if kwargs.has_key?(k) # Covered by named arguments. - - v1 = positional[i] - i += 1 - return false unless compare_values(v1, v2) - end - - other.splat.try &.each do |v2| - v1 = positional.fetch(i) { return false } - i += 1 - return false unless compare_values(v1, v2) - end - - i == positional.size - end - end -end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr deleted file mode 100644 index e5e43f4..0000000 --- a/src/spectator/mocks/double.cr +++ /dev/null @@ -1,214 +0,0 @@ -require "./arguments" -require "./method_call" -require "./stub" -require "./stubbable" -require "./stubbed_name" -require "./stubbed_type" -require "./unexpected_message" -require "./value_stub" - -module Spectator - # Stands in for an object for testing that a SUT calls expected methods. - # - # Handles all messages (method calls), but only responds to those configured. - # Methods called that were not configured will raise `UnexpectedMessage`. - # Doubles should be defined with the `#define` macro. - # - # Use `#_spectator_define_stub` to override behavior of a method in the double. - # Only methods defined in the double's type can have stubs. - # New methods are not defines when a stub is added that doesn't have a matching method name. - abstract class Double - include Stubbable - extend StubbedType - - Log = Spectator::Log.for(self) - - # Defines a test double type. - # - # The *type_name* is the name to give the class. - # Instances of the double can be named by providing a *name*. - # This can be a symbol, string, or even a type. - # See `StubbedName` for details. - # - # After the names, a collection of key-value pairs can be given to quickly define methods. - # Each key is the method name, and the corresponding value is the value returned by the method. - # These methods accept any arguments. - # Additionally, these methods can be overridden later with stubs. - # - # Lastly, a block can be provided to define additional methods and stubs. - # The block is evaluated in the context of the double's class. - # - # ``` - # Double.define(SomeDouble, meth1: 42, meth2: "foobar") do - # stub abstract def meth3 : Symbol - # - # # Default implementation with a dynamic value. - # stub def meth4 - # Time.utc - # end - # end - # ``` - macro define(type_name, name = nil, **value_methods, &block) - {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} - class {{type_name.id}} < {{@type.name}} - {% for key, value in value_methods %} - default_stub def {{key.id}}(*%args, **%kwargs) - {{value}} - end - - default_stub def {{key.id}}(*%args, **%kwargs, &) - {{key.id}} - end - {% end %} - - {{block.body if block}} - end - end - - @calls = [] of MethodCall - - private class_getter _spectator_stubs : Array(Stub) = [] of Stub - - class_getter _spectator_calls : Array(MethodCall) = [] of MethodCall - - # Creates the double. - # - # An initial set of *stubs* can be provided. - def initialize(@stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub) - end - - # Creates the double. - # - # An initial set of stubs can be provided with *value_methods*. - def initialize(**value_methods) - @stubs = value_methods.map do |key, value| - ValueStub.new(key, value).as(Stub) - end - end - - # Compares against another object. - # - # Always returns false. - # This method exists as a workaround to provide an alternative to `Object#same?`, - # which only accepts a `Reference` or `Nil`. - def same?(other) : Bool - false - end - - # Simplified string representation of a double. - # Avoids displaying nested content and bloating method instantiation. - def to_s(io : IO) : Nil - io << "#<" + {{@type.name(generic_args: false).stringify}} + " " - io << _spectator_stubbed_name << '>' - end - - # :ditto: - def inspect(io : IO) : Nil - io << "#<" + {{@type.name(generic_args: false).stringify}} + " " - io << _spectator_stubbed_name - - io << ":0x" - object_id.to_s(io, 16) - io << '>' - end - - # Defines a stub to change the behavior of a method in this double. - # - # NOTE: Defining a stub for a method not defined in the double's type has no effect. - protected def _spectator_define_stub(stub : Stub) : Nil - Log.debug { "Defined stub for #{inspect} #{stub}" } - @stubs.unshift(stub) - end - - protected def _spectator_remove_stub(stub : Stub) : Nil - Log.debug { "Removing stub #{stub} from #{inspect}" } - @stubs.delete(stub) - end - - protected def _spectator_clear_stubs : Nil - Log.debug { "Clearing stubs for #{inspect}" } - @stubs.clear - end - - private def _spectator_find_stub(call : MethodCall) : Stub? - Log.debug { "Finding stub for #{call}" } - stub = @stubs.find &.===(call) - Log.debug { stub ? "Found stub #{stub} for #{call}" : "Did not find stub for #{call}" } - stub - end - - def _spectator_stub_for_method?(method : Symbol) : Bool - @stubs.any? { |stub| stub.method == method } - end - - def _spectator_record_call(call : MethodCall) : Nil - @calls << call - end - - def _spectator_calls - @calls - end - - def _spectator_clear_calls : Nil - @calls.clear - end - - # Returns the double's name formatted for user output. - private def _spectator_stubbed_name : String - {% if anno = @type.annotation(StubbedName) %} - {{(anno[0] || :Anonymous.id).stringify}} - {% else %} - "Anonymous" - {% end %} - end - - private def self._spectator_stubbed_name : String - {% if anno = @type.annotation(StubbedName) %} - {{(anno[0] || :Anonymous.id).stringify}} - {% else %} - "Anonymous" - {% end %} - end - - private def _spectator_stub_fallback(call : MethodCall, &) - Log.trace { "Fallback for #{call} - call original" } - yield - end - - private def _spectator_stub_fallback(call : MethodCall, type, &) - _spectator_stub_fallback(call) { yield } - end - - private def _spectator_abstract_stub_fallback(call : MethodCall) - Log.info do - break unless _spectator_stub_for_method?(call.method) - - "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." - end - - raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") - end - - private def _spectator_abstract_stub_fallback(call : MethodCall, type) - _spectator_abstract_stub_fallback(call) - end - - # "Hide" existing methods and methods from ancestors by overriding them. - macro finished - stub_type {{@type.name(generic_args: false)}} - end - - # Handle all methods but only respond to configured messages. - # Raises an `UnexpectedMessage` error for non-configures messages. - macro method_missing(call) - args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}}) - call = ::Spectator::MethodCall.new({{call.name.symbolize}}, args) - _spectator_record_call(call) - - Log.trace { "#{inspect} got undefined method `#{call}{% if call.block %} { ... }{% end %}`" } - - raise ::Spectator::UnexpectedMessage.new("#{inspect} received unexpected message #{call}") - nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors". - end - end -end diff --git a/src/spectator/mocks/exception_stub.cr b/src/spectator/mocks/exception_stub.cr deleted file mode 100644 index e7b6cb9..0000000 --- a/src/spectator/mocks/exception_stub.cr +++ /dev/null @@ -1,55 +0,0 @@ -require "../location" -require "./arguments" -require "./stub" -require "./stub_modifiers" - -module Spectator - # Stub that raises an exception. - class ExceptionStub < Stub - # Invokes the stubbed implementation. - def call(call : MethodCall) : Nil - raise @exception - end - - # Returns a new stub with constrained arguments. - def with_constraint(constraint : AbstractArguments?) - self.class.new(method, @exception, constraint, location) - end - - # Creates the stub. - def initialize(method : Symbol, @exception : Exception, constraint : AbstractArguments? = nil, location : Location? = nil) - super(method, constraint, location) - end - - # String representation of the stub, formatted as a method call. - def to_s(io : IO) : Nil - super - io << " # raises " << @exception - end - end - - module StubModifiers - # Returns a new stub that raises an exception. - def and_raise(exception : Exception) - ExceptionStub.new(method, exception, constraint, location) - end - - # :ditto: - def and_raise(exception_class : Exception.class, message) - exception = exception_class.new(message) - and_raise(exception) - end - - # :ditto: - def and_raise(message : String? = nil) - exception = Exception.new(message) - and_raise(exception) - end - - # :ditto: - def and_raise(exception_class : Exception.class) - exception = exception_class.new - and_raise(exception) - end - end -end diff --git a/src/spectator/mocks/formal_arguments.cr b/src/spectator/mocks/formal_arguments.cr deleted file mode 100644 index 1c0ca69..0000000 --- a/src/spectator/mocks/formal_arguments.cr +++ /dev/null @@ -1,133 +0,0 @@ -require "./abstract_arguments" - -module Spectator - # Arguments passed into a method. - # - # *Args* must be a `NamedTuple` type representing the standard arguments. - # *Splat* must be a `Tuple` type representing the extra positional arguments. - # *DoubleSplat* must be a `NamedTuple` type representing extra keyword arguments. - class FormalArguments(Args, Splat, DoubleSplat) < AbstractArguments - # Positional arguments. - getter args : Args - - # Additional positional arguments. - getter splat : Splat - - # Keyword arguments. - getter kwargs : DoubleSplat - - # Name of the splat argument, if used. - getter splat_name : Symbol? - - # Creates arguments used in a method call. - def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat) - {% raise "Positional arguments (generic type Args) must be a NamedTuple" unless Args <= NamedTuple %} - {% raise "Splat arguments (generic type Splat) must be a Tuple" unless Splat <= Tuple || Splat <= Nil %} - {% raise "Keyword arguments (generic type DoubleSplat) must be a NamedTuple" unless DoubleSplat <= NamedTuple %} - end - - # Creates arguments used in a method call. - def self.new(args : Args, kwargs : DoubleSplat) - new(args, nil, nil, kwargs) - end - - # Captures arguments passed to a call. - def self.build(args = NamedTuple.new, kwargs = NamedTuple.new) - new(args, nil, nil, kwargs) - end - - # :ditto: - def self.build(args : NamedTuple, splat_name : Symbol, splat : Tuple, kwargs = NamedTuple.new) - new(args, splat_name, splat, kwargs) - end - - # Instance of empty arguments. - class_getter none : AbstractArguments = build - - # Returns the positional argument at the specified index. - def [](index : Int) - positional[index] - end - - # Returns the specified named argument. - def [](arg : Symbol) - return @args[arg] if @args.has_key?(arg) - @kwargs[arg] - end - - # Returns all arguments and splatted arguments as a tuple. - def positional : Tuple - if (splat = @splat) - args.values + splat - else - args.values - end - end - - # Returns all named positional and keyword arguments as a named tuple. - def named : NamedTuple - args.merge(kwargs) - end - - # Constructs a string representation of the arguments. - def to_s(io : IO) : Nil - return io << "(no args)" if args.empty? && ((splat = @splat).nil? || splat.empty?) && kwargs.empty? - - io << '(' - - # Add the positional arguments. - {% if Args < NamedTuple %} - # Include argument names. - args.each_with_index do |name, value, i| - io << ", " if i > 0 - io << name << ": " - value.inspect(io) - end - {% else %} - args.each_with_index do |arg, i| - io << ", " if i > 0 - arg.inspect(io) - end - {% end %} - - # Add the splat arguments. - if (splat = @splat) && !splat.empty? - io << ", " unless args.empty? - if splat_name = !args.empty? && @splat_name - io << '*' << splat_name << ": {" - end - splat.each_with_index do |arg, i| - io << ", " if i > 0 - arg.inspect(io) - end - io << '}' if splat_name - end - - # Add the keyword arguments. - offset = args.size - offset += splat.size if (splat = @splat) - kwargs.each_with_index(offset) do |key, value, i| - io << ", " if i > 0 - io << key << ": " - value.inspect(io) - end - - io << ')' - end - - # Checks if this set of arguments and another are equal. - def ==(other : AbstractArguments) - positional == other.positional && kwargs == other.kwargs - end - - # Checks if another set of arguments matches this set of arguments. - def ===(other : Arguments) - compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs) - end - - # :ditto: - def ===(other : FormalArguments) - compare_named_tuples(args, other.args) && compare_tuples(splat, other.splat) && compare_named_tuples(kwargs, other.kwargs) - end - end -end diff --git a/src/spectator/mocks/lazy_double.cr b/src/spectator/mocks/lazy_double.cr deleted file mode 100644 index 75fe30c..0000000 --- a/src/spectator/mocks/lazy_double.cr +++ /dev/null @@ -1,91 +0,0 @@ -require "../label" -require "./arguments" -require "./double" -require "./method_call" -require "./stub" -require "./value_stub" - -module Spectator - # Stands in for an object for testing that a SUT calls expected methods. - # - # Handles all messages (method calls), but only responds to those configured. - # Methods called that were not configured will raise `UnexpectedMessage`. - # - # Use `#_spectator_define_stub` to override behavior of a method in the double. - # Only methods defined in the double's type can have stubs. - # New methods are not defines when a stub is added that doesn't have a matching method name. - class LazyDouble(Messages) < Double - @name : String? - - def initialize(_spectator_double_name = nil, _spectator_double_stubs = [] of Stub, **@messages : **Messages) - @name = _spectator_double_name.try &.inspect - message_stubs = messages.map do |method, value| - ValueStub.new(method, value) - end - - super(_spectator_double_stubs + message_stubs) - end - - # Defines a stub to change the behavior of a method in this double. - # - # NOTE: Defining a stub for a method not defined in the double's type raises an error. - protected def _spectator_define_stub(stub : Stub) : Nil - return super if Messages.types.has_key?(stub.method) - - raise "Can't define stub #{stub} on lazy double because it wasn't initially defined." - end - - # Returns the double's name formatted for user output. - private def _spectator_stubbed_name : String - @name || "Anonymous" - end - - private def _spectator_stub_fallback(call : MethodCall, &) - if _spectator_stub_for_method?(call.method) - Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." } - raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") - else - Log.trace { "Fallback for #{call} - call original" } - yield - end - end - - # Handles all messages. - macro method_missing(call) - # Capture information about the call. - %args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}}) - %call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args) - _spectator_record_call(%call) - - Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" } - - # Attempt to find a stub that satisfies the method call and arguments. - if %stub = _spectator_find_stub(%call) - # Cast the stub or return value to the expected type. - # This is necessary to match the expected return type of the original message. - \{% if Messages.keys.includes?({{call.name.symbolize}}) %} - _spectator_cast_stub_value(%stub, %call, \{{Messages[{{call.name.symbolize}}.id]}}) - \{% else %} - # A method that was not defined during initialization was stubbed. - # Even though all stubs will have a #call method, the compiler doesn't seem to agree. - # Assert that it will (this should never fail). - raise TypeCastError.new("Stub has no value") unless %stub.responds_to?(:call) - - # Return the value of the stub as-is. - # Might want to give a warning here, as this may produce a "bloated" union of all known stub types. - %stub.call(%call) - \{% end %} - else - # A stub wasn't found, invoke the fallback logic. - \{% if Messages.keys.includes?({{call.name.symbolize}}.id) %} - # Pass along the message type and a block to invoke it. - _spectator_stub_fallback(%call, \{{Messages[{{call.name.symbolize}}.id]}}) { @messages[{{call.name.symbolize}}] } - \{% else %} - # Message received for a methods that isn't stubbed nor defined when initialized. - _spectator_abstract_stub_fallback(%call) - nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors". - \{% end %} - end - end - end -end diff --git a/src/spectator/mocks/method_call.cr b/src/spectator/mocks/method_call.cr deleted file mode 100644 index 9c5fd01..0000000 --- a/src/spectator/mocks/method_call.cr +++ /dev/null @@ -1,42 +0,0 @@ -require "./abstract_arguments" -require "./arguments" -require "./formal_arguments" - -module Spectator - # Stores information about a call to a method. - class MethodCall - # Name of the method. - getter method : Symbol - - # Arguments passed to the method. - getter arguments : AbstractArguments - - # Creates a method call. - def initialize(@method : Symbol, @arguments : AbstractArguments = Arguments.none) - end - - # Creates a method call by splatting its arguments. - def self.capture(method : Symbol, *args, **kwargs) - arguments = Arguments.capture(*args, **kwargs).as(AbstractArguments) - new(method, arguments) - end - - # Creates a method call from within a method. - # Takes the same arguments as `FormalArguments.build` but with the method name first. - def self.build(method : Symbol, *args, **kwargs) - arguments = FormalArguments.build(*args, **kwargs).as(AbstractArguments) - new(method, arguments) - end - - # Constructs a string containing the method name and arguments. - def to_s(io : IO) : Nil - io << '#' << method - arguments.inspect(io) - end - - # :ditto: - def inspect(io : IO) : Nil - to_s(io) - end - end -end diff --git a/src/spectator/mocks/mock.cr b/src/spectator/mocks/mock.cr deleted file mode 100644 index 87e8e23..0000000 --- a/src/spectator/mocks/mock.cr +++ /dev/null @@ -1,227 +0,0 @@ -require "./method_call" -require "./mocked" -require "./mock_registry" -require "./reference_mock_registry" -require "./stub" -require "./stubbed_name" -require "./stubbed_type" -require "./value_mock_registry" -require "./value_stub" - -module Spectator - # Module providing macros for defining new mocks from existing types and injecting mock features into concrete types. - module Mock - # Defines a type that inherits from another, existing type. - # The newly defined subtype will have mocking functionality. - # - # Methods from the inherited type will be overridden to support stubs. - # *base* is the keyword for the type being defined - class or struct. - # *mocked_type* is the original type to inherit from. - # *type_name* is the name of the new type to define. - # An optional *name* of the mock can be provided. - # Any key-value pairs provided with *value_methods* are used as initial stubs for the mocked type. - # - # A block can be provided to define additional methods and stubs. - # The block is evaluated in the context of the derived type. - # - # ``` - # Mock.define_subtype(:class, SomeType, meth1: 42, meth2: "foobar") do - # stub abstract def meth3 : Symbol - # - # # Default implementation with a dynamic value. - # stub def meth4 - # Time.utc - # end - # end - # ``` - macro define_subtype(base, mocked_type, type_name, name = nil, **value_methods, &block) - {% begin %} - {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} - {% if base.id == :module.id %} - {{base.id}} {{type_name.id}} - include {{mocked_type.id}} - - # Mock class that includes the mocked module {{mocked_type.id}} - {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} - private class ClassIncludingMock{{type_name.id}} - include {{type_name.id}} - end - - # Returns a mock class that includes the mocked module {{mocked_type.id}}. - def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}} - # FIXME: Creating the instance normally with `.new` causing infinite recursion. - inst = ClassIncludingMock{{type_name.id}}.allocate - inst.initialize(*args, **kwargs) - inst - end - - # Returns a mock class that includes the mocked module {{mocked_type.id}}. - def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}} - # FIXME: Creating the instance normally with `.new` causing infinite recursion. - inst = ClassIncludingMock{{type_name.id}}.allocate - inst.initialize(*args, **kwargs) { |*yargs| yield *yargs } - inst - end - - {% else %} - {{base.id}} {{type_name.id}} < {{mocked_type.id}} - {% end %} - include ::Spectator::Mocked - extend ::Spectator::StubbedType - - {% begin %} - private getter(_spectator_stubs) do - [ - {% for key, value in value_methods %} - ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}), - {% end %} - ] of ::Spectator::Stub - end - {% end %} - - def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil - @_spectator_stubs.try &.delete(stub) - end - - def _spectator_clear_stubs : ::Nil - @_spectator_stubs = nil - end - - private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub - - class_getter _spectator_calls : ::Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall - - getter _spectator_calls = [] of ::Spectator::MethodCall - - # Returns the mock's name formatted for user output. - private def _spectator_stubbed_name : ::String - \{% if anno = @type.annotation(::Spectator::StubbedName) %} - "#" - \{% else %} - "#" - \{% end %} - end - - private def self._spectator_stubbed_name : ::String - \{% if anno = @type.annotation(::Spectator::StubbedName) %} - "#" - \{% else %} - "#" - \{% end %} - end - - macro finished - stub_type {{mocked_type.id}} - - {{block.body if block}} - end - end - {% end %} - end - - # Injects mock functionality into an existing type. - # - # Generally this method of mocking should be avoiding. - # It modifies types being tested, the mock functionality won't exist outside of tests. - # This option should only be used when sub-types are not possible (e.g. concrete struct). - # - # Methods in the type will be overridden to support stubs. - # The original method functionality will still be accessible, but pass through mock code first. - # *base* is the keyword for the type being defined - class or struct. - # *type_name* is the name of the type to inject mock functionality into. - # This _must_ be full, resolvable path to the type. - # An optional *name* of the mock can be provided. - # Any key-value pairs provided with *value_methods* are used as initial stubs for the mocked type. - # - # A block can be provided to define additional methods and stubs. - # The block is evaluated in the context of the derived type. - # - # ``` - # Mock.inject(:struct, SomeType, meth1: 42, meth2: "foobar") do - # stub abstract def meth3 : Symbol - # - # # Default implementation with a dynamic value. - # stub def meth4 - # Time.utc - # end - # end - # ``` - macro inject(base, type_name, name = nil, **value_methods, &block) - {% begin %} - {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} - {{base.id}} ::{{type_name.id}} - include ::Spectator::Mocked - extend ::Spectator::StubbedType - - {% if base == :class %} - @@_spectator_mock_registry = ::Spectator::ReferenceMockRegistry.new - {% elsif base == :struct %} - @@_spectator_mock_registry = ::Spectator::ValueMockRegistry(self).new - {% else %} - @@_spectator_mock_registry = ::Spectator::MockRegistry.new - {% end %} - - private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub - - class_getter _spectator_calls : ::Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall - - private def _spectator_stubs - entry = @@_spectator_mock_registry.fetch(self) do - _spectator_default_stubs - end - entry.stubs - end - - def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil - @@_spectator_mock_registry[self]?.try &.stubs.delete(stub) - end - - def _spectator_clear_stubs : ::Nil - @@_spectator_mock_registry.delete(self) - end - - def _spectator_calls - entry = @@_spectator_mock_registry.fetch(self) do - _spectator_default_stubs - end - entry.calls - end - - private def _spectator_default_stubs - {% begin %} - [ - {% for key, value in value_methods %} - ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}), - {% end %} - ] of ::Spectator::Stub - {% end %} - end - - # Returns the mock's name formatted for user output. - private def _spectator_stubbed_name : ::String - \{% if anno = @type.annotation(::Spectator::StubbedName) %} - "#" - \{% else %} - "#" - \{% end %} - end - - # Returns the mock's name formatted for user output. - private def self._spectator_stubbed_name : ::String - \{% if anno = @type.annotation(::Spectator::StubbedName) %} - "#" - \{% else %} - "#" - \{% end %} - end - - macro finished - stub_type {{type_name.id}} - - {{block.body if block}} - end - end - {% end %} - end - end -end diff --git a/src/spectator/mocks/mock_registry.cr b/src/spectator/mocks/mock_registry.cr deleted file mode 100644 index 29390c6..0000000 --- a/src/spectator/mocks/mock_registry.cr +++ /dev/null @@ -1,43 +0,0 @@ -require "./mock_registry_entry" -require "./stub" - -module Spectator - # Stores collections of stubs for mocked types. - # - # This type is intended for all mocked modules that have functionality "injected." - # That is, the type itself has mock functionality bolted on. - # Adding instance members should be avoided, for instance, it could mess up serialization. - class MockRegistry - @entry : MockRegistryEntry? - - # Retrieves all stubs. - def [](_object = nil) - @entry.not_nil! - end - - # Retrieves all stubs. - def []?(_object = nil) - @entry - end - - # Retrieves all stubs. - # - # Yields to the block on the first retrieval. - # This allows a mock to populate the registry with initial stubs. - def fetch(object : Reference, & : -> Array(Stub)) - entry = @entry - if entry.nil? - entry = MockRegistryEntry.new - entry.stubs = yield - @entry = entry - else - entry - end - end - - # Clears all stubs defined for a mocked object. - def delete(object : Reference) : Nil - @entry = nil - end - end -end diff --git a/src/spectator/mocks/mock_registry_entry.cr b/src/spectator/mocks/mock_registry_entry.cr deleted file mode 100644 index 9c6670a..0000000 --- a/src/spectator/mocks/mock_registry_entry.cr +++ /dev/null @@ -1,13 +0,0 @@ -require "./method_call" -require "./stub" - -module Spectator - # Stubs and calls for a mock. - private struct MockRegistryEntry - # Retrieves all stubs defined for a mock. - property stubs = [] of Stub - - # Retrieves all calls to stubbed methods. - getter calls = [] of MethodCall - end -end diff --git a/src/spectator/mocks/mocked.cr b/src/spectator/mocks/mocked.cr deleted file mode 100644 index 280eef8..0000000 --- a/src/spectator/mocks/mocked.cr +++ /dev/null @@ -1,127 +0,0 @@ -require "./method_call" -require "./stub" -require "./stubbable" -require "./unexpected_message" - -module Spectator - # Mix-in used for mocked types. - # - # Bridges functionality between mocks and stubs - # Implements the abstracts methods from `Stubbable`. - # - # Types including this module will need to implement `#_spectator_stubs`. - # It should return a mutable list of stubs. - # This is used to store the stubs for the mocked type. - # - # Additionally, the `#_spectator_calls` (getter with no arguments) must be implemented. - # It should return a mutable list of method calls. - # This is used to store the calls to stubs for the mocked type. - module Mocked - include Stubbable - - # Retrieves an mutable collection of stubs. - abstract def _spectator_stubs - - def _spectator_define_stub(stub : ::Spectator::Stub) : Nil - _spectator_stubs.unshift(stub) - end - - def _spectator_remove_stub(stub : Stub) : Nil - _spectator_stubs.delete(stub) - end - - def _spectator_clear_stubs : Nil - _spectator_stubs.clear - end - - private def _spectator_find_stub(call : ::Spectator::MethodCall) : ::Spectator::Stub? - _spectator_stubs.find &.===(call) - end - - def _spectator_stub_for_method?(method : Symbol) : Bool - _spectator_stubs.any? { |stub| stub.method == method } - end - - def _spectator_record_call(call : MethodCall) : Nil - _spectator_calls << call - end - - def _spectator_calls(method : Symbol) : Enumerable(MethodCall) - _spectator_calls.select { |call| call.method == method } - end - - def _spectator_clear_calls : Nil - _spectator_calls.clear - end - - # Method called when a stub isn't found. - # - # The received message is captured in *call*. - # Yield to call the original method's implementation. - # The stubbed method returns the value returned by this method. - # This method can also raise an error if it's impossible to return something. - def _spectator_stub_fallback(call : MethodCall, &) - if _spectator_stub_for_method?(call.method) - Spectator::Log.info do # FIXME: Don't log to top-level Spectator logger (use mock or double logger). - "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." - end - - raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}") - end - - yield # Uninteresting message, allow through. - end - - # Method called when a stub isn't found. - # - # The received message is captured in *call*. - # The expected return type is provided by *type*. - # Yield to call the original method's implementation. - # The stubbed method returns the value returned by this method. - # This method can also raise an error if it's impossible to return something. - def _spectator_stub_fallback(call : MethodCall, type, &) - value = _spectator_stub_fallback(call) { yield } - - begin - type.cast(value) - rescue TypeCastError - raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `#{value.inspect}`, but returned type must be `#{type}`.") - end - end - - # Method called when a stub isn't found. - # - # This is similar to `#_spectator_stub_fallback`, - # but called when the original (un-stubbed) method isn't available. - # The received message is captured in *call*. - # The stubbed method returns the value returned by this method. - # This method can also raise an error if it's impossible to return something. - def _spectator_abstract_stub_fallback(call : MethodCall) - Spectator::Log.info do # FIXME: Don't log to top-level Spectator logger (use mock or double logger). - break unless _spectator_stub_for_method?(call.method) - - "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." - end - - raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}") - end - - # Method called when a stub isn't found. - # - # This is similar to `#_spectator_stub_fallback`, - # but called when the original (un-stubbed) method isn't available. - # The received message is captured in *call*. - # The expected return type is provided by *type*. - # The stubbed method returns the value returned by this method. - # This method can also raise an error if it's impossible to return something. - def _spectator_abstract_stub_fallback(call : MethodCall, type) - value = _spectator_abstract_stub_fallback(call) - - begin - type.cast(value) - rescue TypeCastError - raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `#{value.inspect}`, but returned type must be `#{type}`.") - end - end - end -end diff --git a/src/spectator/mocks/multi_value_stub.cr b/src/spectator/mocks/multi_value_stub.cr deleted file mode 100644 index 8e64db5..0000000 --- a/src/spectator/mocks/multi_value_stub.cr +++ /dev/null @@ -1,35 +0,0 @@ -require "../location" -require "./arguments" -require "./stub_modifiers" -require "./typed_stub" - -module Spectator - # Stub that responds with a multiple values in succession. - class MultiValueStub(T) < TypedStub(T) - # Invokes the stubbed implementation. - def call(call : MethodCall) : T - if @values.size == 1 - @values.first - else - @values.shift - end - end - - # Returns a new stub with constrained arguments. - def with_constraint(constraint : AbstractArguments?) - self.class.new(method, @values, constraint, location) - end - - # Creates the stub. - def initialize(method : Symbol, @values : Array(T), constraint : AbstractArguments? = nil, location : Location? = nil) - super(method, constraint, location) - end - end - - module StubModifiers - # Returns a new stub that returns multiple values in succession. - def and_return(value, *values) - MultiValueStub.new(method, [value, *values], constraint, location) - end - end -end diff --git a/src/spectator/mocks/null_double.cr b/src/spectator/mocks/null_double.cr deleted file mode 100644 index 587f4ab..0000000 --- a/src/spectator/mocks/null_double.cr +++ /dev/null @@ -1,64 +0,0 @@ -require "./double" -require "./method_call" -require "./stubbed_name" -require "./unexpected_message" - -module Spectator - # Stands in for an object for testing that a SUT calls expected methods. - # - # Handles all messages (method calls), but only responds to those configured. - # Methods called that were not configured will return self. - # Doubles should be defined with the `#define` macro. - # - # Use `#_spectator_define_stub` to override behavior of a method in the double. - # Only methods defined in the double's type can have stubs. - # New methods are not defines when a stub is added that doesn't have a matching method name. - abstract class NullDouble < Double - # Returns the double's name formatted for user output. - private def _spectator_stubbed_name : String - {% if anno = @type.annotation(StubbedName) %} - "#" - {% else %} - "#" - {% end %} - end - - private def _spectator_abstract_stub_fallback(call : MethodCall) - if _spectator_stub_for_method?(call.method) - Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." } - raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") - else - Log.trace { "Fallback for #{call} - return self" } - self - end - end - - # Specialization that matches when the return type matches self. - private def _spectator_abstract_stub_fallback(call : MethodCall, _type : self) - _spectator_abstract_stub_fallback(call) - end - - # Default implementation that raises a `TypeCastError` since the return type isn't self. - private def _spectator_abstract_stub_fallback(call : MethodCall, type) - if _spectator_stub_for_method?(call.method) - Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." } - raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") - else - raise TypeCastError.new("#{inspect} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.") - end - end - - # Handles all undefined messages. - # Returns stubbed values if available, otherwise delegates to `#_spectator_abstract_stub_fallback`. - macro method_missing(call) - # Capture information about the call. - %args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}}) - %call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args) - _spectator_record_call(%call) - - Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" } - - self - end - end -end diff --git a/src/spectator/mocks/null_stub.cr b/src/spectator/mocks/null_stub.cr deleted file mode 100644 index f5f0854..0000000 --- a/src/spectator/mocks/null_stub.cr +++ /dev/null @@ -1,16 +0,0 @@ -require "./typed_stub" -require "./value_stub" - -module Spectator - # Stub that does nothing and returns nil. - class NullStub < TypedStub(Nil) - # Invokes the stubbed implementation. - def call(call : MethodCall) : Nil - end - - # Returns a new stub with constrained arguments. - def with_constraint(constraint : AbstractArguments?) - self.class.new(method, constraint, location) - end - end -end diff --git a/src/spectator/mocks/proc_stub.cr b/src/spectator/mocks/proc_stub.cr deleted file mode 100644 index 7bdd0f2..0000000 --- a/src/spectator/mocks/proc_stub.cr +++ /dev/null @@ -1,36 +0,0 @@ -require "../location" -require "./arguments" -require "./typed_stub" - -module Spectator - # Stub that responds with a value returned by calling a proc. - class ProcStub(T) < TypedStub(T) - # Invokes the stubbed implementation. - def call(call : MethodCall) : T - @proc.call(call.arguments) - end - - # Returns a new stub with constrained arguments. - def with_constraint(constraint : AbstractArguments?) - self.class.new(method, @proc, constraint, location) - end - - # Creates the stub. - def initialize(method : Symbol, @proc : Proc(AbstractArguments, T), constraint : AbstractArguments? = nil, location : Location? = nil) - super(method, constraint, location) - end - - # Creates the stub. - def initialize(method : Symbol, constraint : AbstractArguments? = nil, location : Location? = nil, &block : Proc(AbstractArguments, T)) - initialize(method, block, constraint, location) - end - end - - module StubModifiers - # Returns a new stub with an argument constraint. - def with(*args, **kwargs, &block : AbstractArguments -> T) forall T - constraint = Arguments.new(args, kwargs).as(AbstractArguments) - ProcStub(T).new(method, block, constraint, location) - end - end -end diff --git a/src/spectator/mocks/reference_mock_registry.cr b/src/spectator/mocks/reference_mock_registry.cr deleted file mode 100644 index 84227d1..0000000 --- a/src/spectator/mocks/reference_mock_registry.cr +++ /dev/null @@ -1,53 +0,0 @@ -require "./mock_registry_entry" -require "./stub" - -module Spectator - # Stores collections of stubs for mocked reference (class) types. - # - # This type is intended for all mocked reference types that have functionality "injected." - # That is, the type itself has mock functionality bolted on. - # Adding instance members should be avoided, for instance, it could mess up serialization. - # This registry works around that by mapping mocks (via their memory address) to a collection of stubs. - # Doing so prevents adding data to the mocked type. - class ReferenceMockRegistry - @entries : Hash(Void*, MockRegistryEntry) - - # Creates an empty registry. - def initialize - @entries = Hash(Void*, MockRegistryEntry).new do |hash, key| - hash[key] = MockRegistryEntry.new - end - end - - # Retrieves all stubs defined for a mocked object. - def [](object : Reference) - key = Box.box(object) - @entries[key] - end - - # Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet. - def []?(object : Reference) - key = Box.box(object) - @entries[key]? - end - - # Retrieves all stubs defined for a mocked object. - # - # Yields to the block on the first retrieval. - # This allows a mock to populate the registry with initial stubs. - def fetch(object : Reference, & : -> Array(Stub)) - key = Box.box(object) - @entries.fetch(key) do - entry = MockRegistryEntry.new - entry.stubs = yield - @entries[key] = entry - end - end - - # Clears all stubs defined for a mocked object. - def delete(object : Reference) : Nil - key = Box.box(object) - @entries.delete(key) - end - end -end diff --git a/src/spectator/mocks/stub.cr b/src/spectator/mocks/stub.cr deleted file mode 100644 index 606894d..0000000 --- a/src/spectator/mocks/stub.cr +++ /dev/null @@ -1,50 +0,0 @@ -require "./abstract_arguments" -require "./arguments" -require "./method_call" -require "./stub_modifiers" - -module Spectator - # Untyped response to a method call (message). - abstract class Stub - include StubModifiers - - # Name of the method this stub is for. - getter method : Symbol - - # Arguments the method must have been called with to provide this response. - # Is nil when there's no constraint - only the method name must match. - getter constraint : AbstractArguments? - - # Location the stub was defined. - getter location : Location? - - # Creates the base of the stub. - def initialize(@method : Symbol, @constraint : AbstractArguments? = nil, @location : Location? = nil) - end - - # String representation of the stub, formatted as a method call. - def message(io : IO) : Nil - io << "#" << method << (constraint || "(any args)") - end - - # String representation of the stub, formatted as a method call. - def message - String.build do |str| - message(str) - end - end - - # String representation of the stub, formatted as a method definition. - def to_s(io : IO) : Nil - message(io) - end - - # Checks if a method call should receive the response from this stub. - def ===(call : MethodCall) - return false if method != call.method - return true unless constraint = @constraint - - constraint === call.arguments - end - end -end diff --git a/src/spectator/mocks/stub_modifiers.cr b/src/spectator/mocks/stub_modifiers.cr deleted file mode 100644 index a545634..0000000 --- a/src/spectator/mocks/stub_modifiers.cr +++ /dev/null @@ -1,21 +0,0 @@ -require "./arguments" - -module Spectator - # Mixin intended for `Stub` to return new, modified stubs. - module StubModifiers - # Returns a new stub of the same type with constrained arguments. - abstract def with_constraint(constraint : AbstractArguments?) - - # :ditto: - @[AlwaysInline] - def with(constraint : AbstractArguments?) - with_constraint(constraint) - end - - # :ditto: - def with(*args, **kwargs) - constraint = Arguments.new(args, kwargs).as(AbstractArguments) - self.with_constraint(constraint) - end - end -end diff --git a/src/spectator/mocks/stubbable.cr b/src/spectator/mocks/stubbable.cr deleted file mode 100644 index 3385ad4..0000000 --- a/src/spectator/mocks/stubbable.cr +++ /dev/null @@ -1,567 +0,0 @@ -require "../dsl/reserved" -require "./formal_arguments" -require "./method_call" -require "./stub" -require "./typed_stub" - -module Spectator - # Mix-in for mocks and doubles providing method stubs. - # - # Macros in this module can override existing methods. - # Stubbed methods will look for stubs to evaluate in place of their original functionality. - # The primary macro of interest is `#stub`. - # The macros are intended to be called from within the type being stubbed. - # - # Types including this module must define `#_spectator_find_stub` and `#_spectator_stubbed_name`. - # These are internal, reserved method names by Spectator, hence the `_spectator` prefix. - # These methods can't (and shouldn't) be stubbed. - module Stubbable - # Attempts to find a stub that satisfies a method call. - # - # Returns a stub that matches the method *call* - # or nil if no stubs satisfy it. - abstract def _spectator_find_stub(call : MethodCall) : Stub? - - # Utility method that looks for stubs for methods with the name specified. - abstract def _spectator_stub_for_method?(method : Symbol) : Bool - - # Defines a stub to change the behavior of a method. - abstract def _spectator_define_stub(stub : Stub) : Nil - - # Removes a specific, previously defined stub. - abstract def _spectator_remove_stub(stub : Stub) : Nil - - # Clears all previously defined stubs. - abstract def _spectator_clear_stubs : Nil - - # Saves a call that was made to a stubbed method. - abstract def _spectator_record_call(call : MethodCall) : Nil - - # Retrieves all previously saved calls. - abstract def _spectator_calls - - # Clears all previously saved calls. - abstract def _spectator_clear_calls : Nil - - # Method called when a stub isn't found. - # - # The received message is captured in *call*. - # Yield to call the original method's implementation. - # The stubbed method returns the value returned by this method. - # This method can also raise an error if it's impossible to return something. - abstract def _spectator_stub_fallback(call : MethodCall, &) - - # Method called when a stub isn't found. - # - # The received message is captured in *call*. - # The expected return type is provided by *type*. - # Yield to call the original method's implementation. - # The stubbed method returns the value returned by this method. - # This method can also raise an error if it's impossible to return something. - abstract def _spectator_stub_fallback(call : MethodCall, type, &) - - # Method called when a stub isn't found. - # - # This is similar to `#_spectator_stub_fallback`, - # but called when the original (un-stubbed) method isn't available. - # The received message is captured in *call*. - # The stubbed method returns the value returned by this method. - # This method can also raise an error if it's impossible to return something. - abstract def _spectator_abstract_stub_fallback(call : MethodCall) - - # Method called when a stub isn't found. - # - # This is similar to `#_spectator_stub_fallback`, - # but called when the original (un-stubbed) method isn't available. - # The received message is captured in *call*. - # The expected return type is provided by *type*. - # The stubbed method returns the value returned by this method. - # This method can also raise an error if it's impossible to return something. - abstract def _spectator_abstract_stub_fallback(call : MethodCall, type) - - # Utility method returning the stubbed type's name formatted for user output. - abstract def _spectator_stubbed_name : String - - # Clears all previously defined calls and stubs. - def _spectator_reset : Nil - _spectator_clear_calls - _spectator_clear_stubs - end - - # Redefines a method to accept stubs and provides a default response. - # - # The *method* must be a `Def`. - # That is, a normal looking method definition should follow the `default_stub` keyword. - # - # ``` - # default_stub def stubbed_method - # "foobar" - # end - # ``` - # - # The method cannot be abstract, as this method requires a default (fallback) response if a stub isn't provided. - # - # Stubbed methods will call `#_spectator_find_stub` with the method call information. - # If no stub is found, then `#_spectator_stub_fallback` is called. - # The block provided to `#_spectator_stub_fallback` will invoke the default response. - # In other words, `#_spectator_stub_fallback` should yield if it's appropriate to return the default response. - private macro default_stub(method) - {% if method.is_a?(Def) - visibility = method.visibility - elsif method.is_a?(VisibilityModifier) && method.exp.is_a?(Def) - visibility = method.visibility - method = method.exp - else - raise "`default_stub` requires a method definition" - end %} - {% raise "Cannot define a stub inside a method" if @def %} - {% raise "Default stub cannot be an abstract method" if method.abstract? %} - {% raise "Cannot stub method with reserved keyword as name - #{method.name}" if method.name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %} - - {{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( - {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} - {% if method.double_splat %}**{{method.double_splat}}, {% end %} - {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} - ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} - {{method.body}} - end - - {% original = "previous_def" - # Workaround for Crystal not propagating block with previous_def/super. - if method.accepts_block? - original += "(" - if method.splat_index - method.args.each_with_index do |arg, i| - if i == method.splat_index - if arg.internal_name && arg.internal_name.size > 0 - original += "*#{arg.internal_name}, " - end - original += "**#{method.double_splat}, " if method.double_splat - elsif i > method.splat_index - original += "#{arg.name}: #{arg.internal_name}, " - else - original += "#{arg.internal_name}, " - end - end - else - method.args.each do |arg| - original += "#{arg.internal_name}, " - end - original += "**#{method.double_splat}, " if method.double_splat - end - # If the block is captured (i.e. `&block` syntax), it must be passed along as an argument. - # Otherwise, use `yield` to forward the block. - captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0 - method.block_arg.name - else - nil - end - original += "&#{captured_block}" if captured_block - original += ")" - original += " { |*_spectator_yargs| yield *_spectator_yargs }" unless captured_block - end - original = original.id %} - - {% # Reconstruct the method signature. -# I wish there was a better way of doing this, but there isn't (at least not that I'm aware of). -# This chunk of code must reconstruct the method signature exactly as it was originally. -# If it doesn't match, it doesn't override the method and the stubbing won't work. - %} - {{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( - {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} - {% if method.double_splat %}**{{method.double_splat}}, {% end %} - {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} - ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} - - # Capture information about the call. - %call = ::Spectator::MethodCall.build( - {{method.name.symbolize}}, - ::NamedTuple.new( - {% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} - ), - {% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %} - ::NamedTuple.new( - {% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} - ).merge({{method.double_splat}}) - ) - _spectator_record_call(%call) - - # Attempt to find a stub that satisfies the method call and arguments. - # Finding a suitable stub is delegated to the type including the `Stubbable` module. - if %stub = _spectator_find_stub(%call) - # Cast the stub or return value to the expected type. - # This is necessary to match the expected return type of the original method. - _spectator_cast_stub_value(%stub, %call, typeof({{original}}), - {{ if rt = method.return_type - if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn - :no_return - else - # Process as an enumerable type to reduce code repetition. - rt = rt.is_a?(Union) ? rt.types : [rt] - # Check if any types are nilable. - nilable = rt.any? do |t| - # These are all macro types that have the `resolve?` method. - (t.is_a?(TypeNode) || t.is_a?(Path) || t.is_a?(Generic) || t.is_a?(MetaClass)) && - (resolved = t.resolve?).is_a?(TypeNode) && resolved <= Nil - end - if nilable - :nil - else - :raise - end - end - else - :raise - end }}) - else - # Delegate missing stub behavior to concrete type. - _spectator_stub_fallback(%call, typeof({{original}})) do - # Use the default response for the method. - {{original}} - end - end - end - end - - # Redefines a method to require stubs. - # - # This macro is similar to `#default_stub` but requires that a stub is defined for the method if it's called. - # - # The *method* should be a `Def`. - # That is, a normal looking method definition should follow the `stub` keyword. - # - # ``` - # abstract_stub def stubbed_method - # "foobar" - # end - # ``` - # - # The method being stubbed doesn't need to exist yet. - # Its body of the method passed to this macro is ignored. - # The method can be abstract. - # It should have a return type annotation, otherwise the compiled return type will probably end up as a giant union. - # - # ``` - # abstract_stub abstract def stubbed_method : String - # ``` - # - # Stubbed methods will call `#_spectator_find_stub` with the method call information. - # If no stub is found, then `#_spectator_stub_fallback` or `#_spectator_abstract_stub_fallback` is called. - private macro abstract_stub(method) - {% if method.is_a?(Def) - visibility = method.visibility - elsif method.is_a?(VisibilityModifier) && method.exp.is_a?(Def) - visibility = method.visibility - method = method.exp - else - raise "`abstract_stub` requires a method definition" - end %} - {% raise "Cannot define a stub inside a method" if @def %} - {% raise "Cannot stub method with reserved keyword as name - #{method.name}" if method.name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %} - - {% # The logic in this macro follows mostly the same logic from `#default_stub`. -# The main difference is that this macro cannot access the original method being stubbed. -# It might exist or it might not. -# The method could also be abstract. -# For all intents and purposes, this macro defines logic that doesn't depend on an existing method. - %} - - {% unless method.abstract? %} - {{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( - {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} - {% if method.double_splat %}**{{method.double_splat}}, {% end %} - {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} - ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} - {{method.body}} - end - - {% original = "previous_def" - # Workaround for Crystal not propagating block with previous_def/super. - if method.accepts_block? - original += "(" - if method.splat_index - method.args.each_with_index do |arg, i| - if i == method.splat_index - if arg.internal_name && arg.internal_name.size > 0 - original += "*#{arg.internal_name}, " - end - original += "**#{method.double_splat}, " if method.double_splat - elsif i > method.splat_index - original += "#{arg.name}: #{arg.internal_name}" - else - original += "#{arg.internal_name}, " - end - end - else - method.args.each do |arg| - original += "#{arg.internal_name}, " - end - original += "**#{method.double_splat}, " if method.double_splat - end - # If the block is captured (i.e. `&block` syntax), it must be passed along as an argument. - # Otherwise, use `yield` to forward the block. - captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0 - method.block_arg.name - else - nil - end - original += "&#{captured_block}" if captured_block - original += ")" - original += " { |*_spectator_yargs| yield *_spectator_yargs }" unless captured_block - end - original = original.id %} - - {% end %} - - {% # Reconstruct the method signature. -# I wish there was a better way of doing this, but there isn't (at least not that I'm aware of). -# This chunk of code must reconstruct the method signature exactly as it was originally. -# If it doesn't match, it doesn't override the method and the stubbing won't work. - %} - {{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( - {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} - {% if method.double_splat %}**{{method.double_splat}}, {% end %} - {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} - ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} - - # Capture information about the call. - %call = ::Spectator::MethodCall.build( - {{method.name.symbolize}}, - ::NamedTuple.new( - {% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} - ), - {% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %} - ::NamedTuple.new( - {% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} - ).merge({{method.double_splat}}) - ) - _spectator_record_call(%call) - - # Attempt to find a stub that satisfies the method call and arguments. - # Finding a suitable stub is delegated to the type including the `Stubbable` module. - if %stub = _spectator_find_stub(%call) - # Cast the stub or return value to the expected type. - # This is necessary to match the expected return type of the original method. - {% if rt = method.return_type %} - # Return type restriction takes priority since it can be a superset of the original implementation. - _spectator_cast_stub_value(%stub, %call, {{method.return_type}}, - {{ if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn - :no_return - else - # Process as an enumerable type to reduce code repetition. - rt = rt.is_a?(Union) ? rt.types : [rt] - # Check if any types are nilable. - nilable = rt.any? do |t| - # These are all macro types that have the `resolve?` method. - (t.is_a?(TypeNode) || t.is_a?(Path) || t.is_a?(Generic) || t.is_a?(MetaClass)) && - (resolved = t.resolve?).is_a?(TypeNode) && resolved <= Nil - end - if nilable - :nil - else - :raise - end - end }}) - {% elsif !method.abstract? %} - # The method isn't abstract, infer the type it returns without calling it. - _spectator_cast_stub_value(%stub, %call, typeof({{original}})) - {% else %} - # Stubbed method is abstract and there's no return type annotation. - # The value of the stub could be returned as-is. - # This may produce a "bloated" union of all known stub types, - # and generally causes more annoying problems. - raise TypeCastError.new("#{_spectator_stubbed_name} received message #{%call} but cannot resolve the return type. Please add a return type restriction.") - {% end %} - else - # A stub wasn't found, invoke the type-specific fallback logic. - {% if method.return_type %} - # Pass along just the return type annotation. - _spectator_abstract_stub_fallback(%call, {{method.return_type}}) - {% elsif !method.abstract? %} - _spectator_abstract_stub_fallback(%call, typeof({{original}})) - {% else %} - # Stubbed method is abstract and there's no type annotation. - _spectator_abstract_stub_fallback(%call) - {% end %} - end - end - end - - # Redefines a method to require stubs. - # - # The *method* can be a `Def`. - # That is, a normal looking method definition should follow the `stub` keyword. - # - # ``` - # stub def stubbed_method - # "foobar" - # end - # ``` - # - # If the *method* is abstract, then a stub must be provided otherwise attempts to call the method will raise `UnexpectedMessage`. - # - # ``` - # stub abstract def stubbed_method - # ``` - # - # A `Call` can also be specified. - # In this case all methods in the stubbed type and its ancestors that match the call's signature are stubbed. - # - # ``` - # stub stubbed_method(arg) - # ``` - # - # The method being stubbed doesn't need to exist yet. - # Stubbed methods will call `#_spectator_find_stub` with the method call information. - # If no stub is found, then `#_spectator_stub_fallback` or `#_spectator_abstract_stub_fallback` is called. - macro stub(method) - {% raise "Cannot define a stub inside a method" if @def %} - - {% if method.is_a?(Def) %} - {% if method.abstract? %}abstract_stub{% else %}default_stub{% end %} {{method}} - {% elsif method.is_a?(VisibilityModifier) && method.exp.is_a?(Def) %} - {% if method.exp.abstract? %}abstract_stub{% else %}default_stub{% end %} {{method}} - {% elsif method.is_a?(Call) %} - {% raise "Stub on `Call` unsupported." %} - {% else %} - {% raise "Unrecognized syntax for `stub` - #{method}" %} - {% end %} - end - - # Redefines all methods and ones inherited from its parents and mixins to support stubs. - private macro stub_type(type_name = @type) - {% type = type_name.resolve - definitions = [] of Nil - scope = if type == @type - :previous_def - elsif type.module? - type.name - else - :super - end.id - - # Add entries for methods in the target type and its class type. - [[:self.id, type.class], [nil, type]].each do |(receiver, t)| - t.methods.each do |method| - definitions << { - type: t, - method: method, - scope: scope, - receiver: receiver, - } - end - end - - # Iterate through all ancestors and add their methods. - type.ancestors.each do |ancestor| - [[:self.id, ancestor.class], [nil, ancestor]].each do |(receiver, t)| - t.methods.each do |method| - # Skip methods already found to prevent redefining them multiple times. - unless definitions.any? do |d| - m = d[:method] - m.name == method.name && - m.args == method.args && - m.splat_index == method.splat_index && - m.double_splat == method.double_splat && - m.block_arg == method.block_arg - end - definitions << { - type: t, - method: method, - scope: :super.id, - receiver: receiver, - } - end - end - end - end - - definitions = definitions.reject do |definition| - name = definition[:method].name - name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.symbolize) - end %} - - {% for definition in definitions %} - {% original_type = definition[:type] - method = definition[:method] - scope = definition[:scope] - receiver = definition[:receiver] - rewrite_args = method.accepts_block? - # Handle calling methods on other objects (primarily for mock modules). - if scope != :super.id && scope != :previous_def.id - if receiver == :self.id - scope = "#{scope}.#{method.name}".id - rewrite_args = true - else - scope = :super.id - end - end %} - # Redefinition of {{original_type}}{{"#".id}}{{method.name}} - {{(method.abstract? ? "abstract_stub abstract" : "default_stub").id}} {{method.visibility.id if method.visibility != :public}} def {{"#{receiver}.".id if receiver}}{{method.name}}( - {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} - {% if method.double_splat %}**{{method.double_splat}}, {% end %} - {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} - ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} - {% unless method.abstract? %} - {{scope}}{% if rewrite_args %}({% for arg, i in method.args %} - {% if i == method.splat_index && arg.internal_name && arg.internal_name.size > 0 %}*{{arg.internal_name}}, {% if method.double_splat %}**{{method.double_splat}}, {% end %}{% end %} - {% if method.splat_index && i > method.splat_index %}{{arg.name}}: {{arg.internal_name}}, {% end %} - {% if !method.splat_index || i < method.splat_index %}{{arg.internal_name}}, {% end %}{% end %} - {% if !method.splat_index && method.double_splat %}**{{method.double_splat}}, {% end %} - {% captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0 - method.block_arg.name - else - nil - end %} - {% if captured_block %}&{{captured_block}}{% end %} - ){% if !captured_block && method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}{% end %} - end - {% end %} - {% end %} - end - - # Utility macro for casting a stub (and its return value) to the correct type. - # - # *stub* is the variable holding the stub. - # *call* is the variable holding the captured method call. - # *type* is the expected type to cast the value to. - # *fail_cast* indicates the behavior used when the value returned by the stub can't be cast to *type*. - # - `:nil` - return nil. - # - `:raise` - raise a `TypeCastError`. - # - `:no_return` - raise as no value should be returned. - private macro _spectator_cast_stub_value(stub, call, type, fail_cast = :nil) - {% if fail_cast == :no_return %} - {{stub}}.call({{call}}) - raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a value, but it shouldn't have returned (`NoReturn`).") - {% else %} - # Get the value as-is from the stub. - # This will be compiled as a union of all known stubbed value types. - %value = {{stub}}.call({{call}}) - %type = {{type}} - - # Attempt to cast the value to the method's return type. - # If successful, which it will be in most cases, return it. - # The caller will receive a properly typed value without unions or other side-effects. - %cast = %value.as?({{type}}) - - {% if fail_cast == :nil %} - %cast - {% elsif fail_cast == :raise %} - # Check if nil was returned by the stub and if its okay to return it. - if %value.nil? && %type.nilable? - # Value was nil and nil is allowed to be returned. - %type.cast(%cast) - elsif %cast.nil? - # The stubbed value was something else entirely and cannot be cast to the return type. - raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%value.class}`, but returned type must be `#{%type}`.") - else - # Types match and value can be returned as cast type. - %cast - end - {% else %} - {% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %} - {% end %} - {% end %} - end - end -end diff --git a/src/spectator/mocks/stubbed_name.cr b/src/spectator/mocks/stubbed_name.cr deleted file mode 100644 index df3cfc6..0000000 --- a/src/spectator/mocks/stubbed_name.cr +++ /dev/null @@ -1,9 +0,0 @@ -module Spectator - # Defines the name of a double or mock. - # - # When present on a stubbed type, this annotation indicates its name in output such as exceptions. - # Must have one argument - the name of the double or mock. - # This can be a symbol, string literal, or type name. - annotation StubbedName - end -end diff --git a/src/spectator/mocks/stubbed_type.cr b/src/spectator/mocks/stubbed_type.cr deleted file mode 100644 index 5362b84..0000000 --- a/src/spectator/mocks/stubbed_type.cr +++ /dev/null @@ -1,68 +0,0 @@ -require "./method_call" -require "./stub" - -module Spectator - # Defines stubbing functionality at the type level (classes and structs). - # - # This module is intended to be extended when a type includes `Stubbable`. - module StubbedType - private abstract def _spectator_stubs : Array(Stub) - - def _spectator_find_stub(call : MethodCall) : Stub? - _spectator_stubs.find &.===(call) - end - - def _spectator_stub_for_method?(method : Symbol) : Bool - _spectator_stubs.any? { |stub| stub.method == method } - end - - def _spectator_define_stub(stub : Stub) : Nil - _spectator_stubs.unshift(stub) - end - - def _spectator_remove_stub(stub : Stub) : Nil - _spectator_stubs.delete(stub) - end - - def _spectator_clear_stubs : Nil - _spectator_stubs.clear - end - - def _spectator_record_call(call : MethodCall) : Nil - _spectator_calls << call - end - - def _spectator_clear_calls : Nil - _spectator_calls.clear - end - - # Clears all previously defined calls and stubs. - def _spectator_reset : Nil - _spectator_clear_calls - _spectator_clear_stubs - end - - def _spectator_stub_fallback(call : MethodCall, &) - Log.trace { "Fallback for #{call} - call original" } - yield - end - - def _spectator_stub_fallback(call : MethodCall, type, &) - _spectator_stub_fallback(call) { yield } - end - - def _spectator_abstract_stub_fallback(call : MethodCall) - Log.info do - break unless _spectator_stub_for_method?(call.method) - - "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." - end - - raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}") - end - - def _spectator_abstract_stub_fallback(call : MethodCall, type) - _spectator_abstract_stub_fallback(call) - end - end -end diff --git a/src/spectator/mocks/typed_stub.cr b/src/spectator/mocks/typed_stub.cr deleted file mode 100644 index eabbcb9..0000000 --- a/src/spectator/mocks/typed_stub.cr +++ /dev/null @@ -1,19 +0,0 @@ -require "./method_call" -require "./stub" - -module Spectator - # Abstract type of stub that identifies the type of value produced by a stub. - # - # *T* is the type produced by the stub. - # How the stub produces this value is up to subclasses. - abstract class TypedStub(T) < Stub - # Invokes the stubbed implementation. - abstract def call(call : MethodCall) : T - - # String representation of the stub, formatted as a method call. - def to_s(io : IO) : Nil - super - io << " : " << T - end - end -end diff --git a/src/spectator/mocks/unexpected_message.cr b/src/spectator/mocks/unexpected_message.cr deleted file mode 100644 index f769ee8..0000000 --- a/src/spectator/mocks/unexpected_message.cr +++ /dev/null @@ -1,5 +0,0 @@ -module Spectator - # Exception raised by a mock or double when a message is received that have been. - class UnexpectedMessage < Exception - end -end diff --git a/src/spectator/mocks/value_mock_registry.cr b/src/spectator/mocks/value_mock_registry.cr deleted file mode 100644 index 1efd0b0..0000000 --- a/src/spectator/mocks/value_mock_registry.cr +++ /dev/null @@ -1,70 +0,0 @@ -require "string_pool" -require "./mock_registry_entry" -require "./stub" - -module Spectator - # Stores collections of stubs for mocked value (struct) types. - # - # *T* is the type of value to track. - # - # This type is intended for all mocked struct types that have functionality "injected." - # That is, the type itself has mock functionality bolted on. - # Adding instance members should be avoided, for instance, it could mess up serialization. - # This registry works around that by mapping mocks (via their raw memory content) to a collection of stubs. - # Doing so prevents adding data to the mocked type. - class ValueMockRegistry(T) - @pool = StringPool.new # Used to de-dup values. - @entries : Hash(String, MockRegistryEntry) - - # Creates an empty registry. - def initialize - @entries = Hash(String, MockRegistryEntry).new do |hash, key| - hash[key] = MockRegistryEntry.new - end - end - - # Retrieves all stubs defined for a mocked object. - def [](object : T) - key = value_bytes(object) - @entries[key] - end - - # Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet. - def []?(object : T) - key = value_bytes(object) - @entries[key]? - end - - # Retrieves all stubs defined for a mocked object. - # - # Yields to the block on the first retrieval. - # This allows a mock to populate the registry with initial stubs. - def fetch(object : T, & : -> Array(Stub)) - key = value_bytes(object) - @entries.fetch(key) do - entry = MockRegistryEntry.new - entry.stubs = yield - @entries[key] = entry - end - end - - # Clears all stubs defined for a mocked object. - def delete(object : T) : Nil - key = value_bytes(object) - @entries.delete(key) - end - - # Extracts heap-managed bytes for a value. - # - # Strings are used because a string pool is used. - # However, the strings are treated as an array of bytes. - @[AlwaysInline] - private def value_bytes(value : T) : String - # Get slice pointing to the memory used by the value (does not allocate). - bytes = Bytes.new(pointerof(value).as(UInt8*), sizeof(T), read_only: true) - - # De-dup the value (may allocate). - @pool.get(bytes) - end - end -end diff --git a/src/spectator/mocks/value_stub.cr b/src/spectator/mocks/value_stub.cr deleted file mode 100644 index 7a84d19..0000000 --- a/src/spectator/mocks/value_stub.cr +++ /dev/null @@ -1,38 +0,0 @@ -require "../location" -require "./arguments" -require "./stub_modifiers" -require "./typed_stub" - -module Spectator - # Stub that responds with a static value. - class ValueStub(T) < TypedStub(T) - # Invokes the stubbed implementation. - def call(call : MethodCall) : T - @value - end - - # Returns a new stub with constrained arguments. - def with_constraint(constraint : AbstractArguments?) - self.class.new(method, @value, constraint, location) - end - - # Creates the stub. - def initialize(method : Symbol, @value : T, constraint : AbstractArguments? = nil, location : Location? = nil) - super(method, constraint, location) - end - - # String representation of the stub, formatted as a method call and return value. - def to_s(io : IO) : Nil - super - io << " # => " - @value.inspect(io) - end - end - - module StubModifiers - # Returns a new stub that returns a static value. - def and_return(value) - ValueStub.new(method, value, constraint, location) - end - end -end diff --git a/src/spectator/multiple_expectations_failed.cr b/src/spectator/multiple_expectations_failed.cr deleted file mode 100644 index 9a911fa..0000000 --- a/src/spectator/multiple_expectations_failed.cr +++ /dev/null @@ -1,16 +0,0 @@ -require "./example_failed" -require "./expectation" - -module Spectator - # Exception that indicates more than one expectation from a test failed. - # When raised within a test, the test should abort. - class MultipleExpectationsFailed < ExampleFailed - # Expectations that failed. - getter expectations : Array(Expectation) - - # Creates the exception. - def initialize(@expectations : Array(Expectation), message : String? = nil, cause : Exception? = nil) - super(nil, message, cause) - end - end -end diff --git a/src/spectator/name_node_filter.cr b/src/spectator/name_node_filter.cr deleted file mode 100644 index c404246..0000000 --- a/src/spectator/name_node_filter.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "./node_filter" - -module Spectator - # Filter that matches nodes based on their name. - class NameNodeFilter < NodeFilter - # Creates the node filter. - def initialize(@name : String) - end - - # Checks whether the node satisfies the filter. - def includes?(node) : Bool - node.to_s.includes?(@name) - end - end -end diff --git a/src/spectator/node.cr b/src/spectator/node.cr deleted file mode 100644 index 6a5d068..0000000 --- a/src/spectator/node.cr +++ /dev/null @@ -1,94 +0,0 @@ -require "./label" -require "./location" -require "./metadata" - -module Spectator - # A single item in a test spec. - # This is commonly an `Example` or `ExampleGroup`, - # but can be anything that should be iterated over when running the spec. - abstract class Node - # Default text used if none was given by the user for skipping a node. - DEFAULT_PENDING_REASON = "No reason given" - - # Location of the node in source code. - getter! location : Location - - # User-provided name or description of the node. - # This does not include the group name or descriptions. - # Use `#to_s` to get the full name. - # - # This value will be nil if no name was provided. - # In this case, and the node is a runnable example, - # the name should be set to the description - # of the first matcher that runs in the test case. - # - # If this value is a `Symbol`, the user specified a type for the name. - getter! name : Label - - # Updates the name of the node. - protected def name=(@name : String) - end - - # User-defined tags and values used for filtering and behavior modification. - def metadata : Metadata - @metadata ||= Metadata.new - end - - # Creates the node. - # The *name* describes the purpose of the node. - # It can be a `Symbol` to describe a type. - # The *location* tracks where the node exists in source code. - # A set of *metadata* can be used for filtering and modifying example behavior. - def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata? = nil) - end - - # Indicates whether the node has completed. - abstract def finished? : Bool - - # Checks if the node has been marked as pending. - # Pending items should be skipped during execution. - def pending? - return false unless md = @metadata - - md.has_key?(:pending) || md.has_key?(:skip) - end - - # Gets the reason the node has been marked as pending. - def pending_reason - return DEFAULT_PENDING_REASON unless md = @metadata - - md[:pending]? || md[:skip]? || md[:reason]? || DEFAULT_PENDING_REASON - end - - # Retrieves just the tag names applied to the node. - def tags - if md = @metadata - Tags.new(md.keys) - else - Tags.new - end - end - - # Non-nil name used to show the node name. - def display_name - @name || "" - end - - # Constructs the full name or description of the node. - # This prepends names of groups this node is part of. - def to_s(io : IO) : Nil - display_name.to_s(io) - end - - # Exposes information about the node useful for debugging. - def inspect(io : IO) : Nil - # Full node name. - io << '"' << self << '"' - - # Add location if it's available. - if (location = self.location) - io << " @ " << location - end - end - end -end diff --git a/src/spectator/node_builder.cr b/src/spectator/node_builder.cr deleted file mode 100644 index 939374b..0000000 --- a/src/spectator/node_builder.cr +++ /dev/null @@ -1,6 +0,0 @@ -module Spectator - abstract class NodeBuilder - # Produces a node for a spec. - abstract def build(parent = nil) - end -end diff --git a/src/spectator/node_filter.cr b/src/spectator/node_filter.cr deleted file mode 100644 index 6530d4a..0000000 --- a/src/spectator/node_filter.cr +++ /dev/null @@ -1,14 +0,0 @@ -module Spectator - # Base class for all node filters. - # Checks whether a node should be included in the test run. - # Sub-classes must implement the `#includes?` method. - abstract class NodeFilter - # Checks if a node is in the filter, and should be included in the test run. - abstract def includes?(node) : Bool - - # :ditto: - def ===(node) - includes?(node) - end - end -end diff --git a/src/spectator/node_iterator.cr b/src/spectator/node_iterator.cr deleted file mode 100644 index 0dbc2bb..0000000 --- a/src/spectator/node_iterator.cr +++ /dev/null @@ -1,44 +0,0 @@ -require "./node" - -module Spectator - # Iterates through all nodes in a group and its nested groups. - # Nodes are iterated in pre-order. - class NodeIterator - include Iterator(Node) - - # A stack is used to track where in the tree this iterator is. - @stack = Deque(Node).new(1) - - # Creates a new iterator. - # The *group* is the example group to iterate through. - def initialize(@group : Node) - @stack.push(@group) - end - - # Retrieves the next `Node`. - # If there are none left, then `Iterator::Stop` is returned. - def next - # Nothing left to iterate. - return stop if @stack.empty? - - # Retrieve the next node. - node = @stack.pop - - # If the node is a group, add its direct children to the queue - # in reverse order so that the tree is traversed in pre-order. - if node.is_a?(Indexable(Node)) - node.reverse_each { |child| @stack.push(child) } - end - - # Return the current node. - node - end - - # Restart the iterator at the beginning. - def rewind - @stack.clear - @stack.push(@group) - self - end - end -end diff --git a/src/spectator/null_context.cr b/src/spectator/null_context.cr deleted file mode 100644 index 3597203..0000000 --- a/src/spectator/null_context.cr +++ /dev/null @@ -1,6 +0,0 @@ -module Spectator - # Empty context used to construct examples that don't have contexts. - # This is useful for dynamically creating examples outside of a spec. - class NullContext < Context - end -end diff --git a/src/spectator/null_node_filter.cr b/src/spectator/null_node_filter.cr deleted file mode 100644 index b7d37d8..0000000 --- a/src/spectator/null_node_filter.cr +++ /dev/null @@ -1,16 +0,0 @@ -require "./node_filter" - -module Spectator - # Filter that matches all nodes. - class NullNodeFilter < NodeFilter - # Creates the filter. - # The *match* flag indicates whether all examples should match or not. - def initialize(@match : Bool = true) - end - - # Checks whether the node satisfies the filter. - def includes?(node) : Bool - @match - end - end -end diff --git a/src/spectator/pass_result.cr b/src/spectator/pass_result.cr deleted file mode 100644 index 21ed6c5..0000000 --- a/src/spectator/pass_result.cr +++ /dev/null @@ -1,37 +0,0 @@ -require "./result" - -module Spectator - # Outcome that indicates running an example was successful. - class PassResult < Result - # Calls the `pass` method on *visitor*. - def accept(visitor) - visitor.pass(self) - end - - # Calls the `pass` method on *visitor*. - def accept(visitor, &) - visitor.pass(yield self) - end - - # Indicates whether the example passed. - def pass? : Bool - true - end - - # Indicates whether the example failed. - def fail? : Bool - false - end - - # One-word description of the result. - def to_s(io : IO) : Nil - io << "pass" - end - - # Creates a JSON object from the result information. - def to_json(json : JSON::Builder) - super - json.field("status", "passed") - end - end -end diff --git a/src/spectator/pending_example_builder.cr b/src/spectator/pending_example_builder.cr deleted file mode 100644 index 434efe5..0000000 --- a/src/spectator/pending_example_builder.cr +++ /dev/null @@ -1,24 +0,0 @@ -require "./example" -require "./location" -require "./metadata" -require "./node_builder" - -module Spectator - # Constructs pending examples. - # Call `#build` to produce an `Example`. - class PendingExampleBuilder < NodeBuilder - # Creates the builder. - # The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`. - # A default *reason* can be given in case the user didn't provide one. - def initialize(@name : String? = nil, @location : Location? = nil, - @metadata : Metadata? = nil, @reason : String? = nil) - end - - # Constructs an example with previously defined attributes. - # The *parent* is an already constructed example group to nest the new example under. - # It can be nil if the new example won't have a parent. - def build(parent = nil) - Example.pending(@name, @location, parent, @metadata, @reason) - end - end -end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr deleted file mode 100644 index 57f7fd7..0000000 --- a/src/spectator/pending_result.cr +++ /dev/null @@ -1,57 +0,0 @@ -require "./result" - -module Spectator - # Outcome that indicates running an example was pending. - # A pending result means the example is not ready to run yet. - # This can happen when the functionality to be tested is not implemented yet. - class PendingResult < Result - DEFAULT_REASON = "No reason given" - - # Reason the example was skipped or marked pending. - getter reason : String - - # Location the pending result was triggered from. - getter! location : Location - - # Creates the result. - # *elapsed* is the length of time it took to run the example. - # A *reason* for the skip/pending result can be specified. - # If the pending result was triggered inside of an example, then *location* can be set. - def initialize(@reason = DEFAULT_REASON, @location = nil, - elapsed = Time::Span::ZERO, expectations = [] of Expectation) - super(elapsed, expectations) - end - - # Calls the `pending` method on the *visitor*. - def accept(visitor) - visitor.pending(self) - end - - # Calls the `pending` method on the *visitor*. - def accept(visitor, &) - visitor.pending(yield self) - end - - # Indicates whether the example passed. - def pass? : Bool - false - end - - # Indicates whether the example failed. - def fail? : Bool - false - end - - # One-word description of the result. - def to_s(io : IO) : Nil - io << "pending" - end - - # Creates a JSON object from the result information. - def to_json(json : JSON::Builder) - super - json.field("status", "pending") - json.field("pending_message", @reason) - end - end -end diff --git a/src/spectator/profile.cr b/src/spectator/profile.cr deleted file mode 100644 index 79fed6c..0000000 --- a/src/spectator/profile.cr +++ /dev/null @@ -1,61 +0,0 @@ -require "json" -require "./example" - -module Spectator - # Information about the runtime of examples. - class Profile - include Indexable(Example) - - # Total length of time it took to run all examples in the test suite. - getter total_time : Time::Span - - # Creates the profiling information. - # The *slowest* results must already be sorted, longest time first. - def initialize(@slowest : Array(Example), @total_time) - end - - # Produces the profile from a report. - def self.generate(examples, size = 10) - finished = examples.select(&.finished?).to_a - total_time = finished.sum(&.result.elapsed) - sorted_examples = finished.sort_by(&.result.elapsed) - slowest = sorted_examples.last(size).reverse - new(slowest, total_time) - end - - # Number of results in the profile. - def size - @slowest.size - end - - # Retrieves a result at the specified index. - def unsafe_fetch(index) - @slowest.unsafe_fetch(index) - end - - # Length of time it took to run the results in the profile. - def time - @slowest.sum(&.result.elapsed) - end - - # Percentage (from 0 to 100) of time the results in this profile took compared to all examples. - def percentage - time / @total_time * 100 - end - - # Produces a JSON fragment containing the profiling information. - def to_json(json : JSON::Builder) - json.object do - json.field("examples") do - json.array do - @slowest.each(&.to_json(json)) - end - end - - json.field("slowest", @slowest.max_of(&.result.elapsed).total_seconds) - json.field("total", time.total_seconds) - json.field("percentage", percentage) - end - end - end -end diff --git a/src/spectator/report.cr b/src/spectator/report.cr deleted file mode 100644 index 93a88ea..0000000 --- a/src/spectator/report.cr +++ /dev/null @@ -1,132 +0,0 @@ -require "./result" - -module Spectator - # Outcome of all tests in a suite. - class Report - # Records the number of examples that had each type of result. - record Counts, pass = 0, fail = 0, error = 0, pending = 0, remaining = 0 do - # Number of examples that actually ran. - def run - pass + fail + pending - end - - # Total number of examples in the suite that were selected to run. - def total - run + remaining - end - - # Indicates whether there were skipped tests - # because of a failure causing the test suite to abort. - def remaining? - remaining > 0 - end - end - - # Retrieves all examples that were planned to run as part of the suite. - getter examples : Array(Example) - - # Total length of time it took to execute the test suite. - # This includes examples, hooks, and framework processes. - getter runtime : Time::Span - - # Number of examples of each result type. - getter counts : Counts - - # Seed used for random number generation. - getter! random_seed : UInt64? - - # Creates the report. - # The *examples* are all examples in the test suite that were selected to run. - # The *runtime* is the total time it took to execute the suite. - # The *counts* is the number of examples for each type of result. - # The *random_seed* is the seed used for random number generation. - def initialize(@examples : Array(Example), @runtime, @counts : Counts, @random_seed = nil) - end - - # Generates the report from a set of examples. - def self.generate(examples : Enumerable(Example), runtime, random_seed = nil) - counts = count_examples(examples) - new(examples.to_a, runtime, counts, random_seed) - end - - # Counts the number of examples for each result type. - private def self.count_examples(examples) - visitor = CountVisitor.new - - # Number of tests not run. - remaining = 0 - - # Iterate through each example and count the number of each type of result. - # If an example hasn't run (indicated by `Node#finished?`), then count is as "remaining." - # This typically happens in fail-fast mode. - examples.each do |example| - if example.finished? - example.result.accept(visitor) - else - remaining += 1 - end - end - - visitor.counts(remaining) - end - - # Returns a collection of all failed examples. - def failures - @examples.select(&.result.fail?) - end - - # Returns a collection of all pending (skipped) examples. - def pending - @examples.select(&.result.pending?) - end - - # Length of time it took to run just example code. - # This does not include hooks, - # but it does include pre- and post-conditions. - def example_runtime - @examples.sum(&.result.elapsed) - end - - # Length of time spent in framework processes and hooks. - def overhead_time - @runtime - example_runtime - end - - # Totals up the number of each type of result. - # Defines methods for the different types of results. - # Call `#counts` to retrieve the `Counts` instance. - private class CountVisitor - @pass = 0 - @fail = 0 - @error = 0 - @pending = 0 - - # Increments the number of passing examples. - def pass(_result) - @pass += 1 - end - - # Increments the number of failing (non-error) examples. - def fail(_result) - @fail += 1 - end - - # Increments the number of error (and failed) examples. - def error(result) - fail(result) - @error += 1 - end - - # Increments the number of pending (skipped) examples. - def pending(_result) - @pending += 1 - end - - # Produces the total counts. - # The *remaining* number of examples should be provided. - def counts(remaining) - Counts.new(@pass, @fail, @error, @pending, remaining) - end - end - end -end diff --git a/src/spectator/result.cr b/src/spectator/result.cr deleted file mode 100644 index 2da334d..0000000 --- a/src/spectator/result.cr +++ /dev/null @@ -1,44 +0,0 @@ -require "json" -require "./expectation" - -module Spectator - # Base class that represents the outcome of running an example. - # Sub-classes contain additional information specific to the type of result. - abstract class Result - # Length of time it took to run the example. - getter elapsed : Time::Span - - # The assertions checked in the example. - getter expectations : Enumerable(Expectation) - - # Creates the result. - # *elapsed* is the length of time it took to run the example. - def initialize(@elapsed, @expectations = [] of Expectation) - end - - # Calls the corresponding method for the type of result. - # This is the visitor design pattern. - abstract def accept(visitor) - - # Indicates whether the example passed. - abstract def pass? : Bool - - # Indicates whether the example failed. - abstract def fail? : Bool - - # Indicates whether the example was skipped. - def pending? : Bool - !pass? && !fail? - end - - # Creates a JSON object from the result information. - def to_json(json : JSON::Builder) - json.field("run_time", @elapsed.total_seconds) - json.field("expectations") do - json.array do - @expectations.each(&.to_json(json)) - end - end - end - end -end diff --git a/src/spectator/run_flags.cr b/src/spectator/run_flags.cr deleted file mode 100644 index 3ce2d9e..0000000 --- a/src/spectator/run_flags.cr +++ /dev/null @@ -1,21 +0,0 @@ -module Spectator - # Toggles indicating how the test spec should execute. - @[Flags] - enum RunFlags - # Indicates whether the test should abort on first failure. - FailFast - - # Indicates whether the test should fail if there are no examples. - FailBlank - - # Indicates whether the test should be done as a dry-run. - # Examples won't run, but the output will show that they did. - DryRun - - # Indicates whether examples run in a random order. - Randomize - - # Indicates whether timing information should be generated. - Profile - end -end diff --git a/src/spectator/runner.cr b/src/spectator/runner.cr deleted file mode 100644 index 49b6065..0000000 --- a/src/spectator/runner.cr +++ /dev/null @@ -1,98 +0,0 @@ -require "./example" -require "./formatting/formatter" -require "./profile" -require "./report" -require "./run_flags" -require "./runner_events" - -module Spectator - # Logic for executing examples and collecting results. - struct Runner - include RunnerEvents - - # Formatter to send events to. - private getter formatter : Formatting::Formatter - - # Creates the runner. - # The collection of *examples* should be pre-filtered and shuffled. - # This runner will run each example in the order provided. - # The *formatter* will be called for various events. - def initialize(@examples : Array(Example), @formatter : Formatting::Formatter, - @run_flags = RunFlags::None, @random_seed : UInt64? = nil) - end - - # Runs the spec. - # This will run the provided examples - # and invoke the reporters to communicate results. - # True will be returned if the spec ran successfully, - # or false if there was at least one failure. - def run : Bool - start - elapsed = Time.measure { run_examples } - stop - - report = Report.generate(@examples, elapsed, @random_seed) - profile = Profile.generate(@examples) if @run_flags.profile? && report.counts.run > 0 - summarize(report, profile) - - report.counts.fail.zero? - ensure - close - end - - # Attempts to run all examples. - # Returns a list of examples that ran. - private def run_examples - @examples.each do |example| - result = run_example(example) - - # Bail out if the example failed - # and configured to stop after the first failure. - break fail_fast if fail_fast? && result.fail? - end - end - - # Runs a single example and returns the result. - # The formatter is given the example and result information. - private def run_example(example) - example_started(example) - result = if dry_run? - # TODO: Pending examples return a pending result instead of pass in RSpec dry-run. - dry_run_result - else - example.run - end - example_finished(example) - result - end - - # Creates a fake result. - private def dry_run_result - expectations = [] of Expectation - PassResult.new(Time::Span.zero, expectations) - end - - # Generates and returns a profile if one should be displayed. - private def profile(report) - Profile.generate(report) if @config.profile? - end - - # Indicates whether examples should be simulated, but not run. - private def dry_run? - @run_flags.dry_run? - end - - # Indicates whether test execution should stop after the first failure. - private def fail_fast? - @run_flags.fail_fast? - end - - private def fail_fast : Nil - end - - # Number of examples configured to run. - private def example_count - @examples.size - end - end -end diff --git a/src/spectator/runner_events.cr b/src/spectator/runner_events.cr deleted file mode 100644 index 8f4e97e..0000000 --- a/src/spectator/runner_events.cr +++ /dev/null @@ -1,93 +0,0 @@ -require "./formatting/formatter" -require "./formatting/notifications" - -module Spectator - # Mix-in for announcing events from a `Runner`. - # All events invoke their corresponding method on the formatter. - module RunnerEvents - # Triggers the 'start' event. - # See `Formatting::Formatter#start` - private def start - notification = Formatting::StartNotification.new(example_count) - formatter.start(notification) - end - - # Triggers the 'example started' event. - # Must be passed the *example* about to run. - # See `Formatting::Formatter#example_started` - private def example_started(example) - notification = Formatting::ExampleNotification.new(example) - formatter.example_started(notification) - end - - # Triggers the 'example started' event. - # Also triggers the example result event corresponding to the example's outcome. - # Must be passed the completed *example*. - # See `Formatting::Formatter#example_finished` - private def example_finished(example) - notification = Formatting::ExampleNotification.new(example) - visitor = ResultVisitor.new(formatter, notification) - formatter.example_finished(notification) - example.result.accept(visitor) - end - - # Triggers the 'stop' event. - # See `Formatting::Formatter#stop` - private def stop - formatter.stop - end - - # Triggers the 'dump' events. - private def summarize(report, profile) - formatter.start_dump - - notification = Formatting::ExampleSummaryNotification.new(report.pending) - formatter.dump_pending(notification) - - notification = Formatting::ExampleSummaryNotification.new(report.failures) - formatter.dump_failures(notification) - - if profile - notification = Formatting::ProfileNotification.new(profile) - formatter.dump_profile(notification) - end - - notification = Formatting::SummaryNotification.new(report) - formatter.dump_summary(notification) - end - - # Triggers the 'close' event. - # See `Formatting::Formatter#close` - private def close - formatter.close - end - - # Provides methods for the various result types. - private struct ResultVisitor - # Creates the visitor. - # Requires the *formatter* to notify and the *notification* to send it. - def initialize(@formatter : Formatting::Formatter, @notification : Formatting::ExampleNotification) - end - - # Invokes the example passed method. - def pass(_result) - @formatter.example_passed(@notification) - end - - # Invokes the example failed method. - def fail(_result) - @formatter.example_failed(@notification) - end - - # Invokes the example error method. - def error(_result) - @formatter.example_error(@notification) - end - - # Invokes the example pending method. - def pending(_result) - @formatter.example_pending(@notification) - end - end - end -end diff --git a/src/spectator/should.cr b/src/spectator/should.cr deleted file mode 100644 index f0fe075..0000000 --- a/src/spectator/should.cr +++ /dev/null @@ -1,145 +0,0 @@ -class Object - # Extension method to create an expectation for an object. - # This is part of the spec DSL and mimics Crystal Spec's default should-syntax. - # A matcher should immediately follow this method, or be the only argument to it. - # Example usage: - # ``` - # it "equals the expected value" do - # subject.should eq(42) - # end - # ``` - # - # An optional message can be used in case the expectation fails. - # It can be a string or proc returning a string. - # ``` - # subject.should_not be_nil, "Shouldn't be nil" - # ``` - # - # NOTE: By default, the should-syntax is disabled. - # The expect-syntax is preferred, - # since it doesn't [monkey-patch](https://en.wikipedia.org/wiki/Monkey_patch) all objects. - # To enable should-syntax, add the following to your `spec_helper.cr` file: - # ``` - # require "spectator/should" - # ``` - def should(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) - actual = ::Spectator::Value.new(self) - location = ::Spectator::Location.new(_file, _line) - match_data = matcher.match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) - ::Spectator::Harness.current.report(expectation) - end - - # Asserts that some criteria defined by the matcher is satisfied. - # Allows a custom message to be used. - # Returns the expected value cast as the expected type, if the matcher is satisfied. - def should(matcher : ::Spectator::Matchers::TypeMatcher(U), message = nil, *, _file = __FILE__, _line = __LINE__) forall U - actual = ::Spectator::Value.new(self) - location = ::Spectator::Location.new(_file, _line) - match_data = matcher.match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) - if ::Spectator::Harness.current.report(expectation) - return self if self.is_a?(U) - - raise "Spectator bug: expected value should have cast to #{U}" - else - raise TypeCastError.new("Expected value should be a #{U}, but was actually #{self.class}") - end - end - - # Works the same as `#should` except the condition is inverted. - # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) - actual = ::Spectator::Value.new(self) - location = ::Spectator::Location.new(_file, _line) - match_data = matcher.negated_match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) - ::Spectator::Harness.current.report(expectation) - end - - # Asserts that some criteria defined by the matcher is not satisfied. - # Allows a custom message to be used. - # Returns the expected value cast without the unexpected type, if the matcher is satisfied. - def should_not(matcher : ::Spectator::Matchers::TypeMatcher(U), message = nil, *, _file = __FILE__, _line = __LINE__) forall U - actual = ::Spectator::Value.new(self) - location = ::Spectator::Location.new(_file, _line) - match_data = matcher.negated_match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) - if ::Spectator::Harness.current.report(expectation) - return self unless self.is_a?(U) - - raise "Spectator bug: expected value should not be #{U}" - else - raise TypeCastError.new("Expected value is not expected to be a #{U}, but was actually #{self.class}") - end - end - - # Asserts that some criteria defined by the matcher is not satisfied. - # Allows a custom message to be used. - # Returns the expected value cast as a non-nillable type, if the matcher is satisfied. - def should_not(matcher : ::Spectator::Matchers::NilMatcher, message = nil, *, _file = __FILE__, _line = __LINE__) - actual = ::Spectator::Value.new(self) - location = ::Spectator::Location.new(_file, _line) - match_data = matcher.negated_match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) - if ::Spectator::Harness.current.report(expectation) - return self unless self.nil? - - raise "Spectator bug: expected value should not be nil" - else - raise NilAssertionError.new("Expected value should not be nil.") - end - end - - # Works the same as `#should` except that the condition check is postponed. - # The expectation is checked after the example finishes and all hooks have run. - def should_eventually(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) - ::Spectator::Harness.current.defer { should(matcher, message, _file: _file, _line: _line) } - end - - # Works the same as `#should_not` except that the condition check is postponed. - # The expectation is checked after the example finishes and all hooks have run. - def should_never(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) - ::Spectator::Harness.current.defer { should_not(matcher, message, _file: _file, _line: _line) } - end -end - -struct Proc(*T, R) - # Extension method to create an expectation for a block of code (proc). - # Depending on the matcher, the proc may be executed multiple times. - def should(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) - actual = ::Spectator::Block.new(self) - location = ::Spectator::Location.new(_file, _line) - match_data = matcher.match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) - ::Spectator::Harness.current.report(expectation) - end - - # Works the same as `#should` except the condition is inverted. - # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) - actual = ::Spectator::Block.new(self) - location = ::Spectator::Location.new(_file, _line) - match_data = matcher.negated_match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) - ::Spectator::Harness.current.report(expectation) - end -end - -module Spectator::DSL::Expectations - macro should(*args) - expect(subject).to({{args.splat}}) - end - - macro should_not(*args) - expect(subject).to_not({{args.splat}}) - end - - macro should_eventually(*args) - expect(subject).to_eventually({{args.splat}}) - end - - macro should_never(*args) - expect(subject).to_never({{args.splat}}) - end -end diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr deleted file mode 100644 index a086171..0000000 --- a/src/spectator/spec.cr +++ /dev/null @@ -1,30 +0,0 @@ -require "./config" -require "./example_group" -require "./runner" - -module Spectator - # Contains examples to be tested and configuration for running them. - class Spec - # Creates the spec. - # The *root* is the top-most example group. - # All examples in this group and groups nested under are candidates for execution. - # The *config* provides settings controlling how tests will be executed. - def initialize(@root : ExampleGroup, @config : Config) - end - - # Runs all selected examples and returns the results. - # True will be returned if the spec ran successfully, - # or false if there was at least one failure. - def run : Bool - random_seed = (@config.random_seed if @config.run_flags.randomize?) - runner = Runner.new(examples, @config.formatter, @config.run_flags, random_seed) - runner.run - end - - # Selects and shuffles the examples that should run. - private def examples - iterator = @config.iterator(@root) - @config.shuffle!(iterator.to_a) - end - end -end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr deleted file mode 100644 index 17b0284..0000000 --- a/src/spectator/spec_builder.cr +++ /dev/null @@ -1,212 +0,0 @@ -require "./config" -require "./example" -require "./example_builder" -require "./example_context_method" -require "./example_group" -require "./example_group_builder" -require "./hooks" -require "./iterative_example_group_builder" -require "./pending_example_builder" -require "./spec" -require "./metadata" - -module Spectator - # Progressively builds a test spec. - # - # A stack is used to track the current example group. - # Adding an example or group will nest it under the group at the top of the stack. - class SpecBuilder - Log = ::Spectator::Log.for(self) - - delegate before_all, after_all, before_each, after_each, around_each, pre_condition, post_condition, to: current - - # Stack tracking the current group. - # The bottom of the stack (first element) is the root group. - # The root group should never be removed. - # The top of the stack (last element) is the current group. - # New examples should be added to the current group. - @stack : Deque(ExampleGroupBuilder) - - # Creates a new spec builder. - # A root group is pushed onto the group stack. - def initialize(@config : Config) - root = ExampleGroupBuilder.new - @stack = Deque(ExampleGroupBuilder).new - @stack.push(root) - end - - # Constructs the test spec. - # Returns the spec instance. - # - # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. - # This would indicate a logical error somewhere in Spectator or an extension of it. - def build : Spec - raise "Mismatched start and end groups" unless root? - - group = root.build - apply_config_hooks(group) - Spec.new(group, @config) - end - - # Defines a new example group and pushes it onto the group stack. - # Examples and groups defined after calling this method will be nested under the new group. - # The group will be finished and popped off the stack when `#end_example` is called. - # - # The *name* is the name or brief description of the group. - # This should be a symbol when describing a type - the type name is represented as a symbol. - # Otherwise, a string should be used. - # - # The *location* optionally defined where the group originates in source code. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark tests as pending and skip execution. - def start_group(name, location = nil, metadata = nil) : Nil - Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" } - builder = ExampleGroupBuilder.new(name, location, metadata) - - # `before_all` and `after_all` hooks from config are slightly different. - # They are applied to every top-level group (groups just under root). - apply_top_level_config_hooks(builder) if root? - - # Add group to the stack. - current << builder - @stack.push(builder) - end - - # Defines a new iterative example group and pushes it onto the group stack. - # Examples and groups defined after calling this method will be nested under the new group. - # The group will be finished and popped off the stack when `#end_example` is called. - # - # The *collection* is the set of items to iterate over. - # Child nodes in this group will be executed once for every item in the collection. - # The *name* should be a string representation of *collection*. - # The *iterator* is an optional name given to a single item in *collection*. - # - # The *location* optionally defined where the group originates in source code. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark tests as pending and skip execution. - def start_iterative_group(collection, name, iterator = nil, location = nil, metadata = nil) : Nil - Log.trace { "Start iterative group: #{name} (#{typeof(collection)}) @ #{location}; metadata: #{metadata}" } - builder = IterativeExampleGroupBuilder.new(collection, name, iterator, location, metadata) - - # `before_all` and `after_all` hooks from config are slightly different. - # They are applied to every top-level group (groups just under root). - apply_top_level_config_hooks(builder) if root? - - # Add group to the stack. - current << builder - @stack.push(builder) - end - - # Completes a previously defined example group and pops it off the group stack. - # Be sure to call `#start_group` and `#end_group` symmetrically. - def end_group : Nil - Log.trace { "End group: #{current}" } - raise "Can't pop root group" if root? - - @stack.pop - end - - # Defines a new example. - # The example is added to the group currently on the top of the stack. - # - # The *name* is the name or brief description of the example. - # This should be a string or nil. - # When nil, the example's name will be populated by the first expectation run inside of the test code. - # - # The *location* optionally defined where the example originates in source code. - # - # The *context_builder* is a proc that creates a context the test code should run in. - # See `Context` for more information. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark the test as pending and skip execution. - # - # A block must be provided. - # It will be yielded two arguments - the example created by this method, and the *context* argument. - # The return value of the block is ignored. - # It is expected that the test code runs when the block is called. - def add_example(name, location, context_builder, metadata = nil, &block : Example -> _) : Nil - Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } - current << ExampleBuilder.new(context_builder, block, name, location, metadata) - end - - # Defines a new pending example. - # The example is added to the group currently on the top of the stack. - # - # The *name* is the name or brief description of the example. - # This should be a string or nil. - # When nil, the example's name will be an anonymous example reference. - # - # The *location* optionally defined where the example originates in source code. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark the test as pending and skip execution. - # A default *reason* can be given in case the user didn't provide one. - def add_pending_example(name, location, metadata = nil, reason = nil) : Nil - Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } - current << PendingExampleBuilder.new(name, location, metadata, reason) - end - - # Registers a new "before_suite" hook. - # The hook will be appended to the list. - # A new hook will be created by passing args to `ExampleGroupHook.new`. - def before_suite(*args, **kwargs) : Nil - root.before_all(*args, **kwargs) - end - - # Registers a new "before_suite" hook. - # The hook will be appended to the list. - # A new hook will be created by passing args to `ExampleGroupHook.new`. - def before_suite(*args, **kwargs, &block) : Nil - root.before_all(*args, **kwargs, &block) - end - - # Registers a new "after_suite" hook. - # The hook will be pre-pended to the list. - # A new hook will be created by passing args to `ExampleGroupHook.new`. - def after_suite(*args, **kwargs) : Nil - root.before_all(*args, **kwargs) - end - - # Registers a new "after_suite" hook. - # The hook will be pre-pended to the list. - # A new hook will be created by passing args to `ExampleGroupHook.new`. - def after_suite(*args, **kwargs, &block) : Nil - root.after_all(*args, **kwargs, &block) - end - - # Checks if the current group is the root group. - private def root? - @stack.size == 1 - end - - # Retrieves the root group. - private def root - @stack.first - end - - # Retrieves the current group, which is at the top of the stack. - # This is the group that new examples should be added to. - private def current - @stack.last - end - - # Copy all hooks from config to root group. - private def apply_config_hooks(group) - @config.before_suite_hooks.each { |hook| group.before_all(hook) } - @config.after_suite_hooks.reverse_each { |hook| group.after_all(hook) } - @config.before_each_hooks.each { |hook| group.before_each(hook) } - @config.after_each_hooks.reverse_each { |hook| group.after_each(hook) } - @config.around_each_hooks.each { |hook| group.around_each(hook) } - end - - # Copy `before_all` and `after_all` hooks to a group. - private def apply_top_level_config_hooks(group) - # Hooks are dupped so that they retain their original state (call once). - @config.before_all_hooks.each { |hook| group.before_all(hook.dup) } - @config.after_all_hooks.reverse_each { |hook| group.after_all(hook.dup) } - end - end -end diff --git a/src/spectator/system_exit.cr b/src/spectator/system_exit.cr deleted file mode 100644 index d94711e..0000000 --- a/src/spectator/system_exit.cr +++ /dev/null @@ -1,28 +0,0 @@ -module Spectator - # Indicates a call to exit the application was performed. - class SystemExit < Exception - # Status code passed to the exit call. - getter status : Int32 - - # Creates the exception. - def initialize(message : String? = nil, cause : Exception? = nil, @status : Int32 = 0) - super(message, cause) - end - end - - # Allow Spectator to exit normally when needed. - private def self.exit(status = 0) : NoReturn - ::Crystal::System::Process.exit(status) - end -end - -class Process - # Replace the typically used exit method with a method that raises. - # This allows tests to catch attempts to exit the application. - def self.exit(status = 0) : NoReturn - # Re-enable log that is disabled from at-exit handlers. - ::Log.setup_from_env(default_level: :none) - - raise ::Spectator::SystemExit.new(status: status) - end -end diff --git a/src/spectator/tag_node_filter.cr b/src/spectator/tag_node_filter.cr deleted file mode 100644 index 0dedd59..0000000 --- a/src/spectator/tag_node_filter.cr +++ /dev/null @@ -1,18 +0,0 @@ -require "./node_filter" - -module Spectator - # Filter that matches nodes with a given tag. - class TagNodeFilter < NodeFilter - # Creates the filter. - # The *tag* indicates which tag the node must have in its metadata. - def initialize(@tag : String, @value : String? = nil) - end - - # Checks whether the node satisfies the filter. - def includes?(node) : Bool - return false unless metadata = node.metadata - - metadata.any? { |key, value| key.to_s == @tag && (!@value || value == @value) } - end - end -end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr deleted file mode 100644 index e04fe56..0000000 --- a/src/spectator/test_context.cr +++ /dev/null @@ -1,40 +0,0 @@ -require "./context" -require "./dsl" -require "./lazy_wrapper" -require "./metadata" - -# Class used as the base for all specs using the DSL. -# It adds methods and macros necessary to use the DSL from the spec. -# This type is intentionally outside the `Spectator` module. -# The reason for this is to prevent name collision when using the DSL to define a spec. -class SpectatorTestContext < SpectatorContext - include ::Spectator::DSL::Concise - include ::Spectator::DSL::Examples - include ::Spectator::DSL::Expectations - include ::Spectator::DSL::Groups - include ::Spectator::DSL::Hooks - include ::Spectator::DSL::Matchers - include ::Spectator::DSL::Memoize - include ::Spectator::DSL::Mocks - - @subject = ::Spectator::LazyWrapper.new - - # Initial implicit subject for tests. - # This method should be overridden by example groups when an object is described. - private def _spectator_implicit_subject - nil - end - - # Initial subject for tests. - # Returns the implicit subject. - # This method should be overridden when an explicit subject is defined by the DSL. - private def subject - @subject.get { _spectator_implicit_subject } - end - - # Initial metadata for tests. - # This method should be overridden by example groups and examples. - private def self.metadata : ::Spectator::Metadata? - nil - end -end diff --git a/src/spectator/value.cr b/src/spectator/value.cr deleted file mode 100644 index dd81de7..0000000 --- a/src/spectator/value.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "./expression" -require "./label" - -module Spectator - # Represents a value from a test. - # This is typically captured by an `expect` macro. - # It consists of a label and the value of the expression. - # The label should be a string recognizable by the user, - # or nil if one isn't available. - class Value(T) < Expression(T) - # Raw value of the expression. - getter value : T - - # Creates the value. - # Expects the *value* of the expression and a *label* describing it. - # The *label* is usually the Crystal code evaluating to the *value*. - def initialize(@value : T, label : Label) - super(label) - end - - # Creates the value. - # Expects the *value* of the expression. - # It can be nil if it isn't available. - # A label is generated by calling `#inspect` on the *value*. - def initialize(@value : T) - super(@value.inspect) - end - end -end diff --git a/src/spectator/wrapper.cr b/src/spectator/wrapper.cr deleted file mode 100644 index 76f6f44..0000000 --- a/src/spectator/wrapper.cr +++ /dev/null @@ -1,53 +0,0 @@ -module Spectator - # Typeless wrapper for a value. - # Stores any value or reference type. - # However, the type must be known when retrieving the value. - # - # This type is expected to be used like so: - # ``` - # wrapper = Wrapper.new("wrapped") - # value = wrapper.get(String) - # ``` - struct Wrapper - @pointer : Void* - - # Creates a wrapper for the specified value. - def initialize(value) - @pointer = Value.new(value).as(Void*) - end - - # Retrieves the previously wrapped value. - # The *type* of the wrapped value must match otherwise an error will be raised. - def get(type : T.class) : T forall T - @pointer.unsafe_as(Value(T)).get - end - - # Retrieves the previously wrapped value. - # Alternate form of `#get` that accepts a block. - # The block must return the same type as the wrapped value, otherwise an error will be raised. - # This method gets around the issue where the value might be a type (i.e. `Int32.class`). - # The block will never be executed, it is only used for type information. - # - # ``` - # wrapper = Wrapper.new(Int32) - # # type = wrapper.get(Int32.class) # Does not work! - # type = wrapper.get { Int32 } # Returns Int32 - # ``` - def get(& : -> T) : T forall T - @pointer.unsafe_as(Value(T)).get - end - - # Wrapper for a value. - # Similar to `Box`, but doesn't segfault on nil and unions. - private class Value(T) - # Creates the wrapper. - def initialize(@value : T) - end - - # Retrieves the value. - def get : T - @value - end - end - end -end diff --git a/util/mirror-wiki.sh b/util/mirror-wiki.sh deleted file mode 100755 index 684d545..0000000 --- a/util/mirror-wiki.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -ex - -# Mirrors the contents of the GitLab wiki to GitHub. -git clone git@gitlab.com:arctic-fox/spectator.wiki.git -pushd spectator.wiki -git remote add github git@github.com:icy-arctic-fox/spectator.wiki.git -git fetch github -git push github master -popd -rm -rf spectator.wiki diff --git a/util/nightly.sh b/util/nightly.sh deleted file mode 100755 index 460a839..0000000 --- a/util/nightly.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env sh -set -e - -readonly image=crystallang/crystal:nightly -readonly code=/project - -docker run -it -v "$PWD:${code}" -w "${code}" "${image}" crystal spec "$@" diff --git a/util/test-all-individually.sh b/util/test-all-individually.sh deleted file mode 100755 index 97bdd36..0000000 --- a/util/test-all-individually.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -e - -find spec/ -type f -name \*_spec.cr -print0 | \ - xargs -0 -n1 crystal spec --error-on-warnings -v