summaryrefslogtreecommitdiff
path: root/spec/mspec
diff options
context:
space:
mode:
Diffstat (limited to 'spec/mspec')
-rw-r--r--spec/mspec/.rspec1
-rw-r--r--spec/mspec/Gemfile4
-rw-r--r--spec/mspec/Gemfile.lock26
-rw-r--r--spec/mspec/LICENSE22
-rw-r--r--spec/mspec/README.md84
-rw-r--r--spec/mspec/Rakefile6
-rwxr-xr-xspec/mspec/bin/mkspec7
-rwxr-xr-xspec/mspec/bin/mkspec.bat1
-rwxr-xr-xspec/mspec/bin/mspec7
-rwxr-xr-xspec/mspec/bin/mspec-ci7
-rwxr-xr-xspec/mspec/bin/mspec-ci.bat1
-rwxr-xr-xspec/mspec/bin/mspec-run7
-rwxr-xr-xspec/mspec/bin/mspec-run.bat1
-rwxr-xr-xspec/mspec/bin/mspec-tag7
-rwxr-xr-xspec/mspec/bin/mspec-tag.bat1
-rwxr-xr-xspec/mspec/bin/mspec.bat1
-rw-r--r--spec/mspec/lib/mspec.rb8
-rw-r--r--spec/mspec/lib/mspec/commands/mkspec.rb143
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-ci.rb76
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-run.rb87
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-tag.rb132
-rw-r--r--spec/mspec/lib/mspec/commands/mspec.rb113
-rw-r--r--spec/mspec/lib/mspec/expectations.rb2
-rw-r--r--spec/mspec/lib/mspec/expectations/expectations.rb39
-rw-r--r--spec/mspec/lib/mspec/expectations/should.rb41
-rw-r--r--spec/mspec/lib/mspec/guards.rb11
-rw-r--r--spec/mspec/lib/mspec/guards/block_device.rb16
-rw-r--r--spec/mspec/lib/mspec/guards/bug.rb29
-rw-r--r--spec/mspec/lib/mspec/guards/conflict.rb23
-rw-r--r--spec/mspec/lib/mspec/guards/endian.rb25
-rw-r--r--spec/mspec/lib/mspec/guards/feature.rb45
-rw-r--r--spec/mspec/lib/mspec/guards/guard.rb141
-rw-r--r--spec/mspec/lib/mspec/guards/platform.rb122
-rw-r--r--spec/mspec/lib/mspec/guards/quarantine.rb11
-rw-r--r--spec/mspec/lib/mspec/guards/superuser.rb25
-rw-r--r--spec/mspec/lib/mspec/guards/support.rb14
-rw-r--r--spec/mspec/lib/mspec/guards/version.rb72
-rw-r--r--spec/mspec/lib/mspec/helpers.rb13
-rw-r--r--spec/mspec/lib/mspec/helpers/argf.rb35
-rw-r--r--spec/mspec/lib/mspec/helpers/argv.rb44
-rw-r--r--spec/mspec/lib/mspec/helpers/datetime.rb48
-rw-r--r--spec/mspec/lib/mspec/helpers/fixture.rb24
-rw-r--r--spec/mspec/lib/mspec/helpers/flunk.rb3
-rw-r--r--spec/mspec/lib/mspec/helpers/fs.rb64
-rw-r--r--spec/mspec/lib/mspec/helpers/io.rb87
-rw-r--r--spec/mspec/lib/mspec/helpers/mock_to_path.rb6
-rw-r--r--spec/mspec/lib/mspec/helpers/numeric.rb98
-rw-r--r--spec/mspec/lib/mspec/helpers/ruby_exe.rb205
-rw-r--r--spec/mspec/lib/mspec/helpers/scratch.rb21
-rw-r--r--spec/mspec/lib/mspec/helpers/tmp.rb62
-rw-r--r--spec/mspec/lib/mspec/helpers/warning.rb21
-rw-r--r--spec/mspec/lib/mspec/matchers.rb37
-rw-r--r--spec/mspec/lib/mspec/matchers/base.rb95
-rw-r--r--spec/mspec/lib/mspec/matchers/be_an_instance_of.rb26
-rw-r--r--spec/mspec/lib/mspec/matchers/be_ancestor_of.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/be_close.rb29
-rw-r--r--spec/mspec/lib/mspec/matchers/be_computed_by.rb37
-rw-r--r--spec/mspec/lib/mspec/matchers/be_empty.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_false.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_kind_of.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/be_nan.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_nil.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_true.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_true_or_false.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/block_caller.rb37
-rw-r--r--spec/mspec/lib/mspec/matchers/complain.rb69
-rw-r--r--spec/mspec/lib/mspec/matchers/eql.rb26
-rw-r--r--spec/mspec/lib/mspec/matchers/equal.rb26
-rw-r--r--spec/mspec/lib/mspec/matchers/equal_element.rb78
-rw-r--r--spec/mspec/lib/mspec/matchers/have_class_variable.rb12
-rw-r--r--spec/mspec/lib/mspec/matchers/have_constant.rb12
-rw-r--r--spec/mspec/lib/mspec/matchers/have_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_instance_variable.rb12
-rw-r--r--spec/mspec/lib/mspec/matchers/have_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_private_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_private_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_public_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_singleton_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/include.rb31
-rw-r--r--spec/mspec/lib/mspec/matchers/include_any_of.rb29
-rw-r--r--spec/mspec/lib/mspec/matchers/infinity.rb28
-rw-r--r--spec/mspec/lib/mspec/matchers/match_yaml.rb50
-rw-r--r--spec/mspec/lib/mspec/matchers/method.rb10
-rw-r--r--spec/mspec/lib/mspec/matchers/output.rb67
-rw-r--r--spec/mspec/lib/mspec/matchers/output_to_fd.rb71
-rw-r--r--spec/mspec/lib/mspec/matchers/raise_error.rb132
-rw-r--r--spec/mspec/lib/mspec/matchers/respond_to.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/signed_zero.rb28
-rw-r--r--spec/mspec/lib/mspec/matchers/skip.rb5
-rw-r--r--spec/mspec/lib/mspec/matchers/variable.rb24
-rw-r--r--spec/mspec/lib/mspec/mocks.rb3
-rw-r--r--spec/mspec/lib/mspec/mocks/mock.rb209
-rw-r--r--spec/mspec/lib/mspec/mocks/object.rb28
-rw-r--r--spec/mspec/lib/mspec/mocks/proxy.rb186
-rw-r--r--spec/mspec/lib/mspec/runner.rb12
-rw-r--r--spec/mspec/lib/mspec/runner/actions.rb6
-rw-r--r--spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb84
-rw-r--r--spec/mspec/lib/mspec/runner/actions/filter.rb40
-rw-r--r--spec/mspec/lib/mspec/runner/actions/leakchecker.rb377
-rw-r--r--spec/mspec/lib/mspec/runner/actions/profile.rb60
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tag.rb133
-rw-r--r--spec/mspec/lib/mspec/runner/actions/taglist.rb56
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tagpurge.rb56
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tally.rb133
-rw-r--r--spec/mspec/lib/mspec/runner/actions/timeout.rb145
-rw-r--r--spec/mspec/lib/mspec/runner/actions/timer.rb22
-rw-r--r--spec/mspec/lib/mspec/runner/context.rb237
-rw-r--r--spec/mspec/lib/mspec/runner/evaluate.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/example.rb34
-rw-r--r--spec/mspec/lib/mspec/runner/exception.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/filters.rb4
-rw-r--r--spec/mspec/lib/mspec/runner/filters/match.rb18
-rw-r--r--spec/mspec/lib/mspec/runner/filters/profile.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/filters/regexp.rb23
-rw-r--r--spec/mspec/lib/mspec/runner/filters/tag.rb29
-rw-r--r--spec/mspec/lib/mspec/runner/formatters.rb13
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/base.rb150
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/describe.rb23
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/dotted.rb23
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/file.rb24
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/html.rb81
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/junit.rb87
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/launchable.rb88
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/method.rb95
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/multi.rb47
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/profile.rb18
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/specdoc.rb41
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/spinner.rb111
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/stats.rb57
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/summary.rb4
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/unit.rb20
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/yaml.rb38
-rw-r--r--spec/mspec/lib/mspec/runner/mspec.rb424
-rw-r--r--spec/mspec/lib/mspec/runner/object.rb26
-rw-r--r--spec/mspec/lib/mspec/runner/parallel.rb98
-rw-r--r--spec/mspec/lib/mspec/runner/shared.rb14
-rw-r--r--spec/mspec/lib/mspec/runner/tag.rb38
-rw-r--r--spec/mspec/lib/mspec/utils/deprecate.rb6
-rw-r--r--spec/mspec/lib/mspec/utils/format.rb24
-rw-r--r--spec/mspec/lib/mspec/utils/name_map.rb133
-rw-r--r--spec/mspec/lib/mspec/utils/options.rb521
-rw-r--r--spec/mspec/lib/mspec/utils/script.rb305
-rw-r--r--spec/mspec/lib/mspec/utils/version.rb52
-rw-r--r--spec/mspec/lib/mspec/utils/warnings.rb10
-rw-r--r--spec/mspec/lib/mspec/version.rb5
-rw-r--r--spec/mspec/spec/commands/fixtures/four.txt0
-rw-r--r--spec/mspec/spec/commands/fixtures/level2/three_spec.rb1
-rw-r--r--spec/mspec/spec/commands/fixtures/one_spec.rb1
-rw-r--r--spec/mspec/spec/commands/fixtures/three.rb1
-rw-r--r--spec/mspec/spec/commands/fixtures/two_spec.rb1
-rw-r--r--spec/mspec/spec/commands/mkspec_spec.rb363
-rw-r--r--spec/mspec/spec/commands/mspec_ci_spec.rb155
-rw-r--r--spec/mspec/spec/commands/mspec_run_spec.rb178
-rw-r--r--spec/mspec/spec/commands/mspec_spec.rb180
-rw-r--r--spec/mspec/spec/commands/mspec_tag_spec.rb414
-rw-r--r--spec/mspec/spec/expectations/expectations_spec.rb29
-rw-r--r--spec/mspec/spec/expectations/should_spec.rb61
-rw-r--r--spec/mspec/spec/fixtures/a_spec.rb15
-rw-r--r--spec/mspec/spec/fixtures/b_spec.rb7
-rw-r--r--spec/mspec/spec/fixtures/chatty_spec.rb8
-rw-r--r--spec/mspec/spec/fixtures/config.mspec8
-rw-r--r--spec/mspec/spec/fixtures/die_spec.rb7
-rwxr-xr-xspec/mspec/spec/fixtures/my_ruby4
-rw-r--r--spec/mspec/spec/fixtures/object_methods_spec.rb8
-rw-r--r--spec/mspec/spec/fixtures/print_interpreter_spec.rb4
-rw-r--r--spec/mspec/spec/fixtures/should.rb75
-rw-r--r--spec/mspec/spec/fixtures/tagging_spec.rb16
-rw-r--r--spec/mspec/spec/guards/block_device_spec.rb46
-rw-r--r--spec/mspec/spec/guards/bug_spec.rb151
-rw-r--r--spec/mspec/spec/guards/conflict_spec.rb53
-rw-r--r--spec/mspec/spec/guards/endian_spec.rb55
-rw-r--r--spec/mspec/spec/guards/feature_spec.rb120
-rw-r--r--spec/mspec/spec/guards/guard_spec.rb421
-rw-r--r--spec/mspec/spec/guards/platform_spec.rb337
-rw-r--r--spec/mspec/spec/guards/quarantine_spec.rb35
-rw-r--r--spec/mspec/spec/guards/superuser_spec.rb35
-rw-r--r--spec/mspec/spec/guards/support_spec.rb54
-rw-r--r--spec/mspec/spec/guards/user_spec.rb20
-rw-r--r--spec/mspec/spec/guards/version_spec.rb112
-rw-r--r--spec/mspec/spec/helpers/argf_spec.rb37
-rw-r--r--spec/mspec/spec/helpers/argv_spec.rb27
-rw-r--r--spec/mspec/spec/helpers/datetime_spec.rb44
-rw-r--r--spec/mspec/spec/helpers/fixture_spec.rb25
-rw-r--r--spec/mspec/spec/helpers/flunk_spec.rb20
-rw-r--r--spec/mspec/spec/helpers/fs_spec.rb195
-rw-r--r--spec/mspec/spec/helpers/io_spec.rb136
-rw-r--r--spec/mspec/spec/helpers/mock_to_path_spec.rb23
-rw-r--r--spec/mspec/spec/helpers/numeric_spec.rb31
-rw-r--r--spec/mspec/spec/helpers/ruby_exe_spec.rb256
-rw-r--r--spec/mspec/spec/helpers/scratch_spec.rb24
-rw-r--r--spec/mspec/spec/helpers/suppress_warning_spec.rb19
-rw-r--r--spec/mspec/spec/helpers/tmp_spec.rb27
-rw-r--r--spec/mspec/spec/integration/interpreter_spec.rb18
-rw-r--r--spec/mspec/spec/integration/object_methods_spec.rb18
-rw-r--r--spec/mspec/spec/integration/run_spec.rb72
-rw-r--r--spec/mspec/spec/integration/tag_spec.rb60
-rw-r--r--spec/mspec/spec/matchers/base_spec.rb228
-rw-r--r--spec/mspec/spec/matchers/be_an_instance_of_spec.rb50
-rw-r--r--spec/mspec/spec/matchers/be_ancestor_of_spec.rb28
-rw-r--r--spec/mspec/spec/matchers/be_close_spec.rb48
-rw-r--r--spec/mspec/spec/matchers/be_computed_by_spec.rb42
-rw-r--r--spec/mspec/spec/matchers/be_empty_spec.rb26
-rw-r--r--spec/mspec/spec/matchers/be_false_spec.rb28
-rw-r--r--spec/mspec/spec/matchers/be_kind_of_spec.rb31
-rw-r--r--spec/mspec/spec/matchers/be_nan_spec.rb28
-rw-r--r--spec/mspec/spec/matchers/be_nil_spec.rb27
-rw-r--r--spec/mspec/spec/matchers/be_true_or_false_spec.rb19
-rw-r--r--spec/mspec/spec/matchers/be_true_spec.rb28
-rw-r--r--spec/mspec/spec/matchers/block_caller_spec.rb13
-rw-r--r--spec/mspec/spec/matchers/complain_spec.rb102
-rw-r--r--spec/mspec/spec/matchers/eql_spec.rb33
-rw-r--r--spec/mspec/spec/matchers/equal_element_spec.rb75
-rw-r--r--spec/mspec/spec/matchers/equal_spec.rb32
-rw-r--r--spec/mspec/spec/matchers/have_class_variable_spec.rb49
-rw-r--r--spec/mspec/spec/matchers/have_constant_spec.rb37
-rw-r--r--spec/mspec/spec/matchers/have_instance_method_spec.rb53
-rw-r--r--spec/mspec/spec/matchers/have_instance_variable_spec.rb50
-rw-r--r--spec/mspec/spec/matchers/have_method_spec.rb55
-rw-r--r--spec/mspec/spec/matchers/have_private_instance_method_spec.rb57
-rw-r--r--spec/mspec/spec/matchers/have_private_method_spec.rb44
-rw-r--r--spec/mspec/spec/matchers/have_protected_instance_method_spec.rb57
-rw-r--r--spec/mspec/spec/matchers/have_public_instance_method_spec.rb53
-rw-r--r--spec/mspec/spec/matchers/have_singleton_method_spec.rb45
-rw-r--r--spec/mspec/spec/matchers/include_any_of_spec.rb42
-rw-r--r--spec/mspec/spec/matchers/include_spec.rb37
-rw-r--r--spec/mspec/spec/matchers/infinity_spec.rb34
-rw-r--r--spec/mspec/spec/matchers/match_yaml_spec.rb39
-rw-r--r--spec/mspec/spec/matchers/output_spec.rb84
-rw-r--r--spec/mspec/spec/matchers/output_to_fd_spec.rb44
-rw-r--r--spec/mspec/spec/matchers/raise_error_spec.rb234
-rw-r--r--spec/mspec/spec/matchers/respond_to_spec.rb33
-rw-r--r--spec/mspec/spec/matchers/signed_zero_spec.rb32
-rw-r--r--spec/mspec/spec/mocks/mock_spec.rb529
-rw-r--r--spec/mspec/spec/mocks/proxy_spec.rb405
-rw-r--r--spec/mspec/spec/runner/actions/filter_spec.rb84
-rw-r--r--spec/mspec/spec/runner/actions/tag_spec.rb313
-rw-r--r--spec/mspec/spec/runner/actions/taglist_spec.rb152
-rw-r--r--spec/mspec/spec/runner/actions/tagpurge_spec.rb154
-rw-r--r--spec/mspec/spec/runner/actions/tally_spec.rb355
-rw-r--r--spec/mspec/spec/runner/actions/timer_spec.rb44
-rw-r--r--spec/mspec/spec/runner/context_spec.rb1028
-rw-r--r--spec/mspec/spec/runner/example_spec.rb117
-rw-r--r--spec/mspec/spec/runner/exception_spec.rb146
-rw-r--r--spec/mspec/spec/runner/filters/a.yaml4
-rw-r--r--spec/mspec/spec/runner/filters/b.yaml11
-rw-r--r--spec/mspec/spec/runner/filters/match_spec.rb34
-rw-r--r--spec/mspec/spec/runner/filters/profile_spec.rb117
-rw-r--r--spec/mspec/spec/runner/filters/regexp_spec.rb31
-rw-r--r--spec/mspec/spec/runner/filters/tag_spec.rb92
-rw-r--r--spec/mspec/spec/runner/formatters/describe_spec.rb67
-rw-r--r--spec/mspec/spec/runner/formatters/dotted_spec.rb284
-rw-r--r--spec/mspec/spec/runner/formatters/file_spec.rb84
-rw-r--r--spec/mspec/spec/runner/formatters/html_spec.rb220
-rw-r--r--spec/mspec/spec/runner/formatters/junit_spec.rb159
-rw-r--r--spec/mspec/spec/runner/formatters/method_spec.rb177
-rw-r--r--spec/mspec/spec/runner/formatters/multi_spec.rb68
-rw-r--r--spec/mspec/spec/runner/formatters/specdoc_spec.rb106
-rw-r--r--spec/mspec/spec/runner/formatters/spinner_spec.rb83
-rw-r--r--spec/mspec/spec/runner/formatters/summary_spec.rb26
-rw-r--r--spec/mspec/spec/runner/formatters/unit_spec.rb73
-rw-r--r--spec/mspec/spec/runner/formatters/yaml_spec.rb134
-rw-r--r--spec/mspec/spec/runner/mspec_spec.rb597
-rw-r--r--spec/mspec/spec/runner/shared_spec.rb90
-rw-r--r--spec/mspec/spec/runner/tag_spec.rb123
-rw-r--r--spec/mspec/spec/runner/tags.txt4
-rw-r--r--spec/mspec/spec/spec_helper.rb70
-rw-r--r--spec/mspec/spec/utils/deprecate_spec.rb17
-rw-r--r--spec/mspec/spec/utils/fixtures/this_file_raises.rb1
-rw-r--r--spec/mspec/spec/utils/fixtures/this_file_raises2.rb1
-rw-r--r--spec/mspec/spec/utils/name_map_spec.rb187
-rw-r--r--spec/mspec/spec/utils/options_spec.rb1302
-rw-r--r--spec/mspec/spec/utils/script_spec.rb470
-rw-r--r--spec/mspec/spec/utils/version_spec.rb45
-rwxr-xr-xspec/mspec/tool/check_require_spec_helper.rb34
-rwxr-xr-xspec/mspec/tool/find.rb10
-rwxr-xr-xspec/mspec/tool/pull-latest-mspec-spec26
-rwxr-xr-xspec/mspec/tool/remove_old_guards.rb145
-rw-r--r--spec/mspec/tool/sync/.gitignore4
-rw-r--r--spec/mspec/tool/sync/sync-rubyspec.rb254
-rwxr-xr-xspec/mspec/tool/tag_from_output.rb65
-rwxr-xr-xspec/mspec/tool/wrap_with_guard.rb28
282 files changed, 22547 insertions, 0 deletions
diff --git a/spec/mspec/.rspec b/spec/mspec/.rspec
new file mode 100644
index 0000000000..4e1e0d2f72
--- /dev/null
+++ b/spec/mspec/.rspec
@@ -0,0 +1 @@
+--color
diff --git a/spec/mspec/Gemfile b/spec/mspec/Gemfile
new file mode 100644
index 0000000000..617a995cad
--- /dev/null
+++ b/spec/mspec/Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+gem "rake", "~> 12.3"
+gem "rspec", "~> 3.0"
diff --git a/spec/mspec/Gemfile.lock b/spec/mspec/Gemfile.lock
new file mode 100644
index 0000000000..cd39906044
--- /dev/null
+++ b/spec/mspec/Gemfile.lock
@@ -0,0 +1,26 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ diff-lcs (1.4.4)
+ rake (12.3.3)
+ rspec (3.10.0)
+ rspec-core (~> 3.10.0)
+ rspec-expectations (~> 3.10.0)
+ rspec-mocks (~> 3.10.0)
+ rspec-core (3.10.1)
+ rspec-support (~> 3.10.0)
+ rspec-expectations (3.10.1)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.10.0)
+ rspec-mocks (3.10.2)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.10.0)
+ rspec-support (3.10.2)
+
+PLATFORMS
+ java
+ ruby
+
+DEPENDENCIES
+ rake (~> 12.3)
+ rspec (~> 3.0)
diff --git a/spec/mspec/LICENSE b/spec/mspec/LICENSE
new file mode 100644
index 0000000000..d581dd1c9f
--- /dev/null
+++ b/spec/mspec/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2008 Engine Yard, Inc. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights 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 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.
diff --git a/spec/mspec/README.md b/spec/mspec/README.md
new file mode 100644
index 0000000000..94ab608031
--- /dev/null
+++ b/spec/mspec/README.md
@@ -0,0 +1,84 @@
+## Overview
+
+MSpec is a specialized framework that is syntax-compatible with RSpec 2 for
+basic things like `describe`, `it` blocks and `before`, `after` actions.
+MSpec contains additional features that assist in writing specs for
+Ruby implementations in [ruby/spec](https://github.com/ruby/spec).
+
+MSpec attempts to use the simplest Ruby language features so that beginning
+Ruby implementations can run the Ruby specs. For example, no file from the
+standard library or RubyGems is necessary to run MSpec.
+
+MSpec is not intended as a replacement for RSpec. MSpec attempts to provide a
+subset of RSpec's features in some cases and a superset in others. It does not
+provide all the matchers, for instance.
+
+However, MSpec provides several extensions to facilitate writing the Ruby
+specs in a manner compatible with multiple Ruby implementations.
+
+ 1. MSpec offers a set of guards to control execution of the specs. These
+ guards not only enable or disable execution but also annotate the specs
+ with additional information about why they are run or not run.
+
+ 2. MSpec provides a different shared spec implementation specifically
+ designed to ease writing specs for the numerous aliased methods in Ruby.
+
+ 3. MSpec provides various helper methods to simplify some specs, for
+ example, creating temporary file names.
+
+ 4. MSpec has several specialized runner scripts that includes a
+ configuration facility with a default project file and user-specific
+ overrides.
+
+ 5. MSpec support "tagging", that is excluding specs known as failing on
+ a particular Ruby implementation, and automatically adding and removing tags
+ while running the specs.
+
+## Requirements
+
+MSpec requires Ruby 2.6 or more recent.
+
+## Bundler
+
+A Gemfile is provided. Use Bundler to install gem dependencies. To install
+Bundler, run the following:
+
+```bash
+gem install bundler
+```
+
+To install the gem dependencies with Bundler, run the following:
+
+```bash
+ruby -S bundle install
+```
+
+## Development
+
+Use RSpec to run the MSpec specs. There are no plans currently to make the
+MSpec specs runnable by MSpec: https://github.com/ruby/mspec/issues/19.
+
+After installing the gem dependencies, the specs can be run as follows:
+
+```bash
+ruby -S bundle exec rspec
+```
+
+To run an individual spec file, use the following example:
+
+```bash
+ruby -S bundle exec rspec spec/helpers/ruby_exe_spec.rb
+```
+
+## Documentation
+
+See [CONTRIBUTING.md](https://github.com/ruby/spec/blob/master/CONTRIBUTING.md) in ruby/spec
+for a list of matchers and how to use `mspec`.
+
+## Source Code
+
+See https://github.com/ruby/mspec
+
+## License
+
+See the LICENSE in the source code.
diff --git a/spec/mspec/Rakefile b/spec/mspec/Rakefile
new file mode 100644
index 0000000000..6a9de7a95e
--- /dev/null
+++ b/spec/mspec/Rakefile
@@ -0,0 +1,6 @@
+require 'bundler/setup'
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new(:spec)
+
+task :default => :spec
diff --git a/spec/mspec/bin/mkspec b/spec/mspec/bin/mkspec
new file mode 100755
index 0000000000..00f1fdff47
--- /dev/null
+++ b/spec/mspec/bin/mkspec
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mkspec'
+
+MkSpec.main
diff --git a/spec/mspec/bin/mkspec.bat b/spec/mspec/bin/mkspec.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mkspec.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/bin/mspec b/spec/mspec/bin/mspec
new file mode 100755
index 0000000000..5bd753c06d
--- /dev/null
+++ b/spec/mspec/bin/mspec
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mspec'
+
+MSpecMain.main(false)
diff --git a/spec/mspec/bin/mspec-ci b/spec/mspec/bin/mspec-ci
new file mode 100755
index 0000000000..d7cd50a827
--- /dev/null
+++ b/spec/mspec/bin/mspec-ci
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mspec-ci'
+
+MSpecCI.main
diff --git a/spec/mspec/bin/mspec-ci.bat b/spec/mspec/bin/mspec-ci.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mspec-ci.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/bin/mspec-run b/spec/mspec/bin/mspec-run
new file mode 100755
index 0000000000..010ecefe35
--- /dev/null
+++ b/spec/mspec/bin/mspec-run
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mspec-run'
+
+MSpecRun.main
diff --git a/spec/mspec/bin/mspec-run.bat b/spec/mspec/bin/mspec-run.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mspec-run.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/bin/mspec-tag b/spec/mspec/bin/mspec-tag
new file mode 100755
index 0000000000..a5f9fffaaa
--- /dev/null
+++ b/spec/mspec/bin/mspec-tag
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mspec-tag'
+
+MSpecTag.main
diff --git a/spec/mspec/bin/mspec-tag.bat b/spec/mspec/bin/mspec-tag.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mspec-tag.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/bin/mspec.bat b/spec/mspec/bin/mspec.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mspec.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/lib/mspec.rb b/spec/mspec/lib/mspec.rb
new file mode 100644
index 0000000000..d24abd96f1
--- /dev/null
+++ b/spec/mspec/lib/mspec.rb
@@ -0,0 +1,8 @@
+require 'mspec/utils/format'
+require 'mspec/matchers'
+require 'mspec/expectations'
+require 'mspec/mocks'
+require 'mspec/runner'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'mspec/version'
diff --git a/spec/mspec/lib/mspec/commands/mkspec.rb b/spec/mspec/lib/mspec/commands/mkspec.rb
new file mode 100644
index 0000000000..f75e683b19
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mkspec.rb
@@ -0,0 +1,143 @@
+require 'rbconfig'
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/name_map'
+require 'mspec/helpers/fs'
+
+class MkSpec
+ attr_reader :config
+
+ def initialize
+ @config = {
+ :constants => [],
+ :requires => [],
+ :base => "core",
+ :version => nil
+ }
+ @map = NameMap.new true
+ end
+
+ def options(argv = ARGV)
+ options = MSpecOptions.new "mkspec [options]", 32
+
+ options.on("-c", "--constant", "CONSTANT",
+ "Class or Module to generate spec stubs for") do |name|
+ config[:constants] << name
+ end
+ options.on("-b", "--base", "DIR",
+ "Directory to generate specs into") do |directory|
+ config[:base] = File.expand_path directory
+ end
+ options.on("-r", "--require", "LIBRARY",
+ "A library to require") do |file|
+ config[:requires] << file
+ end
+ options.on("-V", "--version-guard", "VERSION",
+ "Specify version for ruby_version_is guards") do |version|
+ config[:version] = version
+ end
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n How might this work in the real world?\n"
+ options.doc " 1. To create spec stubs for every class or module in Object\n"
+ options.doc " $ mkspec\n"
+ options.doc " 2. To create spec stubs for Fixnum\n"
+ options.doc " $ mkspec -c Fixnum\n"
+ options.doc " 3. To create spec stubs for Complex in 'superspec/complex'\n"
+ options.doc " $ mkspec -c Complex -r complex -b superspec"
+ options.doc ""
+
+ options.parse argv
+ end
+
+ def create_directory(mod)
+ subdir = @map.dir_name mod, config[:base]
+
+ if File.exist? subdir
+ unless File.directory? subdir
+ puts "#{subdir} already exists and is not a directory."
+ return nil
+ end
+ else
+ mkdir_p subdir
+ end
+
+ subdir
+ end
+
+ def write_requires(dir, file)
+ prefix = config[:base] + '/'
+ raise dir unless dir.start_with? prefix
+ sub = dir[prefix.size..-1]
+ parents = '../' * (sub.split('/').length + 1)
+
+ File.open(file, 'w') do |f|
+ f.puts "require_relative '#{parents}spec_helper'"
+ config[:requires].each do |lib|
+ f.puts "require '#{lib}'"
+ end
+ end
+ end
+
+ def write_version(f)
+ f.puts ""
+ if version = config[:version]
+ f.puts "ruby_version_is #{version} do"
+ yield " "
+ f.puts "end"
+ else
+ yield ""
+ end
+ end
+
+ def write_spec(file, meth, exists)
+ if exists
+ command = "#{RbConfig.ruby} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e '#{meth}' #{file}"
+ puts "$ #{command}" if $DEBUG
+ out = `#{command}`
+ return if out.include?(meth)
+ end
+
+ File.open file, 'a' do |f|
+ write_version(f) do |indent|
+ f.puts <<-EOS
+#{indent}describe "#{meth}" do
+#{indent} it "needs to be reviewed for spec completeness"
+#{indent}end
+EOS
+ end
+ end
+
+ puts file
+ end
+
+ def create_file(dir, mod, meth, name)
+ file = File.join dir, @map.file_name(meth, mod)
+ exists = File.exist? file
+
+ write_requires dir, file unless exists
+ write_spec file, name, exists
+ end
+
+ def run
+ config[:requires].each { |lib| require lib }
+ constants = config[:constants]
+ constants = Object.constants if constants.empty?
+
+ @map.map({}, constants).each do |mod, methods|
+ name = mod.chop
+ next unless dir = create_directory(name)
+
+ methods.each { |method| create_file dir, name, method, mod + method }
+ end
+ end
+
+ def self.main
+ ENV['MSPEC_RUNNER'] = '1'
+
+ script = new
+ script.options
+ script.run
+ end
+end
diff --git a/spec/mspec/lib/mspec/commands/mspec-ci.rb b/spec/mspec/lib/mspec/commands/mspec-ci.rb
new file mode 100644
index 0000000000..8951572f69
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec-ci.rb
@@ -0,0 +1,76 @@
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+
+
+class MSpecCI < MSpecScript
+ def options(argv = ARGV)
+ options = MSpecOptions.new "mspec ci [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " Ask yourself:"
+ options.doc " 1. How to run the specs?"
+ options.doc " 2. How to modify the guard behavior?"
+ options.doc " 2. How to display the output?"
+ options.doc " 3. What action to perform?"
+ options.doc " 4. When to perform it?"
+
+ options.doc "\n How to run the specs"
+ options.chdir
+ options.prefix
+ options.configure { |f| load f }
+ options.repeat
+ options.pretend
+ options.interrupt
+ options.timeout
+
+ options.doc "\n How to modify the guard behavior"
+ options.unguarded
+ options.verify
+
+ options.doc "\n How to display their output"
+ options.formatters
+ options.verbose
+
+ options.doc "\n What action to perform"
+ options.actions
+
+ options.doc "\n When to perform it"
+ options.action_filters
+
+ options.doc "\n Help!"
+ options.debug
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ options.doc "\n How might this work in the real world?"
+ options.doc "\n 1. To simply run the known good specs"
+ options.doc "\n $ mspec ci"
+ options.doc "\n 2. To run a subset of the known good specs"
+ options.doc "\n $ mspec ci path/to/specs"
+ options.doc "\n 3. To start the debugger before the spec matching 'this crashes'"
+ options.doc "\n $ mspec ci --spec-debug -S 'this crashes'"
+ options.doc ""
+
+ patterns = options.parse argv
+ patterns = config[:ci_files] if patterns.empty?
+ @files = files patterns
+ end
+
+ def run
+ MSpec.register_tags_patterns config[:tags_patterns]
+ MSpec.register_files @files
+
+ tags = ["fails", "critical", "unstable", "incomplete", "unsupported"]
+ tags += Array(config[:ci_xtags])
+
+ require 'mspec/runner/filters/tag'
+ filter = TagFilter.new(:exclude, *tags)
+ filter.register
+
+ MSpec.process
+ exit MSpec.exit_code
+ end
+end
diff --git a/spec/mspec/lib/mspec/commands/mspec-run.rb b/spec/mspec/lib/mspec/commands/mspec-run.rb
new file mode 100644
index 0000000000..0fb338fa23
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec-run.rb
@@ -0,0 +1,87 @@
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+
+
+class MSpecRun < MSpecScript
+ def initialize
+ super
+
+ config[:files] = []
+ end
+
+ def options(argv = ARGV)
+ options = MSpecOptions.new "mspec run [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " Ask yourself:"
+ options.doc " 1. What specs to run?"
+ options.doc " 2. How to modify the execution?"
+ options.doc " 3. How to modify the guard behavior?"
+ options.doc " 4. How to display the output?"
+ options.doc " 5. What action to perform?"
+ options.doc " 6. When to perform it?"
+
+ options.doc "\n What specs to run"
+ options.filters
+
+ options.doc "\n How to modify the execution"
+ options.chdir
+ options.prefix
+ options.configure { |f| load f }
+ options.env
+ options.randomize
+ options.repeat
+ options.pretend
+ options.interrupt
+ options.timeout
+
+ options.doc "\n How to modify the guard behavior"
+ options.unguarded
+ options.verify
+
+ options.doc "\n How to display their output"
+ options.formatters
+ options.verbose
+
+ options.doc "\n What action to perform"
+ options.actions
+
+ options.doc "\n When to perform it"
+ options.action_filters
+
+ options.doc "\n Launchable"
+ options.launchable
+
+ options.doc "\n Help!"
+ options.debug
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ options.doc "\n How might this work in the real world?"
+ options.doc "\n 1. To simply run some specs"
+ options.doc "\n $ mspec path/to/the/specs"
+ options.doc " mspec path/to/the_file_spec.rb"
+ options.doc "\n 2. To run specs tagged with 'fails'"
+ options.doc "\n $ mspec -g fails path/to/the_file_spec.rb"
+ options.doc "\n 3. To start the debugger before the spec matching 'this crashes'"
+ options.doc "\n $ mspec --spec-debug -S 'this crashes' path/to/the_file_spec.rb"
+ options.doc "\n 4. To run some specs matching 'this crashes'"
+ options.doc "\n $ mspec -e 'this crashes' path/to/the_file_spec.rb"
+
+ options.doc ""
+
+ patterns = options.parse argv
+ @files = files_from_patterns(patterns)
+ end
+
+ def run
+ MSpec.register_tags_patterns config[:tags_patterns]
+ MSpec.register_files @files
+
+ MSpec.process
+ exit MSpec.exit_code
+ end
+end
diff --git a/spec/mspec/lib/mspec/commands/mspec-tag.rb b/spec/mspec/lib/mspec/commands/mspec-tag.rb
new file mode 100644
index 0000000000..9ce9f048c6
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec-tag.rb
@@ -0,0 +1,132 @@
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+
+
+class MSpecTag < MSpecScript
+ def initialize
+ super
+
+ config[:tagger] = :add
+ config[:tag] = 'fails:'
+ config[:outcome] = :fail
+ config[:ltags] = []
+ end
+
+ def options(argv = ARGV)
+ options = MSpecOptions.new "mspec tag [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " Ask yourself:"
+ options.doc " 1. What specs to run?"
+ options.doc " 2. How to modify the execution?"
+ options.doc " 3. How to display the output?"
+ options.doc " 4. What tag action to perform?"
+ options.doc " 5. When to perform it?"
+
+ options.doc "\n What specs to run"
+ options.filters
+
+ options.doc "\n How to modify the execution"
+ options.configure { |f| load f }
+ options.pretend
+ options.unguarded
+ options.interrupt
+ options.timeout
+
+ options.doc "\n How to display their output"
+ options.formatters
+ options.verbose
+
+ options.doc "\n What action to perform and when to perform it"
+ options.on("-N", "--add", "TAG",
+ "Add TAG with format 'tag' or 'tag(comment)' (see -Q, -F, -L)") do |o|
+ config[:tagger] = :add
+ config[:tag] = "#{o}:"
+ end
+ options.on("-R", "--del", "TAG",
+ "Delete TAG (see -Q, -F, -L)") do |o|
+ config[:tagger] = :del
+ config[:tag] = "#{o}:"
+ config[:outcome] = :pass
+ end
+ options.on("-Q", "--pass", "Apply action to specs that pass (default for --del)") do
+ config[:outcome] = :pass
+ end
+ options.on("-F", "--fail", "Apply action to specs that fail (default for --add)") do
+ config[:outcome] = :fail
+ end
+ options.on("-L", "--all", "Apply action to all specs") do
+ config[:outcome] = :all
+ end
+ options.on("--list", "TAG", "Display descriptions of any specs tagged with TAG") do |t|
+ config[:tagger] = :list
+ config[:ltags] << t
+ end
+ options.on("--list-all", "Display descriptions of any tagged specs") do
+ config[:tagger] = :list_all
+ end
+ options.on("--purge", "Remove all tags not matching any specs") do
+ config[:tagger] = :purge
+ end
+
+ options.doc "\n Help!"
+ options.debug
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ options.doc "\n How might this work in the real world?"
+ options.doc "\n 1. To add the 'fails' tag to failing specs"
+ options.doc "\n $ mspec tag path/to/the_file_spec.rb"
+ options.doc "\n 2. To remove the 'fails' tag from passing specs"
+ options.doc "\n $ mspec tag --del fails path/to/the_file_spec.rb"
+ options.doc "\n 3. To display the descriptions for all specs tagged with 'fails'"
+ options.doc "\n $ mspec tag --list fails path/to/the/specs"
+ options.doc ""
+
+ patterns = options.parse argv
+ if patterns.empty?
+ puts options
+ puts "No files specified."
+ exit 1
+ end
+ @files = files patterns
+ end
+
+ def register
+ require 'mspec/runner/actions'
+
+ case config[:tagger]
+ when :add, :del
+ tag = SpecTag.new config[:tag]
+ tagger = TagAction.new(config[:tagger], config[:outcome], tag.tag, tag.comment,
+ config[:atags], config[:astrings])
+ when :list, :list_all
+ tagger = TagListAction.new config[:tagger] == :list_all ? nil : config[:ltags]
+ MSpec.register_mode :pretend
+ config[:formatter] = false
+ when :purge
+ tagger = TagPurgeAction.new
+ MSpec.register_mode :pretend
+ MSpec.register_mode :unguarded
+ config[:formatter] = false
+ config[:xtags] = []
+ else
+ raise ArgumentError, "No recognized action given"
+ end
+ tagger.register
+
+ super
+ end
+
+ def run
+ MSpec.register_tags_patterns config[:tags_patterns]
+ MSpec.register_files @files
+
+ MSpec.process
+ exit MSpec.exit_code
+ end
+end
+
diff --git a/spec/mspec/lib/mspec/commands/mspec.rb b/spec/mspec/lib/mspec/commands/mspec.rb
new file mode 100644
index 0000000000..a9d94ca354
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec.rb
@@ -0,0 +1,113 @@
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+require 'mspec/helpers/tmp'
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/timer'
+
+
+class MSpecMain < MSpecScript
+ def initialize
+ super
+
+ config[:loadpath] = []
+ config[:requires] = []
+ config[:target] = ENV['RUBY'] || 'ruby'
+ config[:flags] = []
+ config[:command] = nil
+ config[:options] = []
+ config[:launch] = []
+ end
+
+ def options(argv = ARGV)
+ config[:command] = argv.shift if ["ci", "run", "tag"].include?(argv[0])
+
+ options = MSpecOptions.new "mspec [COMMAND] [options] (FILE|DIRECTORY|GLOB)+", 30, config
+ @options = options
+
+ options.doc " The mspec command sets up and invokes the sub-commands"
+ options.doc " (see below) to enable, for instance, running the specs"
+ options.doc " with different implementations like ruby, jruby, rbx, etc.\n"
+
+ options.configure do |f|
+ load f
+ config[:options] << '-B' << f
+ end
+
+ options.targets
+
+ options.on("-j", "--multi", "Run multiple (possibly parallel) subprocesses") do
+ config[:multi] = true
+ end
+
+ options.version MSpec::VERSION do
+ if config[:command]
+ config[:options] << "-v"
+ else
+ puts "#{File.basename $0} #{MSpec::VERSION}"
+ exit
+ end
+ end
+
+ options.help do
+ if config[:command]
+ config[:options] << "-h"
+ else
+ puts options
+ exit 1
+ end
+ end
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ # The rest of the help output
+ options.doc "\n where COMMAND is one of:\n"
+ options.doc " run - Run the specified specs (default)"
+ options.doc " ci - Run the known good specs"
+ options.doc " tag - Add or remove tags\n"
+ options.doc " mspec COMMAND -h for more options\n"
+ options.doc " example: $ mspec run -h\n"
+
+ options.on_extra { |o| config[:options] << o }
+ options.parse(argv)
+
+ if config[:multi]
+ options = MSpecOptions.new "mspec", 30, config
+ options.all
+ patterns = options.parse(config[:options])
+ @files = files_from_patterns(patterns)
+ end
+ end
+
+ def register; end
+
+ def multi_exec(argv)
+ require 'mspec/runner/formatters/multi'
+ formatter = config_formatter.extend(MultiFormatter)
+
+ require 'mspec/runner/parallel'
+ processes = cores(@files.size)
+ ParallelRunner.new(@files, processes, formatter, argv).run
+ end
+
+ def run
+ argv = config[:target].split(/\s+/)
+
+ argv.concat config[:launch]
+ argv.concat config[:flags]
+ argv.concat config[:loadpath]
+ argv.concat config[:requires]
+ argv << "#{MSPEC_HOME}/bin/mspec-#{config[:command] || 'run'}"
+ argv.concat config[:options]
+
+ if config[:multi]
+ exit multi_exec(argv)
+ else
+ log = config[:options].include?('--error-output') ? $stdout : $stderr
+ log.puts "$ #{argv.join(' ')}"
+ log.flush
+ exec(*argv, close_others: false)
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/expectations.rb b/spec/mspec/lib/mspec/expectations.rb
new file mode 100644
index 0000000000..d07f959b27
--- /dev/null
+++ b/spec/mspec/lib/mspec/expectations.rb
@@ -0,0 +1,2 @@
+require 'mspec/expectations/expectations'
+require 'mspec/expectations/should'
diff --git a/spec/mspec/lib/mspec/expectations/expectations.rb b/spec/mspec/lib/mspec/expectations/expectations.rb
new file mode 100644
index 0000000000..09852ab557
--- /dev/null
+++ b/spec/mspec/lib/mspec/expectations/expectations.rb
@@ -0,0 +1,39 @@
+class SpecExpectationNotMetError < StandardError
+end
+
+class SpecExpectationNotFoundError < StandardError
+ def message
+ "No behavior expectation was found in the example"
+ end
+end
+
+class SkippedSpecError < StandardError
+end
+
+class SpecExpectation
+ def self.fail_with(expected, actual)
+ expected_to_s = expected.to_s
+ actual_to_s = actual.to_s
+ if expected_to_s.size + actual_to_s.size > 80
+ message = "#{expected_to_s}\n#{actual_to_s}"
+ else
+ message = "#{expected_to_s} #{actual_to_s}"
+ end
+ raise SpecExpectationNotMetError, message
+ end
+
+ def self.fail_predicate(receiver, predicate, args, block, result, expectation)
+ receiver_to_s = MSpec.format(receiver)
+ before_method = predicate.to_s =~ /^[a-z]/ ? "." : " "
+ predicate_to_s = "#{before_method}#{predicate}"
+ predicate_to_s += " " unless args.empty?
+ args_to_s = args.map { |arg| MSpec.format(arg) }.join(', ')
+ args_to_s += " { ... }" if block
+ result_to_s = MSpec.format(result)
+ raise SpecExpectationNotMetError, "Expected #{receiver_to_s}#{predicate_to_s}#{args_to_s}\n#{expectation} but was #{result_to_s}"
+ end
+
+ def self.fail_single_arg_predicate(receiver, predicate, arg, result, expectation)
+ fail_predicate(receiver, predicate, [arg], nil, result, expectation)
+ end
+end
diff --git a/spec/mspec/lib/mspec/expectations/should.rb b/spec/mspec/lib/mspec/expectations/should.rb
new file mode 100644
index 0000000000..c1790e0ac8
--- /dev/null
+++ b/spec/mspec/lib/mspec/expectations/should.rb
@@ -0,0 +1,41 @@
+class Object
+ NO_MATCHER_GIVEN = Object.new
+
+ def should(matcher = NO_MATCHER_GIVEN, &block)
+ MSpec.expectation
+ state = MSpec.current.state
+ raise "should outside example" unless state
+ MSpec.actions :expectation, state
+
+ if NO_MATCHER_GIVEN.equal?(matcher)
+ SpecPositiveOperatorMatcher.new(self)
+ else
+ # The block was given to #should syntactically, but it was intended for a matcher like #raise_error
+ matcher.block = block if block
+
+ unless matcher.matches? self
+ expected, actual = matcher.failure_message
+ SpecExpectation.fail_with(expected, actual)
+ end
+ end
+ end
+
+ def should_not(matcher = NO_MATCHER_GIVEN, &block)
+ MSpec.expectation
+ state = MSpec.current.state
+ raise "should_not outside example" unless state
+ MSpec.actions :expectation, state
+
+ if NO_MATCHER_GIVEN.equal?(matcher)
+ SpecNegativeOperatorMatcher.new(self)
+ else
+ # The block was given to #should_not syntactically, but it was intended for the matcher
+ matcher.block = block if block
+
+ if matcher.matches? self
+ expected, actual = matcher.negative_failure_message
+ SpecExpectation.fail_with(expected, actual)
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards.rb b/spec/mspec/lib/mspec/guards.rb
new file mode 100644
index 0000000000..454ac0c776
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards.rb
@@ -0,0 +1,11 @@
+require 'mspec/guards/block_device'
+require 'mspec/guards/bug'
+require 'mspec/guards/conflict'
+require 'mspec/guards/endian'
+require 'mspec/guards/feature'
+require 'mspec/guards/guard'
+require 'mspec/guards/platform'
+require 'mspec/guards/quarantine'
+require 'mspec/guards/support'
+require 'mspec/guards/superuser'
+require 'mspec/guards/version'
diff --git a/spec/mspec/lib/mspec/guards/block_device.rb b/spec/mspec/lib/mspec/guards/block_device.rb
new file mode 100644
index 0000000000..ae736a2d4e
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/block_device.rb
@@ -0,0 +1,16 @@
+require 'mspec/guards/guard'
+
+class BlockDeviceGuard < SpecGuard
+ def match?
+ platform_is_not :freebsd, :windows, :opal do
+ block = `find /dev /devices -type b 2> /dev/null`
+ return !(block.nil? || block.empty?)
+ end
+
+ false
+ end
+end
+
+def with_block_device(&block)
+ BlockDeviceGuard.new.run_if(:with_block_device, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/bug.rb b/spec/mspec/lib/mspec/guards/bug.rb
new file mode 100644
index 0000000000..a6af0ef964
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/bug.rb
@@ -0,0 +1,29 @@
+require 'mspec/guards/version'
+
+class BugGuard < VersionGuard
+ def initialize(bug, requirement)
+ @bug = bug
+ if String === requirement
+ MSpec.deprecate "ruby_bug with a single version", 'an exclusive range ("2.1"..."2.3")'
+ super(FULL_RUBY_VERSION, requirement)
+ @requirement = SpecVersion.new requirement, true
+ else
+ super(FULL_RUBY_VERSION, requirement)
+ end
+ end
+
+ def match?
+ return false if MSpec.mode? :no_ruby_bug
+ return false unless PlatformGuard.standard?
+
+ if Range === @requirement
+ super
+ else
+ FULL_RUBY_VERSION <= @requirement
+ end
+ end
+end
+
+def ruby_bug(bug, requirement, &block)
+ BugGuard.new(bug, requirement).run_unless(:ruby_bug, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/conflict.rb b/spec/mspec/lib/mspec/guards/conflict.rb
new file mode 100644
index 0000000000..4930e5734d
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/conflict.rb
@@ -0,0 +1,23 @@
+require 'mspec/guards/guard'
+require 'mspec/utils/deprecate'
+
+class ConflictsGuard < SpecGuard
+ def initialize(*args)
+ MSpec.deprecate 'conflicts_with', 'guard -> { condition } do'
+ super(*args)
+ end
+
+ def match?
+ # Always convert constants to symbols regardless of version.
+ constants = Object.constants.map { |x| x.to_sym }
+ @parameters.any? { |mod| constants.include? mod }
+ end
+end
+
+# In some cases, libraries will modify another Ruby method's
+# behavior. The specs for the method's behavior will then fail
+# if that library is loaded. This guard will not run if any of
+# the specified constants exist in Object.constants.
+def conflicts_with(*modules, &block)
+ ConflictsGuard.new(*modules).run_unless(:conflicts_with, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/endian.rb b/spec/mspec/lib/mspec/guards/endian.rb
new file mode 100644
index 0000000000..79335a8933
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/endian.rb
@@ -0,0 +1,25 @@
+require 'mspec/guards/guard'
+
+# Despite that these are inverses, the two classes are
+# used to simplify MSpec guard reporting modes
+
+class EndianGuard < SpecGuard
+ def pattern
+ @pattern ||= [1].pack('L')
+ end
+ private :pattern
+end
+
+class BigEndianGuard < EndianGuard
+ def match?
+ pattern[-1] == ?\001
+ end
+end
+
+def big_endian(&block)
+ BigEndianGuard.new.run_if(:big_endian, &block)
+end
+
+def little_endian(&block)
+ BigEndianGuard.new.run_unless(:little_endian, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/feature.rb b/spec/mspec/lib/mspec/guards/feature.rb
new file mode 100644
index 0000000000..d4c6dd1cde
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/feature.rb
@@ -0,0 +1,45 @@
+require 'mspec/guards/guard'
+
+class FeatureGuard < SpecGuard
+ def self.enabled?(*features)
+ new(*features).match?
+ end
+
+ def match?
+ @parameters.all? { |f| MSpec.feature_enabled? f }
+ end
+end
+
+# Provides better documentation in the specs by
+# naming sets of features that work together as
+# a whole. Examples include :encoding, :fiber,
+# :continuation, :fork.
+#
+# Usage example:
+#
+# with_feature :encoding do
+# # specs for a method that provides aspects
+# # of the encoding feature
+# end
+#
+# Multiple features must all be enabled for the
+# guard to run:
+#
+# with_feature :one, :two do
+# # these specs will run if features :one AND
+# # :two are enabled.
+# end
+#
+# The implementation must explicitly enable a feature
+# by adding code like the following to the .mspec
+# configuration file:
+#
+# MSpec.enable_feature :encoding
+#
+def with_feature(*features, &block)
+ FeatureGuard.new(*features).run_if(:with_feature, &block)
+end
+
+def without_feature(*features, &block)
+ FeatureGuard.new(*features).run_unless(:without_feature, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/guard.rb b/spec/mspec/lib/mspec/guards/guard.rb
new file mode 100644
index 0000000000..3a6372a660
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/guard.rb
@@ -0,0 +1,141 @@
+require 'mspec/runner/mspec'
+require 'mspec/runner/actions/tally'
+
+class SpecGuard
+ def self.report
+ @report ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ def self.clear
+ @report = nil
+ end
+
+ def self.finish
+ report.keys.sort.each do |key|
+ desc = report[key]
+ size = desc.size
+ spec = size == 1 ? "spec" : "specs"
+ print "\n\n#{size} #{spec} omitted by guard: #{key}:\n"
+ desc.each { |description| print "\n", description; }
+ end
+
+ print "\n\n"
+ end
+
+ def self.guards
+ @guards ||= []
+ end
+
+ def self.clear_guards
+ @guards = []
+ end
+
+ # Returns a partial Ruby version string based on +which+.
+ # For example, if RUBY_VERSION = 8.2.3:
+ #
+ # :major => "8"
+ # :minor => "8.2"
+ # :tiny => "8.2.3"
+ # :teeny => "8.2.3"
+ # :full => "8.2.3"
+ def self.ruby_version(which = :minor)
+ case which
+ when :major
+ n = 1
+ when :minor
+ n = 2
+ when :tiny, :teeny, :full
+ n = 3
+ end
+
+ RUBY_VERSION.split('.')[0,n].join('.')
+ end
+
+ attr_accessor :name
+
+ def initialize(*args)
+ @parameters = args
+ end
+
+ def yield?(invert = false)
+ return true if MSpec.mode? :unguarded
+
+ allow = match? ^ invert
+
+ if !allow and reporting?
+ MSpec.guard
+ MSpec.register :finish, SpecGuard
+ MSpec.register :add, self
+ return true
+ elsif MSpec.mode? :verify
+ return true
+ end
+
+ allow
+ end
+
+ def run_if(name, &block)
+ @name = name
+ if block
+ yield if yield?(false)
+ else
+ yield?(false)
+ end
+ ensure
+ unregister
+ end
+
+ def run_unless(name, &block)
+ @name = name
+ if block
+ yield if yield?(true)
+ else
+ yield?(true)
+ end
+ ensure
+ unregister
+ end
+
+ def reporting?
+ MSpec.mode?(:report) or
+ (MSpec.mode?(:report_on) and SpecGuard.guards.include?(name))
+ end
+
+ def report_key
+ "#{name} #{@parameters.join(", ")}"
+ end
+
+ def record(description)
+ SpecGuard.report[report_key] << description
+ end
+
+ def add(example)
+ record example.description
+ MSpec.formatter.tally.counter.guards!
+ end
+
+ def unregister
+ MSpec.unguard
+ MSpec.unregister :add, self
+ end
+
+ def match?
+ raise "must be implemented by the subclass"
+ end
+end
+
+# Combined guards
+
+def guard(condition, &block)
+ raise "condition must be a Proc" unless condition.is_a?(Proc)
+ raise LocalJumpError, "no block given" unless block
+ return yield if MSpec.mode? :unguarded or MSpec.mode? :verify or MSpec.mode? :report
+ yield if condition.call
+end
+
+def guard_not(condition, &block)
+ raise "condition must be a Proc" unless condition.is_a?(Proc)
+ raise LocalJumpError, "no block given" unless block
+ return yield if MSpec.mode? :unguarded or MSpec.mode? :verify or MSpec.mode? :report
+ yield unless condition.call
+end
diff --git a/spec/mspec/lib/mspec/guards/platform.rb b/spec/mspec/lib/mspec/guards/platform.rb
new file mode 100644
index 0000000000..fadd8d75ef
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/platform.rb
@@ -0,0 +1,122 @@
+require 'mspec/guards/guard'
+
+class PlatformGuard < SpecGuard
+ def self.implementation?(*args)
+ args.any? do |name|
+ case name
+ when :rubinius
+ RUBY_ENGINE.start_with?('rbx')
+ else
+ RUBY_ENGINE.start_with?(name.to_s)
+ end
+ end
+ end
+
+ def self.standard?
+ implementation? :ruby
+ end
+
+ PLATFORM = if RUBY_ENGINE == "jruby"
+ require 'rbconfig'
+ "#{RbConfig::CONFIG['host_cpu']}-#{RbConfig::CONFIG['host_os']}"
+ else
+ RUBY_PLATFORM
+ end
+
+ def self.os?(*oses)
+ oses.any? do |os|
+ raise ":java is not a valid OS" if os == :java
+ case os
+ when :windows
+ PLATFORM =~ /(mswin|mingw)/
+ when :wsl
+ wsl?
+ else
+ PLATFORM.include?(os.to_s)
+ end
+ end
+ end
+
+ def self.windows?
+ os?(:windows)
+ end
+
+ def self.wasi?
+ os?(:wasi)
+ end
+
+ def self.wsl?
+ if defined?(@wsl_p)
+ @wsl_p
+ else
+ @wsl_p = `uname -r`.match?(/microsoft/i)
+ end
+ end
+
+ # In bits
+ WORD_SIZE = 1.size * 8
+ deprecate_constant :WORD_SIZE
+
+ # In bits
+ POINTER_SIZE = begin
+ require 'rbconfig/sizeof'
+ RbConfig::SIZEOF["void*"] * 8
+ rescue LoadError
+ [0].pack('j').size * 8
+ end
+
+ # In bits
+ C_LONG_SIZE = if defined?(RbConfig::SIZEOF[])
+ RbConfig::SIZEOF["long"] * 8
+ else
+ [0].pack('l!').size * 8
+ end
+
+ def self.wordsize?(size)
+ warn "#wordsize? is deprecated, use #c_long_size?"
+ size == WORD_SIZE
+ end
+
+ def self.pointer_size?(size)
+ size == POINTER_SIZE
+ end
+
+ def self.c_long_size?(size)
+ size == C_LONG_SIZE
+ end
+
+ def initialize(*args)
+ if args.last.is_a?(Hash)
+ @options, @platforms = args.last, args[0..-2]
+ else
+ @options, @platforms = {}, args
+ end
+ @parameters = args
+ end
+
+ def match?
+ match = @platforms.empty? ? true : PlatformGuard.os?(*@platforms)
+ @options.each do |key, value|
+ case key
+ when :os
+ match &&= PlatformGuard.os?(*value)
+ when :pointer_size
+ match &&= PlatformGuard.pointer_size? value
+ when :wordsize
+ warn ":wordsize is deprecated, use :c_long_size"
+ match &&= PlatformGuard.wordsize? value
+ when :c_long_size
+ match &&= PlatformGuard::c_long_size? value
+ end
+ end
+ match
+ end
+end
+
+def platform_is(*args, &block)
+ PlatformGuard.new(*args).run_if(:platform_is, &block)
+end
+
+def platform_is_not(*args, &block)
+ PlatformGuard.new(*args).run_unless(:platform_is_not, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/quarantine.rb b/spec/mspec/lib/mspec/guards/quarantine.rb
new file mode 100644
index 0000000000..ec4d01f9ea
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/quarantine.rb
@@ -0,0 +1,11 @@
+require 'mspec/guards/guard'
+
+class QuarantineGuard < SpecGuard
+ def match?
+ true
+ end
+end
+
+def quarantine!(&block)
+ QuarantineGuard.new.run_unless(:quarantine!, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/superuser.rb b/spec/mspec/lib/mspec/guards/superuser.rb
new file mode 100644
index 0000000000..24daf9b26c
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/superuser.rb
@@ -0,0 +1,25 @@
+require 'mspec/guards/guard'
+
+class SuperUserGuard < SpecGuard
+ def match?
+ Process.euid == 0
+ end
+end
+
+class RealSuperUserGuard < SpecGuard
+ def match?
+ Process.uid == 0
+ end
+end
+
+def as_superuser(&block)
+ SuperUserGuard.new.run_if(:as_superuser, &block)
+end
+
+def as_real_superuser(&block)
+ RealSuperUserGuard.new.run_if(:as_real_superuser, &block)
+end
+
+def as_user(&block)
+ SuperUserGuard.new.run_unless(:as_user, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/support.rb b/spec/mspec/lib/mspec/guards/support.rb
new file mode 100644
index 0000000000..790bea1077
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/support.rb
@@ -0,0 +1,14 @@
+require 'mspec/guards/platform'
+
+class SupportedGuard < SpecGuard
+ def match?
+ if @parameters.include? :ruby
+ raise Exception, "improper use of not_supported_on guard"
+ end
+ !PlatformGuard.standard? and PlatformGuard.implementation?(*@parameters)
+ end
+end
+
+def not_supported_on(*args, &block)
+ SupportedGuard.new(*args).run_unless(:not_supported_on, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/version.rb b/spec/mspec/lib/mspec/guards/version.rb
new file mode 100644
index 0000000000..f5ea1988ae
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/version.rb
@@ -0,0 +1,72 @@
+require 'mspec/utils/deprecate'
+require 'mspec/utils/version'
+require 'mspec/guards/guard'
+
+class VersionGuard < SpecGuard
+ FULL_RUBY_VERSION = SpecVersion.new SpecGuard.ruby_version(:full)
+
+ def initialize(version, requirement)
+ version = SpecVersion.new(version) unless SpecVersion === version
+ @version = version
+
+ case requirement
+ when String
+ @requirement = SpecVersion.new requirement
+ when Range
+ MSpec.deprecate "an empty version range end", 'a specific version' if requirement.end.empty?
+ a = SpecVersion.new requirement.begin
+ b = SpecVersion.new requirement.end
+ unless requirement.exclude_end?
+ MSpec.deprecate "ruby_version_is with an inclusive range", 'an exclusive range ("2.1"..."2.3")'
+ end
+ @requirement = requirement.exclude_end? ? a...b : a..b
+ else
+ raise "version must be a String or Range but was a #{requirement.class}"
+ end
+ super(@version, @requirement)
+ end
+
+ def match?
+ if Range === @requirement
+ @requirement.include? @version
+ else
+ @version >= @requirement
+ end
+ end
+
+ @kernel_version = nil
+ def self.kernel_version
+ if @kernel_version
+ @kernel_version
+ else
+ if v = RUBY_PLATFORM[/darwin(\d+)/, 1] # build time version
+ uname = v
+ else
+ begin
+ require 'etc'
+ etc = true
+ rescue LoadError
+ etc = false
+ end
+ if etc and Etc.respond_to?(:uname)
+ uname = Etc.uname.fetch(:release)
+ else
+ uname = `uname -r`.chomp
+ end
+ end
+ @kernel_version = uname
+ end
+ end
+end
+
+def version_is(base_version, requirement, &block)
+ VersionGuard.new(base_version, requirement).run_if(:version_is, &block)
+end
+
+def ruby_version_is(requirement, &block)
+ VersionGuard.new(VersionGuard::FULL_RUBY_VERSION, requirement).run_if(:ruby_version_is, &block)
+end
+
+def kernel_version_is(requirement, &block)
+ VersionGuard.new(VersionGuard.kernel_version, requirement).run_if(:kernel_version_is, &block)
+end
diff --git a/spec/mspec/lib/mspec/helpers.rb b/spec/mspec/lib/mspec/helpers.rb
new file mode 100644
index 0000000000..90f9fd3fd4
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers.rb
@@ -0,0 +1,13 @@
+require 'mspec/helpers/argf'
+require 'mspec/helpers/argv'
+require 'mspec/helpers/datetime'
+require 'mspec/helpers/fixture'
+require 'mspec/helpers/flunk'
+require 'mspec/helpers/fs'
+require 'mspec/helpers/io'
+require 'mspec/helpers/mock_to_path'
+require 'mspec/helpers/numeric'
+require 'mspec/helpers/ruby_exe'
+require 'mspec/helpers/scratch'
+require 'mspec/helpers/tmp'
+require 'mspec/helpers/warning'
diff --git a/spec/mspec/lib/mspec/helpers/argf.rb b/spec/mspec/lib/mspec/helpers/argf.rb
new file mode 100644
index 0000000000..4d3e0f46b3
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/argf.rb
@@ -0,0 +1,35 @@
+# Convenience helper for specs using ARGF.
+# Set @argf to an instance of ARGF.class with the given +argv+.
+# That instance must be used instead of ARGF as ARGF is global
+# and it is not always possible to reset its state correctly.
+#
+# The helper yields to the block and then close
+# the files open by the instance. Example:
+#
+# describe "That" do
+# it "does something" do
+# argf ['a', 'b'] do
+# # do something
+# end
+# end
+# end
+def argf(argv)
+ if argv.empty? or argv.length > 2
+ raise "Only 1 or 2 filenames are allowed for the argf helper so files can be properly closed: #{argv.inspect}"
+ end
+ @argf ||= nil
+ raise "Cannot nest calls to the argf helper" if @argf
+
+ @argf = ARGF.class.new(*argv)
+ @__mspec_saved_argf_file__ = @argf.file
+ begin
+ yield
+ ensure
+ file1 = @__mspec_saved_argf_file__
+ file2 = @argf.file # Either the first file or the second
+ file1.close if !file1.closed? and file1 != STDIN
+ file2.close if !file2.closed? and file2 != STDIN
+ @argf = nil
+ @__mspec_saved_argf_file__ = nil
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/argv.rb b/spec/mspec/lib/mspec/helpers/argv.rb
new file mode 100644
index 0000000000..9dac384dbd
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/argv.rb
@@ -0,0 +1,44 @@
+# Convenience helper for altering ARGV. Saves the
+# value of ARGV and sets it to +args+. If a block
+# is given, yields to the block and then restores
+# the value of ARGV. The previously saved value of
+# ARGV can be restored by passing +:restore+. The
+# former is useful in a single spec. The latter is
+# useful in before/after actions. For example:
+#
+# describe "This" do
+# before do
+# argv ['a', 'b']
+# end
+#
+# after do
+# argv :restore
+# end
+#
+# it "does something" do
+# # do something
+# end
+# end
+#
+# describe "That" do
+# it "does something" do
+# argv ['a', 'b'] do
+# # do something
+# end
+# end
+# end
+def argv(args)
+ if args == :restore
+ ARGV.replace(@__mspec_saved_argv__ || [])
+ else
+ @__mspec_saved_argv__ = ARGV.dup
+ ARGV.replace args
+ if block_given?
+ begin
+ yield
+ ensure
+ argv :restore
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/datetime.rb b/spec/mspec/lib/mspec/helpers/datetime.rb
new file mode 100644
index 0000000000..84ac86b686
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/datetime.rb
@@ -0,0 +1,48 @@
+# The new_datetime helper makes writing DateTime specs more simple by
+# providing default constructor values and accepting a Hash of only the
+# constructor values needed for the particular spec. For example:
+#
+# new_datetime :hour => 1, :minute => 20
+#
+# Possible keys are:
+# :year, :month, :day, :hour, :minute, :second, :offset and :sg.
+def new_datetime(opts = {})
+ require 'date'
+
+ value = {
+ :year => -4712,
+ :month => 1,
+ :day => 1,
+ :hour => 0,
+ :minute => 0,
+ :second => 0,
+ :offset => 0,
+ :sg => Date::ITALY
+ }.merge opts
+
+ DateTime.new value[:year], value[:month], value[:day], value[:hour],
+ value[:minute], value[:second], value[:offset], value[:sg]
+end
+
+def with_timezone(name, offset = nil, daylight_saving_zone = "")
+ skip "WASI doesn't have TZ concept" if PlatformGuard.wasi?
+ zone = name.dup
+
+ if offset
+ # TZ convention is backwards
+ offset = -offset
+
+ zone += offset.to_s
+ zone += ":00:00"
+ end
+ zone += daylight_saving_zone
+
+ old = ENV["TZ"]
+ ENV["TZ"] = zone
+
+ begin
+ yield
+ ensure
+ ENV["TZ"] = old
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/fixture.rb b/spec/mspec/lib/mspec/helpers/fixture.rb
new file mode 100644
index 0000000000..f3bbe423bd
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/fixture.rb
@@ -0,0 +1,24 @@
+# Returns the name of a fixture file by adjoining the directory
+# of the +file+ argument with "fixtures" and the contents of the
+# +args+ array. For example,
+#
+# +file+ == "some/example_spec.rb"
+#
+# and
+#
+# +args+ == ["subdir", "file.txt"]
+#
+# then the result is the expanded path of
+#
+# "some/fixtures/subdir/file.txt".
+def fixture(file, *args)
+ path = File.dirname(file)
+ path = path[0..-7] if path[-7..-1] == "/shared"
+ fixtures = path[-9..-1] == "/fixtures" ? "" : "fixtures"
+ if File.respond_to?(:realpath)
+ path = File.realpath(path)
+ else
+ path = File.expand_path(path)
+ end
+ File.join(path, fixtures, args)
+end
diff --git a/spec/mspec/lib/mspec/helpers/flunk.rb b/spec/mspec/lib/mspec/helpers/flunk.rb
new file mode 100644
index 0000000000..84fb3ab39c
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/flunk.rb
@@ -0,0 +1,3 @@
+def flunk(msg = "This example is a failure")
+ SpecExpectation.fail_with "Failed:", msg
+end
diff --git a/spec/mspec/lib/mspec/helpers/fs.rb b/spec/mspec/lib/mspec/helpers/fs.rb
new file mode 100644
index 0000000000..67453eb302
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/fs.rb
@@ -0,0 +1,64 @@
+# Copies a file
+def cp(source, dest)
+ IO.copy_stream source, dest
+end
+
+# Creates each directory in path that does not exist.
+def mkdir_p(path)
+ parts = File.expand_path(path).split %r[/|\\]
+ name = parts.shift
+ parts.each do |part|
+ name = File.join name, part
+
+ if File.file? name
+ raise ArgumentError, "path component of #{path} is a file"
+ end
+
+ unless File.directory? name
+ begin
+ Dir.mkdir name
+ rescue Errno::EEXIST => e
+ if File.directory? name
+ # OK, another process/thread created the same directory
+ else
+ raise e
+ end
+ end
+ end
+ end
+end
+
+# Recursively removes all files and directories in +path+
+# if +path+ is a directory. Removes the file if +path+ is
+# a file.
+def rm_r(*paths)
+ paths.each do |path|
+ path = File.expand_path path
+
+ prefix = SPEC_TEMP_DIR
+ unless path[0, prefix.size] == prefix
+ raise ArgumentError, "#{path} is not prefixed by #{prefix}"
+ end
+
+ # File.symlink? needs to be checked first as
+ # File.exist? returns false for dangling symlinks
+ if File.symlink? path
+ File.unlink path
+ elsif File.directory? path
+ Dir.entries(path).each { |x| rm_r "#{path}/#{x}" unless x =~ /^\.\.?$/ }
+ Dir.rmdir path
+ elsif File.exist? path
+ File.delete path
+ end
+ end
+end
+
+# Creates a file +name+. Creates the directory for +name+
+# if it does not exist.
+def touch(name, mode = "w")
+ mkdir_p File.dirname(name)
+
+ File.open(name, mode) do |f|
+ yield f if block_given?
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/io.rb b/spec/mspec/lib/mspec/helpers/io.rb
new file mode 100644
index 0000000000..2ad14f47a1
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/io.rb
@@ -0,0 +1,87 @@
+require 'mspec/guards/feature'
+
+class IOStub
+ def initialize
+ @buffer = []
+ @output = ''
+ end
+
+ def write(*str)
+ self << str.join('')
+ end
+
+ def << str
+ @buffer << str
+ self
+ end
+
+ def print(*str)
+ write(str.join('') + $\.to_s)
+ end
+
+ def method_missing(name, *args, &block)
+ to_s.send(name, *args, &block)
+ end
+
+ def == other
+ to_s == other
+ end
+
+ def =~ other
+ to_s =~ other
+ end
+
+ def puts(*str)
+ if str.empty?
+ write "\n"
+ else
+ write(str.collect { |s| s.to_s.chomp }.concat([nil]).join("\n"))
+ end
+ end
+
+ def printf(format, *args)
+ self << sprintf(format, *args)
+ end
+
+ def flush
+ @output += @buffer.join('')
+ @buffer.clear
+ self
+ end
+
+ def to_s
+ flush
+ @output
+ end
+
+ alias_method :to_str, :to_s
+
+ def inspect
+ to_s.inspect
+ end
+end
+
+# Creates a "bare" file descriptor (i.e. one that is not associated
+# with any Ruby object). The file descriptor can safely be passed
+# to IO.new without creating a Ruby object alias to the fd.
+def new_fd(name, mode = "w:utf-8")
+ if mode.kind_of? Hash
+ if mode.key? :mode
+ mode = mode[:mode]
+ else
+ raise ArgumentError, "new_fd options Hash must include :mode"
+ end
+ end
+
+ IO.sysopen name, mode
+end
+
+# Creates an IO instance for a temporary file name. The file
+# must be deleted.
+def new_io(name, mode = "w:utf-8")
+ if Hash === mode # Avoid kwargs warnings on Ruby 2.7+
+ File.new(name, **mode)
+ else
+ File.new(name, mode)
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/mock_to_path.rb b/spec/mspec/lib/mspec/helpers/mock_to_path.rb
new file mode 100644
index 0000000000..2780afc54a
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/mock_to_path.rb
@@ -0,0 +1,6 @@
+def mock_to_path(path)
+ # Cannot use our Object#mock here since it conflicts with RSpec
+ obj = MockObject.new('path')
+ obj.should_receive(:to_path).and_return(path)
+ obj
+end
diff --git a/spec/mspec/lib/mspec/helpers/numeric.rb b/spec/mspec/lib/mspec/helpers/numeric.rb
new file mode 100644
index 0000000000..0b47855cd2
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/numeric.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+require 'mspec/guards/platform'
+
+def nan_value
+ 0/0.0
+end
+
+def infinity_value
+ 1/0.0
+end
+
+def bignum_value(plus = 0)
+ # Must be >= fixnum_max + 2, so -bignum_value is < fixnum_min
+ # A fixed value has the advantage to be the same numeric value for all Rubies and is much easier to spec
+ (2**64) + plus
+end
+
+def max_long
+ 2**(PlatformGuard::C_LONG_SIZE - 1) - 1
+end
+
+def min_long
+ -(2**(PlatformGuard::C_LONG_SIZE - 1))
+end
+
+# This is a bit hairy, but we need to be able to write specs that cover the
+# boundary between Fixnum and Bignum for operations like Fixnum#<<. Since
+# this boundary is implementation-dependent, we use these helpers to write
+# specs based on the relationship between values rather than specific
+# values.
+if PlatformGuard.standard? or PlatformGuard.implementation? :topaz
+ limits_available = begin
+ require 'rbconfig/sizeof'
+ defined?(RbConfig::LIMITS.[]) && ['FIXNUM_MAX', 'FIXNUM_MIN'].all? do |key|
+ Integer === RbConfig::LIMITS[key]
+ end
+ rescue LoadError
+ false
+ end
+
+ if limits_available
+ def fixnum_max
+ RbConfig::LIMITS['FIXNUM_MAX']
+ end
+
+ def fixnum_min
+ RbConfig::LIMITS['FIXNUM_MIN']
+ end
+ elsif PlatformGuard.c_long_size? 32
+ def fixnum_max
+ (2**30) - 1
+ end
+
+ def fixnum_min
+ -(2**30)
+ end
+ elsif PlatformGuard.c_long_size? 64
+ def fixnum_max
+ (2**62) - 1
+ end
+
+ def fixnum_min
+ -(2**62)
+ end
+ end
+elsif PlatformGuard.implementation? :opal
+ def fixnum_max
+ Integer::MAX
+ end
+
+ def fixnum_min
+ Integer::MIN
+ end
+elsif PlatformGuard.implementation? :rubinius
+ def fixnum_max
+ Fixnum::MAX
+ end
+
+ def fixnum_min
+ Fixnum::MIN
+ end
+elsif PlatformGuard.implementation?(:jruby) || PlatformGuard.implementation?(:truffleruby)
+ def fixnum_max
+ 9223372036854775807
+ end
+
+ def fixnum_min
+ -9223372036854775808
+ end
+else
+ def fixnum_max
+ raise "unknown implementation for fixnum_max() helper"
+ end
+
+ def fixnum_min
+ raise "unknown implementation for fixnum_min() helper"
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/ruby_exe.rb b/spec/mspec/lib/mspec/helpers/ruby_exe.rb
new file mode 100644
index 0000000000..2e499d6f9a
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/ruby_exe.rb
@@ -0,0 +1,205 @@
+require 'mspec/guards/platform'
+require 'mspec/helpers/tmp'
+
+# The ruby_exe helper provides a wrapper for invoking the
+# same Ruby interpreter with the same flags as the one running
+# the specs and getting the output from running the code.
+#
+# If +code+ is a file that exists, it will be run.
+# Otherwise, +code+ will be written to a temporary file and be run.
+# For example:
+#
+# ruby_exe('path/to/some/file.rb')
+#
+# will be executed as
+#
+# `#{RUBY_EXE} 'path/to/some/file.rb'`
+#
+# The ruby_exe helper also accepts an options hash with four
+# keys: :options, :args :env and :exception.
+#
+# For example:
+#
+# ruby_exe('file.rb', :options => "-w",
+# :args => "arg1 arg2",
+# :env => { :FOO => "bar" })
+#
+# will be executed as
+#
+# `#{RUBY_EXE} -w file.rb arg1 arg2`
+#
+# with access to ENV["FOO"] with value "bar".
+#
+# When `exception: false` and Ruby command fails then exception will not be
+# raised.
+#
+# If +nil+ is passed for the first argument, the command line
+# will be built only from the options hash.
+#
+# If no arguments are passed to ruby_exe, it returns an Array
+# containing the interpreter executable and the flags:
+#
+# spawn(*ruby_exe, "-e", "puts :hello")
+#
+# This avoids spawning an extra shell, and ensure the pid returned by spawn
+# corresponds to the ruby process and not the shell.
+#
+# The RUBY_EXE constant is setup by mspec automatically
+# and is used by ruby_exe and ruby_cmd. The mspec runner script
+# will set ENV['RUBY_EXE'] to the name of the executable used
+# to invoke the mspec-run script.
+#
+# The value will only be used if the file exists and is executable.
+# The flags will then be appended to the resulting value, such that
+# the RUBY_EXE constant contains both the executable and the flags.
+#
+# Additionally, the flags passed to mspec
+# (with -T on the command line or in the config with set :flags)
+# will be appended to RUBY_EXE so that the interpreter
+# is always called with those flags.
+#
+# Failure of a Ruby command leads to raising exception by default.
+
+def ruby_exe_options(option)
+ case option
+ when :env
+ ENV['RUBY_EXE']
+ when :engine
+ case RUBY_ENGINE
+ when 'rbx'
+ "bin/rbx"
+ when 'jruby'
+ "bin/jruby"
+ when 'maglev'
+ "maglev-ruby"
+ when 'topaz'
+ "topaz"
+ when 'ironruby'
+ "ir"
+ end
+ when :name
+ require 'rbconfig'
+ bin = RUBY_ENGINE + (RbConfig::CONFIG['EXEEXT'] || '')
+ File.join(".", bin)
+ when :install_name
+ require 'rbconfig'
+ bin = RbConfig::CONFIG["RUBY_INSTALL_NAME"] || RbConfig::CONFIG["ruby_install_name"]
+ bin << (RbConfig::CONFIG['EXEEXT'] || '')
+ File.join(RbConfig::CONFIG['bindir'], bin)
+ end
+end
+
+def resolve_ruby_exe
+ [:env, :engine, :name, :install_name].each do |option|
+ next unless exe = ruby_exe_options(option)
+
+ if File.file?(exe) and File.executable?(exe)
+ exe = File.expand_path(exe)
+ exe = exe.tr('/', '\\') if PlatformGuard.windows?
+ flags = ENV['RUBY_FLAGS']
+ if flags and !flags.empty?
+ return exe + ' ' + flags
+ else
+ return exe
+ end
+ end
+ end
+ raise Exception, "Unable to find a suitable ruby executable."
+end
+
+unless Object.const_defined?(:RUBY_EXE) and RUBY_EXE
+ RUBY_EXE = resolve_ruby_exe
+end
+
+def ruby_exe(code = :not_given, opts = {})
+ skip "WASI doesn't provide subprocess" if PlatformGuard.wasi?
+
+ if opts[:dir]
+ raise "ruby_exe(..., dir: dir) is no longer supported, use Dir.chdir"
+ end
+
+ if code == :not_given
+ return RUBY_EXE.split(' ')
+ end
+
+ env = opts[:env] || {}
+ saved_env = {}
+ env.each do |key, value|
+ key = key.to_s
+ saved_env[key] = ENV[key] if ENV.key? key
+ ENV[key] = value
+ end
+
+ escape = opts.delete(:escape)
+ if code and !File.exist?(code) and escape != false
+ tmpfile = tmp("rubyexe.rb")
+ File.open(tmpfile, "w") { |f| f.write(code) }
+ code = tmpfile
+ end
+
+ expected_status = opts.fetch(:exit_status, 0)
+
+ begin
+ command = ruby_cmd(code, opts)
+
+ # Try to avoid the extra shell for 2>&1
+ # This is notably useful for TimeoutAction which can then signal the ruby subprocess and not the shell
+ popen_options = []
+ if command.end_with?(' 2>&1')
+ command = command[0...-5]
+ popen_options = [{ err: [:child, :out] }]
+ end
+
+ output = IO.popen(command, *popen_options) do |io|
+ pid = io.pid
+ MSpec.subprocesses << pid
+ begin
+ io.read
+ ensure
+ MSpec.subprocesses.delete(pid)
+ end
+ end
+
+ status = Process.last_status
+
+ exit_status = if status.exited?
+ status.exitstatus
+ elsif status.signaled?
+ signame = Signal.signame status.termsig
+ raise "No signal name?" unless signame
+ :"SIG#{signame}"
+ else
+ raise SpecExpectationNotMetError, "#{exit_status.inspect} is neither exited? nor signaled?"
+ end
+ if exit_status != expected_status
+ formatted_output = output.lines.map { |line| " #{line}" }.join
+ raise SpecExpectationNotMetError,
+ "Expected exit status is #{expected_status.inspect} but actual is #{exit_status.inspect} for command ruby_exe(#{command.inspect})\nOutput:\n#{formatted_output}"
+ end
+
+ output
+ ensure
+ saved_env.each { |key, value| ENV[key] = value }
+ env.keys.each do |key|
+ key = key.to_s
+ ENV.delete key unless saved_env.key? key
+ end
+ File.delete tmpfile if tmpfile
+ end
+end
+
+def ruby_cmd(code, opts = {})
+ body = code
+
+ if opts[:escape]
+ raise "escape: true is no longer supported in ruby_cmd, use ruby_exe or a fixture"
+ end
+
+ if code and !File.exist?(code)
+ body = "-e #{code.inspect}"
+ end
+
+ command = [RUBY_EXE, opts[:options], body, opts[:args]].compact.join(' ')
+ STDERR.puts "\nruby_cmd: #{command}" if ENV["DEBUG_MSPEC_RUBY_CMD"] == "true"
+ command
+end
diff --git a/spec/mspec/lib/mspec/helpers/scratch.rb b/spec/mspec/lib/mspec/helpers/scratch.rb
new file mode 100644
index 0000000000..0da3315cd8
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/scratch.rb
@@ -0,0 +1,21 @@
+module ScratchPad
+ def self.clear
+ @record = nil
+ end
+
+ def self.record(arg)
+ @record = arg
+ end
+
+ def self.<<(arg)
+ @record << arg
+ end
+
+ def self.recorded
+ @record
+ end
+
+ def self.inspect
+ "<ScratchPad @record=#{@record.inspect}>"
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/tmp.rb b/spec/mspec/lib/mspec/helpers/tmp.rb
new file mode 100644
index 0000000000..e903dd9f50
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/tmp.rb
@@ -0,0 +1,62 @@
+# Creates a temporary directory in the current working directory
+# for temporary files created while running the specs. All specs
+# should clean up any temporary files created so that the temp
+# directory is empty when the process exits.
+
+SPEC_TEMP_DIR_PID = Process.pid
+
+if spec_temp_dir = ENV["SPEC_TEMP_DIR"]
+ spec_temp_dir = File.realdirpath(spec_temp_dir)
+else
+ spec_temp_dir = "#{File.realpath(Dir.pwd)}/rubyspec_temp/#{SPEC_TEMP_DIR_PID}"
+end
+SPEC_TEMP_DIR = spec_temp_dir
+
+SPEC_TEMP_UNIQUIFIER = +"0"
+
+at_exit do
+ begin
+ if SPEC_TEMP_DIR_PID == Process.pid
+ Dir.delete SPEC_TEMP_DIR if File.directory? SPEC_TEMP_DIR
+ end
+ rescue SystemCallError
+ STDERR.puts <<-EOM
+
+-----------------------------------------------------
+The rubyspec temp directory is not empty. Ensure that
+all specs are cleaning up temporary files:
+ #{SPEC_TEMP_DIR}
+-----------------------------------------------------
+
+ EOM
+ rescue Object => e
+ STDERR.puts "failed to remove spec temp directory"
+ STDERR.puts e.message
+ end
+end
+
+def tmp(name, uniquify = true)
+ if Dir.exist? SPEC_TEMP_DIR
+ stat = File.stat(SPEC_TEMP_DIR)
+ if stat.world_writable? and !stat.sticky?
+ raise ArgumentError, "SPEC_TEMP_DIR (#{SPEC_TEMP_DIR}) is world writable but not sticky"
+ end
+ else
+ platform_is_not :windows do
+ umask = File.umask
+ if (umask & 0002) == 0 # o+w
+ raise ArgumentError, "File.umask #=> #{umask.to_s(8)} (world-writable)"
+ end
+ end
+ mkdir_p SPEC_TEMP_DIR
+ end
+
+ if uniquify and !name.empty?
+ slash = name.rindex "/"
+ index = slash ? slash + 1 : 0
+ name = +name
+ name.insert index, "#{SPEC_TEMP_UNIQUIFIER.succ!}-"
+ end
+
+ File.join SPEC_TEMP_DIR, name
+end
diff --git a/spec/mspec/lib/mspec/helpers/warning.rb b/spec/mspec/lib/mspec/helpers/warning.rb
new file mode 100644
index 0000000000..e3d72b78bd
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/warning.rb
@@ -0,0 +1,21 @@
+require 'mspec/guards/version'
+
+# You might be looking for #silence_warnings, use #suppress_warning instead.
+# MSpec calls it #suppress_warning for consistency with EnvUtil.suppress_warning in CRuby test/.
+def suppress_warning
+ verbose = $VERBOSE
+ $VERBOSE = nil
+ yield
+ensure
+ $VERBOSE = verbose
+end
+
+if ruby_version_is("2.7")
+ def suppress_keyword_warning(&block)
+ suppress_warning(&block)
+ end
+else
+ def suppress_keyword_warning
+ yield
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers.rb b/spec/mspec/lib/mspec/matchers.rb
new file mode 100644
index 0000000000..356e4a9f32
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers.rb
@@ -0,0 +1,37 @@
+require 'mspec/matchers/base'
+require 'mspec/matchers/be_an_instance_of'
+require 'mspec/matchers/be_ancestor_of'
+require 'mspec/matchers/be_close'
+require 'mspec/matchers/be_computed_by'
+require 'mspec/matchers/be_empty'
+require 'mspec/matchers/be_false'
+require 'mspec/matchers/be_kind_of'
+require 'mspec/matchers/be_nan'
+require 'mspec/matchers/be_nil'
+require 'mspec/matchers/be_true'
+require 'mspec/matchers/be_true_or_false'
+require 'mspec/matchers/complain'
+require 'mspec/matchers/eql'
+require 'mspec/matchers/equal'
+require 'mspec/matchers/equal_element'
+require 'mspec/matchers/have_constant'
+require 'mspec/matchers/have_class_variable'
+require 'mspec/matchers/have_instance_method'
+require 'mspec/matchers/have_instance_variable'
+require 'mspec/matchers/have_method'
+require 'mspec/matchers/have_private_instance_method'
+require 'mspec/matchers/have_private_method'
+require 'mspec/matchers/have_protected_instance_method'
+require 'mspec/matchers/have_public_instance_method'
+require 'mspec/matchers/have_singleton_method'
+require 'mspec/matchers/include'
+require 'mspec/matchers/include_any_of'
+require 'mspec/matchers/infinity'
+require 'mspec/matchers/match_yaml'
+require 'mspec/matchers/raise_error'
+require 'mspec/matchers/output'
+require 'mspec/matchers/output_to_fd'
+require 'mspec/matchers/respond_to'
+require 'mspec/matchers/signed_zero'
+require 'mspec/matchers/block_caller'
+require 'mspec/matchers/skip'
diff --git a/spec/mspec/lib/mspec/matchers/base.rb b/spec/mspec/lib/mspec/matchers/base.rb
new file mode 100644
index 0000000000..3534520d88
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/base.rb
@@ -0,0 +1,95 @@
+module MSpecMatchers
+end
+
+class MSpecEnv
+ include MSpecMatchers
+end
+
+# Expectations are sometimes used in a module body
+class Module
+ include MSpecMatchers
+end
+
+class SpecPositiveOperatorMatcher < BasicObject
+ def initialize(actual)
+ @actual = actual
+ end
+
+ def ==(expected)
+ result = @actual == expected
+ unless result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be truthy")
+ end
+ end
+
+ def !=(expected)
+ result = @actual != expected
+ unless result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be truthy")
+ end
+ end
+
+ def equal?(expected)
+ result = @actual.equal?(expected)
+ unless result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be truthy")
+ end
+ end
+
+ def raise(exception = ::Exception, message = nil, options = nil, &block)
+ matcher = ::RaiseErrorMatcher.new(exception, message, options, &block)
+ unless matcher.matches? @actual
+ expected, actual = matcher.failure_message
+ ::SpecExpectation.fail_with(expected, actual)
+ end
+ end
+
+ def method_missing(name, *args, &block)
+ result = @actual.__send__(name, *args, &block)
+ unless result
+ ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be truthy")
+ end
+ end
+end
+
+class SpecNegativeOperatorMatcher < BasicObject
+ def initialize(actual)
+ @actual = actual
+ end
+
+ def ==(expected)
+ result = @actual == expected
+ if result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be falsy")
+ end
+ end
+
+ def !=(expected)
+ result = @actual != expected
+ if result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be falsy")
+ end
+ end
+
+ def equal?(expected)
+ result = @actual.equal?(expected)
+ if result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be falsy")
+ end
+ end
+
+ def raise(exception = ::Exception, message = nil, options = nil, &block)
+ matcher = ::RaiseErrorMatcher.new(exception, message, options, &block)
+ if matcher.matches? @actual
+ expected, actual = matcher.negative_failure_message
+ ::SpecExpectation.fail_with(expected, actual)
+ end
+ end
+
+ def method_missing(name, *args, &block)
+ result = @actual.__send__(name, *args, &block)
+ if result
+ ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be falsy")
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb
new file mode 100644
index 0000000000..fdf3736ac2
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb
@@ -0,0 +1,26 @@
+class BeAnInstanceOfMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.instance_of?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})",
+ "to be an instance of #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})",
+ "not to be an instance of #{@expected}"]
+ end
+end
+
+module MSpecMatchers
+ private def be_an_instance_of(expected)
+ BeAnInstanceOfMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb
new file mode 100644
index 0000000000..05f72099e4
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb
@@ -0,0 +1,24 @@
+class BeAncestorOfMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @expected.ancestors.include? @actual
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be an ancestor of #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be an ancestor of #{@expected}"]
+ end
+end
+
+module MSpecMatchers
+ private def be_ancestor_of(expected)
+ BeAncestorOfMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_close.rb b/spec/mspec/lib/mspec/matchers/be_close.rb
new file mode 100644
index 0000000000..d6a6626f31
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_close.rb
@@ -0,0 +1,29 @@
+TOLERANCE = 0.00003 unless Object.const_defined?(:TOLERANCE)
+# To account for GC, context switches, other processes, load, etc.
+TIME_TOLERANCE = 20.0 unless Object.const_defined?(:TIME_TOLERANCE)
+
+class BeCloseMatcher
+ def initialize(expected, tolerance)
+ @expected = expected
+ @tolerance = tolerance
+ end
+
+ def matches?(actual)
+ @actual = actual
+ (@actual - @expected).abs <= @tolerance
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be within #{@expected} +/- #{@tolerance}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be within #{@expected} +/- #{@tolerance}"]
+ end
+end
+
+module MSpecMatchers
+ private def be_close(expected, tolerance)
+ BeCloseMatcher.new(expected, tolerance)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_computed_by.rb b/spec/mspec/lib/mspec/matchers/be_computed_by.rb
new file mode 100644
index 0000000000..2e31bc93af
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_computed_by.rb
@@ -0,0 +1,37 @@
+class BeComputedByMatcher
+ def initialize(sym, *args)
+ @method = sym
+ @args = args
+ end
+
+ def matches?(array)
+ array.each do |line|
+ @receiver = line.shift
+ @value = line.pop
+ @arguments = line
+ @arguments += @args
+ @actual = @receiver.send(@method, *@arguments)
+ return false unless @actual == @value
+ end
+
+ return true
+ end
+
+ def method_call
+ method_call = "#{@receiver.inspect}.#{@method}"
+ unless @arguments.empty?
+ method_call = "#{method_call} from #{@arguments.map { |x| x.inspect }.join(", ")}"
+ end
+ method_call
+ end
+
+ def failure_message
+ ["Expected #{@value.inspect}", "to be computed by #{method_call} (computed #{@actual.inspect} instead)"]
+ end
+end
+
+module MSpecMatchers
+ private def be_computed_by(sym, *args)
+ BeComputedByMatcher.new(sym, *args)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_empty.rb b/spec/mspec/lib/mspec/matchers/be_empty.rb
new file mode 100644
index 0000000000..5abd5c9485
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_empty.rb
@@ -0,0 +1,20 @@
+class BeEmptyMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual.empty?
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be empty"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be empty"]
+ end
+end
+
+module MSpecMatchers
+ private def be_empty
+ BeEmptyMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_false.rb b/spec/mspec/lib/mspec/matchers/be_false.rb
new file mode 100644
index 0000000000..9e9a2608e1
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_false.rb
@@ -0,0 +1,20 @@
+class BeFalseMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual == false
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be false"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be false"]
+ end
+end
+
+module MSpecMatchers
+ private def be_false
+ BeFalseMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_kind_of.rb b/spec/mspec/lib/mspec/matchers/be_kind_of.rb
new file mode 100644
index 0000000000..a69906f210
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_kind_of.rb
@@ -0,0 +1,24 @@
+class BeKindOfMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.is_a?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "to be kind of #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "not to be kind of #{@expected}"]
+ end
+end
+
+module MSpecMatchers
+ private def be_kind_of(expected)
+ BeKindOfMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_nan.rb b/spec/mspec/lib/mspec/matchers/be_nan.rb
new file mode 100644
index 0000000000..b279d8f1cf
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_nan.rb
@@ -0,0 +1,20 @@
+class BeNaNMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual.kind_of?(Float) && @actual.nan?
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be NaN"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be NaN"]
+ end
+end
+
+module MSpecMatchers
+ private def be_nan
+ BeNaNMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_nil.rb b/spec/mspec/lib/mspec/matchers/be_nil.rb
new file mode 100644
index 0000000000..049b1e3a53
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_nil.rb
@@ -0,0 +1,20 @@
+class BeNilMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual.nil?
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be nil"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be nil"]
+ end
+end
+
+module MSpecMatchers
+ private def be_nil
+ BeNilMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_true.rb b/spec/mspec/lib/mspec/matchers/be_true.rb
new file mode 100644
index 0000000000..52f5013752
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_true.rb
@@ -0,0 +1,20 @@
+class BeTrueMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual == true
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be true"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be true"]
+ end
+end
+
+module MSpecMatchers
+ private def be_true
+ BeTrueMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_true_or_false.rb b/spec/mspec/lib/mspec/matchers/be_true_or_false.rb
new file mode 100644
index 0000000000..4294b08d1b
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_true_or_false.rb
@@ -0,0 +1,20 @@
+class BeTrueOrFalseMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual == true || @actual == false
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be true or false"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be true or false"]
+ end
+end
+
+module MSpecMatchers
+ private def be_true_or_false
+ BeTrueOrFalseMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/block_caller.rb b/spec/mspec/lib/mspec/matchers/block_caller.rb
new file mode 100644
index 0000000000..30fab4fc68
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/block_caller.rb
@@ -0,0 +1,37 @@
+class BlockingMatcher
+ def matches?(block)
+ t = Thread.new do
+ block.call
+ end
+
+ loop do
+ case t.status
+ when "sleep" # blocked
+ t.kill
+ t.join
+ return true
+ when false # terminated normally, so never blocked
+ t.join
+ return false
+ when nil # terminated exceptionally
+ t.value
+ else
+ Thread.pass
+ end
+ end
+ end
+
+ def failure_message
+ ['Expected the given Proc', 'to block the caller']
+ end
+
+ def negative_failure_message
+ ['Expected the given Proc', 'to not block the caller']
+ end
+end
+
+module MSpecMatchers
+ private def block_caller
+ BlockingMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/complain.rb b/spec/mspec/lib/mspec/matchers/complain.rb
new file mode 100644
index 0000000000..19310c0bbb
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/complain.rb
@@ -0,0 +1,69 @@
+require 'mspec/helpers/io'
+
+class ComplainMatcher
+ def initialize(complaint = nil, options = nil)
+ # the proper solution is to use double splat operator e.g.
+ # def initialize(complaint = nil, **options)
+ # but we are trying to minimize language features required to run MSpec
+ if complaint.is_a?(Hash)
+ @complaint = nil
+ @options = complaint
+ else
+ @complaint = complaint
+ @options = options || {}
+ end
+ end
+
+ def matches?(proc)
+ @saved_err = $stderr
+ @verbose = $VERBOSE
+ err = IOStub.new
+
+ $stderr = err
+ $VERBOSE = @options.key?(:verbose) ? @options[:verbose] : false
+ begin
+ proc.call
+ ensure
+ $VERBOSE = @verbose
+ $stderr = @saved_err
+ end
+
+ @warning = err.to_s
+ unless @complaint.nil?
+ case @complaint
+ when Regexp
+ return false unless @warning =~ @complaint
+ else
+ return false unless @warning == @complaint
+ end
+ end
+
+ return @warning.empty? ? false : true
+ end
+
+ def failure_message
+ if @complaint.nil?
+ ["Expected a warning", "but received none"]
+ elsif @complaint.kind_of? Regexp
+ ["Expected warning to match: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"]
+ else
+ ["Expected warning: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"]
+ end
+ end
+
+ def negative_failure_message
+ if @complaint.nil?
+ ["Unexpected warning: ", @warning.chomp.inspect]
+ elsif @complaint.kind_of? Regexp
+ ["Expected warning not to match: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"]
+ else
+ ["Expected warning: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"]
+ end
+ end
+end
+
+module MSpecMatchers
+ private def complain(complaint = nil, options = nil)
+ ComplainMatcher.new(complaint, options)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/eql.rb b/spec/mspec/lib/mspec/matchers/eql.rb
new file mode 100644
index 0000000000..bcab88ebee
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/eql.rb
@@ -0,0 +1,26 @@
+class EqlMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.eql?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "to have same value and type as #{MSpec.format(@expected)}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "not to have same value or type as #{MSpec.format(@expected)}"]
+ end
+end
+
+module MSpecMatchers
+ private def eql(expected)
+ EqlMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/equal.rb b/spec/mspec/lib/mspec/matchers/equal.rb
new file mode 100644
index 0000000000..5ba4856d82
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/equal.rb
@@ -0,0 +1,26 @@
+class EqualMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.equal?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "to be identical to #{MSpec.format(@expected)}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "not to be identical to #{MSpec.format(@expected)}"]
+ end
+end
+
+module MSpecMatchers
+ private def equal(expected)
+ EqualMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/equal_element.rb b/spec/mspec/lib/mspec/matchers/equal_element.rb
new file mode 100644
index 0000000000..8da2567fcf
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/equal_element.rb
@@ -0,0 +1,78 @@
+class EqualElementMatcher
+ def initialize(element, attributes = nil, content = nil, options = {})
+ @element = element
+ @attributes = attributes
+ @content = content
+ @options = options
+ end
+
+ def matches?(actual)
+ @actual = actual
+
+ matched = true
+
+ if @options[:not_closed]
+ matched &&= actual =~ /^#{Regexp.quote("<" + @element)}.*#{Regexp.quote(">" + (@content || ''))}$/
+ else
+ matched &&= actual =~ /^#{Regexp.quote("<" + @element)}/
+ matched &&= actual =~ /#{Regexp.quote("</" + @element + ">")}$/
+ matched &&= actual =~ /#{Regexp.quote(">" + @content + "</")}/ if @content
+ end
+
+ if @attributes
+ if @attributes.empty?
+ matched &&= actual.scan(/\w+\=\"(.*)\"/).size == 0
+ else
+ @attributes.each do |key, value|
+ if value == true
+ matched &&= (actual.scan(/#{Regexp.quote(key)}(\s|>)/).size == 1)
+ else
+ matched &&= (actual.scan(%Q{ #{key}="#{value}"}).size == 1)
+ end
+ end
+ end
+ end
+
+ !!matched
+ end
+
+ def failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "not to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
+ end
+
+ def attributes_for_failure_message
+ if @attributes
+ if @attributes.empty?
+ "no attributes"
+ else
+ @attributes.inject([]) { |memo, n| memo << %Q{#{n[0]}="#{n[1]}"} }.join(" ")
+ end
+ else
+ "any attributes"
+ end
+ end
+
+ def content_for_failure_message
+ if @content
+ if @content.empty?
+ "no content"
+ else
+ "#{@content.inspect} as content"
+ end
+ else
+ "any content"
+ end
+ end
+end
+
+module MSpecMatchers
+ private def equal_element(*args)
+ EqualElementMatcher.new(*args)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_class_variable.rb b/spec/mspec/lib/mspec/matchers/have_class_variable.rb
new file mode 100644
index 0000000000..dd43ced621
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_class_variable.rb
@@ -0,0 +1,12 @@
+require 'mspec/matchers/variable'
+
+class HaveClassVariableMatcher < VariableMatcher
+ self.variables_method = :class_variables
+ self.description = 'class variable'
+end
+
+module MSpecMatchers
+ private def have_class_variable(variable)
+ HaveClassVariableMatcher.new(variable)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_constant.rb b/spec/mspec/lib/mspec/matchers/have_constant.rb
new file mode 100644
index 0000000000..6ec7c75b85
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_constant.rb
@@ -0,0 +1,12 @@
+require 'mspec/matchers/variable'
+
+class HaveConstantMatcher < VariableMatcher
+ self.variables_method = :constants
+ self.description = 'constant'
+end
+
+module MSpecMatchers
+ private def have_constant(variable)
+ HaveConstantMatcher.new(variable)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_instance_method.rb
new file mode 100644
index 0000000000..9a5a31aa0f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_instance_method(method, include_super = true)
+ HaveInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_instance_variable.rb b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb
new file mode 100644
index 0000000000..de51b3209d
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb
@@ -0,0 +1,12 @@
+require 'mspec/matchers/variable'
+
+class HaveInstanceVariableMatcher < VariableMatcher
+ self.variables_method = :instance_variables
+ self.description = 'instance variable'
+end
+
+module MSpecMatchers
+ private def have_instance_variable(variable)
+ HaveInstanceVariableMatcher.new(variable)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_method.rb b/spec/mspec/lib/mspec/matchers/have_method.rb
new file mode 100644
index 0000000000..e962e69e0a
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ @mod.methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_method(method, include_super = true)
+ HaveMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb
new file mode 100644
index 0000000000..d32db76c6a
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HavePrivateInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.private_instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have private instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have private instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_private_instance_method(method, include_super = true)
+ HavePrivateInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_private_method.rb b/spec/mspec/lib/mspec/matchers/have_private_method.rb
new file mode 100644
index 0000000000..c74165cfc7
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_private_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HavePrivateMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.private_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have private method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have private method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_private_method(method, include_super = true)
+ HavePrivateMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb
new file mode 100644
index 0000000000..1deb2f995d
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveProtectedInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.protected_instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have protected instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have protected instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_protected_instance_method(method, include_super = true)
+ HaveProtectedInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb
new file mode 100644
index 0000000000..0e620532c0
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HavePublicInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.public_instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have public instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have public instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_public_instance_method(method, include_super = true)
+ HavePublicInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_singleton_method.rb b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb
new file mode 100644
index 0000000000..b60dd2536b
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveSingletonMethodMatcher < MethodMatcher
+ def matches?(obj)
+ @obj = obj
+ obj.singleton_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@obj} to have singleton method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@obj} NOT to have singleton method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_singleton_method(method, include_super = true)
+ HaveSingletonMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/include.rb b/spec/mspec/lib/mspec/matchers/include.rb
new file mode 100644
index 0000000000..3f07f35548
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/include.rb
@@ -0,0 +1,31 @@
+class IncludeMatcher
+ def initialize(*expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @expected.each do |e|
+ @element = e
+ unless @actual.include?(e)
+ return false
+ end
+ end
+ return true
+ end
+
+ def failure_message
+ ["Expected #{MSpec.format(@actual)}", "to include #{MSpec.format(@element)}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{MSpec.format(@actual)}", "not to include #{MSpec.format(@element)}"]
+ end
+end
+
+# Cannot override #include at the toplevel in MRI
+module MSpecMatchers
+ private def include(*expected)
+ IncludeMatcher.new(*expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/include_any_of.rb b/spec/mspec/lib/mspec/matchers/include_any_of.rb
new file mode 100644
index 0000000000..ce097ccf0f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/include_any_of.rb
@@ -0,0 +1,29 @@
+class IncludeAnyOfMatcher
+ def initialize(*expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @expected.each do |e|
+ if @actual.include?(e)
+ return true
+ end
+ end
+ return false
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to include any of #{@expected.inspect}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to include any of #{@expected.inspect}"]
+ end
+end
+
+module MSpecMatchers
+ private def include_any_of(*expected)
+ IncludeAnyOfMatcher.new(*expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/infinity.rb b/spec/mspec/lib/mspec/matchers/infinity.rb
new file mode 100644
index 0000000000..8bfa6dbd10
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/infinity.rb
@@ -0,0 +1,28 @@
+class InfinityMatcher
+ def initialize(expected_sign)
+ @expected_sign = expected_sign
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.kind_of?(Float) && @actual.infinite? == @expected_sign
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}Infinity"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}Infinity"]
+ end
+end
+
+module MSpecMatchers
+ private def be_positive_infinity
+ InfinityMatcher.new(1)
+ end
+
+ private def be_negative_infinity
+ InfinityMatcher.new(-1)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/match_yaml.rb b/spec/mspec/lib/mspec/matchers/match_yaml.rb
new file mode 100644
index 0000000000..30561627c3
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/match_yaml.rb
@@ -0,0 +1,50 @@
+class MatchYAMLMatcher
+
+ def initialize(expected)
+ if valid_yaml?(expected)
+ @expected = expected
+ else
+ @expected = expected.to_yaml
+ end
+ end
+
+ def matches?(actual)
+ @actual = actual
+ clean_yaml(@actual) == clean_yaml(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", " to match #{@expected.inspect}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", " to match #{@expected.inspect}"]
+ end
+
+ protected
+
+ def clean_yaml(yaml)
+ yaml.gsub(/([^-]|^---)\s+\n/, "\\1\n").sub(/\n\.\.\.\n$/, "\n")
+ end
+
+ def valid_yaml?(obj)
+ require 'yaml'
+ begin
+ if YAML.respond_to?(:unsafe_load)
+ YAML.unsafe_load(obj)
+ else
+ YAML.load(obj)
+ end
+ rescue
+ false
+ else
+ true
+ end
+ end
+end
+
+module MSpecMatchers
+ private def match_yaml(expected)
+ MatchYAMLMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/method.rb b/spec/mspec/lib/mspec/matchers/method.rb
new file mode 100644
index 0000000000..2b54419faa
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/method.rb
@@ -0,0 +1,10 @@
+class MethodMatcher
+ def initialize(method, include_super = true)
+ @include_super = include_super
+ @method = method.to_sym
+ end
+
+ def matches?(mod)
+ raise Exception, "define #matches? in the subclass"
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/output.rb b/spec/mspec/lib/mspec/matchers/output.rb
new file mode 100644
index 0000000000..5bb5d55027
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/output.rb
@@ -0,0 +1,67 @@
+require 'mspec/helpers/io'
+
+class OutputMatcher
+ def initialize(stdout, stderr)
+ @out = stdout
+ @err = stderr
+ end
+
+ def matches?(proc)
+ @saved_out = $stdout
+ @saved_err = $stderr
+ @stdout = $stdout = IOStub.new
+ @stderr = $stderr = IOStub.new
+
+ proc.call
+
+ unless @out.nil?
+ case @out
+ when Regexp
+ return false unless $stdout =~ @out
+ else
+ return false unless $stdout == @out
+ end
+ end
+
+ unless @err.nil?
+ case @err
+ when Regexp
+ return false unless $stderr =~ @err
+ else
+ return false unless $stderr == @err
+ end
+ end
+
+ return true
+ ensure
+ $stdout = @saved_out
+ $stderr = @saved_err
+ end
+
+ def failure_message
+ expected_out = "\n"
+ actual_out = "\n"
+ unless @out.nil?
+ expected_out += " $stdout: #{MSpec.format(@out)}\n"
+ actual_out += " $stdout: #{MSpec.format(@stdout.to_s)}\n"
+ end
+ unless @err.nil?
+ expected_out += " $stderr: #{MSpec.format(@err)}\n"
+ actual_out += " $stderr: #{MSpec.format(@stderr.to_s)}\n"
+ end
+ ["Expected:#{expected_out}", " got:#{actual_out}"]
+ end
+
+ def negative_failure_message
+ out = ""
+ out += " $stdout: #{@stdout.chomp.dump}\n" unless @out.nil?
+ out += " $stderr: #{@stderr.chomp.dump}\n" unless @err.nil?
+ ["Expected output not to be:\n", out]
+ end
+end
+
+module MSpecMatchers
+ private def output(stdout = nil, stderr = nil)
+ OutputMatcher.new(stdout, stderr)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/output_to_fd.rb b/spec/mspec/lib/mspec/matchers/output_to_fd.rb
new file mode 100644
index 0000000000..f4d7b4ea1f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/output_to_fd.rb
@@ -0,0 +1,71 @@
+require 'mspec/helpers/tmp'
+
+# Lower-level output speccing mechanism for a single
+# output stream. Unlike OutputMatcher which provides
+# methods to capture the output, we actually replace
+# the FD itself so that there is no reliance on a
+# certain method being used.
+class OutputToFDMatcher
+ def initialize(expected, to)
+ @to, @expected = to, expected
+
+ case @to
+ when STDOUT
+ @to_name = "STDOUT"
+ when STDERR
+ @to_name = "STDERR"
+ when IO
+ @to_name = @to.object_id.to_s
+ else
+ raise ArgumentError, "#{@to.inspect} is not a supported output target"
+ end
+ end
+
+ def with_tmp
+ path = tmp("mspec_output_to_#{$$}_#{Time.now.to_i}")
+ File.open(path, 'w+') { |io|
+ yield(io)
+ }
+ ensure
+ File.delete path if path
+ end
+
+ def matches?(block)
+ old_to = @to.dup
+ with_tmp do |out|
+ # Replacing with a file handle so that Readline etc. work
+ @to.reopen out
+ begin
+ block.call
+ ensure
+ @to.reopen old_to
+ old_to.close
+ end
+
+ out.rewind
+ @actual = out.read
+
+ case @expected
+ when Regexp
+ !(@actual =~ @expected).nil?
+ else
+ @actual == @expected
+ end
+ end
+ end
+
+ def failure_message()
+ ["Expected (#{@to_name}): #{@expected.inspect}\n",
+ "#{'but got'.rjust(@to_name.length + 10)}: #{@actual.inspect}\nBacktrace"]
+ end
+
+ def negative_failure_message()
+ ["Expected output (#{@to_name}) to NOT be:\n", @actual.inspect]
+ end
+end
+
+module MSpecMatchers
+ private def output_to_fd(what, where = STDOUT)
+ OutputToFDMatcher.new what, where
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/raise_error.rb b/spec/mspec/lib/mspec/matchers/raise_error.rb
new file mode 100644
index 0000000000..8cba842ce3
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/raise_error.rb
@@ -0,0 +1,132 @@
+class RaiseErrorMatcher
+ FAILURE_MESSAGE_FOR_EXCEPTION = {}.compare_by_identity
+ UNDEF_CAUSE = Object.new
+
+ attr_writer :block
+
+ def initialize(exception, message = nil, options = nil, &block)
+ if message.is_a? Hash
+ @message = nil
+ options = message
+ else
+ @message = message
+ end
+ @cause = options ? options.fetch(:cause, UNDEF_CAUSE) : UNDEF_CAUSE
+ @exception = exception
+ @block = block
+ @actual = nil
+ end
+
+ # This #matches? method is unusual because it doesn't always return a boolean but instead
+ # re-raises the original exception if proc.call raises an exception and #matching_exception? is false.
+ # The reasoning is the original exception class matters and we don't want to change it by raising another exception,
+ # so instead we attach the #failure_message and extract it in ExceptionState#message.
+ def matches?(proc)
+ @result = proc.call
+ return false
+ rescue Object => actual
+ @actual = actual
+
+ if matching_exception?(actual)
+ # The block has its own expectations and will throw an exception if it fails
+ @block[actual] if @block
+ return true
+ else
+ FAILURE_MESSAGE_FOR_EXCEPTION[actual] = failure_message
+ raise actual
+ end
+ end
+
+ def matching_class?(exc)
+ @exception === exc
+ end
+
+ def matching_message?(exc)
+ case @message
+ when String
+ @message == exc.message
+ when Regexp
+ @message =~ exc.message
+ else
+ true
+ end
+ end
+
+ def matching_cause?(exc)
+ case @cause
+ when UNDEF_CAUSE
+ true
+ else
+ @cause == exc.cause
+ end
+ end
+
+ def matching_exception?(exc)
+ matching_class?(exc) and matching_message?(exc) and matching_cause?(exc)
+ end
+
+ def exception_class_and_message_and_cause(exception_class, message, cause)
+ string = "#{exception_class}"
+ prefixed = false
+ prefix = -> { prefixed ? ", " : prefixed = "(" }
+
+ if message != nil
+ string << "#{prefix.()}#{message.inspect}"
+ end
+
+ if cause != UNDEF_CAUSE
+ string << "#{prefix.()}cause: #{cause.inspect}"
+ end
+
+ string << ")" if prefixed
+
+ string
+ end
+
+ def format_expected_exception
+ exception_class_and_message_and_cause(@exception, @message, @cause)
+ end
+
+ def format_exception(exception)
+ exception_class_and_message_and_cause(exception.class,
+ @message == nil ? nil : exception.message,
+ @cause == UNDEF_CAUSE ? UNDEF_CAUSE : exception.cause)
+ end
+
+ def failure_message
+ message = ["Expected #{format_expected_exception}"]
+
+ if @actual
+ message << "but got: #{format_exception(@actual)}"
+ else
+ message << "but no exception was raised (#{MSpec.format(@result)} was returned)"
+ end
+
+ message
+ end
+
+ def negative_failure_message
+ message = ["Expected to not get #{format_expected_exception}", ""]
+ unless @actual.class == @exception
+ message[1] = "but got: #{format_exception(@actual)}"
+ end
+ message
+ end
+end
+
+module MSpecMatchers
+ private def raise_error(exception = Exception, message = nil, options = nil, &block)
+ RaiseErrorMatcher.new(exception, message, options, &block)
+ end
+
+ # CRuby < 4.1 has inconsistent coercion errors:
+ # https://bugs.ruby-lang.org/issues/21864
+ # This matcher ignores the message on CRuby < 4.1
+ # and checks the message for all other cases, including other Rubies
+ private def raise_consistent_error(exception = Exception, message = nil, options = nil, &block)
+ if RUBY_ENGINE == "ruby" and ruby_version_is ""..."4.1"
+ message = nil
+ end
+ RaiseErrorMatcher.new(exception, message, options, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/respond_to.rb b/spec/mspec/lib/mspec/matchers/respond_to.rb
new file mode 100644
index 0000000000..6b35ae2d3c
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/respond_to.rb
@@ -0,0 +1,24 @@
+class RespondToMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.respond_to?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "to respond to #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "not to respond to #{@expected}"]
+ end
+end
+
+module MSpecMatchers
+ private def respond_to(expected)
+ RespondToMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/signed_zero.rb b/spec/mspec/lib/mspec/matchers/signed_zero.rb
new file mode 100644
index 0000000000..2ff90f4994
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/signed_zero.rb
@@ -0,0 +1,28 @@
+class SignedZeroMatcher
+ def initialize(expected_sign)
+ @expected_sign = expected_sign
+ end
+
+ def matches?(actual)
+ @actual = actual
+ (1.0/actual).infinite? == @expected_sign
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}0.0"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}0.0"]
+ end
+end
+
+module MSpecMatchers
+ private def be_positive_zero
+ SignedZeroMatcher.new(1)
+ end
+
+ private def be_negative_zero
+ SignedZeroMatcher.new(-1)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/skip.rb b/spec/mspec/lib/mspec/matchers/skip.rb
new file mode 100644
index 0000000000..7c175d358d
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/skip.rb
@@ -0,0 +1,5 @@
+module MSpecMatchers
+ private def skip(reason = 'no reason')
+ raise SkippedSpecError, reason
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/variable.rb b/spec/mspec/lib/mspec/matchers/variable.rb
new file mode 100644
index 0000000000..4d801ea337
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/variable.rb
@@ -0,0 +1,24 @@
+class VariableMatcher
+ class << self
+ attr_accessor :variables_method, :description
+ end
+
+ def initialize(variable)
+ @variable = variable.to_sym
+ end
+
+ def matches?(object)
+ @object = object
+ @object.send(self.class.variables_method).include? @variable
+ end
+
+ def failure_message
+ ["Expected #{@object} to have #{self.class.description} '#{@variable}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@object} NOT to have #{self.class.description} '#{@variable}'",
+ "but it does"]
+ end
+end
diff --git a/spec/mspec/lib/mspec/mocks.rb b/spec/mspec/lib/mspec/mocks.rb
new file mode 100644
index 0000000000..6a029c7b53
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks.rb
@@ -0,0 +1,3 @@
+require 'mspec/mocks/mock'
+require 'mspec/mocks/proxy'
+require 'mspec/mocks/object'
diff --git a/spec/mspec/lib/mspec/mocks/mock.rb b/spec/mspec/lib/mspec/mocks/mock.rb
new file mode 100644
index 0000000000..c61ba35ea7
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks/mock.rb
@@ -0,0 +1,209 @@
+require 'mspec/expectations/expectations'
+require 'mspec/helpers/warning'
+
+module Mock
+ def self.reset
+ @mocks = @stubs = @objects = nil
+ end
+
+ def self.objects
+ @objects ||= {}
+ end
+
+ def self.mocks
+ @mocks ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ def self.stubs
+ @stubs ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ def self.replaced_name(key)
+ :"__mspec_#{key.last}__"
+ end
+
+ def self.replaced_key(obj, sym)
+ [obj.__id__, sym]
+ end
+
+ def self.replaced?(key)
+ mocks.include?(key) or stubs.include?(key)
+ end
+
+ def self.clear_replaced(key)
+ mocks.delete key
+ stubs.delete key
+ end
+
+ def self.mock_respond_to?(obj, sym, include_private = false)
+ key = replaced_key(obj, :respond_to?)
+ if replaced? key
+ name = replaced_name(key)
+ obj.__send__ name, sym, include_private
+ else
+ obj.respond_to? sym, include_private
+ end
+ end
+
+ def self.install_method(obj, sym, type = nil)
+ meta = obj.singleton_class
+
+ key = replaced_key obj, sym
+ sym = sym.to_sym
+
+ if type == :stub and mocks.key?(key)
+ # Defining a stub and there is already a mock, ignore the stub
+ return
+ end
+
+ if (sym == :respond_to? or mock_respond_to?(obj, sym, true)) and !replaced?(key)
+ meta.__send__ :alias_method, replaced_name(key), sym
+ end
+
+ suppress_warning {
+ meta.class_eval {
+ define_method(sym) do |*args, &block|
+ Mock.verify_call self, sym, *args, &block
+ end
+ }
+ }
+
+ proxy = MockProxy.new type
+
+ if proxy.mock?
+ MSpec.expectation
+ MSpec.actions :expectation, MSpec.current.state
+ end
+
+ if proxy.mock? and stubs.key?(key)
+ # Defining a mock and there is already a stub, remove the stub
+ stubs.delete key
+ end
+
+ if proxy.stub?
+ stubs[key].unshift proxy
+ else
+ mocks[key] << proxy
+ end
+ objects[key] = obj
+
+ proxy
+ end
+
+ def self.name_or_inspect(obj)
+ obj.instance_variable_get(:@name) || obj.inspect
+ end
+
+ def self.inspect_args(args)
+ "(#{Array(args).map(&:inspect).join(', ')})"
+ end
+
+ def self.verify_count
+ mocks.each do |key, proxies|
+ obj = objects[key]
+ proxies.each do |proxy|
+ qualifier, count = proxy.count
+ pass = case qualifier
+ when :at_least
+ proxy.calls >= count
+ when :at_most
+ proxy.calls <= count
+ when :exactly
+ proxy.calls == count
+ when :any_number_of_times
+ true
+ else
+ false
+ end
+ unless pass
+ SpecExpectation.fail_with(
+ "Mock '#{name_or_inspect obj}' expected to receive #{key.last}#{inspect_args proxy.arguments} " + \
+ "#{qualifier.to_s.sub('_', ' ')} #{count} times",
+ "but received it #{proxy.calls} times")
+ end
+ end
+ end
+ end
+
+ def self.verify_call(obj, sym, *args, &block)
+ compare = *args
+ compare = compare.first if compare.length <= 1
+
+ key = replaced_key obj, sym
+ [mocks, stubs].each do |proxies|
+ proxies.fetch(key, []).each do |proxy|
+ pass = case proxy.arguments
+ when :any_args
+ true
+ when :no_args
+ compare.nil?
+ else
+ proxy.arguments == compare
+ end
+
+ if proxy.yielding?
+ if block
+ proxy.yielding.each do |args_to_yield|
+ if block.arity == -1 || block.arity == args_to_yield.size
+ block.call(*args_to_yield)
+ else
+ SpecExpectation.fail_with(
+ "Mock '#{name_or_inspect obj}' asked to yield " + \
+ "|#{proxy.yielding.join(', ')}| on #{sym}\n",
+ "but a block with arity #{block.arity} was passed")
+ end
+ end
+ else
+ SpecExpectation.fail_with(
+ "Mock '#{name_or_inspect obj}' asked to yield " + \
+ "|[#{proxy.yielding.join('], [')}]| on #{sym}\n",
+ "but no block was passed")
+ end
+ end
+
+ if pass
+ proxy.called
+
+ if proxy.raising?
+ raise proxy.raising
+ else
+ return proxy.returning
+ end
+ end
+ end
+ end
+
+ if sym.to_sym == :respond_to?
+ mock_respond_to? obj, *args
+ else
+ SpecExpectation.fail_with("Mock '#{name_or_inspect obj}': method #{sym}\n",
+ "called with unexpected arguments #{inspect_args args}")
+ end
+ end
+
+ def self.cleanup
+ objects.each do |key, obj|
+ if obj.kind_of? MockIntObject
+ clear_replaced key
+ next
+ end
+
+ replaced = replaced_name(key)
+ sym = key.last
+ meta = obj.singleton_class
+
+ if mock_respond_to? obj, replaced, true
+ suppress_warning do
+ meta.__send__ :alias_method, sym, replaced
+ end
+ meta.__send__ :remove_method, replaced
+ else
+ meta.__send__ :remove_method, sym
+ end
+
+ clear_replaced key
+ end
+ ensure
+ reset
+ end
+end
diff --git a/spec/mspec/lib/mspec/mocks/object.rb b/spec/mspec/lib/mspec/mocks/object.rb
new file mode 100644
index 0000000000..fcaa1caef0
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks/object.rb
@@ -0,0 +1,28 @@
+require 'mspec/mocks/proxy'
+
+class Object
+ def should_receive(sym)
+ Mock.install_method self, sym
+ end
+
+ def should_not_receive(sym)
+ proxy = Mock.install_method self, sym
+ proxy.exactly(0).times
+ end
+
+ def stub!(sym)
+ Mock.install_method self, sym, :stub
+ end
+end
+
+def mock(name, options = {})
+ MockObject.new name, options
+end
+
+def mock_int(val)
+ MockIntObject.new(val)
+end
+
+def mock_numeric(name, options = {})
+ NumericMockObject.new name, options
+end
diff --git a/spec/mspec/lib/mspec/mocks/proxy.rb b/spec/mspec/lib/mspec/mocks/proxy.rb
new file mode 100644
index 0000000000..8473132b0b
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks/proxy.rb
@@ -0,0 +1,186 @@
+class MockObject
+ def initialize(name, options = {})
+ @name = name
+ @null = options[:null_object]
+ end
+
+ def method_missing(sym, *args, &block)
+ @null ? self : super
+ end
+ private :method_missing
+end
+
+class NumericMockObject < Numeric
+ def initialize(name, options = {})
+ @name = name
+ @null = options[:null_object]
+ end
+
+ def method_missing(sym, *args, &block)
+ @null ? self : super
+ end
+
+ def singleton_method_added(val)
+ end
+end
+
+class MockIntObject
+ def initialize(val)
+ @value = val
+ @calls = 0
+
+ key = [self, :to_int]
+
+ Mock.objects[key] = self
+ Mock.mocks[key] << self
+ end
+
+ attr_reader :calls
+
+ def to_int
+ @calls += 1
+ @value.to_int
+ end
+
+ def count
+ [:at_least, 1]
+ end
+end
+
+class MockProxy
+ attr_reader :raising, :yielding
+
+ def initialize(type = nil)
+ @multiple_returns = nil
+ @returning = nil
+ @raising = nil
+ @yielding = []
+ @arguments = :any_args
+ @type = type || :mock
+ end
+
+ def mock?
+ @type == :mock
+ end
+
+ def stub?
+ @type == :stub
+ end
+
+ def count
+ @count ||= mock? ? [:exactly, 1] : [:any_number_of_times, 0]
+ end
+
+ def arguments
+ @arguments
+ end
+
+ def returning
+ if @multiple_returns
+ if @returning.size == 1
+ @multiple_returns = false
+ return @returning = @returning.shift
+ end
+ return @returning.shift
+ end
+ @returning
+ end
+
+ def times
+ self
+ end
+
+ def calls
+ @calls ||= 0
+ end
+
+ def called
+ @calls = calls + 1
+ end
+
+ def exactly(n)
+ @count = [:exactly, n_times(n)]
+ self
+ end
+
+ def at_least(n)
+ @count = [:at_least, n_times(n)]
+ self
+ end
+
+ def at_most(n)
+ @count = [:at_most, n_times(n)]
+ self
+ end
+
+ def once
+ exactly 1
+ end
+
+ def twice
+ exactly 2
+ end
+
+ def any_number_of_times
+ @count = [:any_number_of_times, 0]
+ self
+ end
+
+ def with(*args)
+ raise ArgumentError, "you must specify the expected arguments" if args.empty?
+ if args.length == 1
+ @arguments = args.first
+ else
+ @arguments = args
+ end
+ self
+ end
+
+ def and_return(*args)
+ case args.size
+ when 0
+ @returning = nil
+ when 1
+ @returning = args[0]
+ else
+ @multiple_returns = true
+ @returning = args
+ count[1] = args.size if count[1] < args.size
+ end
+ self
+ end
+
+ def and_raise(exception)
+ if exception.kind_of? String
+ @raising = RuntimeError.new exception
+ else
+ @raising = exception
+ end
+ end
+
+ def raising?
+ @raising != nil
+ end
+
+ def and_yield(*args)
+ @yielding << args
+ self
+ end
+
+ def yielding?
+ !@yielding.empty?
+ end
+
+ private
+
+ def n_times(n)
+ case n
+ when :once
+ 1
+ when :twice
+ 2
+ else
+ Integer n
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner.rb b/spec/mspec/lib/mspec/runner.rb
new file mode 100644
index 0000000000..df57b9f69b
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner.rb
@@ -0,0 +1,12 @@
+require 'mspec/mocks'
+require 'mspec/runner/mspec'
+require 'mspec/runner/context'
+require 'mspec/runner/evaluate'
+require 'mspec/runner/example'
+require 'mspec/runner/exception'
+require 'mspec/runner/object'
+require 'mspec/runner/formatters'
+require 'mspec/runner/actions'
+require 'mspec/runner/filters'
+require 'mspec/runner/shared'
+require 'mspec/runner/tag'
diff --git a/spec/mspec/lib/mspec/runner/actions.rb b/spec/mspec/lib/mspec/runner/actions.rb
new file mode 100644
index 0000000000..0a5a05fbd1
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions.rb
@@ -0,0 +1,6 @@
+require 'mspec/runner/actions/tally'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/tag'
+require 'mspec/runner/actions/taglist'
+require 'mspec/runner/actions/tagpurge'
diff --git a/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb b/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb
new file mode 100644
index 0000000000..abfb6dd0ee
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb
@@ -0,0 +1,84 @@
+class ConstantsLockFile
+ LOCK_FILE_NAME = '.mspec.constants'
+
+ def self.lock_file
+ @prefix ||= File.expand_path(MSpecScript.get(:prefix) || '.')
+ "#{@prefix}/#{LOCK_FILE_NAME}"
+ end
+
+ def self.load
+ if File.exist?(lock_file)
+ File.readlines(lock_file).map(&:chomp)
+ else
+ []
+ end
+ end
+
+ def self.dump(ary)
+ contents = ary.map(&:to_s).uniq.sort.join("\n") + "\n"
+ File.write(lock_file, contents)
+ end
+end
+
+class ConstantLeakError < StandardError
+end
+
+class ConstantsLeakCheckerAction
+ def initialize(save)
+ @save = save
+ @check = !save
+ @constants_locked = ConstantsLockFile.load
+ @exclude_patterns = MSpecScript.get(:toplevel_constants_excludes) || []
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ def start
+ @constants_start = constants_now
+ end
+
+ def before(state)
+ @constants_before = constants_now
+ end
+
+ def after(state)
+ constants = remove_excludes(constants_now - @constants_before - @constants_locked)
+
+ if @check && !constants.empty?
+ MSpec.protect 'Constants leak check' do
+ raise ConstantLeakError, "Top level constants leaked: #{constants.join(', ')}"
+ end
+ end
+ end
+
+ def finish
+ constants = remove_excludes(constants_now - @constants_start - @constants_locked)
+
+ if @save
+ ConstantsLockFile.dump(@constants_locked + constants)
+ end
+
+ if @check && !constants.empty?
+ MSpec.protect 'Global constants leak check' do
+ raise ConstantLeakError, "Top level constants leaked in the whole test suite: #{constants.join(', ')}"
+ end
+ end
+ end
+
+ private
+
+ def constants_now
+ Object.constants.map(&:to_s)
+ end
+
+ def remove_excludes(constants)
+ constants.reject { |name|
+ @exclude_patterns.any? { |pattern| pattern === name }
+ }
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/filter.rb b/spec/mspec/lib/mspec/runner/actions/filter.rb
new file mode 100644
index 0000000000..b0ad7080da
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/filter.rb
@@ -0,0 +1,40 @@
+require 'mspec/runner/filters/match'
+
+# ActionFilter is a base class for actions that are triggered by
+# specs that match the filter. The filter may be specified by
+# strings that match spec descriptions or by tags for strings
+# that match spec descriptions.
+#
+# Unlike TagFilter and RegexpFilter, ActionFilter instances do
+# not affect the specs that are run. The filter is only used to
+# trigger the action.
+
+class ActionFilter
+ def initialize(tags = nil, descs = nil)
+ @tags = Array(tags)
+ descs = Array(descs)
+ @sfilter = descs.empty? ? nil : MatchFilter.new(nil, *descs)
+ @tfilter = nil
+ end
+
+ def ===(string)
+ @sfilter === string or @tfilter === string
+ end
+
+ def load
+ return if @tags.empty?
+
+ desc = MSpec.read_tags(@tags).map { |t| t.description }
+ return if desc.empty?
+
+ @tfilter = MatchFilter.new(nil, *desc)
+ end
+
+ def register
+ MSpec.register :load, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/leakchecker.rb b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
new file mode 100644
index 0000000000..0a8c9c3252
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
@@ -0,0 +1,377 @@
+# Adapted from ruby's test/lib/leakchecker.rb.
+# Ruby's 2-clause BSDL follows.
+
+# Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+class LeakError < StandardError
+end
+
+class LeakChecker
+ attr_reader :leaks
+
+ def initialize
+ @fd_info = find_fds
+ @tempfile_info = find_tempfiles
+ @thread_info = find_threads
+ @env_info = find_env
+ @argv_info = find_argv
+ @globals_info = find_globals
+ @encoding_info = find_encodings
+ end
+
+ def check(state)
+ @state = state
+ @leaks = []
+ check_fd_leak
+ check_tempfile_leak
+ check_thread_leak
+ check_process_leak
+ check_env
+ check_argv
+ check_globals
+ check_encodings
+ check_tracepoints
+ GC.start unless @leaks.empty?
+ @leaks.empty?
+ end
+
+ private
+ def find_fds
+ fd_dir = "/proc/self/fd"
+ if File.directory?(fd_dir)
+ fds = Dir.open(fd_dir) {|d|
+ a = d.grep(/\A\d+\z/, &:to_i)
+ if d.respond_to? :fileno
+ a -= [d.fileno]
+ end
+ a
+ }
+ fds.sort
+ else
+ []
+ end
+ end
+
+ def check_fd_leak
+ live1 = @fd_info
+ if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
+ m[:close]
+ end
+ live2 = find_fds
+ fd_closed = live1 - live2
+ if !fd_closed.empty?
+ fd_closed.each {|fd|
+ leak "Closed file descriptor: #{fd}"
+ }
+ end
+ fd_leaked = live2 - live1
+ if !fd_leaked.empty?
+ h = {}
+ ObjectSpace.each_object(IO) {|io|
+ inspect = io.inspect
+ begin
+ autoclose = io.autoclose?
+ fd = io.fileno
+ rescue IOError # closed IO object
+ next
+ end
+ (h[fd] ||= []) << [io, autoclose, inspect]
+ }
+ fd_leaked.each {|fd|
+ str = ''
+ if h[fd]
+ str << ' :'
+ h[fd].map {|io, autoclose, inspect|
+ s = ' ' + inspect
+ s << "(not-autoclose)" if !autoclose
+ s
+ }.sort.each {|s|
+ str << s
+ }
+ end
+ leak "Leaked file descriptor: #{fd}#{str}"
+ }
+ #system("lsof -p #$$") if !fd_leaked.empty?
+ h.each {|fd, list|
+ next if list.length <= 1
+ if 1 < list.count {|io, autoclose, inspect| autoclose }
+ str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
+ leak "Multiple autoclose IO object for a file descriptor:#{str}"
+ end
+ }
+ end
+ @fd_info = live2
+ end
+
+ def extend_tempfile_counter
+ return if defined? LeakChecker::TempfileCounter
+ m = Module.new {
+ @count = 0
+ class << self
+ attr_accessor :count
+ end
+
+ def new(...)
+ LeakChecker::TempfileCounter.count += 1
+ super
+ end
+ }
+ LeakChecker.const_set(:TempfileCounter, m)
+
+ class << Tempfile
+ prepend LeakChecker::TempfileCounter
+ end
+ end
+
+ def find_tempfiles(prev_count = -1)
+ return [prev_count, []] unless defined? Tempfile
+ extend_tempfile_counter
+ count = TempfileCounter.count
+ if prev_count == count
+ [prev_count, []]
+ else
+ tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t| t.path }
+ [count, tempfiles]
+ end
+ end
+
+ def check_tempfile_leak
+ return false unless defined? Tempfile
+ count1, initial_tempfiles = @tempfile_info
+ count2, current_tempfiles = find_tempfiles(count1)
+ tempfiles_leaked = current_tempfiles - initial_tempfiles
+ if !tempfiles_leaked.empty?
+ list = tempfiles_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ leak "Leaked tempfile: #{str}"
+ }
+ tempfiles_leaked.each {|t| t.close! }
+ end
+ @tempfile_info = [count2, initial_tempfiles]
+ end
+
+ def find_threads
+ Thread.list.find_all {|t|
+ t != Thread.current && t.alive? &&
+ !(t.thread_variable?(:"\0__detached_thread__") && t.thread_variable_get(:"\0__detached_thread__"))
+ }
+ end
+
+ def check_thread_leak
+ live1 = @thread_info
+ live2 = find_threads
+ thread_finished = live1 - live2
+ if !thread_finished.empty?
+ list = thread_finished.map {|t| t.inspect }.sort
+ list.each {|str|
+ leak "Finished thread: #{str}"
+ }
+ end
+ thread_leaked = live2 - live1
+ if !thread_leaked.empty?
+ list = thread_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ leak "Leaked thread: #{str}"
+ }
+ end
+ @thread_info = live2
+ end
+
+ def check_process_leak
+ subprocesses_leaked = Process.waitall
+ subprocesses_leaked.each { |pid, status|
+ leak "Leaked subprocess: #{pid}: #{status}"
+ }
+ end
+
+ def find_env
+ ENV.to_h
+ end
+
+ def check_env
+ old_env = @env_info
+ new_env = find_env
+ return if old_env == new_env
+
+ (old_env.keys | new_env.keys).sort.each {|k|
+ if old_env.has_key?(k)
+ if new_env.has_key?(k)
+ if old_env[k] != new_env[k]
+ leak "Environment variable changed : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
+ end
+ else
+ leak "Environment variable changed: #{k.inspect} deleted"
+ end
+ else
+ if new_env.has_key?(k)
+ leak "Environment variable changed: #{k.inspect} added"
+ else
+ flunk "unreachable"
+ end
+ end
+ }
+ @env_info = new_env
+ end
+
+ def find_argv
+ ARGV.map { |e| e.dup }
+ end
+
+ def check_argv
+ old_argv = @argv_info
+ new_argv = find_argv
+ if new_argv != old_argv
+ leak "ARGV changed: #{old_argv.inspect} to #{new_argv.inspect}"
+ @argv_info = new_argv
+ end
+ end
+
+ def find_globals
+ { verbose: $VERBOSE, debug: $DEBUG }
+ end
+
+ def check_globals
+ old_globals = @globals_info
+ new_globals = find_globals
+ if new_globals != old_globals
+ leak "Globals changed: #{old_globals.inspect} to #{new_globals.inspect}"
+ @globals_info = new_globals
+ end
+ end
+
+ def find_encodings
+ [Encoding.default_internal, Encoding.default_external]
+ end
+
+ def check_encodings
+ old_internal, old_external = @encoding_info
+ new_internal, new_external = find_encodings
+ if new_internal != old_internal
+ leak "Encoding.default_internal changed: #{old_internal.inspect} to #{new_internal.inspect}"
+ end
+ if new_external != old_external
+ leak "Encoding.default_external changed: #{old_external.inspect} to #{new_external.inspect}"
+ end
+ @encoding_info = [new_internal, new_external]
+ end
+
+ def check_tracepoints
+ ObjectSpace.each_object(TracePoint) do |tp|
+ if tp.enabled?
+ leak "TracePoint is still enabled: #{tp.inspect}"
+ end
+ end
+ end
+
+ def leak(message)
+ if @leaks.empty?
+ $stderr.puts "\n"
+ $stderr.puts @state.description
+ end
+ @leaks << message
+ $stderr.puts message
+ end
+end
+
+class LeakCheckerAction
+ def register
+ MSpec.register :start, self
+ MSpec.register :after, self
+ end
+
+ def start
+ disable_nss_modules
+ @checker = LeakChecker.new
+ end
+
+ def after(state)
+ unless @checker.check(state)
+ leak_messages = @checker.leaks
+ location = state.description
+ if state.example
+ location = "#{location}\n#{state.example.source_location.join(':')}"
+ end
+ MSpec.protect(location) do
+ raise LeakError, leak_messages.join("\n")
+ end
+ end
+ end
+
+ private
+
+ # This function is intended to disable all NSS modules when ruby is compiled
+ # against glibc. NSS modules allow the system administrator to load custom
+ # shared objects into all processes using glibc, and use them to customise
+ # the behaviour of username, groupname, hostname, etc lookups. This is
+ # normally configured in the file /etc/nsswitch.conf.
+ # These modules often do things like open cache files or connect to system
+ # daemons like sssd or dbus, which of course means they have open file
+ # descriptors of their own. This can cause the leak-checking functionality
+ # in this file to report that such descriptors have been leaked, and fail
+ # the test suite.
+ # This function uses glibc's __nss_configure_lookup function to override any
+ # configuration in /etc/nsswitch.conf, and just use the built in files/dns
+ # name lookup functionality (which is of course perfectly sufficient for
+ # running ruby/spec).
+ def disable_nss_modules
+ begin
+ require 'fiddle'
+ rescue LoadError
+ # Make sure it's possible to run the test suite on a ruby implementation
+ # which does not (yet?) have Fiddle.
+ return
+ end
+
+ begin
+ libc = Fiddle.dlopen(nil)
+ # Older versions of fiddle don't have Fiddle::Type (and instead rely on Fiddle::TYPE_)
+ # Even older versions of fiddle don't have CONST_STRING,
+ string_type = defined?(Fiddle::TYPE_CONST_STRING) ? Fiddle::TYPE_CONST_STRING : Fiddle::TYPE_VOIDP
+ nss_configure_lookup = Fiddle::Function.new(
+ libc['__nss_configure_lookup'],
+ [string_type, string_type],
+ Fiddle::TYPE_INT
+ )
+ rescue Fiddle::DLError
+ # We're not running with glibc - no need to do this.
+ return
+ end
+
+ nss_configure_lookup.call 'passwd', 'files'
+ nss_configure_lookup.call 'shadow', 'files'
+ nss_configure_lookup.call 'group', 'files'
+ nss_configure_lookup.call 'hosts', 'files dns'
+ nss_configure_lookup.call 'services', 'files'
+ nss_configure_lookup.call 'netgroup', 'files'
+ nss_configure_lookup.call 'automount', 'files'
+ nss_configure_lookup.call 'aliases', 'files'
+ nss_configure_lookup.call 'ethers', 'files'
+ nss_configure_lookup.call 'gshadow', 'files'
+ nss_configure_lookup.call 'initgroups', 'files'
+ nss_configure_lookup.call 'networks', 'files dns'
+ nss_configure_lookup.call 'protocols', 'files'
+ nss_configure_lookup.call 'publickey', 'files'
+ nss_configure_lookup.call 'rpc', 'files'
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/profile.rb b/spec/mspec/lib/mspec/runner/actions/profile.rb
new file mode 100644
index 0000000000..c743d6e3e8
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/profile.rb
@@ -0,0 +1,60 @@
+class ProfileAction
+ def initialize
+ @describe_name = nil
+ @describe_time = nil
+ @describes = []
+ @its = []
+ end
+
+ def register
+ MSpec.register :enter, self
+ MSpec.register :before,self
+ MSpec.register :after, self
+ MSpec.register :finish,self
+ end
+
+ def enter(describe)
+ if @describe_time
+ @describes << [@describe_name, now - @describe_time]
+ end
+
+ @describe_name = describe
+ @describe_time = now
+ end
+
+ def before(state)
+ @it_name = state.it
+ @it_time = now
+ end
+
+ def after(state = nil)
+ @its << [@describe_name, @it_name, now - @it_time]
+ end
+
+ def finish
+ puts "\nProfiling info:"
+
+ desc = @describes.sort { |a,b| b.last <=> a.last }
+ desc.delete_if { |a| a.last <= 0.001 }
+ show = desc[0, 100]
+
+ puts "Top #{show.size} describes:"
+
+ show.each do |des, time|
+ printf "%3.3f - %s\n", time, des
+ end
+
+ its = @its.sort { |a,b| b.last <=> a.last }
+ its.delete_if { |a| a.last <= 0.001 }
+ show = its[0, 100]
+
+ puts "\nTop #{show.size} its:"
+ show.each do |des, it, time|
+ printf "%3.3f - %s %s\n", time, des, it
+ end
+ end
+
+ def now
+ Time.now.to_f
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tag.rb b/spec/mspec/lib/mspec/runner/actions/tag.rb
new file mode 100644
index 0000000000..d40d562451
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tag.rb
@@ -0,0 +1,133 @@
+require 'mspec/runner/actions/filter'
+
+# TagAction - Write tagged spec description string to a
+# tag file associated with each spec file.
+#
+# The action is triggered by specs whose descriptions
+# match the filter created with 'tags' and/or 'desc'
+#
+# The action fires in the :after event, after the spec
+# had been run. The action fires if the outcome of
+# running the spec matches 'outcome'.
+#
+# The arguments are:
+#
+# action: :add, :del
+# outcome: :pass, :fail, :all
+# tag: the tag to create/delete
+# comment: the comment to create
+# tags: zero or more tags to get matching
+# spec description strings from
+# desc: zero or more strings to match the
+# spec description strings
+
+class TagAction < ActionFilter
+ def initialize(action, outcome, tag, comment, tags = nil, descs = nil)
+ super tags, descs
+ @action = action
+ @outcome = outcome
+ @tag = tag
+ @comment = comment
+ @report = []
+ @exception = false
+ end
+
+ # Returns true if there are no _tag_ or _description_ filters. This
+ # means that a TagAction matches any example by default. Otherwise,
+ # returns true if either the _tag_ or the _description_ filter
+ # matches +string+.
+ def ===(string)
+ return true unless @sfilter or @tfilter
+ @sfilter === string or @tfilter === string
+ end
+
+ # Callback for the MSpec :before event. Resets the +#exception?+
+ # flag to false.
+ def before(state)
+ @exception = false
+ end
+
+ # Callback for the MSpec :exception event. Sets the +#exception?+
+ # flag to true.
+ def exception(exception)
+ @exception = true
+ end
+
+ # Callback for the MSpec :after event. Performs the tag action
+ # depending on the type of action and the outcome of evaluating
+ # the example. See +TagAction+ for a description of the actions.
+ def after(state)
+ if self === state.description and outcome?
+ tag = SpecTag.new
+ tag.tag = @tag
+ tag.comment = @comment
+ tag.description = state.description
+
+ case @action
+ when :add
+ changed = MSpec.write_tag tag
+ when :del
+ changed = MSpec.delete_tag tag
+ end
+
+ @report << state.description if changed
+ end
+ end
+
+ # Returns true if the result of evaluating the example matches
+ # the _outcome_ registered for this tag action. See +TagAction+
+ # for a description of the _outcome_ types.
+ def outcome?
+ @outcome == :all or
+ (@outcome == :pass and not exception?) or
+ (@outcome == :fail and exception?)
+ end
+
+ # Returns true if an exception was raised while evaluating the
+ # current example.
+ def exception?
+ @exception
+ end
+
+ def report
+ @report.join("\n") + "\n"
+ end
+ private :report
+
+ # Callback for the MSpec :finish event. Prints the actions
+ # performed while evaluating the examples.
+ def finish
+ case @action
+ when :add
+ if @report.empty?
+ print "\nTagAction: no specs were tagged with '#{@tag}'\n"
+ else
+ print "\nTagAction: specs tagged with '#{@tag}':\n\n"
+ print report
+ end
+ when :del
+ if @report.empty?
+ print "\nTagAction: no tags '#{@tag}' were deleted\n"
+ else
+ print "\nTagAction: tag '#{@tag}' deleted for specs:\n\n"
+ print report
+ end
+ end
+ end
+
+ def register
+ super
+ MSpec.register :before, self
+ MSpec.register :exception, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ def unregister
+ super
+ MSpec.unregister :before, self
+ MSpec.unregister :exception, self
+ MSpec.unregister :after, self
+ MSpec.unregister :finish, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/taglist.rb b/spec/mspec/lib/mspec/runner/actions/taglist.rb
new file mode 100644
index 0000000000..3097e655d5
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/taglist.rb
@@ -0,0 +1,56 @@
+require 'mspec/runner/actions/filter'
+
+# TagListAction - prints out the descriptions for any specs
+# tagged with +tags+. If +tags+ is an empty list, prints out
+# descriptions for any specs that are tagged.
+class TagListAction
+ def initialize(tags = nil)
+ @tags = tags.nil? || tags.empty? ? nil : Array(tags)
+ @filter = nil
+ end
+
+ # Returns true. This enables us to match any tag when loading
+ # tags from the file.
+ def include?(arg)
+ true
+ end
+
+ # Returns true if any tagged descriptions matches +string+.
+ def ===(string)
+ @filter === string
+ end
+
+ # Prints a banner about matching tagged specs.
+ def start
+ if @tags
+ print "\nListing specs tagged with #{@tags.map { |t| "'#{t}'" }.join(", ") }\n\n"
+ else
+ print "\nListing all tagged specs\n\n"
+ end
+ end
+
+ # Creates a MatchFilter for specific tags or for all tags.
+ def load
+ @filter = nil
+ desc = MSpec.read_tags(@tags || self).map { |t| t.description }
+ @filter = MatchFilter.new(nil, *desc) unless desc.empty?
+ end
+
+ # Prints the spec description if it matches the filter.
+ def after(state)
+ return unless self === state.description
+ print state.description, "\n"
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :load, self
+ MSpec.register :after, self
+ end
+
+ def unregister
+ MSpec.unregister :start, self
+ MSpec.unregister :load, self
+ MSpec.unregister :after, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tagpurge.rb b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb
new file mode 100644
index 0000000000..f4587de6bc
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb
@@ -0,0 +1,56 @@
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/taglist'
+
+# TagPurgeAction - removes all tags not matching any spec
+# descriptions.
+class TagPurgeAction < TagListAction
+ attr_reader :matching
+
+ def initialize
+ @matching = []
+ @filter = nil
+ @tags = nil
+ end
+
+ # Prints a banner about purging tags.
+ def start
+ print "\nRemoving tags not matching any specs\n\n"
+ end
+
+ # Creates a MatchFilter for all tags.
+ def load
+ @filter = nil
+ @tags = MSpec.read_tags self
+ desc = @tags.map { |t| t.description }
+ @filter = MatchFilter.new(nil, *desc) unless desc.empty?
+ end
+
+ # Saves any matching tags
+ def after(state)
+ @matching << state.description if self === state.description
+ end
+
+ # Rewrites any matching tags. Prints non-matching tags.
+ # Deletes the tag file if there were no tags (this cleans
+ # up empty or malformed tag files).
+ def unload
+ if @filter
+ matched = @tags.select { |t| @matching.any? { |s| s == t.description } }
+ MSpec.write_tags matched
+
+ (@tags - matched).each { |t| print t.description, "\n" }
+ else
+ MSpec.delete_tags
+ end
+ end
+
+ def register
+ super
+ MSpec.register :unload, self
+ end
+
+ def unregister
+ super
+ MSpec.unregister :unload, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tally.rb b/spec/mspec/lib/mspec/runner/actions/tally.rb
new file mode 100644
index 0000000000..d6ada53bab
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tally.rb
@@ -0,0 +1,133 @@
+class Tally
+ attr_accessor :files, :examples, :expectations, :failures, :errors, :guards, :tagged
+
+ def initialize
+ @files = @examples = @expectations = @failures = @errors = @guards = @tagged = 0
+ end
+
+ def files!(add = 1)
+ @files += add
+ end
+
+ def examples!(add = 1)
+ @examples += add
+ end
+
+ def expectations!(add = 1)
+ @expectations += add
+ end
+
+ def failures!(add = 1)
+ @failures += add
+ end
+
+ def errors!(add = 1)
+ @errors += add
+ end
+
+ def guards!(add = 1)
+ @guards += add
+ end
+
+ def tagged!(add = 1)
+ @tagged += add
+ end
+
+ def file
+ pluralize files, "file"
+ end
+
+ def example
+ pluralize examples, "example"
+ end
+
+ def expectation
+ pluralize expectations, "expectation"
+ end
+
+ def failure
+ pluralize failures, "failure"
+ end
+
+ def error
+ pluralize errors, "error"
+ end
+
+ def guard
+ pluralize guards, "guard"
+ end
+
+ def tag
+ "#{tagged} tagged"
+ end
+
+ def format
+ results = [ file, example, expectation, failure, error, tag ]
+ if [:report, :report_on, :verify].any? { |m| MSpec.mode? m }
+ results << guard
+ end
+ results.join(", ")
+ end
+
+ alias_method :to_s, :format
+
+ def pluralize(count, singular)
+ "#{count} #{singular}#{'s' unless count == 1}"
+ end
+ private :pluralize
+end
+
+class TallyAction
+ attr_reader :counter
+
+ def initialize
+ @counter = Tally.new
+ end
+
+ def register
+ MSpec.register :load, self
+ MSpec.register :exception, self
+ MSpec.register :example, self
+ MSpec.register :tagged, self
+ MSpec.register :expectation, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ MSpec.unregister :exception, self
+ MSpec.unregister :example, self
+ MSpec.unregister :tagged, self
+ MSpec.unregister :expectation, self
+ end
+
+ def load
+ @counter.files!
+ end
+
+ # Callback for the MSpec :expectation event. Increments the
+ # tally of expectations (e.g. #should, #should_receive, etc.).
+ def expectation(state)
+ @counter.expectations!
+ end
+
+ # Callback for the MSpec :exception event. Increments the
+ # tally of errors and failures.
+ def exception(exception)
+ exception.failure? ? @counter.failures! : @counter.errors!
+ end
+
+ # Callback for the MSpec :example event. Increments the tally
+ # of examples.
+ def example(state, block)
+ @counter.examples!
+ end
+
+ def tagged(state)
+ @counter.examples!
+ @counter.tagged!
+ end
+
+ def format
+ @counter.format
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/timeout.rb b/spec/mspec/lib/mspec/runner/actions/timeout.rb
new file mode 100644
index 0000000000..1200926872
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/timeout.rb
@@ -0,0 +1,145 @@
+class TimeoutAction
+ def initialize(timeout)
+ @timeout = timeout
+ @queue = Queue.new
+ @started = now
+ @fail = false
+ @error_message = "took longer than the configured timeout of #{@timeout}s"
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ private def now
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ end
+
+ private def fetch_item
+ @queue.pop(true)
+ rescue ThreadError
+ nil
+ end
+
+ def start
+ @thread = Thread.new do
+ loop do
+ if action = fetch_item
+ action.call
+ else
+ wakeup_at = @started + @timeout
+ left = wakeup_at - now
+ sleep left if left > 0
+ Thread.pass # Let the main thread run
+
+ if @queue.empty?
+ elapsed = now - @started
+ if elapsed > @timeout
+ if @current_state
+ STDERR.puts "\nExample #{@error_message}:"
+ STDERR.puts "#{@current_state.description}"
+ else
+ STDERR.puts "\nSome code outside an example #{@error_message}"
+ end
+ STDERR.flush
+
+ show_backtraces
+ if MSpec.subprocesses.empty?
+ exit! 2
+ else
+ # Do not exit but signal the subprocess so we can get their output
+ MSpec.subprocesses.each do |pid|
+ kill_wait_one_second :SIGTERM, pid
+ hard_kill :SIGKILL, pid
+ end
+ @fail = true
+ @current_state = nil
+ break # stop this thread, will fail in #after
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def before(state = nil)
+ time = now
+ @queue << -> do
+ @current_state = state
+ @started = time
+ end
+ end
+
+ def after(state = nil)
+ @queue << -> do
+ @current_state = nil
+ end
+
+ if @fail
+ STDERR.puts "\n\nThe last example #{@error_message}. See above for the subprocess stacktrace."
+ exit! 2
+ end
+ end
+
+ def finish
+ @thread.kill
+ @thread.join
+ end
+
+ private def hard_kill(signal, pid)
+ begin
+ Process.kill signal, pid
+ rescue Errno::ESRCH
+ # Process already terminated
+ end
+ end
+
+ private def kill_wait_one_second(signal, pid)
+ begin
+ Process.kill signal, pid
+ sleep 1
+ rescue Errno::ESRCH
+ # Process already terminated
+ end
+ end
+
+ private def show_backtraces
+ java_stacktraces = -> pid {
+ if RUBY_ENGINE == 'truffleruby' || RUBY_ENGINE == 'jruby'
+ STDERR.puts 'Java stacktraces:'
+ kill_wait_one_second :SIGQUIT, pid
+ end
+ }
+
+ if MSpec.subprocesses.empty?
+ java_stacktraces.call Process.pid
+
+ STDERR.puts "\nRuby backtraces:"
+ if defined?(Truffle::Debug.show_backtraces)
+ Truffle::Debug.show_backtraces
+ else
+ Thread.list.each do |thread|
+ unless thread == Thread.current
+ STDERR.puts thread.inspect, thread.backtrace, ''
+ end
+ end
+ end
+ else
+ MSpec.subprocesses.each do |pid|
+ STDERR.puts "\nFor subprocess #{pid}"
+ java_stacktraces.call pid
+
+ if RUBY_ENGINE == 'truffleruby'
+ STDERR.puts "\nRuby backtraces:"
+ kill_wait_one_second :SIGALRM, pid
+ else
+ STDERR.puts "Don't know how to print backtraces of a subprocess on #{RUBY_ENGINE}"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/timer.rb b/spec/mspec/lib/mspec/runner/actions/timer.rb
new file mode 100644
index 0000000000..e7ebfebe0d
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/timer.rb
@@ -0,0 +1,22 @@
+class TimerAction
+ def register
+ MSpec.register :start, self
+ MSpec.register :finish, self
+ end
+
+ def start
+ @start = Time.now
+ end
+
+ def finish
+ @stop = Time.now
+ end
+
+ def elapsed
+ @stop - @start
+ end
+
+ def format
+ "Finished in %f seconds" % elapsed
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/context.rb b/spec/mspec/lib/mspec/runner/context.rb
new file mode 100644
index 0000000000..bcd83b2465
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/context.rb
@@ -0,0 +1,237 @@
+# Holds the state of the +describe+ block that is being
+# evaluated. Every example (i.e. +it+ block) is evaluated
+# in a context, which may include state set up in <tt>before
+# :each</tt> or <tt>before :all</tt> blocks.
+#
+#--
+# A note on naming: this is named _ContextState_ rather
+# than _DescribeState_ because +describe+ is the keyword
+# in the DSL for referring to the context in which an example
+# is evaluated, just as +it+ refers to the example itself.
+#++
+class ContextState
+ attr_reader :state, :parent, :parents, :children, :examples, :to_s
+
+ MOCK_VERIFY = -> { Mock.verify_count }
+ MOCK_CLEANUP = -> { Mock.cleanup }
+ EXPECTATION_MISSING = -> { raise SpecExpectationNotFoundError }
+
+ def initialize(description, options = nil)
+ raise "#describe options should be a Hash or nil" unless Hash === options or options.nil?
+ @to_s = description.to_s
+ @shared = options && options[:shared]
+
+ @parsed = false
+ @before = { :all => [], :each => [] }
+ @after = { :all => [], :each => [] }
+ @pre = {}
+ @post = {}
+ @examples = []
+ @state = nil
+ @parent = nil
+ @parents = [self]
+ @children = []
+ end
+
+ # Remove caching when a ContextState is dup'd for shared specs.
+ def initialize_copy(other)
+ @pre = {}
+ @post = {}
+ end
+
+ # Returns true if this is a shared +ContextState+. Essentially, when
+ # created with: describe "Something", :shared => true { ... }
+ def shared?
+ @shared
+ end
+
+ # Set the parent (enclosing) +ContextState+ for this state. Creates
+ # the +parents+ list.
+ def parent=(parent)
+ @description = nil
+
+ if shared?
+ @parent = nil
+ else
+ @parent = parent
+ parent.child self if parent
+
+ @parents = [self]
+ state = parent
+ while state
+ @parents.unshift state
+ state = state.parent
+ end
+ end
+ end
+
+ # Add the ContextState instance +child+ to the list of nested
+ # describe blocks.
+ def child(child)
+ @children << child
+ end
+
+ # Adds a nested ContextState in a shared ContextState to a containing
+ # ContextState.
+ #
+ # Normal adoption is from the parent's perspective. But adopt is a good
+ # verb and it's reasonable for the child to adopt the parent as well. In
+ # this case, manipulating state from inside the child avoids needlessly
+ # exposing the state to manipulate it externally in the dup. (See
+ # #it_should_behave_like)
+ def adopt(parent)
+ self.parent = parent
+
+ @examples = @examples.map do |example|
+ example = example.dup
+ example.context = self
+ example
+ end
+
+ children = @children
+ @children = []
+
+ children.each { |child| child.dup.adopt self }
+ end
+
+ # Returns a list of all before(+what+) blocks from self and any parents.
+ def pre(what)
+ @pre[what] ||= parents.inject([]) { |l, s| l.push(*s.before(what)) }
+ end
+
+ # Returns a list of all after(+what+) blocks from self and any parents.
+ # The list is in reverse order. In other words, the blocks defined in
+ # inner describes are in the list before those defined in outer describes,
+ # and in a particular describe block those defined later are in the list
+ # before those defined earlier.
+ def post(what)
+ @post[what] ||= parents.inject([]) { |l, s| l.unshift(*s.after(what)) }
+ end
+
+ # Records before(:each) and before(:all) blocks.
+ def before(what, &block)
+ return if MSpec.guarded?
+ block ? @before[what].push(block) : @before[what]
+ end
+
+ # Records after(:each) and after(:all) blocks.
+ def after(what, &block)
+ return if MSpec.guarded?
+ block ? @after[what].unshift(block) : @after[what]
+ end
+
+ # Creates an ExampleState instance for the block and stores it
+ # in a list of examples to evaluate unless the example is filtered.
+ def it(desc, &block)
+ raise "nested #it" if @state
+ example = ExampleState.new(self, desc, block)
+ MSpec.actions :add, example
+ return if MSpec.guarded?
+ @examples << example
+ end
+
+ # Evaluates the block and resets the toplevel +ContextState+ to #parent.
+ def describe(&block)
+ @parsed = protect @to_s, block, false
+ MSpec.register_current parent
+ MSpec.register_shared self if shared?
+ end
+
+ # Returns a description string generated from self and all parents
+ def description
+ @description ||= parents.map { |p| p.to_s }.compact.join(" ")
+ end
+
+ # Injects the before/after blocks and examples from the shared
+ # describe block into this +ContextState+ instance.
+ def it_should_behave_like(desc)
+ return if MSpec.guarded?
+
+ unless state = MSpec.retrieve_shared(desc)
+ raise Exception, "Unable to find shared 'describe' for #{desc}"
+ end
+
+ state.before(:all).each { |b| before :all, &b }
+ state.before(:each).each { |b| before :each, &b }
+ state.after(:each).each { |b| after :each, &b }
+ state.after(:all).each { |b| after :all, &b }
+
+ state.examples.each do |example|
+ example = example.dup
+ example.context = self
+ @examples << example
+ end
+
+ state.children.each do |child|
+ child.dup.adopt self
+ end
+ end
+
+ # Evaluates each block in +blocks+ using the +MSpec.protect+ method
+ # so that exceptions are handled and tallied. Returns true and does
+ # NOT evaluate any blocks if +check+ is true and
+ # <tt>MSpec.mode?(:pretend)</tt> is true.
+ def protect(what, blocks, check = true)
+ return true if check and MSpec.mode? :pretend
+ Array(blocks).all? { |block| MSpec.protect what, &block }
+ end
+
+ # Removes filtered examples. Returns true if there are examples
+ # left to evaluate.
+ def filter_examples
+ filtered, @examples = @examples.partition do |ex|
+ ex.filtered?
+ end
+
+ filtered.each do |ex|
+ MSpec.actions :tagged, ex
+ end
+
+ !@examples.empty?
+ end
+
+ # Evaluates the examples in a +ContextState+. Invokes the MSpec events
+ # for :enter, :before, :after, :leave.
+ def process
+ MSpec.register_current self
+
+ if @parsed and filter_examples
+ MSpec.shuffle @examples if MSpec.randomize?
+ MSpec.actions :enter, description
+
+ if protect "before :all", pre(:all)
+ @examples.each do |state|
+ MSpec.repeat do
+ @state = state
+ example = state.example
+ MSpec.actions :before, state
+
+ if protect "before :each", pre(:each)
+ MSpec.clear_expectations
+ if example
+ passed = protect nil, example
+ passed = protect nil, -> { MSpec.actions :passed, state, example } if passed
+ MSpec.actions :example, state, example
+ protect nil, EXPECTATION_MISSING if !MSpec.expectation? and passed
+ end
+ end
+ protect "after :each", post(:each)
+ protect "Mock.verify_count", MOCK_VERIFY
+
+ protect "Mock.cleanup", MOCK_CLEANUP
+ MSpec.actions :after, state
+ @state = nil
+ end
+ end
+ protect "after :all", post(:all)
+ else
+ protect "Mock.cleanup", MOCK_CLEANUP
+ end
+
+ MSpec.actions :leave
+ end
+
+ MSpec.register_current nil
+ children.each { |child| child.process }
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/evaluate.rb b/spec/mspec/lib/mspec/runner/evaluate.rb
new file mode 100644
index 0000000000..396a84c118
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/evaluate.rb
@@ -0,0 +1,54 @@
+class SpecEvaluate
+ include MSpecMatchers
+
+ def self.desc=(desc)
+ @desc = desc
+ end
+
+ def self.desc
+ @desc ||= "evaluates "
+ end
+
+ def initialize(ruby, desc)
+ @ruby = ruby.rstrip
+ @desc = desc || self.class.desc
+ end
+
+ # Formats the Ruby source code for reabable output in the -fs formatter
+ # option. If the source contains no newline characters, wraps the source in
+ # single quotes to set if off from the rest of the description string. If
+ # the source does contain newline characters, sets the indent level to four
+ # characters.
+ def format(ruby, newline = true)
+ if ruby.include?("\n")
+ lines = ruby.each_line.to_a
+ if /( *)/ =~ lines.first
+ if $1.size > 4
+ dedent = $1.size - 4
+ ruby = lines.map { |l| l[dedent..-1] }.join
+ else
+ indent = " " * (4 - $1.size)
+ ruby = lines.map { |l| "#{indent}#{l}" }.join
+ end
+ end
+ "\n#{ruby}"
+ else
+ "'#{ruby.lstrip}'"
+ end
+ end
+
+ def define(&block)
+ ruby = @ruby
+ desc = @desc
+ evaluator = self
+
+ specify "#{desc} #{format ruby}" do
+ evaluator.instance_eval(ruby)
+ evaluator.instance_eval(&block)
+ end
+ end
+end
+
+def evaluate(str, desc = nil, &block)
+ SpecEvaluate.new(str, desc).define(&block)
+end
diff --git a/spec/mspec/lib/mspec/runner/example.rb b/spec/mspec/lib/mspec/runner/example.rb
new file mode 100644
index 0000000000..0d9f0d618c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/example.rb
@@ -0,0 +1,34 @@
+require 'mspec/runner/mspec'
+
+# Holds some of the state of the example (i.e. +it+ block) that is
+# being evaluated. See also +ContextState+.
+class ExampleState
+ attr_reader :context, :it, :example
+
+ def initialize(context, it, example = nil)
+ @context = context
+ @it = it
+ @example = example
+ end
+
+ def context=(context)
+ @description = nil
+ @context = context
+ end
+
+ def describe
+ @context.description
+ end
+
+ def description
+ @description ||= "#{describe} #{@it}"
+ end
+
+ def filtered?
+ incl = MSpec.include
+ excl = MSpec.exclude
+ included = incl.empty? || incl.any? { |f| f === description }
+ included &&= excl.empty? || !excl.any? { |f| f === description }
+ !included
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/exception.rb b/spec/mspec/lib/mspec/runner/exception.rb
new file mode 100644
index 0000000000..23375733e6
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/exception.rb
@@ -0,0 +1,54 @@
+# Initialize $MSPEC_DEBUG
+$MSPEC_DEBUG ||= false
+
+class ExceptionState
+ attr_reader :description, :describe, :it, :exception
+
+ def initialize(state, location, exception)
+ @exception = exception
+ @failure = exception.class == SpecExpectationNotMetError || exception.class == SpecExpectationNotFoundError
+
+ @description = location ? "An exception occurred during: #{location}" : ""
+ if state
+ @description += "\n" unless @description.empty?
+ @description += state.description
+ @describe = state.describe
+ @it = state.it
+ else
+ @describe = @it = ""
+ end
+ end
+
+ def failure?
+ @failure
+ end
+
+ def message
+ message = @exception.message
+ message = "<No message>" if message.empty?
+
+ if @failure
+ message
+ elsif raise_error_message = RaiseErrorMatcher::FAILURE_MESSAGE_FOR_EXCEPTION[@exception]
+ raise_error_message.join("\n")
+ else
+ "#{@exception.class}: #{message}"
+ end
+ end
+
+ def backtrace
+ @backtrace_filter ||= MSpecScript.config[:backtrace_filter] || %r{(?:/bin/mspec|/lib/mspec/)}
+
+ bt = @exception.backtrace || []
+ unless $MSPEC_DEBUG
+ # Exclude <internal: entries inside MSpec code, so only after the first ignored entry
+ first_excluded_line = bt.index { |line| @backtrace_filter =~ line }
+ if first_excluded_line
+ bt = bt[0...first_excluded_line] + bt[first_excluded_line..-1].reject { |line|
+ @backtrace_filter =~ line || /^<internal:/ =~ line
+ }
+ end
+ end
+ bt.join("\n")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters.rb b/spec/mspec/lib/mspec/runner/filters.rb
new file mode 100644
index 0000000000..d0420faca6
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters.rb
@@ -0,0 +1,4 @@
+require 'mspec/runner/filters/match'
+require 'mspec/runner/filters/regexp'
+require 'mspec/runner/filters/tag'
+require 'mspec/runner/filters/profile'
diff --git a/spec/mspec/lib/mspec/runner/filters/match.rb b/spec/mspec/lib/mspec/runner/filters/match.rb
new file mode 100644
index 0000000000..539fd02d01
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/match.rb
@@ -0,0 +1,18 @@
+class MatchFilter
+ def initialize(what, *strings)
+ @what = what
+ @strings = strings
+ end
+
+ def ===(string)
+ @strings.any? { |s| string.include?(s) }
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/profile.rb b/spec/mspec/lib/mspec/runner/filters/profile.rb
new file mode 100644
index 0000000000..a59722c451
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/profile.rb
@@ -0,0 +1,54 @@
+class ProfileFilter
+ def initialize(what, *files)
+ @what = what
+ @methods = load(*files)
+ @pattern = /([^ .#]+[.#])([^ ]+)/
+ end
+
+ def find(name)
+ return name if File.exist?(File.expand_path(name))
+
+ ["spec/profiles", "spec", "profiles", "."].each do |dir|
+ file = File.join dir, name
+ return file if File.exist? file
+ end
+ end
+
+ def parse(file)
+ pattern = /(\S+):\s*/
+ key = ""
+ file.inject(Hash.new { |h,k| h[k] = [] }) do |hash, line|
+ line.chomp!
+ if line[0,2] == "- "
+ hash[key] << line[2..-1].gsub(/[ '"]/, "")
+ elsif m = pattern.match(line)
+ key = m[1]
+ end
+ hash
+ end
+ end
+
+ def load(*files)
+ files.inject({}) do |hash, file|
+ next hash unless name = find(file)
+
+ File.open name, "r" do |f|
+ hash.merge parse(f)
+ end
+ end
+ end
+
+ def ===(string)
+ return false unless m = @pattern.match(string)
+ return false unless l = @methods[m[1]]
+ l.include? m[2]
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/regexp.rb b/spec/mspec/lib/mspec/runner/filters/regexp.rb
new file mode 100644
index 0000000000..097ec6a755
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/regexp.rb
@@ -0,0 +1,23 @@
+class RegexpFilter
+ def initialize(what, *regexps)
+ @what = what
+ @regexps = to_regexp(*regexps)
+ end
+
+ def ===(string)
+ @regexps.any? { |regexp| regexp === string }
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+
+ def to_regexp(*regexps)
+ regexps.map { |str| Regexp.new str }
+ end
+ private :to_regexp
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/tag.rb b/spec/mspec/lib/mspec/runner/filters/tag.rb
new file mode 100644
index 0000000000..c641c01606
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/tag.rb
@@ -0,0 +1,29 @@
+class TagFilter
+ def initialize(what, *tags)
+ @what = what
+ @tags = tags
+ end
+
+ def load
+ @descriptions = MSpec.read_tags(@tags).map { |t| t.description }
+ MSpec.register @what, self
+ end
+
+ def unload
+ MSpec.unregister @what, self
+ end
+
+ def ===(string)
+ @descriptions.include?(string)
+ end
+
+ def register
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ MSpec.unregister :unload, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters.rb b/spec/mspec/lib/mspec/runner/formatters.rb
new file mode 100644
index 0000000000..66f515ddff
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters.rb
@@ -0,0 +1,13 @@
+require 'mspec/runner/formatters/describe'
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/file'
+require 'mspec/runner/formatters/specdoc'
+require 'mspec/runner/formatters/html'
+require 'mspec/runner/formatters/summary'
+require 'mspec/runner/formatters/unit'
+require 'mspec/runner/formatters/spinner'
+require 'mspec/runner/formatters/method'
+require 'mspec/runner/formatters/stats'
+require 'mspec/runner/formatters/yaml'
+require 'mspec/runner/formatters/profile'
+require 'mspec/runner/formatters/junit'
diff --git a/spec/mspec/lib/mspec/runner/formatters/base.rb b/spec/mspec/lib/mspec/runner/formatters/base.rb
new file mode 100644
index 0000000000..882e15c8c2
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/base.rb
@@ -0,0 +1,150 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/actions/tally'
+require 'mspec/utils/options'
+
+if ENV['CHECK_LEAKS']
+ require 'mspec/runner/actions/leakchecker'
+end
+
+if ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS']
+ require 'mspec/runner/actions/constants_leak_checker'
+end
+
+class BaseFormatter
+ attr_reader :exceptions, :timer, :tally
+
+ def initialize(out = nil)
+ @current_state = nil
+ @exception = false
+ @failure = false
+ @exceptions = []
+
+ @count = 0 # For subclasses
+
+ if out
+ @out = File.open out, "w"
+ else
+ @out = $stdout
+ end
+
+ err = MSpecOptions.latest && MSpecOptions.latest.config[:error_output]
+ if err
+ @err = (err == 'stderr') ? $stderr : File.open(err, "w")
+ else
+ @err = @out
+ end
+ end
+
+ # Creates the +TimerAction+ and +TallyAction+ instances and registers them.
+ def register
+ (@timer = TimerAction.new).register
+ (@tally = TallyAction.new).register
+ @counter = @tally.counter
+
+ if ENV['CHECK_LEAKS']
+ LeakCheckerAction.new.register
+ end
+
+ if (ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS']) && ENV['CHECK_CONSTANT_LEAKS'] != 'false'
+ save = ENV['CHECK_LEAKS'] == 'save' || ENV['CHECK_CONSTANT_LEAKS'] == 'save'
+ ConstantsLeakCheckerAction.new(save).register
+ end
+
+ MSpec.register :abort, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :exception, self
+ MSpec.register :finish, self
+ end
+
+ def abort
+ if @current_state
+ puts "\naborting example: #{@current_state.description}"
+ end
+ end
+
+ # Returns true if any exception is raised while running
+ # an example. This flag is reset before each example
+ # is evaluated.
+ def exception?
+ @exception
+ end
+
+ # Returns true if all exceptions during the evaluation
+ # of an example are failures rather than errors. See
+ # <tt>ExceptionState#failure</tt>. This flag is reset
+ # before each example is evaluated.
+ def failure?
+ @failure
+ end
+
+ # Callback for the MSpec :before event. Resets the
+ # +#exception?+ and +#failure+ flags.
+ def before(state = nil)
+ @current_state = state
+ @failure = @exception = false
+ end
+
+ # Callback for the MSpec :exception event. Stores the
+ # +ExceptionState+ object to generate the list of backtraces
+ # after all the specs are run. Also updates the internal
+ # +#exception?+ and +#failure?+ flags.
+ def exception(exception)
+ @count += 1
+ @failure = @exception ? @failure && exception.failure? : exception.failure?
+ @exception = true
+ @exceptions << exception
+ end
+
+ # Callback for the MSpec :after event.
+ def after(state = nil)
+ @current_state = nil
+ end
+
+ # Callback for the MSpec :start event. Calls :after event.
+ # Defined here, in the base class, and used by MultiFormatter.
+ def start
+ after
+ end
+
+ # Callback for the MSpec :unload event. Calls :after event.
+ # Defined here, in the base class, and used by MultiFormatter.
+ def unload
+ after
+ end
+
+ # Callback for the MSpec :finish event. Prints a description
+ # and backtrace for every exception that occurred while
+ # evaluating the examples.
+ def finish
+ print "\n"
+
+ if MSpecOptions.latest && MSpecOptions.latest.config[:print_skips]
+ print "\nSkips:\n" unless MSpec.skips.empty?
+ MSpec.skips.each do |skip, block|
+ print "#{skip.message} in #{(block.source_location || ['?']).join(':')}\n"
+ end
+ end
+
+ count = 0
+ @exceptions.each do |exc|
+ count += 1
+ print_exception(exc, count)
+ end
+ print "\n#{@timer.format}\n\n#{@tally.format}\n"
+ end
+
+ def print_exception(exc, count)
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ @err.print "\n#{count})\n#{exc.description} #{outcome}\n"
+ @err.print exc.message, "\n"
+ @err.print exc.backtrace, "\n"
+ end
+
+ # A convenience method to allow printing to different outputs.
+ def print(*args)
+ @out.print(*args)
+ @out.flush
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/describe.rb b/spec/mspec/lib/mspec/runner/formatters/describe.rb
new file mode 100644
index 0000000000..fc4122d13b
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/describe.rb
@@ -0,0 +1,23 @@
+require 'mspec/runner/formatters/dotted'
+
+class DescribeFormatter < DottedFormatter
+ # Callback for the MSpec :finish event. Prints a summary of
+ # the number of errors and failures for each +describe+ block.
+ def finish
+ describes = Hash.new { |h,k| h[k] = Tally.new }
+
+ @exceptions.each do |exc|
+ desc = describes[exc.describe]
+ exc.failure? ? desc.failures! : desc.errors!
+ end
+
+ print "\n"
+ describes.each do |d, t|
+ text = d.size > 40 ? "#{d[0,37]}..." : d.ljust(40)
+ print "\n#{text} #{t.failure}, #{t.error}"
+ end
+ print "\n" unless describes.empty?
+
+ print "\n#{@timer.format}\n\n#{@tally.format}\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/dotted.rb b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
new file mode 100644
index 0000000000..672cdf81dc
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
@@ -0,0 +1,23 @@
+require 'mspec/runner/formatters/base'
+
+class DottedFormatter < BaseFormatter
+ def register
+ super
+ MSpec.register :after, self
+ end
+
+ # Callback for the MSpec :after event. Prints an indicator
+ # for the result of evaluating this example as follows:
+ # . = No failure or error
+ # F = An SpecExpectationNotMetError was raised
+ # E = Any exception other than SpecExpectationNotMetError
+ def after(state = nil)
+ super(state)
+
+ if exception?
+ print failure? ? "F" : "E"
+ else
+ print "."
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/file.rb b/spec/mspec/lib/mspec/runner/formatters/file.rb
new file mode 100644
index 0000000000..65cfb1f75b
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/file.rb
@@ -0,0 +1,24 @@
+require 'mspec/runner/formatters/dotted'
+
+class FileFormatter < DottedFormatter
+ # Unregisters DottedFormatter#before, #after methods and
+ # registers #load, #unload, which perform the same duties
+ # as #before, #after in DottedFormatter.
+ def register
+ super
+
+ MSpec.unregister :before, self
+ MSpec.unregister :after, self
+
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ def load(state = nil)
+ before(state)
+ end
+
+ def unload(state = nil)
+ after(state)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/html.rb b/spec/mspec/lib/mspec/runner/formatters/html.rb
new file mode 100644
index 0000000000..e37e89a088
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/html.rb
@@ -0,0 +1,81 @@
+require 'mspec/runner/formatters/base'
+
+class HtmlFormatter < BaseFormatter
+ def register
+ super
+ MSpec.register :start, self
+ MSpec.register :enter, self
+ MSpec.register :leave, self
+ end
+
+ def start
+ print <<-EOH
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>Spec Output For #{RUBY_ENGINE} (#{RUBY_VERSION})</title>
+<style type="text/css">
+ul {
+ list-style: none;
+}
+.fail {
+ color: red;
+}
+.pass {
+ color: green;
+}
+#details :target {
+ background-color: #ffffe0;
+}
+</style>
+</head>
+<body>
+EOH
+ end
+
+ def enter(describe)
+ print "<div><p>#{describe}</p>\n<ul>\n"
+ end
+
+ def leave
+ print "</ul>\n</div>\n"
+ end
+
+ def exception(exception)
+ super(exception)
+ outcome = exception.failure? ? "FAILED" : "ERROR"
+ print %[<li class="fail">- #{exception.it} (<a href="#details-#{@count}">]
+ print %[#{outcome} - #{@count}</a>)</li>\n]
+ end
+
+ def after(state = nil)
+ super(state)
+ print %[<li class="pass">- #{state.it}</li>\n] unless exception?
+ end
+
+ def finish
+ success = @exceptions.empty?
+ unless success
+ print "<hr>\n"
+ print %[<ol id="details">]
+ count = 0
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print %[\n<li id="details-#{count += 1}"><p>#{escape(exc.description)} #{outcome}</p>\n<p>]
+ print escape(exc.message)
+ print "</p>\n<pre>\n"
+ print escape(exc.backtrace)
+ print "</pre>\n</li>\n"
+ end
+ print "</ol>\n"
+ end
+ print %[<p>#{@timer.format}</p>\n]
+ print %[<p class="#{success ? "pass" : "fail"}">#{@tally.format}</p>\n]
+ print "</body>\n</html>\n"
+ end
+
+ def escape(string)
+ string.gsub("&", "&nbsp;").gsub("<", "&lt;").gsub(">", "&gt;")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/junit.rb b/spec/mspec/lib/mspec/runner/formatters/junit.rb
new file mode 100644
index 0000000000..6351ccbce9
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/junit.rb
@@ -0,0 +1,87 @@
+require 'mspec/runner/formatters/yaml'
+
+class JUnitFormatter < YamlFormatter
+ def initialize(out = nil)
+ super(out)
+ @tests = []
+ end
+
+ def after(state = nil)
+ super(state)
+ @tests << {:test => state, :exception => false} unless exception?
+ end
+
+ def exception(exception)
+ super(exception)
+ @tests << {:test => exception, :exception => true}
+ end
+
+ def finish
+ switch
+
+ time = @timer.elapsed
+ tests = @tally.counter.examples
+ errors = @tally.counter.errors
+ failures = @tally.counter.failures
+
+ print <<-XML
+
+<?xml version="1.0" encoding="UTF-8" ?>
+ <testsuites
+ testCount="#{tests}"
+ errorCount="#{errors}"
+ failureCount="#{failures}"
+ timeCount="#{time}" time="#{time}">
+ <testsuite
+ tests="#{tests}"
+ errors="#{errors}"
+ failures="#{failures}"
+ time="#{time}"
+ name="Spec Output For #{::RUBY_ENGINE} (#{::RUBY_VERSION})">
+ XML
+ @tests.each do |h|
+ description = encode_for_xml h[:test].description
+
+ print <<-XML
+ <testcase classname="Spec" name="#{description}" time="0.0">
+ XML
+ if h[:exception]
+ outcome = h[:test].failure? ? "failure" : "error"
+ message = encode_for_xml h[:test].message
+ backtrace = encode_for_xml h[:test].backtrace
+ print <<-XML
+ <#{outcome} message="error in #{description}" type="#{outcome}">
+ #{message}
+ #{backtrace}
+ </#{outcome}>
+ XML
+ end
+ print <<-XML
+ </testcase>
+ XML
+ end
+
+ print <<-XML
+ </testsuite>
+ </testsuites>
+ XML
+ end
+
+ private
+ LT = "&lt;"
+ GT = "&gt;"
+ QU = "&quot;"
+ AP = "&apos;"
+ AM = "&amp;"
+ TARGET_ENCODING = "ISO-8859-1"
+
+ def encode_for_xml(str)
+ encode_as_latin1(str).gsub("<", LT).gsub(">", GT).
+ gsub('"', QU).gsub("'", AP).gsub("&", AM).
+ tr("\x00-\x08", "?")
+ end
+
+ def encode_as_latin1(str)
+ str.encode(TARGET_ENCODING, :undef => :replace, :invalid => :replace)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/launchable.rb b/spec/mspec/lib/mspec/runner/formatters/launchable.rb
new file mode 100644
index 0000000000..f738781c71
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/launchable.rb
@@ -0,0 +1,88 @@
+module LaunchableFormatter
+ def self.extend_object(obj)
+ super
+ obj.init
+ end
+
+ def self.setDir(dir)
+ @@path = File.join(dir, "#{rand.to_s}.json")
+ self
+ end
+
+ def init
+ @timer = nil
+ @tests = []
+ end
+
+ def before(state = nil)
+ super
+ @timer = TimerAction.new
+ @timer.start
+ end
+
+ def after(state = nil)
+ super
+ @timer.finish
+ file = MSpec.file
+ return if file.nil? || state&.example.nil? || exception?
+
+ @tests << {:test => state, :file => file, :exception => false, duration: @timer.elapsed}
+ end
+
+ def exception(exception)
+ super
+ @timer.finish
+ file = MSpec.file
+ return if file.nil?
+
+ @tests << {:test => exception, :file => file, :exception => true, duration: @timer.elapsed}
+ end
+
+ def finish
+ super
+
+ require_relative '../../../../../../tool/lib/launchable'
+
+ @writer = writer = Launchable::JsonStreamWriter.new(@@path)
+ @writer.write_array('testCases')
+ at_exit {
+ @writer.close
+ }
+
+ repo_path = File.expand_path("#{__dir__}/../../../../../../")
+
+ @tests.each do |t|
+ testcase = t[:test].description
+ relative_path = t[:file].delete_prefix("#{repo_path}/")
+ # The test path is a URL-encoded representation.
+ # https://github.com/launchableinc/cli/blob/v1.81.0/launchable/testpath.py#L18
+ test_path = {file: relative_path, testcase: testcase}.map{|key, val|
+ "#{encode_test_path_component(key)}=#{encode_test_path_component(val)}"
+ }.join('#')
+
+ status = 'TEST_PASSED'
+ if t[:exception]
+ message = t[:test].message
+ backtrace = t[:test].backtrace
+ e = "#{message}\n#{backtrace}"
+ status = 'TEST_FAILED'
+ end
+
+ @writer.write_object(
+ {
+ testPath: test_path,
+ status: status,
+ duration: t[:duration],
+ createdAt: Time.now.to_s,
+ stderr: e,
+ stdout: nil
+ }
+ )
+ end
+ end
+
+ private
+ def encode_test_path_component component
+ component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26').tr("\x00-\x08", "")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/method.rb b/spec/mspec/lib/mspec/runner/formatters/method.rb
new file mode 100644
index 0000000000..925858c845
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/method.rb
@@ -0,0 +1,95 @@
+require 'mspec/runner/formatters/base'
+
+class MethodFormatter < BaseFormatter
+ attr_accessor :methods
+
+ def initialize(out = nil)
+ super(out)
+ @methods = Hash.new do |h, k|
+ h[k] = {
+ examples: 0,
+ expectations: 0,
+ failures: 0,
+ errors: 0,
+ exceptions: []
+ }
+ end
+ end
+
+ # Returns the type of method as a "class", "instance",
+ # or "unknown".
+ def method_type(sep)
+ case sep
+ when '.', '::'
+ "class"
+ when '#'
+ "instance"
+ else
+ "unknown"
+ end
+ end
+
+ # Callback for the MSpec :before event. Parses the
+ # describe string into class and method if possible.
+ # Resets the tallies so the counts are only for this
+ # example.
+ def before(state)
+ super(state)
+
+ # The pattern for a method name is not correctly
+ # restrictive but it is simplistic and useful
+ # for our purpose.
+ /^([A-Za-z_]+\w*)(\.|#|::)([^ ]+)/ =~ state.describe
+ @key = $1 && $2 && $3 ? "#{$1}#{$2}#{$3}" : state.describe
+
+ unless methods.key? @key
+ h = methods[@key]
+ h[:class] = "#{$1}"
+ h[:method] = "#{$3}"
+ h[:type] = method_type $2
+ h[:description] = state.description
+ end
+
+ tally.counter.examples = 0
+ tally.counter.expectations = 0
+ tally.counter.failures = 0
+ tally.counter.errors = 0
+
+ @exceptions = []
+ end
+
+ # Callback for the MSpec :after event. Sets or adds to
+ # tallies for the example block.
+ def after(state = nil)
+ super(state)
+
+ h = methods[@key]
+ h[:examples] += tally.counter.examples
+ h[:expectations] += tally.counter.expectations
+ h[:failures] += tally.counter.failures
+ h[:errors] += tally.counter.errors
+ @exceptions.each do |exc|
+ h[:exceptions] << "#{exc.message}\n#{exc.backtrace}\n"
+ end
+ end
+
+ # Callback for the MSpec :finish event. Prints out the
+ # summary information in YAML format for all the methods.
+ def finish
+ print "---\n"
+
+ methods.each do |key, hash|
+ print key.inspect, ":\n"
+ print " class: ", hash[:class].inspect, "\n"
+ print " method: ", hash[:method].inspect, "\n"
+ print " type: ", hash[:type], "\n"
+ print " description: ", hash[:description].inspect, "\n"
+ print " examples: ", hash[:examples], "\n"
+ print " expectations: ", hash[:expectations], "\n"
+ print " failures: ", hash[:failures], "\n"
+ print " errors: ", hash[:errors], "\n"
+ print " exceptions:\n"
+ hash[:exceptions].each { |exc| print " - ", exc.inspect, "\n" }
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/multi.rb b/spec/mspec/lib/mspec/runner/formatters/multi.rb
new file mode 100644
index 0000000000..fa1da3766b
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/multi.rb
@@ -0,0 +1,47 @@
+module MultiFormatter
+ def self.extend_object(obj)
+ super
+ obj.multi_initialize
+ end
+
+ def multi_initialize
+ @tally = TallyAction.new
+ @counter = @tally.counter
+ @timer = TimerAction.new
+ @timer.start
+ end
+
+ def register
+ super
+
+ MSpec.register :start, self
+ MSpec.register :unload, self
+ MSpec.unregister :before, self
+ end
+
+ def aggregate_results(files)
+ require 'yaml'
+
+ @timer.finish
+ @exceptions = []
+
+ files.each do |file|
+ contents = File.read(file)
+ d = YAML.load(contents)
+ File.delete file
+
+ if d # The file might be empty if the child process died
+ @exceptions += Array(d['exceptions'])
+ @counter.files! d['files']
+ @counter.examples! d['examples']
+ @counter.expectations! d['expectations']
+ @counter.errors! d['errors']
+ @counter.failures! d['failures']
+ end
+ end
+ end
+
+ def print_exception(exc, count)
+ @err.print "\n#{count})\n#{exc}\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/profile.rb b/spec/mspec/lib/mspec/runner/formatters/profile.rb
new file mode 100644
index 0000000000..38ef5b12ed
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/profile.rb
@@ -0,0 +1,18 @@
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/actions/profile'
+
+class ProfileFormatter < DottedFormatter
+ def initialize(out = nil)
+ super(out)
+
+ @describe_name = nil
+ @describe_time = nil
+ @describes = []
+ @its = []
+ end
+
+ def register
+ (@profile = ProfileAction.new).register
+ super
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/specdoc.rb b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
new file mode 100644
index 0000000000..d3a5c3d729
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
@@ -0,0 +1,41 @@
+require 'mspec/runner/formatters/base'
+
+class SpecdocFormatter < BaseFormatter
+ def register
+ super
+ MSpec.register :enter, self
+ end
+
+ # Callback for the MSpec :enter event. Prints the
+ # +describe+ block string.
+ def enter(describe)
+ print "\n#{describe}\n"
+ end
+
+ # Callback for the MSpec :before event. Prints the
+ # +it+ block string.
+ def before(state)
+ super(state)
+ print "- #{state.it}"
+ end
+
+ # Callback for the MSpec :exception event. Prints
+ # either 'ERROR - X' or 'FAILED - X' where _X_ is
+ # the sequential number of the exception raised. If
+ # there has already been an exception raised while
+ # evaluating this example, it prints another +it+
+ # block description string so that each description
+ # string has an associated 'ERROR' or 'FAILED'
+ def exception(exception)
+ print "\n- #{exception.it}" if exception?
+ super(exception)
+ print " (#{exception.failure? ? 'FAILED' : 'ERROR'} - #{@count})"
+ end
+
+ # Callback for the MSpec :after event. Prints a
+ # newline to finish the description string output.
+ def after(state = nil)
+ super(state)
+ print "\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/spinner.rb b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
new file mode 100644
index 0000000000..817d8c02be
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
@@ -0,0 +1,111 @@
+require 'mspec/runner/formatters/base'
+
+class SpinnerFormatter < BaseFormatter
+ attr_reader :length
+
+ Spins = %w!| / - \\!
+ HOUR = 3600
+ MIN = 60
+
+ def initialize(out = nil)
+ super(nil)
+
+ @which = 0
+ @loaded = 0
+ self.length = 40
+ @percent = 0
+ @start = Time.now
+
+ term = ENV['TERM']
+ @color = (term != "dumb")
+ @fail_color = "32"
+ @error_color = "32"
+ end
+
+ def register
+ super
+
+ MSpec.register :start, self
+ MSpec.register :unload, self
+ end
+
+ def length=(length)
+ @length = length
+ @ratio = 100.0 / length
+ @position = length / 2 - 2
+ end
+
+ def compute_etr
+ return @etr = "00:00:00" if @percent == 0
+ elapsed = Time.now - @start
+ remain = (100 * elapsed / @percent) - elapsed
+
+ hour = remain >= HOUR ? (remain / HOUR).to_i : 0
+ remain -= hour * HOUR
+ min = remain >= MIN ? (remain / MIN).to_i : 0
+ sec = remain - min * MIN
+
+ @etr = "%02d:%02d:%02d" % [hour, min, sec]
+ end
+
+ def compute_percentage
+ @percent = @loaded * 100 / @total
+ bar = ("=" * (@percent / @ratio)).ljust @length
+ label = "%d%%" % @percent
+ bar[@position, label.size] = label
+ @bar = bar
+ end
+
+ def compute_progress
+ compute_percentage
+ compute_etr
+ end
+
+ def progress_line
+ @which = (@which + 1) % Spins.size
+ data = [Spins[@which], @bar, @etr, @counter.failures, @counter.errors]
+ if @color
+ "\r[%s | %s | %s] \e[0;#{@fail_color}m%6dF \e[0;#{@error_color}m%6dE\e[0m " % data
+ else
+ "\r[%s | %s | %s] %6dF %6dE " % data
+ end
+ end
+
+ def clear_progress_line
+ print "\r#{' '*progress_line.length}"
+ end
+
+ # Callback for the MSpec :start event. Stores the total
+ # number of files that will be processed.
+ def start
+ @total = MSpec.files_array.size
+ compute_progress
+ print progress_line
+ end
+
+ # Callback for the MSpec :unload event. Increments the number
+ # of files that have been run.
+ def unload
+ @loaded += 1
+ compute_progress
+ print progress_line
+ end
+
+ # Callback for the MSpec :exception event. Changes the color
+ # used to display the tally of errors and failures
+ def exception(exception)
+ super
+ @fail_color = "31" if exception.failure?
+ @error_color = "33" unless exception.failure?
+
+ clear_progress_line
+ print_exception(exception, @count)
+ exceptions.clear
+ end
+
+ # Callback for the MSpec :after event. Updates the spinner.
+ def after(state = nil)
+ super(state)
+ print progress_line
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/stats.rb b/spec/mspec/lib/mspec/runner/formatters/stats.rb
new file mode 100644
index 0000000000..8cff96d145
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/stats.rb
@@ -0,0 +1,57 @@
+require 'mspec/runner/formatters/base'
+
+class StatsPerFileFormatter < BaseFormatter
+ def initialize(out = nil)
+ super(out)
+ @data = {}
+ @root = File.expand_path(MSpecScript.get(:prefix) || '.')
+ end
+
+ def register
+ super
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ # Resets the tallies so the counts are only for this file.
+ def load
+ tally.counter.examples = 0
+ tally.counter.errors = 0
+ tally.counter.failures = 0
+ tally.counter.tagged = 0
+ end
+
+ def unload
+ file = format_file MSpec.file
+
+ raise if @data.key?(file)
+ @data[file] = {
+ examples: tally.counter.examples,
+ errors: tally.counter.errors,
+ failures: tally.counter.failures,
+ tagged: tally.counter.tagged,
+ }
+ end
+
+ def finish
+ width = @data.keys.max_by(&:size).size
+ f = "%3d"
+ @data.each_pair do |file, data|
+ total = data[:examples]
+ passing = total - data[:errors] - data[:failures] - data[:tagged]
+ puts "#{file.ljust(width)} #{f % passing}/#{f % total}"
+ end
+
+ require 'yaml'
+ yaml = YAML.dump(@data)
+ File.write "results-#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}.yml", yaml
+ end
+
+ private def format_file(file)
+ if file.start_with?(@root)
+ file[@root.size+1..-1]
+ else
+ raise file
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/summary.rb b/spec/mspec/lib/mspec/runner/formatters/summary.rb
new file mode 100644
index 0000000000..41819d2158
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/summary.rb
@@ -0,0 +1,4 @@
+require 'mspec/runner/formatters/base'
+
+class SummaryFormatter < BaseFormatter
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/unit.rb b/spec/mspec/lib/mspec/runner/formatters/unit.rb
new file mode 100644
index 0000000000..d03ae79e9f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/unit.rb
@@ -0,0 +1,20 @@
+require 'mspec/runner/formatters/dotted'
+
+class UnitdiffFormatter < DottedFormatter
+ def finish
+ print "\n\n#{@timer.format}\n"
+ count = 0
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print "\n#{count += 1})\n#{exc.description} #{outcome}\n"
+ print exc.message, ":\n"
+ print exc.backtrace, "\n"
+ end
+ print "\n#{@tally.format}\n"
+ end
+
+ def backtrace(exc)
+ exc.backtrace && exc.backtrace.join("\n")
+ end
+ private :backtrace
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/yaml.rb b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
new file mode 100644
index 0000000000..6c05cc902f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
@@ -0,0 +1,38 @@
+require 'mspec/runner/formatters/base'
+
+class YamlFormatter < BaseFormatter
+ def initialize(out = nil)
+ super(nil)
+
+ if out.nil?
+ @finish = $stdout
+ else
+ @finish = File.open out, "w"
+ end
+ end
+
+ def switch
+ @out = @finish
+ end
+
+ def finish
+ switch
+
+ print "---\n"
+ print "exceptions:\n"
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ str = "#{exc.description} #{outcome}\n"
+ str << exc.message << "\n" << exc.backtrace
+ print "- ", str.inspect, "\n"
+ end
+
+ print "time: ", @timer.elapsed, "\n"
+ print "files: ", @tally.counter.files, "\n"
+ print "examples: ", @tally.counter.examples, "\n"
+ print "expectations: ", @tally.counter.expectations, "\n"
+ print "failures: ", @tally.counter.failures, "\n"
+ print "errors: ", @tally.counter.errors, "\n"
+ print "tagged: ", @tally.counter.tagged, "\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/mspec.rb b/spec/mspec/lib/mspec/runner/mspec.rb
new file mode 100644
index 0000000000..0e016c67a7
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/mspec.rb
@@ -0,0 +1,424 @@
+require 'mspec/runner/context'
+require 'mspec/runner/exception'
+require 'mspec/runner/tag'
+
+module MSpec
+end
+
+class MSpecEnv
+ include MSpec
+end
+
+module MSpec
+ @exit = nil
+ @abort = nil
+ @start = nil
+ @enter = nil
+ @before = nil
+ @add = nil
+ @after = nil
+ @leave = nil
+ @finish = nil
+ @exclude = []
+ @include = []
+ @leave = nil
+ @load = nil
+ @unload = nil
+ @tagged = nil
+ @current = nil
+ @passed = nil
+ @example = nil
+ @modes = []
+ @shared = {}
+ @guarded = []
+ @features = {}
+ @exception = nil
+ @randomize = false
+ @repeat = 1
+ @expectation = nil
+ @expectations = false
+ @skips = []
+ @subprocesses = []
+
+ class << self
+ attr_reader :file, :include, :exclude, :skips, :subprocesses
+ attr_writer :repeat, :randomize
+ attr_accessor :formatter
+ end
+
+ def self.describe(description, options = nil, &block)
+ state = ContextState.new description, options
+ state.parent = current
+
+ MSpec.register_current state
+ state.describe(&block)
+
+ state.process unless state.shared? or current
+ end
+
+ def self.process
+ STDOUT.puts RUBY_DESCRIPTION
+ STDOUT.flush
+
+ actions :start
+ files
+ actions :finish
+ end
+
+ def self.files_array
+ @files
+ end
+
+ def self.each_file(&block)
+ if ENV["MSPEC_MULTI"]
+ while file = STDIN.gets
+ file = file.chomp
+ return if file == "QUIT"
+ yield file
+ begin
+ STDOUT.print "."
+ STDOUT.flush
+ rescue Errno::EPIPE
+ # The parent died
+ exit 1
+ end
+ end
+ # The parent closed the connection without QUIT
+ abort "the parent did not send QUIT"
+ else
+ return unless files = @files
+ shuffle files if randomize?
+ files.each(&block)
+ end
+ end
+
+ def self.files
+ each_file do |file|
+ setup_env
+ @file = file
+ actions :load
+ protect("loading #{file}") { Kernel.load file }
+ actions :unload
+ raise "#{file} was executed but did not reset the current example: #{@current}" if @current
+ end
+ end
+
+ def self.setup_env
+ @env = MSpecEnv.new
+ end
+
+ def self.actions(action, *args)
+ actions = retrieve(action)
+ actions.each { |obj| obj.send action, *args } if actions
+ end
+
+ def self.protect(location, &block)
+ begin
+ @env.instance_exec(&block)
+ return true
+ rescue SystemExit => e
+ raise e
+ rescue SkippedSpecError => e
+ @skips << [e, block]
+ return false
+ rescue Object => exc
+ register_exit 1
+ actions :exception, ExceptionState.new(current && current.state, location, exc)
+ return false
+ end
+ end
+
+ # Guards can be nested, so a stack is necessary to know when we have
+ # exited the toplevel guard.
+ def self.guard
+ @guarded << true
+ end
+
+ def self.unguard
+ @guarded.pop
+ end
+
+ def self.guarded?
+ !@guarded.empty?
+ end
+
+ # Sets the toplevel ContextState to +state+.
+ def self.register_current(state)
+ @current = state
+ end
+
+ # Sets the toplevel ContextState to +nil+.
+ def self.clear_current
+ @current = nil
+ end
+
+ # Returns the toplevel ContextState.
+ def self.current
+ @current
+ end
+
+ # Stores the shared ContextState keyed by description.
+ def self.register_shared(state)
+ name = state.to_s
+ raise "duplicated shared #describe: #{name}" if @shared.key?(name)
+ @shared[name] = state
+ end
+
+ # Returns the shared ContextState matching description.
+ def self.retrieve_shared(desc)
+ @shared[desc.to_s]
+ end
+
+ # Stores the exit code used by the runner scripts.
+ def self.register_exit(code)
+ @exit = code
+ end
+
+ # Retrieves the stored exit code.
+ def self.exit_code
+ @exit.to_i
+ end
+
+ # Stores the list of files to be evaluated.
+ def self.register_files(files)
+ @files = files
+ end
+
+ # Stores one or more substitution patterns for transforming
+ # a spec filename into a tags filename, where each pattern
+ # has the form:
+ #
+ # [Regexp, String]
+ #
+ # See also +tags_file+.
+ def self.register_tags_patterns(patterns)
+ @tags_patterns = patterns
+ end
+
+ # Registers an operating mode. Modes recognized by MSpec:
+ #
+ # :pretend - actions execute but specs are not run
+ # :verify - specs are run despite guards and the result is
+ # verified to match the expectation of the guard
+ # :report - specs that are guarded are reported
+ # :unguarded - all guards are forced off
+ def self.register_mode(mode)
+ modes = @modes
+ modes << mode unless modes.include? mode
+ end
+
+ # Clears all registered modes.
+ def self.clear_modes
+ @modes = []
+ end
+
+ # Returns +true+ if +mode+ is registered.
+ def self.mode?(mode)
+ @modes.include? mode
+ end
+
+ def self.enable_feature(feature)
+ @features[feature] = true
+ end
+
+ def self.disable_feature(feature)
+ @features[feature] = false
+ end
+
+ def self.feature_enabled?(feature)
+ @features[feature] || false
+ end
+
+ def self.retrieve(symbol)
+ instance_variable_get :"@#{symbol}"
+ end
+
+ def self.store(symbol, value)
+ instance_variable_set :"@#{symbol}", value
+ end
+
+ # This method is used for registering actions that are
+ # run at particular points in the spec cycle:
+ # :start before any specs are run
+ # :load before a spec file is loaded
+ # :enter before a describe block is run
+ # :before before a single spec is run
+ # :add while a describe block is adding examples to run later
+ # :expectation before a 'should', 'should_receive', etc.
+ # :passed after an example block is run and passes, passed the block, run before :example action
+ # :example after an example block is run, passed the block
+ # :exception after an exception is rescued
+ # :after after a single spec is run
+ # :leave after a describe block is run
+ # :unload after a spec file is run
+ # :finish after all specs are run
+ #
+ # Objects registered as actions above should respond to
+ # a method of the same name. For example, if an object
+ # is registered as a :start action, it should respond to
+ # a #start method call.
+ #
+ # Additionally, there are two "action" lists for
+ # filtering specs:
+ # :include return true if the spec should be run
+ # :exclude return true if the spec should NOT be run
+ #
+ def self.register(symbol, action)
+ unless value = retrieve(symbol)
+ value = store symbol, []
+ end
+ value << action unless value.include? action
+ end
+
+ def self.unregister(symbol, action)
+ if value = retrieve(symbol)
+ value.delete action
+ end
+ end
+
+ def self.randomize?
+ @randomize
+ end
+
+ def self.repeat
+ if @repeat == 1
+ yield
+ else
+ @repeat.times do
+ yield
+ end
+ end
+ end
+
+ def self.shuffle(ary)
+ return if ary.empty?
+
+ size = ary.size
+ size.times do |i|
+ r = rand(size - i - 1)
+ ary[i], ary[r] = ary[r], ary[i]
+ end
+ end
+
+ # Records that an expectation has been encountered in an example.
+ def self.expectation
+ @expectations = true
+ end
+
+ # Returns true if an expectation has been encountered
+ def self.expectation?
+ @expectations
+ end
+
+ # Resets the flag that an expectation has been encountered in an example.
+ def self.clear_expectations
+ @expectations = false
+ end
+
+ # Transforms a spec filename into a tags filename by applying each
+ # substitution pattern in :tags_pattern. The default patterns are:
+ #
+ # [%r(/spec/), '/spec/tags/'], [/_spec.rb$/, '_tags.txt']
+ #
+ # which will perform the following transformation:
+ #
+ # path/to/spec/class/method_spec.rb => path/to/spec/tags/class/method_tags.txt
+ #
+ # See also +register_tags_patterns+.
+ def self.tags_file
+ patterns = @tags_patterns ||
+ [[%r(spec/), 'spec/tags/'], [/_spec.rb$/, '_tags.txt']]
+ patterns.inject(@file.dup) do |file, pattern|
+ file.gsub(*pattern)
+ end
+ end
+
+ # Returns a list of tags matching any tag string in +keys+ based
+ # on the return value of <tt>keys.include?("tag_name")</tt>
+ def self.read_tags(keys)
+ tags = []
+ file = tags_file
+ if File.exist? file
+ File.open(file, "r:utf-8") do |f|
+ f.each_line do |line|
+ line.chomp!
+ next if line.empty?
+ tag = SpecTag.new line
+ tags << tag if keys.include? tag.tag
+ end
+ end
+ end
+ tags
+ end
+
+ def self.make_tag_dir(path)
+ parent = File.dirname(path)
+ return if File.exist? parent
+ begin
+ Dir.mkdir(parent)
+ rescue SystemCallError
+ make_tag_dir(parent)
+ Dir.mkdir(parent)
+ end
+ end
+
+ # Writes each tag in +tags+ to the tag file. Overwrites the
+ # tag file if it exists.
+ def self.write_tags(tags)
+ return delete_tags if tags.empty?
+ file = tags_file
+ make_tag_dir(file)
+ File.open(file, "w:utf-8") do |f|
+ tags.each { |t| f.puts t }
+ end
+ end
+
+ # Writes +tag+ to the tag file if it does not already exist.
+ # Returns +true+ if the tag is written, +false+ otherwise.
+ def self.write_tag(tag)
+ tags = read_tags([tag.tag])
+ tags.each do |t|
+ if t.tag == tag.tag and t.description == tag.description
+ return false
+ end
+ end
+
+ file = tags_file
+ make_tag_dir(file)
+ File.open(file, "a:utf-8") { |f| f.puts tag.to_s }
+ return true
+ end
+
+ # Deletes +tag+ from the tag file if it exists. Returns +true+
+ # if the tag is deleted, +false+ otherwise. Deletes the tag
+ # file if it is empty.
+ def self.delete_tag(tag)
+ deleted = false
+ desc = tag.escape(tag.description)
+ file = tags_file
+ if File.exist? file
+ lines = File.readlines(file)
+ File.open(file, "w:utf-8") do |f|
+ lines.each do |line|
+ line = line.chomp
+ if line.start_with?(tag.tag) and line.end_with?(desc)
+ deleted = true
+ else
+ f.puts line unless line.empty?
+ end
+ end
+ end
+ File.delete file unless File.size? file
+ end
+ return deleted
+ end
+
+ # Removes the tag file associated with a spec file.
+ def self.delete_tags
+ file = tags_file
+ File.delete file if File.exist? file
+ end
+
+ # Initialize @env
+ setup_env
+end
diff --git a/spec/mspec/lib/mspec/runner/object.rb b/spec/mspec/lib/mspec/runner/object.rb
new file mode 100644
index 0000000000..58d98cc4df
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/object.rb
@@ -0,0 +1,26 @@
+class Object
+ private def before(at = :each, &block)
+ MSpec.current.before at, &block
+ end
+
+ private def after(at = :each, &block)
+ MSpec.current.after at, &block
+ end
+
+ private def describe(description, options = nil, &block)
+ MSpec.describe description, options, &block
+ end
+
+ private def it(desc, &block)
+ MSpec.current.it desc, &block
+ end
+
+ private def it_should_behave_like(desc)
+ MSpec.current.it_should_behave_like desc
+ end
+
+ alias_method :context, :describe
+ private :context
+ alias_method :specify, :it
+ private :specify
+end
diff --git a/spec/mspec/lib/mspec/runner/parallel.rb b/spec/mspec/lib/mspec/runner/parallel.rb
new file mode 100644
index 0000000000..6a9ecd155d
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/parallel.rb
@@ -0,0 +1,98 @@
+class ParallelRunner
+ def initialize(files, processes, formatter, argv)
+ @files = files
+ @processes = processes
+ @formatter = formatter
+ @argv = argv
+ @last_files = {}
+ @output_files = []
+ @success = true
+ end
+
+ def launch_children
+ @children = @processes.times.map { |i|
+ name = tmp "mspec-multi-#{i}"
+ @output_files << name
+
+ env = {
+ "SPEC_TEMP_DIR" => "#{SPEC_TEMP_DIR}_#{i}",
+ "MSPEC_MULTI" => i.to_s
+ }
+ command = @argv + ["-fy", "-o", name]
+ $stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG
+ IO.popen([env, *command, close_others: false], "rb+")
+ }
+ end
+
+ def handle(child, message)
+ case message
+ when '.'
+ @formatter.unload
+ send_new_file_or_quit(child)
+ else
+ if message == nil
+ msg = "A child mspec-run process died unexpectedly"
+ else
+ msg = "A child mspec-run process printed unexpected output on STDOUT"
+ while chunk = (child.read_nonblock(4096) rescue nil)
+ message += chunk
+ end
+ message.chomp!('.')
+ msg += ": #{message.inspect}"
+ end
+
+ if last_file = @last_files[child]
+ msg += " while running #{last_file}"
+ end
+
+ @success = false
+ quit(child)
+ abort "\n#{msg}"
+ end
+ end
+
+ def quit(child)
+ begin
+ child.puts "QUIT"
+ rescue Errno::EPIPE
+ # The child process already died
+ end
+ _pid, status = Process.wait2(child.pid)
+ @success &&= status.success?
+ child.close
+ @children.delete(child)
+ end
+
+ def send_new_file_or_quit(child)
+ if @files.empty?
+ quit(child)
+ else
+ file = @files.shift
+ @last_files[child] = file
+ child.puts file
+ end
+ end
+
+ def run
+ MSpec.register_files @files
+ launch_children
+
+ puts @children.map { |child| child.gets }.uniq
+ @formatter.start
+ begin
+ @children.each { |child| send_new_file_or_quit(child) }
+
+ until @children.empty?
+ IO.select(@children)[0].each { |child|
+ handle(child, child.read(1))
+ }
+ end
+ ensure
+ @children.dup.each { |child| quit(child) }
+ @formatter.aggregate_results(@output_files)
+ @formatter.finish
+ end
+
+ @success
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/shared.rb b/spec/mspec/lib/mspec/runner/shared.rb
new file mode 100644
index 0000000000..283711c1d7
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/shared.rb
@@ -0,0 +1,14 @@
+require 'mspec/runner/mspec'
+
+def it_behaves_like(desc, meth, obj = nil)
+ before :all do
+ @method = meth
+ @object = obj
+ end
+ after :all do
+ @method = nil
+ @object = nil
+ end
+
+ it_should_behave_like desc.to_s
+end
diff --git a/spec/mspec/lib/mspec/runner/tag.rb b/spec/mspec/lib/mspec/runner/tag.rb
new file mode 100644
index 0000000000..820df9159e
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/tag.rb
@@ -0,0 +1,38 @@
+class SpecTag
+ attr_accessor :tag, :comment, :description
+
+ def initialize(string = nil)
+ parse(string) if string
+ end
+
+ def parse(string)
+ m = /^([^()#:]+)(\(([^)]+)?\))?:(.*)$/.match string
+ @tag, @comment, description = m.values_at(1, 3, 4) if m
+ @description = unescape description
+ end
+
+ def unescape(str)
+ return unless str
+ if str[0] == ?" and str[-1] == ?"
+ str[1..-2].gsub('\n', "\n")
+ else
+ str
+ end
+ end
+
+ def escape(str)
+ if str.include? "\n"
+ %["#{str.gsub("\n", '\n')}"]
+ else
+ str
+ end
+ end
+
+ def to_s
+ "#{@tag}#{ "(#{@comment})" if @comment }:#{escape @description}"
+ end
+
+ def ==(o)
+ @tag == o.tag and @comment == o.comment and @description == o.description
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/deprecate.rb b/spec/mspec/lib/mspec/utils/deprecate.rb
new file mode 100644
index 0000000000..1db843b329
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/deprecate.rb
@@ -0,0 +1,6 @@
+module MSpec
+ def self.deprecate(what, replacement)
+ user_caller = caller.find { |line| !line.include?('lib/mspec') }
+ $stderr.puts "\n#{what} is deprecated, use #{replacement} instead.\nfrom #{user_caller}"
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/format.rb b/spec/mspec/lib/mspec/utils/format.rb
new file mode 100644
index 0000000000..425dd4d11c
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/format.rb
@@ -0,0 +1,24 @@
+# If the implementation on which the specs are run cannot
+# load pp from the standard library, add a pp.rb file that
+# defines the #pretty_inspect method on Object or Kernel.
+begin
+ require 'pp'
+rescue LoadError
+ module Kernel
+ def pretty_inspect
+ inspect
+ end
+ end
+end
+
+module MSpec
+ def self.format(obj)
+ if String === obj and obj.include?("\n")
+ "\n#{obj.inspect.gsub('\n', "\n")}"
+ else
+ obj.pretty_inspect.chomp
+ end
+ rescue => e
+ "#<#{obj.class}>(#pretty_inspect raised #{e.inspect})"
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/name_map.rb b/spec/mspec/lib/mspec/utils/name_map.rb
new file mode 100644
index 0000000000..9b04112e2e
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/name_map.rb
@@ -0,0 +1,133 @@
+class NameMap
+ MAP = {
+ '`' => 'backtick',
+ '+' => 'plus',
+ '-' => 'minus',
+ '+@' => 'uplus',
+ '-@' => 'uminus',
+ '*' => 'multiply',
+ '/' => 'divide',
+ '%' => 'modulo',
+ '<<' => {'Integer' => 'left_shift',
+ 'IO' => 'output',
+ :default => 'append' },
+ '>>' => 'right_shift',
+ '<' => 'lt',
+ '<=' => 'lte',
+ '>' => 'gt',
+ '>=' => 'gte',
+ '=' => 'assignment',
+ '==' => 'equal_value',
+ '===' => 'case_compare',
+ '<=>' => 'comparison',
+ '[]' => 'element_reference',
+ '[]=' => 'element_set',
+ '**' => 'exponent',
+ '!' => 'not',
+ '~' => {'Integer' => 'complement',
+ :default => 'match' },
+ '!=' => 'not_equal',
+ '!~' => 'not_match',
+ '=~' => 'match',
+ '&' => {'Integer' => 'bit_and',
+ 'Array' => 'intersection',
+ 'Set' => 'intersection',
+ :default => 'and' },
+ '|' => {'Integer' => 'bit_or',
+ 'Array' => 'union',
+ 'Set' => 'union',
+ :default => 'or' },
+ '^' => {'Integer' => 'bit_xor',
+ 'Set' => 'exclusion',
+ :default => 'xor' },
+ }
+
+ EXCLUDED = %w[
+ MSpecScript
+ MkSpec
+ MSpecOption
+ MSpecOptions
+ NameMap
+ SpecVersion
+ ]
+
+ ALWAYS_PRIVATE = %w[
+ initialize initialize_copy initialize_clone initialize_dup respond_to_missing?
+ ].map(&:to_sym)
+
+ def initialize(filter = false)
+ @seen = {}
+ @filter = filter
+ end
+
+ def exception?(name)
+ return false unless c = class_or_module(name)
+ c == Errno or c.ancestors.include? Exception
+ end
+
+ def class_or_module(c)
+ begin
+ const = Object.const_get(c, false)
+ rescue NameError, RuntimeError
+ # Either the constant doesn't exist or it is
+ # explicitly raising an error, like `SortedSet`.
+ return nil
+ end
+ return nil unless Module === const
+
+ filtered = @filter && EXCLUDED.include?(const.name)
+ return const unless filtered
+ end
+
+ def namespace(mod, const)
+ return const.to_s if mod.nil? or %w[Object Class Module].include? mod
+ "#{mod}::#{const}"
+ end
+
+ def map(hash, constants, mod = nil)
+ @seen = {} unless mod
+
+ constants.each do |const|
+ name = namespace mod, const
+ m = class_or_module name
+ next unless m and !@seen[m]
+ @seen[m] = true
+
+ ms = m.methods(false).map { |x| x.to_s }
+ hash["#{name}."] = ms.sort unless ms.empty?
+
+ ms = m.public_instance_methods(false) +
+ m.protected_instance_methods(false) +
+ (m.private_instance_methods(false) & ALWAYS_PRIVATE)
+ ms.map! { |x| x.to_s }
+ hash["#{name}#"] = ms.sort unless ms.empty?
+
+ map hash, m.constants(false), name
+ end
+
+ hash
+ end
+
+ def dir_name(c, base)
+ return File.join(base, 'exception') if exception? c
+
+ c.split('::').inject(base) do |dir, name|
+ name.gsub!(/Class/, '') unless name == 'Class'
+ File.join dir, name.downcase
+ end
+ end
+
+ def file_name(m, c)
+ if MAP.key?(m)
+ mapping = MAP[m]
+ if mapping.is_a?(Hash)
+ name = mapping[c.split('::').last] || mapping.fetch(:default)
+ else
+ name = mapping
+ end
+ else
+ name = m.gsub(/[?!=]/, '')
+ end
+ "#{name}_spec.rb"
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/options.rb b/spec/mspec/lib/mspec/utils/options.rb
new file mode 100644
index 0000000000..adeafa1f81
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/options.rb
@@ -0,0 +1,521 @@
+require 'mspec/version'
+
+MSPEC_HOME = File.expand_path('../../../..', __FILE__)
+
+class MSpecOption
+ attr_reader :short, :long, :arg, :description, :block
+
+ def initialize(short, long, arg, description, block)
+ @short = short
+ @long = long
+ @arg = arg
+ @description = description
+ @block = block
+ end
+
+ def arg?
+ @arg != nil
+ end
+
+ def match?(opt)
+ opt == @short or opt == @long
+ end
+end
+
+# MSpecOptions provides a parser for command line options. It also
+# provides a composable set of options from which the runner scripts
+# can select for their particular functionality.
+class MSpecOptions
+ # Raised if incorrect or incomplete formats are passed to #on.
+ class OptionError < Exception; end
+
+ # Raised if an unrecognized option is encountered.
+ class ParseError < Exception; end
+
+ class << self
+ attr_accessor :latest
+ end
+
+ attr_accessor :config, :banner, :width, :options
+
+ def initialize(banner = "", width = 30, config = nil)
+ @banner = banner
+ @config = config
+ @width = width
+ @options = []
+ @doc = []
+ @extra = []
+ @on_extra = lambda { |x|
+ raise ParseError, "Unrecognized option: #{x}" if x[0] == ?-
+ @extra << x
+ }
+
+ MSpecOptions.latest = self
+ end
+
+ # Registers an option. Acceptable formats for arguments are:
+ #
+ # on "-a", "description"
+ # on "-a", "--abdc", "description"
+ # on "-a", "ARG", "description"
+ # on "--abdc", "ARG", "description"
+ # on "-a", "--abdc", "ARG", "description"
+ #
+ # If an block is passed, it will be invoked when the option is
+ # matched. Not passing a block is permitted, but nonsensical.
+ def on(*args, &block)
+ raise OptionError, "option and description are required" if args.size < 2
+
+ description = args.pop
+ short, long, argument = nil
+ args.each do |arg|
+ if arg[0] == ?-
+ if arg[1] == ?-
+ long = arg
+ else
+ short = arg
+ end
+ else
+ argument = arg
+ end
+ end
+
+ add short, long, argument, description, block
+ end
+
+ # Adds documentation text for an option and adds an +MSpecOption+
+ # instance to the list of registered options.
+ def add(short, long, arg, description, block)
+ s = short ? short.dup : " "
+ s += (short ? ", " : " ") if long
+ doc " #{s}#{long} #{arg}".ljust(@width-1) + " #{description}"
+ @options << MSpecOption.new(short, long, arg, description, block)
+ end
+
+ # Searches all registered options to find a match for +opt+. Returns
+ # +nil+ if no registered options match.
+ def match?(opt)
+ @options.find { |o| o.match? opt }
+ end
+
+ # Processes an option. Calls the #on_extra block (or default) for
+ # unrecognized options. For registered options, possibly fetches an
+ # argument and invokes the option's block if it is not nil.
+ def process(argv, entry, opt, arg)
+ unless option = match?(opt)
+ @on_extra[entry]
+ else
+ if option.arg?
+ arg = argv.shift if arg.nil?
+ raise ParseError, "No argument provided for #{opt}" unless arg
+ option.block[arg] if option.block
+ else
+ option.block[] if option.block
+ end
+ end
+ option
+ end
+
+ # Splits a string at +n+ characters into the +opt+ and the +rest+.
+ # The +arg+ is set to +nil+ if +rest+ is an empty string.
+ def split(str, n)
+ opt = str[0, n]
+ rest = str[n, str.size]
+ arg = rest == "" ? nil : rest
+ return opt, arg, rest
+ end
+
+ # Parses an array of command line entries, calling blocks for
+ # registered options.
+ def parse(argv = ARGV)
+ argv = Array(argv).dup
+
+ while entry = argv.shift
+ # collect everything that is not an option
+ if entry[0] != ?- or entry.size < 2
+ @on_extra[entry]
+ next
+ end
+
+ # this is a long option
+ if entry[1] == ?-
+ opt, arg = entry.split "="
+ process argv, entry, opt, arg
+ next
+ end
+
+ # disambiguate short option group from short option with argument
+ opt, arg, rest = split entry, 2
+
+ # process first option
+ option = process argv, entry, opt, arg
+ next unless option and !option.arg?
+
+ # process the rest of the options
+ while rest.size > 0
+ opt, arg, rest = split rest, 1
+ opt = "-" + opt
+ option = process argv, opt, opt, arg
+ break if !option or option.arg?
+ end
+ end
+
+ @extra
+ rescue ParseError => e
+ puts self
+ puts e
+ exit 1
+ end
+
+ # Adds a string of documentation text inline in the text generated
+ # from the options. See #on and #add.
+ def doc(str)
+ @doc << str
+ end
+
+ # Convenience method for providing -v, --version options.
+ def version(version, &block)
+ show = block || lambda { puts "#{File.basename $0} #{version}"; exit }
+ on "-v", "--version", "Show version", &show
+ end
+
+ # Convenience method for providing -h, --help options.
+ def help(&block)
+ help = block || lambda { puts self; exit 1 }
+ on "-h", "--help", "Show this message", &help
+ end
+
+ # Stores a block that will be called with unrecognized options
+ def on_extra(&block)
+ @on_extra = block
+ end
+
+ # Returns a string representation of the options and doc strings.
+ def to_s
+ @banner + "\n\n" + @doc.join("\n") + "\n"
+ end
+
+ # The methods below provide groups of options that
+ # are composed by the particular runners to provide
+ # their functionality
+
+ def configure(&block)
+ on("-B", "--config", "FILE",
+ "Load FILE containing configuration options", &block)
+ end
+
+ def env
+ on("--env", "KEY=VALUE", "Set environment variable") do |env|
+ key, value = env.split('=', 2)
+ ENV[key] = value
+ end
+ end
+
+ def targets
+ on("-t", "--target", "TARGET",
+ "Implementation to run the specs, where TARGET is:") do |t|
+ case t
+ when 'r', 'ruby'
+ config[:target] = 'ruby'
+ when 'x', 'rubinius'
+ config[:target] = './bin/rbx'
+ when 'X', 'rbx'
+ config[:target] = 'rbx'
+ when 'j', 'jruby'
+ config[:target] = 'jruby'
+ when 'i','ironruby'
+ config[:target] = 'ir'
+ when 'm','maglev'
+ config[:target] = 'maglev-ruby'
+ when 't','topaz'
+ config[:target] = 'topaz'
+ when 'o','opal'
+ mspec_lib = File.expand_path('../../../', __FILE__)
+ config[:target] = "./bin/opal -syaml -sfileutils -rnodejs -rnodejs/require -rnodejs/yaml -rprocess -Derror -I#{mspec_lib} -I./lib/ -I. "
+ else
+ config[:target] = t
+ end
+ end
+
+ doc ""
+ doc " r or ruby invokes ruby in PATH"
+ doc " x or rubinius invokes ./bin/rbx"
+ doc " X or rbx invokes rbx in PATH"
+ doc " j or jruby invokes jruby in PATH"
+ doc " i or ironruby invokes ir in PATH"
+ doc " m or maglev invokes maglev-ruby in PATH"
+ doc " t or topaz invokes topaz in PATH"
+ doc " o or opal invokes ./bin/opal with options"
+ doc " full path to EXE invokes EXE directly\n"
+
+ on("-T", "--target-opt", "OPT",
+ "Pass OPT as a flag to the target implementation") do |t|
+ config[:flags] << t
+ end
+ on("-I", "--include", "DIR",
+ "Pass DIR through as the -I option to the target") do |d|
+ config[:loadpath] << "-I#{d}"
+ end
+ on("-r", "--require", "LIBRARY",
+ "Pass LIBRARY through as the -r option to the target") do |f|
+ config[:requires] << "-r#{f}"
+ end
+ end
+
+ def formatters
+ on("-f", "--format", "FORMAT",
+ "Formatter for reporting, where FORMAT is one of:") do |o|
+ require 'mspec/runner/formatters'
+ case o
+ when 's', 'spec', 'specdoc'
+ config[:formatter] = SpecdocFormatter
+ when 'h', 'html'
+ config[:formatter] = HtmlFormatter
+ when 'd', 'dot', 'dotted'
+ config[:formatter] = DottedFormatter
+ when 'b', 'describe'
+ config[:formatter] = DescribeFormatter
+ when 'f', 'file'
+ config[:formatter] = FileFormatter
+ when 'u', 'unit', 'unitdiff'
+ config[:formatter] = UnitdiffFormatter
+ when 'm', 'summary'
+ config[:formatter] = SummaryFormatter
+ when 'a', '*', 'spin'
+ config[:formatter] = SpinnerFormatter
+ when 't', 'method'
+ config[:formatter] = MethodFormatter
+ when 'e', 'stats'
+ config[:formatter] = StatsPerFileFormatter
+ when 'y', 'yaml'
+ config[:formatter] = YamlFormatter
+ when 'p', 'profile'
+ config[:formatter] = ProfileFormatter
+ when 'j', 'junit'
+ config[:formatter] = JUnitFormatter
+ else
+ abort "Unknown format: #{o}" unless File.exist?(o)
+ require File.expand_path(o)
+ if Object.const_defined?(:CUSTOM_MSPEC_FORMATTER)
+ config[:formatter] = CUSTOM_MSPEC_FORMATTER
+ else
+ abort "You must define CUSTOM_MSPEC_FORMATTER in your custom formatter file"
+ end
+ end
+ end
+
+ doc ""
+ doc " s, spec, specdoc SpecdocFormatter"
+ doc " h, html, HtmlFormatter"
+ doc " d, dot, dotted DottedFormatter"
+ doc " f, file FileFormatter"
+ doc " u, unit, unitdiff UnitdiffFormatter"
+ doc " m, summary SummaryFormatter"
+ doc " a, *, spin SpinnerFormatter"
+ doc " t, method MethodFormatter"
+ doc " e, stats StatsPerFileFormatter"
+ doc " y, yaml YamlFormatter"
+ doc " p, profile ProfileFormatter"
+ doc " j, junit JUnitFormatter\n"
+
+ on("-o", "--output", "FILE",
+ "Write formatter output to FILE") do |f|
+ config[:output] = f
+ end
+
+ on("--error-output", "FILE",
+ "Write error output of failing specs to FILE, or $stderr if value is 'stderr'.") do |f|
+ config[:error_output] = f
+ end
+ end
+
+ def filters
+ on("-e", "--example", "STR",
+ "Run examples with descriptions matching STR") do |o|
+ config[:includes] << o
+ end
+ on("-E", "--exclude", "STR",
+ "Exclude examples with descriptions matching STR") do |o|
+ config[:excludes] << o
+ end
+ on("-p", "--pattern", "PATTERN",
+ "Run examples with descriptions matching PATTERN") do |o|
+ config[:patterns] << Regexp.new(o)
+ end
+ on("-P", "--excl-pattern", "PATTERN",
+ "Exclude examples with descriptions matching PATTERN") do |o|
+ config[:xpatterns] << Regexp.new(o)
+ end
+ on("-g", "--tag", "TAG",
+ "Run examples with descriptions matching ones tagged with TAG") do |o|
+ config[:tags] << o
+ end
+ on("-G", "--excl-tag", "TAG",
+ "Exclude examples with descriptions matching ones tagged with TAG") do |o|
+ config[:xtags] << o
+ end
+ on("-w", "--profile", "FILE",
+ "Run examples for methods listed in the profile FILE") do |f|
+ config[:profiles] << f
+ end
+ on("-W", "--excl-profile", "FILE",
+ "Exclude examples for methods listed in the profile FILE") do |f|
+ config[:xprofiles] << f
+ end
+ end
+
+ def chdir
+ on("-C", "--chdir", "DIR",
+ "Change the working directory to DIR before running specs") do |d|
+ Dir.chdir d
+ end
+ end
+
+ def prefix
+ on("--prefix", "STR", "Prepend STR when resolving spec file names") do |p|
+ config[:prefix] = p
+ end
+ end
+
+ def pretend
+ on("-Z", "--dry-run",
+ "Invoke formatters and other actions, but don't execute the specs") do
+ MSpec.register_mode :pretend
+ end
+ end
+
+ def unguarded
+ on("--unguarded", "Turn off all guards") do
+ MSpec.register_mode :unguarded
+ end
+ on("--no-ruby_bug", "Turn off the ruby_bug guard") do
+ MSpec.register_mode :no_ruby_bug
+ end
+ end
+
+ def randomize
+ on("-H", "--random",
+ "Randomize the list of spec files") do
+ MSpec.randomize = true
+ end
+ end
+
+ def repeat
+ on("-R", "--repeat", "NUMBER",
+ "Repeatedly run an example NUMBER times") do |o|
+ MSpec.repeat = Integer(o)
+ end
+ end
+
+ def verbose
+ on("-V", "--verbose", "Output the name of each file processed") do
+ obj = Object.new
+ def obj.start
+ @width = MSpec.files_array.inject(0) { |max, f| f.size > max ? f.size : max }
+ end
+ def obj.load
+ file = MSpec.file
+ STDERR.print "\n#{file.ljust(@width)}\n"
+ end
+ MSpec.register :start, obj
+ MSpec.register :load, obj
+ end
+
+ on("-m", "--marker", "MARKER",
+ "Output MARKER for each file processed") do |o|
+ obj = Object.new
+ obj.instance_variable_set :@marker, o
+ def obj.load
+ STDERR.print @marker
+ end
+ MSpec.register :load, obj
+ end
+
+ on("--print-skips", "Print skips") do
+ config[:print_skips] = true
+ end
+ end
+
+ def interrupt
+ on("--int-spec", "Control-C interrupts the current spec only") do
+ config[:abort] = false
+ end
+ end
+
+ def timeout
+ on("--timeout", "TIMEOUT", "Abort if a spec takes longer than TIMEOUT seconds") do |timeout|
+ require 'mspec/runner/actions/timeout'
+ timeout = Float(timeout)
+ TimeoutAction.new(timeout).register
+ end
+ end
+
+ def verify
+ on("--report-on", "GUARD", "Report specs guarded by GUARD") do |g|
+ MSpec.register_mode :report_on
+ SpecGuard.guards << g.to_sym
+ end
+ on("-O", "--report", "Report guarded specs") do
+ MSpec.register_mode :report
+ end
+ on("-Y", "--verify",
+ "Verify that guarded specs pass and fail as expected") do
+ MSpec.register_mode :verify
+ end
+ end
+
+ def action_filters
+ on("-K", "--action-tag", "TAG",
+ "Spec descriptions marked with TAG will trigger the specified action") do |o|
+ config[:atags] << o
+ end
+ on("-S", "--action-string", "STR",
+ "Spec descriptions matching STR will trigger the specified action") do |o|
+ config[:astrings] << o
+ end
+ end
+
+ def actions
+ on("--spec-debug",
+ "Invoke the debugger when a spec description matches (see -K, -S)") do
+ config[:debugger] = true
+ end
+ end
+
+ def debug
+ on("-d", "--debug",
+ "Disable MSpec backtrace filtering") do
+ $MSPEC_DEBUG = true
+ end
+ end
+
+ def launchable
+ on("--launchable-test-reports", "DIR",
+ "DIR The directory for reporting test results in Launchable JSON format") do |o|
+ require 'mspec/runner/formatters/launchable'
+ config[:launchable] = LaunchableFormatter.setDir(o)
+ end
+ end
+
+ def all
+ configure {}
+ env
+ targets
+ formatters
+ filters
+ chdir
+ prefix
+ pretend
+ unguarded
+ randomize
+ repeat
+ verbose
+ interrupt
+ timeout
+ verify
+ action_filters
+ actions
+ debug
+ launchable
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/script.rb b/spec/mspec/lib/mspec/utils/script.rb
new file mode 100644
index 0000000000..15fd23fabf
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/script.rb
@@ -0,0 +1,305 @@
+require 'mspec/guards/guard'
+require 'mspec/guards/version'
+require 'mspec/utils/warnings'
+
+# MSpecScript provides a skeleton for all the MSpec runner scripts.
+class MSpecScript
+ # Returns the config object. Maintained at the class
+ # level to easily enable simple config files. See the
+ # class method +set+.
+ def self.config
+ @config ||= {
+ :path => ['.', 'spec'],
+ :config_ext => '.mspec'
+ }
+ end
+
+ # Associates +value+ with +key+ in the config object. Enables
+ # simple config files of the form:
+ #
+ # class MSpecScript
+ # set :target, "ruby"
+ # set :files, ["one_spec.rb", "two_spec.rb"]
+ # end
+ def self.set(key, value)
+ config[key] = value
+ end
+
+ # Gets the value of +key+ from the config object. Simplifies
+ # getting values in a config file:
+ #
+ # class MSpecScript
+ # set :a, 1
+ # set :b, 2
+ # set :c, get(:a) + get(:b)
+ # end
+ def self.get(key)
+ config[key]
+ end
+
+ class << self
+ attr_accessor :child_process
+ end
+
+ # True if the current process is the one going to run the specs with `MSpec.process`.
+ # False for e.g. `mspec` which exec's to `mspec-run`.
+ # This is useful in .mspec config files.
+ def self.child_process?
+ MSpecScript.child_process
+ end
+
+ def initialize
+ check_version!
+
+ config[:formatter] = nil
+ config[:includes] = []
+ config[:excludes] = []
+ config[:patterns] = []
+ config[:xpatterns] = []
+ config[:tags] = []
+ config[:xtags] = []
+ config[:profiles] = []
+ config[:xprofiles] = []
+ config[:atags] = []
+ config[:astrings] = []
+ config[:ltags] = []
+ config[:abort] = true
+ @loaded = []
+ end
+
+ # Returns the config object maintained by the instance's class.
+ # See the class methods +set+ and +config+.
+ def config
+ MSpecScript.config
+ end
+
+ # Returns +true+ if the file was located in +config[:path]+,
+ # possibly appending +config[:config_ext]. Returns +false+
+ # otherwise.
+ def try_load(target)
+ names = [target]
+ unless target[-6..-1] == config[:config_ext]
+ names << target + config[:config_ext]
+ end
+
+ names.each do |name|
+ config[:path].each do |dir|
+ file = File.expand_path name, dir
+ if @loaded.include?(file)
+ return true
+ elsif File.exist? file
+ value = Kernel.load(file)
+ @loaded << file
+ return value
+ end
+ end
+ end
+
+ false
+ end
+
+ def load(target)
+ try_load(target) or abort "Could not load config file #{target}"
+ end
+
+ # Attempts to load a default config file. First tries to load
+ # 'default.mspec'. If that fails, attempts to load a config
+ # file name constructed from the value of RUBY_ENGINE and the
+ # first two numbers in RUBY_VERSION. For example, on MRI 1.8.6,
+ # the file name would be 'ruby.1.8.mspec'.
+ def load_default
+ try_load 'default.mspec'
+
+ if Object.const_defined?(:RUBY_ENGINE)
+ engine = RUBY_ENGINE
+ else
+ engine = 'ruby'
+ end
+ try_load "#{engine}.#{SpecGuard.ruby_version}.mspec"
+ try_load "#{engine}.mspec"
+ end
+
+ # Callback for enabling custom options. This version is a no-op.
+ # Provide an implementation specific version in a config file.
+ # Called by #options after the MSpec-provided options are added.
+ def custom_options(options)
+ options.doc " No custom options registered"
+ end
+
+ # Registers all filters and actions.
+ def register
+ require 'mspec/runner/formatters/dotted'
+ require 'mspec/runner/formatters/spinner'
+ require 'mspec/runner/formatters/file'
+ require 'mspec/runner/filters'
+
+ if formatter = config_formatter
+ formatter.register
+ MSpec.formatter = formatter
+ end
+
+ MatchFilter.new(:include, *config[:includes]).register unless config[:includes].empty?
+ MatchFilter.new(:exclude, *config[:excludes]).register unless config[:excludes].empty?
+ RegexpFilter.new(:include, *config[:patterns]).register unless config[:patterns].empty?
+ RegexpFilter.new(:exclude, *config[:xpatterns]).register unless config[:xpatterns].empty?
+ TagFilter.new(:include, *config[:tags]).register unless config[:tags].empty?
+ TagFilter.new(:exclude, *config[:xtags]).register unless config[:xtags].empty?
+ ProfileFilter.new(:include, *config[:profiles]).register unless config[:profiles].empty?
+ ProfileFilter.new(:exclude, *config[:xprofiles]).register unless config[:xprofiles].empty?
+
+ DebugAction.new(config[:atags], config[:astrings]).register if config[:debugger]
+
+ custom_register
+ end
+
+ # Makes a formatter specified by :formatter option.
+ def config_formatter
+ if config[:formatter].nil?
+ config[:formatter] = STDOUT.tty? ? SpinnerFormatter : @files.size < 50 ? DottedFormatter : FileFormatter
+ end
+
+ if config[:formatter]
+ config[:formatter] = config[:formatter].new(config[:output])
+ end
+
+ if config[:launchable]
+ config[:formatter].extend config[:launchable]
+ end
+
+ config[:formatter]
+ end
+
+ # Callback for enabling custom actions, etc. This version is a
+ # no-op. Provide an implementation specific version in a config
+ # file. Called by #register.
+ def custom_register
+ end
+
+ # Sets up signal handlers. Only a handler for SIGINT is
+ # registered currently.
+ def signals
+ if config[:abort]
+ Signal.trap "INT" do
+ MSpec.actions :abort
+ puts "\nProcess aborted!"
+ exit! 1
+ end
+ end
+ end
+
+ # Attempts to resolve +partial+ as a file or directory name in the
+ # following order:
+ #
+ # 1. +partial+
+ # 2. +partial+ + "_spec.rb"
+ # 3. <tt>File.join(config[:prefix], partial)</tt>
+ # 4. <tt>File.join(config[:prefix], partial + "_spec.rb")</tt>
+ #
+ # If it is a file name, returns the name as an entry in an array.
+ # If it is a directory, returns all *_spec.rb files in the
+ # directory and subdirectories.
+ #
+ # If unable to resolve +partial+, +Kernel.abort+ is called.
+ def entries(partial)
+ file = partial + "_spec.rb"
+ patterns = [partial, file]
+ if config[:prefix]
+ patterns << File.join(config[:prefix], partial)
+ patterns << File.join(config[:prefix], file)
+ end
+
+ patterns.each do |pattern|
+ begin
+ expanded = File.realpath(pattern)
+ rescue Errno::ENOENT, Errno::ENOTDIR
+ next
+ end
+ if File.file?(expanded) && expanded.end_with?('.rb')
+ return [expanded]
+ elsif File.directory?(expanded)
+ specs = Dir["#{expanded}/**/*_spec.rb"].sort
+ return specs unless specs.empty?
+ end
+ end
+
+ abort "Could not find spec file #{partial}"
+ end
+
+ # Resolves each entry in +patterns+ to a set of files.
+ #
+ # If the pattern has a leading '^' character, the list of files
+ # is subtracted from the list of files accumulated to that point.
+ #
+ # If the entry has a leading ':' character, the corresponding
+ # key is looked up in the config object and the entries in the
+ # value retrieved are processed through #entries.
+ def files(patterns)
+ list = []
+ patterns.each do |pattern|
+ case pattern[0]
+ when ?^
+ list -= entries(pattern[1..-1])
+ when ?:
+ key = pattern[1..-1].to_sym
+ value = config[key]
+ abort "Key #{pattern} not found in mspec config." unless value
+ list += files(Array(value))
+ else
+ list += entries(pattern)
+ end
+ end
+ list
+ end
+
+ def files_from_patterns(patterns)
+ unless $0.end_with?("_spec.rb")
+ if patterns.empty?
+ patterns = config[:files]
+ end
+ if patterns.empty? and File.directory? "./spec"
+ patterns = ["spec/"]
+ end
+ end
+ list = files(patterns)
+ abort "No files specified." if list.empty?
+ list
+ end
+
+ def cores(max)
+ require 'etc'
+ [Etc.nprocessors, max].min
+ end
+
+ def setup_env
+ ENV['MSPEC_RUNNER'] = '1'
+
+ unless ENV['RUBY_EXE']
+ ENV['RUBY_EXE'] = config[:target] if config[:target]
+ end
+
+ unless ENV['RUBY_FLAGS']
+ ENV['RUBY_FLAGS'] = config[:flags].join(" ") if config[:flags]
+ end
+ end
+
+ # Instantiates an instance and calls the series of methods to
+ # invoke the script.
+ def self.main(child_process = true)
+ MSpecScript.child_process = child_process
+
+ script = new
+ script.load_default
+ script.options
+ script.signals
+ script.register
+ script.setup_env
+ require 'mspec'
+ script.run
+ end
+
+ private def check_version!
+ ruby_version_is ""..."2.6" do
+ warn "MSpec is supported for Ruby 2.6 and above only"
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/version.rb b/spec/mspec/lib/mspec/utils/version.rb
new file mode 100644
index 0000000000..9c1c58b8df
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/version.rb
@@ -0,0 +1,52 @@
+class SpecVersion
+ # If beginning implementations have a problem with this include, we can
+ # manually implement the relational operators that are needed.
+ include Comparable
+
+ # SpecVersion handles comparison correctly for the context by filling in
+ # missing version parts according to the value of +ceil+. If +ceil+ is
+ # +false+, 0 digits fill in missing version parts. If +ceil+ is +true+, 9
+ # digits fill in missing parts. (See e.g. VersionGuard and BugGuard.)
+ def initialize(version, ceil = false)
+ @version = version
+ @ceil = ceil
+ @integer = nil
+ end
+
+ def to_s
+ @version
+ end
+
+ def to_str
+ to_s
+ end
+
+ # Converts a string representation of a version major.minor.tiny
+ # to an integer representation so that comparisons can be made. For example,
+ # "2.2.10" < "2.2.2" would be false if compared as strings.
+ def to_i
+ unless @integer
+ major, minor, tiny = @version.split "."
+ if @ceil
+ tiny = 99 unless tiny
+ end
+ parts = [major, minor, tiny].map { |x| x.to_i }
+ @integer = ("1%02d%02d%02d" % parts).to_i
+ end
+ @integer
+ end
+
+ def to_int
+ to_i
+ end
+
+ def <=>(other)
+ if other.respond_to? :to_int
+ other = Integer(other.to_int)
+ else
+ other = SpecVersion.new(String(other)).to_i
+ end
+
+ self.to_i <=> other
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/warnings.rb b/spec/mspec/lib/mspec/utils/warnings.rb
new file mode 100644
index 0000000000..23efc696a5
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/warnings.rb
@@ -0,0 +1,10 @@
+require 'mspec/guards/version'
+
+# Always enable deprecation warnings when running MSpec, as ruby/spec tests for them,
+# and like in most test frameworks, deprecation warnings should be enabled by default,
+# so that deprecations are noticed before the breaking change.
+# Disable experimental warnings, we want to test new experimental features in ruby/spec.
+if Object.const_defined?(:Warning) and Warning.respond_to?(:[]=)
+ Warning[:deprecated] = true
+ Warning[:experimental] = false
+end
diff --git a/spec/mspec/lib/mspec/version.rb b/spec/mspec/lib/mspec/version.rb
new file mode 100644
index 0000000000..9126f5366e
--- /dev/null
+++ b/spec/mspec/lib/mspec/version.rb
@@ -0,0 +1,5 @@
+require 'mspec/utils/version'
+
+module MSpec
+ VERSION = SpecVersion.new "1.8.0"
+end
diff --git a/spec/mspec/spec/commands/fixtures/four.txt b/spec/mspec/spec/commands/fixtures/four.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/four.txt
diff --git a/spec/mspec/spec/commands/fixtures/level2/three_spec.rb b/spec/mspec/spec/commands/fixtures/level2/three_spec.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/level2/three_spec.rb
@@ -0,0 +1 @@
+
diff --git a/spec/mspec/spec/commands/fixtures/one_spec.rb b/spec/mspec/spec/commands/fixtures/one_spec.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/one_spec.rb
@@ -0,0 +1 @@
+
diff --git a/spec/mspec/spec/commands/fixtures/three.rb b/spec/mspec/spec/commands/fixtures/three.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/three.rb
@@ -0,0 +1 @@
+
diff --git a/spec/mspec/spec/commands/fixtures/two_spec.rb b/spec/mspec/spec/commands/fixtures/two_spec.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/two_spec.rb
@@ -0,0 +1 @@
+
diff --git a/spec/mspec/spec/commands/mkspec_spec.rb b/spec/mspec/spec/commands/mkspec_spec.rb
new file mode 100644
index 0000000000..32262723de
--- /dev/null
+++ b/spec/mspec/spec/commands/mkspec_spec.rb
@@ -0,0 +1,363 @@
+require 'spec_helper'
+require 'mspec/commands/mkspec'
+require 'fileutils'
+
+RSpec.describe "The -c, --constant CONSTANT option" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ @config = @script.config
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-c", "--constant", "CONSTANT",
+ an_instance_of(String))
+ @script.options []
+ end
+
+ it "adds CONSTANT to the list of constants" do
+ ["-c", "--constant"].each do |opt|
+ @config[:constants] = []
+ @script.options [opt, "Object"]
+ expect(@config[:constants]).to include("Object")
+ end
+ end
+end
+
+RSpec.describe "The -b, --base DIR option" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ @config = @script.config
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-b", "--base", "DIR",
+ an_instance_of(String))
+ @script.options []
+ end
+
+ it "sets the base directory relative to which the spec directories are created" do
+ ["-b", "--base"].each do |opt|
+ @config[:base] = nil
+ @script.options [opt, "superspec"]
+ expect(@config[:base]).to eq(File.expand_path("superspec"))
+ end
+ end
+end
+
+RSpec.describe "The -r, --require LIBRARY option" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ @config = @script.config
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-r", "--require", "LIBRARY",
+ an_instance_of(String))
+ @script.options []
+ end
+
+ it "adds CONSTANT to the list of constants" do
+ ["-r", "--require"].each do |opt|
+ @config[:requires] = []
+ @script.options [opt, "libspec"]
+ expect(@config[:requires]).to include("libspec")
+ end
+ end
+end
+
+RSpec.describe "The -V, --version-guard VERSION option" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ @config = @script.config
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-V", "--version-guard", "VERSION",
+ an_instance_of(String))
+ @script.options []
+ end
+
+ it "sets the version for the ruby_version_is guards to VERSION" do
+ ["-r", "--require"].each do |opt|
+ @config[:requires] = []
+ @script.options [opt, "libspec"]
+ expect(@config[:requires]).to include("libspec")
+ end
+ end
+end
+
+RSpec.describe MkSpec, "#options" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ end
+
+ it "parses the command line options" do
+ expect(@options).to receive(:parse).with(["--this", "and", "--that"])
+ @script.options ["--this", "and", "--that"]
+ end
+
+ it "parses ARGV unless passed other options" do
+ expect(@options).to receive(:parse).with(ARGV)
+ @script.options
+ end
+
+ it "prints help and exits if passed an unrecognized option" do
+ expect(@options).to receive(:raise).with(MSpecOptions::ParseError, an_instance_of(String))
+ allow(@options).to receive(:puts)
+ allow(@options).to receive(:exit)
+ @script.options ["--iunknown"]
+ end
+end
+
+RSpec.describe MkSpec, "#create_directory" do
+ before :each do
+ @script = MkSpec.new
+ @script.config[:base] = "spec"
+ end
+
+ it "prints a warning if a file with the directory name exists" do
+ expect(File).to receive(:exist?).and_return(true)
+ expect(File).to receive(:directory?).and_return(false)
+ expect(FileUtils).not_to receive(:mkdir_p)
+ expect(@script).to receive(:puts).with("spec/class already exists and is not a directory.")
+ expect(@script.create_directory("Class")).to eq(nil)
+ end
+
+ it "does nothing if the directory already exists" do
+ expect(File).to receive(:exist?).and_return(true)
+ expect(File).to receive(:directory?).and_return(true)
+ expect(FileUtils).not_to receive(:mkdir_p)
+ expect(@script.create_directory("Class")).to eq("spec/class")
+ end
+
+ it "creates the directory if it does not exist" do
+ expect(File).to receive(:exist?).and_return(false)
+ expect(@script).to receive(:mkdir_p).with("spec/class")
+ expect(@script.create_directory("Class")).to eq("spec/class")
+ end
+
+ it "creates the directory for a namespaced module if it does not exist" do
+ expect(File).to receive(:exist?).and_return(false)
+ expect(@script).to receive(:mkdir_p).with("spec/struct/tms")
+ expect(@script.create_directory("Struct::Tms")).to eq("spec/struct/tms")
+ end
+end
+
+RSpec.describe MkSpec, "#write_requires" do
+ before :each do
+ @script = MkSpec.new
+ @script.config[:base] = "spec"
+
+ @file = double("file")
+ allow(File).to receive(:open).and_yield(@file)
+ end
+
+ it "writes the spec_helper require line" do
+ expect(@file).to receive(:puts).with("require_relative '../../../spec_helper'")
+ @script.write_requires("spec/core/tcejbo", "spec/core/tcejbo/inspect_spec.rb")
+ end
+
+ it "writes require lines for each library specified on the command line" do
+ allow(@file).to receive(:puts)
+ expect(@file).to receive(:puts).with("require_relative '../../../spec_helper'")
+ expect(@file).to receive(:puts).with("require 'complex'")
+ @script.config[:requires] << 'complex'
+ @script.write_requires("spec/core/tcejbo", "spec/core/tcejbo/inspect_spec.rb")
+ end
+end
+
+RSpec.describe MkSpec, "#write_spec" do
+ before :each do
+ @file = IOStub.new
+ allow(File).to receive(:open).and_yield(@file)
+
+ @script = MkSpec.new
+ allow(@script).to receive(:puts)
+
+ @response = double("system command response")
+ allow(@response).to receive(:include?).and_return(false)
+ allow(@script).to receive(:`).and_return(@response)
+ end
+
+ it "checks if specs exist for the method if the spec file exists" do
+ name = Regexp.escape(RbConfig.ruby)
+ expect(@script).to receive(:`).with(
+ %r"#{name} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e 'Object#inspect' spec/core/tcejbo/inspect_spec.rb")
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ end
+
+ it "checks for the method name in the spec file output" do
+ expect(@response).to receive(:include?).with("Array#[]=")
+ @script.write_spec("spec/core/yarra/element_set_spec.rb", "Array#[]=", true)
+ end
+
+ it "returns nil if the spec file exists and contains a spec for the method" do
+ allow(@response).to receive(:include?).and_return(true)
+ expect(@script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)).to eq(nil)
+ end
+
+ it "does not print the spec file name if it exists and contains a spec for the method" do
+ allow(@response).to receive(:include?).and_return(true)
+ expect(@script).not_to receive(:puts)
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ end
+
+ it "prints the spec file name if a template spec is written" do
+ expect(@script).to receive(:puts).with("spec/core/tcejbo/inspect_spec.rb")
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ end
+
+ it "writes a template spec to the file if the spec file does not exist" do
+ expect(@file).to receive(:puts).twice
+ expect(@script).to receive(:puts).with("spec/core/tcejbo/inspect_spec.rb")
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", false)
+ end
+
+ it "writes a template spec to the file if it exists but contains no spec for the method" do
+ expect(@response).to receive(:include?).and_return(false)
+ expect(@file).to receive(:puts).twice
+ expect(@script).to receive(:puts).with("spec/core/tcejbo/inspect_spec.rb")
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ end
+
+ it "writes a template spec" do
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ expect(@file).to eq <<EOS
+
+describe "Object#inspect" do
+ it "needs to be reviewed for spec completeness"
+end
+EOS
+ end
+
+ it "writes a template spec with version guard" do
+ @script.config[:version] = '""..."1.9"'
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ expect(@file).to eq <<EOS
+
+ruby_version_is ""..."1.9" do
+ describe "Object#inspect" do
+ it "needs to be reviewed for spec completeness"
+ end
+end
+EOS
+
+ end
+end
+
+RSpec.describe MkSpec, "#create_file" do
+ before :each do
+ @script = MkSpec.new
+ allow(@script).to receive(:write_requires)
+ allow(@script).to receive(:write_spec)
+
+ allow(File).to receive(:exist?).and_return(false)
+ end
+
+ it "generates a file name based on the directory, class/module, and method" do
+ expect(File).to receive(:join).with("spec/tcejbo", "inspect_spec.rb"
+ ).and_return("spec/tcejbo/inspect_spec.rb")
+ @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect")
+ end
+
+ it "does not call #write_requires if the spec file already exists" do
+ expect(File).to receive(:exist?).and_return(true)
+ expect(@script).not_to receive(:write_requires)
+ @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect")
+ end
+
+ it "calls #write_requires if the spec file does not exist" do
+ expect(File).to receive(:exist?).and_return(false)
+ expect(@script).to receive(:write_requires).with(
+ "spec/tcejbo", "spec/tcejbo/inspect_spec.rb")
+ @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect")
+ end
+
+ it "calls #write_spec with the file, method name" do
+ expect(@script).to receive(:write_spec).with(
+ "spec/tcejbo/inspect_spec.rb", "Object#inspect", false)
+ @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect")
+ end
+end
+
+RSpec.describe MkSpec, "#run" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @map = NameMap.new
+ allow(NameMap).to receive(:new).and_return(@map)
+
+ @script = MkSpec.new
+ allow(@script).to receive(:create_directory).and_return("spec/mkspec")
+ allow(@script).to receive(:create_file)
+ @script.config[:constants] = [MkSpec]
+ end
+
+ it "loads files in the requires list" do
+ allow(@script).to receive(:require)
+ expect(@script).to receive(:require).with("alib")
+ expect(@script).to receive(:require).with("blib")
+ @script.config[:requires] = ["alib", "blib"]
+ @script.run
+ end
+
+ it "creates a map of constants to methods" do
+ expect(@map).to receive(:map).with({}, @script.config[:constants]).and_return({})
+ @script.run
+ end
+
+ it "calls #create_directory for each class/module in the map" do
+ expect(@script).to receive(:create_directory).with("MkSpec").twice
+ @script.run
+ end
+
+ it "calls #create_file for each method on each class/module in the map" do
+ expect(@map).to receive(:map).with({}, @script.config[:constants]
+ ).and_return({"MkSpec#" => ["run"]})
+ expect(@script).to receive(:create_file).with("spec/mkspec", "MkSpec", "run", "MkSpec#run")
+ @script.run
+ end
+end
+
+RSpec.describe MkSpec, ".main" do
+ before :each do
+ @script = double("MkSpec").as_null_object
+ allow(MkSpec).to receive(:new).and_return(@script)
+ end
+
+ it "sets MSPEC_RUNNER = '1' in the environment" do
+ ENV["MSPEC_RUNNER"] = "0"
+ MkSpec.main
+ expect(ENV["MSPEC_RUNNER"]).to eq("1")
+ end
+
+ it "creates an instance of MSpecScript" do
+ expect(MkSpec).to receive(:new).and_return(@script)
+ MkSpec.main
+ end
+
+ it "calls the #options method on the script" do
+ expect(@script).to receive(:options)
+ MkSpec.main
+ end
+
+ it "calls the #run method on the script" do
+ expect(@script).to receive(:run)
+ MkSpec.main
+ end
+end
diff --git a/spec/mspec/spec/commands/mspec_ci_spec.rb b/spec/mspec/spec/commands/mspec_ci_spec.rb
new file mode 100644
index 0000000000..b8dc9d062f
--- /dev/null
+++ b/spec/mspec/spec/commands/mspec_ci_spec.rb
@@ -0,0 +1,155 @@
+require 'spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/tag'
+require 'mspec/commands/mspec-ci'
+
+RSpec.describe MSpecCI, "#options" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @script = MSpecCI.new
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:files).and_return([])
+ end
+
+ it "enables the chdir option" do
+ expect(@options).to receive(:chdir)
+ @script.options []
+ end
+
+ it "enables the prefix option" do
+ expect(@options).to receive(:prefix)
+ @script.options []
+ end
+
+ it "enables the config option" do
+ expect(@options).to receive(:configure)
+ @script.options []
+ end
+
+ it "provides a custom action (block) to the config option" do
+ expect(@script).to receive(:load).with("cfg.mspec")
+ @script.options ["-B", "cfg.mspec"]
+ end
+
+ it "enables the dry run option" do
+ expect(@options).to receive(:pretend)
+ @script.options []
+ end
+
+ it "enables the unguarded option" do
+ expect(@options).to receive(:unguarded)
+ @script.options []
+ end
+
+ it "enables the interrupt single specs option" do
+ expect(@options).to receive(:interrupt)
+ @script.options []
+ end
+
+ it "enables the formatter options" do
+ expect(@options).to receive(:formatters)
+ @script.options []
+ end
+
+ it "enables the verbose option" do
+ expect(@options).to receive(:verbose)
+ @script.options []
+ end
+
+ it "enables the action options" do
+ expect(@options).to receive(:actions)
+ @script.options []
+ end
+
+ it "enables the action filter options" do
+ expect(@options).to receive(:action_filters)
+ @script.options []
+ end
+
+ it "enables the version option" do
+ expect(@options).to receive(:version)
+ @script.options []
+ end
+
+ it "enables the help option" do
+ expect(@options).to receive(:help)
+ @script.options []
+ end
+
+ it "enables the repeat option" do
+ expect(@options).to receive(:repeat)
+ @script.options @argv
+ end
+
+ it "calls #custom_options" do
+ expect(@script).to receive(:custom_options).with(@options)
+ @script.options []
+ end
+end
+
+RSpec.describe MSpecCI, "#run" do
+ before :each do
+ allow(MSpec).to receive(:process)
+
+ @filter = double("TagFilter")
+ allow(TagFilter).to receive(:new).and_return(@filter)
+ allow(@filter).to receive(:register)
+
+ @tags = ["fails", "critical", "unstable", "incomplete", "unsupported"]
+
+ @config = { :ci_files => ["one", "two"] }
+ @script = MSpecCI.new
+ allow(@script).to receive(:exit)
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:files).and_return(["one", "two"])
+ @script.options []
+ end
+
+ it "registers the tags patterns" do
+ @config[:tags_patterns] = [/spec/, "tags"]
+ expect(MSpec).to receive(:register_tags_patterns).with([/spec/, "tags"])
+ @script.run
+ end
+
+ it "registers the files to process" do
+ expect(MSpec).to receive(:register_files).with(["one", "two"])
+ @script.run
+ end
+
+ it "registers a tag filter for 'fails', 'unstable', 'incomplete', 'critical', 'unsupported'" do
+ filter = double("fails filter")
+ expect(TagFilter).to receive(:new).with(:exclude, *@tags).and_return(filter)
+ expect(filter).to receive(:register)
+ @script.run
+ end
+
+ it "registers an additional exclude tag specified by :ci_xtags" do
+ @config[:ci_xtags] = "windows"
+ filter = double("fails filter")
+ expect(TagFilter).to receive(:new).with(:exclude, *(@tags + ["windows"])).and_return(filter)
+ expect(filter).to receive(:register)
+ @script.run
+ end
+
+ it "registers additional exclude tags specified by a :ci_xtags array" do
+ @config[:ci_xtags] = ["windows", "windoze"]
+ filter = double("fails filter")
+ expect(TagFilter).to receive(:new).with(:exclude,
+ *(@tags + ["windows", "windoze"])).and_return(filter)
+ expect(filter).to receive(:register)
+ @script.run
+ end
+
+ it "processes the files" do
+ expect(MSpec).to receive(:process)
+ @script.run
+ end
+
+ it "exits with the exit code registered with MSpec" do
+ allow(MSpec).to receive(:exit_code).and_return(7)
+ expect(@script).to receive(:exit).with(7)
+ @script.run
+ end
+end
diff --git a/spec/mspec/spec/commands/mspec_run_spec.rb b/spec/mspec/spec/commands/mspec_run_spec.rb
new file mode 100644
index 0000000000..f96be2b43e
--- /dev/null
+++ b/spec/mspec/spec/commands/mspec_run_spec.rb
@@ -0,0 +1,178 @@
+require 'spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/commands/mspec-run'
+
+one_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/one_spec.rb'
+two_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/two_spec.rb'
+
+RSpec.describe MSpecRun, ".new" do
+ before :each do
+ @script = MSpecRun.new
+ end
+
+ it "sets config[:files] to an empty list" do
+ expect(@script.config[:files]).to eq([])
+ end
+end
+
+RSpec.describe MSpecRun, "#options" do
+ before :each do
+ @argv = [one_spec, two_spec]
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @script = MSpecRun.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ it "enables the filter options" do
+ expect(@options).to receive(:filters)
+ @script.options @argv
+ end
+
+ it "enables the chdir option" do
+ expect(@options).to receive(:chdir)
+ @script.options @argv
+ end
+
+ it "enables the prefix option" do
+ expect(@options).to receive(:prefix)
+ @script.options @argv
+ end
+
+ it "enables the configure option" do
+ expect(@options).to receive(:configure)
+ @script.options @argv
+ end
+
+ it "provides a custom action (block) to the config option" do
+ expect(@script).to receive(:load).with("cfg.mspec")
+ @script.options ["-B", "cfg.mspec", one_spec]
+ end
+
+ it "enables the randomize option to runs specs in random order" do
+ expect(@options).to receive(:randomize)
+ @script.options @argv
+ end
+
+ it "enables the dry run option" do
+ expect(@options).to receive(:pretend)
+ @script.options @argv
+ end
+
+ it "enables the unguarded option" do
+ expect(@options).to receive(:unguarded)
+ @script.options @argv
+ end
+
+ it "enables the interrupt single specs option" do
+ expect(@options).to receive(:interrupt)
+ @script.options @argv
+ end
+
+ it "enables the formatter options" do
+ expect(@options).to receive(:formatters)
+ @script.options @argv
+ end
+
+ it "enables the verbose option" do
+ expect(@options).to receive(:verbose)
+ @script.options @argv
+ end
+
+ it "enables the verify options" do
+ expect(@options).to receive(:verify)
+ @script.options @argv
+ end
+
+ it "enables the action options" do
+ expect(@options).to receive(:actions)
+ @script.options @argv
+ end
+
+ it "enables the action filter options" do
+ expect(@options).to receive(:action_filters)
+ @script.options @argv
+ end
+
+ it "enables the version option" do
+ expect(@options).to receive(:version)
+ @script.options @argv
+ end
+
+ it "enables the help option" do
+ expect(@options).to receive(:help)
+ @script.options @argv
+ end
+
+ it "enables the repeat option" do
+ expect(@options).to receive(:repeat)
+ @script.options @argv
+ end
+
+ it "exits if there are no files to process and './spec' is not a directory" do
+ expect(File).to receive(:directory?).with("./spec").and_return(false)
+ expect(@options).to receive(:parse).and_return([])
+ expect(@script).to receive(:abort).with("No files specified.")
+ @script.options
+ end
+
+ it "process 'spec/' if it is a directory and no files were specified" do
+ expect(File).to receive(:directory?).with("./spec").and_return(true)
+ expect(@options).to receive(:parse).and_return([])
+ expect(@script).to receive(:files).with(["spec/"]).and_return(["spec/a_spec.rb"])
+ @script.options
+ end
+
+ it "calls #custom_options" do
+ expect(@script).to receive(:custom_options).with(@options)
+ @script.options @argv
+ end
+end
+
+RSpec.describe MSpecRun, "#run" do
+ before :each do
+ @script = MSpecRun.new
+ allow(@script).to receive(:exit)
+ @spec_dir = File.expand_path(File.dirname(__FILE__)+"/fixtures")
+ @file_patterns = [
+ @spec_dir+"/level2",
+ @spec_dir+"/one_spec.rb",
+ @spec_dir+"/two_spec.rb"]
+ @files = [
+ @spec_dir+"/level2/three_spec.rb",
+ @spec_dir+"/one_spec.rb",
+ @spec_dir+"/two_spec.rb"]
+ @script.options @file_patterns
+ allow(MSpec).to receive :process
+ end
+
+ it "registers the tags patterns" do
+ @script.config[:tags_patterns] = [/spec/, "tags"]
+ expect(MSpec).to receive(:register_tags_patterns).with([/spec/, "tags"])
+ @script.run
+ end
+
+ it "registers the files to process" do
+ expect(MSpec).to receive(:register_files).with(@files)
+ @script.run
+ end
+
+ it "uses config[:files] if no files are given on the command line" do
+ @script.config[:files] = @file_patterns
+ expect(MSpec).to receive(:register_files).with(@files)
+ @script.options []
+ @script.run
+ end
+
+ it "processes the files" do
+ expect(MSpec).to receive(:process)
+ @script.run
+ end
+
+ it "exits with the exit code registered with MSpec" do
+ allow(MSpec).to receive(:exit_code).and_return(7)
+ expect(@script).to receive(:exit).with(7)
+ @script.run
+ end
+end
diff --git a/spec/mspec/spec/commands/mspec_spec.rb b/spec/mspec/spec/commands/mspec_spec.rb
new file mode 100644
index 0000000000..d19bebb2d6
--- /dev/null
+++ b/spec/mspec/spec/commands/mspec_spec.rb
@@ -0,0 +1,180 @@
+require 'spec_helper'
+require 'yaml'
+require 'mspec/commands/mspec'
+
+RSpec.describe MSpecMain, "#options" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:load)
+ end
+
+ it "enables the configure option" do
+ expect(@options).to receive(:configure)
+ @script.options
+ end
+
+ it "provides a custom action (block) to the config option" do
+ @script.options ["-B", "config"]
+ expect(@config[:options]).to include("-B", "config")
+ end
+
+ it "loads the file specified by the config option" do
+ expect(@script).to receive(:load).with("config")
+ @script.options ["-B", "config"]
+ end
+
+ it "enables the target options" do
+ expect(@options).to receive(:targets)
+ @script.options
+ end
+
+ it "sets config[:options] to all argv entries that are not registered options" do
+ @options.on "-X", "--exclude", "ARG", "description"
+ @script.options [".", "-G", "fail", "-X", "ARG", "--list", "unstable", "some/file.rb"]
+ expect(@config[:options]).to eq([".", "-G", "fail", "--list", "unstable", "some/file.rb"])
+ end
+
+ it "calls #custom_options" do
+ expect(@script).to receive(:custom_options).with(@options)
+ @script.options
+ end
+end
+
+RSpec.describe MSpecMain, "#run" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:exec)
+ @err = $stderr
+ $stderr = IOStub.new
+ end
+
+ after :each do
+ $stderr = @err
+ end
+
+ it "uses exec to invoke the runner script" do
+ expect(@script).to receive(:exec).with("ruby", "#{MSPEC_HOME}/bin/mspec-run", close_others: false)
+ @script.options []
+ @script.run
+ end
+
+ it "shows the command line on stderr" do
+ expect(@script).to receive(:exec).with("ruby", "#{MSPEC_HOME}/bin/mspec-run", close_others: false)
+ @script.options []
+ @script.run
+ expect($stderr.to_s).to eq("$ ruby #{Dir.pwd}/bin/mspec-run\n")
+ end
+
+ it "adds config[:launch] to the exec options" do
+ expect(@script).to receive(:exec).with("ruby",
+ "-Xlaunch.option", "#{MSPEC_HOME}/bin/mspec-run", close_others: false)
+ @config[:launch] << "-Xlaunch.option"
+ @script.options []
+ @script.run
+ expect($stderr.to_s).to eq("$ ruby -Xlaunch.option #{Dir.pwd}/bin/mspec-run\n")
+ end
+
+ it "calls #multi_exec if the command is 'ci' and the multi option is passed" do
+ expect(@script).to receive(:multi_exec) do |argv|
+ expect(argv).to eq(["ruby", "#{MSPEC_HOME}/bin/mspec-ci"])
+ end
+ @script.options ["ci", "-j"]
+ expect do
+ @script.run
+ end.to raise_error(SystemExit)
+ end
+end
+
+RSpec.describe "The -j, --multi option" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-j", "--multi", an_instance_of(String))
+ @script.options
+ end
+
+ it "sets the multiple process option" do
+ ["-j", "--multi"].each do |opt|
+ @config[:multi] = nil
+ @script.options [opt]
+ expect(@config[:multi]).to eq(true)
+ end
+ end
+end
+
+RSpec.describe "The -h, --help option" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-h", "--help", an_instance_of(String))
+ @script.options
+ end
+
+ it "passes the option to the subscript" do
+ ["-h", "--help"].each do |opt|
+ @config[:options] = []
+ @script.options ["ci", opt]
+ expect(@config[:options].sort).to eq(["-h"])
+ end
+ end
+
+ it "prints help and exits" do
+ expect(@script).to receive(:puts).twice
+ expect(@script).to receive(:exit).twice
+ ["-h", "--help"].each do |opt|
+ @script.options [opt]
+ end
+ end
+end
+
+RSpec.describe "The -v, --version option" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-v", "--version", an_instance_of(String))
+ @script.options
+ end
+
+ it "passes the option to the subscripts" do
+ ["-v", "--version"].each do |opt|
+ @config[:options] = []
+ @script.options ["ci", opt]
+ expect(@config[:options].sort).to eq(["-v"])
+ end
+ end
+
+ it "prints the version and exits if no subscript is invoked" do
+ @config[:command] = nil
+ allow(File).to receive(:basename).and_return("mspec")
+ expect(@script).to receive(:puts).twice.with("mspec #{MSpec::VERSION}")
+ expect(@script).to receive(:exit).twice
+ ["-v", "--version"].each do |opt|
+ @script.options [opt]
+ end
+ end
+end
diff --git a/spec/mspec/spec/commands/mspec_tag_spec.rb b/spec/mspec/spec/commands/mspec_tag_spec.rb
new file mode 100644
index 0000000000..1ab5f6ea58
--- /dev/null
+++ b/spec/mspec/spec/commands/mspec_tag_spec.rb
@@ -0,0 +1,414 @@
+require 'spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/commands/mspec-tag'
+require 'mspec/runner/actions/tag'
+require 'mspec/runner/actions/taglist'
+require 'mspec/runner/actions/tagpurge'
+
+one_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/one_spec.rb'
+two_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/two_spec.rb'
+
+RSpec.describe MSpecTag, ".new" do
+ before :each do
+ @script = MSpecTag.new
+ end
+
+ it "sets config[:ltags] to an empty list" do
+ expect(@script.config[:ltags]).to eq([])
+ end
+
+ it "sets config[:tagger] to :add" do
+ @script.config[:tagger] = :add
+ end
+
+ it "sets config[:tag] to 'fails:'" do
+ @script.config[:tag] = 'fails:'
+ end
+
+ it "sets config[:outcome] to :fail" do
+ @script.config[:outcome] = :fail
+ end
+end
+
+RSpec.describe MSpecTag, "#options" do
+ before :each do
+ @stdout, $stdout = $stdout, IOStub.new
+
+ @argv = [one_spec, two_spec]
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @script = MSpecTag.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "enables the filter options" do
+ expect(@options).to receive(:filters)
+ @script.options @argv
+ end
+
+ it "enables the configure option" do
+ expect(@options).to receive(:configure)
+ @script.options @argv
+ end
+
+ it "provides a custom action (block) to the config option" do
+ expect(@script).to receive(:load).with("cfg.mspec")
+ @script.options ["-B", "cfg.mspec", one_spec]
+ end
+
+ it "enables the dry run option" do
+ expect(@options).to receive(:pretend)
+ @script.options @argv
+ end
+
+ it "enables the unguarded option" do
+ expect(@options).to receive(:unguarded)
+ @script.options @argv
+ end
+
+ it "enables the interrupt single specs option" do
+ expect(@options).to receive(:interrupt)
+ @script.options @argv
+ end
+
+ it "enables the formatter options" do
+ expect(@options).to receive(:formatters)
+ @script.options @argv
+ end
+
+ it "enables the verbose option" do
+ expect(@options).to receive(:verbose)
+ @script.options @argv
+ end
+
+ it "enables the version option" do
+ expect(@options).to receive(:version)
+ @script.options @argv
+ end
+
+ it "enables the help option" do
+ expect(@options).to receive(:help)
+ @script.options @argv
+ end
+
+ it "calls #custom_options" do
+ expect(@script).to receive(:custom_options).with(@options)
+ @script.options @argv
+ end
+
+ it "exits if there are no files to process" do
+ expect(@options).to receive(:parse).and_return([])
+ expect(@script).to receive(:exit)
+ @script.options
+ expect($stdout.to_s).to include "No files specified"
+ end
+end
+
+RSpec.describe MSpecTag, "options" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecTag.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ describe "-N, --add TAG" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-N", "--add", "TAG", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the mode to :add and sets the tag to TAG" do
+ ["-N", "--add"].each do |opt|
+ @config[:tagger] = nil
+ @config[:tag] = nil
+ @script.options [opt, "taggit", one_spec]
+ expect(@config[:tagger]).to eq(:add)
+ expect(@config[:tag]).to eq("taggit:")
+ end
+ end
+ end
+
+ describe "-R, --del TAG" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-R", "--del", "TAG",
+ an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "it sets the mode to :del, the tag to TAG, and the outcome to :pass" do
+ ["-R", "--del"].each do |opt|
+ @config[:tagger] = nil
+ @config[:tag] = nil
+ @config[:outcome] = nil
+ @script.options [opt, "taggit", one_spec]
+ expect(@config[:tagger]).to eq(:del)
+ expect(@config[:tag]).to eq("taggit:")
+ expect(@config[:outcome]).to eq(:pass)
+ end
+ end
+ end
+
+ describe "-Q, --pass" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-Q", "--pass", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the outcome to :pass" do
+ ["-Q", "--pass"].each do |opt|
+ @config[:outcome] = nil
+ @script.options [opt, one_spec]
+ expect(@config[:outcome]).to eq(:pass)
+ end
+ end
+ end
+
+ describe "-F, --fail" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-F", "--fail", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the outcome to :fail" do
+ ["-F", "--fail"].each do |opt|
+ @config[:outcome] = nil
+ @script.options [opt, one_spec]
+ expect(@config[:outcome]).to eq(:fail)
+ end
+ end
+ end
+
+ describe "-L, --all" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-L", "--all", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the outcome to :all" do
+ ["-L", "--all"].each do |opt|
+ @config[:outcome] = nil
+ @script.options [opt, one_spec]
+ expect(@config[:outcome]).to eq(:all)
+ end
+ end
+ end
+
+ describe "--list TAG" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--list", "TAG", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the mode to :list" do
+ @config[:tagger] = nil
+ @script.options ["--list", "TAG", one_spec]
+ expect(@config[:tagger]).to eq(:list)
+ end
+
+ it "sets ltags to include TAG" do
+ @config[:tag] = nil
+ @script.options ["--list", "TAG", one_spec]
+ expect(@config[:ltags]).to eq(["TAG"])
+ end
+ end
+
+ describe "--list-all" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--list-all", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the mode to :list_all" do
+ @config[:tagger] = nil
+ @script.options ["--list-all", one_spec]
+ expect(@config[:tagger]).to eq(:list_all)
+ end
+ end
+
+ describe "--purge" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--purge", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the mode to :purge" do
+ @config[:tagger] = nil
+ @script.options ["--purge", one_spec]
+ expect(@config[:tagger]).to eq(:purge)
+ end
+ end
+end
+
+RSpec.describe MSpecTag, "#run" do
+ before :each do
+ allow(MSpec).to receive(:process)
+
+ options = double("MSpecOptions").as_null_object
+ allow(options).to receive(:parse).and_return(["one", "two"])
+ allow(MSpecOptions).to receive(:new).and_return(options)
+
+ @config = { }
+ @script = MSpecTag.new
+ allow(@script).to receive(:exit)
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:files).and_return(["one", "two"])
+ @script.options
+ end
+
+ it "registers the tags patterns" do
+ @config[:tags_patterns] = [/spec/, "tags"]
+ expect(MSpec).to receive(:register_tags_patterns).with([/spec/, "tags"])
+ @script.run
+ end
+
+ it "registers the files to process" do
+ expect(MSpec).to receive(:register_files).with(["one", "two"])
+ @script.run
+ end
+
+ it "processes the files" do
+ expect(MSpec).to receive(:process)
+ @script.run
+ end
+
+ it "exits with the exit code registered with MSpec" do
+ allow(MSpec).to receive(:exit_code).and_return(7)
+ expect(@script).to receive(:exit).with(7)
+ @script.run
+ end
+end
+
+RSpec.describe MSpecTag, "#register" do
+ before :each do
+ @script = MSpecTag.new
+ @config = @script.config
+ @config[:tag] = "fake:"
+ @config[:atags] = []
+ @config[:astrings] = []
+ @config[:ltags] = ["fails", "unstable"]
+
+ allow(@script).to receive(:files).and_return([])
+ @script.options "fake"
+
+ @t = double("TagAction")
+ allow(@t).to receive(:register)
+
+ @tl = double("TagListAction")
+ allow(@tl).to receive(:register)
+ end
+
+ it "raises an ArgumentError if no recognized action is given" do
+ @config[:tagger] = :totally_whack
+ expect { @script.register }.to raise_error(ArgumentError)
+ end
+
+ describe "when config[:tagger] is the default (:add)" do
+ before :each do
+ @config[:formatter] = false
+ end
+
+ it "creates a TagAction" do
+ expect(TagAction).to receive(:new).and_return(@t)
+ @script.register
+ end
+
+ it "creates a TagAction if config[:tagger] is :del" do
+ @config[:tagger] = :del
+ @config[:outcome] = :pass
+ expect(TagAction).to receive(:new).with(:del, :pass, "fake", nil, [], []).and_return(@t)
+ @script.register
+ end
+
+ it "calls #register on the TagAction instance" do
+ expect(TagAction).to receive(:new).and_return(@t)
+ expect(@t).to receive(:register)
+ @script.register
+ end
+ end
+
+ describe "when config[:tagger] is :list" do
+ before :each do
+ expect(TagListAction).to receive(:new).with(@config[:ltags]).and_return(@tl)
+ @config[:tagger] = :list
+ end
+
+ it "creates a TagListAction" do
+ expect(@tl).to receive(:register)
+ @script.register
+ end
+
+ it "registers MSpec pretend mode" do
+ expect(MSpec).to receive(:register_mode).with(:pretend)
+ @script.register
+ end
+
+ it "sets config[:formatter] to false" do
+ @script.register
+ expect(@config[:formatter]).to be_falsey
+ end
+ end
+
+ describe "when config[:tagger] is :list_all" do
+ before :each do
+ expect(TagListAction).to receive(:new).with(nil).and_return(@tl)
+ @config[:tagger] = :list_all
+ end
+
+ it "creates a TagListAction" do
+ expect(@tl).to receive(:register)
+ @script.register
+ end
+
+ it "registers MSpec pretend mode" do
+ expect(MSpec).to receive(:register_mode).with(:pretend)
+ @script.register
+ end
+
+ it "sets config[:formatter] to false" do
+ @script.register
+ expect(@config[:formatter]).to be_falsey
+ end
+ end
+
+ describe "when config[:tagger] is :purge" do
+ before :each do
+ expect(TagPurgeAction).to receive(:new).and_return(@tl)
+ allow(MSpec).to receive(:register_mode)
+ @config[:tagger] = :purge
+ end
+
+ it "creates a TagPurgeAction" do
+ expect(@tl).to receive(:register)
+ @script.register
+ end
+
+ it "registers MSpec in pretend mode" do
+ expect(MSpec).to receive(:register_mode).with(:pretend)
+ @script.register
+ end
+
+ it "registers MSpec in unguarded mode" do
+ expect(MSpec).to receive(:register_mode).with(:unguarded)
+ @script.register
+ end
+
+ it "sets config[:formatter] to false" do
+ @script.register
+ expect(@config[:formatter]).to be_falsey
+ end
+ end
+end
diff --git a/spec/mspec/spec/expectations/expectations_spec.rb b/spec/mspec/spec/expectations/expectations_spec.rb
new file mode 100644
index 0000000000..371829d4f9
--- /dev/null
+++ b/spec/mspec/spec/expectations/expectations_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+
+RSpec.describe SpecExpectationNotMetError do
+ it "is a subclass of StandardError" do
+ expect(SpecExpectationNotMetError.ancestors).to include(StandardError)
+ end
+end
+
+RSpec.describe SpecExpectationNotFoundError do
+ it "is a subclass of StandardError" do
+ expect(SpecExpectationNotFoundError.ancestors).to include(StandardError)
+ end
+end
+
+RSpec.describe SpecExpectationNotFoundError, "#message" do
+ it "returns 'No behavior expectation was found in the example'" do
+ m = SpecExpectationNotFoundError.new.message
+ expect(m).to eq("No behavior expectation was found in the example")
+ end
+end
+
+RSpec.describe SpecExpectation, "#fail_with" do
+ it "raises an SpecExpectationNotMetError" do
+ expect {
+ SpecExpectation.fail_with "expected this", "to equal that"
+ }.to raise_error(SpecExpectationNotMetError, "expected this to equal that")
+ end
+end
diff --git a/spec/mspec/spec/expectations/should_spec.rb b/spec/mspec/spec/expectations/should_spec.rb
new file mode 100644
index 0000000000..472890979d
--- /dev/null
+++ b/spec/mspec/spec/expectations/should_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+require 'rbconfig'
+
+RSpec.describe "MSpec" do
+ before :all do
+ path = RbConfig::CONFIG['bindir']
+ exe = RbConfig::CONFIG['ruby_install_name']
+ file = File.expand_path('../../fixtures/should.rb', __FILE__)
+ @out = `#{path}/#{exe} #{file}`
+ end
+
+ describe "#should" do
+ it "records failures" do
+ expect(@out).to include <<-EOS
+1)
+MSpec expectation method #should causes a failure to be recorded FAILED
+Expected 1 == 2
+to be truthy but was false
+EOS
+ end
+
+ it "raises exceptions for examples with no expectations" do
+ expect(@out).to include <<-EOS
+2)
+MSpec expectation method #should registers that an expectation has been encountered FAILED
+No behavior expectation was found in the example
+EOS
+ end
+ end
+
+ describe "#should_not" do
+ it "records failures" do
+ expect(@out).to include <<-EOS
+3)
+MSpec expectation method #should_not causes a failure to be recorded FAILED
+Expected 1 == 1
+to be falsy but was true
+EOS
+ end
+
+ it "raises exceptions for examples with no expectations" do
+ expect(@out).to include <<-EOS
+4)
+MSpec expectation method #should_not registers that an expectation has been encountered FAILED
+No behavior expectation was found in the example
+EOS
+ end
+ end
+
+ it "prints status information" do
+ expect(@out).to include ".FF..FF."
+ end
+
+ it "prints out a summary" do
+ expect(@out).to include "0 files, 8 examples, 6 expectations, 4 failures, 0 errors"
+ end
+
+ it "records expectations" do
+ expect(@out).to include "I was called 6 times"
+ end
+end
diff --git a/spec/mspec/spec/fixtures/a_spec.rb b/spec/mspec/spec/fixtures/a_spec.rb
new file mode 100644
index 0000000000..17a7e8b664
--- /dev/null
+++ b/spec/mspec/spec/fixtures/a_spec.rb
@@ -0,0 +1,15 @@
+unless defined?(RSpec)
+ describe "Foo#bar" do
+ it "passes" do
+ 1.should == 1
+ end
+
+ it "errors" do
+ 1.should == 2
+ end
+
+ it "fails" do
+ raise "failure"
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/b_spec.rb b/spec/mspec/spec/fixtures/b_spec.rb
new file mode 100644
index 0000000000..f1f63317cb
--- /dev/null
+++ b/spec/mspec/spec/fixtures/b_spec.rb
@@ -0,0 +1,7 @@
+unless defined?(RSpec)
+ describe "Bar#baz" do
+ it "works" do
+ 1.should == 1
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/chatty_spec.rb b/spec/mspec/spec/fixtures/chatty_spec.rb
new file mode 100644
index 0000000000..2d110d8ce4
--- /dev/null
+++ b/spec/mspec/spec/fixtures/chatty_spec.rb
@@ -0,0 +1,8 @@
+unless defined?(RSpec)
+ describe "Chatty#spec" do
+ it "prints too much" do
+ STDOUT.puts "Hello\nIt's me!"
+ 1.should == 1
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/config.mspec b/spec/mspec/spec/fixtures/config.mspec
new file mode 100644
index 0000000000..01654c5094
--- /dev/null
+++ b/spec/mspec/spec/fixtures/config.mspec
@@ -0,0 +1,8 @@
+class MSpecScript
+ set :target, 'ruby'
+
+ set :tags_patterns, [
+ [%r(spec/fixtures/), 'spec/fixtures/tags/'],
+ [/_spec.rb$/, '_tags.txt']
+ ]
+end
diff --git a/spec/mspec/spec/fixtures/die_spec.rb b/spec/mspec/spec/fixtures/die_spec.rb
new file mode 100644
index 0000000000..0f66793274
--- /dev/null
+++ b/spec/mspec/spec/fixtures/die_spec.rb
@@ -0,0 +1,7 @@
+unless defined?(RSpec)
+ describe "Deadly#spec" do
+ it "dies" do
+ abort "DEAD"
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/my_ruby b/spec/mspec/spec/fixtures/my_ruby
new file mode 100755
index 0000000000..eeda3eeeec
--- /dev/null
+++ b/spec/mspec/spec/fixtures/my_ruby
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+echo $RUBY_EXE
+exec ruby "$@"
diff --git a/spec/mspec/spec/fixtures/object_methods_spec.rb b/spec/mspec/spec/fixtures/object_methods_spec.rb
new file mode 100644
index 0000000000..9b7c1523e5
--- /dev/null
+++ b/spec/mspec/spec/fixtures/object_methods_spec.rb
@@ -0,0 +1,8 @@
+unless defined?(RSpec)
+ describe "Object" do
+ it ".public_instance_methods(false) is empty" do
+ Object.public_instance_methods(false).sort.should ==
+ [:should, :should_not, :should_not_receive, :should_receive, :stub!]
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/print_interpreter_spec.rb b/spec/mspec/spec/fixtures/print_interpreter_spec.rb
new file mode 100644
index 0000000000..a662346d0a
--- /dev/null
+++ b/spec/mspec/spec/fixtures/print_interpreter_spec.rb
@@ -0,0 +1,4 @@
+unless defined?(RSpec)
+ puts ENV["RUBY_EXE"]
+ puts ruby_cmd(nil).split.first
+end
diff --git a/spec/mspec/spec/fixtures/should.rb b/spec/mspec/spec/fixtures/should.rb
new file mode 100644
index 0000000000..f494775c5f
--- /dev/null
+++ b/spec/mspec/spec/fixtures/should.rb
@@ -0,0 +1,75 @@
+$: << File.dirname(__FILE__) + '/../../lib'
+require 'mspec'
+require 'mspec/utils/script'
+
+# The purpose of these specs is to confirm that the #should
+# and #should_not methods are functioning appropriately. We
+# use a separate spec file that is invoked from the MSpec
+# specs but is run by MSpec. This avoids conflicting with
+# RSpec's #should and #should_not methods.
+
+raise "RSpec should not be loaded" if defined?(RSpec)
+
+class ShouldSpecsMonitor
+ def initialize
+ @called = 0
+ end
+
+ def expectation(state)
+ @called += 1
+ end
+
+ def finish
+ puts "I was called #{@called} times"
+ end
+end
+
+# Simplistic runner
+formatter = DottedFormatter.new
+formatter.register
+
+monitor = ShouldSpecsMonitor.new
+MSpec.register :expectation, monitor
+MSpec.register :finish, monitor
+
+at_exit { MSpec.actions :finish }
+
+MSpec.actions :start
+MSpec.setup_env
+
+# Specs
+describe "MSpec expectation method #should" do
+ it "accepts a matcher" do
+ :sym.should be_kind_of(Symbol)
+ end
+
+ it "causes a failure to be recorded" do
+ 1.should == 2
+ end
+
+ it "registers that an expectation has been encountered" do
+ # an empty example block causes an exception because
+ # no expectation was encountered
+ end
+
+ it "invokes the MSpec :expectation actions" do
+ 1.should == 1
+ end
+end
+
+describe "MSpec expectation method #should_not" do
+ it "accepts a matcher" do
+ "sym".should_not be_kind_of(Symbol)
+ end
+
+ it "causes a failure to be recorded" do
+ 1.should_not == 1
+ end
+
+ it "registers that an expectation has been encountered" do
+ end
+
+ it "invokes the MSpec :expectation actions" do
+ 1.should_not == 2
+ end
+end
diff --git a/spec/mspec/spec/fixtures/tagging_spec.rb b/spec/mspec/spec/fixtures/tagging_spec.rb
new file mode 100644
index 0000000000..0097fd1808
--- /dev/null
+++ b/spec/mspec/spec/fixtures/tagging_spec.rb
@@ -0,0 +1,16 @@
+# encoding: utf-8
+unless defined?(RSpec)
+ describe "Tag#me" do
+ it "passes" do
+ 1.should == 1
+ end
+
+ it "errors" do
+ 1.should == 2
+ end
+
+ it "érròrs in unicode" do
+ 1.should == 2
+ end
+ end
+end
diff --git a/spec/mspec/spec/guards/block_device_spec.rb b/spec/mspec/spec/guards/block_device_spec.rb
new file mode 100644
index 0000000000..dd420d4a81
--- /dev/null
+++ b/spec/mspec/spec/guards/block_device_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#with_block_device" do
+ before :each do
+ ScratchPad.clear
+
+ @guard = BlockDeviceGuard.new
+ allow(BlockDeviceGuard).to receive(:new).and_return(@guard)
+ end
+
+ platform_is_not :freebsd, :windows do
+ it "yields if block device is available" do
+ expect(@guard).to receive(:`).and_return("block devices")
+ with_block_device { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield if block device is not available" do
+ expect(@guard).to receive(:`).and_return(nil)
+ with_block_device { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+ end
+
+ platform_is :freebsd, :windows do
+ it "does not yield, since platform does not support block devices" do
+ expect(@guard).not_to receive(:`)
+ with_block_device { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+ end
+
+ it "sets the name of the guard to :with_block_device" do
+ with_block_device { }
+ expect(@guard.name).to eq(:with_block_device)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ with_block_device { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/bug_spec.rb b/spec/mspec/spec/guards/bug_spec.rb
new file mode 100644
index 0000000000..72a3405dbc
--- /dev/null
+++ b/spec/mspec/spec/guards/bug_spec.rb
@@ -0,0 +1,151 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe BugGuard, "#match? when #implementation? is 'ruby'" do
+ before :all do
+ @verbose = $VERBOSE
+ $VERBOSE = nil
+ end
+
+ after :all do
+ $VERBOSE = @verbose
+ end
+
+ before :each do
+ hide_deprecation_warnings
+ stub_const "VersionGuard::FULL_RUBY_VERSION", SpecVersion.new('1.8.6')
+ @ruby_engine = Object.const_get :RUBY_ENGINE
+ Object.const_set :RUBY_ENGINE, 'ruby'
+ end
+
+ after :each do
+ Object.const_set :RUBY_ENGINE, @ruby_engine
+ end
+
+ it "returns false when version argument is less than RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.5").match?).to eq(false)
+ end
+
+ it "returns true when version argument is equal to RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(true)
+ end
+
+ it "returns true when version argument is greater than RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.7").match?).to eq(true)
+ end
+
+ it "returns true when version argument implicitly includes RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8").match?).to eq(true)
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(true)
+ end
+
+ it "returns true when the argument range includes RUBY_VERSION" do
+ expect(BugGuard.new("#1", '1.8.5'..'1.8.7').match?).to eq(true)
+ expect(BugGuard.new("#1", '1.8'..'1.9').match?).to eq(true)
+ expect(BugGuard.new("#1", '1.8'...'1.9').match?).to eq(true)
+ expect(BugGuard.new("#1", '1.8'..'1.8.6').match?).to eq(true)
+ expect(BugGuard.new("#1", '1.8.5'..'1.8.6').match?).to eq(true)
+ expect(BugGuard.new("#1", ''...'1.8.7').match?).to eq(true)
+ end
+
+ it "returns false when the argument range does not include RUBY_VERSION" do
+ expect(BugGuard.new("#1", '1.8.7'..'1.8.9').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.4'..'1.8.5').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.4'...'1.8.6').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.5'...'1.8.6').match?).to eq(false)
+ expect(BugGuard.new("#1", ''...'1.8.6').match?).to eq(false)
+ end
+
+ it "returns false when MSpec.mode?(:no_ruby_bug) is true" do
+ expect(MSpec).to receive(:mode?).with(:no_ruby_bug).twice.and_return(:true)
+ expect(BugGuard.new("#1", "1.8.5").match?).to eq(false)
+ expect(BugGuard.new("#1", "1.8").match?).to eq(false)
+ end
+end
+
+RSpec.describe BugGuard, "#match? when #implementation? is not 'ruby'" do
+ before :all do
+ @verbose = $VERBOSE
+ $VERBOSE = nil
+ end
+
+ after :all do
+ $VERBOSE = @verbose
+ end
+
+ before :each do
+ hide_deprecation_warnings
+ @ruby_version = Object.const_get :RUBY_VERSION
+ @ruby_engine = Object.const_get :RUBY_ENGINE
+
+ Object.const_set :RUBY_VERSION, '1.8.6'
+ Object.const_set :RUBY_ENGINE, 'jruby'
+ end
+
+ after :each do
+ Object.const_set :RUBY_VERSION, @ruby_version
+ Object.const_set :RUBY_ENGINE, @ruby_engine
+ end
+
+ it "returns false when version argument is less than RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8").match?).to eq(false)
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(false)
+ end
+
+ it "returns false when version argument is equal to RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(false)
+ end
+
+ it "returns false when version argument is greater than RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.7").match?).to eq(false)
+ end
+
+ it "returns false no matter if the argument range includes RUBY_VERSION" do
+ expect(BugGuard.new("#1", '1.8'...'1.9').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.5'...'1.8.7').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.4'...'1.8.6').match?).to eq(false)
+ end
+
+ it "returns false when MSpec.mode?(:no_ruby_bug) is true" do
+ allow(MSpec).to receive(:mode?).and_return(:true)
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(false)
+ end
+end
+
+RSpec.describe Object, "#ruby_bug" do
+ before :each do
+ hide_deprecation_warnings
+ @guard = BugGuard.new "#1234", "x.x.x"
+ allow(BugGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields when #match? returns false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ ruby_bug("#1234", "1.8.6") { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when #match? returns true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ ruby_bug("#1234", "1.8.6") { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "requires a bug tracker number and a version number" do
+ expect { ruby_bug { } }.to raise_error(ArgumentError)
+ expect { ruby_bug("#1234") { } }.to raise_error(ArgumentError)
+ end
+
+ it "sets the name of the guard to :ruby_bug" do
+ ruby_bug("#1234", "1.8.6") { }
+ expect(@guard.name).to eq(:ruby_bug)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:unregister)
+ expect do
+ ruby_bug("", "") { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/conflict_spec.rb b/spec/mspec/spec/guards/conflict_spec.rb
new file mode 100644
index 0000000000..7dbe83153d
--- /dev/null
+++ b/spec/mspec/spec/guards/conflict_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#conflicts_with" do
+ before :each do
+ hide_deprecation_warnings
+ ScratchPad.clear
+ end
+
+ it "does not yield if Object.constants includes any of the arguments" do
+ allow(Object).to receive(:constants).and_return(["SomeClass", "OtherClass"])
+ conflicts_with(:SomeClass, :AClass, :BClass) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "does not yield if Object.constants (as Symbols) includes any of the arguments" do
+ allow(Object).to receive(:constants).and_return([:SomeClass, :OtherClass])
+ conflicts_with(:SomeClass, :AClass, :BClass) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields if Object.constants does not include any of the arguments" do
+ allow(Object).to receive(:constants).and_return(["SomeClass", "OtherClass"])
+ conflicts_with(:AClass, :BClass) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "yields if Object.constants (as Symbols) does not include any of the arguments" do
+ allow(Object).to receive(:constants).and_return([:SomeClass, :OtherClass])
+ conflicts_with(:AClass, :BClass) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+end
+
+RSpec.describe Object, "#conflicts_with" do
+ before :each do
+ hide_deprecation_warnings
+ @guard = ConflictsGuard.new
+ allow(ConflictsGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "sets the name of the guard to :conflicts_with" do
+ conflicts_with(:AClass, :BClass) { }
+ expect(@guard.name).to eq(:conflicts_with)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:unregister)
+ expect do
+ conflicts_with(:AClass, :BClass) { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/endian_spec.rb b/spec/mspec/spec/guards/endian_spec.rb
new file mode 100644
index 0000000000..943b558ed3
--- /dev/null
+++ b/spec/mspec/spec/guards/endian_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#big_endian" do
+ before :each do
+ @guard = BigEndianGuard.new
+ allow(BigEndianGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields on big-endian platforms" do
+ allow(@guard).to receive(:pattern).and_return([?\001])
+ big_endian { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield on little-endian platforms" do
+ allow(@guard).to receive(:pattern).and_return([?\000])
+ big_endian { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "sets the name of the guard to :big_endian" do
+ big_endian { }
+ expect(@guard.name).to eq(:big_endian)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ allow(@guard).to receive(:pattern).and_return([?\001])
+ expect(@guard).to receive(:unregister)
+ expect do
+ big_endian { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#little_endian" do
+ before :each do
+ @guard = BigEndianGuard.new
+ allow(BigEndianGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields on little-endian platforms" do
+ allow(@guard).to receive(:pattern).and_return([?\000])
+ little_endian { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield on big-endian platforms" do
+ allow(@guard).to receive(:pattern).and_return([?\001])
+ little_endian { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+end
diff --git a/spec/mspec/spec/guards/feature_spec.rb b/spec/mspec/spec/guards/feature_spec.rb
new file mode 100644
index 0000000000..fcb8997591
--- /dev/null
+++ b/spec/mspec/spec/guards/feature_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe FeatureGuard, ".enabled?" do
+ it "returns true if the feature is enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(true)
+ expect(FeatureGuard.enabled?(:encoding)).to be_truthy
+ end
+
+ it "returns false if the feature is not enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(false)
+ expect(FeatureGuard.enabled?(:encoding)).to be_falsey
+ end
+
+ it "returns true if all the features are enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true)
+ expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(true)
+ expect(FeatureGuard.enabled?(:one, :two)).to be_truthy
+ end
+
+ it "returns false if any of the features are not enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true)
+ expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(false)
+ expect(FeatureGuard.enabled?(:one, :two)).to be_falsey
+ end
+end
+
+RSpec.describe Object, "#with_feature" do
+ before :each do
+ ScratchPad.clear
+
+ @guard = FeatureGuard.new :encoding
+ allow(FeatureGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "sets the name of the guard to :with_feature" do
+ with_feature(:encoding) { }
+ expect(@guard.name).to eq(:with_feature)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ with_feature { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#with_feature" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "yields if the feature is enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(true)
+ with_feature(:encoding) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "yields if all the features are enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true)
+ expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(true)
+ with_feature(:one, :two) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield if the feature is not enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(false)
+ with_feature(:encoding) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to be_nil
+ end
+
+ it "does not yield if any of the features are not enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true)
+ expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(false)
+ with_feature(:one, :two) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to be_nil
+ end
+end
+
+RSpec.describe Object, "#without_feature" do
+ before :each do
+ ScratchPad.clear
+
+ @guard = FeatureGuard.new :encoding
+ allow(FeatureGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "sets the name of the guard to :without_feature" do
+ without_feature(:encoding) { }
+ expect(@guard.name).to eq(:without_feature)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(false)
+ expect(@guard).to receive(:unregister)
+ expect do
+ without_feature { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#without_feature" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "does not yield if the feature is enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(true)
+ without_feature(:encoding) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to be_nil
+ end
+
+ it "yields if the feature is disabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(false)
+ without_feature(:encoding) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+end
diff --git a/spec/mspec/spec/guards/guard_spec.rb b/spec/mspec/spec/guards/guard_spec.rb
new file mode 100644
index 0000000000..e29d235747
--- /dev/null
+++ b/spec/mspec/spec/guards/guard_spec.rb
@@ -0,0 +1,421 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'rbconfig'
+
+RSpec.describe SpecGuard, ".ruby_version" do
+ before :each do
+ stub_const "RUBY_VERSION", "8.2.3"
+ end
+
+ it "returns the full version for :full" do
+ expect(SpecGuard.ruby_version(:full)).to eq("8.2.3")
+ end
+
+ it "returns major.minor.tiny for :tiny" do
+ expect(SpecGuard.ruby_version(:tiny)).to eq("8.2.3")
+ end
+
+ it "returns major.minor.tiny for :teeny" do
+ expect(SpecGuard.ruby_version(:tiny)).to eq("8.2.3")
+ end
+
+ it "returns major.minor for :minor" do
+ expect(SpecGuard.ruby_version(:minor)).to eq("8.2")
+ end
+
+ it "defaults to :minor" do
+ expect(SpecGuard.ruby_version).to eq("8.2")
+ end
+
+ it "returns major for :major" do
+ expect(SpecGuard.ruby_version(:major)).to eq("8")
+ end
+end
+
+RSpec.describe SpecGuard, "#yield?" do
+ before :each do
+ MSpec.clear_modes
+ @guard = SpecGuard.new
+ allow(@guard).to receive(:match?).and_return(false)
+ end
+
+ after :each do
+ MSpec.unregister :add, @guard
+ MSpec.clear_modes
+ SpecGuard.clear_guards
+ end
+
+ it "returns true if MSpec.mode?(:unguarded) is true" do
+ MSpec.register_mode :unguarded
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:verify) is true" do
+ MSpec.register_mode :verify
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:verify) is true regardless of invert being true" do
+ MSpec.register_mode :verify
+ expect(@guard.yield?(true)).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:report) is true" do
+ MSpec.register_mode :report
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:report) is true regardless of invert being true" do
+ MSpec.register_mode :report
+ expect(@guard.yield?(true)).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:report_on) is true and SpecGuards.guards contains the named guard" do
+ MSpec.register_mode :report_on
+ SpecGuard.guards << :guard_name
+ expect(@guard.yield?).to eq(false)
+ @guard.name = :guard_name
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns #match? if neither report nor verify mode are true" do
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.yield?).to eq(false)
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns #match? if invert is true and neither report nor verify mode are true" do
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.yield?(true)).to eq(true)
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.yield?(true)).to eq(false)
+ end
+end
+
+RSpec.describe SpecGuard, "#match?" do
+ before :each do
+ @guard = SpecGuard.new
+ end
+
+ it "must be implemented in subclasses" do
+ expect {
+ @guard.match?
+ }.to raise_error("must be implemented by the subclass")
+ end
+end
+
+RSpec.describe SpecGuard, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ @guard = SpecGuard.new
+ end
+
+ it "unregisters from MSpec :add actions" do
+ expect(MSpec).to receive(:unregister).with(:add, @guard)
+ @guard.unregister
+ end
+end
+
+RSpec.describe SpecGuard, "#record" do
+ after :each do
+ SpecGuard.clear
+ end
+
+ it "saves the name of the guarded spec under the name of the guard" do
+ guard = SpecGuard.new "a", "1.8"..."1.9"
+ guard.name = :named_guard
+ guard.record "SomeClass#action returns true"
+ expect(SpecGuard.report).to eq({
+ 'named_guard a, 1.8...1.9' => ["SomeClass#action returns true"]
+ })
+ end
+end
+
+RSpec.describe SpecGuard, ".guards" do
+ it "returns an Array" do
+ expect(SpecGuard.guards).to be_kind_of(Array)
+ end
+end
+
+RSpec.describe SpecGuard, ".clear_guards" do
+ it "resets the array to empty" do
+ SpecGuard.guards << :guard
+ expect(SpecGuard.guards).to eq([:guard])
+ SpecGuard.clear_guards
+ expect(SpecGuard.guards).to eq([])
+ end
+end
+
+RSpec.describe SpecGuard, ".finish" do
+ before :each do
+ $stdout = @out = IOStub.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ SpecGuard.clear
+ end
+
+ it "prints the descriptions of the guarded specs" do
+ guard = SpecGuard.new "a", "1.8"..."1.9"
+ guard.name = :named_guard
+ guard.record "SomeClass#action returns true"
+ guard.record "SomeClass#reverse returns false"
+ SpecGuard.finish
+ expect($stdout).to eq(%[
+
+2 specs omitted by guard: named_guard a, 1.8...1.9:
+
+SomeClass#action returns true
+SomeClass#reverse returns false
+
+])
+ end
+end
+
+RSpec.describe SpecGuard, ".run_if" do
+ before :each do
+ @guard = SpecGuard.new
+ ScratchPad.clear
+ end
+
+ it "yields if match? returns true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ @guard.run_if(:name) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield if match? returns false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ @guard.run_if(:name) { fail }
+ end
+
+ it "returns the result of the block if match? is true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.run_if(:name) { 42 }).to eq(42)
+ end
+
+ it "returns nil if given a block and match? is false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.run_if(:name) { 42 }).to eq(nil)
+ end
+
+ it "returns what #match? returns when no block is given" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.run_if(:name)).to eq(true)
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.run_if(:name)).to eq(false)
+ end
+end
+
+RSpec.describe SpecGuard, ".run_unless" do
+ before :each do
+ @guard = SpecGuard.new
+ ScratchPad.clear
+ end
+
+ it "yields if match? returns false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ @guard.run_unless(:name) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield if match? returns true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ @guard.run_unless(:name) { fail }
+ end
+
+ it "returns the result of the block if match? is false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.run_unless(:name) { 42 }).to eq(42)
+ end
+
+ it "returns nil if given a block and match? is true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.run_unless(:name) { 42 }).to eq(nil)
+ end
+
+ it "returns the opposite of what #match? returns when no block is given" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.run_unless(:name)).to eq(false)
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.run_unless(:name)).to eq(true)
+ end
+end
+
+RSpec.describe Object, "#guard" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ after :each do
+ MSpec.clear_modes
+ end
+
+ it "allows to combine guards" do
+ guard1 = VersionGuard.new '1.2.3', 'x.x.x'
+ allow(VersionGuard).to receive(:new).and_return(guard1)
+ guard2 = PlatformGuard.new :dummy
+ allow(PlatformGuard).to receive(:new).and_return(guard2)
+
+ allow(guard1).to receive(:match?).and_return(true)
+ allow(guard2).to receive(:match?).and_return(true)
+ guard -> { ruby_version_is "2.4" and platform_is :linux } do
+ ScratchPad.record :yield
+ end
+ expect(ScratchPad.recorded).to eq(:yield)
+
+ allow(guard1).to receive(:match?).and_return(false)
+ allow(guard2).to receive(:match?).and_return(true)
+ guard -> { ruby_version_is "2.4" and platform_is :linux } do
+ fail
+ end
+
+ allow(guard1).to receive(:match?).and_return(true)
+ allow(guard2).to receive(:match?).and_return(false)
+ guard -> { ruby_version_is "2.4" and platform_is :linux } do
+ fail
+ end
+
+ allow(guard1).to receive(:match?).and_return(false)
+ allow(guard2).to receive(:match?).and_return(false)
+ guard -> { ruby_version_is "2.4" and platform_is :linux } do
+ fail
+ end
+ end
+
+ it "yields when the Proc returns true" do
+ guard -> { true } do
+ ScratchPad.record :yield
+ end
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when the Proc returns false" do
+ guard -> { false } do
+ fail
+ end
+ end
+
+ it "yields if MSpec.mode?(:unguarded) is true" do
+ MSpec.register_mode :unguarded
+
+ guard -> { false } do
+ ScratchPad.record :yield1
+ end
+ expect(ScratchPad.recorded).to eq(:yield1)
+
+ guard -> { true } do
+ ScratchPad.record :yield2
+ end
+ expect(ScratchPad.recorded).to eq(:yield2)
+ end
+
+ it "yields if MSpec.mode?(:verify) is true" do
+ MSpec.register_mode :verify
+
+ guard -> { false } do
+ ScratchPad.record :yield1
+ end
+ expect(ScratchPad.recorded).to eq(:yield1)
+
+ guard -> { true } do
+ ScratchPad.record :yield2
+ end
+ expect(ScratchPad.recorded).to eq(:yield2)
+ end
+
+ it "yields if MSpec.mode?(:report) is true" do
+ MSpec.register_mode :report
+
+ guard -> { false } do
+ ScratchPad.record :yield1
+ end
+ expect(ScratchPad.recorded).to eq(:yield1)
+
+ guard -> { true } do
+ ScratchPad.record :yield2
+ end
+ expect(ScratchPad.recorded).to eq(:yield2)
+ end
+
+ it "raises an error if no Proc is given" do
+ expect { guard :foo }.to raise_error(RuntimeError)
+ end
+
+ it "requires a block" do
+ expect {
+ guard(-> { true })
+ }.to raise_error(LocalJumpError)
+ expect {
+ guard(-> { false })
+ }.to raise_error(LocalJumpError)
+ end
+end
+
+RSpec.describe Object, "#guard_not" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "allows to combine guards" do
+ guard1 = VersionGuard.new '1.2.3', 'x.x.x'
+ allow(VersionGuard).to receive(:new).and_return(guard1)
+ guard2 = PlatformGuard.new :dummy
+ allow(PlatformGuard).to receive(:new).and_return(guard2)
+
+ allow(guard1).to receive(:match?).and_return(true)
+ allow(guard2).to receive(:match?).and_return(true)
+ guard_not -> { ruby_version_is "2.4" and platform_is :linux } do
+ fail
+ end
+
+ allow(guard1).to receive(:match?).and_return(false)
+ allow(guard2).to receive(:match?).and_return(true)
+ guard_not -> { ruby_version_is "2.4" and platform_is :linux } do
+ ScratchPad.record :yield1
+ end
+ expect(ScratchPad.recorded).to eq(:yield1)
+
+ allow(guard1).to receive(:match?).and_return(true)
+ allow(guard2).to receive(:match?).and_return(false)
+ guard_not -> { ruby_version_is "2.4" and platform_is :linux } do
+ ScratchPad.record :yield2
+ end
+ expect(ScratchPad.recorded).to eq(:yield2)
+
+ allow(guard1).to receive(:match?).and_return(false)
+ allow(guard2).to receive(:match?).and_return(false)
+ guard_not -> { ruby_version_is "2.4" and platform_is :linux } do
+ ScratchPad.record :yield3
+ end
+ expect(ScratchPad.recorded).to eq(:yield3)
+ end
+
+ it "yields when the Proc returns false" do
+ guard_not -> { false } do
+ ScratchPad.record :yield
+ end
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when the Proc returns true" do
+ guard_not -> { true } do
+ fail
+ end
+ end
+
+ it "raises an error if no Proc is given" do
+ expect { guard_not :foo }.to raise_error(RuntimeError)
+ end
+
+ it "requires a block" do
+ expect {
+ guard_not(-> { true })
+ }.to raise_error(LocalJumpError)
+ expect {
+ guard_not(-> { false })
+ }.to raise_error(LocalJumpError)
+ end
+end
diff --git a/spec/mspec/spec/guards/platform_spec.rb b/spec/mspec/spec/guards/platform_spec.rb
new file mode 100644
index 0000000000..bd37432800
--- /dev/null
+++ b/spec/mspec/spec/guards/platform_spec.rb
@@ -0,0 +1,337 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#platform_is" do
+ before :each do
+ @guard = PlatformGuard.new :dummy
+ allow(PlatformGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "does not yield when #os? returns false" do
+ allow(PlatformGuard).to receive(:os?).and_return(false)
+ platform_is(:ruby) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields when #os? returns true" do
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ platform_is(:solarce) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "returns what #os? returns when no block is given" do
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ expect(platform_is(:solarce)).to eq(true)
+ allow(PlatformGuard).to receive(:os?).and_return(false)
+ expect(platform_is(:solarce)).to eq(false)
+ end
+
+ it "sets the name of the guard to :platform_is" do
+ platform_is(:solarce) { }
+ expect(@guard.name).to eq(:platform_is)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ platform_is(:solarce) { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#platform_is_not" do
+ before :each do
+ @guard = PlatformGuard.new :dummy
+ allow(PlatformGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "does not yield when #os? returns true" do
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ platform_is_not(:ruby) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields when #os? returns false" do
+ allow(PlatformGuard).to receive(:os?).and_return(false)
+ platform_is_not(:solarce) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "returns the opposite of what #os? returns when no block is given" do
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ expect(platform_is_not(:solarce)).to eq(false)
+ allow(PlatformGuard).to receive(:os?).and_return(false)
+ expect(platform_is_not(:solarce)).to eq(true)
+ end
+
+ it "sets the name of the guard to :platform_is_not" do
+ platform_is_not(:solarce) { }
+ expect(@guard.name).to eq(:platform_is_not)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(false)
+ expect(@guard).to receive(:unregister)
+ expect do
+ platform_is_not(:solarce) { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#platform_is :c_long_size => SIZE_SPEC" do
+ before :each do
+ @guard = PlatformGuard.new :darwin, :c_long_size => 32
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ allow(PlatformGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields when #c_long_size? returns true" do
+ allow(PlatformGuard).to receive(:c_long_size?).and_return(true)
+ platform_is(:c_long_size => 32) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "doesn not yield when #c_long_size? returns false" do
+ allow(PlatformGuard).to receive(:c_long_size?).and_return(false)
+ platform_is(:c_long_size => 32) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+end
+
+RSpec.describe Object, "#platform_is_not :c_long_size => SIZE_SPEC" do
+ before :each do
+ @guard = PlatformGuard.new :darwin, :c_long_size => 32
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ allow(PlatformGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields when #c_long_size? returns false" do
+ allow(PlatformGuard).to receive(:c_long_size?).and_return(false)
+ platform_is_not(:c_long_size => 32) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "doesn not yield when #c_long_size? returns true" do
+ allow(PlatformGuard).to receive(:c_long_size?).and_return(true)
+ platform_is_not(:c_long_size => 32) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+end
+
+RSpec.describe PlatformGuard, ".implementation?" do
+ it "returns true if passed :ruby and RUBY_ENGINE == 'ruby'" do
+ stub_const 'RUBY_ENGINE', 'ruby'
+ expect(PlatformGuard.implementation?(:ruby)).to eq(true)
+ end
+
+ it "returns true if passed :rubinius and RUBY_ENGINE == 'rbx'" do
+ stub_const 'RUBY_ENGINE', 'rbx'
+ expect(PlatformGuard.implementation?(:rubinius)).to eq(true)
+ end
+
+ it "returns true if passed :jruby and RUBY_ENGINE == 'jruby'" do
+ stub_const 'RUBY_ENGINE', 'jruby'
+ expect(PlatformGuard.implementation?(:jruby)).to eq(true)
+ end
+
+ it "returns true if passed :ironruby and RUBY_ENGINE == 'ironruby'" do
+ stub_const 'RUBY_ENGINE', 'ironruby'
+ expect(PlatformGuard.implementation?(:ironruby)).to eq(true)
+ end
+
+ it "returns true if passed :maglev and RUBY_ENGINE == 'maglev'" do
+ stub_const 'RUBY_ENGINE', 'maglev'
+ expect(PlatformGuard.implementation?(:maglev)).to eq(true)
+ end
+
+ it "returns true if passed :topaz and RUBY_ENGINE == 'topaz'" do
+ stub_const 'RUBY_ENGINE', 'topaz'
+ expect(PlatformGuard.implementation?(:topaz)).to eq(true)
+ end
+
+ it "returns true if passed :ruby and RUBY_ENGINE matches /^ruby/" do
+ stub_const 'RUBY_ENGINE', 'ruby'
+ expect(PlatformGuard.implementation?(:ruby)).to eq(true)
+
+ stub_const 'RUBY_ENGINE', 'ruby1.8'
+ expect(PlatformGuard.implementation?(:ruby)).to eq(true)
+
+ stub_const 'RUBY_ENGINE', 'ruby1.9'
+ expect(PlatformGuard.implementation?(:ruby)).to eq(true)
+ end
+
+ it "works for an unrecognized name" do
+ stub_const 'RUBY_ENGINE', 'myrubyimplementation'
+ expect(PlatformGuard.implementation?(:myrubyimplementation)).to eq(true)
+ expect(PlatformGuard.implementation?(:other)).to eq(false)
+ end
+end
+
+RSpec.describe PlatformGuard, ".standard?" do
+ it "returns true if implementation? returns true" do
+ expect(PlatformGuard).to receive(:implementation?).with(:ruby).and_return(true)
+ expect(PlatformGuard.standard?).to be_truthy
+ end
+
+ it "returns false if implementation? returns false" do
+ expect(PlatformGuard).to receive(:implementation?).with(:ruby).and_return(false)
+ expect(PlatformGuard.standard?).to be_falsey
+ end
+end
+
+RSpec.describe PlatformGuard, ".c_long_size?" do
+ it "returns true when arg is 32 and 1.size is 4" do
+ expect(PlatformGuard.c_long_size?(32)).to eq(1.size == 4)
+ end
+
+ it "returns true when arg is 64 and 1.size is 8" do
+ expect(PlatformGuard.c_long_size?(64)).to eq(1.size == 8)
+ end
+end
+
+RSpec.describe PlatformGuard, ".os?" do
+ before :each do
+ stub_const 'PlatformGuard::PLATFORM', 'solarce'
+ end
+
+ it "returns false when arg does not match the platform" do
+ expect(PlatformGuard.os?(:ruby)).to eq(false)
+ end
+
+ it "returns false when no arg matches the platform" do
+ expect(PlatformGuard.os?(:ruby, :jruby, :rubinius, :maglev)).to eq(false)
+ end
+
+ it "returns true when arg matches the platform" do
+ expect(PlatformGuard.os?(:solarce)).to eq(true)
+ end
+
+ it "returns true when any arg matches the platform" do
+ expect(PlatformGuard.os?(:ruby, :jruby, :solarce, :rubinius, :maglev)).to eq(true)
+ end
+
+ it "returns true when arg is :windows and the platform contains 'mswin'" do
+ stub_const 'PlatformGuard::PLATFORM', 'mswin32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns true when arg is :windows and the platform contains 'mingw'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mswin'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mswin32'
+ expect(PlatformGuard.os?(:linux)).to eq(false)
+ end
+
+ it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mingw'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:linux)).to eq(false)
+ end
+end
+
+RSpec.describe PlatformGuard, ".os?" do
+ it "returns true if called with the current OS or architecture" do
+ os = RbConfig::CONFIG["host_os"].sub("-gnu", "")
+ arch = RbConfig::CONFIG["host_arch"]
+ expect(PlatformGuard.os?(os)).to eq(true)
+ expect(PlatformGuard.os?(arch)).to eq(true)
+ expect(PlatformGuard.os?("#{arch}-#{os}")).to eq(true)
+ end
+end
+
+RSpec.describe PlatformGuard, ".os? on JRuby" do
+ before :all do
+ @verbose = $VERBOSE
+ $VERBOSE = nil
+ end
+
+ after :all do
+ $VERBOSE = @verbose
+ end
+
+ before :each do
+ @ruby_platform = Object.const_get :RUBY_PLATFORM
+ Object.const_set :RUBY_PLATFORM, 'java'
+ end
+
+ after :each do
+ Object.const_set :RUBY_PLATFORM, @ruby_platform
+ end
+
+ it "raises an error when testing for a :java platform" do
+ expect {
+ PlatformGuard.os?(:java)
+ }.to raise_error(":java is not a valid OS")
+ end
+
+ it "returns true when arg is :windows and RUBY_PLATFORM contains 'java' and os?(:windows) is true" do
+ stub_const 'PlatformGuard::PLATFORM', 'mswin32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns true when RUBY_PLATFORM contains 'java' and os?(argument) is true" do
+ stub_const 'PlatformGuard::PLATFORM', 'amiga'
+ expect(PlatformGuard.os?(:amiga)).to eq(true)
+ end
+end
+
+RSpec.describe PlatformGuard, ".os?" do
+ before :each do
+ stub_const 'PlatformGuard::PLATFORM', 'unreal'
+ end
+
+ it "returns true if argument matches RbConfig::CONFIG['host_os']" do
+ expect(PlatformGuard.os?(:unreal)).to eq(true)
+ end
+
+ it "returns true if any argument matches RbConfig::CONFIG['host_os']" do
+ expect(PlatformGuard.os?(:bsd, :unreal, :amiga)).to eq(true)
+ end
+
+ it "returns false if no argument matches RbConfig::CONFIG['host_os']" do
+ expect(PlatformGuard.os?(:bsd, :netbsd, :amiga, :msdos)).to eq(false)
+ end
+
+ it "returns false if argument does not match RbConfig::CONFIG['host_os']" do
+ expect(PlatformGuard.os?(:amiga)).to eq(false)
+ end
+
+ it "returns true when arg is :windows and RbConfig::CONFIG['host_os'] contains 'mswin'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mswin32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns true when arg is :windows and RbConfig::CONFIG['host_os'] contains 'mingw'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mswin'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:linux)).to eq(false)
+ end
+
+ it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mingw'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:linux)).to eq(false)
+ end
+end
+
+RSpec.describe PlatformGuard, ".windows?" do
+ it "returns true on windows" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.windows?).to eq(true)
+ end
+
+ it "returns false on non-windows" do
+ stub_const 'PlatformGuard::PLATFORM', 'i586-linux'
+ expect(PlatformGuard.windows?).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/guards/quarantine_spec.rb b/spec/mspec/spec/guards/quarantine_spec.rb
new file mode 100644
index 0000000000..eb5ff1da27
--- /dev/null
+++ b/spec/mspec/spec/guards/quarantine_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe QuarantineGuard, "#match?" do
+ it "returns true" do
+ expect(QuarantineGuard.new.match?).to eq(true)
+ end
+end
+
+RSpec.describe Object, "#quarantine!" do
+ before :each do
+ ScratchPad.clear
+
+ @guard = QuarantineGuard.new
+ allow(QuarantineGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "does not yield" do
+ quarantine! { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "sets the name of the guard to :quarantine!" do
+ quarantine! { }
+ expect(@guard.name).to eq(:quarantine!)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(false)
+ expect(@guard).to receive(:unregister)
+ expect do
+ quarantine! { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/superuser_spec.rb b/spec/mspec/spec/guards/superuser_spec.rb
new file mode 100644
index 0000000000..aba2cc2bb0
--- /dev/null
+++ b/spec/mspec/spec/guards/superuser_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#as_superuser" do
+ before :each do
+ @guard = SuperUserGuard.new
+ allow(SuperUserGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "does not yield when Process.euid is not 0" do
+ allow(Process).to receive(:euid).and_return(501)
+ as_superuser { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields when Process.euid is 0" do
+ allow(Process).to receive(:euid).and_return(0)
+ as_superuser { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "sets the name of the guard to :as_superuser" do
+ as_superuser { }
+ expect(@guard.name).to eq(:as_superuser)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ as_superuser { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/support_spec.rb b/spec/mspec/spec/guards/support_spec.rb
new file mode 100644
index 0000000000..a61d003d6c
--- /dev/null
+++ b/spec/mspec/spec/guards/support_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#not_supported_on" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "raises an Exception when passed :ruby" do
+ stub_const "RUBY_ENGINE", "jruby"
+ expect {
+ not_supported_on(:ruby) { ScratchPad.record :yield }
+ }.to raise_error(Exception)
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "does not yield when #implementation? returns true" do
+ stub_const "RUBY_ENGINE", "jruby"
+ not_supported_on(:jruby) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields when #standard? returns true" do
+ stub_const "RUBY_ENGINE", "ruby"
+ not_supported_on(:rubinius) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "yields when #implementation? returns false" do
+ stub_const "RUBY_ENGINE", "jruby"
+ not_supported_on(:rubinius) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+end
+
+RSpec.describe Object, "#not_supported_on" do
+ before :each do
+ @guard = SupportedGuard.new
+ allow(SupportedGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "sets the name of the guard to :not_supported_on" do
+ not_supported_on(:rubinius) { }
+ expect(@guard.name).to eq(:not_supported_on)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(false)
+ expect(@guard).to receive(:unregister)
+ expect do
+ not_supported_on(:rubinius) { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/user_spec.rb b/spec/mspec/spec/guards/user_spec.rb
new file mode 100644
index 0000000000..2526504656
--- /dev/null
+++ b/spec/mspec/spec/guards/user_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#as_user" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "yields when the Process.euid is not 0" do
+ allow(Process).to receive(:euid).and_return(501)
+ as_user { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when the Process.euid is 0" do
+ allow(Process).to receive(:euid).and_return(0)
+ as_user { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+end
diff --git a/spec/mspec/spec/guards/version_spec.rb b/spec/mspec/spec/guards/version_spec.rb
new file mode 100644
index 0000000000..5a5f4ddc3b
--- /dev/null
+++ b/spec/mspec/spec/guards/version_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+# The VersionGuard specifies a version of Ruby with a String of
+# the form: v = 'major.minor.tiny'.
+#
+# A VersionGuard instance can be created with a single String,
+# which means any version >= each component of v.
+# Or, the guard can be created with a Range, a..b, or a...b,
+# where a, b are of the same form as v. The meaning of the Range
+# is as typically understood: a..b means v >= a and v <= b;
+# a...b means v >= a and v < b.
+
+RSpec.describe VersionGuard, "#match?" do
+ before :each do
+ hide_deprecation_warnings
+ @current = '1.8.6'
+ end
+
+ it "returns true when the argument is equal to RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8.6').match?).to eq(true)
+ end
+
+ it "returns true when the argument is less than RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8.5').match?).to eq(true)
+ end
+
+ it "returns false when the argument is greater than RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8.7').match?).to eq(false)
+ expect(VersionGuard.new(@current, '1.9.2').match?).to eq(false)
+ end
+
+ it "returns true when the argument range includes RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8.5'..'1.8.7').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8'..'1.9').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8'...'1.9').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8'..'1.8.6').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8.5'..'1.8.6').match?).to eq(true)
+ expect(VersionGuard.new(@current, ''...'1.8.7').match?).to eq(true)
+ end
+
+ it "returns false when the argument range does not include RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8.7'..'1.8.9').match?).to eq(false)
+ expect(VersionGuard.new(@current, '1.8.4'..'1.8.5').match?).to eq(false)
+ expect(VersionGuard.new(@current, '1.8.4'...'1.8.6').match?).to eq(false)
+ expect(VersionGuard.new(@current, '1.8.5'...'1.8.6').match?).to eq(false)
+ expect(VersionGuard.new(@current, ''...'1.8.6').match?).to eq(false)
+ end
+end
+
+RSpec.describe Object, "#ruby_version_is" do
+ before :each do
+ @guard = VersionGuard.new '1.2.3', 'x.x.x'
+ allow(VersionGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields when #match? returns true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ ruby_version_is('x.x.x') { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when #match? returns false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ ruby_version_is('x.x.x') { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "returns what #match? returns when no block is given" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(ruby_version_is('x.x.x')).to eq(true)
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(ruby_version_is('x.x.x')).to eq(false)
+ end
+
+ it "sets the name of the guard to :ruby_version_is" do
+ ruby_version_is("") { }
+ expect(@guard.name).to eq(:ruby_version_is)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ ruby_version_is("") { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#version_is" do
+ before :each do
+ hide_deprecation_warnings
+ end
+
+ it "returns the expected values" do
+ expect(version_is('1.2.3', '1.2.2')).to eq(true)
+ expect(version_is('1.2.3', '1.2.3')).to eq(true)
+ expect(version_is('1.2.3', '1.2.4')).to eq(false)
+
+ expect(version_is('1.2.3', '1')).to eq(true)
+ expect(version_is('1.2.3', '1.0')).to eq(true)
+ expect(version_is('1.2.3', '2')).to eq(false)
+ expect(version_is('1.2.3', '2.0')).to eq(false)
+
+ expect(version_is('1.2.3', '1.2.2'..'1.2.4')).to eq(true)
+ expect(version_is('1.2.3', '1.2.2'..'1.2.3')).to eq(true)
+ expect(version_is('1.2.3', '1.2.2'...'1.2.3')).to eq(false)
+ expect(version_is('1.2.3', '1.2.3'..'1.2.4')).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/helpers/argf_spec.rb b/spec/mspec/spec/helpers/argf_spec.rb
new file mode 100644
index 0000000000..1412d71f84
--- /dev/null
+++ b/spec/mspec/spec/helpers/argf_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#argf" do
+ before :each do
+ @saved_argv = ARGV.dup
+ @argv = [__FILE__]
+ end
+
+ it "sets @argf to an instance of ARGF.class with the given argv" do
+ argf @argv do
+ expect(@argf).to be_an_instance_of ARGF.class
+ expect(@argf.filename).to eq(@argv.first)
+ end
+ expect(@argf).to be_nil
+ end
+
+ it "does not alter ARGV nor ARGF" do
+ argf @argv do
+ end
+ expect(ARGV).to eq(@saved_argv)
+ expect(ARGF.argv).to eq(@saved_argv)
+ end
+
+ it "does not close STDIN" do
+ argf ['-'] do
+ end
+ expect(STDIN).not_to be_closed
+ end
+
+ it "disallows nested calls" do
+ argf @argv do
+ expect { argf @argv }.to raise_error
+ end
+ end
+end
diff --git a/spec/mspec/spec/helpers/argv_spec.rb b/spec/mspec/spec/helpers/argv_spec.rb
new file mode 100644
index 0000000000..1db7e38650
--- /dev/null
+++ b/spec/mspec/spec/helpers/argv_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#argv" do
+ before :each do
+ ScratchPad.clear
+
+ @saved_argv = ARGV.dup
+ @argv = ["a", "b"]
+ end
+
+ it "replaces and restores the value of ARGV" do
+ argv @argv
+ expect(ARGV).to eq(@argv)
+ argv :restore
+ expect(ARGV).to eq(@saved_argv)
+ end
+
+ it "yields to the block after setting ARGV" do
+ argv @argv do
+ ScratchPad.record ARGV.dup
+ end
+ expect(ScratchPad.recorded).to eq(@argv)
+ expect(ARGV).to eq(@saved_argv)
+ end
+end
diff --git a/spec/mspec/spec/helpers/datetime_spec.rb b/spec/mspec/spec/helpers/datetime_spec.rb
new file mode 100644
index 0000000000..af4f557376
--- /dev/null
+++ b/spec/mspec/spec/helpers/datetime_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#new_datetime" do
+ it "returns a default DateTime instance" do
+ expect(new_datetime).to eq(DateTime.new)
+ end
+
+ it "returns a DateTime instance with the specified year value" do
+ d = new_datetime :year => 1970
+ expect(d.year).to eq(1970)
+ end
+
+ it "returns a DateTime instance with the specified month value" do
+ d = new_datetime :month => 11
+ expect(d.mon).to eq(11)
+ end
+
+ it "returns a DateTime instance with the specified day value" do
+ d = new_datetime :day => 23
+ expect(d.day).to eq(23)
+ end
+
+ it "returns a DateTime instance with the specified hour value" do
+ d = new_datetime :hour => 10
+ expect(d.hour).to eq(10)
+ end
+
+ it "returns a DateTime instance with the specified minute value" do
+ d = new_datetime :minute => 10
+ expect(d.min).to eq(10)
+ end
+
+ it "returns a DateTime instance with the specified second value" do
+ d = new_datetime :second => 2
+ expect(d.sec).to eq(2)
+ end
+
+ it "returns a DateTime instance with the specified offset value" do
+ d = new_datetime :offset => Rational(3,24)
+ expect(d.offset).to eq(Rational(3,24))
+ end
+end
diff --git a/spec/mspec/spec/helpers/fixture_spec.rb b/spec/mspec/spec/helpers/fixture_spec.rb
new file mode 100644
index 0000000000..d8e2ae7be4
--- /dev/null
+++ b/spec/mspec/spec/helpers/fixture_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#fixture" do
+ before :each do
+ @dir = File.realpath("..", __FILE__)
+ end
+
+ it "returns the expanded path to a fixture file" do
+ name = fixture(__FILE__, "subdir", "file.txt")
+ expect(name).to eq("#{@dir}/fixtures/subdir/file.txt")
+ end
+
+ it "omits '/shared' if it is the suffix of the directory string" do
+ name = fixture("#{@dir}/shared/file.rb", "subdir", "file.txt")
+ expect(name).to eq("#{@dir}/fixtures/subdir/file.txt")
+ end
+
+ it "does not append '/fixtures' if it is the suffix of the directory string" do
+ commands_dir = "#{File.dirname(@dir)}/commands"
+ name = fixture("#{commands_dir}/fixtures/file.rb", "subdir", "file.txt")
+ expect(name).to eq("#{commands_dir}/fixtures/subdir/file.txt")
+ end
+end
diff --git a/spec/mspec/spec/helpers/flunk_spec.rb b/spec/mspec/spec/helpers/flunk_spec.rb
new file mode 100644
index 0000000000..b6a1f21c12
--- /dev/null
+++ b/spec/mspec/spec/helpers/flunk_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/runner/mspec'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#flunk" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ it "raises an SpecExpectationNotMetError unconditionally" do
+ expect { flunk }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "accepts on argument for an optional message" do
+ expect {flunk "test"}.to raise_error(SpecExpectationNotMetError)
+ end
+end
diff --git a/spec/mspec/spec/helpers/fs_spec.rb b/spec/mspec/spec/helpers/fs_spec.rb
new file mode 100644
index 0000000000..15bb43903a
--- /dev/null
+++ b/spec/mspec/spec/helpers/fs_spec.rb
@@ -0,0 +1,195 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#cp" do
+ before :each do
+ @source = tmp("source.txt")
+ @copy = tmp("copied.txt")
+
+ @contents = "This is a copy."
+ File.open(@source, "w") { |f| f.write @contents }
+ end
+
+ after :each do
+ File.delete @source if File.exist? @source
+ File.delete @copy if File.exist? @copy
+ end
+
+ it "copies a file" do
+ cp @source, @copy
+ data = IO.read(@copy)
+ expect(data).to eq(@contents)
+ expect(data).to eq(IO.read(@source))
+ end
+end
+
+RSpec.describe Object, "#touch" do
+ before :all do
+ @name = tmp("touched.txt")
+ end
+
+ after :each do
+ File.delete @name if File.exist? @name
+ end
+
+ it "creates a file" do
+ touch @name
+ expect(File.exist?(@name)).to be_truthy
+ end
+
+ it "accepts an optional mode argument" do
+ touch @name, "wb"
+ expect(File.exist?(@name)).to be_truthy
+ end
+
+ it "overwrites an existing file" do
+ File.open(@name, "w") { |f| f.puts "used" }
+ expect(File.size(@name)).to be > 0
+
+ touch @name
+ expect(File.size(@name)).to eq(0)
+ end
+
+ it "yields the open file if passed a block" do
+ touch(@name) { |f| f.write "touching" }
+ expect(IO.read(@name)).to eq("touching")
+ end
+end
+
+RSpec.describe Object, "#touch" do
+ before :all do
+ @name = tmp("subdir/touched.txt")
+ end
+
+ after :each do
+ rm_r File.dirname(@name)
+ end
+
+ it "creates all the directories in the path to the file" do
+ touch @name
+ expect(File.exist?(@name)).to be_truthy
+ end
+end
+
+RSpec.describe Object, "#mkdir_p" do
+ before :all do
+ @dir1 = tmp("/nested")
+ @dir2 = @dir1 + "/directory"
+ @paths = [ @dir2, @dir1 ]
+ end
+
+ after :each do
+ File.delete @dir1 if File.file? @dir1
+ @paths.each { |path| Dir.rmdir path if File.directory? path }
+ end
+
+ it "creates all the directories in a path" do
+ mkdir_p @dir2
+ expect(File.directory?(@dir2)).to be_truthy
+ end
+
+ it "raises an ArgumentError if a path component is a file" do
+ File.open(@dir1, "w") { |f| }
+ expect { mkdir_p @dir2 }.to raise_error(ArgumentError)
+ end
+
+ it "works if multiple processes try to create the same directory concurrently" do
+ original = File.method(:directory?)
+ expect(File).to receive(:directory?).at_least(:once) { |dir|
+ ret = original.call(dir)
+ if !ret and dir == @dir1
+ Dir.mkdir(dir) # Simulate race
+ end
+ ret
+ }
+ mkdir_p @dir1
+ expect(original.call(@dir1)).to be_truthy
+ end
+end
+
+RSpec.describe Object, "#rm_r" do
+ before :all do
+ @topdir = tmp("rm_r_tree")
+ @topfile = @topdir + "/file.txt"
+ @link = @topdir + "/file.lnk"
+ @socket = @topdir + "/socket.sck"
+ @subdir1 = @topdir + "/subdir1"
+ @subdir2 = @subdir1 + "/subdir2"
+ @subfile = @subdir1 + "/subfile.txt"
+ end
+
+ before :each do
+ mkdir_p @subdir2
+ touch @topfile
+ touch @subfile
+ end
+
+ after :each do
+ File.delete @link if File.exist? @link or File.symlink? @link
+ File.delete @socket if File.exist? @socket
+ File.delete @subfile if File.exist? @subfile
+ File.delete @topfile if File.exist? @topfile
+
+ Dir.rmdir @subdir2 if File.directory? @subdir2
+ Dir.rmdir @subdir1 if File.directory? @subdir1
+ Dir.rmdir @topdir if File.directory? @topdir
+ end
+
+ it "raises an ArgumentError if the path is not prefixed by MSPEC_RM_PREFIX" do
+ expect { rm_r "some_file.txt" }.to raise_error(ArgumentError)
+ end
+
+ it "removes a single file" do
+ rm_r @subfile
+ expect(File.exist?(@subfile)).to be_falsey
+ end
+
+ it "removes multiple files" do
+ rm_r @topfile, @subfile
+ expect(File.exist?(@topfile)).to be_falsey
+ expect(File.exist?(@subfile)).to be_falsey
+ end
+
+ platform_is_not :windows do
+ it "removes a symlink to a file" do
+ File.symlink @topfile, @link
+ rm_r @link
+ expect(File.exist?(@link)).to be_falsey
+ end
+
+ it "removes a symlink to a directory" do
+ File.symlink @subdir1, @link
+ rm_r @link
+ expect do
+ File.lstat(@link)
+ end.to raise_error(Errno::ENOENT)
+ expect(File.exist?(@subdir1)).to be_truthy
+ end
+
+ it "removes a dangling symlink" do
+ File.symlink "non_existent_file", @link
+ rm_r @link
+ expect do
+ File.lstat(@link)
+ end.to raise_error(Errno::ENOENT)
+ end
+
+ it "removes a socket" do
+ require 'socket'
+ UNIXServer.new(@socket).close
+ rm_r @socket
+ expect(File.exist?(@socket)).to be_falsey
+ end
+ end
+
+ it "removes a single directory" do
+ rm_r @subdir2
+ expect(File.directory?(@subdir2)).to be_falsey
+ end
+
+ it "recursively removes a directory tree" do
+ rm_r @topdir
+ expect(File.directory?(@topdir)).to be_falsey
+ end
+end
diff --git a/spec/mspec/spec/helpers/io_spec.rb b/spec/mspec/spec/helpers/io_spec.rb
new file mode 100644
index 0000000000..14c1a2d6b5
--- /dev/null
+++ b/spec/mspec/spec/helpers/io_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe IOStub do
+ before :each do
+ @out = IOStub.new
+ @sep = $\
+ end
+
+ after :each do
+ $\ = @sep
+ end
+
+ it "provides a write method" do
+ @out.write "this"
+ expect(@out).to eq("this")
+ end
+
+ it "concatenates the arguments sent to write" do
+ @out.write "flim ", "flam"
+ expect(@out).to eq("flim flam")
+ end
+
+ it "provides a print method that appends the default separator" do
+ $\ = " [newline] "
+ @out.print "hello"
+ @out.print "world"
+ expect(@out).to eq("hello [newline] world [newline] ")
+ end
+
+ it "provides a puts method that appends the default separator" do
+ @out.puts "hello", 1, 2, 3
+ expect(@out).to eq("hello\n1\n2\n3\n")
+ end
+
+ it "provides a puts method that appends separator if argument not given" do
+ @out.puts
+ expect(@out).to eq("\n")
+ end
+
+ it "provides a printf method" do
+ @out.printf "%-10s, %03d, %2.1f", "test", 42, 4.2
+ expect(@out).to eq("test , 042, 4.2")
+ end
+
+ it "provides a flush method that does nothing and returns self" do
+ expect(@out.flush).to eq(@out)
+ end
+end
+
+RSpec.describe Object, "#new_fd" do
+ before :each do
+ @name = tmp("io_specs")
+ @io = nil
+ end
+
+ after :each do
+ @io.close if @io and not @io.closed?
+ rm_r @name
+ end
+
+ it "returns an Integer that can be used to create an IO instance" do
+ fd = new_fd @name
+ expect(fd).to be_kind_of(Integer)
+
+ @io = IO.new fd, 'w:utf-8'
+ @io.sync = true
+ @io.print "io data"
+
+ expect(IO.read(@name)).to eq("io data")
+ end
+
+ it "accepts an options Hash" do
+ allow(FeatureGuard).to receive(:enabled?).and_return(true)
+ fd = new_fd @name, { :mode => 'w:utf-8' }
+ expect(fd).to be_kind_of(Integer)
+
+ @io = IO.new fd, 'w:utf-8'
+ @io.sync = true
+ @io.print "io data"
+
+ expect(IO.read(@name)).to eq("io data")
+ end
+
+ it "raises an ArgumentError if the options Hash does not include :mode" do
+ allow(FeatureGuard).to receive(:enabled?).and_return(true)
+ expect { new_fd @name, { :encoding => "utf-8" } }.to raise_error(ArgumentError)
+ end
+end
+
+RSpec.describe Object, "#new_io" do
+ before :each do
+ @name = tmp("io_specs.txt")
+ end
+
+ after :each do
+ @io.close if @io and !@io.closed?
+ rm_r @name
+ end
+
+ it "returns a File instance" do
+ @io = new_io @name
+ expect(@io).to be_an_instance_of(File)
+ end
+
+ it "opens the IO for reading if passed 'r'" do
+ touch(@name) { |f| f.print "io data" }
+ @io = new_io @name, "r"
+ expect(@io.read).to eq("io data")
+ expect { @io.puts "more data" }.to raise_error(IOError)
+ end
+
+ it "opens the IO for writing if passed 'w'" do
+ @io = new_io @name, "w"
+ @io.sync = true
+
+ @io.print "io data"
+ expect(IO.read(@name)).to eq("io data")
+ end
+
+ it "opens the IO for reading if passed { :mode => 'r' }" do
+ touch(@name) { |f| f.print "io data" }
+ @io = new_io @name, { :mode => "r" }
+ expect(@io.read).to eq("io data")
+ expect { @io.puts "more data" }.to raise_error(IOError)
+ end
+
+ it "opens the IO for writing if passed { :mode => 'w' }" do
+ @io = new_io @name, { :mode => "w" }
+ @io.sync = true
+
+ @io.print "io data"
+ expect(IO.read(@name)).to eq("io data")
+ end
+end
diff --git a/spec/mspec/spec/helpers/mock_to_path_spec.rb b/spec/mspec/spec/helpers/mock_to_path_spec.rb
new file mode 100644
index 0000000000..c2ce985190
--- /dev/null
+++ b/spec/mspec/spec/helpers/mock_to_path_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'mspec/mocks'
+
+RSpec.describe Object, "#mock_to_path" do
+ before :each do
+ state = double("run state").as_null_object
+ expect(MSpec).to receive(:current).and_return(state)
+ end
+
+ it "returns an object that responds to #to_path" do
+ obj = mock_to_path("foo")
+ expect(obj).to be_a(MockObject)
+ expect(obj).to respond_to(:to_path)
+ obj.to_path
+ end
+
+ it "returns the provided path when #to_path is called" do
+ obj = mock_to_path("/tmp/foo")
+ expect(obj.to_path).to eq("/tmp/foo")
+ end
+end
diff --git a/spec/mspec/spec/helpers/numeric_spec.rb b/spec/mspec/spec/helpers/numeric_spec.rb
new file mode 100644
index 0000000000..64495b7276
--- /dev/null
+++ b/spec/mspec/spec/helpers/numeric_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#bignum_value" do
+ it "returns a value that is an instance of Bignum on any platform" do
+ expect(bignum_value).to be > fixnum_max
+ end
+
+ it "returns the default value incremented by the argument" do
+ expect(bignum_value(42)).to eq(bignum_value + 42)
+ end
+end
+
+RSpec.describe Object, "-bignum_value" do
+ it "returns a value that is an instance of Bignum on any platform" do
+ expect(-bignum_value).to be < fixnum_min
+ end
+end
+
+RSpec.describe Object, "#nan_value" do
+ it "returns NaN" do
+ expect(nan_value.nan?).to be_truthy
+ end
+end
+
+RSpec.describe Object, "#infinity_value" do
+ it "returns Infinity" do
+ expect(infinity_value.infinite?).to eq(1)
+ end
+end
diff --git a/spec/mspec/spec/helpers/ruby_exe_spec.rb b/spec/mspec/spec/helpers/ruby_exe_spec.rb
new file mode 100644
index 0000000000..56bade1ba9
--- /dev/null
+++ b/spec/mspec/spec/helpers/ruby_exe_spec.rb
@@ -0,0 +1,256 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'rbconfig'
+
+class RubyExeSpecs
+ public :ruby_exe_options
+ public :resolve_ruby_exe
+ public :ruby_cmd
+ public :ruby_exe
+end
+
+RSpec.describe "#ruby_exe_options" do
+ before :each do
+ @ruby_exe_env = ENV['RUBY_EXE']
+ @script = RubyExeSpecs.new
+ end
+
+ after :each do
+ ENV['RUBY_EXE'] = @ruby_exe_env
+ end
+
+ it "returns ENV['RUBY_EXE'] when passed :env" do
+ ENV['RUBY_EXE'] = "kowabunga"
+ expect(@script.ruby_exe_options(:env)).to eq("kowabunga")
+ end
+
+ it "returns 'bin/jruby' when passed :engine and RUBY_ENGINE is 'jruby'" do
+ stub_const "RUBY_ENGINE", 'jruby'
+ expect(@script.ruby_exe_options(:engine)).to eq('bin/jruby')
+ end
+
+ it "returns 'bin/rbx' when passed :engine, RUBY_ENGINE is 'rbx'" do
+ stub_const "RUBY_ENGINE", 'rbx'
+ expect(@script.ruby_exe_options(:engine)).to eq('bin/rbx')
+ end
+
+ it "returns 'ir' when passed :engine and RUBY_ENGINE is 'ironruby'" do
+ stub_const "RUBY_ENGINE", 'ironruby'
+ expect(@script.ruby_exe_options(:engine)).to eq('ir')
+ end
+
+ it "returns 'maglev-ruby' when passed :engine and RUBY_ENGINE is 'maglev'" do
+ stub_const "RUBY_ENGINE", 'maglev'
+ expect(@script.ruby_exe_options(:engine)).to eq('maglev-ruby')
+ end
+
+ it "returns 'topaz' when passed :engine and RUBY_ENGINE is 'topaz'" do
+ stub_const "RUBY_ENGINE", 'topaz'
+ expect(@script.ruby_exe_options(:engine)).to eq('topaz')
+ end
+
+ it "returns RUBY_ENGINE + $(EXEEXT) when passed :name" do
+ bin = RUBY_ENGINE + (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
+ name = File.join ".", bin
+ expect(@script.ruby_exe_options(:name)).to eq(name)
+ end
+
+ it "returns $(bindir)/$(RUBY_INSTALL_NAME) + $(EXEEXT) when passed :install_name" do
+ bin = RbConfig::CONFIG['RUBY_INSTALL_NAME'] + (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
+ name = File.join RbConfig::CONFIG['bindir'], bin
+ expect(@script.ruby_exe_options(:install_name)).to eq(name)
+ end
+end
+
+RSpec.describe "#resolve_ruby_exe" do
+ before :each do
+ @name = "ruby_spec_exe"
+ @script = RubyExeSpecs.new
+ end
+
+ it "returns the value returned by #ruby_exe_options if it exists and is executable" do
+ expect(@script).to receive(:ruby_exe_options).and_return(@name)
+ expect(File).to receive(:file?).with(@name).and_return(true)
+ expect(File).to receive(:executable?).with(@name).and_return(true)
+ expect(File).to receive(:expand_path).with(@name).and_return(@name)
+ expect(@script.resolve_ruby_exe).to eq(@name)
+ end
+
+ it "expands the path portion of the result of #ruby_exe_options" do
+ expect(@script).to receive(:ruby_exe_options).and_return("#{@name}")
+ expect(File).to receive(:file?).with(@name).and_return(true)
+ expect(File).to receive(:executable?).with(@name).and_return(true)
+ expect(File).to receive(:expand_path).with(@name).and_return("/usr/bin/#{@name}")
+ expect(@script.resolve_ruby_exe).to eq("/usr/bin/#{@name}")
+ end
+
+ it "adds the flags after the executable" do
+ @name = 'bin/rbx'
+ expect(@script).to receive(:ruby_exe_options).and_return(@name)
+ expect(File).to receive(:file?).with(@name).and_return(true)
+ expect(File).to receive(:executable?).with(@name).and_return(true)
+ expect(File).to receive(:expand_path).with(@name).and_return(@name)
+
+ expect(ENV).to receive(:[]).with("RUBY_FLAGS").and_return('-X19')
+ expect(@script.resolve_ruby_exe).to eq('bin/rbx -X19')
+ end
+
+ it "raises an exception if no exe is found" do
+ expect(File).to receive(:file?).at_least(:once).and_return(false)
+ expect {
+ @script.resolve_ruby_exe
+ }.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#ruby_cmd" do
+ before :each do
+ stub_const 'RUBY_EXE', 'ruby_spec_exe -w -Q'
+
+ @file = "some/ruby/file.rb"
+ @code = %(some "real" 'ruby' code)
+
+ @script = RubyExeSpecs.new
+ end
+
+ it "returns a command that runs the given file if it is a file that exists" do
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(@script.ruby_cmd(@file)).to eq("ruby_spec_exe -w -Q some/ruby/file.rb")
+ end
+
+ it "includes the given options and arguments with a file" do
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(@script.ruby_cmd(@file, :options => "-w -Cdir", :args => "< file.txt")).to eq(
+ "ruby_spec_exe -w -Q -w -Cdir some/ruby/file.rb < file.txt"
+ )
+ end
+
+ it "includes the given options and arguments with -e" do
+ expect(File).to receive(:exist?).with(@code).and_return(false)
+ expect(@script.ruby_cmd(@code, :options => "-W0 -Cdir", :args => "< file.txt")).to eq(
+ %(ruby_spec_exe -w -Q -W0 -Cdir -e "some \\"real\\" 'ruby' code" < file.txt)
+ )
+ end
+
+ it "returns a command with options and arguments but without code or file" do
+ expect(@script.ruby_cmd(nil, :options => "-c", :args => "> file.txt")).to eq(
+ "ruby_spec_exe -w -Q -c > file.txt"
+ )
+ end
+end
+
+RSpec.describe Object, "#ruby_exe" do
+ before :each do
+ stub_const 'RUBY_EXE', 'ruby_spec_exe -w -Q'
+
+ @script = RubyExeSpecs.new
+ allow(IO).to receive(:popen).and_return('OUTPUT')
+
+ status_successful = double(Process::Status, exited?: true, exitstatus: 0)
+ allow(Process).to receive(:last_status).and_return(status_successful)
+ end
+
+ it "returns command STDOUT when given command" do
+ code = "code"
+ options = {}
+ output = "output"
+ expect(IO).to receive(:popen).and_return(output)
+
+ expect(@script.ruby_exe(code, options)).to eq output
+ end
+
+ it "returns an Array containing the interpreter executable and flags when given no arguments" do
+ expect(@script.ruby_exe).to eq(['ruby_spec_exe', '-w', '-Q'])
+ end
+
+ it "executes (using `) the result of calling #ruby_cmd with the given arguments" do
+ code = "code"
+ options = {}
+ expect(@script).to receive(:ruby_cmd).and_return("ruby_cmd")
+ expect(IO).to receive(:popen).with("ruby_cmd")
+ @script.ruby_exe(code, options)
+ end
+
+ it "raises exception when command exit status is not successful" do
+ code = "code"
+ options = {}
+
+ status_failed = double(Process::Status, exited?: true, exitstatus: 4)
+ allow(Process).to receive(:last_status).and_return(status_failed)
+
+ expect {
+ @script.ruby_exe(code, options)
+ }.to raise_error(%r{Expected exit status is 0 but actual is 4 for command ruby_exe\(.+\)})
+ end
+
+ it "shows in the exception message if a signal killed the process" do
+ code = "code"
+ options = {}
+
+ status_failed = double(Process::Status, exited?: false, signaled?: true, termsig: Signal.list.fetch('TERM'))
+ allow(Process).to receive(:last_status).and_return(status_failed)
+
+ expect {
+ @script.ruby_exe(code, options)
+ }.to raise_error(%r{Expected exit status is 0 but actual is :SIGTERM for command ruby_exe\(.+\)})
+ end
+
+ describe "with :dir option" do
+ it "is deprecated" do
+ expect {
+ @script.ruby_exe nil, :dir => "tmp"
+ }.to raise_error(/no longer supported, use Dir\.chdir/)
+ end
+ end
+
+ describe "with :env option" do
+ it "preserves the values of existing ENV keys" do
+ ENV["ABC"] = "123"
+ allow(ENV).to receive(:[])
+ expect(ENV).to receive(:[]).with("ABC")
+ @script.ruby_exe nil, :env => { :ABC => "xyz" }
+ end
+
+ it "adds the :env entries to ENV" do
+ expect(ENV).to receive(:[]=).with("ABC", "xyz")
+ @script.ruby_exe nil, :env => { :ABC => "xyz" }
+ end
+
+ it "deletes the :env entries in ENV when an exception is raised" do
+ expect(ENV).to receive(:delete).with("XYZ")
+ @script.ruby_exe nil, :env => { :XYZ => "xyz" }
+ end
+
+ it "resets the values of existing ENV keys when an exception is raised" do
+ ENV["ABC"] = "123"
+ expect(ENV).to receive(:[]=).with("ABC", "xyz")
+ expect(ENV).to receive(:[]=).with("ABC", "123")
+
+ expect(IO).to receive(:popen).and_raise(Exception)
+ expect do
+ @script.ruby_exe nil, :env => { :ABC => "xyz" }
+ end.to raise_error(Exception)
+ end
+ end
+
+ describe "with :exit_status option" do
+ before do
+ status_failed = double(Process::Status, exited?: true, exitstatus: 4)
+ allow(Process).to receive(:last_status).and_return(status_failed)
+ end
+
+ it "raises exception when command ends with not expected status" do
+ expect {
+ @script.ruby_exe("path", exit_status: 1)
+ }.to raise_error(%r{Expected exit status is 1 but actual is 4 for command ruby_exe\(.+\)})
+ end
+
+ it "does not raise exception when command ends with expected status" do
+ output = "output"
+ expect(IO).to receive(:popen).and_return(output)
+
+ expect(@script.ruby_exe("path", exit_status: 4)).to eq output
+ end
+ end
+end
diff --git a/spec/mspec/spec/helpers/scratch_spec.rb b/spec/mspec/spec/helpers/scratch_spec.rb
new file mode 100644
index 0000000000..9dbef94aa3
--- /dev/null
+++ b/spec/mspec/spec/helpers/scratch_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe ScratchPad do
+ it "records an object and returns a previously recorded object" do
+ ScratchPad.record :this
+ expect(ScratchPad.recorded).to eq(:this)
+ end
+
+ it "clears the recorded object" do
+ ScratchPad.record :that
+ expect(ScratchPad.recorded).to eq(:that)
+ ScratchPad.clear
+ expect(ScratchPad.recorded).to eq(nil)
+ end
+
+ it "provides a convenience shortcut to append to a previously recorded object" do
+ ScratchPad.record []
+ ScratchPad << :new
+ ScratchPad << :another
+ expect(ScratchPad.recorded).to eq([:new, :another])
+ end
+end
diff --git a/spec/mspec/spec/helpers/suppress_warning_spec.rb b/spec/mspec/spec/helpers/suppress_warning_spec.rb
new file mode 100644
index 0000000000..4cae189bd3
--- /dev/null
+++ b/spec/mspec/spec/helpers/suppress_warning_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#suppress_warning" do
+ it "hides warnings" do
+ suppress_warning do
+ warn "should not be shown"
+ end
+ end
+
+ it "yields the block" do
+ a = 0
+ suppress_warning do
+ a = 1
+ end
+ expect(a).to eq(1)
+ end
+end
diff --git a/spec/mspec/spec/helpers/tmp_spec.rb b/spec/mspec/spec/helpers/tmp_spec.rb
new file mode 100644
index 0000000000..c41dcd57b3
--- /dev/null
+++ b/spec/mspec/spec/helpers/tmp_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#tmp" do
+ before :all do
+ @dir = SPEC_TEMP_DIR
+ end
+
+ it "returns a name relative to the current working directory" do
+ expect(tmp("test.txt")).to eq("#{@dir}/#{SPEC_TEMP_UNIQUIFIER}-test.txt")
+ end
+
+ it "returns a 'unique' name on repeated calls" do
+ a = tmp("text.txt")
+ b = tmp("text.txt")
+ expect(a).not_to eq(b)
+ end
+
+ it "does not 'uniquify' the name if requested not to" do
+ expect(tmp("test.txt", false)).to eq("#{@dir}/test.txt")
+ end
+
+ it "returns the name of the temporary directory when passed an empty string" do
+ expect(tmp("")).to eq("#{@dir}/")
+ end
+end
diff --git a/spec/mspec/spec/integration/interpreter_spec.rb b/spec/mspec/spec/integration/interpreter_spec.rb
new file mode 100644
index 0000000000..dbf3987a08
--- /dev/null
+++ b/spec/mspec/spec/integration/interpreter_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+RSpec.describe "The interpreter passed with -t" do
+ it "is used in subprocess" do
+ fixtures = "spec/fixtures"
+ interpreter = "#{fixtures}/my_ruby"
+ out, ret = run_mspec("run", "#{fixtures}/print_interpreter_spec.rb -t #{interpreter}")
+ out = out.lines.map(&:chomp).reject { |line|
+ line == 'RUBY_DESCRIPTION'
+ }.take(3)
+ expect(out).to eq([
+ interpreter,
+ interpreter,
+ "CWD/#{interpreter}"
+ ])
+ expect(ret.success?).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/integration/object_methods_spec.rb b/spec/mspec/spec/integration/object_methods_spec.rb
new file mode 100644
index 0000000000..697fbd10fa
--- /dev/null
+++ b/spec/mspec/spec/integration/object_methods_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+expected_output = <<EOS
+RUBY_DESCRIPTION
+.
+
+Finished in D.DDDDDD seconds
+
+1 file, 1 example, 1 expectation, 0 failures, 0 errors, 0 tagged
+EOS
+
+RSpec.describe "MSpec" do
+ it "does not define public methods on Object" do
+ out, ret = run_mspec("run", "spec/fixtures/object_methods_spec.rb")
+ expect(out).to eq(expected_output)
+ expect(ret.success?).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/integration/run_spec.rb b/spec/mspec/spec/integration/run_spec.rb
new file mode 100644
index 0000000000..ea0735e9b2
--- /dev/null
+++ b/spec/mspec/spec/integration/run_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+RSpec.describe "Running mspec" do
+ q = BACKTRACE_QUOTE
+ a_spec_output = <<EOS
+
+1)
+Foo#bar errors FAILED
+Expected 1 == 2
+to be truthy but was false
+CWD/spec/fixtures/a_spec.rb:8:in #{q}block (2 levels) in <top (required)>'
+CWD/spec/fixtures/a_spec.rb:2:in #{q}<top (required)>'
+
+2)
+Foo#bar fails ERROR
+RuntimeError: failure
+CWD/spec/fixtures/a_spec.rb:12:in #{q}block (2 levels) in <top (required)>'
+CWD/spec/fixtures/a_spec.rb:2:in #{q}<top (required)>'
+
+Finished in D.DDDDDD seconds
+EOS
+
+ a_stats = "1 file, 3 examples, 2 expectations, 1 failure, 1 error, 0 tagged\n"
+ ab_stats = "2 files, 4 examples, 3 expectations, 1 failure, 1 error, 0 tagged\n"
+ fixtures = "spec/fixtures"
+
+ it "runs the specs" do
+ out, ret = run_mspec("run", "#{fixtures}/a_spec.rb")
+ expect(out).to eq("RUBY_DESCRIPTION\n.FE\n#{a_spec_output}\n#{a_stats}")
+ expect(ret.success?).to eq(false)
+ end
+
+ it "directly with mspec-run runs the specs" do
+ out, ret = run_mspec("-run", "#{fixtures}/a_spec.rb")
+ expect(out).to eq("RUBY_DESCRIPTION\n.FE\n#{a_spec_output}\n#{a_stats}")
+ expect(ret.success?).to eq(false)
+ end
+
+ it "runs the specs in parallel with -j using the dotted formatter" do
+ out, ret = run_mspec("run", "-j #{fixtures}/a_spec.rb #{fixtures}/b_spec.rb")
+ expect(out).to eq("RUBY_DESCRIPTION\n...\n#{a_spec_output}\n#{ab_stats}")
+ expect(ret.success?).to eq(false)
+ end
+
+ it "runs the specs in parallel with -j -fa" do
+ out, ret = run_mspec("run", "-j -fa #{fixtures}/a_spec.rb #{fixtures}/b_spec.rb")
+ progress_bar =
+ "\r[/ | 0% | 00:00:00] \e[0;32m 0F \e[0;32m 0E\e[0m " +
+ "\r[- | ==================50% | 00:00:00] \e[0;32m 0F \e[0;32m 0E\e[0m " +
+ "\r[\\ | ==================100%================== | 00:00:00] \e[0;32m 0F \e[0;32m 0E\e[0m "
+ expect(out).to eq("RUBY_DESCRIPTION\n#{progress_bar}\n#{a_spec_output}\n#{ab_stats}")
+ expect(ret.success?).to eq(false)
+ end
+
+ it "gives a useful error message when a subprocess dies in parallel mode" do
+ out, ret = run_mspec("run", "-j #{fixtures}/b_spec.rb #{fixtures}/die_spec.rb")
+ lines = out.lines
+ expect(lines).to include "A child mspec-run process died unexpectedly while running CWD/spec/fixtures/die_spec.rb\n"
+ expect(lines).to include "Finished in D.DDDDDD seconds\n"
+ expect(lines.last).to match(/^\d files?, \d examples?, \d expectations?, 0 failures, 0 errors, 0 tagged$/)
+ expect(ret.success?).to eq(false)
+ end
+
+ it "gives a useful error message when a subprocess prints unexpected output on STDOUT in parallel mode" do
+ out, ret = run_mspec("run", "-j #{fixtures}/b_spec.rb #{fixtures}/chatty_spec.rb")
+ lines = out.lines
+ expect(lines).to include "A child mspec-run process printed unexpected output on STDOUT: #{'"Hello\nIt\'s me!\n"'} while running CWD/spec/fixtures/chatty_spec.rb\n"
+ expect(lines).to include "Finished in D.DDDDDD seconds\n"
+ expect(lines.last).to eq("2 files, 2 examples, 2 expectations, 0 failures, 0 errors, 0 tagged\n")
+ expect(ret.success?).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/integration/tag_spec.rb b/spec/mspec/spec/integration/tag_spec.rb
new file mode 100644
index 0000000000..ae08e9d45f
--- /dev/null
+++ b/spec/mspec/spec/integration/tag_spec.rb
@@ -0,0 +1,60 @@
+# encoding: utf-8
+require 'spec_helper'
+
+RSpec.describe "Running mspec tag" do
+ before :all do
+ FileUtils.rm_rf 'spec/fixtures/tags'
+ end
+
+ after :all do
+ FileUtils.rm_rf 'spec/fixtures/tags'
+ end
+
+ it "tags the failing specs" do
+ fixtures = "spec/fixtures"
+ out, ret = run_mspec("tag", "--add fails --fail #{fixtures}/tagging_spec.rb")
+ q = BACKTRACE_QUOTE
+ expect(out).to eq <<EOS
+RUBY_DESCRIPTION
+.FF
+TagAction: specs tagged with 'fails':
+
+Tag#me errors
+Tag#me érròrs in unicode
+
+
+1)
+Tag#me errors FAILED
+Expected 1 == 2
+to be truthy but was false
+CWD/spec/fixtures/tagging_spec.rb:9:in #{q}block (2 levels) in <top (required)>'
+CWD/spec/fixtures/tagging_spec.rb:3:in #{q}<top (required)>'
+
+2)
+Tag#me érròrs in unicode FAILED
+Expected 1 == 2
+to be truthy but was false
+CWD/spec/fixtures/tagging_spec.rb:13:in #{q}block (2 levels) in <top (required)>'
+CWD/spec/fixtures/tagging_spec.rb:3:in #{q}<top (required)>'
+
+Finished in D.DDDDDD seconds
+
+1 file, 3 examples, 3 expectations, 2 failures, 0 errors, 0 tagged
+EOS
+ expect(ret.success?).to eq(false)
+ end
+
+ it "does not run already tagged specs" do
+ fixtures = "spec/fixtures"
+ out, ret = run_mspec("run", "--excl-tag fails #{fixtures}/tagging_spec.rb")
+ expect(out).to eq <<EOS
+RUBY_DESCRIPTION
+.
+
+Finished in D.DDDDDD seconds
+
+1 file, 3 examples, 1 expectation, 0 failures, 0 errors, 2 tagged
+EOS
+ expect(ret.success?).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/matchers/base_spec.rb b/spec/mspec/spec/matchers/base_spec.rb
new file mode 100644
index 0000000000..6b5a3fbd72
--- /dev/null
+++ b/spec/mspec/spec/matchers/base_spec.rb
@@ -0,0 +1,228 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+require 'time'
+
+RSpec.describe SpecPositiveOperatorMatcher, "== operator" do
+ it "provides a failure message that 'Expected x to equal y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(1) == 2
+ }.to raise_error(SpecExpectationNotMetError, "Expected 1 == 2\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when == returns true" do
+ SpecPositiveOperatorMatcher.new(1) == 1
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "=~ operator" do
+ it "provides a failure message that 'Expected \"x\" to match y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new('real') =~ /fake/
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"real\" =~ /fake/\nto be truthy but was nil")
+ end
+
+ it "does not raise an exception when =~ returns true" do
+ SpecPositiveOperatorMatcher.new('real') =~ /real/
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "> operator" do
+ it "provides a failure message that 'Expected x to be greater than y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(4) > 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 4 > 5\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when > returns true" do
+ SpecPositiveOperatorMatcher.new(5) > 4
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, ">= operator" do
+ it "provides a failure message that 'Expected x to be greater than or equal to y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(4) >= 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 4 >= 5\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when > returns true" do
+ SpecPositiveOperatorMatcher.new(5) >= 4
+ SpecPositiveOperatorMatcher.new(5) >= 5
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "< operator" do
+ it "provides a failure message that 'Expected x to be less than y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(5) < 4
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 < 4\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when < returns true" do
+ SpecPositiveOperatorMatcher.new(4) < 5
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "<= operator" do
+ it "provides a failure message that 'Expected x to be less than or equal to y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(5) <= 4
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 <= 4\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when < returns true" do
+ SpecPositiveOperatorMatcher.new(4) <= 5
+ SpecPositiveOperatorMatcher.new(4) <= 4
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "arbitrary predicates" do
+ it "do not raise an exception when the predicate is truthy" do
+ SpecPositiveOperatorMatcher.new(2).eql?(2)
+ SpecPositiveOperatorMatcher.new(2).equal?(2)
+ SpecPositiveOperatorMatcher.new([1, 2, 3]).include?(2)
+ SpecPositiveOperatorMatcher.new("abc").start_with?("ab")
+ SpecPositiveOperatorMatcher.new("abc").start_with?("d", "a")
+ SpecPositiveOperatorMatcher.new(3).odd?
+ SpecPositiveOperatorMatcher.new([1, 2]).any? { |e| e.even? }
+ end
+
+ it "provide a failure message when the predicate returns a falsy value" do
+ expect {
+ SpecPositiveOperatorMatcher.new(2).eql?(3)
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.eql? 3\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new(2).equal?(3)
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.equal? 3\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new([1, 2, 3]).include?(4)
+ }.to raise_error(SpecExpectationNotMetError, "Expected [1, 2, 3].include? 4\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new("abc").start_with?("de")
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"de\"\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new("abc").start_with?("d", "e")
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"d\", \"e\"\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new(2).odd?
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.odd?\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new([1, 3]).any? { |e| e.even? }
+ }.to raise_error(SpecExpectationNotMetError, "Expected [1, 3].any? { ... }\nto be truthy but was false")
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "arbitrary predicates" do
+ it "do not raise an exception when the predicate returns a falsy value" do
+ SpecNegativeOperatorMatcher.new(2).eql?(3)
+ SpecNegativeOperatorMatcher.new(2).equal?(3)
+ SpecNegativeOperatorMatcher.new([1, 2, 3]).include?(4)
+ SpecNegativeOperatorMatcher.new("abc").start_with?("de")
+ SpecNegativeOperatorMatcher.new("abc").start_with?("d", "e")
+ SpecNegativeOperatorMatcher.new(2).odd?
+ SpecNegativeOperatorMatcher.new([1, 3]).any? { |e| e.even? }
+ end
+
+ it "provide a failure message when the predicate returns a truthy value" do
+ expect {
+ SpecNegativeOperatorMatcher.new(2).eql?(2)
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.eql? 2\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new(2).equal?(2)
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.equal? 2\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new([1, 2, 3]).include?(2)
+ }.to raise_error(SpecExpectationNotMetError, "Expected [1, 2, 3].include? 2\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new("abc").start_with?("ab")
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"ab\"\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new("abc").start_with?("d", "a")
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"d\", \"a\"\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new(3).odd?
+ }.to raise_error(SpecExpectationNotMetError, "Expected 3.odd?\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new([1, 2]).any? { |e| e.even? }
+ }.to raise_error(SpecExpectationNotMetError, "Expected [1, 2].any? { ... }\nto be falsy but was true")
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "== operator" do
+ it "provides a failure message that 'Expected x not to equal y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(1) == 1
+ }.to raise_error(SpecExpectationNotMetError, "Expected 1 == 1\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when == returns false" do
+ SpecNegativeOperatorMatcher.new(1) == 2
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "=~ operator" do
+ it "provides a failure message that 'Expected \"x\" not to match /y/'" do
+ expect {
+ SpecNegativeOperatorMatcher.new('real') =~ /real/
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"real\" =~ /real/\nto be falsy but was 0")
+ end
+
+ it "does not raise an exception when =~ returns false" do
+ SpecNegativeOperatorMatcher.new('real') =~ /fake/
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "< operator" do
+ it "provides a failure message that 'Expected x not to be less than y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(4) < 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 4 < 5\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when < returns false" do
+ SpecNegativeOperatorMatcher.new(5) < 4
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "<= operator" do
+ it "provides a failure message that 'Expected x not to be less than or equal to y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(4) <= 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 4 <= 5\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new(5) <= 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 <= 5\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when <= returns false" do
+ SpecNegativeOperatorMatcher.new(5) <= 4
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "> operator" do
+ it "provides a failure message that 'Expected x not to be greater than y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(5) > 4
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 > 4\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when > returns false" do
+ SpecNegativeOperatorMatcher.new(4) > 5
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, ">= operator" do
+ it "provides a failure message that 'Expected x not to be greater than or equal to y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(5) >= 4
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 >= 4\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new(5) >= 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 >= 5\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when >= returns false" do
+ SpecNegativeOperatorMatcher.new(4) >= 5
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_an_instance_of_spec.rb b/spec/mspec/spec/matchers/be_an_instance_of_spec.rb
new file mode 100644
index 0000000000..7c74249d24
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_an_instance_of_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+module BeAnInOfSpecs
+ class A
+ end
+
+ class B < A
+ end
+
+ class C < B
+ end
+end
+
+RSpec.describe BeAnInstanceOfMatcher do
+ it "matches when actual is an instance_of? expected" do
+ a = BeAnInOfSpecs::A.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::A).matches?(a)).to be_truthy
+
+ b = BeAnInOfSpecs::B.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::B).matches?(b)).to be_truthy
+ end
+
+ it "does not match when actual is not an instance_of? expected" do
+ a = BeAnInOfSpecs::A.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::B).matches?(a)).to be_falsey
+
+ b = BeAnInOfSpecs::B.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::A).matches?(b)).to be_falsey
+
+ c = BeAnInOfSpecs::C.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::A).matches?(c)).to be_falsey
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::B).matches?(c)).to be_falsey
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeAnInstanceOfMatcher.new(Numeric)
+ matcher.matches?("string")
+ expect(matcher.failure_message).to eq([
+ "Expected \"string\" (String)", "to be an instance of Numeric"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeAnInstanceOfMatcher.new(Numeric)
+ matcher.matches?(4.0)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected 4.0 (Float)", "not to be an instance of Numeric"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_ancestor_of_spec.rb b/spec/mspec/spec/matchers/be_ancestor_of_spec.rb
new file mode 100644
index 0000000000..abc05e0f7a
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_ancestor_of_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class Parent; end
+class Child < Parent; end
+
+RSpec.describe BeAncestorOfMatcher do
+ it "matches when actual is an ancestor of expected" do
+ expect(BeAncestorOfMatcher.new(Child).matches?(Parent)).to eq(true)
+ end
+
+ it "does not match when actual is not an ancestor of expected" do
+ expect(BeAncestorOfMatcher.new(Parent).matches?(Child)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeAncestorOfMatcher.new(Parent)
+ matcher.matches?(Child)
+ expect(matcher.failure_message).to eq(["Expected Child", "to be an ancestor of Parent"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeAncestorOfMatcher.new(Child)
+ matcher.matches?(Parent)
+ expect(matcher.negative_failure_message).to eq(["Expected Parent", "not to be an ancestor of Child"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_close_spec.rb b/spec/mspec/spec/matchers/be_close_spec.rb
new file mode 100644
index 0000000000..dfd4f4ddbb
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_close_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+# Adapted from RSpec 1.0.8
+RSpec.describe BeCloseMatcher do
+ it "matches when actual == expected" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.0)).to eq(true)
+ end
+
+ it "matches when actual < (expected + tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.49)).to eq(true)
+ end
+
+ it "matches when actual > (expected - tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.51)).to eq(true)
+ end
+
+ it "matches when actual == (expected + tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.5)).to eq(true)
+ expect(BeCloseMatcher.new(3, 2).matches?(5)).to eq(true)
+ end
+
+ it "matches when actual == (expected - tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.5)).to eq(true)
+ expect(BeCloseMatcher.new(3, 2).matches?(1)).to eq(true)
+ end
+
+ it "does not match when actual < (expected - tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.49)).to eq(false)
+ end
+
+ it "does not match when actual > (expected + tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.51)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeCloseMatcher.new(5.0, 0.5)
+ matcher.matches?(6.5)
+ expect(matcher.failure_message).to eq(["Expected 6.5", "to be within 5.0 +/- 0.5"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeCloseMatcher.new(5.0, 0.5)
+ matcher.matches?(4.9)
+ expect(matcher.negative_failure_message).to eq(["Expected 4.9", "not to be within 5.0 +/- 0.5"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_computed_by_spec.rb b/spec/mspec/spec/matchers/be_computed_by_spec.rb
new file mode 100644
index 0000000000..f73861a576
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_computed_by_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+require 'mspec/matchers'
+
+RSpec.describe BeComputedByMatcher do
+ it "matches when all entries in the Array compute" do
+ array = [ [65, "A"],
+ [90, "Z"] ]
+ expect(BeComputedByMatcher.new(:chr).matches?(array)).to be_truthy
+ end
+
+ it "matches when all entries in the Array with arguments compute" do
+ array = [ [1, 2, 3],
+ [2, 4, 6] ]
+ expect(BeComputedByMatcher.new(:+).matches?(array)).to be_truthy
+ end
+
+ it "does not match when any entry in the Array does not compute" do
+ array = [ [65, "A" ],
+ [91, "Z" ] ]
+ expect(BeComputedByMatcher.new(:chr).matches?(array)).to be_falsey
+ end
+
+ it "accepts an argument list to apply to each method call" do
+ array = [ [65, "1000001" ],
+ [90, "1011010" ] ]
+ expect(BeComputedByMatcher.new(:to_s, 2).matches?(array)).to be_truthy
+ end
+
+ it "does not match when any entry in the Array with arguments does not compute" do
+ array = [ [1, 2, 3],
+ [2, 4, 7] ]
+ expect(BeComputedByMatcher.new(:+).matches?(array)).to be_falsey
+ end
+
+ it "provides a useful failure message" do
+ array = [ [65, "A" ],
+ [91, "Z" ] ]
+ matcher = BeComputedByMatcher.new(:chr)
+ matcher.matches?(array)
+ expect(matcher.failure_message).to eq(["Expected \"Z\"", "to be computed by 91.chr (computed \"[\" instead)"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_empty_spec.rb b/spec/mspec/spec/matchers/be_empty_spec.rb
new file mode 100644
index 0000000000..30678fe85f
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_empty_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeEmptyMatcher do
+ it "matches when actual is empty" do
+ expect(BeEmptyMatcher.new.matches?("")).to eq(true)
+ end
+
+ it "does not match when actual is not empty" do
+ expect(BeEmptyMatcher.new.matches?([10])).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeEmptyMatcher.new
+ matcher.matches?("not empty string")
+ expect(matcher.failure_message).to eq(["Expected \"not empty string\"", "to be empty"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeEmptyMatcher.new
+ matcher.matches?("")
+ expect(matcher.negative_failure_message).to eq(["Expected \"\"", "not to be empty"])
+ end
+end
+
diff --git a/spec/mspec/spec/matchers/be_false_spec.rb b/spec/mspec/spec/matchers/be_false_spec.rb
new file mode 100644
index 0000000000..46d7253220
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_false_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeFalseMatcher do
+ it "matches when actual is false" do
+ expect(BeFalseMatcher.new.matches?(false)).to eq(true)
+ end
+
+ it "does not match when actual is not false" do
+ expect(BeFalseMatcher.new.matches?("")).to eq(false)
+ expect(BeFalseMatcher.new.matches?(true)).to eq(false)
+ expect(BeFalseMatcher.new.matches?(nil)).to eq(false)
+ expect(BeFalseMatcher.new.matches?(0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeFalseMatcher.new
+ matcher.matches?("some string")
+ expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be false"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeFalseMatcher.new
+ matcher.matches?(false)
+ expect(matcher.negative_failure_message).to eq(["Expected false", "not to be false"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_kind_of_spec.rb b/spec/mspec/spec/matchers/be_kind_of_spec.rb
new file mode 100644
index 0000000000..1e19058411
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_kind_of_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeKindOfMatcher do
+ it "matches when actual is a kind_of? expected" do
+ expect(BeKindOfMatcher.new(Numeric).matches?(1)).to eq(true)
+ expect(BeKindOfMatcher.new(Integer).matches?(2)).to eq(true)
+ expect(BeKindOfMatcher.new(Regexp).matches?(/m/)).to eq(true)
+ end
+
+ it "does not match when actual is not a kind_of? expected" do
+ expect(BeKindOfMatcher.new(Integer).matches?(1.5)).to eq(false)
+ expect(BeKindOfMatcher.new(String).matches?(:a)).to eq(false)
+ expect(BeKindOfMatcher.new(Hash).matches?([])).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeKindOfMatcher.new(Numeric)
+ matcher.matches?('string')
+ expect(matcher.failure_message).to eq([
+ "Expected \"string\" (String)", "to be kind of Numeric"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeKindOfMatcher.new(Numeric)
+ matcher.matches?(4.0)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected 4.0 (Float)", "not to be kind of Numeric"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_nan_spec.rb b/spec/mspec/spec/matchers/be_nan_spec.rb
new file mode 100644
index 0000000000..baa7447943
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_nan_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'mspec/matchers'
+
+RSpec.describe BeNaNMatcher do
+ it "matches when actual is NaN" do
+ expect(BeNaNMatcher.new.matches?(nan_value)).to eq(true)
+ end
+
+ it "does not match when actual is not NaN" do
+ expect(BeNaNMatcher.new.matches?(1.0)).to eq(false)
+ expect(BeNaNMatcher.new.matches?(0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeNaNMatcher.new
+ matcher.matches?(0)
+ expect(matcher.failure_message).to eq(["Expected 0", "to be NaN"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeNaNMatcher.new
+ matcher.matches?(nan_value)
+ expect(matcher.negative_failure_message).to eq(["Expected NaN", "not to be NaN"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_nil_spec.rb b/spec/mspec/spec/matchers/be_nil_spec.rb
new file mode 100644
index 0000000000..e2768acf83
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_nil_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeNilMatcher do
+ it "matches when actual is nil" do
+ expect(BeNilMatcher.new.matches?(nil)).to eq(true)
+ end
+
+ it "does not match when actual is not nil" do
+ expect(BeNilMatcher.new.matches?("")).to eq(false)
+ expect(BeNilMatcher.new.matches?(false)).to eq(false)
+ expect(BeNilMatcher.new.matches?(0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeNilMatcher.new
+ matcher.matches?("some string")
+ expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be nil"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeNilMatcher.new
+ matcher.matches?(nil)
+ expect(matcher.negative_failure_message).to eq(["Expected nil", "not to be nil"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_true_or_false_spec.rb b/spec/mspec/spec/matchers/be_true_or_false_spec.rb
new file mode 100644
index 0000000000..e4b456eafc
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_true_or_false_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeTrueOrFalseMatcher do
+ it "matches when actual is true" do
+ expect(BeTrueOrFalseMatcher.new.matches?(true)).to eq(true)
+ end
+
+ it "matches when actual is false" do
+ expect(BeTrueOrFalseMatcher.new.matches?(false)).to eq(true)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeTrueOrFalseMatcher.new
+ matcher.matches?("some string")
+ expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be true or false"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_true_spec.rb b/spec/mspec/spec/matchers/be_true_spec.rb
new file mode 100644
index 0000000000..39ef05a0f8
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_true_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeTrueMatcher do
+ it "matches when actual is true" do
+ expect(BeTrueMatcher.new.matches?(true)).to eq(true)
+ end
+
+ it "does not match when actual is not true" do
+ expect(BeTrueMatcher.new.matches?("")).to eq(false)
+ expect(BeTrueMatcher.new.matches?(false)).to eq(false)
+ expect(BeTrueMatcher.new.matches?(nil)).to eq(false)
+ expect(BeTrueMatcher.new.matches?(0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeTrueMatcher.new
+ matcher.matches?("some string")
+ expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be true"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeTrueMatcher.new
+ matcher.matches?(true)
+ expect(matcher.negative_failure_message).to eq(["Expected true", "not to be true"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/block_caller_spec.rb b/spec/mspec/spec/matchers/block_caller_spec.rb
new file mode 100644
index 0000000000..5d7085fa63
--- /dev/null
+++ b/spec/mspec/spec/matchers/block_caller_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BlockingMatcher do
+ it 'matches when a Proc blocks the caller' do
+ expect(BlockingMatcher.new.matches?(proc { sleep })).to eq(true)
+ end
+
+ it 'does not match when a Proc does not block the caller' do
+ expect(BlockingMatcher.new.matches?(proc { 1 })).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/matchers/complain_spec.rb b/spec/mspec/spec/matchers/complain_spec.rb
new file mode 100644
index 0000000000..399ef3105b
--- /dev/null
+++ b/spec/mspec/spec/matchers/complain_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe ComplainMatcher do
+ it "matches when executing the proc results in output to $stderr" do
+ proc = lambda { warn "I'm gonna tell yo mama" }
+ expect(ComplainMatcher.new(nil).matches?(proc)).to eq(true)
+ end
+
+ it "matches when executing the proc results in the expected output to $stderr" do
+ proc = lambda { warn "Que haces?" }
+ expect(ComplainMatcher.new("Que haces?\n").matches?(proc)).to eq(true)
+ expect(ComplainMatcher.new("Que pasa?\n").matches?(proc)).to eq(false)
+ expect(ComplainMatcher.new(/Que/).matches?(proc)).to eq(true)
+ expect(ComplainMatcher.new(/Quoi/).matches?(proc)).to eq(false)
+ end
+
+ it "does not match when there is no output to $stderr" do
+ expect(ComplainMatcher.new(nil).matches?(lambda {})).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = ComplainMatcher.new(nil)
+ matcher.matches?(lambda { })
+ expect(matcher.failure_message).to eq(["Expected a warning", "but received none"])
+ matcher = ComplainMatcher.new("listen here")
+ matcher.matches?(lambda { warn "look out" })
+ expect(matcher.failure_message).to eq(
+ ["Expected warning: \"listen here\"", "but got: \"look out\""]
+ )
+ matcher = ComplainMatcher.new(/talk/)
+ matcher.matches?(lambda { warn "listen up" })
+ expect(matcher.failure_message).to eq(
+ ["Expected warning to match: /talk/", "but got: \"listen up\""]
+ )
+ end
+
+ it "provides a useful negative failure message" do
+ proc = lambda { warn "ouch" }
+ matcher = ComplainMatcher.new(nil)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Unexpected warning: ", "\"ouch\""]
+ )
+ matcher = ComplainMatcher.new("ouchy")
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected warning: \"ouchy\"", "but got: \"ouch\""]
+ )
+ matcher = ComplainMatcher.new(/ou/)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected warning not to match: /ou/", "but got: \"ouch\""]
+ )
+ end
+
+ context "`verbose` option specified" do
+ before do
+ $VERBOSE, @verbose = nil, $VERBOSE
+ end
+
+ after do
+ $VERBOSE = @verbose
+ end
+
+ it "sets $VERBOSE with specified second optional parameter" do
+ verbose = nil
+ proc = lambda { verbose = $VERBOSE }
+
+ ComplainMatcher.new(nil, verbose: true).matches?(proc)
+ expect(verbose).to eq(true)
+
+ ComplainMatcher.new(nil, verbose: false).matches?(proc)
+ expect(verbose).to eq(false)
+ end
+
+ it "sets $VERBOSE with false by default" do
+ verbose = nil
+ proc = lambda { verbose = $VERBOSE }
+
+ ComplainMatcher.new(nil).matches?(proc)
+ expect(verbose).to eq(false)
+ end
+
+ it "does not have side effect" do
+ proc = lambda { safe_value = $VERBOSE }
+
+ expect do
+ ComplainMatcher.new(nil, verbose: true).matches?(proc)
+ end.not_to change { $VERBOSE }
+ end
+
+ it "accepts a verbose level as single argument" do
+ verbose = nil
+ proc = lambda { verbose = $VERBOSE }
+
+ ComplainMatcher.new(verbose: true).matches?(proc)
+ expect(verbose).to eq(true)
+ end
+ end
+end
diff --git a/spec/mspec/spec/matchers/eql_spec.rb b/spec/mspec/spec/matchers/eql_spec.rb
new file mode 100644
index 0000000000..66307d2a9d
--- /dev/null
+++ b/spec/mspec/spec/matchers/eql_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe EqlMatcher do
+ it "matches when actual is eql? to expected" do
+ expect(EqlMatcher.new(1).matches?(1)).to eq(true)
+ expect(EqlMatcher.new(1.5).matches?(1.5)).to eq(true)
+ expect(EqlMatcher.new("red").matches?("red")).to eq(true)
+ expect(EqlMatcher.new(:blue).matches?(:blue)).to eq(true)
+ expect(EqlMatcher.new(Object).matches?(Object)).to eq(true)
+
+ o = Object.new
+ expect(EqlMatcher.new(o).matches?(o)).to eq(true)
+ end
+
+ it "does not match when actual is not eql? to expected" do
+ expect(EqlMatcher.new(1).matches?(1.0)).to eq(false)
+ expect(EqlMatcher.new(Hash).matches?(Object)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = EqlMatcher.new("red")
+ matcher.matches?("red")
+ expect(matcher.failure_message).to eq(["Expected \"red\"", "to have same value and type as \"red\""])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = EqlMatcher.new(1)
+ matcher.matches?(1.0)
+ expect(matcher.negative_failure_message).to eq(["Expected 1.0", "not to have same value or type as 1"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/equal_element_spec.rb b/spec/mspec/spec/matchers/equal_element_spec.rb
new file mode 100644
index 0000000000..3a5ae4ede2
--- /dev/null
+++ b/spec/mspec/spec/matchers/equal_element_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe EqualElementMatcher do
+ it "matches if it finds an element with the passed name, no matter what attributes/content" do
+ expect(EqualElementMatcher.new("A").matches?('<A></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A").matches?('<A HREF="http://example.com"></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A").matches?('<A HREF="http://example.com"></A>')).to be_truthy
+
+ expect(EqualElementMatcher.new("BASE").matches?('<BASE></A>')).to be_falsey
+ expect(EqualElementMatcher.new("BASE").matches?('<A></BASE>')).to be_falsey
+ expect(EqualElementMatcher.new("BASE").matches?('<A></A>')).to be_falsey
+ expect(EqualElementMatcher.new("BASE").matches?('<A HREF="http://example.com"></A>')).to be_falsey
+ expect(EqualElementMatcher.new("BASE").matches?('<A HREF="http://example.com"></A>')).to be_falsey
+ end
+
+ it "matches if it finds an element with the passed name and the passed attributes" do
+ expect(EqualElementMatcher.new("A", {}).matches?('<A></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A", nil).matches?('<A HREF="http://example.com"></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A HREF="http://example.com"></A>')).to be_truthy
+
+ expect(EqualElementMatcher.new("A", {}).matches?('<A HREF="http://example.com"></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A HREF="http://test.com"></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A HREF="http://example.com" HREF="http://example.com"></A>')).to be_falsey
+ end
+
+ it "matches if it finds an element with the passed name, the passed attributes and the passed content" do
+ expect(EqualElementMatcher.new("A", {}, "").matches?('<A></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A", {"HREF" => "http://example.com"}, "Example").matches?('<A HREF="http://example.com">Example</A>')).to be_truthy
+
+ expect(EqualElementMatcher.new("A", {}, "Test").matches?('<A></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", {"HREF" => "http://example.com"}, "Example").matches?('<A HREF="http://example.com"></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", {"HREF" => "http://example.com"}, "Example").matches?('<A HREF="http://example.com">Test</A>')).to be_falsey
+ end
+
+ it "can match unclosed elements" do
+ expect(EqualElementMatcher.new("BASE", nil, nil, :not_closed => true).matches?('<BASE>')).to be_truthy
+ expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, nil, :not_closed => true).matches?('<BASE HREF="http://example.com">')).to be_truthy
+ expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, "Example", :not_closed => true).matches?('<BASE HREF="http://example.com">Example')).to be_truthy
+
+ expect(EqualElementMatcher.new("BASE", {}, nil, :not_closed => true).matches?('<BASE HREF="http://example.com">')).to be_falsey
+ expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, "", :not_closed => true).matches?('<BASE HREF="http://example.com">Example')).to be_falsey
+ expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, "Test", :not_closed => true).matches?('<BASE HREF="http://example.com">Example')).to be_falsey
+ end
+
+ it "provides a useful failure message" do
+ equal_element = EqualElementMatcher.new("A", {}, "Test")
+ expect(equal_element.matches?('<A></A>')).to be_falsey
+ expect(equal_element.failure_message).to eq([%{Expected "<A></A>"}, %{to be a 'A' element with no attributes and "Test" as content}])
+
+ equal_element = EqualElementMatcher.new("A", {}, "")
+ expect(equal_element.matches?('<A>Test</A>')).to be_falsey
+ expect(equal_element.failure_message).to eq([%{Expected "<A>Test</A>"}, %{to be a 'A' element with no attributes and no content}])
+
+ equal_element = EqualElementMatcher.new("A", "HREF" => "http://www.example.com")
+ expect(equal_element.matches?('<A>Test</A>')).to be_falsey
+ expect(equal_element.failure_message).to eq([%{Expected "<A>Test</A>"}, %{to be a 'A' element with HREF="http://www.example.com" and any content}])
+ end
+
+ it "provides a useful negative failure message" do
+ equal_element = EqualElementMatcher.new("A", {}, "Test")
+ expect(equal_element.matches?('<A></A>')).to be_falsey
+ expect(equal_element.negative_failure_message).to eq([%{Expected "<A></A>"}, %{not to be a 'A' element with no attributes and "Test" as content}])
+
+ equal_element = EqualElementMatcher.new("A", {}, "")
+ expect(equal_element.matches?('<A>Test</A>')).to be_falsey
+ expect(equal_element.negative_failure_message).to eq([%{Expected "<A>Test</A>"}, %{not to be a 'A' element with no attributes and no content}])
+
+ equal_element = EqualElementMatcher.new("A", "HREF" => "http://www.example.com")
+ expect(equal_element.matches?('<A>Test</A>')).to be_falsey
+ expect(equal_element.negative_failure_message).to eq([%{Expected "<A>Test</A>"}, %{not to be a 'A' element with HREF="http://www.example.com" and any content}])
+ end
+end
diff --git a/spec/mspec/spec/matchers/equal_spec.rb b/spec/mspec/spec/matchers/equal_spec.rb
new file mode 100644
index 0000000000..2df1de54b4
--- /dev/null
+++ b/spec/mspec/spec/matchers/equal_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe EqualMatcher do
+ it "matches when actual is equal? to expected" do
+ expect(EqualMatcher.new(1).matches?(1)).to eq(true)
+ expect(EqualMatcher.new(:blue).matches?(:blue)).to eq(true)
+ expect(EqualMatcher.new(Object).matches?(Object)).to eq(true)
+
+ o = Object.new
+ expect(EqualMatcher.new(o).matches?(o)).to eq(true)
+ end
+
+ it "does not match when actual is not a equal? to expected" do
+ expect(EqualMatcher.new(1).matches?(1.0)).to eq(false)
+ expect(EqualMatcher.new("blue").matches?("blue")).to eq(false)
+ expect(EqualMatcher.new(Hash).matches?(Object)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = EqualMatcher.new("red")
+ matcher.matches?("red")
+ expect(matcher.failure_message).to eq(["Expected \"red\"", "to be identical to \"red\""])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = EqualMatcher.new(1)
+ matcher.matches?(1)
+ expect(matcher.negative_failure_message).to eq(["Expected 1", "not to be identical to 1"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_class_variable_spec.rb b/spec/mspec/spec/matchers/have_class_variable_spec.rb
new file mode 100644
index 0000000000..d6fcf9d4e2
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_class_variable_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class IVarModMock
+ def self.class_variables
+ [:@foo]
+ end
+end
+
+RSpec.describe HaveClassVariableMatcher, "on RUBY_VERSION >= 1.9" do
+ it "matches when mod has the class variable, given as string" do
+ matcher = HaveClassVariableMatcher.new('@foo')
+ expect(matcher.matches?(IVarModMock)).to be_truthy
+ end
+
+ it "matches when mod has the class variable, given as symbol" do
+ matcher = HaveClassVariableMatcher.new(:@foo)
+ expect(matcher.matches?(IVarModMock)).to be_truthy
+ end
+
+ it "does not match when mod hasn't got the class variable, given as string" do
+ matcher = HaveClassVariableMatcher.new('@bar')
+ expect(matcher.matches?(IVarModMock)).to be_falsey
+ end
+
+ it "does not match when mod hasn't got the class variable, given as symbol" do
+ matcher = HaveClassVariableMatcher.new(:@bar)
+ expect(matcher.matches?(IVarModMock)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveClassVariableMatcher.new(:@bar)
+ matcher.matches?(IVarModMock)
+ expect(matcher.failure_message).to eq([
+ "Expected IVarModMock to have class variable '@bar'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveClassVariableMatcher.new(:@bar)
+ matcher.matches?(IVarModMock)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected IVarModMock NOT to have class variable '@bar'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_constant_spec.rb b/spec/mspec/spec/matchers/have_constant_spec.rb
new file mode 100644
index 0000000000..0bf44dbe2b
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_constant_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HCMSpecs
+ X = :x
+end
+
+RSpec.describe HaveConstantMatcher do
+ it "matches when mod has the constant" do
+ matcher = HaveConstantMatcher.new :X
+ expect(matcher.matches?(HCMSpecs)).to be_truthy
+ end
+
+ it "does not match when mod does not have the constant" do
+ matcher = HaveConstantMatcher.new :A
+ expect(matcher.matches?(HCMSpecs)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveConstantMatcher.new :A
+ matcher.matches?(HCMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HCMSpecs to have constant 'A'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveConstantMatcher.new :X
+ matcher.matches?(HCMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HCMSpecs NOT to have constant 'X'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_instance_method_spec.rb b/spec/mspec/spec/matchers/have_instance_method_spec.rb
new file mode 100644
index 0000000000..7c2e50dba6
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_instance_method_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HIMMSpecs
+ def instance_method
+ end
+
+ class Subclass < HIMMSpecs
+ def instance_sub_method
+ end
+ end
+end
+
+RSpec.describe HaveInstanceMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HaveInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the instance method" do
+ matcher = HaveInstanceMethodMatcher.new :instance_method
+ expect(matcher.matches?(HIMMSpecs)).to be_truthy
+ expect(matcher.matches?(HIMMSpecs::Subclass)).to be_truthy
+ end
+
+ it "does not match when mod does not have the instance method" do
+ matcher = HaveInstanceMethodMatcher.new :another_method
+ expect(matcher.matches?(HIMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HaveInstanceMethodMatcher.new :instance_method, false
+ expect(matcher.matches?(HIMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveInstanceMethodMatcher.new :some_method
+ matcher.matches?(HIMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HIMMSpecs to have instance method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveInstanceMethodMatcher.new :some_method
+ matcher.matches?(HIMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HIMMSpecs NOT to have instance method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_instance_variable_spec.rb b/spec/mspec/spec/matchers/have_instance_variable_spec.rb
new file mode 100644
index 0000000000..12e2470f14
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_instance_variable_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe HaveInstanceVariableMatcher do
+ before :each do
+ @object = Object.new
+ def @object.instance_variables
+ [:@foo]
+ end
+ end
+
+ it "matches when object has the instance variable, given as string" do
+ matcher = HaveInstanceVariableMatcher.new('@foo')
+ expect(matcher.matches?(@object)).to be_truthy
+ end
+
+ it "matches when object has the instance variable, given as symbol" do
+ matcher = HaveInstanceVariableMatcher.new(:@foo)
+ expect(matcher.matches?(@object)).to be_truthy
+ end
+
+ it "does not match when object hasn't got the instance variable, given as string" do
+ matcher = HaveInstanceVariableMatcher.new('@bar')
+ expect(matcher.matches?(@object)).to be_falsey
+ end
+
+ it "does not match when object hasn't got the instance variable, given as symbol" do
+ matcher = HaveInstanceVariableMatcher.new(:@bar)
+ expect(matcher.matches?(@object)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveInstanceVariableMatcher.new(:@bar)
+ matcher.matches?(@object)
+ expect(matcher.failure_message).to eq([
+ "Expected #{@object.inspect} to have instance variable '@bar'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveInstanceVariableMatcher.new(:@bar)
+ matcher.matches?(@object)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected #{@object.inspect} NOT to have instance variable '@bar'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_method_spec.rb b/spec/mspec/spec/matchers/have_method_spec.rb
new file mode 100644
index 0000000000..4fc0bf5e45
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_method_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HMMSpecs
+ def instance_method
+ end
+
+ class Subclass < HMMSpecs
+ def instance_sub_method
+ end
+ end
+end
+
+RSpec.describe HaveMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HaveMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the method" do
+ matcher = HaveMethodMatcher.new :instance_method
+ expect(matcher.matches?(HMMSpecs)).to be_truthy
+ expect(matcher.matches?(HMMSpecs.new)).to be_truthy
+ expect(matcher.matches?(HMMSpecs::Subclass)).to be_truthy
+ expect(matcher.matches?(HMMSpecs::Subclass.new)).to be_truthy
+ end
+
+ it "does not match when mod does not have the method" do
+ matcher = HaveMethodMatcher.new :another_method
+ expect(matcher.matches?(HMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HaveMethodMatcher.new :instance_method, false
+ expect(matcher.matches?(HMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveMethodMatcher.new :some_method
+ matcher.matches?(HMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HMMSpecs to have method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveMethodMatcher.new :some_method
+ matcher.matches?(HMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HMMSpecs NOT to have method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_private_instance_method_spec.rb b/spec/mspec/spec/matchers/have_private_instance_method_spec.rb
new file mode 100644
index 0000000000..0e65c264d9
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_private_instance_method_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HPIMMSpecs
+ private
+
+ def private_method
+ end
+
+ class Subclass < HPIMMSpecs
+ private
+
+ def private_sub_method
+ end
+ end
+end
+
+RSpec.describe HavePrivateInstanceMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HavePrivateInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the private instance method" do
+ matcher = HavePrivateInstanceMethodMatcher.new :private_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_truthy
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy
+ end
+
+ it "does not match when mod does not have the private instance method" do
+ matcher = HavePrivateInstanceMethodMatcher.new :another_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HavePrivateInstanceMethodMatcher.new :private_method, false
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HavePrivateInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HPIMMSpecs to have private instance method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure message for #should_not" do
+ matcher = HavePrivateInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HPIMMSpecs NOT to have private instance method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_private_method_spec.rb b/spec/mspec/spec/matchers/have_private_method_spec.rb
new file mode 100644
index 0000000000..f433288057
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_private_method_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HPMMSpecs
+ def self.private_method
+ end
+
+ private_class_method :private_method
+end
+
+RSpec.describe HavePrivateMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HavePrivateMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the private method" do
+ matcher = HavePrivateMethodMatcher.new :private_method
+ expect(matcher.matches?(HPMMSpecs)).to be_truthy
+ end
+
+ it "does not match when mod does not have the private method" do
+ matcher = HavePrivateMethodMatcher.new :another_method
+ expect(matcher.matches?(HPMMSpecs)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HavePrivateMethodMatcher.new :some_method
+ matcher.matches?(HPMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HPMMSpecs to have private method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure message for #should_not" do
+ matcher = HavePrivateMethodMatcher.new :private_method
+ matcher.matches?(HPMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HPMMSpecs NOT to have private method 'private_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_protected_instance_method_spec.rb b/spec/mspec/spec/matchers/have_protected_instance_method_spec.rb
new file mode 100644
index 0000000000..45b39004a3
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_protected_instance_method_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HPIMMSpecs
+ protected
+
+ def protected_method
+ end
+
+ class Subclass < HPIMMSpecs
+ protected
+
+ def protected_sub_method
+ end
+ end
+end
+
+RSpec.describe HaveProtectedInstanceMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HaveProtectedInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the protected instance method" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :protected_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_truthy
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy
+ end
+
+ it "does not match when mod does not have the protected instance method" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :another_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :protected_method, false
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HPIMMSpecs to have protected instance method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HPIMMSpecs NOT to have protected instance method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_public_instance_method_spec.rb b/spec/mspec/spec/matchers/have_public_instance_method_spec.rb
new file mode 100644
index 0000000000..771d5b7911
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_public_instance_method_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HPIMMSpecs
+ def public_method
+ end
+
+ class Subclass < HPIMMSpecs
+ def public_sub_method
+ end
+ end
+end
+
+RSpec.describe HavePublicInstanceMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HavePublicInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the public instance method" do
+ matcher = HavePublicInstanceMethodMatcher.new :public_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_truthy
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy
+ end
+
+ it "does not match when mod does not have the public instance method" do
+ matcher = HavePublicInstanceMethodMatcher.new :another_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HavePublicInstanceMethodMatcher.new :public_method, false
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HavePublicInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HPIMMSpecs to have public instance method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HavePublicInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HPIMMSpecs NOT to have public instance method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_singleton_method_spec.rb b/spec/mspec/spec/matchers/have_singleton_method_spec.rb
new file mode 100644
index 0000000000..61ef00d49c
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_singleton_method_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HSMMSpecs
+ def self.singleton_method
+ end
+end
+
+RSpec.describe HaveSingletonMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HaveSingletonMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when the class has a singleton method" do
+ matcher = HaveSingletonMethodMatcher.new :singleton_method
+ expect(matcher.matches?(HSMMSpecs)).to be_truthy
+ end
+
+ it "matches when the object has a singleton method" do
+ obj = double("HSMMSpecs")
+ def obj.singleton_method; end
+
+ matcher = HaveSingletonMethodMatcher.new :singleton_method
+ expect(matcher.matches?(obj)).to be_truthy
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveSingletonMethodMatcher.new :some_method
+ matcher.matches?(HSMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HSMMSpecs to have singleton method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure message for #should_not" do
+ matcher = HaveSingletonMethodMatcher.new :singleton_method
+ matcher.matches?(HSMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HSMMSpecs NOT to have singleton method 'singleton_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/include_any_of_spec.rb b/spec/mspec/spec/matchers/include_any_of_spec.rb
new file mode 100644
index 0000000000..1473bb6d0b
--- /dev/null
+++ b/spec/mspec/spec/matchers/include_any_of_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe IncludeAnyOfMatcher do
+ it "matches when actual includes expected" do
+ expect(IncludeAnyOfMatcher.new(2).matches?([1,2,3])).to eq(true)
+ expect(IncludeAnyOfMatcher.new("b").matches?("abc")).to eq(true)
+ end
+
+ it "does not match when actual does not include expected" do
+ expect(IncludeAnyOfMatcher.new(4).matches?([1,2,3])).to eq(false)
+ expect(IncludeAnyOfMatcher.new("d").matches?("abc")).to eq(false)
+ end
+
+ it "matches when actual includes all expected" do
+ expect(IncludeAnyOfMatcher.new(3, 2, 1).matches?([1,2,3])).to eq(true)
+ expect(IncludeAnyOfMatcher.new("a", "b", "c").matches?("abc")).to eq(true)
+ end
+
+ it "matches when actual includes any expected" do
+ expect(IncludeAnyOfMatcher.new(3, 4, 5).matches?([1,2,3])).to eq(true)
+ expect(IncludeAnyOfMatcher.new("c", "d", "e").matches?("abc")).to eq(true)
+ end
+
+ it "does not match when actual does not include any expected" do
+ expect(IncludeAnyOfMatcher.new(4, 5).matches?([1,2,3])).to eq(false)
+ expect(IncludeAnyOfMatcher.new("de").matches?("abc")).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = IncludeAnyOfMatcher.new(5, 6)
+ matcher.matches?([1,2,3])
+ expect(matcher.failure_message).to eq(["Expected [1, 2, 3]", "to include any of [5, 6]"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = IncludeAnyOfMatcher.new(1, 2, 3)
+ matcher.matches?([1,2])
+ expect(matcher.negative_failure_message).to eq(["Expected [1, 2]", "not to include any of [1, 2, 3]"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/include_spec.rb b/spec/mspec/spec/matchers/include_spec.rb
new file mode 100644
index 0000000000..6bf1bef085
--- /dev/null
+++ b/spec/mspec/spec/matchers/include_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe IncludeMatcher do
+ it "matches when actual includes expected" do
+ expect(IncludeMatcher.new(2).matches?([1,2,3])).to eq(true)
+ expect(IncludeMatcher.new("b").matches?("abc")).to eq(true)
+ end
+
+ it "does not match when actual does not include expected" do
+ expect(IncludeMatcher.new(4).matches?([1,2,3])).to eq(false)
+ expect(IncludeMatcher.new("d").matches?("abc")).to eq(false)
+ end
+
+ it "matches when actual includes all expected" do
+ expect(IncludeMatcher.new(3, 2, 1).matches?([1,2,3])).to eq(true)
+ expect(IncludeMatcher.new("a", "b", "c").matches?("abc")).to eq(true)
+ end
+
+ it "does not match when actual does not include all expected" do
+ expect(IncludeMatcher.new(3, 2, 4).matches?([1,2,3])).to eq(false)
+ expect(IncludeMatcher.new("a", "b", "c", "d").matches?("abc")).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = IncludeMatcher.new(5, 2)
+ matcher.matches?([1,2,3])
+ expect(matcher.failure_message).to eq(["Expected [1, 2, 3]", "to include 5"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = IncludeMatcher.new(1, 2, 3)
+ matcher.matches?([1,2,3])
+ expect(matcher.negative_failure_message).to eq(["Expected [1, 2, 3]", "not to include 3"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/infinity_spec.rb b/spec/mspec/spec/matchers/infinity_spec.rb
new file mode 100644
index 0000000000..78c4194526
--- /dev/null
+++ b/spec/mspec/spec/matchers/infinity_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'mspec/matchers'
+
+RSpec.describe InfinityMatcher do
+ it "matches when actual is infinite and has the correct sign" do
+ expect(InfinityMatcher.new(1).matches?(infinity_value)).to eq(true)
+ expect(InfinityMatcher.new(-1).matches?(-infinity_value)).to eq(true)
+ end
+
+ it "does not match when actual is not infinite" do
+ expect(InfinityMatcher.new(1).matches?(1.0)).to eq(false)
+ expect(InfinityMatcher.new(-1).matches?(-1.0)).to eq(false)
+ end
+
+ it "does not match when actual is infinite but has the incorrect sign" do
+ expect(InfinityMatcher.new(1).matches?(-infinity_value)).to eq(false)
+ expect(InfinityMatcher.new(-1).matches?(infinity_value)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = InfinityMatcher.new(-1)
+ matcher.matches?(0)
+ expect(matcher.failure_message).to eq(["Expected 0", "to be -Infinity"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = InfinityMatcher.new(1)
+ matcher.matches?(infinity_value)
+ expect(matcher.negative_failure_message).to eq(["Expected Infinity", "not to be Infinity"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/match_yaml_spec.rb b/spec/mspec/spec/matchers/match_yaml_spec.rb
new file mode 100644
index 0000000000..85123bb87d
--- /dev/null
+++ b/spec/mspec/spec/matchers/match_yaml_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe MatchYAMLMatcher do
+ before :each do
+ @matcher = MatchYAMLMatcher.new("--- \nfoo: bar\n")
+ end
+
+ it "compares YAML documents and matches if they're equivalent" do
+ expect(@matcher.matches?("--- \nfoo: bar\n")).to eq(true)
+ end
+
+ it "compares YAML documents and does not match if they're not equivalent" do
+ expect(@matcher.matches?("--- \nbar: foo\n")).to eq(false)
+ expect(@matcher.matches?("--- \nfoo: \nbar\n")).to eq(false)
+ end
+
+ it "also receives objects that respond_to to_yaml" do
+ matcher = MatchYAMLMatcher.new("some string")
+ expect(matcher.matches?("some string")).to eq(true)
+
+ matcher = MatchYAMLMatcher.new(['a', 'b'])
+ expect(matcher.matches?("--- \n- a\n- b\n")).to eq(true)
+
+ matcher = MatchYAMLMatcher.new("foo" => "bar")
+ expect(matcher.matches?("--- \nfoo: bar\n")).to eq(true)
+ end
+
+ it "matches documents with trailing whitespace" do
+ expect(@matcher.matches?("--- \nfoo: bar \n")).to eq(true)
+ expect(@matcher.matches?("--- \nfoo: bar \n")).to eq(true)
+ end
+
+ it "fails with a descriptive error message" do
+ expect(@matcher.matches?("foo")).to eq(false)
+ expect(@matcher.failure_message).to eq(["Expected \"foo\"", " to match \"--- \\nfoo: bar\\n\""])
+ end
+end
diff --git a/spec/mspec/spec/matchers/output_spec.rb b/spec/mspec/spec/matchers/output_spec.rb
new file mode 100644
index 0000000000..3baad9a4b2
--- /dev/null
+++ b/spec/mspec/spec/matchers/output_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe OutputMatcher do
+ it "matches when executing the proc results in the expected output to $stdout" do
+ proc = Proc.new { puts "bang!" }
+ expect(OutputMatcher.new("bang!\n", nil).matches?(proc)).to eq(true)
+ expect(OutputMatcher.new("pop", nil).matches?(proc)).to eq(false)
+ expect(OutputMatcher.new(/bang/, nil).matches?(proc)).to eq(true)
+ expect(OutputMatcher.new(/po/, nil).matches?(proc)).to eq(false)
+ end
+
+ it "matches when executing the proc results in the expected output to $stderr" do
+ proc = Proc.new { $stderr.write "boom!" }
+ expect(OutputMatcher.new(nil, "boom!").matches?(proc)).to eq(true)
+ expect(OutputMatcher.new(nil, "fizzle").matches?(proc)).to eq(false)
+ expect(OutputMatcher.new(nil, /boom/).matches?(proc)).to eq(true)
+ expect(OutputMatcher.new(nil, /fizzl/).matches?(proc)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ proc = Proc.new { print "unexpected"; $stderr.print "unerror" }
+ matcher = OutputMatcher.new("expected", "error")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stdout: \"expected\"\n $stderr: \"error\"\n",
+ " got:\n $stdout: \"unexpected\"\n $stderr: \"unerror\"\n"]
+ )
+ matcher = OutputMatcher.new("expected", nil)
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stdout: \"expected\"\n",
+ " got:\n $stdout: \"unexpected\"\n"]
+ )
+ matcher = OutputMatcher.new(nil, "error")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stderr: \"error\"\n",
+ " got:\n $stderr: \"unerror\"\n"]
+ )
+ matcher = OutputMatcher.new(/base/, nil)
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stdout: /base/\n",
+ " got:\n $stdout: \"unexpected\"\n"]
+ )
+ matcher = OutputMatcher.new(nil, /octave/)
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stderr: /octave/\n",
+ " got:\n $stderr: \"unerror\"\n"]
+ )
+ end
+
+ it "provides a useful negative failure message" do
+ proc = Proc.new { puts "expected"; $stderr.puts "error" }
+ matcher = OutputMatcher.new("expected", "error")
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stdout: \"expected\"\n $stderr: \"error\"\n"]
+ )
+ matcher = OutputMatcher.new("expected", nil)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stdout: \"expected\"\n"]
+ )
+ matcher = OutputMatcher.new(nil, "error")
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stderr: \"error\"\n"]
+ )
+ matcher = OutputMatcher.new(/expect/, nil)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stdout: \"expected\"\n"]
+ )
+ matcher = OutputMatcher.new(nil, /err/)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stderr: \"error\"\n"]
+ )
+ end
+end
diff --git a/spec/mspec/spec/matchers/output_to_fd_spec.rb b/spec/mspec/spec/matchers/output_to_fd_spec.rb
new file mode 100644
index 0000000000..a39cab3206
--- /dev/null
+++ b/spec/mspec/spec/matchers/output_to_fd_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe OutputToFDMatcher do
+ # Figure out how in the hell to achieve this
+ it "matches when running the block produces the expected output to the given FD" do
+ expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { $stderr.print "Hi\n" })).to eq(true)
+ end
+
+ it "does not match if running the block does not produce the expected output to the FD" do
+ expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { $stderr.puts("Hello\n") })).to eq(false)
+ end
+
+ it "propagate the exception if one is thrown while matching" do
+ exc = RuntimeError.new("propagates")
+ expect {
+ expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda {
+ raise exc
+ })).to eq(false)
+ }.to raise_error(exc)
+ end
+
+ it "defaults to matching against STDOUT" do
+ object = Object.new
+ object.extend MSpecMatchers
+ expect(object.send(:output_to_fd, "Hi\n").matches?(lambda { $stdout.print "Hi\n" })).to eq(true)
+ end
+
+ it "accepts any IO instance" do
+ io = IO.new STDOUT.fileno
+ expect(OutputToFDMatcher.new("Hi\n", io).matches?(lambda { io.print "Hi\n" })).to eq(true)
+ end
+
+ it "allows matching with a Regexp" do
+ s = "Hi there\n"
+ expect(OutputToFDMatcher.new(/Hi/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/Hi?/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/[hH]i?/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/.*/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/H.*?here/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/Ahoy/, STDERR).matches?(lambda { $stderr.print s })).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/matchers/raise_error_spec.rb b/spec/mspec/spec/matchers/raise_error_spec.rb
new file mode 100644
index 0000000000..3849c7dd2a
--- /dev/null
+++ b/spec/mspec/spec/matchers/raise_error_spec.rb
@@ -0,0 +1,234 @@
+require 'spec_helper'
+
+class ExpectedException < Exception; end
+class UnexpectedException < Exception; end
+
+RSpec.describe RaiseErrorMatcher do
+ before :each do
+ state = double("run state").as_null_object
+ allow(MSpec).to receive(:current).and_return(state)
+ end
+
+ it "matches when the proc raises the expected exception" do
+ proc = Proc.new { raise ExpectedException }
+ matcher = RaiseErrorMatcher.new(ExpectedException, nil)
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "executes its optional {/} block if matched" do
+ ensure_mspec_method(-> {}.method(:should))
+
+ run = false
+ -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error { |error|
+ expect(error.class).to eq(ExpectedException)
+ run = true
+ }
+ expect(run).to eq(true)
+ end
+
+ it "executes its optional do/end block if matched" do
+ ensure_mspec_method(-> {}.method(:should))
+
+ run = false
+ -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error do |error|
+ expect(error.class).to eq(ExpectedException)
+ run = true
+ end
+ expect(run).to eq(true)
+ end
+
+ it "matches when the proc raises the expected exception with the expected message" do
+ proc = Proc.new { raise ExpectedException, "message" }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "message")
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "matches when the proc raises the expected exception with a matching message" do
+ proc = Proc.new { raise ExpectedException, "some message" }
+ matcher = RaiseErrorMatcher.new(ExpectedException, /some/)
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "does not match when the proc does not raise the expected exception" do
+ exc = UnexpectedException.new
+ matcher = RaiseErrorMatcher.new(ExpectedException, nil)
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ expect {
+ matcher.matches?(Proc.new { raise exc })
+ }.to raise_error(UnexpectedException)
+ end
+
+ it "does not match when the proc raises the expected exception with an unexpected message" do
+ exc = ExpectedException.new("unexpected")
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ expect {
+ matcher.matches?(Proc.new { raise exc })
+ }.to raise_error(ExpectedException)
+ end
+
+ it "does not match when the proc does not raise an exception" do
+ proc = Proc.new {}
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ expect(matcher.matches?(proc)).to eq(false)
+ end
+
+ it "provides a useful failure message when the exception class differs" do
+ exc = UnexpectedException.new("message")
+ matcher = RaiseErrorMatcher.new(ExpectedException, "message")
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ begin
+ matcher.matches?(Proc.new { raise exc })
+ rescue UnexpectedException => e
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("message")', 'but got: UnexpectedException("message")']
+ )
+ expect(ExceptionState.new(nil, nil, e).message).to eq(
+ "Expected ExpectedException(\"message\")\nbut got: UnexpectedException(\"message\")"
+ )
+ else
+ raise "no exception"
+ end
+ end
+
+ it "provides a useful failure message when the proc raises the expected exception with an unexpected message" do
+ exc = ExpectedException.new("unexpected")
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ begin
+ matcher.matches?(Proc.new { raise exc })
+ rescue ExpectedException => e
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', 'but got: ExpectedException("unexpected")']
+ )
+ expect(ExceptionState.new(nil, nil, e).message).to eq(
+ "Expected ExpectedException(\"expected\")\nbut got: ExpectedException(\"unexpected\")"
+ )
+ else
+ raise "no exception"
+ end
+ end
+
+ it "provides a useful failure message when both the exception class and message differ" do
+ exc = UnexpectedException.new("unexpected")
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ begin
+ matcher.matches?(Proc.new { raise exc })
+ rescue UnexpectedException => e
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', 'but got: UnexpectedException("unexpected")']
+ )
+ expect(ExceptionState.new(nil, nil, e).message).to eq(
+ "Expected ExpectedException(\"expected\")\nbut got: UnexpectedException(\"unexpected\")"
+ )
+ else
+ raise "no exception"
+ end
+ end
+
+ it "provides a useful failure message when no exception is raised" do
+ proc = Proc.new { 120 }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', "but no exception was raised (120 was returned)"]
+ )
+ end
+
+ it "provides a useful failure message when no exception is raised and nil is returned" do
+ proc = Proc.new { nil }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', "but no exception was raised (nil was returned)"]
+ )
+ end
+
+ it "provides a useful failure message when no exception is raised and the result raises in #pretty_inspect" do
+ result = Object.new
+ def result.pretty_inspect
+ raise ArgumentError, "bad"
+ end
+ proc = Proc.new { result }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', 'but no exception was raised (#<Object>(#pretty_inspect raised #<ArgumentError: bad>) was returned)']
+ )
+ end
+
+ it "provides a useful negative failure message" do
+ proc = Proc.new { raise ExpectedException, "expected" }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ['Expected to not get ExpectedException("expected")', ""]
+ )
+ end
+
+ it "provides a useful negative failure message for strict subclasses of the matched exception class" do
+ proc = Proc.new { raise UnexpectedException, "unexpected" }
+ matcher = RaiseErrorMatcher.new(Exception, nil)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ['Expected to not get Exception', 'but got: UnexpectedException']
+ )
+ end
+
+ it "matches cause if given" do
+ cause = RuntimeError.new("foo")
+ proc = -> do
+ raise cause
+ rescue
+ raise "bar"
+ end
+
+ matcher = RaiseErrorMatcher.new(RuntimeError, cause: cause)
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "matches message and cause if given" do
+ cause = RuntimeError.new("foo")
+ proc = -> do
+ raise cause
+ rescue
+ raise "bar"
+ end
+
+ matcher = RaiseErrorMatcher.new(RuntimeError, "bar", cause: cause)
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "provides useful negative failure message when cause does not match" do
+ cause = RuntimeError.new("bar")
+ proc = -> do
+ raise "foo"
+ end
+
+ matcher = RaiseErrorMatcher.new(RuntimeError, cause: cause)
+
+ begin
+ matcher.matches?(proc)
+ rescue RuntimeError
+ expect(matcher.failure_message).to eq(
+ ['Expected RuntimeError(cause: #<RuntimeError: bar>)', 'but got: RuntimeError(cause: nil)']
+ )
+ end
+
+ matcher = RaiseErrorMatcher.new(RuntimeError, "foo", cause: cause)
+
+ begin
+ matcher.matches?(proc)
+ rescue RuntimeError
+ expect(matcher.failure_message).to eq(
+ ['Expected RuntimeError("foo", cause: #<RuntimeError: bar>)', 'but got: RuntimeError("foo", cause: nil)']
+ )
+ end
+ end
+end
diff --git a/spec/mspec/spec/matchers/respond_to_spec.rb b/spec/mspec/spec/matchers/respond_to_spec.rb
new file mode 100644
index 0000000000..6f1cd8d148
--- /dev/null
+++ b/spec/mspec/spec/matchers/respond_to_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe RespondToMatcher do
+ it "matches when actual does respond_to? expected" do
+ expect(RespondToMatcher.new(:to_s).matches?(Object.new)).to eq(true)
+ expect(RespondToMatcher.new(:inject).matches?([])).to eq(true)
+ expect(RespondToMatcher.new(:[]).matches?(1)).to eq(true)
+ expect(RespondToMatcher.new(:[]=).matches?("string")).to eq(true)
+ end
+
+ it "does not match when actual does not respond_to? expected" do
+ expect(RespondToMatcher.new(:to_i).matches?(Object.new)).to eq(false)
+ expect(RespondToMatcher.new(:inject).matches?(1)).to eq(false)
+ expect(RespondToMatcher.new(:non_existent_method).matches?([])).to eq(false)
+ expect(RespondToMatcher.new(:[]=).matches?(1)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = RespondToMatcher.new(:non_existent_method)
+ matcher.matches?('string')
+ expect(matcher.failure_message).to eq([
+ "Expected \"string\" (String)", "to respond to non_existent_method"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = RespondToMatcher.new(:to_i)
+ matcher.matches?(4.0)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected 4.0 (Float)", "not to respond to to_i"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/signed_zero_spec.rb b/spec/mspec/spec/matchers/signed_zero_spec.rb
new file mode 100644
index 0000000000..6d1c1007bc
--- /dev/null
+++ b/spec/mspec/spec/matchers/signed_zero_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe SignedZeroMatcher do
+ it "matches when actual is zero and has the correct sign" do
+ expect(SignedZeroMatcher.new(1).matches?(0.0)).to eq(true)
+ expect(SignedZeroMatcher.new(-1).matches?(-0.0)).to eq(true)
+ end
+
+ it "does not match when actual is non-zero" do
+ expect(SignedZeroMatcher.new(1).matches?(1.0)).to eq(false)
+ expect(SignedZeroMatcher.new(-1).matches?(-1.0)).to eq(false)
+ end
+
+ it "does not match when actual is zero but has the incorrect sign" do
+ expect(SignedZeroMatcher.new(1).matches?(-0.0)).to eq(false)
+ expect(SignedZeroMatcher.new(-1).matches?(0.0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = SignedZeroMatcher.new(-1)
+ matcher.matches?(0.0)
+ expect(matcher.failure_message).to eq(["Expected 0.0", "to be -0.0"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = SignedZeroMatcher.new(-1)
+ matcher.matches?(-0.0)
+ expect(matcher.negative_failure_message).to eq(["Expected -0.0", "not to be -0.0"])
+ end
+end
diff --git a/spec/mspec/spec/mocks/mock_spec.rb b/spec/mspec/spec/mocks/mock_spec.rb
new file mode 100644
index 0000000000..7426e0ff88
--- /dev/null
+++ b/spec/mspec/spec/mocks/mock_spec.rb
@@ -0,0 +1,529 @@
+# This is a bit awkward. Currently the way to verify that the
+# opposites are true (for example a failure when the specified
+# arguments are NOT provided) is to simply alter the particular
+# spec to a failure condition.
+require 'spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/mocks/mock'
+require 'mspec/mocks/proxy'
+
+RSpec.describe Mock, ".mocks" do
+ it "returns a Hash" do
+ expect(Mock.mocks).to be_kind_of(Hash)
+ end
+end
+
+RSpec.describe Mock, ".stubs" do
+ it "returns a Hash" do
+ expect(Mock.stubs).to be_kind_of(Hash)
+ end
+end
+
+RSpec.describe Mock, ".replaced_name" do
+ it "returns the name for a method that is being replaced by a mock method" do
+ m = double('a fake id')
+ expect(Mock.replaced_name(Mock.replaced_key(m, :method_call))).to eq(:"__mspec_method_call__")
+ end
+end
+
+RSpec.describe Mock, ".replaced_key" do
+ it "returns a key used internally by Mock" do
+ m = double('a fake id')
+ expect(Mock.replaced_key(m, :method_call)).to eq([m.object_id, :method_call])
+ end
+end
+
+RSpec.describe Mock, ".replaced?" do
+ before :each do
+ @mock = double('install_method')
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ it "returns true if a method has been stubbed on an object" do
+ Mock.install_method @mock, :method_call
+ expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_truthy
+ end
+
+ it "returns true if a method has been mocked on an object" do
+ Mock.install_method @mock, :method_call, :stub
+ expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_truthy
+ end
+
+ it "returns false if a method has not been stubbed or mocked" do
+ expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_falsey
+ end
+end
+
+RSpec.describe Mock, ".name_or_inspect" do
+ before :each do
+ @mock = double("I have a #name")
+ end
+
+ it "returns the value of @name if set" do
+ @mock.instance_variable_set(:@name, "Myself")
+ expect(Mock.name_or_inspect(@mock)).to eq("Myself")
+ end
+end
+
+RSpec.describe Mock, ".install_method for mocks" do
+ before :each do
+ @mock = double('install_method')
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ after :each do
+ Mock.reset
+ end
+
+ it "returns a MockProxy instance" do
+ expect(Mock.install_method(@mock, :method_call)).to be_an_instance_of(MockProxy)
+ end
+
+ it "does not override a previously mocked method with the same name" do
+ Mock.install_method(@mock, :method_call).with(:a, :b).and_return(1)
+ Mock.install_method(@mock, :method_call).with(:c).and_return(2)
+ @mock.method_call(:a, :b)
+ @mock.method_call(:c)
+ expect { @mock.method_call(:d) }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ # This illustrates RSpec's behavior. This spec fails in mock call count verification
+ # on RSpec (i.e. Mock 'foo' expected :foo with (any args) once, but received it 0 times)
+ # and we mimic the behavior of RSpec.
+ #
+ # describe "A mock receiving multiple calls to #should_receive" do
+ # it "returns the first value mocked" do
+ # m = mock 'multiple #should_receive'
+ # m.should_receive(:foo).and_return(true)
+ # m.foo.should == true
+ # m.should_receive(:foo).and_return(false)
+ # m.foo.should == true
+ # end
+ # end
+ #
+ it "does not override a previously mocked method having the same arguments" do
+ Mock.install_method(@mock, :method_call).with(:a).and_return(true)
+ expect(@mock.method_call(:a)).to eq(true)
+ Mock.install_method(@mock, :method_call).with(:a).and_return(false)
+ expect(@mock.method_call(:a)).to eq(true)
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "properly sends #respond_to? calls to the aliased respond_to? method when not matching mock expectations" do
+ Mock.install_method(@mock, :respond_to?).with(:to_str).and_return('mock to_str')
+ Mock.install_method(@mock, :respond_to?).with(:to_int).and_return('mock to_int')
+ expect(@mock.respond_to?(:to_str)).to eq('mock to_str')
+ expect(@mock.respond_to?(:to_int)).to eq('mock to_int')
+ expect(@mock.respond_to?(:to_s)).to eq(true)
+ expect(@mock.respond_to?(:not_really_a_real_method_seriously)).to eq(false)
+ end
+
+ it "adds to the expectation tally" do
+ state = double("run state").as_null_object
+ allow(state).to receive(:state).and_return(double("spec state"))
+ expect(MSpec).to receive(:current).and_return(state)
+ expect(MSpec).to receive(:actions).with(:expectation, state.state)
+ Mock.install_method(@mock, :method_call).and_return(1)
+ expect(@mock.method_call).to eq(1)
+ end
+
+ it "registers that an expectation has been encountered" do
+ state = double("run state").as_null_object
+ allow(state).to receive(:state).and_return(double("spec state"))
+ expect(MSpec).to receive(:expectation)
+ Mock.install_method(@mock, :method_call).and_return(1)
+ expect(@mock.method_call).to eq(1)
+ end
+end
+
+RSpec.describe Mock, ".install_method for stubs" do
+ before :each do
+ @mock = double('install_method')
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "returns a MockProxy instance" do
+ expect(Mock.install_method(@mock, :method_call, :stub)).to be_an_instance_of(MockProxy)
+ end
+
+ # This illustrates RSpec's behavior. This spec passes on RSpec and we mimic it
+ #
+ # describe "A mock receiving multiple calls to #stub" do
+ # it "returns the last value stubbed" do
+ # m = mock 'multiple #stub'
+ # m.stub(:foo).and_return(true)
+ # m.foo.should == true
+ # m.stub(:foo).and_return(false)
+ # m.foo.should == false
+ # end
+ # end
+ it "inserts new stubs before old stubs" do
+ Mock.install_method(@mock, :method_call, :stub).with(:a).and_return(true)
+ expect(@mock.method_call(:a)).to eq(true)
+ Mock.install_method(@mock, :method_call, :stub).with(:a).and_return(false)
+ expect(@mock.method_call(:a)).to eq(false)
+ Mock.verify_count
+ end
+
+ it "does not add to the expectation tally" do
+ state = double("run state").as_null_object
+ allow(state).to receive(:state).and_return(double("spec state"))
+ expect(MSpec).not_to receive(:actions)
+ Mock.install_method(@mock, :method_call, :stub).and_return(1)
+ expect(@mock.method_call).to eq(1)
+ end
+end
+
+RSpec.describe Mock, ".install_method" do
+ before :each do
+ @mock = double('install_method')
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "does not alias a mocked or stubbed method when installing a new mock or stub" do
+ expect(@mock).not_to respond_to(:method_call)
+
+ Mock.install_method @mock, :method_call
+ expect(@mock).to respond_to(:method_call)
+ expect(@mock).not_to respond_to(Mock.replaced_name(Mock.replaced_key(@mock, :method_call)))
+
+ Mock.install_method @mock, :method_call, :stub
+ expect(@mock).to respond_to(:method_call)
+ expect(@mock).not_to respond_to(Mock.replaced_name(Mock.replaced_key(@mock, :method_call)))
+ end
+end
+
+class MockAndRaiseError < Exception; end
+
+RSpec.describe Mock, ".verify_call" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('verify_call')
+ @proxy = Mock.install_method @mock, :method_call
+ end
+
+ after :each do
+ ScratchPad.clear
+ Mock.cleanup
+ end
+
+ it "does not raise an exception when the mock method receives the expected arguments" do
+ @proxy.with(1, 'two', :three)
+ Mock.verify_call @mock, :method_call, 1, 'two', :three
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock method does not receive the expected arguments" do
+ @proxy.with(4, 2)
+ expect {
+ Mock.verify_call @mock, :method_call, 42
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock method is called with arguments but expects none" do
+ expect {
+ @proxy.with(:no_args)
+ Mock.verify_call @mock, :method_call, "hello"
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock method is called with no arguments but expects some" do
+ @proxy.with("hello", "beautiful", "world")
+ expect {
+ Mock.verify_call @mock, :method_call
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "does not raise an exception when the mock method is called with arguments and is expecting :any_args" do
+ @proxy.with(:any_args)
+ Mock.verify_call @mock, :method_call, 1, 2, 3
+ end
+
+ it "yields a passed block when it is expected to" do
+ @proxy.and_yield()
+ Mock.verify_call @mock, :method_call do
+ ScratchPad.record true
+ end
+ expect(ScratchPad.recorded).to eq(true)
+ end
+
+ it "does not yield a passed block when it is not expected to" do
+ Mock.verify_call @mock, :method_call do
+ ScratchPad.record true
+ end
+ expect(ScratchPad.recorded).to eq(nil)
+ end
+
+ it "can yield subsequently" do
+ @proxy.and_yield(1).and_yield(2).and_yield(3)
+
+ ScratchPad.record []
+ Mock.verify_call @mock, :method_call do |arg|
+ ScratchPad << arg
+ end
+ expect(ScratchPad.recorded).to eq([1, 2, 3])
+ end
+
+ it "can yield and return an expected value" do
+ @proxy.and_yield(1).and_return(3)
+
+ expect(Mock.verify_call(@mock, :method_call) { |arg| ScratchPad.record arg }).to eq(3)
+ expect(ScratchPad.recorded).to eq(1)
+ end
+
+ it "raises an exception when it is expected to yield but no block is given" do
+ @proxy.and_yield(1, 2, 3)
+ expect {
+ Mock.verify_call(@mock, :method_call)
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "raises an exception when it is expected to yield more arguments than the block can take" do
+ @proxy.and_yield(1, 2, 3)
+ expect {
+ Mock.verify_call(@mock, :method_call) {|a, b|}
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "does not raise an exception when it is expected to yield to a block that can take any number of arguments" do
+ @proxy.and_yield(1, 2, 3)
+ expect {
+ Mock.verify_call(@mock, :method_call) {|*a|}
+ }.not_to raise_error
+ end
+
+ it "raises an exception when expected to" do
+ @proxy.and_raise(MockAndRaiseError)
+ expect {
+ Mock.verify_call @mock, :method_call
+ }.to raise_error(MockAndRaiseError)
+ end
+end
+
+RSpec.describe Mock, ".verify_call mixing mocks and stubs" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('verify_call')
+ end
+
+ after :each do
+ ScratchPad.clear
+ Mock.cleanup
+ end
+
+ it "checks the mock arguments when a mock is defined after a stub" do
+ Mock.install_method @mock, :method_call, :stub
+ Mock.install_method(@mock, :method_call, :mock).with("arg")
+
+ expect {
+ @mock.method_call
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \(\)/)
+
+ expect {
+ @mock.method_call("a", "b")
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("a", "b"\)/)
+
+ expect {
+ @mock.method_call("foo")
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("foo"\)/)
+
+ @mock.method_call("arg")
+ end
+
+ it "checks the mock arguments when a stub is defined after a mock" do
+ Mock.install_method(@mock, :method_call, :mock).with("arg")
+ Mock.install_method @mock, :method_call, :stub
+
+ expect {
+ @mock.method_call
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \(\)/)
+
+ expect {
+ @mock.method_call("a", "b")
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("a", "b"\)/)
+
+ expect {
+ @mock.method_call("foo")
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("foo"\)/)
+
+ @mock.method_call("arg")
+ end
+end
+
+RSpec.describe Mock, ".verify_count" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('verify_count')
+ @proxy = Mock.install_method @mock, :method_call
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "does not raise an exception when the mock receives at least the expected number of calls" do
+ @proxy.at_least(2)
+ @mock.method_call
+ @mock.method_call
+ Mock.verify_count
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock receives less than at least the expected number of calls" do
+ @proxy.at_least(2)
+ @mock.method_call
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "does not raise an exception when the mock receives at most the expected number of calls" do
+ @proxy.at_most(2)
+ @mock.method_call
+ @mock.method_call
+ Mock.verify_count
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock receives more than at most the expected number of calls" do
+ @proxy.at_most(2)
+ @mock.method_call
+ @mock.method_call
+ @mock.method_call
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "does not raise an exception when the mock receives exactly the expected number of calls" do
+ @proxy.exactly(2)
+ @mock.method_call
+ @mock.method_call
+ Mock.verify_count
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock receives less than exactly the expected number of calls" do
+ @proxy.exactly(2)
+ @mock.method_call
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock receives more than exactly the expected number of calls" do
+ @proxy.exactly(2)
+ @mock.method_call
+ @mock.method_call
+ @mock.method_call
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+end
+
+RSpec.describe Mock, ".verify_count mixing mocks and stubs" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('verify_count')
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "does not raise an exception for a stubbed method that is never called" do
+ Mock.install_method @mock, :method_call, :stub
+ Mock.verify_count
+ end
+
+ it "verifies the calls to the mocked method when a mock is defined after a stub" do
+ Mock.install_method @mock, :method_call, :stub
+ Mock.install_method @mock, :method_call, :mock
+
+ expect {
+ Mock.verify_count
+ }.to raise_error(SpecExpectationNotMetError, /received it 0 times/)
+
+ @mock.method_call
+ Mock.verify_count
+ end
+
+ it "verifies the calls to the mocked method when a mock is defined before a stub" do
+ Mock.install_method @mock, :method_call, :mock
+ Mock.install_method @mock, :method_call, :stub
+
+ expect {
+ Mock.verify_count
+ }.to raise_error(SpecExpectationNotMetError, /received it 0 times/)
+
+ @mock.method_call
+ Mock.verify_count
+ end
+end
+
+RSpec.describe Mock, ".cleanup" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('cleanup')
+ @proxy = Mock.install_method @mock, :method_call
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "removes the mock method call if it did not override an existing method" do
+ expect(@mock).to respond_to(:method_call)
+
+ Mock.cleanup
+ expect(@mock).not_to respond_to(:method_call)
+ end
+
+ it "removes the replaced method if the mock method overrides an existing method" do
+ def @mock.already_here() :hey end
+ expect(@mock).to respond_to(:already_here)
+ replaced_name = Mock.replaced_name(Mock.replaced_key(@mock, :already_here))
+ Mock.install_method @mock, :already_here
+ expect(@mock).to respond_to(replaced_name)
+
+ Mock.cleanup
+ expect(@mock).not_to respond_to(replaced_name)
+ expect(@mock).to respond_to(:already_here)
+ expect(@mock.already_here).to eq(:hey)
+ end
+
+ it "removes all mock expectations" do
+ expect(Mock.mocks).to eq({ Mock.replaced_key(@mock, :method_call) => [@proxy] })
+ Mock.cleanup
+ expect(Mock.mocks).to eq({})
+ end
+
+ it "removes all stubs" do
+ Mock.cleanup # remove @proxy
+ @stub = Mock.install_method @mock, :method_call, :stub
+ expect(Mock.stubs).to eq({ Mock.replaced_key(@mock, :method_call) => [@stub] })
+ Mock.cleanup
+ expect(Mock.stubs).to eq({})
+ end
+
+ it "removes the replaced name for mocks" do
+ replaced_key = Mock.replaced_key(@mock, :method_call)
+ expect(Mock).to receive(:clear_replaced).with(replaced_key)
+
+ expect(Mock.replaced?(replaced_key)).to be_truthy
+
+ Mock.cleanup
+ expect(Mock.replaced?(replaced_key)).to be_falsey
+ end
+end
diff --git a/spec/mspec/spec/mocks/proxy_spec.rb b/spec/mspec/spec/mocks/proxy_spec.rb
new file mode 100644
index 0000000000..b994634694
--- /dev/null
+++ b/spec/mspec/spec/mocks/proxy_spec.rb
@@ -0,0 +1,405 @@
+require 'spec_helper'
+require 'mspec/mocks/proxy'
+
+RSpec.describe MockObject, ".new" do
+ it "creates a new mock object" do
+ m = MockObject.new('not a null object')
+ expect { m.not_a_method }.to raise_error(NoMethodError)
+ end
+
+ it "creates a new mock object that follows the NullObject pattern" do
+ m = MockObject.new('null object', :null_object => true)
+ expect(m.not_really_a_method).to equal(m)
+ end
+end
+
+RSpec.describe MockProxy, ".new" do
+ it "creates a mock proxy by default" do
+ expect(MockProxy.new.mock?).to be_truthy
+ end
+
+ it "creates a stub proxy by request" do
+ expect(MockProxy.new(:stub).stub?).to be_truthy
+ end
+
+ it "sets the call expectation to 1 call for a mock" do
+ expect(MockProxy.new.count).to eq([:exactly, 1])
+ end
+
+ it "sets the call expectation to any number of times for a stub" do
+ expect(MockProxy.new(:stub).count).to eq([:any_number_of_times, 0])
+ end
+end
+
+RSpec.describe MockProxy, "#count" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns the expected number of calls the mock should receive" do
+ expect(@proxy.count).to eq([:exactly, 1])
+ expect(@proxy.at_least(3).count).to eq([:at_least, 3])
+ end
+end
+
+RSpec.describe MockProxy, "#arguments" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns the expected arguments" do
+ expect(@proxy.arguments).to eq(:any_args)
+ end
+end
+
+RSpec.describe MockProxy, "#with" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.with(:a)).to be_equal(@proxy)
+ end
+
+ it "raises an ArgumentError if no arguments are given" do
+ expect { @proxy.with }.to raise_error(ArgumentError)
+ end
+
+ it "accepts any number of arguments" do
+ expect(@proxy.with(1, 2, 3)).to be_an_instance_of(MockProxy)
+ expect(@proxy.arguments).to eq([1,2,3])
+ end
+end
+
+RSpec.describe MockProxy, "#once" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.once).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to 1" do
+ @proxy.once
+ expect(@proxy.count).to eq([:exactly, 1])
+ end
+
+ it "accepts no arguments" do
+ expect { @proxy.once(:a) }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#twice" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.twice).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to 2" do
+ @proxy.twice
+ expect(@proxy.count).to eq([:exactly, 2])
+ end
+
+ it "accepts no arguments" do
+ expect { @proxy.twice(:b) }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#exactly" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.exactly(2)).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to exactly n" do
+ @proxy.exactly(5)
+ expect(@proxy.count).to eq([:exactly, 5])
+ end
+
+ it "does not accept an argument that Integer() cannot convert" do
+ expect { @proxy.exactly('x') }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#at_least" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.at_least(3)).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to at least n" do
+ @proxy.at_least(3)
+ expect(@proxy.count).to eq([:at_least, 3])
+ end
+
+ it "accepts :once :twice" do
+ @proxy.at_least(:once)
+ expect(@proxy.count).to eq([:at_least, 1])
+ @proxy.at_least(:twice)
+ expect(@proxy.count).to eq([:at_least, 2])
+ end
+
+ it "does not accept an argument that Integer() cannot convert" do
+ expect { @proxy.at_least('x') }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#at_most" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.at_most(2)).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to at most n" do
+ @proxy.at_most(2)
+ expect(@proxy.count).to eq([:at_most, 2])
+ end
+
+ it "accepts :once, :twice" do
+ @proxy.at_most(:once)
+ expect(@proxy.count).to eq([:at_most, 1])
+ @proxy.at_most(:twice)
+ expect(@proxy.count).to eq([:at_most, 2])
+ end
+
+ it "does not accept an argument that Integer() cannot convert" do
+ expect { @proxy.at_most('x') }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#any_number_of_times" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.any_number_of_times).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to any number of times" do
+ @proxy.any_number_of_times
+ expect(@proxy.count).to eq([:any_number_of_times, 0])
+ end
+
+ it "does not accept an argument" do
+ expect { @proxy.any_number_of_times(2) }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#and_return" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.and_return(false)).to equal(@proxy)
+ end
+
+ it "sets the expected return value" do
+ @proxy.and_return(false)
+ expect(@proxy.returning).to eq(false)
+ end
+
+ it "accepts any number of return values" do
+ @proxy.and_return(1, 2, 3)
+ expect(@proxy.returning).to eq(1)
+ expect(@proxy.returning).to eq(2)
+ expect(@proxy.returning).to eq(3)
+ end
+
+ it "implicitly sets the expected number of calls" do
+ @proxy.and_return(1, 2, 3)
+ expect(@proxy.count).to eq([:exactly, 3])
+ end
+
+ it "only sets the expected number of calls if it is higher than what is already set" do
+ @proxy.at_least(5).times.and_return(1, 2, 3)
+ expect(@proxy.count).to eq([:at_least, 5])
+
+ @proxy.at_least(2).times.and_return(1, 2, 3)
+ expect(@proxy.count).to eq([:at_least, 3])
+ end
+end
+
+RSpec.describe MockProxy, "#returning" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns nil by default" do
+ expect(@proxy.returning).to be_nil
+ end
+
+ it "returns the value set by #and_return" do
+ @proxy.and_return(2)
+ expect(@proxy.returning).to eq(2)
+ expect(@proxy.returning).to eq(2)
+ end
+
+ it "returns a sequence of values set by #and_return" do
+ @proxy.and_return(1,2,3,4)
+ expect(@proxy.returning).to eq(1)
+ expect(@proxy.returning).to eq(2)
+ expect(@proxy.returning).to eq(3)
+ expect(@proxy.returning).to eq(4)
+ expect(@proxy.returning).to eq(4)
+ expect(@proxy.returning).to eq(4)
+ end
+end
+
+RSpec.describe MockProxy, "#calls" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns the number of times the proxy is called" do
+ expect(@proxy.calls).to eq(0)
+ end
+end
+
+RSpec.describe MockProxy, "#called" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "increments the number of times the proxy is called" do
+ @proxy.called
+ @proxy.called
+ expect(@proxy.calls).to eq(2)
+ end
+end
+
+RSpec.describe MockProxy, "#times" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "is a no-op" do
+ expect(@proxy.times).to eq(@proxy)
+ end
+end
+
+RSpec.describe MockProxy, "#stub?" do
+ it "returns true if the proxy is created as a stub" do
+ expect(MockProxy.new(:stub).stub?).to be_truthy
+ end
+
+ it "returns false if the proxy is created as a mock" do
+ expect(MockProxy.new(:mock).stub?).to be_falsey
+ end
+end
+
+RSpec.describe MockProxy, "#mock?" do
+ it "returns true if the proxy is created as a mock" do
+ expect(MockProxy.new(:mock).mock?).to be_truthy
+ end
+
+ it "returns false if the proxy is created as a stub" do
+ expect(MockProxy.new(:stub).mock?).to be_falsey
+ end
+end
+
+RSpec.describe MockProxy, "#and_yield" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.and_yield(false)).to equal(@proxy)
+ end
+
+ it "sets the expected values to yield" do
+ expect(@proxy.and_yield(1).yielding).to eq([[1]])
+ end
+
+ it "accepts multiple values to yield" do
+ expect(@proxy.and_yield(1, 2, 3).yielding).to eq([[1, 2, 3]])
+ end
+end
+
+RSpec.describe MockProxy, "#raising" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns nil by default" do
+ expect(@proxy.raising).to be_nil
+ end
+
+ it "returns the exception object passed to #and_raise" do
+ exc = double("exception")
+ @proxy.and_raise(exc)
+ expect(@proxy.raising).to equal(exc)
+ end
+
+ it "returns an instance of RuntimeError when a String is passed to #and_raise" do
+ @proxy.and_raise("an error")
+ exc = @proxy.raising
+ expect(exc).to be_an_instance_of(RuntimeError)
+ expect(exc.message).to eq("an error")
+ end
+end
+
+RSpec.describe MockProxy, "#yielding" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns an empty array by default" do
+ expect(@proxy.yielding).to eq([])
+ end
+
+ it "returns an array of arrays of values the proxy should yield" do
+ @proxy.and_yield(3)
+ expect(@proxy.yielding).to eq([[3]])
+ end
+
+ it "returns an accumulation of arrays of values the proxy should yield" do
+ @proxy.and_yield(1).and_yield(2, 3)
+ expect(@proxy.yielding).to eq([[1], [2, 3]])
+ end
+end
+
+RSpec.describe MockProxy, "#yielding?" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns false if the proxy is not yielding" do
+ expect(@proxy.yielding?).to be_falsey
+ end
+
+ it "returns true if the proxy is yielding" do
+ @proxy.and_yield(1)
+ expect(@proxy.yielding?).to be_truthy
+ end
+end
+
+RSpec.describe MockIntObject, "#to_int" do
+ before :each do
+ @int = MockIntObject.new(10)
+ end
+
+ it "returns the number if to_int is called" do
+ expect(@int.to_int).to eq(10)
+ expect(@int.count).to eq([:at_least, 1])
+ end
+
+ it "tries to convert the target to int if to_int is called" do
+ expect(MockIntObject.new(@int).to_int).to eq(10)
+ expect(@int.count).to eq([:at_least, 1])
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/filter_spec.rb b/spec/mspec/spec/runner/actions/filter_spec.rb
new file mode 100644
index 0000000000..7582b31c1d
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/filter_spec.rb
@@ -0,0 +1,84 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/mspec'
+require 'mspec/runner/tag'
+
+RSpec.describe ActionFilter do
+ it "creates a filter when not passed a description" do
+ expect(MatchFilter).not_to receive(:new)
+ ActionFilter.new(nil, nil)
+ end
+
+ it "creates a filter from a single description" do
+ expect(MatchFilter).to receive(:new).with(nil, "match me")
+ ActionFilter.new(nil, "match me")
+ end
+
+ it "creates a filter from an array of descriptions" do
+ expect(MatchFilter).to receive(:new).with(nil, "match me", "again")
+ ActionFilter.new(nil, ["match me", "again"])
+ end
+end
+
+RSpec.describe ActionFilter, "#===" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return(["match"])
+ @action = ActionFilter.new(nil, ["catch", "if you"])
+ end
+
+ it "returns false if there are no filters" do
+ action = ActionFilter.new
+ expect(action.===("anything")).to eq(false)
+ end
+
+ it "returns true if the argument matches any of the descriptions" do
+ expect(@action.===("catch")).to eq(true)
+ expect(@action.===("if you can")).to eq(true)
+ end
+
+ it "returns false if the argument does not match any of the descriptions" do
+ expect(@action.===("patch me")).to eq(false)
+ expect(@action.===("if I can")).to eq(false)
+ end
+end
+
+RSpec.describe ActionFilter, "#load" do
+ before :each do
+ @tag = SpecTag.new "tag(comment):description"
+ end
+
+ it "creates a filter from a single tag" do
+ expect(MSpec).to receive(:read_tags).with(["tag"]).and_return([@tag])
+ expect(MatchFilter).to receive(:new).with(nil, "description")
+ ActionFilter.new("tag", nil).load
+ end
+
+ it "creates a filter from an array of tags" do
+ expect(MSpec).to receive(:read_tags).with(["tag", "key"]).and_return([@tag])
+ expect(MatchFilter).to receive(:new).with(nil, "description")
+ ActionFilter.new(["tag", "key"], nil).load
+ end
+
+ it "creates a filter from both tags and descriptions" do
+ expect(MSpec).to receive(:read_tags).and_return([@tag])
+ filter = ActionFilter.new("tag", ["match me", "again"])
+ expect(MatchFilter).to receive(:new).with(nil, "description")
+ filter.load
+ end
+end
+
+RSpec.describe ActionFilter, "#register" do
+ it "registers itself with MSpec for the :load actions" do
+ filter = ActionFilter.new
+ expect(MSpec).to receive(:register).with(:load, filter)
+ filter.register
+ end
+end
+
+RSpec.describe ActionFilter, "#unregister" do
+ it "unregisters itself with MSpec for the :load actions" do
+ filter = ActionFilter.new
+ expect(MSpec).to receive(:unregister).with(:load, filter)
+ filter.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/tag_spec.rb b/spec/mspec/spec/runner/actions/tag_spec.rb
new file mode 100644
index 0000000000..738e9a18c9
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/tag_spec.rb
@@ -0,0 +1,313 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/tag'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/runner/tag'
+
+RSpec.describe TagAction, ".new" do
+ it "creates an MatchFilter with its tag and desc arguments" do
+ filter = double('action filter').as_null_object
+ expect(MatchFilter).to receive(:new).with(nil, "some", "thing").and_return(filter)
+ TagAction.new :add, :all, nil, nil, ["tag", "key"], ["some", "thing"]
+ end
+end
+
+RSpec.describe TagAction, "#===" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return(["match"])
+ @action = TagAction.new :add, :fail, nil, nil, nil, ["catch", "if you"]
+ end
+
+ it "returns true if there are no filters" do
+ action = TagAction.new :add, :all, nil, nil
+ expect(action.===("anything")).to eq(true)
+ end
+
+ it "returns true if the argument matches any of the descriptions" do
+ expect(@action.===("catch")).to eq(true)
+ expect(@action.===("if you can")).to eq(true)
+ end
+
+ it "returns false if the argument does not match any of the descriptions" do
+ expect(@action.===("patch me")).to eq(false)
+ expect(@action.===("if I can")).to eq(false)
+ end
+end
+
+RSpec.describe TagAction, "#exception?" do
+ before :each do
+ @action = TagAction.new :add, :fail, nil, nil, nil, nil
+ end
+
+ it "returns false if no exception has been raised while evaluating an example" do
+ expect(@action.exception?).to be_falsey
+ end
+
+ it "returns true if an exception was raised while evaluating an example" do
+ @action.exception ExceptionState.new nil, nil, Exception.new("failed")
+ expect(@action.exception?).to be_truthy
+ end
+end
+
+RSpec.describe TagAction, "#outcome?" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return([])
+ @exception = ExceptionState.new nil, nil, Exception.new("failed")
+ end
+
+ it "returns true if outcome is :fail and the spec fails" do
+ action = TagAction.new :add, :fail, nil, nil, nil, nil
+ action.exception @exception
+ expect(action.outcome?).to eq(true)
+ end
+
+ it "returns false if the outcome is :fail and the spec passes" do
+ action = TagAction.new :add, :fail, nil, nil, nil, nil
+ expect(action.outcome?).to eq(false)
+ end
+
+ it "returns true if the outcome is :pass and the spec passes" do
+ action = TagAction.new :del, :pass, nil, nil, nil, nil
+ expect(action.outcome?).to eq(true)
+ end
+
+ it "returns false if the outcome is :pass and the spec fails" do
+ action = TagAction.new :del, :pass, nil, nil, nil, nil
+ action.exception @exception
+ expect(action.outcome?).to eq(false)
+ end
+
+ it "returns true if the outcome is :all" do
+ action = TagAction.new :add, :all, nil, nil, nil, nil
+ action.exception @exception
+ expect(action.outcome?).to eq(true)
+ end
+end
+
+RSpec.describe TagAction, "#before" do
+ it "resets the #exception? flag to false" do
+ action = TagAction.new :add, :fail, nil, nil, nil, nil
+ expect(action.exception?).to be_falsey
+ action.exception ExceptionState.new(nil, nil, Exception.new("Fail!"))
+ expect(action.exception?).to be_truthy
+ action.before(ExampleState.new(ContextState.new("describe"), "it"))
+ expect(action.exception?).to be_falsey
+ end
+end
+
+RSpec.describe TagAction, "#exception" do
+ it "sets the #exception? flag" do
+ action = TagAction.new :add, :fail, nil, nil, nil, nil
+ expect(action.exception?).to be_falsey
+ action.exception ExceptionState.new(nil, nil, Exception.new("Fail!"))
+ expect(action.exception?).to be_truthy
+ end
+end
+
+RSpec.describe TagAction, "#after when action is :add" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return([])
+ context = ContextState.new "Catch#me"
+ @state = ExampleState.new context, "if you can"
+ @tag = SpecTag.new "tag(comment):Catch#me if you can"
+ allow(SpecTag).to receive(:new).and_return(@tag)
+ @exception = ExceptionState.new nil, nil, Exception.new("failed")
+ end
+
+ it "does not write a tag if the description does not match" do
+ expect(MSpec).not_to receive(:write_tag)
+ action = TagAction.new :add, :all, "tag", "comment", nil, "match"
+ action.after @state
+ end
+
+ it "does not write a tag if outcome is :fail and the spec passed" do
+ expect(MSpec).not_to receive(:write_tag)
+ action = TagAction.new :add, :fail, "tag", "comment", nil, "can"
+ action.after @state
+ end
+
+ it "writes a tag if the outcome is :fail and the spec failed" do
+ expect(MSpec).to receive(:write_tag).with(@tag)
+ action = TagAction.new :add, :fail, "tag", "comment", nil, "can"
+ action.exception @exception
+ action.after @state
+ end
+
+ it "does not write a tag if outcome is :pass and the spec failed" do
+ expect(MSpec).not_to receive(:write_tag)
+ action = TagAction.new :add, :pass, "tag", "comment", nil, "can"
+ action.exception @exception
+ action.after @state
+ end
+
+ it "writes a tag if the outcome is :pass and the spec passed" do
+ expect(MSpec).to receive(:write_tag).with(@tag)
+ action = TagAction.new :add, :pass, "tag", "comment", nil, "can"
+ action.after @state
+ end
+
+ it "writes a tag if the outcome is :all" do
+ expect(MSpec).to receive(:write_tag).with(@tag)
+ action = TagAction.new :add, :all, "tag", "comment", nil, "can"
+ action.after @state
+ end
+end
+
+RSpec.describe TagAction, "#after when action is :del" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return([])
+ context = ContextState.new "Catch#me"
+ @state = ExampleState.new context, "if you can"
+ @tag = SpecTag.new "tag(comment):Catch#me if you can"
+ allow(SpecTag).to receive(:new).and_return(@tag)
+ @exception = ExceptionState.new nil, nil, Exception.new("failed")
+ end
+
+ it "does not delete a tag if the description does not match" do
+ expect(MSpec).not_to receive(:delete_tag)
+ action = TagAction.new :del, :all, "tag", "comment", nil, "match"
+ action.after @state
+ end
+
+ it "does not delete a tag if outcome is :fail and the spec passed" do
+ expect(MSpec).not_to receive(:delete_tag)
+ action = TagAction.new :del, :fail, "tag", "comment", nil, "can"
+ action.after @state
+ end
+
+ it "deletes a tag if the outcome is :fail and the spec failed" do
+ expect(MSpec).to receive(:delete_tag).with(@tag)
+ action = TagAction.new :del, :fail, "tag", "comment", nil, "can"
+ action.exception @exception
+ action.after @state
+ end
+
+ it "does not delete a tag if outcome is :pass and the spec failed" do
+ expect(MSpec).not_to receive(:delete_tag)
+ action = TagAction.new :del, :pass, "tag", "comment", nil, "can"
+ action.exception @exception
+ action.after @state
+ end
+
+ it "deletes a tag if the outcome is :pass and the spec passed" do
+ expect(MSpec).to receive(:delete_tag).with(@tag)
+ action = TagAction.new :del, :pass, "tag", "comment", nil, "can"
+ action.after @state
+ end
+
+ it "deletes a tag if the outcome is :all" do
+ expect(MSpec).to receive(:delete_tag).with(@tag)
+ action = TagAction.new :del, :all, "tag", "comment", nil, "can"
+ action.after @state
+ end
+end
+
+RSpec.describe TagAction, "#finish" do
+ before :each do
+ $stdout = @out = IOStub.new
+ context = ContextState.new "Catch#me"
+ @state = ExampleState.new context, "if you can"
+ allow(MSpec).to receive(:write_tag).and_return(true)
+ allow(MSpec).to receive(:delete_tag).and_return(true)
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "reports no specs tagged if none where tagged" do
+ action = TagAction.new :add, :fail, "tag", "comment", nil, "can"
+ allow(action).to receive(:outcome?).and_return(false)
+ action.after @state
+ action.finish
+ expect(@out).to eq("\nTagAction: no specs were tagged with 'tag'\n")
+ end
+
+ it "reports no specs tagged if none where tagged" do
+ action = TagAction.new :del, :fail, "tag", "comment", nil, "can"
+ allow(action).to receive(:outcome?).and_return(false)
+ action.after @state
+ action.finish
+ expect(@out).to eq("\nTagAction: no tags 'tag' were deleted\n")
+ end
+
+ it "reports the spec descriptions that were tagged" do
+ action = TagAction.new :add, :fail, "tag", "comment", nil, "can"
+ allow(action).to receive(:outcome?).and_return(true)
+ action.after @state
+ action.finish
+ expect(@out).to eq(%[
+TagAction: specs tagged with 'tag':
+
+Catch#me if you can
+])
+ end
+
+ it "reports the spec descriptions for the tags that were deleted" do
+ action = TagAction.new :del, :fail, "tag", "comment", nil, "can"
+ allow(action).to receive(:outcome?).and_return(true)
+ action.after @state
+ action.finish
+ expect(@out).to eq(%[
+TagAction: tag 'tag' deleted for specs:
+
+Catch#me if you can
+])
+ end
+end
+
+RSpec.describe TagAction, "#register" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ allow(MSpec).to receive(:read_tags).and_return([])
+ @action = TagAction.new :add, :all, nil, nil, nil, nil
+ end
+
+ it "registers itself with MSpec for the :before event" do
+ expect(MSpec).to receive(:register).with(:before, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :after event" do
+ expect(MSpec).to receive(:register).with(:after, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :exception event" do
+ expect(MSpec).to receive(:register).with(:exception, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :finish event" do
+ expect(MSpec).to receive(:register).with(:finish, @action)
+ @action.register
+ end
+end
+
+RSpec.describe TagAction, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ allow(MSpec).to receive(:read_tags).and_return([])
+ @action = TagAction.new :add, :all, nil, nil, nil, nil
+ end
+
+ it "unregisters itself with MSpec for the :before event" do
+ expect(MSpec).to receive(:unregister).with(:before, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :after event" do
+ expect(MSpec).to receive(:unregister).with(:after, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :exception event" do
+ expect(MSpec).to receive(:unregister).with(:exception, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :finish event" do
+ expect(MSpec).to receive(:unregister).with(:finish, @action)
+ @action.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/taglist_spec.rb b/spec/mspec/spec/runner/actions/taglist_spec.rb
new file mode 100644
index 0000000000..b6a5400f7d
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/taglist_spec.rb
@@ -0,0 +1,152 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/taglist'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/runner/tag'
+
+RSpec.describe TagListAction, "#include?" do
+ it "returns true" do
+ expect(TagListAction.new.include?(:anything)).to be_truthy
+ end
+end
+
+RSpec.describe TagListAction, "#===" do
+ before :each do
+ tag = SpecTag.new "fails:description"
+ allow(MSpec).to receive(:read_tags).and_return([tag])
+ @filter = double("MatchFilter").as_null_object
+ allow(MatchFilter).to receive(:new).and_return(@filter)
+ @action = TagListAction.new
+ @action.load
+ end
+
+ it "returns true if filter === string returns true" do
+ expect(@filter).to receive(:===).with("str").and_return(true)
+ expect(@action.===("str")).to be_truthy
+ end
+
+ it "returns false if filter === string returns false" do
+ expect(@filter).to receive(:===).with("str").and_return(false)
+ expect(@action.===("str")).to be_falsey
+ end
+end
+
+RSpec.describe TagListAction, "#start" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "prints a banner for specific tags" do
+ action = TagListAction.new ["fails", "unstable"]
+ action.start
+ expect($stdout).to eq("\nListing specs tagged with 'fails', 'unstable'\n\n")
+ end
+
+ it "prints a banner for all tags" do
+ action = TagListAction.new
+ action.start
+ expect($stdout).to eq("\nListing all tagged specs\n\n")
+ end
+end
+
+RSpec.describe TagListAction, "#load" do
+ before :each do
+ @t1 = SpecTag.new "fails:I fail"
+ @t2 = SpecTag.new "unstable:I'm unstable"
+ end
+
+ it "creates a MatchFilter for matching tags" do
+ expect(MSpec).to receive(:read_tags).with(["fails"]).and_return([@t1])
+ expect(MatchFilter).to receive(:new).with(nil, "I fail")
+ TagListAction.new(["fails"]).load
+ end
+
+ it "creates a MatchFilter for all tags" do
+ expect(MSpec).to receive(:read_tags).and_return([@t1, @t2])
+ expect(MatchFilter).to receive(:new).with(nil, "I fail", "I'm unstable")
+ TagListAction.new.load
+ end
+
+ it "does not create a MatchFilter if there are no matching tags" do
+ allow(MSpec).to receive(:read_tags).and_return([])
+ expect(MatchFilter).not_to receive(:new)
+ TagListAction.new(["fails"]).load
+ end
+end
+
+RSpec.describe TagListAction, "#after" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+
+ @state = double("ExampleState")
+ allow(@state).to receive(:description).and_return("str")
+
+ @action = TagListAction.new
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "prints nothing if the filter does not match" do
+ expect(@action).to receive(:===).with("str").and_return(false)
+ @action.after(@state)
+ expect($stdout).to eq("")
+ end
+
+ it "prints the example description if the filter matches" do
+ expect(@action).to receive(:===).with("str").and_return(true)
+ @action.after(@state)
+ expect($stdout).to eq("str\n")
+ end
+end
+
+RSpec.describe TagListAction, "#register" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ @action = TagListAction.new
+ end
+
+ it "registers itself with MSpec for the :start event" do
+ expect(MSpec).to receive(:register).with(:start, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :load event" do
+ expect(MSpec).to receive(:register).with(:load, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :after event" do
+ expect(MSpec).to receive(:register).with(:after, @action)
+ @action.register
+ end
+end
+
+RSpec.describe TagListAction, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ @action = TagListAction.new
+ end
+
+ it "unregisters itself with MSpec for the :start event" do
+ expect(MSpec).to receive(:unregister).with(:start, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :load event" do
+ expect(MSpec).to receive(:unregister).with(:load, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :after event" do
+ expect(MSpec).to receive(:unregister).with(:after, @action)
+ @action.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/tagpurge_spec.rb b/spec/mspec/spec/runner/actions/tagpurge_spec.rb
new file mode 100644
index 0000000000..37df0afd5a
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/tagpurge_spec.rb
@@ -0,0 +1,154 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/tagpurge'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/runner/tag'
+
+RSpec.describe TagPurgeAction, "#start" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "prints a banner" do
+ action = TagPurgeAction.new
+ action.start
+ expect($stdout).to eq("\nRemoving tags not matching any specs\n\n")
+ end
+end
+
+RSpec.describe TagPurgeAction, "#load" do
+ before :each do
+ @t1 = SpecTag.new "fails:I fail"
+ @t2 = SpecTag.new "unstable:I'm unstable"
+ end
+
+ it "creates a MatchFilter for all tags" do
+ expect(MSpec).to receive(:read_tags).and_return([@t1, @t2])
+ expect(MatchFilter).to receive(:new).with(nil, "I fail", "I'm unstable")
+ TagPurgeAction.new.load
+ end
+end
+
+RSpec.describe TagPurgeAction, "#after" do
+ before :each do
+ @state = double("ExampleState")
+ allow(@state).to receive(:description).and_return("str")
+
+ @action = TagPurgeAction.new
+ end
+
+ it "does not save the description if the filter does not match" do
+ expect(@action).to receive(:===).with("str").and_return(false)
+ @action.after @state
+ expect(@action.matching).to eq([])
+ end
+
+ it "saves the description if the filter matches" do
+ expect(@action).to receive(:===).with("str").and_return(true)
+ @action.after @state
+ expect(@action.matching).to eq(["str"])
+ end
+end
+
+RSpec.describe TagPurgeAction, "#unload" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+
+ @t1 = SpecTag.new "fails:I fail"
+ @t2 = SpecTag.new "unstable:I'm unstable"
+ @t3 = SpecTag.new "fails:I'm unstable"
+
+ allow(MSpec).to receive(:read_tags).and_return([@t1, @t2, @t3])
+ allow(MSpec).to receive(:write_tags)
+
+ @state = double("ExampleState")
+ allow(@state).to receive(:description).and_return("I'm unstable")
+
+ @action = TagPurgeAction.new
+ @action.load
+ @action.after @state
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "does not rewrite any tags if there were no tags for the specs" do
+ expect(MSpec).to receive(:read_tags).and_return([])
+ expect(MSpec).to receive(:delete_tags)
+ expect(MSpec).not_to receive(:write_tags)
+
+ @action.load
+ @action.after @state
+ @action.unload
+
+ expect($stdout).to eq("")
+ end
+
+ it "rewrites tags that were matched" do
+ expect(MSpec).to receive(:write_tags).with([@t2, @t3])
+ @action.unload
+ end
+
+ it "prints tags that were not matched" do
+ @action.unload
+ expect($stdout).to eq("I fail\n")
+ end
+end
+
+RSpec.describe TagPurgeAction, "#unload" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+
+ allow(MSpec).to receive(:read_tags).and_return([])
+
+ @state = double("ExampleState")
+ allow(@state).to receive(:description).and_return("I'm unstable")
+
+ @action = TagPurgeAction.new
+ @action.load
+ @action.after @state
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "deletes the tag file if no tags were found" do
+ expect(MSpec).not_to receive(:write_tags)
+ expect(MSpec).to receive(:delete_tags)
+ @action.unload
+ expect($stdout).to eq("")
+ end
+end
+
+RSpec.describe TagPurgeAction, "#register" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ @action = TagPurgeAction.new
+ end
+
+ it "registers itself with MSpec for the :unload event" do
+ expect(MSpec).to receive(:register).with(:unload, @action)
+ @action.register
+ end
+end
+
+RSpec.describe TagPurgeAction, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ @action = TagPurgeAction.new
+ end
+
+ it "unregisters itself with MSpec for the :unload event" do
+ expect(MSpec).to receive(:unregister).with(:unload, @action)
+ @action.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/tally_spec.rb b/spec/mspec/spec/runner/actions/tally_spec.rb
new file mode 100644
index 0000000000..d80ab1164a
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/tally_spec.rb
@@ -0,0 +1,355 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/runner/actions/tally'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+
+RSpec.describe Tally, "#files!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #files" do
+ @tally.files! 3
+ expect(@tally.files).to eq(3)
+ @tally.files!
+ expect(@tally.files).to eq(4)
+ end
+end
+
+RSpec.describe Tally, "#examples!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #examples" do
+ @tally.examples! 2
+ expect(@tally.examples).to eq(2)
+ @tally.examples! 2
+ expect(@tally.examples).to eq(4)
+ end
+end
+
+RSpec.describe Tally, "#expectations!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #expectations" do
+ @tally.expectations!
+ expect(@tally.expectations).to eq(1)
+ @tally.expectations! 3
+ expect(@tally.expectations).to eq(4)
+ end
+end
+
+RSpec.describe Tally, "#failures!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #failures" do
+ @tally.failures! 1
+ expect(@tally.failures).to eq(1)
+ @tally.failures!
+ expect(@tally.failures).to eq(2)
+ end
+end
+
+RSpec.describe Tally, "#errors!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #errors" do
+ @tally.errors!
+ expect(@tally.errors).to eq(1)
+ @tally.errors! 2
+ expect(@tally.errors).to eq(3)
+ end
+end
+
+RSpec.describe Tally, "#guards!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #guards" do
+ @tally.guards!
+ expect(@tally.guards).to eq(1)
+ @tally.guards! 2
+ expect(@tally.guards).to eq(3)
+ end
+end
+
+RSpec.describe Tally, "#file" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #files" do
+ expect(@tally.file).to eq("0 files")
+ @tally.files!
+ expect(@tally.file).to eq("1 file")
+ @tally.files!
+ expect(@tally.file).to eq("2 files")
+ end
+end
+
+RSpec.describe Tally, "#example" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #examples" do
+ expect(@tally.example).to eq("0 examples")
+ @tally.examples!
+ expect(@tally.example).to eq("1 example")
+ @tally.examples!
+ expect(@tally.example).to eq("2 examples")
+ end
+end
+
+RSpec.describe Tally, "#expectation" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #expectations" do
+ expect(@tally.expectation).to eq("0 expectations")
+ @tally.expectations!
+ expect(@tally.expectation).to eq("1 expectation")
+ @tally.expectations!
+ expect(@tally.expectation).to eq("2 expectations")
+ end
+end
+
+RSpec.describe Tally, "#failure" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #failures" do
+ expect(@tally.failure).to eq("0 failures")
+ @tally.failures!
+ expect(@tally.failure).to eq("1 failure")
+ @tally.failures!
+ expect(@tally.failure).to eq("2 failures")
+ end
+end
+
+RSpec.describe Tally, "#error" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #errors" do
+ expect(@tally.error).to eq("0 errors")
+ @tally.errors!
+ expect(@tally.error).to eq("1 error")
+ @tally.errors!
+ expect(@tally.error).to eq("2 errors")
+ end
+end
+
+RSpec.describe Tally, "#guard" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #guards" do
+ expect(@tally.guard).to eq("0 guards")
+ @tally.guards!
+ expect(@tally.guard).to eq("1 guard")
+ @tally.guards!
+ expect(@tally.guard).to eq("2 guards")
+ end
+end
+
+RSpec.describe Tally, "#format" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ after :each do
+ MSpec.clear_modes
+ end
+
+ it "returns a formatted string of counts" do
+ @tally.files!
+ @tally.examples! 2
+ @tally.expectations! 4
+ @tally.errors!
+ @tally.tagged!
+ expect(@tally.format).to eq("1 file, 2 examples, 4 expectations, 0 failures, 1 error, 1 tagged")
+ end
+
+ it "includes guards if MSpec is in verify mode" do
+ MSpec.register_mode :verify
+ @tally.files!
+ @tally.examples! 2
+ @tally.expectations! 4
+ @tally.errors!
+ @tally.tagged!
+ @tally.guards!
+ expect(@tally.format).to eq(
+ "1 file, 2 examples, 4 expectations, 0 failures, 1 error, 1 tagged, 1 guard"
+ )
+ end
+
+ it "includes guards if MSpec is in report mode" do
+ MSpec.register_mode :report
+ @tally.files!
+ @tally.examples! 2
+ @tally.expectations! 4
+ @tally.errors!
+ @tally.tagged!
+ @tally.guards! 2
+ expect(@tally.format).to eq(
+ "1 file, 2 examples, 4 expectations, 0 failures, 1 error, 1 tagged, 2 guards"
+ )
+ end
+
+ it "includes guards if MSpec is in report_on mode" do
+ MSpec.register_mode :report_on
+ @tally.files!
+ @tally.examples! 2
+ @tally.expectations! 4
+ @tally.errors!
+ @tally.guards! 2
+ expect(@tally.format).to eq(
+ "1 file, 2 examples, 4 expectations, 0 failures, 1 error, 0 tagged, 2 guards"
+ )
+ end
+end
+
+RSpec.describe TallyAction, "#counter" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "returns the Tally object" do
+ expect(@tally.counter).to be_kind_of(Tally)
+ end
+end
+
+RSpec.describe TallyAction, "#load" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments the count returned by Tally#files" do
+ @tally.load
+ expect(@tally.counter.files).to eq(1)
+ end
+end
+
+RSpec.describe TallyAction, "#expectation" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments the count returned by Tally#expectations" do
+ @tally.expectation @state
+ expect(@tally.counter.expectations).to eq(1)
+ end
+end
+
+RSpec.describe TallyAction, "#example" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments counts returned by Tally#examples" do
+ @tally.example @state, nil
+ expect(@tally.counter.examples).to eq(1)
+ expect(@tally.counter.expectations).to eq(0)
+ expect(@tally.counter.failures).to eq(0)
+ expect(@tally.counter.errors).to eq(0)
+ end
+end
+
+RSpec.describe TallyAction, "#exception" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments counts returned by Tally#failures" do
+ exc = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("Failed!")
+ @tally.exception exc
+ expect(@tally.counter.examples).to eq(0)
+ expect(@tally.counter.expectations).to eq(0)
+ expect(@tally.counter.failures).to eq(1)
+ expect(@tally.counter.errors).to eq(0)
+ end
+end
+
+RSpec.describe TallyAction, "#exception" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments counts returned by Tally#errors" do
+ exc = ExceptionState.new nil, nil, Exception.new("Error!")
+ @tally.exception exc
+ expect(@tally.counter.examples).to eq(0)
+ expect(@tally.counter.expectations).to eq(0)
+ expect(@tally.counter.failures).to eq(0)
+ expect(@tally.counter.errors).to eq(1)
+ end
+end
+
+RSpec.describe TallyAction, "#format" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "returns a readable string of counts" do
+ @tally.load
+ @tally.example @state, nil
+ @tally.expectation @state
+ @tally.expectation @state
+ exc = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("Failed!")
+ @tally.exception exc
+ expect(@tally.format).to eq("1 file, 1 example, 2 expectations, 1 failure, 0 errors, 0 tagged")
+ end
+end
+
+RSpec.describe TallyAction, "#register" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "registers itself with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:register).with(:load, @tally)
+ expect(MSpec).to receive(:register).with(:exception, @tally)
+ expect(MSpec).to receive(:register).with(:example, @tally)
+ expect(MSpec).to receive(:register).with(:tagged, @tally)
+ expect(MSpec).to receive(:register).with(:expectation, @tally)
+ @tally.register
+ end
+end
+
+RSpec.describe TallyAction, "#unregister" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "unregisters itself with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:unregister).with(:load, @tally)
+ expect(MSpec).to receive(:unregister).with(:exception, @tally)
+ expect(MSpec).to receive(:unregister).with(:example, @tally)
+ expect(MSpec).to receive(:unregister).with(:tagged, @tally)
+ expect(MSpec).to receive(:unregister).with(:expectation, @tally)
+ @tally.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/timer_spec.rb b/spec/mspec/spec/runner/actions/timer_spec.rb
new file mode 100644
index 0000000000..28a317177b
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/timer_spec.rb
@@ -0,0 +1,44 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/mspec'
+require 'time'
+
+RSpec.describe TimerAction do
+ before :each do
+ @timer = TimerAction.new
+ @start_time = Time.utc(2009, 3, 30, 14, 5, 19)
+ @stop_time = Time.utc(2009, 3, 30, 14, 5, 52)
+ end
+
+ it "responds to #start by recording the current time" do
+ expect(Time).to receive(:now)
+ @timer.start
+ end
+
+ it "responds to #finish by recording the current time" do
+ expect(Time).to receive(:now)
+ @timer.finish
+ end
+
+ it "responds to #elapsed by returning the difference between stop and start" do
+ allow(Time).to receive(:now).and_return(@start_time)
+ @timer.start
+ allow(Time).to receive(:now).and_return(@stop_time)
+ @timer.finish
+ expect(@timer.elapsed).to eq(33)
+ end
+
+ it "responds to #format by returning a readable string of elapsed time" do
+ allow(Time).to receive(:now).and_return(@start_time)
+ @timer.start
+ allow(Time).to receive(:now).and_return(@stop_time)
+ @timer.finish
+ expect(@timer.format).to eq("Finished in 33.000000 seconds")
+ end
+
+ it "responds to #register by registering itself with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:register).with(:start, @timer)
+ expect(MSpec).to receive(:register).with(:finish, @timer)
+ @timer.register
+ end
+end
diff --git a/spec/mspec/spec/runner/context_spec.rb b/spec/mspec/spec/runner/context_spec.rb
new file mode 100644
index 0000000000..9ebc708c0c
--- /dev/null
+++ b/spec/mspec/spec/runner/context_spec.rb
@@ -0,0 +1,1028 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers/base'
+require 'mspec/runner/mspec'
+require 'mspec/mocks/mock'
+require 'mspec/runner/context'
+require 'mspec/runner/example'
+
+RSpec.describe ContextState, "#describe" do
+ before :each do
+ @state = ContextState.new "C#m"
+ @proc = proc { ScratchPad.record :a }
+ ScratchPad.clear
+ end
+
+ it "evaluates the passed block" do
+ @state.describe(&@proc)
+ expect(ScratchPad.recorded).to eq(:a)
+ end
+
+ it "evaluates the passed block via #protect" do
+ expect(@state).to receive(:protect).with("C#m", @proc, false)
+ @state.describe(&@proc)
+ end
+
+ it "registers #parent as the current MSpec ContextState" do
+ parent = ContextState.new ""
+ @state.parent = parent
+ expect(MSpec).to receive(:register_current).with(parent)
+ @state.describe { }
+ end
+
+ it "registers self with MSpec when #shared? is true" do
+ state = ContextState.new "something shared", :shared => true
+ expect(MSpec).to receive(:register_shared).with(state)
+ state.describe { }
+ end
+end
+
+RSpec.describe ContextState, "#shared?" do
+ it "returns false when the ContextState is not shared" do
+ expect(ContextState.new("").shared?).to be_falsey
+ end
+
+ it "returns true when the ContextState is shared" do
+ expect(ContextState.new("", {:shared => true}).shared?).to be_truthy
+ end
+end
+
+RSpec.describe ContextState, "#to_s" do
+ it "returns a description string for self when passed a Module" do
+ expect(ContextState.new(Object).to_s).to eq("Object")
+ end
+
+ it "returns a description string for self when passed a String" do
+ expect(ContextState.new("SomeClass").to_s).to eq("SomeClass")
+ end
+end
+
+RSpec.describe ContextState, "#description" do
+ before :each do
+ @state = ContextState.new "when empty"
+ @parent = ContextState.new "Toplevel"
+ end
+
+ it "returns a composite description string from self and all parents" do
+ expect(@parent.description).to eq("Toplevel")
+ expect(@state.description).to eq("when empty")
+ @state.parent = @parent
+ expect(@state.description).to eq("Toplevel when empty")
+ end
+end
+
+RSpec.describe ContextState, "#it" do
+ before :each do
+ @state = ContextState.new ""
+ @proc = lambda {|*| }
+
+ @ex = ExampleState.new("", "", &@proc)
+ end
+
+ it "creates an ExampleState instance for the block" do
+ expect(ExampleState).to receive(:new).with(@state, "it", @proc).and_return(@ex)
+ @state.describe(&@proc)
+ @state.it("it", &@proc)
+ end
+
+ it "calls registered :add actions" do
+ expect(ExampleState).to receive(:new).with(@state, "it", @proc).and_return(@ex)
+
+ add_action = double("add")
+ expect(add_action).to receive(:add).with(@ex) { ScratchPad.record :add }
+ MSpec.register :add, add_action
+
+ @state.it("it", &@proc)
+ expect(ScratchPad.recorded).to eq(:add)
+ MSpec.unregister :add, add_action
+ end
+end
+
+RSpec.describe ContextState, "#examples" do
+ before :each do
+ @state = ContextState.new ""
+ end
+
+ it "returns a list of all examples in this ContextState" do
+ @state.it("first") { }
+ @state.it("second") { }
+ expect(@state.examples.size).to eq(2)
+ end
+end
+
+RSpec.describe ContextState, "#before" do
+ before :each do
+ @state = ContextState.new ""
+ @proc = lambda {|*| }
+ end
+
+ it "records the block for :each" do
+ @state.before(:each, &@proc)
+ expect(@state.before(:each)).to eq([@proc])
+ end
+
+ it "records the block for :all" do
+ @state.before(:all, &@proc)
+ expect(@state.before(:all)).to eq([@proc])
+ end
+end
+
+RSpec.describe ContextState, "#after" do
+ before :each do
+ @state = ContextState.new ""
+ @proc = lambda {|*| }
+ end
+
+ it "records the block for :each" do
+ @state.after(:each, &@proc)
+ expect(@state.after(:each)).to eq([@proc])
+ end
+
+ it "records the block for :all" do
+ @state.after(:all, &@proc)
+ expect(@state.after(:all)).to eq([@proc])
+ end
+end
+
+RSpec.describe ContextState, "#pre" do
+ before :each do
+ @a = lambda {|*| }
+ @b = lambda {|*| }
+ @c = lambda {|*| }
+
+ parent = ContextState.new ""
+ parent.before(:each, &@c)
+ parent.before(:all, &@c)
+
+ @state = ContextState.new ""
+ @state.parent = parent
+ end
+
+ it "returns before(:each) actions in the order they were defined" do
+ @state.before(:each, &@a)
+ @state.before(:each, &@b)
+ expect(@state.pre(:each)).to eq([@c, @a, @b])
+ end
+
+ it "returns before(:all) actions in the order they were defined" do
+ @state.before(:all, &@a)
+ @state.before(:all, &@b)
+ expect(@state.pre(:all)).to eq([@c, @a, @b])
+ end
+end
+
+RSpec.describe ContextState, "#post" do
+ before :each do
+ @a = lambda {|*| }
+ @b = lambda {|*| }
+ @c = lambda {|*| }
+
+ parent = ContextState.new ""
+ parent.after(:each, &@c)
+ parent.after(:all, &@c)
+
+ @state = ContextState.new ""
+ @state.parent = parent
+ end
+
+ it "returns after(:each) actions in the reverse order they were defined" do
+ @state.after(:each, &@a)
+ @state.after(:each, &@b)
+ expect(@state.post(:each)).to eq([@b, @a, @c])
+ end
+
+ it "returns after(:all) actions in the reverse order they were defined" do
+ @state.after(:all, &@a)
+ @state.after(:all, &@b)
+ expect(@state.post(:all)).to eq([@b, @a, @c])
+ end
+end
+
+RSpec.describe ContextState, "#protect" do
+ before :each do
+ ScratchPad.record []
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ @c = lambda {|*| raise Exception, "Fail!" }
+ end
+
+ it "returns true and does execute any blocks if check and MSpec.mode?(:pretend) are true" do
+ expect(MSpec).to receive(:mode?).with(:pretend).and_return(true)
+ expect(ContextState.new("").protect("message", [@a, @b])).to be_truthy
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "executes the blocks if MSpec.mode?(:pretend) is false" do
+ expect(MSpec).to receive(:mode?).with(:pretend).and_return(false)
+ ContextState.new("").protect("message", [@a, @b])
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "executes the blocks if check is false" do
+ ContextState.new("").protect("message", [@a, @b], false)
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "returns true if none of the blocks raise an exception" do
+ expect(ContextState.new("").protect("message", [@a, @b])).to be_truthy
+ end
+
+ it "returns false if any of the blocks raise an exception" do
+ expect(ContextState.new("").protect("message", [@a, @c, @b])).to be_falsey
+ end
+end
+
+RSpec.describe ContextState, "#parent=" do
+ before :each do
+ @state = ContextState.new ""
+ @parent = double("describe")
+ allow(@parent).to receive(:parent).and_return(nil)
+ allow(@parent).to receive(:child)
+ end
+
+ it "does not set self as a child of parent if shared" do
+ expect(@parent).not_to receive(:child)
+ state = ContextState.new "", :shared => true
+ state.parent = @parent
+ end
+
+ it "does not set parents if shared" do
+ state = ContextState.new "", :shared => true
+ state.parent = @parent
+ expect(state.parents).to eq([state])
+ end
+
+ it "sets self as a child of parent" do
+ expect(@parent).to receive(:child).with(@state)
+ @state.parent = @parent
+ end
+
+ it "creates the list of parents" do
+ @state.parent = @parent
+ expect(@state.parents).to eq([@parent, @state])
+ end
+end
+
+RSpec.describe ContextState, "#parent" do
+ before :each do
+ @state = ContextState.new ""
+ @parent = double("describe")
+ allow(@parent).to receive(:parent).and_return(nil)
+ allow(@parent).to receive(:child)
+ end
+
+ it "returns nil if parent has not been set" do
+ expect(@state.parent).to be_nil
+ end
+
+ it "returns the parent" do
+ @state.parent = @parent
+ expect(@state.parent).to eq(@parent)
+ end
+end
+
+RSpec.describe ContextState, "#parents" do
+ before :each do
+ @first = ContextState.new ""
+ @second = ContextState.new ""
+ @parent = double("describe")
+ allow(@parent).to receive(:parent).and_return(nil)
+ allow(@parent).to receive(:child)
+ end
+
+ it "returns a list of all enclosing ContextState instances" do
+ @first.parent = @parent
+ @second.parent = @first
+ expect(@second.parents).to eq([@parent, @first, @second])
+ end
+end
+
+RSpec.describe ContextState, "#child" do
+ before :each do
+ @first = ContextState.new ""
+ @second = ContextState.new ""
+ @parent = double("describe")
+ allow(@parent).to receive(:parent).and_return(nil)
+ allow(@parent).to receive(:child)
+ end
+
+ it "adds the ContextState to the list of contained ContextStates" do
+ @first.child @second
+ expect(@first.children).to eq([@second])
+ end
+end
+
+RSpec.describe ContextState, "#children" do
+ before :each do
+ @parent = ContextState.new ""
+ @first = ContextState.new ""
+ @second = ContextState.new ""
+ end
+
+ it "returns the list of directly contained ContextStates" do
+ @first.parent = @parent
+ @second.parent = @first
+ expect(@parent.children).to eq([@first])
+ expect(@first.children).to eq([@second])
+ end
+end
+
+RSpec.describe ContextState, "#state" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ end
+
+ it "returns nil if no spec is being executed" do
+ expect(@state.state).to eq(nil)
+ end
+
+ it "returns a ExampleState instance if an example is being executed" do
+ ScratchPad.record @state
+ @state.describe { }
+ @state.it("") { ScratchPad.record ScratchPad.recorded.state }
+ @state.process
+ expect(@state.state).to eq(nil)
+ expect(ScratchPad.recorded).to be_kind_of(ExampleState)
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+ allow(MSpec).to receive(:register_current)
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ ScratchPad.record []
+ end
+
+ it "calls each before(:all) block" do
+ @state.before(:all, &@a)
+ @state.before(:all, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "calls each after(:all) block" do
+ @state.after(:all, &@a)
+ @state.after(:all, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:b, :a])
+ end
+
+ it "calls each it block" do
+ @state.it("one", &@a)
+ @state.it("two", &@b)
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "does not call the #it block if #filtered? returns true" do
+ @state.it("one", &@a)
+ @state.it("two", &@b)
+ allow(@state.examples.first).to receive(:filtered?).and_return(true)
+ @state.process
+ expect(ScratchPad.recorded).to eq([:b])
+ end
+
+ it "calls each before(:each) block" do
+ @state.before(:each, &@a)
+ @state.before(:each, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "calls each after(:each) block" do
+ @state.after(:each, &@a)
+ @state.after(:each, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:b, :a])
+ end
+
+ it "calls Mock.cleanup for each it block" do
+ @state.it("") { }
+ @state.it("") { }
+ expect(Mock).to receive(:cleanup).twice
+ @state.process
+ end
+
+ it "calls Mock.verify_count for each it block" do
+ @state.it("") { }
+ @state.it("") { }
+ expect(Mock).to receive(:verify_count).twice
+ @state.process
+ end
+
+ it "calls the describe block" do
+ ScratchPad.record []
+ @state.describe { ScratchPad << :a }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a])
+ end
+
+ it "creates a new ExampleState instance for each example" do
+ ScratchPad.record @state
+ @state.describe { }
+ @state.it("it") { ScratchPad.record ScratchPad.recorded.state }
+ @state.process
+ expect(ScratchPad.recorded).to be_kind_of(ExampleState)
+ end
+
+ it "clears the expectations flag before evaluating the #it block" do
+ MSpec.clear_expectations
+ expect(MSpec).to receive(:clear_expectations)
+ @state.it("it") { ScratchPad.record MSpec.expectation? }
+ @state.process
+ expect(ScratchPad.recorded).to be_falsey
+ end
+
+ it "shuffles the spec list if MSpec.randomize? is true" do
+ MSpec.randomize = true
+ begin
+ expect(MSpec).to receive(:shuffle)
+ @state.it("") { }
+ @state.process
+ ensure
+ MSpec.randomize = false
+ end
+ end
+
+ it "sets the current MSpec ContextState" do
+ expect(MSpec).to receive(:register_current).with(@state)
+ @state.process
+ end
+
+ it "resets the current MSpec ContextState to nil when there are examples" do
+ expect(MSpec).to receive(:register_current).with(nil)
+ @state.it("") { }
+ @state.process
+ end
+
+ it "resets the current MSpec ContextState to nil when there are no examples" do
+ expect(MSpec).to receive(:register_current).with(nil)
+ @state.process
+ end
+
+ it "call #process on children when there are examples" do
+ child = ContextState.new ""
+ expect(child).to receive(:process)
+ @state.child child
+ @state.it("") { }
+ @state.process
+ end
+
+ it "call #process on children when there are no examples" do
+ child = ContextState.new ""
+ expect(child).to receive(:process)
+ @state.child child
+ @state.process
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :exception, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ action = double("action")
+ def action.exception(exc)
+ ScratchPad.record :exception if exc.exception.is_a? SpecExpectationNotFoundError
+ end
+ MSpec.register :exception, action
+
+ MSpec.clear_expectations
+ ScratchPad.clear
+ end
+
+ after :each do
+ MSpec.store :exception, nil
+ end
+
+ it "raises an SpecExpectationNotFoundError if an #it block does not contain an expectation" do
+ @state.it("it") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq(:exception)
+ end
+
+ it "does not raise an SpecExpectationNotFoundError if an #it block does contain an expectation" do
+ @state.it("it") { MSpec.expectation }
+ @state.process
+ expect(ScratchPad.recorded).to be_nil
+ end
+
+ it "does not raise an SpecExpectationNotFoundError if the #it block causes a failure" do
+ @state.it("it") { raise Exception, "Failed!" }
+ @state.process
+ expect(ScratchPad.recorded).to be_nil
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :example, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ example = double("example")
+ def example.example(state, spec)
+ ScratchPad << state << spec
+ end
+ MSpec.register :example, example
+
+ ScratchPad.record []
+ end
+
+ after :each do
+ MSpec.store :example, nil
+ end
+
+ it "calls registered :example actions with the current ExampleState and block" do
+ @state.it("") { MSpec.expectation }
+ @state.process
+
+ expect(ScratchPad.recorded.first).to be_kind_of(ExampleState)
+ expect(ScratchPad.recorded.last).to be_kind_of(Proc)
+ end
+
+ it "does not call registered example actions if the example has no block" do
+ @state.it("empty example")
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+ @state.it("") { MSpec.expectation }
+ end
+
+ after :each do
+ MSpec.store :before, nil
+ MSpec.store :after, nil
+ end
+
+ it "calls registered :before actions with the current ExampleState instance" do
+ before = double("before")
+ expect(before).to receive(:before) {
+ ScratchPad.record :before
+ @spec_state = @state.state
+ }
+ MSpec.register :before, before
+ @state.process
+ expect(ScratchPad.recorded).to eq(:before)
+ expect(@spec_state).to be_kind_of(ExampleState)
+ end
+
+ it "calls registered :after actions with the current ExampleState instance" do
+ after = double("after")
+ expect(after).to receive(:after) {
+ ScratchPad.record :after
+ @spec_state = @state.state
+ }
+ MSpec.register :after, after
+ @state.process
+ expect(ScratchPad.recorded).to eq(:after)
+ expect(@spec_state).to be_kind_of(ExampleState)
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :enter, []
+ MSpec.store :leave, []
+
+ @state = ContextState.new "C#m"
+ @state.describe { }
+ @state.it("") { MSpec.expectation }
+ end
+
+ after :each do
+ MSpec.store :enter, nil
+ MSpec.store :leave, nil
+ end
+
+ it "calls registered :enter actions with the current #describe string" do
+ enter = double("enter")
+ expect(enter).to receive(:enter).with("C#m") { ScratchPad.record :enter }
+ MSpec.register :enter, enter
+ @state.process
+ expect(ScratchPad.recorded).to eq(:enter)
+ end
+
+ it "calls registered :leave actions" do
+ leave = double("leave")
+ expect(leave).to receive(:leave) { ScratchPad.record :leave }
+ MSpec.register :leave, leave
+ @state.process
+ expect(ScratchPad.recorded).to eq(:leave)
+ end
+end
+
+RSpec.describe ContextState, "#process when an exception is raised in before(:all)" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ ScratchPad.record []
+
+ @state.before(:all) { raise Exception, "Fail!" }
+ end
+
+ after :each do
+ MSpec.store :before, nil
+ MSpec.store :after, nil
+ end
+
+ it "does not call before(:each)" do
+ @state.before(:each, &@a)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call the it block" do
+ @state.it("one", &@a)
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call after(:each)" do
+ @state.after(:each, &@a)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call after(:each)" do
+ @state.after(:all, &@a)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call Mock.verify_count" do
+ @state.it("") { }
+ expect(Mock).not_to receive(:verify_count)
+ @state.process
+ end
+
+ it "calls Mock.cleanup" do
+ @state.it("") { }
+ expect(Mock).to receive(:cleanup)
+ @state.process
+ end
+end
+
+RSpec.describe ContextState, "#process when an exception is raised in before(:each)" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ ScratchPad.record []
+
+ @state.before(:each) { raise Exception, "Fail!" }
+ end
+
+ after :each do
+ MSpec.store :before, nil
+ MSpec.store :after, nil
+ end
+
+ it "does not call the it block" do
+ @state.it("one", &@a)
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "calls after(:each)" do
+ @state.after(:each, &@a)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a])
+ end
+
+ it "calls Mock.verify_count" do
+ @state.it("") { }
+ expect(Mock).to receive(:verify_count)
+ @state.process
+ end
+end
+
+RSpec.describe ContextState, "#process in pretend mode" do
+ before :all do
+ MSpec.register_mode :pretend
+ end
+
+ after :all do
+ MSpec.clear_modes
+ end
+
+ before :each do
+ ScratchPad.clear
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+ @state.it("") { }
+ end
+
+ after :each do
+ MSpec.store :before, nil
+ MSpec.store :after, nil
+ end
+
+ it "calls registered :before actions with the current ExampleState instance" do
+ before = double("before")
+ expect(before).to receive(:before) {
+ ScratchPad.record :before
+ @spec_state = @state.state
+ }
+ MSpec.register :before, before
+ @state.process
+ expect(ScratchPad.recorded).to eq(:before)
+ expect(@spec_state).to be_kind_of(ExampleState)
+ end
+
+ it "calls registered :after actions with the current ExampleState instance" do
+ after = double("after")
+ expect(after).to receive(:after) {
+ ScratchPad.record :after
+ @spec_state = @state.state
+ }
+ MSpec.register :after, after
+ @state.process
+ expect(ScratchPad.recorded).to eq(:after)
+ expect(@spec_state).to be_kind_of(ExampleState)
+ end
+end
+
+RSpec.describe ContextState, "#process in pretend mode" do
+ before :all do
+ MSpec.register_mode :pretend
+ end
+
+ after :all do
+ MSpec.clear_modes
+ end
+
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ ScratchPad.record []
+ end
+
+ it "calls the describe block" do
+ ScratchPad.record []
+ @state.describe { ScratchPad << :a }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a])
+ end
+
+ it "does not call any before(:all) block" do
+ @state.before(:all, &@a)
+ @state.before(:all, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call any after(:all) block" do
+ @state.after(:all, &@a)
+ @state.after(:all, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call any it block" do
+ @state.it("one", &@a)
+ @state.it("two", &@b)
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call any before(:each) block" do
+ @state.before(:each, &@a)
+ @state.before(:each, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call any after(:each) block" do
+ @state.after(:each, &@a)
+ @state.after(:each, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call Mock.cleanup" do
+ @state.it("") { }
+ @state.it("") { }
+ expect(Mock).not_to receive(:cleanup)
+ @state.process
+ end
+end
+
+RSpec.describe ContextState, "#process in pretend mode" do
+ before :all do
+ MSpec.register_mode :pretend
+ end
+
+ after :all do
+ MSpec.clear_modes
+ end
+
+ before :each do
+ MSpec.store :enter, []
+ MSpec.store :leave, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+ @state.it("") { }
+ end
+
+ after :each do
+ MSpec.store :enter, nil
+ MSpec.store :leave, nil
+ end
+
+ it "calls registered :enter actions with the current #describe string" do
+ enter = double("enter")
+ expect(enter).to receive(:enter) { ScratchPad.record :enter }
+ MSpec.register :enter, enter
+ @state.process
+ expect(ScratchPad.recorded).to eq(:enter)
+ end
+
+ it "calls registered :leave actions" do
+ leave = double("leave")
+ expect(leave).to receive(:leave) { ScratchPad.record :leave }
+ MSpec.register :leave, leave
+ @state.process
+ expect(ScratchPad.recorded).to eq(:leave)
+ end
+end
+
+RSpec.describe ContextState, "#it_should_behave_like" do
+ before :each do
+ @shared_desc = :shared_context
+ @shared = ContextState.new(@shared_desc, :shared => true)
+ allow(MSpec).to receive(:retrieve_shared).and_return(@shared)
+
+ @state = ContextState.new "Top level"
+ @a = lambda {|*| }
+ @b = lambda {|*| }
+ end
+
+ it "raises an Exception if unable to find the shared ContextState" do
+ expect(MSpec).to receive(:retrieve_shared).and_return(nil)
+ expect { @state.it_should_behave_like :this }.to raise_error(Exception)
+ end
+
+ describe "for nested ContextState instances" do
+ before :each do
+ @nested = ContextState.new "nested context"
+ @nested.parents.unshift @shared
+
+ @shared.children << @nested
+
+ @nested_dup = @nested.dup
+ allow(@nested).to receive(:dup).and_return(@nested_dup)
+ end
+
+ it "duplicates the nested ContextState" do
+ @state.it_should_behave_like @shared_desc
+ expect(@state.children.first).to equal(@nested_dup)
+ end
+
+ it "sets the parent of the nested ContextState to the containing ContextState" do
+ @state.it_should_behave_like @shared_desc
+ expect(@nested_dup.parent).to equal(@state)
+ end
+
+ it "sets the context for nested examples to the nested ContextState's dup" do
+ @shared.it "an example", &@a
+ @shared.it "another example", &@b
+ @state.it_should_behave_like @shared_desc
+ @nested_dup.examples.each { |x| expect(x.context).to equal(@nested_dup) }
+ end
+
+ it "omits the shored ContextState's description" do
+ @nested.it "an example", &@a
+ @nested.it "another example", &@b
+ @state.it_should_behave_like @shared_desc
+
+ expect(@nested_dup.description).to eq("Top level nested context")
+ expect(@nested_dup.examples.first.description).to eq("Top level nested context an example")
+ expect(@nested_dup.examples.last.description).to eq("Top level nested context another example")
+ end
+ end
+
+ it "adds duped examples from the shared ContextState" do
+ @shared.it "some method", &@a
+ ex_dup = @shared.examples.first.dup
+ allow(@shared.examples.first).to receive(:dup).and_return(ex_dup)
+
+ @state.it_should_behave_like @shared_desc
+ expect(@state.examples).to eq([ex_dup])
+ end
+
+ it "sets the context for examples to the containing ContextState" do
+ @shared.it "an example", &@a
+ @shared.it "another example", &@b
+ @state.it_should_behave_like @shared_desc
+ @state.examples.each { |x| expect(x.context).to equal(@state) }
+ end
+
+ it "adds before(:all) blocks from the shared ContextState" do
+ @shared.before :all, &@a
+ @shared.before :all, &@b
+ @state.it_should_behave_like @shared_desc
+ expect(@state.before(:all)).to include(*@shared.before(:all))
+ end
+
+ it "adds before(:each) blocks from the shared ContextState" do
+ @shared.before :each, &@a
+ @shared.before :each, &@b
+ @state.it_should_behave_like @shared_desc
+ expect(@state.before(:each)).to include(*@shared.before(:each))
+ end
+
+ it "adds after(:each) blocks from the shared ContextState" do
+ @shared.after :each, &@a
+ @shared.after :each, &@b
+ @state.it_should_behave_like @shared_desc
+ expect(@state.after(:each)).to include(*@shared.after(:each))
+ end
+
+ it "adds after(:all) blocks from the shared ContextState" do
+ @shared.after :all, &@a
+ @shared.after :all, &@b
+ @state.it_should_behave_like @shared_desc
+ expect(@state.after(:all)).to include(*@shared.after(:all))
+ end
+end
+
+RSpec.describe ContextState, "#filter_examples" do
+ before :each do
+ @state = ContextState.new ""
+ @state.it("one") { }
+ @state.it("two") { }
+ end
+
+ it "removes examples that are filtered" do
+ allow(@state.examples.first).to receive(:filtered?).and_return(true)
+ expect(@state.examples.size).to eq(2)
+ @state.filter_examples
+ expect(@state.examples.size).to eq(1)
+ end
+
+ it "returns true if there are remaining examples to evaluate" do
+ allow(@state.examples.first).to receive(:filtered?).and_return(true)
+ expect(@state.filter_examples).to be_truthy
+ end
+
+ it "returns false if there are no remaining examples to evaluate" do
+ allow(@state.examples.first).to receive(:filtered?).and_return(true)
+ allow(@state.examples.last).to receive(:filtered?).and_return(true)
+ expect(@state.filter_examples).to be_falsey
+ end
+end
diff --git a/spec/mspec/spec/runner/example_spec.rb b/spec/mspec/spec/runner/example_spec.rb
new file mode 100644
index 0000000000..8bac166da8
--- /dev/null
+++ b/spec/mspec/spec/runner/example_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+require 'mspec/matchers/base'
+require 'mspec/runner/mspec'
+require 'mspec/mocks/mock'
+require 'mspec/runner/example'
+
+RSpec.describe ExampleState do
+ it "is initialized with the ContextState, #it string, and #it block" do
+ prc = lambda { }
+ context = ContextState.new ""
+ expect(ExampleState.new(context, "does", prc)).to be_kind_of(ExampleState)
+ end
+end
+
+RSpec.describe ExampleState, "#describe" do
+ before :each do
+ @context = ContextState.new "Object#to_s"
+ @state = ExampleState.new @context, "it"
+ end
+
+ it "returns the ContextState#description" do
+ expect(@state.describe).to eq(@context.description)
+ end
+end
+
+RSpec.describe ExampleState, "#it" do
+ before :each do
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ it "returns the argument to the #it block" do
+ expect(@state.it).to eq("it")
+ end
+end
+
+RSpec.describe ExampleState, "#context=" do
+ before :each do
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ @context = ContextState.new "New#context"
+ end
+
+ it "sets the containing ContextState" do
+ @state.context = @context
+ expect(@state.context).to eq(@context)
+ end
+
+ it "resets the description" do
+ expect(@state.description).to eq("describe it")
+ @state.context = @context
+ expect(@state.description).to eq("New#context it")
+ end
+end
+
+RSpec.describe ExampleState, "#example" do
+ before :each do
+ @proc = lambda { }
+ @state = ExampleState.new ContextState.new("describe"), "it", @proc
+ end
+
+ it "returns the #it block" do
+ expect(@state.example).to eq(@proc)
+ end
+end
+
+RSpec.describe ExampleState, "#filtered?" do
+ before :each do
+ MSpec.store :include, []
+ MSpec.store :exclude, []
+
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ @filter = double("filter")
+ end
+
+ after :each do
+ MSpec.store :include, []
+ MSpec.store :exclude, []
+ end
+
+ it "returns false if MSpec include filters list is empty" do
+ expect(@state.filtered?).to eq(false)
+ end
+
+ it "returns false if MSpec include filters match this spec" do
+ expect(@filter).to receive(:===).and_return(true)
+ MSpec.register :include, @filter
+ expect(@state.filtered?).to eq(false)
+ end
+
+ it "returns true if MSpec include filters do not match this spec" do
+ expect(@filter).to receive(:===).and_return(false)
+ MSpec.register :include, @filter
+ expect(@state.filtered?).to eq(true)
+ end
+
+ it "returns false if MSpec exclude filters list is empty" do
+ expect(@state.filtered?).to eq(false)
+ end
+
+ it "returns false if MSpec exclude filters do not match this spec" do
+ expect(@filter).to receive(:===).and_return(false)
+ MSpec.register :exclude, @filter
+ expect(@state.filtered?).to eq(false)
+ end
+
+ it "returns true if MSpec exclude filters match this spec" do
+ expect(@filter).to receive(:===).and_return(true)
+ MSpec.register :exclude, @filter
+ expect(@state.filtered?).to eq(true)
+ end
+
+ it "returns true if MSpec include and exclude filters match this spec" do
+ expect(@filter).to receive(:===).twice.and_return(true)
+ MSpec.register :include, @filter
+ MSpec.register :exclude, @filter
+ expect(@state.filtered?).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/runner/exception_spec.rb b/spec/mspec/spec/runner/exception_spec.rb
new file mode 100644
index 0000000000..a77a2c9cf4
--- /dev/null
+++ b/spec/mspec/spec/runner/exception_spec.rb
@@ -0,0 +1,146 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/runner/example'
+require 'mspec/runner/exception'
+require 'mspec/utils/script'
+
+RSpec.describe ExceptionState, "#initialize" do
+ it "takes a state, location (e.g. before :each), and exception" do
+ context = ContextState.new "Class#method"
+ state = ExampleState.new context, "does something"
+ exc = Exception.new "Fail!"
+ expect(ExceptionState.new(state, "location", exc)).to be_kind_of(ExceptionState)
+ end
+end
+
+RSpec.describe ExceptionState, "#description" do
+ before :each do
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new context, "does something"
+ end
+
+ it "returns the state description if state was not nil" do
+ exc = ExceptionState.new(@state, nil, nil)
+ expect(exc.description).to eq("Class#method does something")
+ end
+
+ it "returns the location if it is not nil and description is nil" do
+ exc = ExceptionState.new(nil, "location", nil)
+ expect(exc.description).to eq("An exception occurred during: location")
+ end
+
+ it "returns both description and location if neither are nil" do
+ exc = ExceptionState.new(@state, "location", nil)
+ expect(exc.description).to eq("An exception occurred during: location\nClass#method does something")
+ end
+end
+
+RSpec.describe ExceptionState, "#describe" do
+ before :each do
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new context, "does something"
+ end
+
+ it "returns the ExampleState#describe string if created with a non-nil state" do
+ expect(ExceptionState.new(@state, nil, nil).describe).to eq(@state.describe)
+ end
+
+ it "returns an empty string if created with a nil state" do
+ expect(ExceptionState.new(nil, nil, nil).describe).to eq("")
+ end
+end
+
+RSpec.describe ExceptionState, "#it" do
+ before :each do
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new context, "does something"
+ end
+
+ it "returns the ExampleState#it string if created with a non-nil state" do
+ expect(ExceptionState.new(@state, nil, nil).it).to eq(@state.it)
+ end
+
+ it "returns an empty string if created with a nil state" do
+ expect(ExceptionState.new(nil, nil, nil).it).to eq("")
+ end
+end
+
+RSpec.describe ExceptionState, "#failure?" do
+ before :each do
+ @state = ExampleState.new ContextState.new("C#m"), "works"
+ end
+
+ it "returns true if the exception is an SpecExpectationNotMetError" do
+ exc = ExceptionState.new @state, "", SpecExpectationNotMetError.new("Fail!")
+ expect(exc.failure?).to be_truthy
+ end
+
+ it "returns true if the exception is an SpecExpectationNotFoundError" do
+ exc = ExceptionState.new @state, "", SpecExpectationNotFoundError.new("Fail!")
+ expect(exc.failure?).to be_truthy
+ end
+
+ it "returns false if the exception is not an SpecExpectationNotMetError or an SpecExpectationNotFoundError" do
+ exc = ExceptionState.new @state, "", Exception.new("Fail!")
+ expect(exc.failure?).to be_falsey
+ end
+end
+
+RSpec.describe ExceptionState, "#message" do
+ before :each do
+ @state = ExampleState.new ContextState.new("C#m"), "works"
+ end
+
+ it "returns <No message> if the exception message is empty" do
+ exc = ExceptionState.new @state, "", Exception.new("")
+ expect(exc.message).to eq("Exception: <No message>")
+ end
+
+ it "returns the message without exception class when the exception is an SpecExpectationNotMetError" do
+ exc = ExceptionState.new @state, "", SpecExpectationNotMetError.new("Fail!")
+ expect(exc.message).to eq("Fail!")
+ end
+
+ it "returns SpecExpectationNotFoundError#message when the exception is an SpecExpectationNotFoundError" do
+ e = SpecExpectationNotFoundError.new
+ exc = ExceptionState.new @state, "", e
+ expect(exc.message).to eq(e.message)
+ end
+
+ it "returns the message with exception class when the exception is not an SpecExpectationNotMetError or an SpecExpectationNotFoundError" do
+ exc = ExceptionState.new @state, "", Exception.new("Fail!")
+ expect(exc.message).to eq("Exception: Fail!")
+ end
+end
+
+RSpec.describe ExceptionState, "#backtrace" do
+ before :each do
+ @state = ExampleState.new ContextState.new("C#m"), "works"
+ begin
+ raise Exception
+ rescue Exception => @exception
+ @exc = ExceptionState.new @state, "", @exception
+ end
+ end
+
+ after :each do
+ $MSPEC_DEBUG = nil
+ end
+
+ it "returns a string representation of the exception backtrace" do
+ expect(@exc.backtrace).to be_kind_of(String)
+ end
+
+ it "does not filter files from the backtrace if $MSPEC_DEBUG is true" do
+ $MSPEC_DEBUG = true
+ expect(@exc.backtrace).to eq(@exception.backtrace.join("\n"))
+ end
+
+ it "filters files matching config[:backtrace_filter]" do
+ MSpecScript.set :backtrace_filter, %r[mspec/lib]
+ $MSPEC_DEBUG = nil
+ @exc.backtrace.split("\n").each do |line|
+ expect(line).not_to match(%r[mspec/lib])
+ end
+ end
+end
diff --git a/spec/mspec/spec/runner/filters/a.yaml b/spec/mspec/spec/runner/filters/a.yaml
new file mode 100644
index 0000000000..1940e3cba6
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/a.yaml
@@ -0,0 +1,4 @@
+---
+A#:
+- a
+- aa
diff --git a/spec/mspec/spec/runner/filters/b.yaml b/spec/mspec/spec/runner/filters/b.yaml
new file mode 100644
index 0000000000..a24bdb2f19
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/b.yaml
@@ -0,0 +1,11 @@
+---
+B.:
+- b
+- bb
+B::C#:
+- b!
+- b=
+- b?
+- "-"
+- "[]"
+- "[]="
diff --git a/spec/mspec/spec/runner/filters/match_spec.rb b/spec/mspec/spec/runner/filters/match_spec.rb
new file mode 100644
index 0000000000..970da00446
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/match_spec.rb
@@ -0,0 +1,34 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/match'
+
+RSpec.describe MatchFilter, "#===" do
+ before :each do
+ @filter = MatchFilter.new nil, 'a', 'b', 'c'
+ end
+
+ it "returns true if the argument matches any of the #initialize strings" do
+ expect(@filter.===('aaa')).to eq(true)
+ expect(@filter.===('bccb')).to eq(true)
+ end
+
+ it "returns false if the argument matches none of the #initialize strings" do
+ expect(@filter.===('d')).to eq(false)
+ end
+end
+
+RSpec.describe MatchFilter, "#register" do
+ it "registers itself with MSpec for the designated action list" do
+ filter = MatchFilter.new :include
+ expect(MSpec).to receive(:register).with(:include, filter)
+ filter.register
+ end
+end
+
+RSpec.describe MatchFilter, "#unregister" do
+ it "unregisters itself with MSpec for the designated action list" do
+ filter = MatchFilter.new :exclude
+ expect(MSpec).to receive(:unregister).with(:exclude, filter)
+ filter.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/filters/profile_spec.rb b/spec/mspec/spec/runner/filters/profile_spec.rb
new file mode 100644
index 0000000000..25f5e07aef
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/profile_spec.rb
@@ -0,0 +1,117 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/profile'
+
+RSpec.describe ProfileFilter, "#find" do
+ before :each do
+ @filter = ProfileFilter.new nil
+ allow(File).to receive(:exist?).and_return(false)
+ @file = "rails.yaml"
+ end
+
+ it "attempts to locate the file through the expanded path name" do
+ expect(File).to receive(:expand_path).with(@file).and_return(@file)
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(@filter.find(@file)).to eq(@file)
+ end
+
+ it "attempts to locate the file in 'spec/profiles'" do
+ path = File.join "spec/profiles", @file
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(@filter.find(@file)).to eq(path)
+ end
+
+ it "attempts to locate the file in 'spec'" do
+ path = File.join "spec", @file
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(@filter.find(@file)).to eq(path)
+ end
+
+ it "attempts to locate the file in 'profiles'" do
+ path = File.join "profiles", @file
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(@filter.find(@file)).to eq(path)
+ end
+
+ it "attempts to locate the file in '.'" do
+ path = File.join ".", @file
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(@filter.find(@file)).to eq(path)
+ end
+end
+
+RSpec.describe ProfileFilter, "#parse" do
+ before :each do
+ @filter = ProfileFilter.new nil
+ @file = File.open(File.dirname(__FILE__) + "/b.yaml", "r")
+ end
+
+ after :each do
+ @file.close
+ end
+
+ it "creates a Hash of the contents of the YAML file" do
+ expect(@filter.parse(@file)).to eq({
+ "B." => ["b", "bb"],
+ "B::C#" => ["b!", "b=", "b?", "-", "[]", "[]="]
+ })
+ end
+end
+
+RSpec.describe ProfileFilter, "#load" do
+ before :each do
+ @filter = ProfileFilter.new nil
+ @files = [
+ File.dirname(__FILE__) + "/a.yaml",
+ File.dirname(__FILE__) + "/b.yaml"
+ ]
+ end
+
+ it "generates a composite hash from multiple YAML files" do
+ expect(@filter.load(*@files)).to eq({
+ "A#" => ["a", "aa"],
+ "B." => ["b", "bb"],
+ "B::C#" => ["b!", "b=", "b?", "-", "[]", "[]="]
+ })
+ end
+end
+
+RSpec.describe ProfileFilter, "#===" do
+ before :each do
+ @filter = ProfileFilter.new nil
+ allow(@filter).to receive(:load).and_return({ "A#" => ["[]=", "a", "a!", "a?", "aa="]})
+ @filter.send :initialize, nil
+ end
+
+ it "returns true if the spec description is for a method in the profile" do
+ expect(@filter.===("The A#[]= method")).to eq(true)
+ expect(@filter.===("A#a returns")).to eq(true)
+ expect(@filter.===("A#a! replaces")).to eq(true)
+ expect(@filter.===("A#a? returns")).to eq(true)
+ expect(@filter.===("A#aa= raises")).to eq(true)
+ end
+
+ it "returns false if the spec description is for a method not in the profile" do
+ expect(@filter.===("The A#[] method")).to eq(false)
+ expect(@filter.===("B#a returns")).to eq(false)
+ expect(@filter.===("A.a! replaces")).to eq(false)
+ expect(@filter.===("AA#a? returns")).to eq(false)
+ expect(@filter.===("A#aa raises")).to eq(false)
+ end
+end
+
+RSpec.describe ProfileFilter, "#register" do
+ it "registers itself with MSpec for the designated action list" do
+ filter = ProfileFilter.new :include
+ expect(MSpec).to receive(:register).with(:include, filter)
+ filter.register
+ end
+end
+
+RSpec.describe ProfileFilter, "#unregister" do
+ it "unregisters itself with MSpec for the designated action list" do
+ filter = ProfileFilter.new :exclude
+ expect(MSpec).to receive(:unregister).with(:exclude, filter)
+ filter.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/filters/regexp_spec.rb b/spec/mspec/spec/runner/filters/regexp_spec.rb
new file mode 100644
index 0000000000..1d1d3554f6
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/regexp_spec.rb
@@ -0,0 +1,31 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/regexp'
+
+RSpec.describe MatchFilter, "#===" do
+ before :each do
+ @filter = RegexpFilter.new nil, 'a(b|c)', 'b[^ab]', 'cc?'
+ end
+
+ it "returns true if the argument matches any of the #initialize strings" do
+ expect(@filter.===('ab')).to eq(true)
+ expect(@filter.===('bc suffix')).to eq(true)
+ expect(@filter.===('prefix cc')).to eq(true)
+ end
+
+ it "returns false if the argument matches none of the #initialize strings" do
+ expect(@filter.===('aa')).to eq(false)
+ expect(@filter.===('ba')).to eq(false)
+ expect(@filter.===('prefix d suffix')).to eq(false)
+ end
+end
+
+RSpec.describe RegexpFilter, "#to_regexp" do
+ before :each do
+ @filter = RegexpFilter.new nil
+ end
+
+ it "converts its arguments to Regexp instances" do
+ expect(@filter.send(:to_regexp, 'a(b|c)', 'b[^ab]', 'cc?')).to eq([/a(b|c)/, /b[^ab]/, /cc?/])
+ end
+end
diff --git a/spec/mspec/spec/runner/filters/tag_spec.rb b/spec/mspec/spec/runner/filters/tag_spec.rb
new file mode 100644
index 0000000000..356175a754
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/tag_spec.rb
@@ -0,0 +1,92 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/match'
+require 'mspec/runner/filters/tag'
+
+RSpec.describe TagFilter, "#load" do
+ before :each do
+ @match = double("match filter").as_null_object
+ @filter = TagFilter.new :include, "tag", "key"
+ @tag = SpecTag.new "tag(comment):description"
+ allow(MSpec).to receive(:read_tags).and_return([@tag])
+ allow(MSpec).to receive(:register)
+ end
+
+ it "loads tags from the tag file" do
+ expect(MSpec).to receive(:read_tags).with(["tag", "key"]).and_return([])
+ @filter.load
+ end
+
+
+ it "registers itself with MSpec for the :include action" do
+ filter = TagFilter.new(:include)
+ expect(MSpec).to receive(:register).with(:include, filter)
+ filter.load
+ end
+
+ it "registers itself with MSpec for the :exclude action" do
+ filter = TagFilter.new(:exclude)
+ expect(MSpec).to receive(:register).with(:exclude, filter)
+ filter.load
+ end
+end
+
+RSpec.describe TagFilter, "#unload" do
+ before :each do
+ @filter = TagFilter.new :include, "tag", "key"
+ @tag = SpecTag.new "tag(comment):description"
+ allow(MSpec).to receive(:read_tags).and_return([@tag])
+ allow(MSpec).to receive(:register)
+ end
+
+ it "unregisters itself" do
+ @filter.load
+ expect(MSpec).to receive(:unregister).with(:include, @filter)
+ @filter.unload
+ end
+end
+
+RSpec.describe TagFilter, "#register" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ end
+
+ it "registers itself with MSpec for the :load, :unload actions" do
+ filter = TagFilter.new(nil)
+ expect(MSpec).to receive(:register).with(:load, filter)
+ expect(MSpec).to receive(:register).with(:unload, filter)
+ filter.register
+ end
+end
+
+RSpec.describe TagFilter, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ end
+
+ it "unregisters itself with MSpec for the :load, :unload actions" do
+ filter = TagFilter.new(nil)
+ expect(MSpec).to receive(:unregister).with(:load, filter)
+ expect(MSpec).to receive(:unregister).with(:unload, filter)
+ filter.unregister
+ end
+end
+
+RSpec.describe TagFilter, "#===" do
+ before :each do
+ @filter = TagFilter.new nil, "tag", "key"
+ @tag = SpecTag.new "tag(comment):description"
+ allow(MSpec).to receive(:read_tags).and_return([@tag])
+ allow(MSpec).to receive(:register)
+ @filter.load
+ end
+
+ it "returns true if the argument matches any of the descriptions" do
+ expect(@filter.===('description')).to eq(true)
+ end
+
+ it "returns false if the argument matches none of the descriptions" do
+ expect(@filter.===('descriptionA')).to eq(false)
+ expect(@filter.===('adescription')).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/describe_spec.rb b/spec/mspec/spec/runner/formatters/describe_spec.rb
new file mode 100644
index 0000000000..55f497aca6
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/describe_spec.rb
@@ -0,0 +1,67 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/describe'
+require 'mspec/runner/example'
+
+RSpec.describe DescribeFormatter, "#finish" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ allow(MSpec).to receive(:unregister)
+
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+ allow(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+
+ $stdout = @out = IOStub.new
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new(context, "runs")
+
+ @formatter = DescribeFormatter.new
+ @formatter.register
+
+ @tally = @formatter.tally
+ @counter = @tally.counter
+
+ @counter.files!
+ @counter.examples!
+ @counter.expectations! 2
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a summary of elapsed time" do
+ @formatter.finish
+ expect(@out).to match(/^Finished in 2.0 seconds$/)
+ end
+
+ it "prints a tally of counts" do
+ @formatter.finish
+ expect(@out).to match(/^1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged$/)
+ end
+
+ it "does not print exceptions" do
+ @formatter.finish
+ expect(@out).to eq(%[
+
+Finished in 2.0 seconds
+
+1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged
+])
+ end
+
+ it "prints a summary of failures and errors for each describe block" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.finish
+ expect(@out).to eq(%[
+
+Class#method 0 failures, 1 error
+
+Finished in 2.0 seconds
+
+1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/dotted_spec.rb b/spec/mspec/spec/runner/formatters/dotted_spec.rb
new file mode 100644
index 0000000000..336b1227ed
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/dotted_spec.rb
@@ -0,0 +1,284 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/utils/script'
+
+RSpec.describe DottedFormatter, "#initialize" do
+ it "permits zero arguments" do
+ DottedFormatter.new
+ end
+
+ it "accepts one argument" do
+ DottedFormatter.new nil
+ end
+end
+
+RSpec.describe DottedFormatter, "#register" do
+ before :each do
+ @formatter = DottedFormatter.new
+ allow(MSpec).to receive(:register)
+ end
+
+ it "registers self with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:register).with(:exception, @formatter)
+ expect(MSpec).to receive(:register).with(:before, @formatter)
+ expect(MSpec).to receive(:register).with(:after, @formatter)
+ expect(MSpec).to receive(:register).with(:finish, @formatter)
+ @formatter.register
+ end
+
+ it "creates TimerAction and TallyAction" do
+ timer = double("timer")
+ tally = double("tally")
+ expect(timer).to receive(:register)
+ expect(tally).to receive(:register)
+ expect(tally).to receive(:counter)
+ expect(TimerAction).to receive(:new).and_return(timer)
+ expect(TallyAction).to receive(:new).and_return(tally)
+ @formatter.register
+ end
+end
+
+RSpec.describe DottedFormatter, "#print" do
+ before :each do
+ $stdout = IOStub.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "writes to $stdout by default" do
+ formatter = DottedFormatter.new
+ formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ end
+
+ it "writes to the file specified when the formatter was created" do
+ out = IOStub.new
+ expect(File).to receive(:open).with("some/file", "w").and_return(out)
+ formatter = DottedFormatter.new "some/file"
+ formatter.print "begonias"
+ expect(out).to eq("begonias")
+ end
+
+ it "flushes the IO output" do
+ expect($stdout).to receive(:flush)
+ formatter = DottedFormatter.new
+ formatter.print "begonias"
+ end
+end
+
+RSpec.describe DottedFormatter, "#exception" do
+ before :each do
+ @formatter = DottedFormatter.new
+ @failure = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("failed")
+ @error = ExceptionState.new nil, nil, MSpecExampleError.new("boom!")
+ end
+
+ it "sets the #failure? flag" do
+ @formatter.exception @failure
+ expect(@formatter.failure?).to be_truthy
+ @formatter.exception @error
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "sets the #exception? flag" do
+ @formatter.exception @error
+ expect(@formatter.exception?).to be_truthy
+ @formatter.exception @failure
+ expect(@formatter.exception?).to be_truthy
+ end
+
+ it "adds the exception to the list of exceptions" do
+ expect(@formatter.exceptions).to eq([])
+ @formatter.exception @error
+ @formatter.exception @failure
+ expect(@formatter.exceptions).to eq([@error, @failure])
+ end
+end
+
+RSpec.describe DottedFormatter, "#exception?" do
+ before :each do
+ @formatter = DottedFormatter.new
+ @failure = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("failed")
+ @error = ExceptionState.new nil, nil, MSpecExampleError.new("boom!")
+ end
+
+ it "returns false if there have been no exceptions" do
+ expect(@formatter.exception?).to be_falsey
+ end
+
+ it "returns true if any exceptions are errors" do
+ @formatter.exception @failure
+ @formatter.exception @error
+ expect(@formatter.exception?).to be_truthy
+ end
+
+ it "returns true if all exceptions are failures" do
+ @formatter.exception @failure
+ @formatter.exception @failure
+ expect(@formatter.exception?).to be_truthy
+ end
+
+ it "returns true if all exceptions are errors" do
+ @formatter.exception @error
+ @formatter.exception @error
+ expect(@formatter.exception?).to be_truthy
+ end
+end
+
+RSpec.describe DottedFormatter, "#failure?" do
+ before :each do
+ @formatter = DottedFormatter.new
+ @failure = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("failed")
+ @error = ExceptionState.new nil, nil, MSpecExampleError.new("boom!")
+ end
+
+ it "returns false if there have been no exceptions" do
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "returns false if any exceptions are errors" do
+ @formatter.exception @failure
+ @formatter.exception @error
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "returns true if all exceptions are failures" do
+ @formatter.exception @failure
+ @formatter.exception @failure
+ expect(@formatter.failure?).to be_truthy
+ end
+end
+
+RSpec.describe DottedFormatter, "#before" do
+ before :each do
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ @formatter = DottedFormatter.new
+ @formatter.exception ExceptionState.new(nil, nil, SpecExpectationNotMetError.new("Failed!"))
+ end
+
+ it "resets the #failure? flag to false" do
+ expect(@formatter.failure?).to be_truthy
+ @formatter.before @state
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "resets the #exception? flag to false" do
+ expect(@formatter.exception?).to be_truthy
+ @formatter.before @state
+ expect(@formatter.exception?).to be_falsey
+ end
+end
+
+RSpec.describe DottedFormatter, "#after" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = DottedFormatter.new
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a '.' if there was no exception raised" do
+ @formatter.after(@state)
+ expect(@out).to eq(".")
+ end
+
+ it "prints an 'F' if there was an expectation failure" do
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.after(@state)
+ expect(@out).to eq("F")
+ end
+
+ it "prints an 'E' if there was an exception other than expectation failure" do
+ exc = MSpecExampleError.new("boom!")
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.after(@state)
+ expect(@out).to eq("E")
+ end
+
+ it "prints an 'E' if there are mixed exceptions and exepctation failures" do
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ exc = MSpecExampleError.new("boom!")
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.after(@state)
+ expect(@out).to eq("E")
+ end
+end
+
+RSpec.describe DottedFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ allow(TallyAction).to receive(:new).and_return(@tally)
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ $stdout = @out = IOStub.new
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new(context, "runs")
+ allow(MSpec).to receive(:register)
+ @formatter = DottedFormatter.new
+ @formatter.register
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a failure message for an exception" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ @formatter.exception exc
+ @formatter.after @state
+ @formatter.finish
+ expect(@out).to match(/^1\)\nClass#method runs ERROR$/)
+ end
+
+ it "prints a backtrace for an exception" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.after @state
+ @formatter.finish
+ expect(@out).to match(%r[path/to/some/file.rb:35:in method$])
+ end
+
+ it "prints a summary of elapsed time" do
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ @formatter.finish
+ expect(@out).to match(/^Finished in 2.0 seconds$/)
+ end
+
+ it "prints a tally of counts" do
+ expect(@tally).to receive(:format).and_return("1 example, 0 failures")
+ @formatter.finish
+ expect(@out).to match(/^1 example, 0 failures$/)
+ end
+
+ it "prints errors, backtraces, elapsed time, and tallies" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ expect(@tally).to receive(:format).and_return("1 example, 1 failure")
+ @formatter.after @state
+ @formatter.finish
+ expect(@out).to eq(%[E
+
+1)
+Class#method runs ERROR
+MSpecExampleError: broken
+path/to/some/file.rb:35:in method
+
+Finished in 2.0 seconds
+
+1 example, 1 failure
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/file_spec.rb b/spec/mspec/spec/runner/formatters/file_spec.rb
new file mode 100644
index 0000000000..ae11d60845
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/file_spec.rb
@@ -0,0 +1,84 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/file'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+
+RSpec.describe FileFormatter, "#register" do
+ before :each do
+ @formatter = FileFormatter.new
+ allow(MSpec).to receive(:register)
+ allow(MSpec).to receive(:unregister)
+ end
+
+ it "registers self with MSpec for :load, :unload actions" do
+ expect(MSpec).to receive(:register).with(:load, @formatter)
+ expect(MSpec).to receive(:register).with(:unload, @formatter)
+ @formatter.register
+ end
+
+ it "unregisters self with MSpec for :before, :after actions" do
+ expect(MSpec).to receive(:unregister).with(:before, @formatter)
+ expect(MSpec).to receive(:unregister).with(:after, @formatter)
+ @formatter.register
+ end
+end
+
+RSpec.describe FileFormatter, "#load" do
+ before :each do
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ @formatter = FileFormatter.new
+ @formatter.exception ExceptionState.new(nil, nil, SpecExpectationNotMetError.new("Failed!"))
+ end
+
+ it "resets the #failure? flag to false" do
+ expect(@formatter.failure?).to be_truthy
+ @formatter.load @state
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "resets the #exception? flag to false" do
+ expect(@formatter.exception?).to be_truthy
+ @formatter.load @state
+ expect(@formatter.exception?).to be_falsey
+ end
+end
+
+RSpec.describe FileFormatter, "#unload" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = FileFormatter.new
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a '.' if there was no exception raised" do
+ @formatter.unload(@state)
+ expect(@out).to eq(".")
+ end
+
+ it "prints an 'F' if there was an expectation failure" do
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.unload(@state)
+ expect(@out).to eq("F")
+ end
+
+ it "prints an 'E' if there was an exception other than expectation failure" do
+ exc = MSpecExampleError.new("boom!")
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.unload(@state)
+ expect(@out).to eq("E")
+ end
+
+ it "prints an 'E' if there are mixed exceptions and exepctation failures" do
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ exc = MSpecExampleError.new("boom!")
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.unload(@state)
+ expect(@out).to eq("E")
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/html_spec.rb b/spec/mspec/spec/runner/formatters/html_spec.rb
new file mode 100644
index 0000000000..ed973ad93f
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/html_spec.rb
@@ -0,0 +1,220 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/guards/guard'
+require 'mspec/runner/formatters/html'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/utils/script'
+require 'mspec/helpers'
+
+RSpec.describe HtmlFormatter do
+ before :each do
+ @formatter = HtmlFormatter.new
+ end
+
+ it "responds to #register by registering itself with MSpec for appropriate actions" do
+ allow(MSpec).to receive(:register)
+ expect(MSpec).to receive(:register).with(:start, @formatter)
+ expect(MSpec).to receive(:register).with(:enter, @formatter)
+ expect(MSpec).to receive(:register).with(:leave, @formatter)
+ @formatter.register
+ end
+end
+
+RSpec.describe HtmlFormatter, "#start" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the HTML head" do
+ @formatter.start
+ ruby_engine = RUBY_ENGINE
+ expect(ruby_engine).to match(/^#{ruby_engine}/)
+ expect(@out).to eq(%[<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>Spec Output For #{ruby_engine} (#{RUBY_VERSION})</title>
+<style type="text/css">
+ul {
+ list-style: none;
+}
+.fail {
+ color: red;
+}
+.pass {
+ color: green;
+}
+#details :target {
+ background-color: #ffffe0;
+}
+</style>
+</head>
+<body>
+])
+ end
+end
+
+RSpec.describe HtmlFormatter, "#enter" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #describe string" do
+ @formatter.enter "describe"
+ expect(@out).to eq("<div><p>describe</p>\n<ul>\n")
+ end
+end
+
+RSpec.describe HtmlFormatter, "#leave" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the closing tags for the #describe string" do
+ @formatter.leave
+ expect(@out).to eq("</ul>\n</div>\n")
+ end
+end
+
+RSpec.describe HtmlFormatter, "#exception" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ @formatter.register
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #it string once for each exception raised" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful")
+ @formatter.exception exc
+ expect(@out).to eq(%[<li class="fail">- it (<a href="#details-1">FAILED - 1</a>)</li>
+<li class="fail">- it (<a href="#details-2">ERROR - 2</a>)</li>
+])
+ end
+end
+
+RSpec.describe HtmlFormatter, "#after" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ @formatter.register
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #it once when there are no exceptions raised" do
+ @formatter.after @state
+ expect(@out).to eq(%[<li class="pass">- it</li>\n])
+ end
+
+ it "does not print any output if an exception is raised" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ out = @out.dup
+ @formatter.after @state
+ expect(@out).to eq(out)
+ end
+end
+
+RSpec.describe HtmlFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ allow(TallyAction).to receive(:new).and_return(@tally)
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ @out = tmp("HtmlFormatter")
+
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+ allow(MSpec).to receive(:register)
+ @formatter = HtmlFormatter.new(@out)
+ @formatter.register
+ @exception = MSpecExampleError.new("broken")
+ allow(@exception).to receive(:backtrace).and_return(["file.rb:1", "file.rb:2"])
+ end
+
+ after :each do
+ rm_r @out
+ end
+
+ it "prints a failure message for an exception" do
+ exc = ExceptionState.new @state, nil, @exception
+ @formatter.exception exc
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "<p>describe it ERROR</p>"
+ end
+
+ it "prints a backtrace for an exception" do
+ exc = ExceptionState.new @state, nil, @exception
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to match(%r[<pre>.*path/to/some/file.rb:35:in method.*</pre>]m)
+ end
+
+ it "prints a summary of elapsed time" do
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "<p>Finished in 2.0 seconds</p>\n"
+ end
+
+ it "prints a tally of counts" do
+ expect(@tally).to receive(:format).and_return("1 example, 0 failures")
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include '<p class="pass">1 example, 0 failures</p>'
+ end
+
+ it "prints errors, backtraces, elapsed time, and tallies" do
+ exc = ExceptionState.new @state, nil, @exception
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ expect(@tally).to receive(:format).and_return("1 example, 1 failures")
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to eq(%[<li class=\"fail\">- it (<a href=\"#details-1\">ERROR - 1</a>)</li>
+<hr>
+<ol id="details">
+<li id="details-1"><p>describe it ERROR</p>
+<p>MSpecExampleError: broken</p>
+<pre>
+path/to/some/file.rb:35:in method</pre>
+</li>
+</ol>
+<p>Finished in 2.0 seconds</p>
+<p class="fail">1 example, 1 failures</p>
+</body>
+</html>
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/junit_spec.rb b/spec/mspec/spec/runner/formatters/junit_spec.rb
new file mode 100644
index 0000000000..3b3da73849
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/junit_spec.rb
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/junit'
+require 'mspec/runner/example'
+require 'mspec/helpers'
+
+RSpec.describe JUnitFormatter, "#initialize" do
+ it "permits zero arguments" do
+ expect { JUnitFormatter.new }.not_to raise_error
+ end
+
+ it "accepts one argument" do
+ expect { JUnitFormatter.new nil }.not_to raise_error
+ end
+end
+
+RSpec.describe JUnitFormatter, "#print" do
+ before :each do
+ $stdout = IOStub.new
+ @out = IOStub.new
+ allow(File).to receive(:open).and_return(@out)
+ @formatter = JUnitFormatter.new "some/file"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "writes to $stdout if #switch has not been called" do
+ @formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ expect(@out).to eq("")
+ end
+
+ it "writes to the file passed to #initialize once #switch has been called" do
+ @formatter.switch
+ @formatter.print "begonias"
+ expect($stdout).to eq("")
+ expect(@out).to eq("begonias")
+ end
+
+ it "writes to $stdout once #switch is called if no file was passed to #initialize" do
+ formatter = JUnitFormatter.new
+ formatter.switch
+ formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ expect(@out).to eq("")
+ end
+end
+
+RSpec.describe JUnitFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ @counter = double("counter").as_null_object
+ allow(@tally).to receive(:counter).and_return(@counter)
+ allow(TallyAction).to receive(:new).and_return(@tally)
+
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ @out = tmp("JUnitFormatter")
+
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+
+ @formatter = JUnitFormatter.new(@out)
+ allow(@formatter).to receive(:backtrace).and_return("")
+ allow(MSpec).to receive(:register)
+ @formatter.register
+
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.after @state
+ end
+
+ after :each do
+ rm_r @out
+ end
+
+ it "calls #switch" do
+ expect(@formatter).to receive(:switch).and_call_original
+ @formatter.finish
+ end
+
+ it "outputs a failure message and backtrace" do
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'message="error in describe it" type="error"'
+ expect(output).to include "MSpecExampleError: broken\n"
+ expect(output).to include "path/to/some/file.rb:35:in method"
+ end
+
+ it "encodes message and backtrace in latin1 for jenkins" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken…")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in methød")
+ @formatter.exception exc
+ @formatter.finish
+ output = File.binread(@out)
+ expect(output).to match(/MSpecExampleError: broken((\.\.\.)|\?)\n/)
+ expect(output).to match(/path\/to\/some\/file\.rb:35:in meth(\?|o)d/)
+ end
+
+ it "outputs an elapsed time" do
+ expect(@timer).to receive(:elapsed).and_return(4.2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'time="4.2"'
+ end
+
+ it "outputs overall elapsed time" do
+ expect(@timer).to receive(:elapsed).and_return(4.2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'timeCount="4.2"'
+ end
+
+ it "outputs the number of examples as test count" do
+ expect(@counter).to receive(:examples).and_return(9)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'tests="9"'
+ end
+
+ it "outputs overall number of examples as test count" do
+ expect(@counter).to receive(:examples).and_return(9)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'testCount="9"'
+ end
+
+ it "outputs a failure count" do
+ expect(@counter).to receive(:failures).and_return(2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'failureCount="2"'
+ end
+
+ it "outputs overall failure count" do
+ expect(@counter).to receive(:failures).and_return(2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'failures="2"'
+ end
+
+ it "outputs an error count" do
+ expect(@counter).to receive(:errors).and_return(1)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'errors="1"'
+ end
+
+ it "outputs overall error count" do
+ expect(@counter).to receive(:errors).and_return(1)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'errorCount="1"'
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/method_spec.rb b/spec/mspec/spec/runner/formatters/method_spec.rb
new file mode 100644
index 0000000000..02bf47d538
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/method_spec.rb
@@ -0,0 +1,177 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/method'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/utils/script'
+
+RSpec.describe MethodFormatter, "#method_type" do
+ before :each do
+ @formatter = MethodFormatter.new
+ end
+
+ it "returns 'class' if the separator is '.' or '::'" do
+ expect(@formatter.method_type('.')).to eq("class")
+ expect(@formatter.method_type('::')).to eq("class")
+ end
+
+ it "returns 'instance' if the separator is '#'" do
+ expect(@formatter.method_type('#')).to eq("instance")
+ end
+
+ it "returns 'unknown' for all other cases" do
+ expect(@formatter.method_type(nil)).to eq("unknown")
+ end
+end
+
+RSpec.describe MethodFormatter, "#before" do
+ before :each do
+ @formatter = MethodFormatter.new
+ allow(MSpec).to receive(:register)
+ @formatter.register
+ end
+
+ it "resets the tally counters to 0" do
+ @formatter.tally.counter.examples = 3
+ @formatter.tally.counter.expectations = 4
+ @formatter.tally.counter.failures = 2
+ @formatter.tally.counter.errors = 1
+
+ state = ExampleState.new ContextState.new("describe"), "it"
+ @formatter.before state
+ expect(@formatter.tally.counter.examples).to eq(0)
+ expect(@formatter.tally.counter.expectations).to eq(0)
+ expect(@formatter.tally.counter.failures).to eq(0)
+ expect(@formatter.tally.counter.errors).to eq(0)
+ end
+
+ it "records the class, method if available" do
+ state = ExampleState.new ContextState.new("Some#method"), "it"
+ @formatter.before state
+ key = "Some#method"
+ expect(@formatter.methods.keys).to include(key)
+ h = @formatter.methods[key]
+ expect(h[:class]).to eq("Some")
+ expect(h[:method]).to eq("method")
+ expect(h[:description]).to eq("Some#method it")
+ end
+
+ it "does not record class, method unless both are available" do
+ state = ExampleState.new ContextState.new("Some method"), "it"
+ @formatter.before state
+ key = "Some method"
+ expect(@formatter.methods.keys).to include(key)
+ h = @formatter.methods[key]
+ expect(h[:class]).to eq("")
+ expect(h[:method]).to eq("")
+ expect(h[:description]).to eq("Some method it")
+ end
+
+ it "sets the method type to unknown if class and method are not available" do
+ state = ExampleState.new ContextState.new("Some method"), "it"
+ @formatter.before state
+ key = "Some method"
+ h = @formatter.methods[key]
+ expect(h[:type]).to eq("unknown")
+ end
+
+ it "sets the method type based on the class, method separator" do
+ [["C#m", "instance"], ["C.m", "class"], ["C::m", "class"]].each do |k, t|
+ state = ExampleState.new ContextState.new(k), "it"
+ @formatter.before state
+ h = @formatter.methods[k]
+ expect(h[:type]).to eq(t)
+ end
+ end
+
+ it "clears the list of exceptions" do
+ state = ExampleState.new ContextState.new("describe"), "it"
+ @formatter.exceptions << "stuff"
+ @formatter.before state
+ expect(@formatter.exceptions).to be_empty
+ end
+end
+
+RSpec.describe MethodFormatter, "#after" do
+ before :each do
+ @formatter = MethodFormatter.new
+ allow(MSpec).to receive(:register)
+ @formatter.register
+ end
+
+ it "sets the tally counts" do
+ state = ExampleState.new ContextState.new("Some#method"), "it"
+ @formatter.before state
+
+ @formatter.tally.counter.examples = 3
+ @formatter.tally.counter.expectations = 4
+ @formatter.tally.counter.failures = 2
+ @formatter.tally.counter.errors = 1
+
+ @formatter.after state
+ h = @formatter.methods["Some#method"]
+ expect(h[:examples]).to eq(3)
+ expect(h[:expectations]).to eq(4)
+ expect(h[:failures]).to eq(2)
+ expect(h[:errors]).to eq(1)
+ end
+
+ it "renders the list of exceptions" do
+ state = ExampleState.new ContextState.new("Some#method"), "it"
+ @formatter.before state
+
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(state, nil, exc)
+ @formatter.exception ExceptionState.new(state, nil, exc)
+
+ @formatter.after state
+ h = @formatter.methods["Some#method"]
+ expect(h[:exceptions]).to eq([
+ %[failed\n\n],
+ %[failed\n\n]
+ ])
+ end
+end
+
+RSpec.describe MethodFormatter, "#after" do
+ before :each do
+ $stdout = IOStub.new
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new(context, "runs")
+ @formatter = MethodFormatter.new
+ allow(MSpec).to receive(:register)
+ @formatter.register
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a summary of the results of an example in YAML format" do
+ @formatter.before @state
+ @formatter.tally.counter.examples = 3
+ @formatter.tally.counter.expectations = 4
+ @formatter.tally.counter.failures = 2
+ @formatter.tally.counter.errors = 1
+
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+
+ @formatter.after @state
+ @formatter.finish
+ expect($stdout).to eq(%[---
+"Class#method":
+ class: "Class"
+ method: "method"
+ type: instance
+ description: "Class#method runs"
+ examples: 3
+ expectations: 4
+ failures: 2
+ errors: 1
+ exceptions:
+ - "failed\\n\\n"
+ - "failed\\n\\n"
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/multi_spec.rb b/spec/mspec/spec/runner/formatters/multi_spec.rb
new file mode 100644
index 0000000000..2d13c05836
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/multi_spec.rb
@@ -0,0 +1,68 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/multi'
+require 'mspec/runner/example'
+require 'yaml'
+
+RSpec.describe MultiFormatter, "#aggregate_results" do
+ before :each do
+ @stdout, $stdout = $stdout, IOStub.new
+
+ @file = double("file").as_null_object
+
+ allow(File).to receive(:delete)
+ allow(File).to receive(:read)
+
+ @hash = { "files"=>1, "examples"=>1, "expectations"=>2, "failures"=>0, "errors"=>0 }
+ allow(YAML).to receive(:load).and_return(@hash)
+
+ @formatter = DottedFormatter.new.extend(MultiFormatter)
+ allow(@formatter.timer).to receive(:format).and_return("Finished in 42 seconds")
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "outputs a summary without errors" do
+ @formatter.aggregate_results(["a", "b"])
+ @formatter.finish
+ expect($stdout).to eq(%[
+
+Finished in 42 seconds
+
+2 files, 2 examples, 4 expectations, 0 failures, 0 errors, 0 tagged
+])
+ end
+
+ it "outputs a summary with errors" do
+ @hash["exceptions"] = [
+ "Some#method works real good FAILED\nExpected real good\n to equal fail\n\nfoo.rb:1\nfoo.rb:2",
+ "Some#method never fails ERROR\nExpected 5\n to equal 3\n\nfoo.rb:1\nfoo.rb:2"
+ ]
+ @formatter.aggregate_results(["a"])
+ @formatter.finish
+ expect($stdout).to eq(%[
+
+1)
+Some#method works real good FAILED
+Expected real good
+ to equal fail
+
+foo.rb:1
+foo.rb:2
+
+2)
+Some#method never fails ERROR
+Expected 5
+ to equal 3
+
+foo.rb:1
+foo.rb:2
+
+Finished in 42 seconds
+
+1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/specdoc_spec.rb b/spec/mspec/spec/runner/formatters/specdoc_spec.rb
new file mode 100644
index 0000000000..54b5e2cf0d
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/specdoc_spec.rb
@@ -0,0 +1,106 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/specdoc'
+require 'mspec/runner/example'
+
+RSpec.describe SpecdocFormatter do
+ before :each do
+ @formatter = SpecdocFormatter.new
+ end
+
+ it "responds to #register by registering itself with MSpec for appropriate actions" do
+ allow(MSpec).to receive(:register)
+ expect(MSpec).to receive(:register).with(:enter, @formatter)
+ @formatter.register
+ end
+end
+
+RSpec.describe SpecdocFormatter, "#enter" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SpecdocFormatter.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #describe string" do
+ @formatter.enter("describe")
+ expect(@out).to eq("\ndescribe\n")
+ end
+end
+
+RSpec.describe SpecdocFormatter, "#before" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SpecdocFormatter.new
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #it string" do
+ @formatter.before @state
+ expect(@out).to eq("- it")
+ end
+
+ it "resets the #exception? flag" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ expect(@formatter.exception?).to be_truthy
+ @formatter.before @state
+ expect(@formatter.exception?).to be_falsey
+ end
+end
+
+RSpec.describe SpecdocFormatter, "#exception" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SpecdocFormatter.new
+ context = ContextState.new "describe"
+ @state = ExampleState.new context, "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints 'ERROR' if an exception is not an SpecExpectationNotMetError" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful")
+ @formatter.exception exc
+ expect(@out).to eq(" (ERROR - 1)")
+ end
+
+ it "prints 'FAILED' if an exception is an SpecExpectationNotMetError" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ expect(@out).to eq(" (FAILED - 1)")
+ end
+
+ it "prints the #it string if an exception has already been raised" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful")
+ @formatter.exception exc
+ expect(@out).to eq(" (FAILED - 1)\n- it (ERROR - 2)")
+ end
+end
+
+RSpec.describe SpecdocFormatter, "#after" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SpecdocFormatter.new
+ @state = ExampleState.new "describe", "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a newline character" do
+ @formatter.after @state
+ expect(@out).to eq("\n")
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/spinner_spec.rb b/spec/mspec/spec/runner/formatters/spinner_spec.rb
new file mode 100644
index 0000000000..5c93d38822
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/spinner_spec.rb
@@ -0,0 +1,83 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/spinner'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+
+RSpec.describe SpinnerFormatter, "#initialize" do
+ it "permits zero arguments" do
+ SpinnerFormatter.new
+ end
+
+ it "accepts one argument" do
+ SpinnerFormatter.new nil
+ end
+end
+
+RSpec.describe SpinnerFormatter, "#register" do
+ before :each do
+ @formatter = SpinnerFormatter.new
+ allow(MSpec).to receive(:register)
+ end
+
+ it "registers self with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:register).with(:start, @formatter)
+ expect(MSpec).to receive(:register).with(:unload, @formatter)
+ expect(MSpec).to receive(:register).with(:after, @formatter)
+ expect(MSpec).to receive(:register).with(:finish, @formatter)
+ @formatter.register
+ end
+
+ it "creates TimerAction and TallyAction" do
+ timer = double("timer")
+ tally = double("tally")
+ expect(timer).to receive(:register)
+ expect(tally).to receive(:register)
+ expect(tally).to receive(:counter)
+ expect(TimerAction).to receive(:new).and_return(timer)
+ expect(TallyAction).to receive(:new).and_return(tally)
+ @formatter.register
+ end
+end
+
+RSpec.describe SpinnerFormatter, "#print" do
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "ignores the argument to #initialize and writes to $stdout" do
+ $stdout = IOStub.new
+ formatter = SpinnerFormatter.new "some/file"
+ formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ end
+end
+
+RSpec.describe SpinnerFormatter, "#after" do
+ before :each do
+ $stdout = IOStub.new
+ MSpec.store(:files, ["a", "b", "c", "d"])
+ @formatter = SpinnerFormatter.new
+ @formatter.register
+ @state = ExampleState.new("describe", "it")
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "updates the spinner" do
+ @formatter.start
+ @formatter.after @state
+ @formatter.unload
+
+ if ENV["TERM"] != "dumb"
+ green = "\e[0;32m"
+ reset = "\e[0m"
+ end
+
+ output = "\r[/ | 0% | 00:00:00] #{green} 0F #{green} 0E#{reset} " \
+ "\r[- | 0% | 00:00:00] #{green} 0F #{green} 0E#{reset} " \
+ "\r[\\ | ========== 25% | 00:00:00] #{green} 0F #{green} 0E#{reset} "
+ expect($stdout).to eq(output)
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/summary_spec.rb b/spec/mspec/spec/runner/formatters/summary_spec.rb
new file mode 100644
index 0000000000..c87d940042
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/summary_spec.rb
@@ -0,0 +1,26 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/summary'
+require 'mspec/runner/example'
+
+RSpec.describe SummaryFormatter, "#after" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SummaryFormatter.new
+ @formatter.register
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "does not print anything" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful")
+ @formatter.exception exc
+ @formatter.after(@state)
+ expect(@out).to eq("")
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/unit_spec.rb b/spec/mspec/spec/runner/formatters/unit_spec.rb
new file mode 100644
index 0000000000..d349e6871d
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/unit_spec.rb
@@ -0,0 +1,73 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/unit'
+require 'mspec/runner/example'
+require 'mspec/utils/script'
+
+RSpec.describe UnitdiffFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ allow(TallyAction).to receive(:new).and_return(@tally)
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ $stdout = @out = IOStub.new
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+ allow(MSpec).to receive(:register)
+ @formatter = UnitdiffFormatter.new
+ @formatter.register
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a failure message for an exception" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ @formatter.exception exc
+ @formatter.after @state
+ @formatter.finish
+ expect(@out).to match(/^1\)\ndescribe it ERROR$/)
+ end
+
+ it "prints a backtrace for an exception" do
+ exc = ExceptionState.new @state, nil, Exception.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.finish
+ expect(@out).to match(%r[path/to/some/file.rb:35:in method$])
+ end
+
+ it "prints a summary of elapsed time" do
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ @formatter.finish
+ expect(@out).to match(/^Finished in 2.0 seconds$/)
+ end
+
+ it "prints a tally of counts" do
+ expect(@tally).to receive(:format).and_return("1 example, 0 failures")
+ @formatter.finish
+ expect(@out).to match(/^1 example, 0 failures$/)
+ end
+
+ it "prints errors, backtraces, elapsed time, and tallies" do
+ exc = ExceptionState.new @state, nil, Exception.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.after @state
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ expect(@tally).to receive(:format).and_return("1 example, 0 failures")
+ @formatter.finish
+ expect(@out).to eq(%[E
+
+Finished in 2.0 seconds
+
+1)
+describe it ERROR
+Exception: broken:
+path/to/some/file.rb:35:in method
+
+1 example, 0 failures
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/yaml_spec.rb b/spec/mspec/spec/runner/formatters/yaml_spec.rb
new file mode 100644
index 0000000000..2e334fdbb9
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/yaml_spec.rb
@@ -0,0 +1,134 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/yaml'
+require 'mspec/runner/example'
+require 'mspec/helpers'
+
+RSpec.describe YamlFormatter, "#initialize" do
+ it "permits zero arguments" do
+ YamlFormatter.new
+ end
+
+ it "accepts one argument" do
+ YamlFormatter.new nil
+ end
+end
+
+RSpec.describe YamlFormatter, "#print" do
+ before :each do
+ $stdout = IOStub.new
+ @out = IOStub.new
+ allow(File).to receive(:open).and_return(@out)
+ @formatter = YamlFormatter.new "some/file"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "writes to $stdout if #switch has not been called" do
+ @formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ expect(@out).to eq("")
+ end
+
+ it "writes to the file passed to #initialize once #switch has been called" do
+ @formatter.switch
+ @formatter.print "begonias"
+ expect($stdout).to eq("")
+ expect(@out).to eq("begonias")
+ end
+
+ it "writes to $stdout once #switch is called if no file was passed to #initialize" do
+ formatter = YamlFormatter.new
+ formatter.switch
+ formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ expect(@out).to eq("")
+ end
+end
+
+RSpec.describe YamlFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ @counter = double("counter").as_null_object
+ allow(@tally).to receive(:counter).and_return(@counter)
+ allow(TallyAction).to receive(:new).and_return(@tally)
+
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ @out = tmp("YamlFormatter")
+
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+
+ @formatter = YamlFormatter.new(@out)
+ allow(@formatter).to receive(:backtrace).and_return("")
+ allow(MSpec).to receive(:register)
+ @formatter.register
+
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.after @state
+ end
+
+ after :each do
+ rm_r @out
+ end
+
+ it "calls #switch" do
+ expect(@formatter).to receive(:switch).and_call_original
+ @formatter.finish
+ end
+
+ it "outputs a failure message and backtrace" do
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "describe it ERROR"
+ expect(output).to include "MSpecExampleError: broken\\n"
+ expect(output).to include "path/to/some/file.rb:35:in method"
+ end
+
+ it "outputs an elapsed time" do
+ expect(@timer).to receive(:elapsed).and_return(4.2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "time: 4.2"
+ end
+
+ it "outputs a file count" do
+ expect(@counter).to receive(:files).and_return(3)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "files: 3"
+ end
+
+ it "outputs an example count" do
+ expect(@counter).to receive(:examples).and_return(3)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "examples: 3"
+ end
+
+ it "outputs an expectation count" do
+ expect(@counter).to receive(:expectations).and_return(9)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "expectations: 9"
+ end
+
+ it "outputs a failure count" do
+ expect(@counter).to receive(:failures).and_return(2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "failures: 2"
+ end
+
+ it "outputs an error count" do
+ expect(@counter).to receive(:errors).and_return(1)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "errors: 1"
+ end
+end
diff --git a/spec/mspec/spec/runner/mspec_spec.rb b/spec/mspec/spec/runner/mspec_spec.rb
new file mode 100644
index 0000000000..4af01806c0
--- /dev/null
+++ b/spec/mspec/spec/runner/mspec_spec.rb
@@ -0,0 +1,597 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/helpers/tmp'
+require 'mspec/helpers/fs'
+require 'mspec/matchers/base'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+
+RSpec.describe MSpec, ".register_files" do
+ it "records which spec files to run" do
+ MSpec.register_files [:one, :two, :three]
+ expect(MSpec.files_array).to eq([:one, :two, :three])
+ end
+end
+
+RSpec.describe MSpec, ".register_mode" do
+ before :each do
+ MSpec.clear_modes
+ end
+
+ it "sets execution mode flags" do
+ MSpec.register_mode :verify
+ expect(MSpec.retrieve(:modes)).to eq([:verify])
+ end
+end
+
+RSpec.describe MSpec, ".register_tags_patterns" do
+ it "records the patterns for generating a tag file from a spec file" do
+ MSpec.register_tags_patterns [[/spec\/ruby/, "spec/tags"], [/frozen/, "ruby"]]
+ expect(MSpec.retrieve(:tags_patterns)).to eq([[/spec\/ruby/, "spec/tags"], [/frozen/, "ruby"]])
+ end
+end
+
+RSpec.describe MSpec, ".register_exit" do
+ before :each do
+ MSpec.store :exit, 0
+ end
+
+ it "records the exit code" do
+ expect(MSpec.exit_code).to eq(0)
+ MSpec.register_exit 1
+ expect(MSpec.exit_code).to eq(1)
+ end
+end
+
+RSpec.describe MSpec, ".exit_code" do
+ it "retrieves the code set with .register_exit" do
+ MSpec.register_exit 99
+ expect(MSpec.exit_code).to eq(99)
+ end
+end
+
+RSpec.describe MSpec, ".store" do
+ it "records data for MSpec settings" do
+ MSpec.store :anything, :value
+ expect(MSpec.retrieve(:anything)).to eq(:value)
+ end
+end
+
+RSpec.describe MSpec, ".retrieve" do
+ it "accesses .store'd data" do
+ MSpec.register :retrieve, :first
+ expect(MSpec.retrieve(:retrieve)).to eq([:first])
+ end
+end
+
+RSpec.describe MSpec, ".randomize" do
+ it "sets the flag to randomize spec execution order" do
+ expect(MSpec.randomize?).to eq(false)
+ MSpec.randomize = true
+ expect(MSpec.randomize?).to eq(true)
+ MSpec.randomize = false
+ expect(MSpec.randomize?).to eq(false)
+ end
+end
+
+RSpec.describe MSpec, ".register" do
+ it "is the gateway behind the register(symbol, action) facility" do
+ MSpec.register :bonus, :first
+ MSpec.register :bonus, :second
+ MSpec.register :bonus, :second
+ expect(MSpec.retrieve(:bonus)).to eq([:first, :second])
+ end
+end
+
+RSpec.describe MSpec, ".unregister" do
+ it "is the gateway behind the unregister(symbol, actions) facility" do
+ MSpec.register :unregister, :first
+ MSpec.register :unregister, :second
+ MSpec.unregister :unregister, :second
+ expect(MSpec.retrieve(:unregister)).to eq([:first])
+ end
+end
+
+RSpec.describe MSpec, ".protect" do
+ before :each do
+ MSpec.clear_current
+ @cs = ContextState.new "C#m"
+ @cs.parent = MSpec.current
+
+ @es = ExampleState.new @cs, "runs"
+ ScratchPad.record Exception.new("Sharp!")
+ end
+
+ it "returns true if no exception is raised" do
+ expect(MSpec.protect("passed") { 1 }).to be_truthy
+ end
+
+ it "returns false if an exception is raised" do
+ expect(MSpec.protect("testing") { raise ScratchPad.recorded }).to be_falsey
+ end
+
+ it "rescues any exceptions raised when evaluating the block argument" do
+ MSpec.protect("") { raise Exception, "Now you see me..." }
+ end
+
+ it "does not rescue SystemExit" do
+ begin
+ MSpec.protect("") { exit 1 }
+ rescue SystemExit
+ ScratchPad.record :system_exit
+ end
+ expect(ScratchPad.recorded).to eq(:system_exit)
+ end
+
+ it "calls all the exception actions" do
+ exc = ExceptionState.new @es, "testing", ScratchPad.recorded
+ allow(ExceptionState).to receive(:new).and_return(exc)
+ action = double("exception")
+ expect(action).to receive(:exception).with(exc)
+ MSpec.register :exception, action
+ MSpec.protect("testing") { raise ScratchPad.recorded }
+ MSpec.unregister :exception, action
+ end
+
+ it "registers a non-zero exit code when an exception is raised" do
+ expect(MSpec).to receive(:register_exit).with(1)
+ MSpec.protect("testing") { raise ScratchPad.recorded }
+ end
+end
+
+RSpec.describe MSpec, ".register_current" do
+ before :each do
+ MSpec.clear_current
+ end
+
+ it "sets the value returned by MSpec.current" do
+ expect(MSpec.current).to be_nil
+ MSpec.register_current :a
+ expect(MSpec.current).to eq(:a)
+ end
+end
+
+RSpec.describe MSpec, ".clear_current" do
+ it "sets the value returned by MSpec.current to nil" do
+ MSpec.register_current :a
+ expect(MSpec.current).not_to be_nil
+ MSpec.clear_current
+ expect(MSpec.current).to be_nil
+ end
+end
+
+RSpec.describe MSpec, ".current" do
+ before :each do
+ MSpec.clear_current
+ end
+
+ it "returns nil if no ContextState has been registered" do
+ expect(MSpec.current).to be_nil
+ end
+
+ it "returns the most recently registered ContextState" do
+ first = ContextState.new ""
+ second = ContextState.new ""
+ MSpec.register_current first
+ expect(MSpec.current).to eq(first)
+ MSpec.register_current second
+ expect(MSpec.current).to eq(second)
+ end
+end
+
+RSpec.describe MSpec, ".actions" do
+ before :each do
+ MSpec.store :start, []
+ ScratchPad.record []
+ start_one = double("one")
+ allow(start_one).to receive(:start) { ScratchPad << :one }
+ start_two = double("two")
+ allow(start_two).to receive(:start) { ScratchPad << :two }
+ MSpec.register :start, start_one
+ MSpec.register :start, start_two
+ end
+
+ it "does not attempt to run any actions if none have been registered" do
+ MSpec.store :finish, nil
+ expect { MSpec.actions :finish }.not_to raise_error
+ end
+
+ it "runs each action registered as a start action" do
+ MSpec.actions :start
+ expect(ScratchPad.recorded).to eq([:one, :two])
+ end
+end
+
+RSpec.describe MSpec, ".mode?" do
+ before :each do
+ MSpec.clear_modes
+ end
+
+ it "returns true if the mode has been set" do
+ expect(MSpec.mode?(:verify)).to eq(false)
+ MSpec.register_mode :verify
+ expect(MSpec.mode?(:verify)).to eq(true)
+ end
+end
+
+RSpec.describe MSpec, ".clear_modes" do
+ it "clears all registered modes" do
+ MSpec.register_mode(:pretend)
+ MSpec.register_mode(:verify)
+
+ expect(MSpec.mode?(:pretend)).to eq(true)
+ expect(MSpec.mode?(:verify)).to eq(true)
+
+ MSpec.clear_modes
+
+ expect(MSpec.mode?(:pretend)).to eq(false)
+ expect(MSpec.mode?(:verify)).to eq(false)
+ end
+end
+
+RSpec.describe MSpec, ".guarded?" do
+ before :each do
+ MSpec.instance_variable_set :@guarded, []
+ end
+
+ it "returns false if no guard has run" do
+ expect(MSpec.guarded?).to eq(false)
+ end
+
+ it "returns true if a single guard has run" do
+ MSpec.guard
+ expect(MSpec.guarded?).to eq(true)
+ end
+
+ it "returns true if more than one guard has run" do
+ MSpec.guard
+ MSpec.guard
+ expect(MSpec.guarded?).to eq(true)
+ end
+
+ it "returns true until all guards have finished" do
+ MSpec.guard
+ MSpec.guard
+ expect(MSpec.guarded?).to eq(true)
+ MSpec.unguard
+ expect(MSpec.guarded?).to eq(true)
+ MSpec.unguard
+ expect(MSpec.guarded?).to eq(false)
+ end
+end
+
+RSpec.describe MSpec, ".describe" do
+ before :each do
+ MSpec.clear_current
+ @cs = ContextState.new ""
+ allow(ContextState).to receive(:new).and_return(@cs)
+ allow(MSpec).to receive(:current).and_return(nil)
+ allow(MSpec).to receive(:register_current)
+ end
+
+ it "creates a new ContextState for the block" do
+ expect(ContextState).to receive(:new).and_return(@cs)
+ MSpec.describe(Object) { }
+ end
+
+ it "accepts an optional second argument" do
+ expect(ContextState).to receive(:new).and_return(@cs)
+ MSpec.describe(Object, "msg") { }
+ end
+
+ it "registers the newly created ContextState" do
+ expect(MSpec).to receive(:register_current).with(@cs).twice
+ MSpec.describe(Object) { }
+ end
+
+ it "invokes the ContextState#describe method" do
+ expect(@cs).to receive(:describe)
+ MSpec.describe(Object, "msg") {}
+ end
+end
+
+RSpec.describe MSpec, ".process" do
+ before :each do
+ allow(MSpec).to receive(:files)
+ MSpec.store :start, []
+ MSpec.store :finish, []
+ allow(STDOUT).to receive(:puts)
+ end
+
+ it "prints the RUBY_DESCRIPTION" do
+ expect(STDOUT).to receive(:puts).with(RUBY_DESCRIPTION)
+ MSpec.process
+ end
+
+ it "calls all start actions" do
+ start = double("start")
+ allow(start).to receive(:start) { ScratchPad.record :start }
+ MSpec.register :start, start
+ MSpec.process
+ expect(ScratchPad.recorded).to eq(:start)
+ end
+
+ it "calls all finish actions" do
+ finish = double("finish")
+ allow(finish).to receive(:finish) { ScratchPad.record :finish }
+ MSpec.register :finish, finish
+ MSpec.process
+ expect(ScratchPad.recorded).to eq(:finish)
+ end
+
+ it "calls the files method" do
+ expect(MSpec).to receive(:files)
+ MSpec.process
+ end
+end
+
+RSpec.describe MSpec, ".files" do
+ before :each do
+ MSpec.store :load, []
+ MSpec.store :unload, []
+ MSpec.register_files [:one, :two, :three]
+ allow(Kernel).to receive(:load)
+ end
+
+ it "calls load actions before each file" do
+ load = double("load")
+ allow(load).to receive(:load) { ScratchPad.record :load }
+ MSpec.register :load, load
+ MSpec.files
+ expect(ScratchPad.recorded).to eq(:load)
+ end
+
+ it "shuffles the file list if .randomize? is true" do
+ MSpec.randomize = true
+ expect(MSpec).to receive(:shuffle)
+ MSpec.files
+ MSpec.randomize = false
+ end
+
+ it "registers the current file" do
+ load = double("load")
+ files = []
+ allow(load).to receive(:load) { files << MSpec.file }
+ MSpec.register :load, load
+ MSpec.files
+ expect(files).to eq([:one, :two, :three])
+ end
+end
+
+RSpec.describe MSpec, ".shuffle" do
+ before :each do
+ @base = (0..100).to_a
+ @list = @base.clone
+ MSpec.shuffle @list
+ end
+
+ it "does not alter the elements in the list" do
+ @base.each do |elt|
+ expect(@list).to include(elt)
+ end
+ end
+
+ it "changes the order of the list" do
+ # obviously, this spec has a certain probability
+ # of failing. If it fails, run it again.
+ expect(@list).not_to eq(@base)
+ end
+end
+
+RSpec.describe MSpec, ".tags_file" do
+ before :each do
+ MSpec.store :file, "path/to/spec/something/some_spec.rb"
+ MSpec.store :tags_patterns, nil
+ end
+
+ it "returns the default tags file for the current spec file" do
+ expect(MSpec.tags_file).to eq("path/to/spec/tags/something/some_tags.txt")
+ end
+
+ it "returns the tags file for the current spec file with custom tags_patterns" do
+ MSpec.register_tags_patterns [[/^(.*)\/spec/, '\1/tags'], [/_spec.rb/, "_tags.txt"]]
+ expect(MSpec.tags_file).to eq("path/to/tags/something/some_tags.txt")
+ end
+
+ it "performs multiple substitutions" do
+ MSpec.register_tags_patterns [
+ [%r(/spec/something/), "/spec/other/"],
+ [%r(/spec/), "/spec/tags/"],
+ [/_spec.rb/, "_tags.txt"]
+ ]
+ expect(MSpec.tags_file).to eq("path/to/spec/tags/other/some_tags.txt")
+ end
+
+ it "handles cases where no substitution is performed" do
+ MSpec.register_tags_patterns [[/nothing/, "something"]]
+ expect(MSpec.tags_file).to eq("path/to/spec/something/some_spec.rb")
+ end
+end
+
+RSpec.describe MSpec, ".read_tags" do
+ before :each do
+ allow(MSpec).to receive(:tags_file).and_return(File.dirname(__FILE__) + '/tags.txt')
+ end
+
+ it "returns a list of tag instances for matching tag names found" do
+ one = SpecTag.new "fail(broken):Some#method? works"
+ expect(MSpec.read_tags(["fail", "pass"])).to eq([one])
+ end
+
+ it "returns [] if no tags names match" do
+ expect(MSpec.read_tags("super")).to eq([])
+ end
+end
+
+RSpec.describe MSpec, ".read_tags" do
+ before :each do
+ @tag = SpecTag.new "fails:Some#method"
+ File.open(tmp("tags.txt", false), "w") do |f|
+ f.puts ""
+ f.puts @tag
+ f.puts ""
+ end
+ allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false))
+ end
+
+ it "does not return a tag object for empty lines" do
+ expect(MSpec.read_tags(["fails"])).to eq([@tag])
+ end
+end
+
+RSpec.describe MSpec, ".write_tags" do
+ before :each do
+ FileUtils.cp File.dirname(__FILE__) + "/tags.txt", tmp("tags.txt", false)
+ allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false))
+ @tag1 = SpecTag.new "check(broken):Tag#rewrite works"
+ @tag2 = SpecTag.new "broken:Tag#write_tags fails"
+ end
+
+ after :all do
+ rm_r tmp("tags.txt", false)
+ end
+
+ it "overwrites the tags in the tag file" do
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[fail(broken):Some#method? works
+incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+extended():\"Multi-line\\ntext\\ntag\"
+])
+ MSpec.write_tags [@tag1, @tag2]
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[check(broken):Tag#rewrite works
+broken:Tag#write_tags fails
+])
+ end
+end
+
+RSpec.describe MSpec, ".write_tag" do
+ before :each do
+ allow(FileUtils).to receive(:mkdir_p)
+ allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false))
+ @tag = SpecTag.new "fail(broken):Some#method works"
+ end
+
+ after :all do
+ rm_r tmp("tags.txt", false)
+ end
+
+ it "writes a tag to the tags file for the current spec file" do
+ MSpec.write_tag @tag
+ expect(IO.read(tmp("tags.txt", false))).to eq("fail(broken):Some#method works\n")
+ end
+
+ it "does not write a duplicate tag" do
+ File.open(tmp("tags.txt", false), "w") { |f| f.puts @tag }
+ MSpec.write_tag @tag
+ expect(IO.read(tmp("tags.txt", false))).to eq("fail(broken):Some#method works\n")
+ end
+end
+
+RSpec.describe MSpec, ".delete_tag" do
+ before :each do
+ FileUtils.cp File.dirname(__FILE__) + "/tags.txt", tmp("tags.txt", false)
+ allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false))
+ @tag = SpecTag.new "fail(Comments don't matter):Some#method? works"
+ end
+
+ after :each do
+ rm_r tmp("tags.txt", false)
+ end
+
+ it "deletes the tag if it exists" do
+ expect(MSpec.delete_tag(@tag)).to eq(true)
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+extended():\"Multi-line\\ntext\\ntag\"
+])
+ end
+
+ it "deletes a tag with escaped newlines" do
+ expect(MSpec.delete_tag(SpecTag.new('extended:"Multi-line\ntext\ntag"'))).to eq(true)
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[fail(broken):Some#method? works
+incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+])
+ end
+
+ it "does not change the tags file contents if the tag doesn't exist" do
+ @tag.tag = "failed"
+ expect(MSpec.delete_tag(@tag)).to eq(false)
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[fail(broken):Some#method? works
+incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+extended():\"Multi-line\\ntext\\ntag\"
+])
+ end
+
+ it "deletes the tag file if it is empty" do
+ expect(MSpec.delete_tag(@tag)).to eq(true)
+ expect(MSpec.delete_tag(SpecTag.new("incomplete:The#best method ever"))).to eq(true)
+ expect(MSpec.delete_tag(SpecTag.new("benchmark:The#fastest method today"))).to eq(true)
+ expect(MSpec.delete_tag(SpecTag.new('extended:"Multi-line\ntext\ntag"'))).to eq(true)
+ expect(File.exist?(tmp("tags.txt", false))).to eq(false)
+ end
+end
+
+RSpec.describe MSpec, ".delete_tags" do
+ before :each do
+ @tags = tmp("tags.txt", false)
+ FileUtils.cp File.dirname(__FILE__) + "/tags.txt", @tags
+ allow(MSpec).to receive(:tags_file).and_return(@tags)
+ end
+
+ it "deletes the tag file" do
+ MSpec.delete_tags
+ expect(File.exist?(@tags)).to be_falsey
+ end
+end
+
+RSpec.describe MSpec, ".expectation" do
+ it "sets the flag that an expectation has been reported" do
+ MSpec.clear_expectations
+ expect(MSpec.expectation?).to be_falsey
+ MSpec.expectation
+ expect(MSpec.expectation?).to be_truthy
+ end
+end
+
+RSpec.describe MSpec, ".expectation?" do
+ it "returns true if an expectation has been reported" do
+ MSpec.expectation
+ expect(MSpec.expectation?).to be_truthy
+ end
+
+ it "returns false if an expectation has not been reported" do
+ MSpec.clear_expectations
+ expect(MSpec.expectation?).to be_falsey
+ end
+end
+
+RSpec.describe MSpec, ".clear_expectations" do
+ it "clears the flag that an expectation has been reported" do
+ MSpec.expectation
+ expect(MSpec.expectation?).to be_truthy
+ MSpec.clear_expectations
+ expect(MSpec.expectation?).to be_falsey
+ end
+end
+
+RSpec.describe MSpec, ".register_shared" do
+ it "stores a shared ContextState by description" do
+ parent = ContextState.new "container"
+ state = ContextState.new "shared"
+ state.parent = parent
+ prc = lambda { }
+ state.describe(&prc)
+ MSpec.register_shared(state)
+ expect(MSpec.retrieve(:shared)["shared"]).to eq(state)
+ end
+end
+
+RSpec.describe MSpec, ".retrieve_shared" do
+ it "retrieves the shared ContextState matching description" do
+ state = ContextState.new ""
+ MSpec.retrieve(:shared)["shared"] = state
+ expect(MSpec.retrieve_shared(:shared)).to eq(state)
+ end
+end
diff --git a/spec/mspec/spec/runner/shared_spec.rb b/spec/mspec/spec/runner/shared_spec.rb
new file mode 100644
index 0000000000..153b8f0698
--- /dev/null
+++ b/spec/mspec/spec/runner/shared_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+require 'mspec/runner/shared'
+require 'mspec/runner/context'
+require 'mspec/runner/example'
+
+RSpec.describe Object, "#it_behaves_like" do
+ before :each do
+ ScratchPad.clear
+
+ MSpec.setup_env
+
+ @state = ContextState.new "Top level"
+ @state.instance_variable_set :@parsed, true
+ @state.singleton_class.send(:public, :it_behaves_like)
+
+ @shared = ContextState.new :shared_spec, :shared => true
+ allow(MSpec).to receive(:retrieve_shared).and_return(@shared)
+ end
+
+ it "creates @method set to the name of the aliased method" do
+ @shared.it("an example") { ScratchPad.record @method }
+ @state.it_behaves_like :shared_spec, :some_method
+ @state.process
+ expect(ScratchPad.recorded).to eq(:some_method)
+ end
+
+ it "creates @object if the passed object" do
+ object = Object.new
+ @shared.it("an example") { ScratchPad.record @object }
+ @state.it_behaves_like :shared_spec, :some_method, object
+ @state.process
+ expect(ScratchPad.recorded).to eq(object)
+ end
+
+ it "creates @object if the passed false" do
+ object = false
+ @shared.it("an example") { ScratchPad.record @object }
+ @state.it_behaves_like :shared_spec, :some_method, object
+ @state.process
+ expect(ScratchPad.recorded).to eq(object)
+ end
+
+ it "sends :it_should_behave_like" do
+ expect(@state).to receive(:it_should_behave_like)
+ @state.it_behaves_like :shared_spec, :some_method
+ end
+
+ describe "with multiple shared contexts" do
+ before :each do
+ @obj = Object.new
+ @obj2 = Object.new
+
+ @state2 = ContextState.new "Second top level"
+ @state2.instance_variable_set :@parsed, true
+ @state2.singleton_class.send(:public, :it_behaves_like)
+ end
+
+ it "ensures the shared spec state is distinct" do
+ @shared.it("an example") { ScratchPad.record [@method, @object] }
+
+ @state.it_behaves_like :shared_spec, :some_method, @obj
+
+ @state.process
+ expect(ScratchPad.recorded).to eq([:some_method, @obj])
+
+ @state2.it_behaves_like :shared_spec, :another_method, @obj2
+
+ @state2.process
+ expect(ScratchPad.recorded).to eq([:another_method, @obj2])
+ end
+
+ it "ensures the shared spec state is distinct for nested shared specs" do
+ nested = ContextState.new "nested context"
+ nested.instance_variable_set :@parsed, true
+ nested.parent = @shared
+
+ nested.it("another example") { ScratchPad.record [:shared, @method, @object] }
+
+ @state.it_behaves_like :shared_spec, :some_method, @obj
+
+ @state.process
+ expect(ScratchPad.recorded).to eq([:shared, :some_method, @obj])
+
+ @state2.it_behaves_like :shared_spec, :another_method, @obj2
+
+ @state2.process
+ expect(ScratchPad.recorded).to eq([:shared, :another_method, @obj2])
+ end
+ end
+end
diff --git a/spec/mspec/spec/runner/tag_spec.rb b/spec/mspec/spec/runner/tag_spec.rb
new file mode 100644
index 0000000000..bda9ac4280
--- /dev/null
+++ b/spec/mspec/spec/runner/tag_spec.rb
@@ -0,0 +1,123 @@
+require 'spec_helper'
+require 'mspec/runner/tag'
+
+RSpec.describe SpecTag do
+ it "accepts an optional string to parse into fields" do
+ tag = SpecTag.new "tag(comment):description"
+ expect(tag.tag).to eq("tag")
+ expect(tag.comment).to eq("comment")
+ expect(tag.description).to eq("description")
+ end
+end
+
+RSpec.describe SpecTag, "#parse" do
+ before :each do
+ @tag = SpecTag.new
+ end
+
+ it "accepts 'tag(comment):description'" do
+ @tag.parse "tag(I'm real):Some#method returns a value"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq("I'm real")
+ expect(@tag.description).to eq("Some#method returns a value")
+ end
+
+ it "accepts 'tag:description'" do
+ @tag.parse "tag:Another#method"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq(nil)
+ expect(@tag.description).to eq("Another#method")
+ end
+
+ it "accepts 'tag():description'" do
+ @tag.parse "tag():Another#method"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq(nil)
+ expect(@tag.description).to eq("Another#method")
+ end
+
+ it "accepts 'tag:'" do
+ @tag.parse "tag:"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq(nil)
+ expect(@tag.description).to eq("")
+ end
+
+ it "accepts 'tag(bug:555):Another#method'" do
+ @tag.parse "tag(bug:555):Another#method"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq("bug:555")
+ expect(@tag.description).to eq("Another#method")
+ end
+
+ it "accepts 'tag(http://someplace.com/neato):Another#method'" do
+ @tag.parse "tag(http://someplace.com/neato):Another#method"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq("http://someplace.com/neato")
+ expect(@tag.description).to eq("Another#method")
+ end
+
+ it "accepts 'tag(comment):\"Multi-line\\ntext\"'" do
+ @tag.parse 'tag(comment):"Multi-line\ntext"'
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq("comment")
+ expect(@tag.description).to eq("Multi-line\ntext")
+ end
+
+ it "ignores '#anything'" do
+ @tag.parse "# this could be a comment"
+ expect(@tag.tag).to eq(nil)
+ expect(@tag.comment).to eq(nil)
+ expect(@tag.description).to eq(nil)
+ end
+end
+
+RSpec.describe SpecTag, "#to_s" do
+ it "formats itself as 'tag(comment):description'" do
+ txt = "tag(comment):description"
+ tag = SpecTag.new txt
+ expect(tag.tag).to eq("tag")
+ expect(tag.comment).to eq("comment")
+ expect(tag.description).to eq("description")
+ expect(tag.to_s).to eq(txt)
+ end
+
+ it "formats itself as 'tag:description" do
+ txt = "tag:description"
+ tag = SpecTag.new txt
+ expect(tag.tag).to eq("tag")
+ expect(tag.comment).to eq(nil)
+ expect(tag.description).to eq("description")
+ expect(tag.to_s).to eq(txt)
+ end
+
+ it "formats itself as 'tag(comment):\"multi-line\\ntext\\ntag\"'" do
+ txt = 'tag(comment):"multi-line\ntext\ntag"'
+ tag = SpecTag.new txt
+ expect(tag.tag).to eq("tag")
+ expect(tag.comment).to eq("comment")
+ expect(tag.description).to eq("multi-line\ntext\ntag")
+ expect(tag.to_s).to eq(txt)
+ end
+end
+
+RSpec.describe SpecTag, "#==" do
+ it "returns true if the tags have the same fields" do
+ one = SpecTag.new "tag(this):unicorn"
+ two = SpecTag.new "tag(this):unicorn"
+ expect(one.==(two)).to eq(true)
+ expect([one].==([two])).to eq(true)
+ end
+end
+
+RSpec.describe SpecTag, "#unescape" do
+ it "replaces \\n by LF when the description is quoted" do
+ tag = SpecTag.new 'tag:"desc with\nnew line"'
+ expect(tag.description).to eq("desc with\nnew line")
+ end
+
+ it "does not replaces \\n by LF when the description is not quoted " do
+ tag = SpecTag.new 'tag:desc with\nnew line'
+ expect(tag.description).to eq("desc with\\nnew line")
+ end
+end
diff --git a/spec/mspec/spec/runner/tags.txt b/spec/mspec/spec/runner/tags.txt
new file mode 100644
index 0000000000..f4eb6ad034
--- /dev/null
+++ b/spec/mspec/spec/runner/tags.txt
@@ -0,0 +1,4 @@
+fail(broken):Some#method? works
+incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+extended():"Multi-line\ntext\ntag"
diff --git a/spec/mspec/spec/spec_helper.rb b/spec/mspec/spec/spec_helper.rb
new file mode 100644
index 0000000000..5cabfe5626
--- /dev/null
+++ b/spec/mspec/spec/spec_helper.rb
@@ -0,0 +1,70 @@
+RSpec.configure do |config|
+ config.disable_monkey_patching!
+ config.raise_errors_for_deprecations!
+end
+
+require 'mspec'
+
+# Remove this when MRI has intelligent warnings
+$VERBOSE = nil unless $VERBOSE
+
+class MOSConfig < Hash
+ def initialize
+ self[:loadpath] = []
+ self[:requires] = []
+ self[:flags] = []
+ self[:options] = []
+ self[:includes] = []
+ self[:excludes] = []
+ self[:patterns] = []
+ self[:xpatterns] = []
+ self[:tags] = []
+ self[:xtags] = []
+ self[:atags] = []
+ self[:astrings] = []
+ self[:target] = 'ruby'
+ self[:command] = nil
+ self[:ltags] = []
+ self[:files] = []
+ self[:launch] = []
+ end
+end
+
+def new_option
+ config = MOSConfig.new
+ return MSpecOptions.new("spec", 20, config), config
+end
+
+# Just to have an exception name output not be "Exception"
+class MSpecExampleError < Exception
+end
+
+def hide_deprecation_warnings
+ allow(MSpec).to receive(:deprecate)
+end
+
+def run_mspec(command, args)
+ cwd = Dir.pwd
+ command = " #{command}" unless command.start_with?('-')
+ cmd = "#{cwd}/bin/mspec#{command} -B spec/fixtures/config.mspec #{args}"
+ out = `#{cmd} 2>&1`
+ ret = $?
+ out = out.sub(/\A\$.+\n/, '') # Remove printed command line
+ out = out.sub(RUBY_DESCRIPTION, "RUBY_DESCRIPTION")
+ out = out.gsub(/\d+\.\d{6}/, "D.DDDDDD") # Specs total time
+ out = out.gsub(/\d{2}:\d{2}:\d{2}/, "00:00:00") # Progress bar time
+ out = out.gsub(cwd, "CWD")
+ return out, ret
+end
+
+def ensure_mspec_method(method)
+ file, _line = method.source_location
+ expect(file).to start_with(File.expand_path('../../lib/mspec', __FILE__ ))
+end
+
+PublicMSpecMatchers = Class.new {
+ include MSpecMatchers
+ public :raise_error
+}.new
+
+BACKTRACE_QUOTE = RUBY_VERSION >= "3.4" ? "'" : "`"
diff --git a/spec/mspec/spec/utils/deprecate_spec.rb b/spec/mspec/spec/utils/deprecate_spec.rb
new file mode 100644
index 0000000000..73eaf7d04e
--- /dev/null
+++ b/spec/mspec/spec/utils/deprecate_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+require 'mspec/utils/deprecate'
+
+RSpec.describe MSpec, "#deprecate" do
+ it "warns when using a deprecated method" do
+ warning = nil
+ allow($stderr).to receive(:puts) { |str| warning = str }
+ MSpec.deprecate(:some_method, :other_method)
+ expect(warning).to start_with(<<-EOS.chomp)
+
+some_method is deprecated, use other_method instead.
+from
+EOS
+ expect(warning).to include(__FILE__)
+ expect(warning).to include('8')
+ end
+end
diff --git a/spec/mspec/spec/utils/fixtures/this_file_raises.rb b/spec/mspec/spec/utils/fixtures/this_file_raises.rb
new file mode 100644
index 0000000000..8e37a587bf
--- /dev/null
+++ b/spec/mspec/spec/utils/fixtures/this_file_raises.rb
@@ -0,0 +1 @@
+raise "This is a BAD file"
diff --git a/spec/mspec/spec/utils/fixtures/this_file_raises2.rb b/spec/mspec/spec/utils/fixtures/this_file_raises2.rb
new file mode 100644
index 0000000000..8efc10199a
--- /dev/null
+++ b/spec/mspec/spec/utils/fixtures/this_file_raises2.rb
@@ -0,0 +1 @@
+raise "This is a BAD file 2"
diff --git a/spec/mspec/spec/utils/name_map_spec.rb b/spec/mspec/spec/utils/name_map_spec.rb
new file mode 100644
index 0000000000..a42dc9ffec
--- /dev/null
+++ b/spec/mspec/spec/utils/name_map_spec.rb
@@ -0,0 +1,187 @@
+require 'spec_helper'
+require 'mspec/utils/name_map'
+
+module NameMapSpecs
+ class A
+ A = self
+
+ def self.a; end
+ def a; end
+ def c; end
+
+ class B
+ def b; end
+ end
+ end
+
+ class Error
+ end
+
+ class Fixnum
+ def f; end
+ end
+
+ autoload :BadFile, "#{__dir__}/fixtures/this_file_raises.rb"
+ autoload :BadFile2, "#{__dir__}/fixtures/this_file_raises2.rb"
+
+ def self.n; end
+ def n; end
+end
+
+RSpec.describe NameMap, "#exception?" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "returns true if the constant is Errno" do
+ expect(@map.exception?("Errno")).to eq(true)
+ end
+
+ it "returns true if the constant is a kind of Exception" do
+ expect(@map.exception?("Errno::EBADF")).to eq(true)
+ expect(@map.exception?("LoadError")).to eq(true)
+ expect(@map.exception?("SystemExit")).to eq(true)
+ end
+
+ it "returns false if the constant is not a kind of Exception" do
+ expect(@map.exception?("NameMapSpecs::Error")).to eq(false)
+ expect(@map.exception?("NameMapSpecs")).to eq(false)
+ end
+
+ it "returns false if the constant does not exist" do
+ expect(@map.exception?("Nonexistent")).to eq(false)
+ end
+end
+
+RSpec.describe NameMap, "#class_or_module" do
+ before :each do
+ @map = NameMap.new true
+ end
+
+ it "returns the constant specified by the string" do
+ expect(@map.class_or_module("NameMapSpecs")).to eq(NameMapSpecs)
+ end
+
+ it "returns the constant specified by the 'A::B' string" do
+ expect(@map.class_or_module("NameMapSpecs::A")).to eq(NameMapSpecs::A)
+ end
+
+ it "returns nil if the constant is not a class or module" do
+ expect(@map.class_or_module("Float::MAX")).to eq(nil)
+ end
+
+ it "returns nil if the constant is in the set of excluded constants" do
+ excluded = %w[
+ MSpecScript
+ MkSpec
+ NameMap
+ ]
+
+ excluded.each do |const|
+ expect(@map.class_or_module(const)).to eq(nil)
+ end
+ end
+
+ it "returns nil if the constant does not exist" do
+ expect(@map.class_or_module("Heaven")).to eq(nil)
+ expect(@map.class_or_module("Hell")).to eq(nil)
+ expect(@map.class_or_module("Bush::Brain")).to eq(nil)
+ end
+
+ it "returns nil if accessing the constant raises RuntimeError" do
+ expect { NameMapSpecs::BadFile }.to raise_error(RuntimeError)
+ expect(@map.class_or_module("NameMapSpecs::BadFile")).to eq(nil)
+ end
+
+ it "returns nil if accessing the constant raises RuntimeError when not triggering the autoload before" do
+ expect(@map.class_or_module("NameMapSpecs::BadFile2")).to eq(nil)
+ end
+end
+
+RSpec.describe NameMap, "#dir_name" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "returns a directory name from the base name and constant" do
+ expect(@map.dir_name("NameMapSpecs", 'spec/core')).to eq('spec/core/namemapspecs')
+ end
+
+ it "returns a directory name from the components in the constants name" do
+ expect(@map.dir_name("NameMapSpecs::A", 'spec')).to eq('spec/namemapspecs/a')
+ expect(@map.dir_name("NameMapSpecs::A::B", 'spec')).to eq('spec/namemapspecs/a/b')
+ end
+
+ it "returns a directory name without 'class' for constants like TrueClass" do
+ expect(@map.dir_name("TrueClass", 'spec')).to eq('spec/true')
+ expect(@map.dir_name("FalseClass", 'spec')).to eq('spec/false')
+ end
+
+ it "returns 'exception' for the directory name of any Exception subclass" do
+ expect(@map.dir_name("SystemExit", 'spec')).to eq('spec/exception')
+ expect(@map.dir_name("Errno::EBADF", 'spec')).to eq('spec/exception')
+ end
+
+ it "returns 'class' for Class" do
+ expect(@map.dir_name("Class", 'spec')).to eq('spec/class')
+ end
+end
+
+# These specs do not cover all the mappings, but only describe how the
+# name is derived when the hash item maps to a single value, a hash with
+# a specific item, or a hash with a :default item.
+RSpec.describe NameMap, "#file_name" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "returns the name of the spec file based on the constant and method" do
+ expect(@map.file_name("[]=", "Array")).to eq("element_set_spec.rb")
+ end
+
+ it "returns the name of the spec file based on the special entry for the method" do
+ expect(@map.file_name("~", "Regexp")).to eq("match_spec.rb")
+ expect(@map.file_name("~", "Integer")).to eq("complement_spec.rb")
+ end
+
+ it "returns the name of the spec file based on the default entry for the method" do
+ expect(@map.file_name("<<", "NameMapSpecs")).to eq("append_spec.rb")
+ end
+
+ it "uses the last component of the constant to look up the method name" do
+ expect(@map.file_name("^", "NameMapSpecs::Integer")).to eq("bit_xor_spec.rb")
+ end
+end
+
+RSpec.describe NameMap, "#namespace" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "prepends the module to the constant name" do
+ expect(@map.namespace("SubModule", Integer)).to eq("SubModule::Integer")
+ end
+
+ it "does not prepend Object, Class, or Module to the constant name" do
+ expect(@map.namespace("Object", String)).to eq("String")
+ expect(@map.namespace("Module", Integer)).to eq("Integer")
+ expect(@map.namespace("Class", Float)).to eq("Float")
+ end
+end
+
+RSpec.describe NameMap, "#map" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "flattens an object hierarchy into a single Hash" do
+ expect(@map.map({}, [NameMapSpecs])).to eq({
+ "NameMapSpecs." => ["n"],
+ "NameMapSpecs#" => ["n"],
+ "NameMapSpecs::A." => ["a"],
+ "NameMapSpecs::A#" => ["a", "c"],
+ "NameMapSpecs::A::B#" => ["b"],
+ "NameMapSpecs::Fixnum#" => ["f"]
+ })
+ end
+end
diff --git a/spec/mspec/spec/utils/options_spec.rb b/spec/mspec/spec/utils/options_spec.rb
new file mode 100644
index 0000000000..2e3925f579
--- /dev/null
+++ b/spec/mspec/spec/utils/options_spec.rb
@@ -0,0 +1,1302 @@
+require 'spec_helper'
+require 'mspec/utils/options'
+require 'mspec/version'
+require 'mspec/guards/guard'
+require 'mspec/runner/mspec'
+require 'mspec/runner/formatters'
+
+RSpec.describe MSpecOption, ".new" do
+ before :each do
+ @opt = MSpecOption.new("-a", "--bdc", "ARG", "desc", :block)
+ end
+
+ it "sets the short attribute" do
+ expect(@opt.short).to eq("-a")
+ end
+
+ it "sets the long attribute" do
+ expect(@opt.long).to eq("--bdc")
+ end
+
+ it "sets the arg attribute" do
+ expect(@opt.arg).to eq("ARG")
+ end
+
+ it "sets the description attribute" do
+ expect(@opt.description).to eq("desc")
+ end
+
+ it "sets the block attribute" do
+ expect(@opt.block).to eq(:block)
+ end
+end
+
+RSpec.describe MSpecOption, "#arg?" do
+ it "returns true if arg attribute is not nil" do
+ expect(MSpecOption.new(nil, nil, "ARG", nil, nil).arg?).to be_truthy
+ end
+
+ it "returns false if arg attribute is nil" do
+ expect(MSpecOption.new(nil, nil, nil, nil, nil).arg?).to be_falsey
+ end
+end
+
+RSpec.describe MSpecOption, "#match?" do
+ before :each do
+ @opt = MSpecOption.new("-a", "--bdc", "ARG", "desc", :block)
+ end
+
+ it "returns true if the argument matches the short option" do
+ expect(@opt.match?("-a")).to be_truthy
+ end
+
+ it "returns true if the argument matches the long option" do
+ expect(@opt.match?("--bdc")).to be_truthy
+ end
+
+ it "returns false if the argument matches neither the short nor long option" do
+ expect(@opt.match?("-b")).to be_falsey
+ expect(@opt.match?("-abdc")).to be_falsey
+ end
+end
+
+RSpec.describe MSpecOptions, ".new" do
+ before :each do
+ @opt = MSpecOptions.new("cmd", 20, :config)
+ end
+
+ it "sets the banner attribute" do
+ expect(@opt.banner).to eq("cmd")
+ end
+
+ it "sets the config attribute" do
+ expect(@opt.config).to eq(:config)
+ end
+
+ it "sets the width attribute" do
+ expect(@opt.width).to eq(20)
+ end
+
+ it "sets the default width attribute" do
+ expect(MSpecOptions.new.width).to eq(30)
+ end
+end
+
+RSpec.describe MSpecOptions, "#on" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "adds a short option" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "adds a short option taking an argument" do
+ expect(@opt).to receive(:add).with("-a", nil, "ARG", "desc", nil)
+ @opt.on("-a", "ARG", "desc")
+ end
+
+ it "adds a long option" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "adds a long option taking an argument" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "adds a short and long option" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "adds a short and long option taking an argument" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "raises MSpecOptions::OptionError if pass less than 2 arguments" do
+ expect { @opt.on }.to raise_error(MSpecOptions::OptionError)
+ expect { @opt.on "" }.to raise_error(MSpecOptions::OptionError)
+ end
+end
+
+RSpec.describe MSpecOptions, "#add" do
+ before :each do
+ @opt = MSpecOptions.new "cmd", 20
+ @prc = lambda { }
+ end
+
+ it "adds documentation for an option" do
+ expect(@opt).to receive(:doc).with(" -t, --typo ARG Correct typo ARG")
+ @opt.add("-t", "--typo", "ARG", "Correct typo ARG", @prc)
+ end
+
+ it "leaves spaces in the documentation for a missing short option" do
+ expect(@opt).to receive(:doc).with(" --typo ARG Correct typo ARG")
+ @opt.add(nil, "--typo", "ARG", "Correct typo ARG", @prc)
+ end
+
+ it "handles a short option with argument but no long argument" do
+ expect(@opt).to receive(:doc).with(" -t ARG Correct typo ARG")
+ @opt.add("-t", nil, "ARG", "Correct typo ARG", @prc)
+ end
+
+ it "registers an option" do
+ option = MSpecOption.new "-t", "--typo", "ARG", "Correct typo ARG", @prc
+ expect(MSpecOption).to receive(:new).with(
+ "-t", "--typo", "ARG", "Correct typo ARG", @prc).and_return(option)
+ @opt.add("-t", "--typo", "ARG", "Correct typo ARG", @prc)
+ expect(@opt.options).to eq([option])
+ end
+end
+
+RSpec.describe MSpecOptions, "#match?" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "returns the MSpecOption instance matching the argument" do
+ @opt.on "-a", "--abdc", "desc"
+ option = @opt.match? "-a"
+ expect(@opt.match?("--abdc")).to be(option)
+ expect(option).to be_kind_of(MSpecOption)
+ expect(option.short).to eq("-a")
+ expect(option.long).to eq("--abdc")
+ expect(option.description).to eq("desc")
+ end
+end
+
+RSpec.describe MSpecOptions, "#process" do
+ before :each do
+ @opt = MSpecOptions.new
+ ScratchPad.clear
+ end
+
+ it "calls the on_extra block if the argument does not match any option" do
+ @opt.on_extra { ScratchPad.record :extra }
+ @opt.process ["-a"], "-a", "-a", nil
+ expect(ScratchPad.recorded).to eq(:extra)
+ end
+
+ it "returns the matching option" do
+ @opt.on "-a", "ARG", "desc"
+ option = @opt.process [], "-a", "-a", "ARG"
+ expect(option).to be_kind_of(MSpecOption)
+ expect(option.short).to eq("-a")
+ expect(option.arg).to eq("ARG")
+ expect(option.description).to eq("desc")
+ end
+
+ it "raises an MSpecOptions::ParseError if arg is nil and there are no more entries in argv" do
+ @opt.on "-a", "ARG", "desc"
+ expect { @opt.process [], "-a", "-a", nil }.to raise_error(MSpecOptions::ParseError)
+ end
+
+ it "fetches the argument for the option from argv if arg is nil" do
+ @opt.on("-a", "ARG", "desc") { |o| ScratchPad.record o }
+ @opt.process ["ARG"], "-a", "-a", nil
+ expect(ScratchPad.recorded).to eq("ARG")
+ end
+
+ it "calls the option's block" do
+ @opt.on("-a", "ARG", "desc") { ScratchPad.record :option }
+ @opt.process [], "-a", "-a", "ARG"
+ expect(ScratchPad.recorded).to eq(:option)
+ end
+
+ it "does not call the option's block if it is nil" do
+ @opt.on "-a", "ARG", "desc"
+ expect { @opt.process [], "-a", "-a", "ARG" }.not_to raise_error
+ end
+end
+
+RSpec.describe MSpecOptions, "#split" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "breaks a string at the nth character" do
+ opt, arg, rest = @opt.split "-bdc", 2
+ expect(opt).to eq("-b")
+ expect(arg).to eq("dc")
+ expect(rest).to eq("dc")
+ end
+
+ it "returns nil for arg if there are no characters left" do
+ opt, arg, rest = @opt.split "-b", 2
+ expect(opt).to eq("-b")
+ expect(arg).to eq(nil)
+ expect(rest).to eq("")
+ end
+end
+
+RSpec.describe MSpecOptions, "#parse" do
+ before :each do
+ @opt = MSpecOptions.new
+ @prc = lambda { ScratchPad.record :parsed }
+ @arg_prc = lambda { |o| ScratchPad.record [:parsed, o] }
+ ScratchPad.clear
+ end
+
+ it "parses a short option" do
+ @opt.on "-a", "desc", &@prc
+ @opt.parse ["-a"]
+ expect(ScratchPad.recorded).to eq(:parsed)
+ end
+
+ it "parse a long option" do
+ @opt.on "--abdc", "desc", &@prc
+ @opt.parse ["--abdc"]
+ expect(ScratchPad.recorded).to eq(:parsed)
+ end
+
+ it "parses a short option group" do
+ @opt.on "-a", "ARG", "desc", &@arg_prc
+ @opt.parse ["-a", "ARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a short option with an argument" do
+ @opt.on "-a", "ARG", "desc", &@arg_prc
+ @opt.parse ["-a", "ARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a short option with connected argument" do
+ @opt.on "-a", "ARG", "desc", &@arg_prc
+ @opt.parse ["-aARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a long option with an argument" do
+ @opt.on "--abdc", "ARG", "desc", &@arg_prc
+ @opt.parse ["--abdc", "ARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a long option with an '=' argument" do
+ @opt.on "--abdc", "ARG", "desc", &@arg_prc
+ @opt.parse ["--abdc=ARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a short option group with the final option taking an argument" do
+ ScratchPad.record []
+ @opt.on("-a", "desc") { |o| ScratchPad << :a }
+ @opt.on("-b", "ARG", "desc") { |o| ScratchPad << [:b, o] }
+ @opt.parse ["-ab", "ARG"]
+ expect(ScratchPad.recorded).to eq([:a, [:b, "ARG"]])
+ end
+
+ it "parses a short option group with a connected argument" do
+ ScratchPad.record []
+ @opt.on("-a", "desc") { |o| ScratchPad << :a }
+ @opt.on("-b", "ARG", "desc") { |o| ScratchPad << [:b, o] }
+ @opt.on("-c", "desc") { |o| ScratchPad << :c }
+ @opt.parse ["-acbARG"]
+ expect(ScratchPad.recorded).to eq([:a, :c, [:b, "ARG"]])
+ end
+
+ it "returns the unprocessed entries" do
+ @opt.on "-a", "ARG", "desc", &@arg_prc
+ expect(@opt.parse(["abdc", "-a", "ilny"])).to eq(["abdc"])
+ end
+
+ it "calls the on_extra handler with unrecognized options" do
+ ScratchPad.record []
+ @opt.on_extra { |o| ScratchPad << o }
+ @opt.on "-a", "desc"
+ @opt.parse ["-a", "-b"]
+ expect(ScratchPad.recorded).to eq(["-b"])
+ end
+
+ it "does not attempt to call the block if it is nil" do
+ @opt.on "-a", "ARG", "desc"
+ expect(@opt.parse(["-a", "ARG"])).to eq([])
+ end
+
+ it "raises MSpecOptions::ParseError if passed an unrecognized option" do
+ expect(@opt).to receive(:raise).with(MSpecOptions::ParseError, an_instance_of(String))
+ allow(@opt).to receive(:puts)
+ allow(@opt).to receive(:exit)
+ @opt.parse "-u"
+ end
+end
+
+RSpec.describe MSpecOptions, "#banner=" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "sets the banner attribute" do
+ expect(@opt.banner).to eq("")
+ @opt.banner = "banner"
+ expect(@opt.banner).to eq("banner")
+ end
+end
+
+RSpec.describe MSpecOptions, "#width=" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "sets the width attribute" do
+ expect(@opt.width).to eq(30)
+ @opt.width = 20
+ expect(@opt.width).to eq(20)
+ end
+end
+
+RSpec.describe MSpecOptions, "#config=" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "sets the config attribute" do
+ expect(@opt.config).to be_nil
+ @opt.config = :config
+ expect(@opt.config).to eq(:config)
+ end
+end
+
+RSpec.describe MSpecOptions, "#doc" do
+ before :each do
+ @opt = MSpecOptions.new "command"
+ end
+
+ it "adds text to be displayed with #to_s" do
+ @opt.doc "Some message"
+ @opt.doc "Another message"
+ expect(@opt.to_s).to eq <<-EOD
+command
+
+Some message
+Another message
+EOD
+ end
+end
+
+RSpec.describe MSpecOptions, "#version" do
+ before :each do
+ @opt = MSpecOptions.new
+ ScratchPad.clear
+ end
+
+ it "installs a basic -v, --version option" do
+ expect(@opt).to receive(:puts)
+ expect(@opt).to receive(:exit)
+ @opt.version "1.0.0"
+ @opt.parse "-v"
+ end
+
+ it "accepts a block instead of using the default block" do
+ @opt.version("1.0.0") { |o| ScratchPad.record :version }
+ @opt.parse "-v"
+ expect(ScratchPad.recorded).to eq(:version)
+ end
+end
+
+RSpec.describe MSpecOptions, "#help" do
+ before :each do
+ @opt = MSpecOptions.new
+ ScratchPad.clear
+ end
+
+ it "installs a basic -h, --help option" do
+ expect(@opt).to receive(:puts)
+ expect(@opt).to receive(:exit).with(1)
+ @opt.help
+ @opt.parse "-h"
+ end
+
+ it "accepts a block instead of using the default block" do
+ @opt.help { |o| ScratchPad.record :help }
+ @opt.parse "-h"
+ expect(ScratchPad.recorded).to eq(:help)
+ end
+end
+
+RSpec.describe MSpecOptions, "#on_extra" do
+ before :each do
+ @opt = MSpecOptions.new
+ ScratchPad.clear
+ end
+
+ it "registers a block to be called when an option is not recognized" do
+ @opt.on_extra { ScratchPad.record :extra }
+ @opt.parse "-g"
+ expect(ScratchPad.recorded).to eq(:extra)
+ end
+end
+
+RSpec.describe MSpecOptions, "#to_s" do
+ before :each do
+ @opt = MSpecOptions.new "command"
+ end
+
+ it "returns the banner and descriptive strings for all registered options" do
+ @opt.on "-t", "--this ARG", "Adds this ARG to the list"
+ expect(@opt.to_s).to eq <<-EOD
+command
+
+ -t, --this ARG Adds this ARG to the list
+EOD
+ end
+end
+
+RSpec.describe "The -B, --config FILE option" do
+ before :each do
+ @options, @config = new_option
+ end
+
+ it "is enabled with #configure { }" do
+ expect(@options).to receive(:on).with("-B", "--config", "FILE",
+ an_instance_of(String))
+ @options.configure {}
+ end
+
+ it "calls the passed block" do
+ ["-B", "--config"].each do |opt|
+ ScratchPad.clear
+
+ @options.configure { |x| ScratchPad.record x }
+ @options.parse [opt, "file"]
+ expect(ScratchPad.recorded).to eq("file")
+ end
+ end
+end
+
+RSpec.describe "The -C, --chdir DIR option" do
+ before :each do
+ @options, @config = new_option
+ @options.chdir
+ end
+
+ it "is enabled with #chdir" do
+ expect(@options).to receive(:on).with("-C", "--chdir", "DIR",
+ an_instance_of(String))
+ @options.chdir
+ end
+
+ it "changes the working directory to DIR" do
+ expect(Dir).to receive(:chdir).with("dir").twice
+ ["-C", "--chdir"].each do |opt|
+ @options.parse [opt, "dir"]
+ end
+ end
+end
+
+RSpec.describe "The --prefix STR option" do
+ before :each do
+ @options, @config = new_option
+ end
+
+ it "is enabled with #prefix" do
+ expect(@options).to receive(:on).with("--prefix", "STR",
+ an_instance_of(String))
+ @options.prefix
+ end
+
+ it "sets the prefix config value" do
+ @options.prefix
+ @options.parse ["--prefix", "some/dir"]
+ expect(@config[:prefix]).to eq("some/dir")
+ end
+end
+
+RSpec.describe "The -t, --target TARGET option" do
+ before :each do
+ @options, @config = new_option
+ @options.targets
+ end
+
+ it "is enabled with #targets" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-t", "--target", "TARGET",
+ an_instance_of(String))
+ @options.targets
+ end
+
+ it "sets the target to 'ruby' and flags to verbose with TARGET 'r' or 'ruby'" do
+ ["-t", "--target"].each do |opt|
+ ["r", "ruby"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("ruby")
+ end
+ end
+ end
+
+ it "sets the target to 'jruby' with TARGET 'j' or 'jruby'" do
+ ["-t", "--target"].each do |opt|
+ ["j", "jruby"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("jruby")
+ end
+ end
+ end
+
+ it "sets the target to 'shotgun/rubinius' with TARGET 'x' or 'rubinius'" do
+ ["-t", "--target"].each do |opt|
+ ["x", "rubinius"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("./bin/rbx")
+ end
+ end
+ end
+
+ it "set the target to 'rbx' with TARGET 'rbx'" do
+ ["-t", "--target"].each do |opt|
+ ["X", "rbx"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("rbx")
+ end
+ end
+ end
+
+ it "sets the target to 'maglev' with TARGET 'm' or 'maglev'" do
+ ["-t", "--target"].each do |opt|
+ ["m", "maglev"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("maglev-ruby")
+ end
+ end
+ end
+
+ it "sets the target to 'topaz' with TARGET 't' or 'topaz'" do
+ ["-t", "--target"].each do |opt|
+ ["t", "topaz"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("topaz")
+ end
+ end
+ end
+
+ it "sets the target to TARGET" do
+ ["-t", "--target"].each do |opt|
+ @config[:target] = nil
+ @options.parse [opt, "whateva"]
+ expect(@config[:target]).to eq("whateva")
+ end
+ end
+end
+
+RSpec.describe "The -T, --target-opt OPT option" do
+ before :each do
+ @options, @config = new_option
+ @options.targets
+ end
+
+ it "is enabled with #targets" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-T", "--target-opt", "OPT",
+ an_instance_of(String))
+ @options.targets
+ end
+
+ it "adds OPT to flags" do
+ ["-T", "--target-opt"].each do |opt|
+ @config[:flags].delete "--whateva"
+ @options.parse [opt, "--whateva"]
+ expect(@config[:flags]).to include("--whateva")
+ end
+ end
+end
+
+RSpec.describe "The -I, --include DIR option" do
+ before :each do
+ @options, @config = new_option
+ @options.targets
+ end
+
+ it "is enabled with #targets" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-I", "--include", "DIR",
+ an_instance_of(String))
+ @options.targets
+ end
+
+ it "add DIR to the load path" do
+ ["-I", "--include"].each do |opt|
+ @config[:loadpath].delete "-Ipackage"
+ @options.parse [opt, "package"]
+ expect(@config[:loadpath]).to include("-Ipackage")
+ end
+ end
+end
+
+RSpec.describe "The -r, --require LIBRARY option" do
+ before :each do
+ @options, @config = new_option
+ @options.targets
+ end
+
+ it "is enabled with #targets" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-r", "--require", "LIBRARY",
+ an_instance_of(String))
+ @options.targets
+ end
+
+ it "adds LIBRARY to the requires list" do
+ ["-r", "--require"].each do |opt|
+ @config[:requires].delete "-rlibrick"
+ @options.parse [opt, "librick"]
+ expect(@config[:requires]).to include("-rlibrick")
+ end
+ end
+end
+
+RSpec.describe "The -f, --format FORMAT option" do
+ before :each do
+ @options, @config = new_option
+ @options.formatters
+ end
+
+ it "is enabled with #formatters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-f", "--format", "FORMAT",
+ an_instance_of(String))
+ @options.formatters
+ end
+
+ it "sets the SpecdocFormatter with FORMAT 's' or 'specdoc'" do
+ ["-f", "--format"].each do |opt|
+ ["s", "specdoc"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(SpecdocFormatter)
+ end
+ end
+ end
+
+ it "sets the HtmlFormatter with FORMAT 'h' or 'html'" do
+ ["-f", "--format"].each do |opt|
+ ["h", "html"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(HtmlFormatter)
+ end
+ end
+ end
+
+ it "sets the DottedFormatter with FORMAT 'd', 'dot' or 'dotted'" do
+ ["-f", "--format"].each do |opt|
+ ["d", "dot", "dotted"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(DottedFormatter)
+ end
+ end
+ end
+
+ it "sets the DescribeFormatter with FORMAT 'b' or 'describe'" do
+ ["-f", "--format"].each do |opt|
+ ["b", "describe"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(DescribeFormatter)
+ end
+ end
+ end
+
+ it "sets the FileFormatter with FORMAT 'f', 'file'" do
+ ["-f", "--format"].each do |opt|
+ ["f", "file"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(FileFormatter)
+ end
+ end
+ end
+
+ it "sets the UnitdiffFormatter with FORMAT 'u', 'unit', or 'unitdiff'" do
+ ["-f", "--format"].each do |opt|
+ ["u", "unit", "unitdiff"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(UnitdiffFormatter)
+ end
+ end
+ end
+
+ it "sets the SummaryFormatter with FORMAT 'm' or 'summary'" do
+ ["-f", "--format"].each do |opt|
+ ["m", "summary"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(SummaryFormatter)
+ end
+ end
+ end
+
+ it "sets the SpinnerFormatter with FORMAT 'a', '*', or 'spin'" do
+ ["-f", "--format"].each do |opt|
+ ["a", "*", "spin"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(SpinnerFormatter)
+ end
+ end
+ end
+
+ it "sets the MethodFormatter with FORMAT 't' or 'method'" do
+ ["-f", "--format"].each do |opt|
+ ["t", "method"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(MethodFormatter)
+ end
+ end
+ end
+
+ it "sets the YamlFormatter with FORMAT 'y' or 'yaml'" do
+ ["-f", "--format"].each do |opt|
+ ["y", "yaml"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(YamlFormatter)
+ end
+ end
+ end
+
+ it "sets the JUnitFormatter with FORMAT 'j' or 'junit'" do
+ ["-f", "--format"].each do |opt|
+ ["j", "junit"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(JUnitFormatter)
+ end
+ end
+ end
+end
+
+RSpec.describe "The -o, --output FILE option" do
+ before :each do
+ @options, @config = new_option
+ @options.formatters
+ end
+
+ it "is enabled with #formatters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-o", "--output", "FILE",
+ an_instance_of(String))
+ @options.formatters
+ end
+
+ it "sets the output to FILE" do
+ ["-o", "--output"].each do |opt|
+ @config[:output] = nil
+ @options.parse [opt, "some/file"]
+ expect(@config[:output]).to eq("some/file")
+ end
+ end
+end
+
+RSpec.describe "The -e, --example STR" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-e", "--example", "STR",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds STR to the includes list" do
+ ["-e", "--example"].each do |opt|
+ @config[:includes] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:includes]).to include("this spec")
+ end
+ end
+end
+
+RSpec.describe "The -E, --exclude STR" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-E", "--exclude", "STR",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds STR to the excludes list" do
+ ["-E", "--exclude"].each do |opt|
+ @config[:excludes] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:excludes]).to include("this spec")
+ end
+ end
+end
+
+RSpec.describe "The -p, --pattern PATTERN" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-p", "--pattern", "PATTERN",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds PATTERN to the included patterns list" do
+ ["-p", "--pattern"].each do |opt|
+ @config[:patterns] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:patterns]).to include(/this spec/)
+ end
+ end
+end
+
+RSpec.describe "The -P, --excl-pattern PATTERN" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-P", "--excl-pattern", "PATTERN",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds PATTERN to the excluded patterns list" do
+ ["-P", "--excl-pattern"].each do |opt|
+ @config[:xpatterns] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:xpatterns]).to include(/this spec/)
+ end
+ end
+end
+
+RSpec.describe "The -g, --tag TAG" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-g", "--tag", "TAG",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds TAG to the included tags list" do
+ ["-g", "--tag"].each do |opt|
+ @config[:tags] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:tags]).to include("this spec")
+ end
+ end
+end
+
+RSpec.describe "The -G, --excl-tag TAG" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-G", "--excl-tag", "TAG",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds TAG to the excluded tags list" do
+ ["-G", "--excl-tag"].each do |opt|
+ @config[:xtags] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:xtags]).to include("this spec")
+ end
+ end
+end
+
+RSpec.describe "The -w, --profile FILE option" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-w", "--profile", "FILE",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds FILE to the included profiles list" do
+ ["-w", "--profile"].each do |opt|
+ @config[:profiles] = []
+ @options.parse [opt, "spec/profiles/rails.yaml"]
+ expect(@config[:profiles]).to include("spec/profiles/rails.yaml")
+ end
+ end
+end
+
+RSpec.describe "The -W, --excl-profile FILE option" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-W", "--excl-profile", "FILE",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds FILE to the excluded profiles list" do
+ ["-W", "--excl-profile"].each do |opt|
+ @config[:xprofiles] = []
+ @options.parse [opt, "spec/profiles/rails.yaml"]
+ expect(@config[:xprofiles]).to include("spec/profiles/rails.yaml")
+ end
+ end
+end
+
+RSpec.describe "The -Z, --dry-run option" do
+ before :each do
+ @options, @config = new_option
+ @options.pretend
+ end
+
+ it "is enabled with #pretend" do
+ expect(@options).to receive(:on).with("-Z", "--dry-run", an_instance_of(String))
+ @options.pretend
+ end
+
+ it "registers the MSpec pretend mode" do
+ expect(MSpec).to receive(:register_mode).with(:pretend).twice
+ ["-Z", "--dry-run"].each do |opt|
+ @options.parse opt
+ end
+ end
+end
+
+RSpec.describe "The --unguarded option" do
+ before :each do
+ @options, @config = new_option
+ @options.unguarded
+ end
+
+ it "is enabled with #unguarded" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--unguarded", an_instance_of(String))
+ @options.unguarded
+ end
+
+ it "registers the MSpec unguarded mode" do
+ expect(MSpec).to receive(:register_mode).with(:unguarded)
+ @options.parse "--unguarded"
+ end
+end
+
+RSpec.describe "The --no-ruby_guard option" do
+ before :each do
+ @options, @config = new_option
+ @options.unguarded
+ end
+
+ it "is enabled with #unguarded" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--no-ruby_bug", an_instance_of(String))
+ @options.unguarded
+ end
+
+ it "registers the MSpec no_ruby_bug mode" do
+ expect(MSpec).to receive(:register_mode).with(:no_ruby_bug)
+ @options.parse "--no-ruby_bug"
+ end
+end
+
+RSpec.describe "The -H, --random option" do
+ before :each do
+ @options, @config = new_option
+ @options.randomize
+ end
+
+ it "is enabled with #randomize" do
+ expect(@options).to receive(:on).with("-H", "--random", an_instance_of(String))
+ @options.randomize
+ end
+
+ it "registers the MSpec randomize mode" do
+ expect(MSpec).to receive(:randomize=).twice
+ ["-H", "--random"].each do |opt|
+ @options.parse opt
+ end
+ end
+end
+
+RSpec.describe "The -R, --repeat option" do
+ before :each do
+ @options, @config = new_option
+ @options.repeat
+ end
+
+ it "is enabled with #repeat" do
+ expect(@options).to receive(:on).with("-R", "--repeat", "NUMBER", an_instance_of(String))
+ @options.repeat
+ end
+
+ it "registers the MSpec repeat mode" do
+ ["-R", "--repeat"].each do |opt|
+ MSpec.repeat = 1
+ @options.parse [opt, "10"]
+ repeat_count = 0
+ MSpec.repeat do
+ repeat_count += 1
+ end
+ expect(repeat_count).to eq(10)
+ end
+ end
+end
+
+RSpec.describe "The -V, --verbose option" do
+ before :each do
+ @options, @config = new_option
+ @options.verbose
+ end
+
+ it "is enabled with #verbose" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-V", "--verbose", an_instance_of(String))
+ @options.verbose
+ end
+
+ it "registers a verbose output object with MSpec" do
+ expect(MSpec).to receive(:register).with(:start, anything()).twice
+ expect(MSpec).to receive(:register).with(:load, anything()).twice
+ ["-V", "--verbose"].each do |opt|
+ @options.parse opt
+ end
+ end
+end
+
+RSpec.describe "The -m, --marker MARKER option" do
+ before :each do
+ @options, @config = new_option
+ @options.verbose
+ end
+
+ it "is enabled with #verbose" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-m", "--marker", "MARKER",
+ an_instance_of(String))
+ @options.verbose
+ end
+
+ it "registers a marker output object with MSpec" do
+ expect(MSpec).to receive(:register).with(:load, anything()).twice
+ ["-m", "--marker"].each do |opt|
+ @options.parse [opt, ","]
+ end
+ end
+end
+
+RSpec.describe "The --int-spec option" do
+ before :each do
+ @options, @config = new_option
+ @options.interrupt
+ end
+
+ it "is enabled with #interrupt" do
+ expect(@options).to receive(:on).with("--int-spec", an_instance_of(String))
+ @options.interrupt
+ end
+
+ it "sets the abort config option to false to only abort the running spec with ^C" do
+ @config[:abort] = true
+ @options.parse "--int-spec"
+ expect(@config[:abort]).to eq(false)
+ end
+end
+
+RSpec.describe "The -Y, --verify option" do
+ before :each do
+ @options, @config = new_option
+ @options.verify
+ end
+
+ it "is enabled with #interrupt" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-Y", "--verify", an_instance_of(String))
+ @options.verify
+ end
+
+ it "sets the MSpec mode to :verify" do
+ expect(MSpec).to receive(:register_mode).with(:verify).twice
+ ["-Y", "--verify"].each do |m|
+ @options.parse m
+ end
+ end
+end
+
+RSpec.describe "The -O, --report option" do
+ before :each do
+ @options, @config = new_option
+ @options.verify
+ end
+
+ it "is enabled with #interrupt" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-O", "--report", an_instance_of(String))
+ @options.verify
+ end
+
+ it "sets the MSpec mode to :report" do
+ expect(MSpec).to receive(:register_mode).with(:report).twice
+ ["-O", "--report"].each do |m|
+ @options.parse m
+ end
+ end
+end
+
+RSpec.describe "The --report-on GUARD option" do
+ before :each do
+ allow(MSpec).to receive(:register_mode)
+
+ @options, @config = new_option
+ @options.verify
+
+ SpecGuard.clear_guards
+ end
+
+ after :each do
+ SpecGuard.clear_guards
+ end
+
+ it "is enabled with #interrupt" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--report-on", "GUARD", an_instance_of(String))
+ @options.verify
+ end
+
+ it "sets the MSpec mode to :report_on" do
+ expect(MSpec).to receive(:register_mode).with(:report_on)
+ @options.parse ["--report-on", "ruby_bug"]
+ end
+
+ it "converts the guard name to a symbol" do
+ name = double("ruby_bug")
+ expect(name).to receive(:to_sym)
+ @options.parse ["--report-on", name]
+ end
+
+ it "saves the name of the guard" do
+ @options.parse ["--report-on", "ruby_bug"]
+ expect(SpecGuard.guards).to eq([:ruby_bug])
+ end
+end
+
+RSpec.describe "The -K, --action-tag TAG option" do
+ before :each do
+ @options, @config = new_option
+ @options.action_filters
+ end
+
+ it "is enabled with #action_filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-K", "--action-tag", "TAG",
+ an_instance_of(String))
+ @options.action_filters
+ end
+
+ it "adds TAG to the list of tags that trigger actions" do
+ ["-K", "--action-tag"].each do |opt|
+ @config[:atags] = []
+ @options.parse [opt, "action-tag"]
+ expect(@config[:atags]).to include("action-tag")
+ end
+ end
+end
+
+RSpec.describe "The -S, --action-string STR option" do
+ before :each do
+ @options, @config = new_option
+ @options.action_filters
+ end
+
+ it "is enabled with #action_filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-S", "--action-string", "STR",
+ an_instance_of(String))
+ @options.action_filters
+ end
+
+ it "adds STR to the list of spec descriptions that trigger actions" do
+ ["-S", "--action-string"].each do |opt|
+ @config[:astrings] = []
+ @options.parse [opt, "action-str"]
+ expect(@config[:astrings]).to include("action-str")
+ end
+ end
+end
+
+RSpec.describe "The -d, --debug option" do
+ before :each do
+ @options, @config = new_option
+ @options.debug
+ end
+
+ after :each do
+ $MSPEC_DEBUG = nil
+ end
+
+ it "is enabled with #debug" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-d", "--debug", an_instance_of(String))
+ @options.debug
+ end
+
+ it "sets $MSPEC_DEBUG to true" do
+ ["-d", "--debug"].each do |opt|
+ expect($MSPEC_DEBUG).not_to be_truthy
+ @options.parse opt
+ expect($MSPEC_DEBUG).to be_truthy
+ $MSPEC_DEBUG = nil
+ end
+ end
+end
+
+RSpec.describe "MSpecOptions#all" do
+ it "includes all options" do
+ meth = MSpecOptions.instance_method(:all)
+ file, line = meth.source_location
+ contents = File.read(file)
+ lines = contents.lines
+
+ from = line
+ to = from
+ to += 1 until /^\s*end\s*$/ =~ lines[to]
+ calls = lines[from...to].map(&:strip)
+
+ option_methods = contents.scan(/def (\w+).*\n\s*on\(/).map(&:first)
+ option_methods[0].sub!("configure", "configure {}")
+
+ expect(calls).to eq(option_methods)
+ end
+end
diff --git a/spec/mspec/spec/utils/script_spec.rb b/spec/mspec/spec/utils/script_spec.rb
new file mode 100644
index 0000000000..c35bda8b47
--- /dev/null
+++ b/spec/mspec/spec/utils/script_spec.rb
@@ -0,0 +1,470 @@
+require 'spec_helper'
+require 'mspec/utils/script'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters'
+require 'mspec/runner/actions/filter'
+
+RSpec.describe MSpecScript, ".config" do
+ it "returns a Hash" do
+ expect(MSpecScript.config).to be_kind_of(Hash)
+ end
+end
+
+RSpec.describe MSpecScript, ".set" do
+ it "sets the config hash key, value" do
+ MSpecScript.set :a, 10
+ expect(MSpecScript.config[:a]).to eq(10)
+ end
+end
+
+RSpec.describe MSpecScript, ".get" do
+ it "gets the config hash value for a key" do
+ MSpecScript.set :a, 10
+ expect(MSpecScript.get(:a)).to eq(10)
+ end
+end
+
+RSpec.describe MSpecScript, "#config" do
+ it "returns the MSpecScript config hash" do
+ MSpecScript.set :b, 5
+ expect(MSpecScript.new.config[:b]).to eq(5)
+ end
+
+ it "returns the MSpecScript config hash from subclasses" do
+ class MSSClass < MSpecScript; end
+ MSpecScript.set :b, 5
+ expect(MSSClass.new.config[:b]).to eq(5)
+ end
+end
+
+RSpec.describe MSpecScript, "#load_default" do
+ before :all do
+ @verbose = $VERBOSE
+ $VERBOSE = nil
+ end
+
+ after :all do
+ $VERBOSE = @verbose
+ end
+
+ before :each do
+ @version = RUBY_VERSION
+ if Object.const_defined? :RUBY_ENGINE
+ @engine = Object.const_get :RUBY_ENGINE
+ end
+ @script = MSpecScript.new
+ allow(MSpecScript).to receive(:new).and_return(@script)
+ end
+
+ after :each do
+ Object.const_set :RUBY_VERSION, @version
+ Object.const_set :RUBY_ENGINE, @engine if @engine
+ end
+
+ it "attempts to load 'default.mspec'" do
+ allow(@script).to receive(:try_load)
+ expect(@script).to receive(:try_load).with('default.mspec').and_return(true)
+ @script.load_default
+ end
+
+ it "attempts to load a config file based on RUBY_ENGINE and RUBY_VERSION" do
+ Object.const_set :RUBY_ENGINE, "ybur"
+ Object.const_set :RUBY_VERSION, "1.8.9"
+ default = "ybur.1.8.mspec"
+ expect(@script).to receive(:try_load).with('default.mspec').and_return(false)
+ expect(@script).to receive(:try_load).with(default)
+ expect(@script).to receive(:try_load).with('ybur.mspec')
+ @script.load_default
+ end
+end
+
+RSpec.describe MSpecScript, ".main" do
+ before :each do
+ @script = double("MSpecScript").as_null_object
+ allow(MSpecScript).to receive(:new).and_return(@script)
+ # Do not require full mspec as it would conflict with RSpec
+ expect(MSpecScript).to receive(:require).with('mspec')
+ end
+
+ it "creates an instance of MSpecScript" do
+ expect(MSpecScript).to receive(:new).and_return(@script)
+ MSpecScript.main
+ end
+
+ it "attempts to load the default config" do
+ expect(@script).to receive(:load_default)
+ MSpecScript.main
+ end
+
+ it "calls the #options method on the script" do
+ expect(@script).to receive(:options)
+ MSpecScript.main
+ end
+
+ it "calls the #signals method on the script" do
+ expect(@script).to receive(:signals)
+ MSpecScript.main
+ end
+
+ it "calls the #register method on the script" do
+ expect(@script).to receive(:register)
+ MSpecScript.main
+ end
+
+ it "calls the #setup_env method on the script" do
+ expect(@script).to receive(:setup_env)
+ MSpecScript.main
+ end
+
+ it "calls the #run method on the script" do
+ expect(@script).to receive(:run)
+ MSpecScript.main
+ end
+end
+
+RSpec.describe MSpecScript, "#initialize" do
+ before :each do
+ @config = MSpecScript.new.config
+ end
+
+ it "sets the default config values" do
+ expect(@config[:formatter]).to eq(nil)
+ expect(@config[:includes]).to eq([])
+ expect(@config[:excludes]).to eq([])
+ expect(@config[:patterns]).to eq([])
+ expect(@config[:xpatterns]).to eq([])
+ expect(@config[:tags]).to eq([])
+ expect(@config[:xtags]).to eq([])
+ expect(@config[:atags]).to eq([])
+ expect(@config[:astrings]).to eq([])
+ expect(@config[:abort]).to eq(true)
+ expect(@config[:config_ext]).to eq('.mspec')
+ end
+end
+
+RSpec.describe MSpecScript, "#load" do
+ before :each do
+ allow(File).to receive(:exist?).and_return(false)
+ @script = MSpecScript.new
+ @file = "default.mspec"
+ @base = "default"
+ end
+
+ it "attempts to locate the file through the expanded path name" do
+ expect(File).to receive(:expand_path).with(@file, ".").and_return(@file)
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(Kernel).to receive(:load).with(@file).and_return(:loaded)
+ expect(@script.load(@file)).to eq(:loaded)
+ end
+
+ it "appends config[:config_ext] to the name and attempts to locate the file through the expanded path name" do
+ expect(File).to receive(:expand_path).with(@base, ".").and_return(@base)
+ expect(File).to receive(:expand_path).with(@base, "spec").and_return(@base)
+ expect(File).to receive(:expand_path).with(@file, ".").and_return(@file)
+ expect(File).to receive(:exist?).with(@base).and_return(false)
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(Kernel).to receive(:load).with(@file).and_return(:loaded)
+ expect(@script.load(@base)).to eq(:loaded)
+ end
+
+ it "attempts to locate the file in '.'" do
+ path = File.expand_path @file, "."
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).with(path).and_return(:loaded)
+ expect(@script.load(@file)).to eq(:loaded)
+ end
+
+ it "appends config[:config_ext] to the name and attempts to locate the file in '.'" do
+ path = File.expand_path @file, "."
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).with(path).and_return(:loaded)
+ expect(@script.load(@base)).to eq(:loaded)
+ end
+
+ it "attempts to locate the file in 'spec'" do
+ path = File.expand_path @file, "spec"
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).with(path).and_return(:loaded)
+ expect(@script.load(@file)).to eq(:loaded)
+ end
+
+ it "appends config[:config_ext] to the name and attempts to locate the file in 'spec'" do
+ path = File.expand_path @file, "spec"
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).with(path).and_return(:loaded)
+ expect(@script.load(@base)).to eq(:loaded)
+ end
+
+ it "loads a given file only once" do
+ path = File.expand_path @file, "spec"
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).once.with(path).and_return(:loaded)
+ expect(@script.load(@base)).to eq(:loaded)
+ expect(@script.load(@base)).to eq(true)
+ end
+end
+
+RSpec.describe MSpecScript, "#custom_options" do
+ before :each do
+ @script = MSpecScript.new
+ end
+
+ after :each do
+ end
+
+ it "prints 'None'" do
+ options = double("options")
+ expect(options).to receive(:doc).with(" No custom options registered")
+ @script.custom_options options
+ end
+end
+
+RSpec.describe MSpecScript, "#register" do
+ before :each do
+ @script = MSpecScript.new
+
+ @formatter = double("formatter").as_null_object
+ @script.config[:formatter] = @formatter
+ end
+
+ it "creates and registers the formatter" do
+ expect(@formatter).to receive(:new).and_return(@formatter)
+ expect(@formatter).to receive(:register)
+ @script.register
+ end
+
+ it "does not register the formatter if config[:formatter] is false" do
+ @script.config[:formatter] = false
+ @script.register
+ end
+
+ it "calls #custom_register" do
+ expect(@script).to receive(:custom_register)
+ @script.register
+ end
+
+ it "registers :formatter with the formatter instance" do
+ allow(@formatter).to receive(:new).and_return(@formatter)
+ @script.register
+ expect(MSpec.formatter).to be(@formatter)
+ end
+
+ it "does not register :formatter if config[:formatter] is false" do
+ @script.config[:formatter] = false
+ expect(MSpec).not_to receive(:store)
+ @script.register
+ end
+end
+
+RSpec.describe MSpecScript, "#register" do
+ before :each do
+ @script = MSpecScript.new
+
+ @formatter = double("formatter").as_null_object
+ @script.config[:formatter] = @formatter
+
+ @filter = double("filter")
+ expect(@filter).to receive(:register)
+
+ @ary = ["some", "spec"]
+ end
+
+ it "creates and registers a MatchFilter for include specs" do
+ expect(MatchFilter).to receive(:new).with(:include, *@ary).and_return(@filter)
+ @script.config[:includes] = @ary
+ @script.register
+ end
+
+ it "creates and registers a MatchFilter for excluded specs" do
+ expect(MatchFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter)
+ @script.config[:excludes] = @ary
+ @script.register
+ end
+
+ it "creates and registers a RegexpFilter for include specs" do
+ expect(RegexpFilter).to receive(:new).with(:include, *@ary).and_return(@filter)
+ @script.config[:patterns] = @ary
+ @script.register
+ end
+
+ it "creates and registers a RegexpFilter for excluded specs" do
+ expect(RegexpFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter)
+ @script.config[:xpatterns] = @ary
+ @script.register
+ end
+
+ it "creates and registers a TagFilter for include specs" do
+ expect(TagFilter).to receive(:new).with(:include, *@ary).and_return(@filter)
+ @script.config[:tags] = @ary
+ @script.register
+ end
+
+ it "creates and registers a TagFilter for excluded specs" do
+ expect(TagFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter)
+ @script.config[:xtags] = @ary
+ @script.register
+ end
+
+ it "creates and registers a ProfileFilter for include specs" do
+ expect(ProfileFilter).to receive(:new).with(:include, *@ary).and_return(@filter)
+ @script.config[:profiles] = @ary
+ @script.register
+ end
+
+ it "creates and registers a ProfileFilter for excluded specs" do
+ expect(ProfileFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter)
+ @script.config[:xprofiles] = @ary
+ @script.register
+ end
+end
+
+RSpec.describe MSpecScript, "#signals" do
+ before :each do
+ @script = MSpecScript.new
+ @abort = @script.config[:abort]
+ end
+
+ after :each do
+ @script.config[:abort] = @abort
+ end
+
+ it "traps the INT signal if config[:abort] is true" do
+ expect(Signal).to receive(:trap).with("INT")
+ @script.config[:abort] = true
+ @script.signals
+ end
+
+ it "does not trap the INT signal if config[:abort] is not true" do
+ expect(Signal).not_to receive(:trap).with("INT")
+ @script.config[:abort] = false
+ @script.signals
+ end
+end
+
+RSpec.describe MSpecScript, "#entries" do
+ before :each do
+ @script = MSpecScript.new
+
+ allow(File).to receive(:realpath).and_return("name")
+ allow(File).to receive(:file?).and_return(false)
+ allow(File).to receive(:directory?).and_return(false)
+ end
+
+ it "returns the pattern in an array if it is a file" do
+ expect(File).to receive(:realpath).with("file").and_return("file/expanded.rb")
+ expect(File).to receive(:file?).with("file/expanded.rb").and_return(true)
+ expect(@script.entries("file")).to eq(["file/expanded.rb"])
+ end
+
+ it "returns Dir['pattern/**/*_spec.rb'] if pattern is a directory" do
+ expect(File).to receive(:directory?).with("name").and_return(true)
+ allow(File).to receive(:realpath).and_return("name", "name/**/*_spec.rb")
+ expect(Dir).to receive(:[]).with("name/**/*_spec.rb").and_return(["dir1", "dir2"])
+ expect(@script.entries("name")).to eq(["dir1", "dir2"])
+ end
+
+ it "aborts if pattern cannot be resolved to a file nor a directory" do
+ expect(@script).to receive(:abort)
+ @script.entries("pattern")
+ end
+
+ describe "with config[:prefix] set" do
+ before :each do
+ prefix = "prefix/dir"
+ @script.config[:prefix] = prefix
+ @name = prefix + "/name"
+ end
+
+ it "returns the pattern in an array if it is a file" do
+ name = "#{@name}.rb"
+ expect(File).to receive(:realpath).with(name).and_return(name)
+ expect(File).to receive(:file?).with(name).and_return(true)
+ expect(@script.entries("name.rb")).to eq([name])
+ end
+
+ it "returns Dir['pattern/**/*_spec.rb'] if pattern is a directory" do
+ allow(File).to receive(:realpath).and_return(@name, @name+"/**/*_spec.rb")
+ expect(File).to receive(:directory?).with(@name).and_return(true)
+ expect(Dir).to receive(:[]).with(@name + "/**/*_spec.rb").and_return(["dir1", "dir2"])
+ expect(@script.entries("name")).to eq(["dir1", "dir2"])
+ end
+
+ it "aborts if pattern cannot be resolved to a file nor a directory" do
+ expect(@script).to receive(:abort)
+ @script.entries("pattern")
+ end
+ end
+end
+
+RSpec.describe MSpecScript, "#files" do
+ before :each do
+ @script = MSpecScript.new
+ end
+
+ it "accumulates the values returned by #entries" do
+ expect(@script).to receive(:entries).and_return(["file1"], ["file2"])
+ expect(@script.files(["a", "b"])).to eq(["file1", "file2"])
+ end
+
+ it "strips a leading '^' and removes the values returned by #entries" do
+ expect(@script).to receive(:entries).and_return(["file1"], ["file2"], ["file1"])
+ expect(@script.files(["a", "b", "^a"])).to eq(["file2"])
+ end
+
+ it "processes the array elements in order" do
+ expect(@script).to receive(:entries).and_return(["file1"], ["file1"], ["file2"])
+ expect(@script.files(["^a", "a", "b"])).to eq(["file1", "file2"])
+ end
+end
+
+RSpec.describe MSpecScript, "#files" do
+ before :each do
+ MSpecScript.set :files, ["file1", "file2"]
+
+ @script = MSpecScript.new
+ end
+
+ after :each do
+ MSpecScript.config.delete :files
+ end
+
+ it "looks up items with leading ':' in the config object" do
+ expect(@script).to receive(:entries).and_return(["file1"], ["file2"])
+ expect(@script.files([":files"])).to eq(["file1", "file2"])
+ end
+
+ it "aborts if the config key is not set" do
+ expect(@script).to receive(:abort).with("Key :all_files not found in mspec config.")
+ @script.files([":all_files"])
+ end
+end
+
+RSpec.describe MSpecScript, "#setup_env" do
+ before :each do
+ @script = MSpecScript.new
+ @options, @config = new_option
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ after :each do
+ end
+
+ it "sets MSPEC_RUNNER = '1' in the environment" do
+ ENV["MSPEC_RUNNER"] = "0"
+ @script.setup_env
+ expect(ENV["MSPEC_RUNNER"]).to eq("1")
+ end
+
+ it "sets RUBY_EXE = config[:target] in the environment" do
+ ENV["RUBY_EXE"] = nil
+ @script.setup_env
+ expect(ENV["RUBY_EXE"]).to eq(@config[:target])
+ end
+
+ it "sets RUBY_FLAGS = config[:flags] in the environment" do
+ ENV["RUBY_FLAGS"] = nil
+ @config[:flags] = ["-w", "-Q"]
+ @script.setup_env
+ expect(ENV["RUBY_FLAGS"]).to eq("-w -Q")
+ end
+end
diff --git a/spec/mspec/spec/utils/version_spec.rb b/spec/mspec/spec/utils/version_spec.rb
new file mode 100644
index 0000000000..ec367d2a1e
--- /dev/null
+++ b/spec/mspec/spec/utils/version_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+require 'mspec/utils/version'
+
+RSpec.describe SpecVersion, "#to_s" do
+ it "returns the string with which it was initialized" do
+ expect(SpecVersion.new("1.8").to_s).to eq("1.8")
+ expect(SpecVersion.new("2.118.9").to_s).to eq("2.118.9")
+ end
+end
+
+RSpec.describe SpecVersion, "#to_str" do
+ it "returns the same string as #to_s" do
+ version = SpecVersion.new("2.118.9")
+ expect(version.to_str).to eq(version.to_s)
+ end
+end
+
+RSpec.describe SpecVersion, "#to_i with ceil = false" do
+ it "returns an integer representation of the version string" do
+ expect(SpecVersion.new("2.23.10").to_i).to eq(1022310)
+ end
+
+ it "replaces missing version parts with zeros" do
+ expect(SpecVersion.new("1.8").to_i).to eq(1010800)
+ expect(SpecVersion.new("1.8.6").to_i).to eq(1010806)
+ end
+end
+
+RSpec.describe SpecVersion, "#to_i with ceil = true" do
+ it "returns an integer representation of the version string" do
+ expect(SpecVersion.new("1.8.6", true).to_i).to eq(1010806)
+ end
+
+ it "fills in 9s for missing tiny values" do
+ expect(SpecVersion.new("1.8", true).to_i).to eq(1010899)
+ expect(SpecVersion.new("1.8.6", true).to_i).to eq(1010806)
+ end
+end
+
+RSpec.describe SpecVersion, "#to_int" do
+ it "returns the same value as #to_i" do
+ version = SpecVersion.new("4.16.87")
+ expect(version.to_int).to eq(version.to_i)
+ end
+end
diff --git a/spec/mspec/tool/check_require_spec_helper.rb b/spec/mspec/tool/check_require_spec_helper.rb
new file mode 100755
index 0000000000..07126e68dc
--- /dev/null
+++ b/spec/mspec/tool/check_require_spec_helper.rb
@@ -0,0 +1,34 @@
+#!/usr/bin/env ruby
+
+# This script is used to check that each *_spec.rb file has
+# a relative_require for spec_helper which should live higher
+# up in the ruby/spec repo directory tree.
+#
+# Prints errors to $stderr and returns a non-zero exit code when
+# errors are found.
+#
+# Related to https://github.com/ruby/spec/pull/992
+
+def check_file(fn)
+ File.foreach(fn) do |line|
+ return $1 if line =~ /^\s*require_relative\s*['"](.*spec_helper)['"]/
+ end
+ nil
+end
+
+rootdir = ARGV[0] || "."
+fglob = File.join(rootdir, "**", "*_spec.rb")
+specfiles = Dir.glob(fglob)
+raise "No spec files found in #{fglob.inspect}. Give an argument to specify the root-directory of ruby/spec" if specfiles.empty?
+
+errors = 0
+specfiles.sort.each do |fn|
+ result = check_file(fn)
+ if result.nil?
+ warn "Missing require_relative for *spec_helper for file: #{fn}"
+ errors += 1
+ end
+end
+
+puts "# Found #{errors} files with require_relative spec_helper issues."
+exit 1 if errors > 0
diff --git a/spec/mspec/tool/find.rb b/spec/mspec/tool/find.rb
new file mode 100755
index 0000000000..322b023f15
--- /dev/null
+++ b/spec/mspec/tool/find.rb
@@ -0,0 +1,10 @@
+#!/usr/bin/env ruby
+Dir.chdir('../rubyspec') do
+ regexp = Regexp.new(ARGV[0])
+ Dir.glob('**/*.rb') do |file|
+ contents = File.read(file)
+ if regexp =~ contents
+ puts file
+ end
+ end
+end
diff --git a/spec/mspec/tool/pull-latest-mspec-spec b/spec/mspec/tool/pull-latest-mspec-spec
new file mode 100755
index 0000000000..154a353e64
--- /dev/null
+++ b/spec/mspec/tool/pull-latest-mspec-spec
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# Assumes all commits have been synchronized to https://github.com/ruby/spec
+# See spec/mspec/tool/sync/sync-rubyspec.rb
+
+function sync {
+ dir="$1"
+ repo="$2"
+ short_repo_name="ruby/$(basename "$repo" .git)"
+
+ rm -rf "$dir"
+ git clone --depth 1 "$repo" "$dir"
+ commit=$(git -C "$dir" log -n 1 --format='%h')
+ rm -rf "$dir/.git"
+
+ # Remove CI files to avoid confusion
+ rm -f "$dir/appveyor.yml"
+ rm -f "$dir/.travis.yml"
+ rm -rf "$dir/.github"
+
+ git add "$dir"
+ git commit -m "Update to ${short_repo_name}@${commit}"
+}
+
+sync spec/mspec https://github.com/ruby/mspec.git
+sync spec/ruby https://github.com/ruby/spec.git
diff --git a/spec/mspec/tool/remove_old_guards.rb b/spec/mspec/tool/remove_old_guards.rb
new file mode 100755
index 0000000000..bc5612c78d
--- /dev/null
+++ b/spec/mspec/tool/remove_old_guards.rb
@@ -0,0 +1,145 @@
+#!/usr/bin/env ruby
+
+# Removes old version guards in ruby/spec.
+# Run it from the ruby/spec repository root.
+# The argument is the new minimum supported version.
+#
+# cd spec
+# ../mspec/tool/remove_old_guards.rb <ruby-version>
+#
+# where <ruby-version> is a version guard with which should be removed
+#
+# Example:
+# tool/remove_old_guards.rb 3.1
+#
+# As a result guards like
+# ruby_version_is "3.1" do
+# # ...
+# end
+#
+# will be removed.
+
+def dedent(line)
+ if line.start_with?(" ")
+ line[2..-1]
+ else
+ line
+ end
+end
+
+def each_spec_file(&block)
+ Dir["*/**/*.rb"].each(&block)
+end
+
+def each_file(&block)
+ Dir["**/*"].each { |path|
+ yield path if File.file?(path)
+ }
+end
+
+def remove_guards(guard, keep)
+ each_spec_file do |file|
+ contents = File.binread(file)
+ if contents =~ guard
+ puts file
+ lines = contents.lines.to_a
+ while first = lines.find_index { |line| line =~ guard }
+ comment = first
+ while comment > 0 and lines[comment-1] =~ /^(\s*)#/
+ comment -= 1
+ end
+ indent = lines[first][/^(\s*)/, 1].length
+ last = (first+1...lines.size).find { |i|
+ space = lines[i][/^(\s*)end$/, 1] and space.length == indent
+ }
+ raise file unless last
+ if keep
+ lines[comment..last] = lines[first+1..last-1].map { |l| dedent(l) }
+ else
+ if comment > 0 and lines[comment-1] == "\n"
+ comment -= 1
+ elsif lines[last+1] == "\n"
+ last += 1
+ end
+ lines[comment..last] = []
+ end
+ end
+ File.binwrite file, lines.join
+ end
+ end
+end
+
+def remove_empty_files
+ each_spec_file do |file|
+ unless file.include?("fixtures/")
+ lines = File.readlines(file)
+ if lines.all? { |line| line.chomp.empty? or line.start_with?('require', '#') }
+ puts "Removing empty file #{file}"
+ File.delete(file)
+ end
+ end
+ end
+end
+
+def remove_unused_shared_specs
+ shared_groups = {}
+ # Dir["**/shared/**/*.rb"].each do |shared|
+ each_spec_file do |shared|
+ next if File.basename(shared) == 'constants.rb'
+ contents = File.binread(shared)
+ found = false
+ contents.scan(/^\s*describe (:[\w_?]+), shared: true do$/) {
+ shared_groups[$1] = 0
+ found = true
+ }
+ if !found and shared.include?('shared/') and !shared.include?('fixtures/') and !shared.end_with?('/constants.rb')
+ puts "no shared describe in #{shared} ?"
+ end
+ end
+
+ each_spec_file do |file|
+ contents = File.binread(file)
+ contents.scan(/(?:it_behaves_like|it_should_behave_like) (:[\w_?]+)[,\s]/) do
+ puts $1 unless shared_groups.key?($1)
+ shared_groups[$1] += 1
+ end
+ end
+
+ shared_groups.each_pair do |group, value|
+ if value == 0
+ puts "Shared describe #{group} seems unused"
+ elsif value == 1
+ puts "Shared describe #{group} seems used only once" if $VERBOSE
+ end
+ end
+end
+
+def search(regexp)
+ each_file do |file|
+ contents = File.binread(file)
+ if contents =~ regexp
+ puts file
+ contents.each_line do |line|
+ if line =~ regexp
+ puts line
+ end
+ end
+ end
+ end
+end
+
+abort "usage: #{$0} <ruby-version>" if ARGV.empty?
+
+version = Regexp.escape(ARGV.fetch(0))
+version += "(?:\\.0)?" if version.count(".") < 2
+remove_guards(/ruby_version_is (["'])#{version}\1 do/, true)
+remove_guards(/ruby_version_is (["'])[0-9.]*\1 *... *(["'])#{version}\2 do/, false)
+remove_guards(/ruby_bug ["']#\d+["'], (["'])[0-9.]*\1 *... *(["'])#{version}\2 do/, true)
+
+remove_empty_files
+remove_unused_shared_specs
+
+puts "Search:"
+search(/(["'])#{version}\1/)
+search(/^\s*#.+#{version}/)
+search(/RUBY_VERSION_IS_#{version.tr('.', '_')}/)
diff --git a/spec/mspec/tool/sync/.gitignore b/spec/mspec/tool/sync/.gitignore
new file mode 100644
index 0000000000..e64f1e8542
--- /dev/null
+++ b/spec/mspec/tool/sync/.gitignore
@@ -0,0 +1,4 @@
+/jruby
+/rubinius
+/ruby
+/truffleruby
diff --git a/spec/mspec/tool/sync/sync-rubyspec.rb b/spec/mspec/tool/sync/sync-rubyspec.rb
new file mode 100644
index 0000000000..86c43d0dc8
--- /dev/null
+++ b/spec/mspec/tool/sync/sync-rubyspec.rb
@@ -0,0 +1,254 @@
+# This script is based on commands from the wiki:
+# https://github.com/ruby/spec/wiki/Merging-specs-from-JRuby-and-other-sources
+
+IMPLS = {
+ truffleruby: {
+ git: "https://github.com/truffleruby/truffleruby.git",
+ from_commit: "f10ab6988d",
+ },
+ jruby: {
+ git: "https://github.com/jruby/jruby.git",
+ from_commit: "f10ab6988d",
+ },
+ rbx: {
+ git: "https://github.com/rubinius/rubinius.git",
+ },
+ mri: {
+ git: "https://github.com/ruby/ruby.git",
+ },
+}
+
+MSPEC = ARGV.delete('--mspec')
+
+CHECK_LAST_MERGE = !MSPEC && ENV['CHECK_LAST_MERGE'] != 'false'
+TEST_MASTER = ENV['TEST_MASTER'] != 'false'
+
+ONLY_FILTER = ENV['ONLY_FILTER'] == 'true'
+
+MSPEC_REPO = File.expand_path("../../..", __FILE__)
+raise MSPEC_REPO if !Dir.exist?(MSPEC_REPO) or !Dir.exist?("#{MSPEC_REPO}/.git")
+
+# Assuming the rubyspec repo is a sibling of the mspec repo
+RUBYSPEC_REPO = File.expand_path("../rubyspec", MSPEC_REPO)
+raise RUBYSPEC_REPO unless Dir.exist?(RUBYSPEC_REPO)
+
+SOURCE_REPO = MSPEC ? MSPEC_REPO : RUBYSPEC_REPO
+
+# LAST_MERGE is a commit of ruby/spec or ruby/mspec
+# which is the spec/mspec commit that was last imported in the Ruby implementation
+# (i.e. the commit in "Update to ruby/spec@commit").
+# It is normally automatically computed, but can be manually set when
+# e.g. the last update of specs wasn't merged in the Ruby implementation.
+LAST_MERGE = ENV["LAST_MERGE"]
+
+NOW = Time.now
+
+BRIGHT_RED = "\e[31;1m"
+BRIGHT_YELLOW = "\e[33;1m"
+RESET = "\e[0m"
+
+# git filter-branch --subdirectory-filter works fine for our use case
+ENV['FILTER_BRANCH_SQUELCH_WARNING'] = '1'
+
+class RubyImplementation
+ attr_reader :name
+
+ def initialize(name, data)
+ @name = name.to_s
+ @data = data
+ end
+
+ def git_url
+ @data[:git]
+ end
+
+ def repo_name
+ File.basename(git_url, ".git")
+ end
+
+ def repo_path
+ "#{__dir__}/#{repo_name}"
+ end
+
+ def repo_org
+ File.basename(File.dirname(git_url))
+ end
+
+ def from_commit
+ from = @data[:from_commit]
+ "#{from}..." if from
+ end
+
+ def last_merge_message
+ message = @data[:merge_message] || "Update to ruby/spec@"
+ message.gsub!("ruby/spec", "ruby/mspec") if MSPEC
+ message
+ end
+
+ def prefix
+ MSPEC ? "spec/mspec" : "spec/ruby"
+ end
+
+ def rebased_branch
+ "#{@name}-rebased"
+ end
+end
+
+def sh(*args)
+ puts args.join(' ')
+ system(*args)
+ raise unless $?.success?
+end
+
+def branch?(name)
+ branches = `git branch`.sub('*', '').lines.map(&:strip)
+ branches.include?(name)
+end
+
+def update_repo(impl)
+ unless File.directory? impl.repo_name
+ sh "git", "clone", impl.git_url
+ end
+
+ Dir.chdir(impl.repo_name) do
+ puts Dir.pwd
+
+ sh "git", "checkout", "master"
+ sh "git", "pull"
+ end
+end
+
+def filter_commits(impl)
+ Dir.chdir(impl.repo_name) do
+ date = NOW.strftime("%F")
+ branch = "#{MSPEC ? :mspec : :specs}-#{date}"
+
+ unless branch?(branch)
+ sh "git", "checkout", "-b", branch
+ sh "git", "filter-branch", "-f", "--subdirectory-filter", impl.prefix, *impl.from_commit
+ sh "git", "push", "-f", SOURCE_REPO, "#{branch}:#{impl.name}"
+ end
+ end
+end
+
+def rebase_commits(impl)
+ Dir.chdir(SOURCE_REPO) do
+ sh "git", "checkout", "master"
+ sh "git", "pull"
+
+ rebased = impl.rebased_branch
+ if branch?(rebased)
+ last_commit = Time.at(Integer(`git log -n 1 --format='%ct' #{rebased}`))
+ days_since_last_commit = (NOW-last_commit) / 86400
+ if days_since_last_commit > 7
+ abort "#{BRIGHT_RED}#{rebased} exists but last commit is old (#{last_commit}), delete the branch if it was merged#{RESET}"
+ else
+ puts "#{BRIGHT_YELLOW}#{rebased} already exists, last commit on #{last_commit}, assuming it correct#{RESET}"
+ sh "git", "checkout", rebased
+ end
+ else
+ sh "git", "checkout", impl.name
+
+ if LAST_MERGE
+ last_merge = `git log -n 1 --format='%H %ct' #{LAST_MERGE}`
+ else
+ last_merge = `git log --grep='^#{impl.last_merge_message}' -n 1 --format='%H %ct'`
+ end
+ last_merge, commit_timestamp = last_merge.split(' ')
+
+ raise "Could not find last merge" unless last_merge
+ puts "Last merge is #{last_merge}"
+
+ commit_date = Time.at(Integer(commit_timestamp))
+ days_since_last_merge = (NOW-commit_date) / 86400
+ if CHECK_LAST_MERGE and days_since_last_merge > 60
+ raise "#{days_since_last_merge.floor} days since last merge, probably wrong commit"
+ end
+
+ puts "Checking if the last merge is consistent with upstream files"
+ rubyspec_commit = `git log -n 1 --format='%s' #{last_merge}`.chomp.split('@', 2)[-1]
+ sh "git", "checkout", last_merge
+ sh "git", "diff", "--exit-code", rubyspec_commit, "--", ":!.github"
+
+ puts "Rebasing..."
+ sh "git", "branch", "-D", rebased if branch?(rebased)
+ sh "git", "checkout", "-b", rebased, impl.name
+ sh "git", "rebase", "--onto", "master", last_merge
+ end
+ end
+end
+
+def new_commits?(impl)
+ Dir.chdir(SOURCE_REPO) do
+ diff = `git diff master #{impl.rebased_branch}`
+ !diff.empty?
+ end
+end
+
+def test_new_specs
+ require "yaml"
+ Dir.chdir(SOURCE_REPO) do
+ workflow = YAML.load_file(".github/workflows/ci.yml")
+ job_name = MSPEC ? "test" : "specs"
+ versions = workflow.dig("jobs", job_name, "strategy", "matrix", "ruby").map(&:to_s)
+ versions = versions.grep(/^\d+\./) # Test on MRI
+ min_version, max_version = versions.minmax
+
+ test_command = MSPEC ? "bundle install && bundle exec rspec" : "../mspec/bin/mspec -j"
+
+ run_test = -> version {
+ command = "chruby ruby-#{version} && #{test_command}"
+ sh ENV["SHELL"], "-c", command
+ }
+
+ run_test[min_version]
+ run_test[max_version]
+ run_test["master"] if TEST_MASTER
+ end
+end
+
+def fast_forward_master(impl)
+ Dir.chdir(SOURCE_REPO) do
+ sh "git", "checkout", "master"
+ sh "git", "merge", "--ff-only", impl.rebased_branch
+ sh "git", "branch", "--delete", impl.rebased_branch
+ end
+end
+
+def check_ci
+ puts
+ puts <<-EOS
+ Push to master, and check that the CI passes:
+ https://github.com/ruby/#{:m if MSPEC}spec/commits/master
+
+ EOS
+end
+
+def main(impls)
+ impls.each_pair do |impl, data|
+ impl = RubyImplementation.new(impl, data)
+ update_repo(impl)
+ filter_commits(impl)
+ unless ONLY_FILTER
+ rebase_commits(impl)
+ if new_commits?(impl)
+ test_new_specs
+ fast_forward_master(impl)
+ check_ci
+ else
+ STDERR.puts "#{BRIGHT_YELLOW}No new commits#{RESET}"
+ fast_forward_master(impl)
+ end
+ end
+ end
+end
+
+if ARGV == ["all"]
+ impls = IMPLS
+else
+ args = ARGV.map { |arg| arg.to_sym }
+ raise ARGV.to_s unless (args - IMPLS.keys).empty?
+ impls = IMPLS.select { |impl| args.include?(impl) }
+end
+
+main(impls)
diff --git a/spec/mspec/tool/tag_from_output.rb b/spec/mspec/tool/tag_from_output.rb
new file mode 100755
index 0000000000..41aa70f932
--- /dev/null
+++ b/spec/mspec/tool/tag_from_output.rb
@@ -0,0 +1,65 @@
+#!/usr/bin/env ruby
+
+# Adds tags based on error and failures output (e.g., from a CI log),
+# without running any spec code.
+
+tag = ENV["TAG"] || "fails"
+
+tags_dir = %w[
+ spec/tags
+ spec/tags/ruby
+].find { |dir| Dir.exist?("#{dir}/language") }
+abort 'Could not find tags directory' unless tags_dir
+
+output = ARGF.readlines
+
+# Automatically strip datetime of GitHub Actions
+if output.first =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z /
+ output = output.map { |line| line.split(' ', 2).last }
+end
+
+NUMBER = /^\d+\)$/
+ERROR_OR_FAILED = / (ERROR|FAILED)$/
+SPEC_FILE = /^((?:\/|[CD]:\/).+_spec\.rb)\:\d+/
+
+output.slice_before(NUMBER).select { |number, *rest|
+ number =~ NUMBER and rest.any? { |line| line =~ ERROR_OR_FAILED }
+}.each { |number, *rest|
+ error_line = rest.find { |line| line =~ ERROR_OR_FAILED }
+ description = error_line.match(ERROR_OR_FAILED).pre_match
+
+ spec_file = rest.find { |line| line =~ SPEC_FILE }
+ if spec_file
+ spec_file = spec_file[SPEC_FILE, 1] or raise
+ else
+ if error_line =~ /^([\w:]+)[#\.](\w+) /
+ mod, method = $1, $2
+ file = "#{mod.downcase.gsub('::', '/')}/#{method}_spec.rb"
+ spec_file = ['spec/ruby/core', 'spec/ruby/library', *Dir.glob('spec/ruby/library/*')].find { |dir|
+ path = "#{dir}/#{file}"
+ break path if File.exist?(path)
+ }
+ end
+
+ unless spec_file
+ warn "Could not find file for:\n#{error_line}"
+ next
+ end
+ end
+
+ prefix = spec_file.index('spec/ruby/') || spec_file.index('spec/truffle/')
+ spec_file = spec_file[prefix..-1]
+
+ tags_file = spec_file.sub('spec/ruby/', "#{tags_dir}/").sub('spec/truffle/', "#{tags_dir}/truffle/")
+ tags_file = tags_file.sub(/_spec\.rb$/, '_tags.txt')
+
+ dir = File.dirname(tags_file)
+ Dir.mkdir(dir) unless Dir.exist?(dir)
+
+ tag_line = "#{tag}:#{description}"
+ lines = File.exist?(tags_file) ? File.readlines(tags_file, chomp: true) : []
+ unless lines.include?(tag_line)
+ puts tags_file
+ File.write(tags_file, (lines + [tag_line]).join("\n") + "\n")
+ end
+}
diff --git a/spec/mspec/tool/wrap_with_guard.rb b/spec/mspec/tool/wrap_with_guard.rb
new file mode 100755
index 0000000000..5b1bf4d7f7
--- /dev/null
+++ b/spec/mspec/tool/wrap_with_guard.rb
@@ -0,0 +1,28 @@
+#!/usr/bin/env ruby
+# Wrap the passed the files with a guard (e.g., `ruby_version_is ""..."3.0"`).
+# Notably if some methods are removed, this is a convenient way to skip such file from a given version.
+# Example usage:
+# $ spec/mspec/tool/wrap_with_guard.rb 'ruby_version_is ""..."3.0"' spec/ruby/library/set/sortedset/**/*_spec.rb
+
+guard, *files = ARGV
+abort "Usage: #{$0} GUARD FILES..." if files.empty?
+
+files.each do |file|
+ contents = File.binread(file)
+ lines = contents.lines.to_a
+
+ lines = lines.map { |line| line.chomp.empty? ? line : " #{line}" }
+
+ version_line = "#{guard} do\n"
+ if lines[0] =~ /^\s*require.+spec_helper/
+ lines[0] = lines[0].sub(/^ /, '')
+ lines.insert 1, "\n", version_line
+ else
+ warn "Could not find 'require spec_helper' line in #{file}"
+ lines.insert 0, version_line
+ end
+
+ lines << "end\n"
+
+ File.binwrite file, lines.join
+end