├── .rspec ├── spec ├── commands │ └── fixtures │ │ ├── four.txt │ │ ├── three.rb │ │ ├── one_spec.rb │ │ ├── two_spec.rb │ │ └── level2 │ │ └── three_spec.rb ├── runner │ ├── filters │ │ ├── a.yaml │ │ ├── b.yaml │ │ ├── regexp_spec.rb │ │ └── match_spec.rb │ ├── tags.txt │ ├── formatters │ │ ├── summary_spec.rb │ │ ├── multi_spec.rb │ │ ├── describe_spec.rb │ │ └── unit_spec.rb │ └── actions │ │ └── timer_spec.rb ├── utils │ ├── fixtures │ │ ├── this_file_raises.rb │ │ └── this_file_raises2.rb │ ├── deprecate_spec.rb │ └── version_spec.rb ├── fixtures │ ├── my_ruby │ ├── print_interpreter_spec.rb │ ├── b_spec.rb │ ├── die_spec.rb │ ├── chatty_spec.rb │ ├── config.mspec │ ├── a_spec.rb │ ├── object_methods_spec.rb │ ├── tagging_spec.rb │ └── should.rb ├── helpers │ ├── suppress_warning_spec.rb │ ├── argv_spec.rb │ ├── flunk_spec.rb │ ├── mock_to_path_spec.rb │ ├── scratch_spec.rb │ ├── tmp_spec.rb │ ├── numeric_spec.rb │ ├── argf_spec.rb │ ├── fixture_spec.rb │ └── datetime_spec.rb ├── matchers │ ├── block_caller_spec.rb │ ├── be_true_or_false_spec.rb │ ├── be_empty_spec.rb │ ├── be_nan_spec.rb │ ├── be_nil_spec.rb │ ├── be_true_spec.rb │ ├── be_ancestor_of_spec.rb │ ├── be_false_spec.rb │ ├── have_constant_spec.rb │ ├── be_kind_of_spec.rb │ ├── equal_spec.rb │ ├── signed_zero_spec.rb │ ├── eql_spec.rb │ ├── infinity_spec.rb │ ├── respond_to_spec.rb │ ├── have_private_method_spec.rb │ ├── include_spec.rb │ ├── match_yaml_spec.rb │ ├── have_singleton_method_spec.rb │ ├── be_computed_by_spec.rb │ ├── be_an_instance_of_spec.rb │ ├── have_class_variable_spec.rb │ ├── have_instance_variable_spec.rb │ ├── include_any_of_spec.rb │ ├── be_close_spec.rb │ ├── have_instance_method_spec.rb │ ├── have_method_spec.rb │ ├── have_public_instance_method_spec.rb │ ├── have_private_instance_method_spec.rb │ ├── have_protected_instance_method_spec.rb │ └── output_to_fd_spec.rb ├── integration │ ├── object_methods_spec.rb │ ├── interpreter_spec.rb │ └── tag_spec.rb ├── guards │ ├── user_spec.rb │ ├── quarantine_spec.rb │ ├── superuser_spec.rb │ ├── block_device_spec.rb │ ├── support_spec.rb │ ├── endian_spec.rb │ └── conflict_spec.rb ├── expectations │ ├── expectations_spec.rb │ └── should_spec.rb └── spec_helper.rb ├── bin ├── mkspec.bat ├── mspec.bat ├── mspec-ci.bat ├── mspec-run.bat ├── mspec-tag.bat ├── mkspec ├── mspec-ci ├── mspec ├── mspec-run └── mspec-tag ├── tool ├── sync │ └── .gitignore ├── find.rb ├── pull-latest-mspec-spec ├── wrap_with_guard.rb ├── check_require_spec_helper.rb └── tag_from_output.rb ├── Gemfile ├── lib ├── mspec │ ├── expectations.rb │ ├── mocks.rb │ ├── version.rb │ ├── helpers │ │ ├── flunk.rb │ │ ├── mock_to_path.rb │ │ ├── scratch.rb │ │ ├── warning.rb │ │ ├── fixture.rb │ │ ├── argv.rb │ │ ├── argf.rb │ │ ├── datetime.rb │ │ ├── fs.rb │ │ ├── io.rb │ │ ├── tmp.rb │ │ └── numeric.rb │ ├── runner │ │ ├── formatters │ │ │ ├── summary.rb │ │ │ ├── profile.rb │ │ │ ├── unit.rb │ │ │ ├── file.rb │ │ │ ├── dotted.rb │ │ │ ├── describe.rb │ │ │ ├── yaml.rb │ │ │ ├── multi.rb │ │ │ ├── specdoc.rb │ │ │ ├── stats.rb │ │ │ ├── html.rb │ │ │ ├── junit.rb │ │ │ └── launchable.rb │ │ ├── filters.rb │ │ ├── actions.rb │ │ ├── shared.rb │ │ ├── filters │ │ │ ├── match.rb │ │ │ ├── regexp.rb │ │ │ ├── tag.rb │ │ │ └── profile.rb │ │ ├── actions │ │ │ ├── timer.rb │ │ │ ├── filter.rb │ │ │ ├── profile.rb │ │ │ ├── tagpurge.rb │ │ │ ├── taglist.rb │ │ │ └── constants_leak_checker.rb │ │ ├── formatters.rb │ │ ├── object.rb │ │ ├── example.rb │ │ ├── tag.rb │ │ ├── evaluate.rb │ │ ├── exception.rb │ │ └── parallel.rb │ ├── matchers │ │ ├── skip.rb │ │ ├── method.rb │ │ ├── have_constant.rb │ │ ├── have_class_variable.rb │ │ ├── have_instance_variable.rb │ │ ├── be_nil.rb │ │ ├── be_nan.rb │ │ ├── be_true.rb │ │ ├── be_empty.rb │ │ ├── be_false.rb │ │ ├── be_true_or_false.rb │ │ ├── be_ancestor_of.rb │ │ ├── be_kind_of.rb │ │ ├── respond_to.rb │ │ ├── equal.rb │ │ ├── eql.rb │ │ ├── have_method.rb │ │ ├── variable.rb │ │ ├── be_an_instance_of.rb │ │ ├── have_private_method.rb │ │ ├── signed_zero.rb │ │ ├── have_instance_method.rb │ │ ├── have_singleton_method.rb │ │ ├── include_any_of.rb │ │ ├── infinity.rb │ │ ├── have_public_instance_method.rb │ │ ├── have_private_instance_method.rb │ │ ├── have_protected_instance_method.rb │ │ ├── include.rb │ │ ├── block_caller.rb │ │ ├── be_close.rb │ │ ├── be_computed_by.rb │ │ ├── match_yaml.rb │ │ ├── output.rb │ │ ├── output_to_fd.rb │ │ ├── complain.rb │ │ ├── base.rb │ │ └── equal_element.rb │ ├── guards │ │ ├── quarantine.rb │ │ ├── block_device.rb │ │ ├── support.rb │ │ ├── superuser.rb │ │ ├── endian.rb │ │ ├── bug.rb │ │ ├── conflict.rb │ │ ├── feature.rb │ │ └── version.rb │ ├── utils │ │ ├── deprecate.rb │ │ ├── warnings.rb │ │ ├── format.rb │ │ └── version.rb │ ├── guards.rb │ ├── runner.rb │ ├── helpers.rb │ ├── mocks │ │ └── object.rb │ ├── expectations │ │ ├── should.rb │ │ └── expectations.rb │ ├── matchers.rb │ └── commands │ │ ├── mspec-ci.rb │ │ └── mspec-run.rb └── mspec.rb ├── Rakefile ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── Gemfile.lock └── LICENSE /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/commands/fixtures/four.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/commands/fixtures/three.rb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bin/mkspec.bat: -------------------------------------------------------------------------------- 1 | @"ruby.exe" "%~dpn0" %* 2 | -------------------------------------------------------------------------------- /bin/mspec.bat: -------------------------------------------------------------------------------- 1 | @"ruby.exe" "%~dpn0" %* 2 | -------------------------------------------------------------------------------- /spec/commands/fixtures/one_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/commands/fixtures/two_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bin/mspec-ci.bat: -------------------------------------------------------------------------------- 1 | @"ruby.exe" "%~dpn0" %* 2 | -------------------------------------------------------------------------------- /bin/mspec-run.bat: -------------------------------------------------------------------------------- 1 | @"ruby.exe" "%~dpn0" %* 2 | -------------------------------------------------------------------------------- /bin/mspec-tag.bat: -------------------------------------------------------------------------------- 1 | @"ruby.exe" "%~dpn0" %* 2 | -------------------------------------------------------------------------------- /spec/commands/fixtures/level2/three_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/runner/filters/a.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | A#: 3 | - a 4 | - aa 5 | -------------------------------------------------------------------------------- /spec/utils/fixtures/this_file_raises.rb: -------------------------------------------------------------------------------- 1 | raise "This is a BAD file" 2 | -------------------------------------------------------------------------------- /spec/utils/fixtures/this_file_raises2.rb: -------------------------------------------------------------------------------- 1 | raise "This is a BAD file 2" 2 | -------------------------------------------------------------------------------- /tool/sync/.gitignore: -------------------------------------------------------------------------------- 1 | /jruby 2 | /rubinius 3 | /ruby 4 | /truffleruby 5 | -------------------------------------------------------------------------------- /spec/fixtures/my_ruby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo $RUBY_EXE 4 | exec ruby "$@" 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "rake", "~> 12.3" 4 | gem "rspec", "~> 3.0" 5 | -------------------------------------------------------------------------------- /lib/mspec/expectations.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/expectations/expectations' 2 | require 'mspec/expectations/should' 3 | -------------------------------------------------------------------------------- /lib/mspec/mocks.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/mocks/mock' 2 | require 'mspec/mocks/proxy' 3 | require 'mspec/mocks/object' 4 | -------------------------------------------------------------------------------- /lib/mspec/version.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/utils/version' 2 | 3 | module MSpec 4 | VERSION = SpecVersion.new "1.8.0" 5 | end 6 | -------------------------------------------------------------------------------- /lib/mspec/helpers/flunk.rb: -------------------------------------------------------------------------------- 1 | def flunk(msg = "This example is a failure") 2 | SpecExpectation.fail_with "Failed:", msg 3 | end 4 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/summary.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/base' 2 | 3 | class SummaryFormatter < BaseFormatter 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/print_interpreter_spec.rb: -------------------------------------------------------------------------------- 1 | unless defined?(RSpec) 2 | puts ENV["RUBY_EXE"] 3 | puts ruby_cmd(nil).split.first 4 | end 5 | -------------------------------------------------------------------------------- /spec/runner/filters/b.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | B.: 3 | - b 4 | - bb 5 | B::C#: 6 | - b! 7 | - b= 8 | - b? 9 | - "-" 10 | - "[]" 11 | - "[]=" 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /lib/mspec/matchers/skip.rb: -------------------------------------------------------------------------------- 1 | module MSpecMatchers 2 | private def skip(reason = 'no reason') 3 | raise SkippedSpecError, reason 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/mkspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path('../../lib', __FILE__) 4 | 5 | require 'mspec/commands/mkspec' 6 | 7 | MkSpec.main 8 | -------------------------------------------------------------------------------- /spec/fixtures/b_spec.rb: -------------------------------------------------------------------------------- 1 | unless defined?(RSpec) 2 | describe "Bar#baz" do 3 | it "works" do 4 | 1.should == 1 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/die_spec.rb: -------------------------------------------------------------------------------- 1 | unless defined?(RSpec) 2 | describe "Deadly#spec" do 3 | it "dies" do 4 | abort "DEAD" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/mspec-ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path('../../lib', __FILE__) 4 | 5 | require 'mspec/commands/mspec-ci' 6 | 7 | MSpecCI.main 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /bin/mspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path('../../lib', __FILE__) 4 | 5 | require 'mspec/commands/mspec' 6 | 7 | MSpecMain.main(false) 8 | -------------------------------------------------------------------------------- /bin/mspec-run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path('../../lib', __FILE__) 4 | 5 | require 'mspec/commands/mspec-run' 6 | 7 | MSpecRun.main 8 | -------------------------------------------------------------------------------- /bin/mspec-tag: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path('../../lib', __FILE__) 4 | 5 | require 'mspec/commands/mspec-tag' 6 | 7 | MSpecTag.main 8 | -------------------------------------------------------------------------------- /spec/runner/tags.txt: -------------------------------------------------------------------------------- 1 | fail(broken):Some#method? works 2 | incomplete(20%):The#best method ever 3 | benchmark(0.01825):The#fastest method today 4 | extended():"Multi-line\ntext\ntag" 5 | -------------------------------------------------------------------------------- /lib/mspec/runner/filters.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/filters/match' 2 | require 'mspec/runner/filters/regexp' 3 | require 'mspec/runner/filters/tag' 4 | require 'mspec/runner/filters/profile' 5 | -------------------------------------------------------------------------------- /spec/fixtures/chatty_spec.rb: -------------------------------------------------------------------------------- 1 | unless defined?(RSpec) 2 | describe "Chatty#spec" do 3 | it "prints too much" do 4 | STDOUT.puts "Hello\nIt's me!" 5 | 1.should == 1 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/mspec/helpers/mock_to_path.rb: -------------------------------------------------------------------------------- 1 | def mock_to_path(path) 2 | # Cannot use our Object#mock here since it conflicts with RSpec 3 | obj = MockObject.new('path') 4 | obj.should_receive(:to_path).and_return(path) 5 | obj 6 | end 7 | -------------------------------------------------------------------------------- /lib/mspec.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/utils/format' 2 | require 'mspec/matchers' 3 | require 'mspec/expectations' 4 | require 'mspec/mocks' 5 | require 'mspec/runner' 6 | require 'mspec/guards' 7 | require 'mspec/helpers' 8 | require 'mspec/version' 9 | -------------------------------------------------------------------------------- /lib/mspec/guards/quarantine.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/guards/guard' 2 | 3 | class QuarantineGuard < SpecGuard 4 | def match? 5 | true 6 | end 7 | end 8 | 9 | def quarantine!(&block) 10 | QuarantineGuard.new.run_unless(:quarantine!, &block) 11 | end 12 | -------------------------------------------------------------------------------- /tool/find.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | Dir.chdir('../rubyspec') do 3 | regexp = Regexp.new(ARGV[0]) 4 | Dir.glob('**/*.rb') do |file| 5 | contents = File.read(file) 6 | if regexp =~ contents 7 | puts file 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/config.mspec: -------------------------------------------------------------------------------- 1 | class MSpecScript 2 | set :target, 'ruby' 3 | 4 | set :tags_patterns, [ 5 | [%r(spec/fixtures/), 'spec/fixtures/tags/'], 6 | [/_spec.rb$/, '_tags.txt'] 7 | ] 8 | end 9 | -------------------------------------------------------------------------------- /lib/mspec/runner/actions.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/actions/tally' 2 | require 'mspec/runner/actions/timer' 3 | require 'mspec/runner/actions/filter' 4 | require 'mspec/runner/actions/tag' 5 | require 'mspec/runner/actions/taglist' 6 | require 'mspec/runner/actions/tagpurge' 7 | -------------------------------------------------------------------------------- /lib/mspec/utils/deprecate.rb: -------------------------------------------------------------------------------- 1 | module MSpec 2 | def self.deprecate(what, replacement) 3 | user_caller = caller.find { |line| !line.include?('lib/mspec') } 4 | $stderr.puts "\n#{what} is deprecated, use #{replacement} instead.\nfrom #{user_caller}" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/mspec/matchers/method.rb: -------------------------------------------------------------------------------- 1 | class MethodMatcher 2 | def initialize(method, include_super = true) 3 | @include_super = include_super 4 | @method = method.to_sym 5 | end 6 | 7 | def matches?(mod) 8 | raise Exception, "define #matches? in the subclass" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/a_spec.rb: -------------------------------------------------------------------------------- 1 | unless defined?(RSpec) 2 | describe "Foo#bar" do 3 | it "passes" do 4 | 1.should == 1 5 | end 6 | 7 | it "errors" do 8 | 1.should == 2 9 | end 10 | 11 | it "fails" do 12 | raise "failure" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/fixtures/object_methods_spec.rb: -------------------------------------------------------------------------------- 1 | unless defined?(RSpec) 2 | describe "Object" do 3 | it ".public_instance_methods(false) is empty" do 4 | Object.public_instance_methods(false).sort.should == 5 | [:should, :should_not, :should_not_receive, :should_receive, :stub!] 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/mspec/runner/shared.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/mspec' 2 | 3 | def it_behaves_like(desc, meth, obj = nil) 4 | before :all do 5 | @method = meth 6 | @object = obj 7 | end 8 | after :all do 9 | @method = nil 10 | @object = nil 11 | end 12 | 13 | it_should_behave_like desc.to_s 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/tagging_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | unless defined?(RSpec) 3 | describe "Tag#me" do 4 | it "passes" do 5 | 1.should == 1 6 | end 7 | 8 | it "errors" do 9 | 1.should == 2 10 | end 11 | 12 | it "érròrs in unicode" do 13 | 1.should == 2 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_constant.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/variable' 2 | 3 | class HaveConstantMatcher < VariableMatcher 4 | self.variables_method = :constants 5 | self.description = 'constant' 6 | end 7 | 8 | module MSpecMatchers 9 | private def have_constant(variable) 10 | HaveConstantMatcher.new(variable) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_class_variable.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/variable' 2 | 3 | class HaveClassVariableMatcher < VariableMatcher 4 | self.variables_method = :class_variables 5 | self.description = 'class variable' 6 | end 7 | 8 | module MSpecMatchers 9 | private def have_class_variable(variable) 10 | HaveClassVariableMatcher.new(variable) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mspec/helpers/scratch.rb: -------------------------------------------------------------------------------- 1 | module ScratchPad 2 | def self.clear 3 | @record = nil 4 | end 5 | 6 | def self.record(arg) 7 | @record = arg 8 | end 9 | 10 | def self.<<(arg) 11 | @record << arg 12 | end 13 | 14 | def self.recorded 15 | @record 16 | end 17 | 18 | def self.inspect 19 | "" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mspec/runner/filters/match.rb: -------------------------------------------------------------------------------- 1 | class MatchFilter 2 | def initialize(what, *strings) 3 | @what = what 4 | @strings = strings 5 | end 6 | 7 | def ===(string) 8 | @strings.any? { |s| string.include?(s) } 9 | end 10 | 11 | def register 12 | MSpec.register @what, self 13 | end 14 | 15 | def unregister 16 | MSpec.unregister @what, self 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_instance_variable.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/variable' 2 | 3 | class HaveInstanceVariableMatcher < VariableMatcher 4 | self.variables_method = :instance_variables 5 | self.description = 'instance variable' 6 | end 7 | 8 | module MSpecMatchers 9 | private def have_instance_variable(variable) 10 | HaveInstanceVariableMatcher.new(variable) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mspec/guards.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/guards/block_device' 2 | require 'mspec/guards/bug' 3 | require 'mspec/guards/conflict' 4 | require 'mspec/guards/endian' 5 | require 'mspec/guards/feature' 6 | require 'mspec/guards/guard' 7 | require 'mspec/guards/platform' 8 | require 'mspec/guards/quarantine' 9 | require 'mspec/guards/support' 10 | require 'mspec/guards/superuser' 11 | require 'mspec/guards/version' 12 | -------------------------------------------------------------------------------- /lib/mspec/runner/actions/timer.rb: -------------------------------------------------------------------------------- 1 | class TimerAction 2 | def register 3 | MSpec.register :start, self 4 | MSpec.register :finish, self 5 | end 6 | 7 | def start 8 | @start = Time.now 9 | end 10 | 11 | def finish 12 | @stop = Time.now 13 | end 14 | 15 | def elapsed 16 | @stop - @start 17 | end 18 | 19 | def format 20 | "Finished in %f seconds" % elapsed 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/mspec/runner.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/mocks' 2 | require 'mspec/runner/mspec' 3 | require 'mspec/runner/context' 4 | require 'mspec/runner/evaluate' 5 | require 'mspec/runner/example' 6 | require 'mspec/runner/exception' 7 | require 'mspec/runner/object' 8 | require 'mspec/runner/formatters' 9 | require 'mspec/runner/actions' 10 | require 'mspec/runner/filters' 11 | require 'mspec/runner/shared' 12 | require 'mspec/runner/tag' 13 | -------------------------------------------------------------------------------- /lib/mspec/guards/block_device.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/guards/guard' 2 | 3 | class BlockDeviceGuard < SpecGuard 4 | def match? 5 | platform_is_not :freebsd, :windows, :opal do 6 | block = `find /dev /devices -type b 2> /dev/null` 7 | return !(block.nil? || block.empty?) 8 | end 9 | 10 | false 11 | end 12 | end 13 | 14 | def with_block_device(&block) 15 | BlockDeviceGuard.new.run_if(:with_block_device, &block) 16 | end 17 | -------------------------------------------------------------------------------- /lib/mspec/matchers/be_nil.rb: -------------------------------------------------------------------------------- 1 | class BeNilMatcher 2 | def matches?(actual) 3 | @actual = actual 4 | @actual.nil? 5 | end 6 | 7 | def failure_message 8 | ["Expected #{@actual.inspect}", "to be nil"] 9 | end 10 | 11 | def negative_failure_message 12 | ["Expected #{@actual.inspect}", "not to be nil"] 13 | end 14 | end 15 | 16 | module MSpecMatchers 17 | private def be_nil 18 | BeNilMatcher.new 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/helpers/suppress_warning_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | require 'mspec/helpers' 4 | 5 | RSpec.describe Object, "#suppress_warning" do 6 | it "hides warnings" do 7 | suppress_warning do 8 | warn "should not be shown" 9 | end 10 | end 11 | 12 | it "yields the block" do 13 | a = 0 14 | suppress_warning do 15 | a = 1 16 | end 17 | expect(a).to eq(1) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/profile.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/dotted' 2 | require 'mspec/runner/actions/profile' 3 | 4 | class ProfileFormatter < DottedFormatter 5 | def initialize(out = nil) 6 | super(out) 7 | 8 | @describe_name = nil 9 | @describe_time = nil 10 | @describes = [] 11 | @its = [] 12 | end 13 | 14 | def register 15 | (@profile = ProfileAction.new).register 16 | super 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/mspec/matchers/be_nan.rb: -------------------------------------------------------------------------------- 1 | class BeNaNMatcher 2 | def matches?(actual) 3 | @actual = actual 4 | @actual.kind_of?(Float) && @actual.nan? 5 | end 6 | 7 | def failure_message 8 | ["Expected #{@actual}", "to be NaN"] 9 | end 10 | 11 | def negative_failure_message 12 | ["Expected #{@actual}", "not to be NaN"] 13 | end 14 | end 15 | 16 | module MSpecMatchers 17 | private def be_nan 18 | BeNaNMatcher.new 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mspec/matchers/be_true.rb: -------------------------------------------------------------------------------- 1 | class BeTrueMatcher 2 | def matches?(actual) 3 | @actual = actual 4 | @actual == true 5 | end 6 | 7 | def failure_message 8 | ["Expected #{@actual.inspect}", "to be true"] 9 | end 10 | 11 | def negative_failure_message 12 | ["Expected #{@actual.inspect}", "not to be true"] 13 | end 14 | end 15 | 16 | module MSpecMatchers 17 | private def be_true 18 | BeTrueMatcher.new 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mspec/guards/support.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/guards/platform' 2 | 3 | class SupportedGuard < SpecGuard 4 | def match? 5 | if @parameters.include? :ruby 6 | raise Exception, "improper use of not_supported_on guard" 7 | end 8 | !PlatformGuard.standard? and PlatformGuard.implementation?(*@parameters) 9 | end 10 | end 11 | 12 | def not_supported_on(*args, &block) 13 | SupportedGuard.new(*args).run_unless(:not_supported_on, &block) 14 | end 15 | -------------------------------------------------------------------------------- /lib/mspec/matchers/be_empty.rb: -------------------------------------------------------------------------------- 1 | class BeEmptyMatcher 2 | def matches?(actual) 3 | @actual = actual 4 | @actual.empty? 5 | end 6 | 7 | def failure_message 8 | ["Expected #{@actual.inspect}", "to be empty"] 9 | end 10 | 11 | def negative_failure_message 12 | ["Expected #{@actual.inspect}", "not to be empty"] 13 | end 14 | end 15 | 16 | module MSpecMatchers 17 | private def be_empty 18 | BeEmptyMatcher.new 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mspec/matchers/be_false.rb: -------------------------------------------------------------------------------- 1 | class BeFalseMatcher 2 | def matches?(actual) 3 | @actual = actual 4 | @actual == false 5 | end 6 | 7 | def failure_message 8 | ["Expected #{@actual.inspect}", "to be false"] 9 | end 10 | 11 | def negative_failure_message 12 | ["Expected #{@actual.inspect}", "not to be false"] 13 | end 14 | end 15 | 16 | module MSpecMatchers 17 | private def be_false 18 | BeFalseMatcher.new 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mspec/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/helpers/argf' 2 | require 'mspec/helpers/argv' 3 | require 'mspec/helpers/datetime' 4 | require 'mspec/helpers/fixture' 5 | require 'mspec/helpers/flunk' 6 | require 'mspec/helpers/fs' 7 | require 'mspec/helpers/io' 8 | require 'mspec/helpers/mock_to_path' 9 | require 'mspec/helpers/numeric' 10 | require 'mspec/helpers/ruby_exe' 11 | require 'mspec/helpers/scratch' 12 | require 'mspec/helpers/tmp' 13 | require 'mspec/helpers/warning' 14 | -------------------------------------------------------------------------------- /spec/matchers/block_caller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe BlockingMatcher do 6 | it 'matches when a Proc blocks the caller' do 7 | expect(BlockingMatcher.new.matches?(proc { sleep })).to eq(true) 8 | end 9 | 10 | it 'does not match when a Proc does not block the caller' do 11 | expect(BlockingMatcher.new.matches?(proc { 1 })).to eq(false) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/integration/object_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | expected_output = < 3.10.0) 8 | rspec-expectations (~> 3.10.0) 9 | rspec-mocks (~> 3.10.0) 10 | rspec-core (3.10.1) 11 | rspec-support (~> 3.10.0) 12 | rspec-expectations (3.10.1) 13 | diff-lcs (>= 1.2.0, < 2.0) 14 | rspec-support (~> 3.10.0) 15 | rspec-mocks (3.10.2) 16 | diff-lcs (>= 1.2.0, < 2.0) 17 | rspec-support (~> 3.10.0) 18 | rspec-support (3.10.2) 19 | 20 | PLATFORMS 21 | java 22 | ruby 23 | 24 | DEPENDENCIES 25 | rake (~> 12.3) 26 | rspec (~> 3.0) 27 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/file.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/dotted' 2 | 3 | class FileFormatter < DottedFormatter 4 | # Unregisters DottedFormatter#before, #after methods and 5 | # registers #load, #unload, which perform the same duties 6 | # as #before, #after in DottedFormatter. 7 | def register 8 | super 9 | 10 | MSpec.unregister :before, self 11 | MSpec.unregister :after, self 12 | 13 | MSpec.register :load, self 14 | MSpec.register :unload, self 15 | end 16 | 17 | def load(state = nil) 18 | before(state) 19 | end 20 | 21 | def unload(state = nil) 22 | after(state) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mspec/utils/format.rb: -------------------------------------------------------------------------------- 1 | # If the implementation on which the specs are run cannot 2 | # load pp from the standard library, add a pp.rb file that 3 | # defines the #pretty_inspect method on Object or Kernel. 4 | begin 5 | require 'pp' 6 | rescue LoadError 7 | module Kernel 8 | def pretty_inspect 9 | inspect 10 | end 11 | end 12 | end 13 | 14 | module MSpec 15 | def self.format(obj) 16 | if String === obj and obj.include?("\n") 17 | "\n#{obj.inspect.gsub('\n', "\n")}" 18 | else 19 | obj.pretty_inspect.chomp 20 | end 21 | rescue => e 22 | "#<#{obj.class}>(#pretty_inspect raised #{e.inspect})" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mspec/matchers/eql.rb: -------------------------------------------------------------------------------- 1 | class EqlMatcher 2 | def initialize(expected) 3 | @expected = expected 4 | end 5 | 6 | def matches?(actual) 7 | @actual = actual 8 | @actual.eql?(@expected) 9 | end 10 | 11 | def failure_message 12 | ["Expected #{MSpec.format(@actual)}", 13 | "to have same value and type as #{MSpec.format(@expected)}"] 14 | end 15 | 16 | def negative_failure_message 17 | ["Expected #{MSpec.format(@actual)}", 18 | "not to have same value or type as #{MSpec.format(@expected)}"] 19 | end 20 | end 21 | 22 | module MSpecMatchers 23 | private def eql(expected) 24 | EqlMatcher.new(expected) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_method.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/method' 2 | 3 | class HaveMethodMatcher < MethodMatcher 4 | def matches?(mod) 5 | @mod = mod 6 | @mod.methods(@include_super).include? @method 7 | end 8 | 9 | def failure_message 10 | ["Expected #{@mod} to have method '#{@method.to_s}'", 11 | "but it does not"] 12 | end 13 | 14 | def negative_failure_message 15 | ["Expected #{@mod} NOT to have method '#{@method.to_s}'", 16 | "but it does"] 17 | end 18 | end 19 | 20 | module MSpecMatchers 21 | private def have_method(method, include_super = true) 22 | HaveMethodMatcher.new method, include_super 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/dotted.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/base' 2 | 3 | class DottedFormatter < BaseFormatter 4 | def register 5 | super 6 | MSpec.register :after, self 7 | end 8 | 9 | # Callback for the MSpec :after event. Prints an indicator 10 | # for the result of evaluating this example as follows: 11 | # . = No failure or error 12 | # F = An SpecExpectationNotMetError was raised 13 | # E = Any exception other than SpecExpectationNotMetError 14 | def after(state = nil) 15 | super(state) 16 | 17 | if exception? 18 | print failure? ? "F" : "E" 19 | else 20 | print "." 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/mspec/matchers/variable.rb: -------------------------------------------------------------------------------- 1 | class VariableMatcher 2 | class << self 3 | attr_accessor :variables_method, :description 4 | end 5 | 6 | def initialize(variable) 7 | @variable = variable.to_sym 8 | end 9 | 10 | def matches?(object) 11 | @object = object 12 | @object.send(self.class.variables_method).include? @variable 13 | end 14 | 15 | def failure_message 16 | ["Expected #{@object} to have #{self.class.description} '#{@variable}'", 17 | "but it does not"] 18 | end 19 | 20 | def negative_failure_message 21 | ["Expected #{@object} NOT to have #{self.class.description} '#{@variable}'", 22 | "but it does"] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/matchers/be_true_or_false_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe BeTrueOrFalseMatcher do 6 | it "matches when actual is true" do 7 | expect(BeTrueOrFalseMatcher.new.matches?(true)).to eq(true) 8 | end 9 | 10 | it "matches when actual is false" do 11 | expect(BeTrueOrFalseMatcher.new.matches?(false)).to eq(true) 12 | end 13 | 14 | it "provides a useful failure message" do 15 | matcher = BeTrueOrFalseMatcher.new 16 | matcher.matches?("some string") 17 | expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be true or false"]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/helpers/argv_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | require 'mspec/helpers' 4 | 5 | RSpec.describe Object, "#argv" do 6 | before :each do 7 | ScratchPad.clear 8 | 9 | @saved_argv = ARGV.dup 10 | @argv = ["a", "b"] 11 | end 12 | 13 | it "replaces and restores the value of ARGV" do 14 | argv @argv 15 | expect(ARGV).to eq(@argv) 16 | argv :restore 17 | expect(ARGV).to eq(@saved_argv) 18 | end 19 | 20 | it "yields to the block after setting ARGV" do 21 | argv @argv do 22 | ScratchPad.record ARGV.dup 23 | end 24 | expect(ScratchPad.recorded).to eq(@argv) 25 | expect(ARGV).to eq(@saved_argv) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/mspec/runner/object.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | private def before(at = :each, &block) 3 | MSpec.current.before at, &block 4 | end 5 | 6 | private def after(at = :each, &block) 7 | MSpec.current.after at, &block 8 | end 9 | 10 | private def describe(description, options = nil, &block) 11 | MSpec.describe description, options, &block 12 | end 13 | 14 | private def it(desc, &block) 15 | MSpec.current.it desc, &block 16 | end 17 | 18 | private def it_should_behave_like(desc) 19 | MSpec.current.it_should_behave_like desc 20 | end 21 | 22 | alias_method :context, :describe 23 | private :context 24 | alias_method :specify, :it 25 | private :specify 26 | end 27 | -------------------------------------------------------------------------------- /spec/helpers/flunk_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/runner/mspec' 4 | require 'mspec/guards' 5 | require 'mspec/helpers' 6 | 7 | RSpec.describe Object, "#flunk" do 8 | before :each do 9 | allow(MSpec).to receive(:actions) 10 | allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) 11 | end 12 | 13 | it "raises an SpecExpectationNotMetError unconditionally" do 14 | expect { flunk }.to raise_error(SpecExpectationNotMetError) 15 | end 16 | 17 | it "accepts on argument for an optional message" do 18 | expect {flunk "test"}.to raise_error(SpecExpectationNotMetError) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mspec/matchers/be_an_instance_of.rb: -------------------------------------------------------------------------------- 1 | class BeAnInstanceOfMatcher 2 | def initialize(expected) 3 | @expected = expected 4 | end 5 | 6 | def matches?(actual) 7 | @actual = actual 8 | @actual.instance_of?(@expected) 9 | end 10 | 11 | def failure_message 12 | ["Expected #{@actual.inspect} (#{@actual.class})", 13 | "to be an instance of #{@expected}"] 14 | end 15 | 16 | def negative_failure_message 17 | ["Expected #{@actual.inspect} (#{@actual.class})", 18 | "not to be an instance of #{@expected}"] 19 | end 20 | end 21 | 22 | module MSpecMatchers 23 | private def be_an_instance_of(expected) 24 | BeAnInstanceOfMatcher.new(expected) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_private_method.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/method' 2 | 3 | class HavePrivateMethodMatcher < MethodMatcher 4 | def matches?(mod) 5 | @mod = mod 6 | mod.private_methods(@include_super).include? @method 7 | end 8 | 9 | def failure_message 10 | ["Expected #{@mod} to have private method '#{@method.to_s}'", 11 | "but it does not"] 12 | end 13 | 14 | def negative_failure_message 15 | ["Expected #{@mod} NOT to have private method '#{@method.to_s}'", 16 | "but it does"] 17 | end 18 | end 19 | 20 | module MSpecMatchers 21 | private def have_private_method(method, include_super = true) 22 | HavePrivateMethodMatcher.new method, include_super 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/helpers/mock_to_path_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | require 'mspec/helpers' 4 | require 'mspec/mocks' 5 | 6 | RSpec.describe Object, "#mock_to_path" do 7 | before :each do 8 | state = double("run state").as_null_object 9 | expect(MSpec).to receive(:current).and_return(state) 10 | end 11 | 12 | it "returns an object that responds to #to_path" do 13 | obj = mock_to_path("foo") 14 | expect(obj).to be_a(MockObject) 15 | expect(obj).to respond_to(:to_path) 16 | obj.to_path 17 | end 18 | 19 | it "returns the provided path when #to_path is called" do 20 | obj = mock_to_path("/tmp/foo") 21 | expect(obj.to_path).to eq("/tmp/foo") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/mspec/matchers/signed_zero.rb: -------------------------------------------------------------------------------- 1 | class SignedZeroMatcher 2 | def initialize(expected_sign) 3 | @expected_sign = expected_sign 4 | end 5 | 6 | def matches?(actual) 7 | @actual = actual 8 | (1.0/actual).infinite? == @expected_sign 9 | end 10 | 11 | def failure_message 12 | ["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}0.0"] 13 | end 14 | 15 | def negative_failure_message 16 | ["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}0.0"] 17 | end 18 | end 19 | 20 | module MSpecMatchers 21 | private def be_positive_zero 22 | SignedZeroMatcher.new(1) 23 | end 24 | 25 | private def be_negative_zero 26 | SignedZeroMatcher.new(-1) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_instance_method.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/method' 2 | 3 | class HaveInstanceMethodMatcher < MethodMatcher 4 | def matches?(mod) 5 | @mod = mod 6 | mod.instance_methods(@include_super).include? @method 7 | end 8 | 9 | def failure_message 10 | ["Expected #{@mod} to have instance method '#{@method.to_s}'", 11 | "but it does not"] 12 | end 13 | 14 | def negative_failure_message 15 | ["Expected #{@mod} NOT to have instance method '#{@method.to_s}'", 16 | "but it does"] 17 | end 18 | end 19 | 20 | module MSpecMatchers 21 | private def have_instance_method(method, include_super = true) 22 | HaveInstanceMethodMatcher.new method, include_super 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_singleton_method.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/method' 2 | 3 | class HaveSingletonMethodMatcher < MethodMatcher 4 | def matches?(obj) 5 | @obj = obj 6 | obj.singleton_methods(@include_super).include? @method 7 | end 8 | 9 | def failure_message 10 | ["Expected #{@obj} to have singleton method '#{@method.to_s}'", 11 | "but it does not"] 12 | end 13 | 14 | def negative_failure_message 15 | ["Expected #{@obj} NOT to have singleton method '#{@method.to_s}'", 16 | "but it does"] 17 | end 18 | end 19 | 20 | module MSpecMatchers 21 | private def have_singleton_method(method, include_super = true) 22 | HaveSingletonMethodMatcher.new method, include_super 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mspec/matchers/include_any_of.rb: -------------------------------------------------------------------------------- 1 | class IncludeAnyOfMatcher 2 | def initialize(*expected) 3 | @expected = expected 4 | end 5 | 6 | def matches?(actual) 7 | @actual = actual 8 | @expected.each do |e| 9 | if @actual.include?(e) 10 | return true 11 | end 12 | end 13 | return false 14 | end 15 | 16 | def failure_message 17 | ["Expected #{@actual.inspect}", "to include any of #{@expected.inspect}"] 18 | end 19 | 20 | def negative_failure_message 21 | ["Expected #{@actual.inspect}", "not to include any of #{@expected.inspect}"] 22 | end 23 | end 24 | 25 | module MSpecMatchers 26 | private def include_any_of(*expected) 27 | IncludeAnyOfMatcher.new(*expected) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/mspec/matchers/infinity.rb: -------------------------------------------------------------------------------- 1 | class InfinityMatcher 2 | def initialize(expected_sign) 3 | @expected_sign = expected_sign 4 | end 5 | 6 | def matches?(actual) 7 | @actual = actual 8 | @actual.kind_of?(Float) && @actual.infinite? == @expected_sign 9 | end 10 | 11 | def failure_message 12 | ["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}Infinity"] 13 | end 14 | 15 | def negative_failure_message 16 | ["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}Infinity"] 17 | end 18 | end 19 | 20 | module MSpecMatchers 21 | private def be_positive_infinity 22 | InfinityMatcher.new(1) 23 | end 24 | 25 | private def be_negative_infinity 26 | InfinityMatcher.new(-1) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/mspec/helpers/fixture.rb: -------------------------------------------------------------------------------- 1 | # Returns the name of a fixture file by adjoining the directory 2 | # of the +file+ argument with "fixtures" and the contents of the 3 | # +args+ array. For example, 4 | # 5 | # +file+ == "some/example_spec.rb" 6 | # 7 | # and 8 | # 9 | # +args+ == ["subdir", "file.txt"] 10 | # 11 | # then the result is the expanded path of 12 | # 13 | # "some/fixtures/subdir/file.txt". 14 | def fixture(file, *args) 15 | path = File.dirname(file) 16 | path = path[0..-7] if path[-7..-1] == "/shared" 17 | fixtures = path[-9..-1] == "/fixtures" ? "" : "fixtures" 18 | if File.respond_to?(:realpath) 19 | path = File.realpath(path) 20 | else 21 | path = File.expand_path(path) 22 | end 23 | File.join(path, fixtures, args) 24 | end 25 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_public_instance_method.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/method' 2 | 3 | class HavePublicInstanceMethodMatcher < MethodMatcher 4 | def matches?(mod) 5 | @mod = mod 6 | mod.public_instance_methods(@include_super).include? @method 7 | end 8 | 9 | def failure_message 10 | ["Expected #{@mod} to have public instance method '#{@method.to_s}'", 11 | "but it does not"] 12 | end 13 | 14 | def negative_failure_message 15 | ["Expected #{@mod} NOT to have public instance method '#{@method.to_s}'", 16 | "but it does"] 17 | end 18 | end 19 | 20 | module MSpecMatchers 21 | private def have_public_instance_method(method, include_super = true) 22 | HavePublicInstanceMethodMatcher.new method, include_super 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_private_instance_method.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/method' 2 | 3 | class HavePrivateInstanceMethodMatcher < MethodMatcher 4 | def matches?(mod) 5 | @mod = mod 6 | mod.private_instance_methods(@include_super).include? @method 7 | end 8 | 9 | def failure_message 10 | ["Expected #{@mod} to have private instance method '#{@method.to_s}'", 11 | "but it does not"] 12 | end 13 | 14 | def negative_failure_message 15 | ["Expected #{@mod} NOT to have private instance method '#{@method.to_s}'", 16 | "but it does"] 17 | end 18 | end 19 | 20 | module MSpecMatchers 21 | private def have_private_instance_method(method, include_super = true) 22 | HavePrivateInstanceMethodMatcher.new method, include_super 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/helpers/scratch_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | require 'mspec/helpers' 4 | 5 | RSpec.describe ScratchPad do 6 | it "records an object and returns a previously recorded object" do 7 | ScratchPad.record :this 8 | expect(ScratchPad.recorded).to eq(:this) 9 | end 10 | 11 | it "clears the recorded object" do 12 | ScratchPad.record :that 13 | expect(ScratchPad.recorded).to eq(:that) 14 | ScratchPad.clear 15 | expect(ScratchPad.recorded).to eq(nil) 16 | end 17 | 18 | it "provides a convenience shortcut to append to a previously recorded object" do 19 | ScratchPad.record [] 20 | ScratchPad << :new 21 | ScratchPad << :another 22 | expect(ScratchPad.recorded).to eq([:new, :another]) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /tool/pull-latest-mspec-spec: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Assumes all commits have been synchronized to https://github.com/ruby/spec 4 | # See spec/mspec/tool/sync/sync-rubyspec.rb 5 | 6 | function sync { 7 | dir="$1" 8 | repo="$2" 9 | short_repo_name="ruby/$(basename "$repo" .git)" 10 | 11 | rm -rf "$dir" 12 | git clone --depth 1 "$repo" "$dir" 13 | commit=$(git -C "$dir" log -n 1 --format='%h') 14 | rm -rf "$dir/.git" 15 | 16 | # Remove CI files to avoid confusion 17 | rm -f "$dir/appveyor.yml" 18 | rm -f "$dir/.travis.yml" 19 | rm -rf "$dir/.github" 20 | 21 | git add "$dir" 22 | git commit -m "Update to ${short_repo_name}@${commit}" 23 | } 24 | 25 | sync spec/mspec https://github.com/ruby/mspec.git 26 | sync spec/ruby https://github.com/ruby/spec.git 27 | -------------------------------------------------------------------------------- /lib/mspec/matchers/have_protected_instance_method.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/method' 2 | 3 | class HaveProtectedInstanceMethodMatcher < MethodMatcher 4 | def matches?(mod) 5 | @mod = mod 6 | mod.protected_instance_methods(@include_super).include? @method 7 | end 8 | 9 | def failure_message 10 | ["Expected #{@mod} to have protected instance method '#{@method.to_s}'", 11 | "but it does not"] 12 | end 13 | 14 | def negative_failure_message 15 | ["Expected #{@mod} NOT to have protected instance method '#{@method.to_s}'", 16 | "but it does"] 17 | end 18 | end 19 | 20 | module MSpecMatchers 21 | private def have_protected_instance_method(method, include_super = true) 22 | HaveProtectedInstanceMethodMatcher.new method, include_super 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/describe.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/dotted' 2 | 3 | class DescribeFormatter < DottedFormatter 4 | # Callback for the MSpec :finish event. Prints a summary of 5 | # the number of errors and failures for each +describe+ block. 6 | def finish 7 | describes = Hash.new { |h,k| h[k] = Tally.new } 8 | 9 | @exceptions.each do |exc| 10 | desc = describes[exc.describe] 11 | exc.failure? ? desc.failures! : desc.errors! 12 | end 13 | 14 | print "\n" 15 | describes.each do |d, t| 16 | text = d.size > 40 ? "#{d[0,37]}..." : d.ljust(40) 17 | print "\n#{text} #{t.failure}, #{t.error}" 18 | end 19 | print "\n" unless describes.empty? 20 | 21 | print "\n#{@timer.format}\n\n#{@tally.format}\n" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/mspec/matchers/include.rb: -------------------------------------------------------------------------------- 1 | class IncludeMatcher 2 | def initialize(*expected) 3 | @expected = expected 4 | end 5 | 6 | def matches?(actual) 7 | @actual = actual 8 | @expected.each do |e| 9 | @element = e 10 | unless @actual.include?(e) 11 | return false 12 | end 13 | end 14 | return true 15 | end 16 | 17 | def failure_message 18 | ["Expected #{MSpec.format(@actual)}", "to include #{MSpec.format(@element)}"] 19 | end 20 | 21 | def negative_failure_message 22 | ["Expected #{MSpec.format(@actual)}", "not to include #{MSpec.format(@element)}"] 23 | end 24 | end 25 | 26 | # Cannot override #include at the toplevel in MRI 27 | module MSpecMatchers 28 | private def include(*expected) 29 | IncludeMatcher.new(*expected) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/helpers/tmp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | require 'mspec/helpers' 4 | 5 | RSpec.describe Object, "#tmp" do 6 | before :all do 7 | @dir = SPEC_TEMP_DIR 8 | end 9 | 10 | it "returns a name relative to the current working directory" do 11 | expect(tmp("test.txt")).to eq("#{@dir}/#{SPEC_TEMP_UNIQUIFIER}-test.txt") 12 | end 13 | 14 | it "returns a 'unique' name on repeated calls" do 15 | a = tmp("text.txt") 16 | b = tmp("text.txt") 17 | expect(a).not_to eq(b) 18 | end 19 | 20 | it "does not 'uniquify' the name if requested not to" do 21 | expect(tmp("test.txt", false)).to eq("#{@dir}/test.txt") 22 | end 23 | 24 | it "returns the name of the temporary directory when passed an empty string" do 25 | expect(tmp("")).to eq("#{@dir}/") 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/mspec/guards/bug.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/guards/version' 2 | 3 | class BugGuard < VersionGuard 4 | def initialize(bug, requirement) 5 | @bug = bug 6 | if String === requirement 7 | MSpec.deprecate "ruby_bug with a single version", 'an exclusive range ("2.1"..."2.3")' 8 | super(FULL_RUBY_VERSION, requirement) 9 | @requirement = SpecVersion.new requirement, true 10 | else 11 | super(FULL_RUBY_VERSION, requirement) 12 | end 13 | end 14 | 15 | def match? 16 | return false if MSpec.mode? :no_ruby_bug 17 | return false unless PlatformGuard.standard? 18 | 19 | if Range === @requirement 20 | super 21 | else 22 | FULL_RUBY_VERSION <= @requirement 23 | end 24 | end 25 | end 26 | 27 | def ruby_bug(bug, requirement, &block) 28 | BugGuard.new(bug, requirement).run_unless(:ruby_bug, &block) 29 | end 30 | -------------------------------------------------------------------------------- /lib/mspec/guards/conflict.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/guards/guard' 2 | require 'mspec/utils/deprecate' 3 | 4 | class ConflictsGuard < SpecGuard 5 | def initialize(*args) 6 | MSpec.deprecate 'conflicts_with', 'guard -> { condition } do' 7 | super(*args) 8 | end 9 | 10 | def match? 11 | # Always convert constants to symbols regardless of version. 12 | constants = Object.constants.map { |x| x.to_sym } 13 | @parameters.any? { |mod| constants.include? mod } 14 | end 15 | end 16 | 17 | # In some cases, libraries will modify another Ruby method's 18 | # behavior. The specs for the method's behavior will then fail 19 | # if that library is loaded. This guard will not run if any of 20 | # the specified constants exist in Object.constants. 21 | def conflicts_with(*modules, &block) 22 | ConflictsGuard.new(*modules).run_unless(:conflicts_with, &block) 23 | end 24 | -------------------------------------------------------------------------------- /lib/mspec/matchers/block_caller.rb: -------------------------------------------------------------------------------- 1 | class BlockingMatcher 2 | def matches?(block) 3 | t = Thread.new do 4 | block.call 5 | end 6 | 7 | loop do 8 | case t.status 9 | when "sleep" # blocked 10 | t.kill 11 | t.join 12 | return true 13 | when false # terminated normally, so never blocked 14 | t.join 15 | return false 16 | when nil # terminated exceptionally 17 | t.value 18 | else 19 | Thread.pass 20 | end 21 | end 22 | end 23 | 24 | def failure_message 25 | ['Expected the given Proc', 'to block the caller'] 26 | end 27 | 28 | def negative_failure_message 29 | ['Expected the given Proc', 'to not block the caller'] 30 | end 31 | end 32 | 33 | module MSpecMatchers 34 | private def block_caller 35 | BlockingMatcher.new 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/runner/formatters/summary_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | require 'mspec/runner/formatters/summary' 3 | require 'mspec/runner/example' 4 | 5 | RSpec.describe SummaryFormatter, "#after" do 6 | before :each do 7 | $stdout = @out = IOStub.new 8 | @formatter = SummaryFormatter.new 9 | @formatter.register 10 | context = ContextState.new "describe" 11 | @state = ExampleState.new(context, "it") 12 | end 13 | 14 | after :each do 15 | $stdout = STDOUT 16 | end 17 | 18 | it "does not print anything" do 19 | exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing") 20 | @formatter.exception exc 21 | exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful") 22 | @formatter.exception exc 23 | @formatter.after(@state) 24 | expect(@out).to eq("") 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mspec/runner/example.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/mspec' 2 | 3 | # Holds some of the state of the example (i.e. +it+ block) that is 4 | # being evaluated. See also +ContextState+. 5 | class ExampleState 6 | attr_reader :context, :it, :example 7 | 8 | def initialize(context, it, example = nil) 9 | @context = context 10 | @it = it 11 | @example = example 12 | end 13 | 14 | def context=(context) 15 | @description = nil 16 | @context = context 17 | end 18 | 19 | def describe 20 | @context.description 21 | end 22 | 23 | def description 24 | @description ||= "#{describe} #{@it}" 25 | end 26 | 27 | def filtered? 28 | incl = MSpec.include 29 | excl = MSpec.exclude 30 | included = incl.empty? || incl.any? { |f| f === description } 31 | included &&= excl.empty? || !excl.any? { |f| f === description } 32 | !included 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mspec/matchers/be_close.rb: -------------------------------------------------------------------------------- 1 | TOLERANCE = 0.00003 unless Object.const_defined?(:TOLERANCE) 2 | # To account for GC, context switches, other processes, load, etc. 3 | TIME_TOLERANCE = 20.0 unless Object.const_defined?(:TIME_TOLERANCE) 4 | 5 | class BeCloseMatcher 6 | def initialize(expected, tolerance) 7 | @expected = expected 8 | @tolerance = tolerance 9 | end 10 | 11 | def matches?(actual) 12 | @actual = actual 13 | (@actual - @expected).abs <= @tolerance 14 | end 15 | 16 | def failure_message 17 | ["Expected #{@actual}", "to be within #{@expected} +/- #{@tolerance}"] 18 | end 19 | 20 | def negative_failure_message 21 | ["Expected #{@actual}", "not to be within #{@expected} +/- #{@tolerance}"] 22 | end 23 | end 24 | 25 | module MSpecMatchers 26 | private def be_close(expected, tolerance) 27 | BeCloseMatcher.new(expected, tolerance) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/matchers/be_empty_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe BeEmptyMatcher do 6 | it "matches when actual is empty" do 7 | expect(BeEmptyMatcher.new.matches?("")).to eq(true) 8 | end 9 | 10 | it "does not match when actual is not empty" do 11 | expect(BeEmptyMatcher.new.matches?([10])).to eq(false) 12 | end 13 | 14 | it "provides a useful failure message" do 15 | matcher = BeEmptyMatcher.new 16 | matcher.matches?("not empty string") 17 | expect(matcher.failure_message).to eq(["Expected \"not empty string\"", "to be empty"]) 18 | end 19 | 20 | it "provides a useful negative failure message" do 21 | matcher = BeEmptyMatcher.new 22 | matcher.matches?("") 23 | expect(matcher.negative_failure_message).to eq(["Expected \"\"", "not to be empty"]) 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | ruby: [ 2.6, 2.7, '3.0', 3.1, 3.2, 3.3, 3.4, ruby-head, truffleruby-head ] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v5 12 | - uses: ruby/setup-ruby@v1 13 | with: 14 | ruby-version: ${{ matrix.ruby }} 15 | bundler-cache: true 16 | - run: bundle exec rspec 17 | 18 | rubyspec: 19 | name: Check ruby/spec still passes with MSpec changes 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v5 23 | - uses: actions/checkout@v5 24 | with: 25 | repository: ruby/spec 26 | path: rubyspec 27 | - uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ruby 30 | bundler: none 31 | - run: bin/mspec --timeout 30 rubyspec 32 | -------------------------------------------------------------------------------- /spec/helpers/numeric_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | require 'mspec/helpers' 4 | 5 | RSpec.describe Object, "#bignum_value" do 6 | it "returns a value that is an instance of Bignum on any platform" do 7 | expect(bignum_value).to be > fixnum_max 8 | end 9 | 10 | it "returns the default value incremented by the argument" do 11 | expect(bignum_value(42)).to eq(bignum_value + 42) 12 | end 13 | end 14 | 15 | RSpec.describe Object, "-bignum_value" do 16 | it "returns a value that is an instance of Bignum on any platform" do 17 | expect(-bignum_value).to be < fixnum_min 18 | end 19 | end 20 | 21 | RSpec.describe Object, "#nan_value" do 22 | it "returns NaN" do 23 | expect(nan_value.nan?).to be_truthy 24 | end 25 | end 26 | 27 | RSpec.describe Object, "#infinity_value" do 28 | it "returns Infinity" do 29 | expect(infinity_value.infinite?).to eq(1) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/mspec/runner/tag.rb: -------------------------------------------------------------------------------- 1 | class SpecTag 2 | attr_accessor :tag, :comment, :description 3 | 4 | def initialize(string = nil) 5 | parse(string) if string 6 | end 7 | 8 | def parse(string) 9 | m = /^([^()#:]+)(\(([^)]+)?\))?:(.*)$/.match string 10 | @tag, @comment, description = m.values_at(1, 3, 4) if m 11 | @description = unescape description 12 | end 13 | 14 | def unescape(str) 15 | return unless str 16 | if str[0] == ?" and str[-1] == ?" 17 | str[1..-2].gsub('\n', "\n") 18 | else 19 | str 20 | end 21 | end 22 | 23 | def escape(str) 24 | if str.include? "\n" 25 | %["#{str.gsub("\n", '\n')}"] 26 | else 27 | str 28 | end 29 | end 30 | 31 | def to_s 32 | "#{@tag}#{ "(#{@comment})" if @comment }:#{escape @description}" 33 | end 34 | 35 | def ==(o) 36 | @tag == o.tag and @comment == o.comment and @description == o.description 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/helpers/argf_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | require 'mspec/helpers' 4 | 5 | RSpec.describe Object, "#argf" do 6 | before :each do 7 | @saved_argv = ARGV.dup 8 | @argv = [__FILE__] 9 | end 10 | 11 | it "sets @argf to an instance of ARGF.class with the given argv" do 12 | argf @argv do 13 | expect(@argf).to be_an_instance_of ARGF.class 14 | expect(@argf.filename).to eq(@argv.first) 15 | end 16 | expect(@argf).to be_nil 17 | end 18 | 19 | it "does not alter ARGV nor ARGF" do 20 | argf @argv do 21 | end 22 | expect(ARGV).to eq(@saved_argv) 23 | expect(ARGF.argv).to eq(@saved_argv) 24 | end 25 | 26 | it "does not close STDIN" do 27 | argf ['-'] do 28 | end 29 | expect(STDIN).not_to be_closed 30 | end 31 | 32 | it "disallows nested calls" do 33 | argf @argv do 34 | expect { argf @argv }.to raise_error 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/helpers/fixture_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | require 'mspec/helpers' 4 | 5 | RSpec.describe Object, "#fixture" do 6 | before :each do 7 | @dir = File.realpath("..", __FILE__) 8 | end 9 | 10 | it "returns the expanded path to a fixture file" do 11 | name = fixture(__FILE__, "subdir", "file.txt") 12 | expect(name).to eq("#{@dir}/fixtures/subdir/file.txt") 13 | end 14 | 15 | it "omits '/shared' if it is the suffix of the directory string" do 16 | name = fixture("#{@dir}/shared/file.rb", "subdir", "file.txt") 17 | expect(name).to eq("#{@dir}/fixtures/subdir/file.txt") 18 | end 19 | 20 | it "does not append '/fixtures' if it is the suffix of the directory string" do 21 | commands_dir = "#{File.dirname(@dir)}/commands" 22 | name = fixture("#{commands_dir}/fixtures/file.rb", "subdir", "file.txt") 23 | expect(name).to eq("#{commands_dir}/fixtures/subdir/file.txt") 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/matchers/be_nan_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/guards' 4 | require 'mspec/helpers' 5 | require 'mspec/matchers' 6 | 7 | RSpec.describe BeNaNMatcher do 8 | it "matches when actual is NaN" do 9 | expect(BeNaNMatcher.new.matches?(nan_value)).to eq(true) 10 | end 11 | 12 | it "does not match when actual is not NaN" do 13 | expect(BeNaNMatcher.new.matches?(1.0)).to eq(false) 14 | expect(BeNaNMatcher.new.matches?(0)).to eq(false) 15 | end 16 | 17 | it "provides a useful failure message" do 18 | matcher = BeNaNMatcher.new 19 | matcher.matches?(0) 20 | expect(matcher.failure_message).to eq(["Expected 0", "to be NaN"]) 21 | end 22 | 23 | it "provides a useful negative failure message" do 24 | matcher = BeNaNMatcher.new 25 | matcher.matches?(nan_value) 26 | expect(matcher.negative_failure_message).to eq(["Expected NaN", "not to be NaN"]) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/matchers/be_nil_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe BeNilMatcher do 6 | it "matches when actual is nil" do 7 | expect(BeNilMatcher.new.matches?(nil)).to eq(true) 8 | end 9 | 10 | it "does not match when actual is not nil" do 11 | expect(BeNilMatcher.new.matches?("")).to eq(false) 12 | expect(BeNilMatcher.new.matches?(false)).to eq(false) 13 | expect(BeNilMatcher.new.matches?(0)).to eq(false) 14 | end 15 | 16 | it "provides a useful failure message" do 17 | matcher = BeNilMatcher.new 18 | matcher.matches?("some string") 19 | expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be nil"]) 20 | end 21 | 22 | it "provides a useful negative failure message" do 23 | matcher = BeNilMatcher.new 24 | matcher.matches?(nil) 25 | expect(matcher.negative_failure_message).to eq(["Expected nil", "not to be nil"]) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /tool/wrap_with_guard.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Wrap the passed the files with a guard (e.g., `ruby_version_is ""..."3.0"`). 3 | # Notably if some methods are removed, this is a convenient way to skip such file from a given version. 4 | # Example usage: 5 | # $ spec/mspec/tool/wrap_with_guard.rb 'ruby_version_is ""..."3.0"' spec/ruby/library/set/sortedset/**/*_spec.rb 6 | 7 | guard, *files = ARGV 8 | abort "Usage: #{$0} GUARD FILES..." if files.empty? 9 | 10 | files.each do |file| 11 | contents = File.binread(file) 12 | lines = contents.lines.to_a 13 | 14 | lines = lines.map { |line| line.chomp.empty? ? line : " #{line}" } 15 | 16 | version_line = "#{guard} do\n" 17 | if lines[0] =~ /^\s*require.+spec_helper/ 18 | lines[0] = lines[0].sub(/^ /, '') 19 | lines.insert 1, "\n", version_line 20 | else 21 | warn "Could not find 'require spec_helper' line in #{file}" 22 | lines.insert 0, version_line 23 | end 24 | 25 | lines << "end\n" 26 | 27 | File.binwrite file, lines.join 28 | end 29 | -------------------------------------------------------------------------------- /lib/mspec/matchers/be_computed_by.rb: -------------------------------------------------------------------------------- 1 | class BeComputedByMatcher 2 | def initialize(sym, *args) 3 | @method = sym 4 | @args = args 5 | end 6 | 7 | def matches?(array) 8 | array.each do |line| 9 | @receiver = line.shift 10 | @value = line.pop 11 | @arguments = line 12 | @arguments += @args 13 | @actual = @receiver.send(@method, *@arguments) 14 | return false unless @actual == @value 15 | end 16 | 17 | return true 18 | end 19 | 20 | def method_call 21 | method_call = "#{@receiver.inspect}.#{@method}" 22 | unless @arguments.empty? 23 | method_call = "#{method_call} from #{@arguments.map { |x| x.inspect }.join(", ")}" 24 | end 25 | method_call 26 | end 27 | 28 | def failure_message 29 | ["Expected #{@value.inspect}", "to be computed by #{method_call} (computed #{@actual.inspect} instead)"] 30 | end 31 | end 32 | 33 | module MSpecMatchers 34 | private def be_computed_by(sym, *args) 35 | BeComputedByMatcher.new(sym, *args) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/guards/quarantine_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | 4 | RSpec.describe QuarantineGuard, "#match?" do 5 | it "returns true" do 6 | expect(QuarantineGuard.new.match?).to eq(true) 7 | end 8 | end 9 | 10 | RSpec.describe Object, "#quarantine!" do 11 | before :each do 12 | ScratchPad.clear 13 | 14 | @guard = QuarantineGuard.new 15 | allow(QuarantineGuard).to receive(:new).and_return(@guard) 16 | end 17 | 18 | it "does not yield" do 19 | quarantine! { ScratchPad.record :yield } 20 | expect(ScratchPad.recorded).not_to eq(:yield) 21 | end 22 | 23 | it "sets the name of the guard to :quarantine!" do 24 | quarantine! { } 25 | expect(@guard.name).to eq(:quarantine!) 26 | end 27 | 28 | it "calls #unregister even when an exception is raised in the guard block" do 29 | expect(@guard).to receive(:match?).and_return(false) 30 | expect(@guard).to receive(:unregister) 31 | expect do 32 | quarantine! { raise Exception } 33 | end.to raise_error(Exception) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/matchers/be_true_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe BeTrueMatcher do 6 | it "matches when actual is true" do 7 | expect(BeTrueMatcher.new.matches?(true)).to eq(true) 8 | end 9 | 10 | it "does not match when actual is not true" do 11 | expect(BeTrueMatcher.new.matches?("")).to eq(false) 12 | expect(BeTrueMatcher.new.matches?(false)).to eq(false) 13 | expect(BeTrueMatcher.new.matches?(nil)).to eq(false) 14 | expect(BeTrueMatcher.new.matches?(0)).to eq(false) 15 | end 16 | 17 | it "provides a useful failure message" do 18 | matcher = BeTrueMatcher.new 19 | matcher.matches?("some string") 20 | expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be true"]) 21 | end 22 | 23 | it "provides a useful negative failure message" do 24 | matcher = BeTrueMatcher.new 25 | matcher.matches?(true) 26 | expect(matcher.negative_failure_message).to eq(["Expected true", "not to be true"]) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/matchers/be_ancestor_of_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | class Parent; end 6 | class Child < Parent; end 7 | 8 | RSpec.describe BeAncestorOfMatcher do 9 | it "matches when actual is an ancestor of expected" do 10 | expect(BeAncestorOfMatcher.new(Child).matches?(Parent)).to eq(true) 11 | end 12 | 13 | it "does not match when actual is not an ancestor of expected" do 14 | expect(BeAncestorOfMatcher.new(Parent).matches?(Child)).to eq(false) 15 | end 16 | 17 | it "provides a useful failure message" do 18 | matcher = BeAncestorOfMatcher.new(Parent) 19 | matcher.matches?(Child) 20 | expect(matcher.failure_message).to eq(["Expected Child", "to be an ancestor of Parent"]) 21 | end 22 | 23 | it "provides a useful negative failure message" do 24 | matcher = BeAncestorOfMatcher.new(Child) 25 | matcher.matches?(Parent) 26 | expect(matcher.negative_failure_message).to eq(["Expected Parent", "not to be an ancestor of Child"]) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/matchers/be_false_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe BeFalseMatcher do 6 | it "matches when actual is false" do 7 | expect(BeFalseMatcher.new.matches?(false)).to eq(true) 8 | end 9 | 10 | it "does not match when actual is not false" do 11 | expect(BeFalseMatcher.new.matches?("")).to eq(false) 12 | expect(BeFalseMatcher.new.matches?(true)).to eq(false) 13 | expect(BeFalseMatcher.new.matches?(nil)).to eq(false) 14 | expect(BeFalseMatcher.new.matches?(0)).to eq(false) 15 | end 16 | 17 | it "provides a useful failure message" do 18 | matcher = BeFalseMatcher.new 19 | matcher.matches?("some string") 20 | expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be false"]) 21 | end 22 | 23 | it "provides a useful negative failure message" do 24 | matcher = BeFalseMatcher.new 25 | matcher.matches?(false) 26 | expect(matcher.negative_failure_message).to eq(["Expected false", "not to be false"]) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/expectations/expectations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | 4 | RSpec.describe SpecExpectationNotMetError do 5 | it "is a subclass of StandardError" do 6 | expect(SpecExpectationNotMetError.ancestors).to include(StandardError) 7 | end 8 | end 9 | 10 | RSpec.describe SpecExpectationNotFoundError do 11 | it "is a subclass of StandardError" do 12 | expect(SpecExpectationNotFoundError.ancestors).to include(StandardError) 13 | end 14 | end 15 | 16 | RSpec.describe SpecExpectationNotFoundError, "#message" do 17 | it "returns 'No behavior expectation was found in the example'" do 18 | m = SpecExpectationNotFoundError.new.message 19 | expect(m).to eq("No behavior expectation was found in the example") 20 | end 21 | end 22 | 23 | RSpec.describe SpecExpectation, "#fail_with" do 24 | it "raises an SpecExpectationNotMetError" do 25 | expect { 26 | SpecExpectation.fail_with "expected this", "to equal that" 27 | }.to raise_error(SpecExpectationNotMetError, "expected this to equal that") 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/runner/filters/regexp_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | require 'mspec/runner/mspec' 3 | require 'mspec/runner/filters/regexp' 4 | 5 | RSpec.describe MatchFilter, "#===" do 6 | before :each do 7 | @filter = RegexpFilter.new nil, 'a(b|c)', 'b[^ab]', 'cc?' 8 | end 9 | 10 | it "returns true if the argument matches any of the #initialize strings" do 11 | expect(@filter.===('ab')).to eq(true) 12 | expect(@filter.===('bc suffix')).to eq(true) 13 | expect(@filter.===('prefix cc')).to eq(true) 14 | end 15 | 16 | it "returns false if the argument matches none of the #initialize strings" do 17 | expect(@filter.===('aa')).to eq(false) 18 | expect(@filter.===('ba')).to eq(false) 19 | expect(@filter.===('prefix d suffix')).to eq(false) 20 | end 21 | end 22 | 23 | RSpec.describe RegexpFilter, "#to_regexp" do 24 | before :each do 25 | @filter = RegexpFilter.new nil 26 | end 27 | 28 | it "converts its arguments to Regexp instances" do 29 | expect(@filter.send(:to_regexp, 'a(b|c)', 'b[^ab]', 'cc?')).to eq([/a(b|c)/, /b[^ab]/, /cc?/]) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /tool/check_require_spec_helper.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This script is used to check that each *_spec.rb file has 4 | # a relative_require for spec_helper which should live higher 5 | # up in the ruby/spec repo directory tree. 6 | # 7 | # Prints errors to $stderr and returns a non-zero exit code when 8 | # errors are found. 9 | # 10 | # Related to https://github.com/ruby/spec/pull/992 11 | 12 | def check_file(fn) 13 | File.foreach(fn) do |line| 14 | return $1 if line =~ /^\s*require_relative\s*['"](.*spec_helper)['"]/ 15 | end 16 | nil 17 | end 18 | 19 | rootdir = ARGV[0] || "." 20 | fglob = File.join(rootdir, "**", "*_spec.rb") 21 | specfiles = Dir.glob(fglob) 22 | raise "No spec files found in #{fglob.inspect}. Give an argument to specify the root-directory of ruby/spec" if specfiles.empty? 23 | 24 | errors = 0 25 | specfiles.sort.each do |fn| 26 | result = check_file(fn) 27 | if result.nil? 28 | warn "Missing require_relative for *spec_helper for file: #{fn}" 29 | errors += 1 30 | end 31 | end 32 | 33 | puts "# Found #{errors} files with require_relative spec_helper issues." 34 | exit 1 if errors > 0 35 | -------------------------------------------------------------------------------- /lib/mspec/helpers/argv.rb: -------------------------------------------------------------------------------- 1 | # Convenience helper for altering ARGV. Saves the 2 | # value of ARGV and sets it to +args+. If a block 3 | # is given, yields to the block and then restores 4 | # the value of ARGV. The previously saved value of 5 | # ARGV can be restored by passing +:restore+. The 6 | # former is useful in a single spec. The latter is 7 | # useful in before/after actions. For example: 8 | # 9 | # describe "This" do 10 | # before do 11 | # argv ['a', 'b'] 12 | # end 13 | # 14 | # after do 15 | # argv :restore 16 | # end 17 | # 18 | # it "does something" do 19 | # # do something 20 | # end 21 | # end 22 | # 23 | # describe "That" do 24 | # it "does something" do 25 | # argv ['a', 'b'] do 26 | # # do something 27 | # end 28 | # end 29 | # end 30 | def argv(args) 31 | if args == :restore 32 | ARGV.replace(@__mspec_saved_argv__ || []) 33 | else 34 | @__mspec_saved_argv__ = ARGV.dup 35 | ARGV.replace args 36 | if block_given? 37 | begin 38 | yield 39 | ensure 40 | argv :restore 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/mspec/matchers/match_yaml.rb: -------------------------------------------------------------------------------- 1 | class MatchYAMLMatcher 2 | 3 | def initialize(expected) 4 | if valid_yaml?(expected) 5 | @expected = expected 6 | else 7 | @expected = expected.to_yaml 8 | end 9 | end 10 | 11 | def matches?(actual) 12 | @actual = actual 13 | clean_yaml(@actual) == clean_yaml(@expected) 14 | end 15 | 16 | def failure_message 17 | ["Expected #{@actual.inspect}", " to match #{@expected.inspect}"] 18 | end 19 | 20 | def negative_failure_message 21 | ["Expected #{@actual.inspect}", " to match #{@expected.inspect}"] 22 | end 23 | 24 | protected 25 | 26 | def clean_yaml(yaml) 27 | yaml.gsub(/([^-]|^---)\s+\n/, "\\1\n").sub(/\n\.\.\.\n$/, "\n") 28 | end 29 | 30 | def valid_yaml?(obj) 31 | require 'yaml' 32 | begin 33 | if YAML.respond_to?(:unsafe_load) 34 | YAML.unsafe_load(obj) 35 | else 36 | YAML.load(obj) 37 | end 38 | rescue 39 | false 40 | else 41 | true 42 | end 43 | end 44 | end 45 | 46 | module MSpecMatchers 47 | private def match_yaml(expected) 48 | MatchYAMLMatcher.new(expected) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Engine Yard, Inc. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/matchers/have_constant_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | class HCMSpecs 6 | X = :x 7 | end 8 | 9 | RSpec.describe HaveConstantMatcher do 10 | it "matches when mod has the constant" do 11 | matcher = HaveConstantMatcher.new :X 12 | expect(matcher.matches?(HCMSpecs)).to be_truthy 13 | end 14 | 15 | it "does not match when mod does not have the constant" do 16 | matcher = HaveConstantMatcher.new :A 17 | expect(matcher.matches?(HCMSpecs)).to be_falsey 18 | end 19 | 20 | it "provides a failure message for #should" do 21 | matcher = HaveConstantMatcher.new :A 22 | matcher.matches?(HCMSpecs) 23 | expect(matcher.failure_message).to eq([ 24 | "Expected HCMSpecs to have constant 'A'", 25 | "but it does not" 26 | ]) 27 | end 28 | 29 | it "provides a failure messoge for #should_not" do 30 | matcher = HaveConstantMatcher.new :X 31 | matcher.matches?(HCMSpecs) 32 | expect(matcher.negative_failure_message).to eq([ 33 | "Expected HCMSpecs NOT to have constant 'X'", 34 | "but it does" 35 | ]) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/mspec/runner/actions/filter.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/filters/match' 2 | 3 | # ActionFilter is a base class for actions that are triggered by 4 | # specs that match the filter. The filter may be specified by 5 | # strings that match spec descriptions or by tags for strings 6 | # that match spec descriptions. 7 | # 8 | # Unlike TagFilter and RegexpFilter, ActionFilter instances do 9 | # not affect the specs that are run. The filter is only used to 10 | # trigger the action. 11 | 12 | class ActionFilter 13 | def initialize(tags = nil, descs = nil) 14 | @tags = Array(tags) 15 | descs = Array(descs) 16 | @sfilter = descs.empty? ? nil : MatchFilter.new(nil, *descs) 17 | @tfilter = nil 18 | end 19 | 20 | def ===(string) 21 | @sfilter === string or @tfilter === string 22 | end 23 | 24 | def load 25 | return if @tags.empty? 26 | 27 | desc = MSpec.read_tags(@tags).map { |t| t.description } 28 | return if desc.empty? 29 | 30 | @tfilter = MatchFilter.new(nil, *desc) 31 | end 32 | 33 | def register 34 | MSpec.register :load, self 35 | end 36 | 37 | def unregister 38 | MSpec.unregister :load, self 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/yaml.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/base' 2 | 3 | class YamlFormatter < BaseFormatter 4 | def initialize(out = nil) 5 | super(nil) 6 | 7 | if out.nil? 8 | @finish = $stdout 9 | else 10 | @finish = File.open out, "w" 11 | end 12 | end 13 | 14 | def switch 15 | @out = @finish 16 | end 17 | 18 | def finish 19 | switch 20 | 21 | print "---\n" 22 | print "exceptions:\n" 23 | @exceptions.each do |exc| 24 | outcome = exc.failure? ? "FAILED" : "ERROR" 25 | str = "#{exc.description} #{outcome}\n" 26 | str << exc.message << "\n" << exc.backtrace 27 | print "- ", str.inspect, "\n" 28 | end 29 | 30 | print "time: ", @timer.elapsed, "\n" 31 | print "files: ", @tally.counter.files, "\n" 32 | print "examples: ", @tally.counter.examples, "\n" 33 | print "expectations: ", @tally.counter.expectations, "\n" 34 | print "failures: ", @tally.counter.failures, "\n" 35 | print "errors: ", @tally.counter.errors, "\n" 36 | print "tagged: ", @tally.counter.tagged, "\n" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/guards/superuser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | 4 | RSpec.describe Object, "#as_superuser" do 5 | before :each do 6 | @guard = SuperUserGuard.new 7 | allow(SuperUserGuard).to receive(:new).and_return(@guard) 8 | ScratchPad.clear 9 | end 10 | 11 | it "does not yield when Process.euid is not 0" do 12 | allow(Process).to receive(:euid).and_return(501) 13 | as_superuser { ScratchPad.record :yield } 14 | expect(ScratchPad.recorded).not_to eq(:yield) 15 | end 16 | 17 | it "yields when Process.euid is 0" do 18 | allow(Process).to receive(:euid).and_return(0) 19 | as_superuser { ScratchPad.record :yield } 20 | expect(ScratchPad.recorded).to eq(:yield) 21 | end 22 | 23 | it "sets the name of the guard to :as_superuser" do 24 | as_superuser { } 25 | expect(@guard.name).to eq(:as_superuser) 26 | end 27 | 28 | it "calls #unregister even when an exception is raised in the guard block" do 29 | expect(@guard).to receive(:match?).and_return(true) 30 | expect(@guard).to receive(:unregister) 31 | expect do 32 | as_superuser { raise Exception } 33 | end.to raise_error(Exception) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/runner/filters/match_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | require 'mspec/runner/mspec' 3 | require 'mspec/runner/filters/match' 4 | 5 | RSpec.describe MatchFilter, "#===" do 6 | before :each do 7 | @filter = MatchFilter.new nil, 'a', 'b', 'c' 8 | end 9 | 10 | it "returns true if the argument matches any of the #initialize strings" do 11 | expect(@filter.===('aaa')).to eq(true) 12 | expect(@filter.===('bccb')).to eq(true) 13 | end 14 | 15 | it "returns false if the argument matches none of the #initialize strings" do 16 | expect(@filter.===('d')).to eq(false) 17 | end 18 | end 19 | 20 | RSpec.describe MatchFilter, "#register" do 21 | it "registers itself with MSpec for the designated action list" do 22 | filter = MatchFilter.new :include 23 | expect(MSpec).to receive(:register).with(:include, filter) 24 | filter.register 25 | end 26 | end 27 | 28 | RSpec.describe MatchFilter, "#unregister" do 29 | it "unregisters itself with MSpec for the designated action list" do 30 | filter = MatchFilter.new :exclude 31 | expect(MSpec).to receive(:unregister).with(:exclude, filter) 32 | filter.unregister 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mspec/helpers/argf.rb: -------------------------------------------------------------------------------- 1 | # Convenience helper for specs using ARGF. 2 | # Set @argf to an instance of ARGF.class with the given +argv+. 3 | # That instance must be used instead of ARGF as ARGF is global 4 | # and it is not always possible to reset its state correctly. 5 | # 6 | # The helper yields to the block and then close 7 | # the files open by the instance. Example: 8 | # 9 | # describe "That" do 10 | # it "does something" do 11 | # argf ['a', 'b'] do 12 | # # do something 13 | # end 14 | # end 15 | # end 16 | def argf(argv) 17 | if argv.empty? or argv.length > 2 18 | raise "Only 1 or 2 filenames are allowed for the argf helper so files can be properly closed: #{argv.inspect}" 19 | end 20 | @argf ||= nil 21 | raise "Cannot nest calls to the argf helper" if @argf 22 | 23 | @argf = ARGF.class.new(*argv) 24 | @__mspec_saved_argf_file__ = @argf.file 25 | begin 26 | yield 27 | ensure 28 | file1 = @__mspec_saved_argf_file__ 29 | file2 = @argf.file # Either the first file or the second 30 | file1.close if !file1.closed? and file1 != STDIN 31 | file2.close if !file2.closed? and file2 != STDIN 32 | @argf = nil 33 | @__mspec_saved_argf_file__ = nil 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/multi.rb: -------------------------------------------------------------------------------- 1 | module MultiFormatter 2 | def self.extend_object(obj) 3 | super 4 | obj.multi_initialize 5 | end 6 | 7 | def multi_initialize 8 | @tally = TallyAction.new 9 | @counter = @tally.counter 10 | @timer = TimerAction.new 11 | @timer.start 12 | end 13 | 14 | def register 15 | super 16 | 17 | MSpec.register :start, self 18 | MSpec.register :unload, self 19 | MSpec.unregister :before, self 20 | end 21 | 22 | def aggregate_results(files) 23 | require 'yaml' 24 | 25 | @timer.finish 26 | @exceptions = [] 27 | 28 | files.each do |file| 29 | contents = File.read(file) 30 | d = YAML.load(contents) 31 | File.delete file 32 | 33 | if d # The file might be empty if the child process died 34 | @exceptions += Array(d['exceptions']) 35 | @counter.files! d['files'] 36 | @counter.examples! d['examples'] 37 | @counter.expectations! d['expectations'] 38 | @counter.errors! d['errors'] 39 | @counter.failures! d['failures'] 40 | end 41 | end 42 | end 43 | 44 | def print_exception(exc, count) 45 | @err.print "\n#{count})\n#{exc}\n" 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/matchers/be_kind_of_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe BeKindOfMatcher do 6 | it "matches when actual is a kind_of? expected" do 7 | expect(BeKindOfMatcher.new(Numeric).matches?(1)).to eq(true) 8 | expect(BeKindOfMatcher.new(Integer).matches?(2)).to eq(true) 9 | expect(BeKindOfMatcher.new(Regexp).matches?(/m/)).to eq(true) 10 | end 11 | 12 | it "does not match when actual is not a kind_of? expected" do 13 | expect(BeKindOfMatcher.new(Integer).matches?(1.5)).to eq(false) 14 | expect(BeKindOfMatcher.new(String).matches?(:a)).to eq(false) 15 | expect(BeKindOfMatcher.new(Hash).matches?([])).to eq(false) 16 | end 17 | 18 | it "provides a useful failure message" do 19 | matcher = BeKindOfMatcher.new(Numeric) 20 | matcher.matches?('string') 21 | expect(matcher.failure_message).to eq([ 22 | "Expected \"string\" (String)", "to be kind of Numeric"]) 23 | end 24 | 25 | it "provides a useful negative failure message" do 26 | matcher = BeKindOfMatcher.new(Numeric) 27 | matcher.matches?(4.0) 28 | expect(matcher.negative_failure_message).to eq([ 29 | "Expected 4.0 (Float)", "not to be kind of Numeric"]) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/matchers/equal_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe EqualMatcher do 6 | it "matches when actual is equal? to expected" do 7 | expect(EqualMatcher.new(1).matches?(1)).to eq(true) 8 | expect(EqualMatcher.new(:blue).matches?(:blue)).to eq(true) 9 | expect(EqualMatcher.new(Object).matches?(Object)).to eq(true) 10 | 11 | o = Object.new 12 | expect(EqualMatcher.new(o).matches?(o)).to eq(true) 13 | end 14 | 15 | it "does not match when actual is not a equal? to expected" do 16 | expect(EqualMatcher.new(1).matches?(1.0)).to eq(false) 17 | expect(EqualMatcher.new("blue").matches?("blue")).to eq(false) 18 | expect(EqualMatcher.new(Hash).matches?(Object)).to eq(false) 19 | end 20 | 21 | it "provides a useful failure message" do 22 | matcher = EqualMatcher.new("red") 23 | matcher.matches?("red") 24 | expect(matcher.failure_message).to eq(["Expected \"red\"", "to be identical to \"red\""]) 25 | end 26 | 27 | it "provides a useful negative failure message" do 28 | matcher = EqualMatcher.new(1) 29 | matcher.matches?(1) 30 | expect(matcher.negative_failure_message).to eq(["Expected 1", "not to be identical to 1"]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/matchers/signed_zero_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe SignedZeroMatcher do 6 | it "matches when actual is zero and has the correct sign" do 7 | expect(SignedZeroMatcher.new(1).matches?(0.0)).to eq(true) 8 | expect(SignedZeroMatcher.new(-1).matches?(-0.0)).to eq(true) 9 | end 10 | 11 | it "does not match when actual is non-zero" do 12 | expect(SignedZeroMatcher.new(1).matches?(1.0)).to eq(false) 13 | expect(SignedZeroMatcher.new(-1).matches?(-1.0)).to eq(false) 14 | end 15 | 16 | it "does not match when actual is zero but has the incorrect sign" do 17 | expect(SignedZeroMatcher.new(1).matches?(-0.0)).to eq(false) 18 | expect(SignedZeroMatcher.new(-1).matches?(0.0)).to eq(false) 19 | end 20 | 21 | it "provides a useful failure message" do 22 | matcher = SignedZeroMatcher.new(-1) 23 | matcher.matches?(0.0) 24 | expect(matcher.failure_message).to eq(["Expected 0.0", "to be -0.0"]) 25 | end 26 | 27 | it "provides a useful negative failure message" do 28 | matcher = SignedZeroMatcher.new(-1) 29 | matcher.matches?(-0.0) 30 | expect(matcher.negative_failure_message).to eq(["Expected -0.0", "not to be -0.0"]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mspec/guards/feature.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/guards/guard' 2 | 3 | class FeatureGuard < SpecGuard 4 | def self.enabled?(*features) 5 | new(*features).match? 6 | end 7 | 8 | def match? 9 | @parameters.all? { |f| MSpec.feature_enabled? f } 10 | end 11 | end 12 | 13 | # Provides better documentation in the specs by 14 | # naming sets of features that work together as 15 | # a whole. Examples include :encoding, :fiber, 16 | # :continuation, :fork. 17 | # 18 | # Usage example: 19 | # 20 | # with_feature :encoding do 21 | # # specs for a method that provides aspects 22 | # # of the encoding feature 23 | # end 24 | # 25 | # Multiple features must all be enabled for the 26 | # guard to run: 27 | # 28 | # with_feature :one, :two do 29 | # # these specs will run if features :one AND 30 | # # :two are enabled. 31 | # end 32 | # 33 | # The implementation must explicitly enable a feature 34 | # by adding code like the following to the .mspec 35 | # configuration file: 36 | # 37 | # MSpec.enable_feature :encoding 38 | # 39 | def with_feature(*features, &block) 40 | FeatureGuard.new(*features).run_if(:with_feature, &block) 41 | end 42 | 43 | def without_feature(*features, &block) 44 | FeatureGuard.new(*features).run_unless(:without_feature, &block) 45 | end 46 | -------------------------------------------------------------------------------- /lib/mspec/helpers/datetime.rb: -------------------------------------------------------------------------------- 1 | # The new_datetime helper makes writing DateTime specs more simple by 2 | # providing default constructor values and accepting a Hash of only the 3 | # constructor values needed for the particular spec. For example: 4 | # 5 | # new_datetime :hour => 1, :minute => 20 6 | # 7 | # Possible keys are: 8 | # :year, :month, :day, :hour, :minute, :second, :offset and :sg. 9 | def new_datetime(opts = {}) 10 | require 'date' 11 | 12 | value = { 13 | :year => -4712, 14 | :month => 1, 15 | :day => 1, 16 | :hour => 0, 17 | :minute => 0, 18 | :second => 0, 19 | :offset => 0, 20 | :sg => Date::ITALY 21 | }.merge opts 22 | 23 | DateTime.new value[:year], value[:month], value[:day], value[:hour], 24 | value[:minute], value[:second], value[:offset], value[:sg] 25 | end 26 | 27 | def with_timezone(name, offset = nil, daylight_saving_zone = "") 28 | skip "WASI doesn't have TZ concept" if PlatformGuard.wasi? 29 | zone = name.dup 30 | 31 | if offset 32 | # TZ convention is backwards 33 | offset = -offset 34 | 35 | zone += offset.to_s 36 | zone += ":00:00" 37 | end 38 | zone += daylight_saving_zone 39 | 40 | old = ENV["TZ"] 41 | ENV["TZ"] = zone 42 | 43 | begin 44 | yield 45 | ensure 46 | ENV["TZ"] = old 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/specdoc.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/base' 2 | 3 | class SpecdocFormatter < BaseFormatter 4 | def register 5 | super 6 | MSpec.register :enter, self 7 | end 8 | 9 | # Callback for the MSpec :enter event. Prints the 10 | # +describe+ block string. 11 | def enter(describe) 12 | print "\n#{describe}\n" 13 | end 14 | 15 | # Callback for the MSpec :before event. Prints the 16 | # +it+ block string. 17 | def before(state) 18 | super(state) 19 | print "- #{state.it}" 20 | end 21 | 22 | # Callback for the MSpec :exception event. Prints 23 | # either 'ERROR - X' or 'FAILED - X' where _X_ is 24 | # the sequential number of the exception raised. If 25 | # there has already been an exception raised while 26 | # evaluating this example, it prints another +it+ 27 | # block description string so that each description 28 | # string has an associated 'ERROR' or 'FAILED' 29 | def exception(exception) 30 | print "\n- #{exception.it}" if exception? 31 | super(exception) 32 | print " (#{exception.failure? ? 'FAILED' : 'ERROR'} - #{@count})" 33 | end 34 | 35 | # Callback for the MSpec :after event. Prints a 36 | # newline to finish the description string output. 37 | def after(state = nil) 38 | super(state) 39 | print "\n" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/matchers/eql_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe EqlMatcher do 6 | it "matches when actual is eql? to expected" do 7 | expect(EqlMatcher.new(1).matches?(1)).to eq(true) 8 | expect(EqlMatcher.new(1.5).matches?(1.5)).to eq(true) 9 | expect(EqlMatcher.new("red").matches?("red")).to eq(true) 10 | expect(EqlMatcher.new(:blue).matches?(:blue)).to eq(true) 11 | expect(EqlMatcher.new(Object).matches?(Object)).to eq(true) 12 | 13 | o = Object.new 14 | expect(EqlMatcher.new(o).matches?(o)).to eq(true) 15 | end 16 | 17 | it "does not match when actual is not eql? to expected" do 18 | expect(EqlMatcher.new(1).matches?(1.0)).to eq(false) 19 | expect(EqlMatcher.new(Hash).matches?(Object)).to eq(false) 20 | end 21 | 22 | it "provides a useful failure message" do 23 | matcher = EqlMatcher.new("red") 24 | matcher.matches?("red") 25 | expect(matcher.failure_message).to eq(["Expected \"red\"", "to have same value and type as \"red\""]) 26 | end 27 | 28 | it "provides a useful negative failure message" do 29 | matcher = EqlMatcher.new(1) 30 | matcher.matches?(1.0) 31 | expect(matcher.negative_failure_message).to eq(["Expected 1.0", "not to have same value or type as 1"]) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mspec/runner/filters/profile.rb: -------------------------------------------------------------------------------- 1 | class ProfileFilter 2 | def initialize(what, *files) 3 | @what = what 4 | @methods = load(*files) 5 | @pattern = /([^ .#]+[.#])([^ ]+)/ 6 | end 7 | 8 | def find(name) 9 | return name if File.exist?(File.expand_path(name)) 10 | 11 | ["spec/profiles", "spec", "profiles", "."].each do |dir| 12 | file = File.join dir, name 13 | return file if File.exist? file 14 | end 15 | end 16 | 17 | def parse(file) 18 | pattern = /(\S+):\s*/ 19 | key = "" 20 | file.inject(Hash.new { |h,k| h[k] = [] }) do |hash, line| 21 | line.chomp! 22 | if line[0,2] == "- " 23 | hash[key] << line[2..-1].gsub(/[ '"]/, "") 24 | elsif m = pattern.match(line) 25 | key = m[1] 26 | end 27 | hash 28 | end 29 | end 30 | 31 | def load(*files) 32 | files.inject({}) do |hash, file| 33 | next hash unless name = find(file) 34 | 35 | File.open name, "r" do |f| 36 | hash.merge parse(f) 37 | end 38 | end 39 | end 40 | 41 | def ===(string) 42 | return false unless m = @pattern.match(string) 43 | return false unless l = @methods[m[1]] 44 | l.include? m[2] 45 | end 46 | 47 | def register 48 | MSpec.register @what, self 49 | end 50 | 51 | def unregister 52 | MSpec.unregister @what, self 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/matchers/infinity_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/guards' 4 | require 'mspec/helpers' 5 | require 'mspec/matchers' 6 | 7 | RSpec.describe InfinityMatcher do 8 | it "matches when actual is infinite and has the correct sign" do 9 | expect(InfinityMatcher.new(1).matches?(infinity_value)).to eq(true) 10 | expect(InfinityMatcher.new(-1).matches?(-infinity_value)).to eq(true) 11 | end 12 | 13 | it "does not match when actual is not infinite" do 14 | expect(InfinityMatcher.new(1).matches?(1.0)).to eq(false) 15 | expect(InfinityMatcher.new(-1).matches?(-1.0)).to eq(false) 16 | end 17 | 18 | it "does not match when actual is infinite but has the incorrect sign" do 19 | expect(InfinityMatcher.new(1).matches?(-infinity_value)).to eq(false) 20 | expect(InfinityMatcher.new(-1).matches?(infinity_value)).to eq(false) 21 | end 22 | 23 | it "provides a useful failure message" do 24 | matcher = InfinityMatcher.new(-1) 25 | matcher.matches?(0) 26 | expect(matcher.failure_message).to eq(["Expected 0", "to be -Infinity"]) 27 | end 28 | 29 | it "provides a useful negative failure message" do 30 | matcher = InfinityMatcher.new(1) 31 | matcher.matches?(infinity_value) 32 | expect(matcher.negative_failure_message).to eq(["Expected Infinity", "not to be Infinity"]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/helpers/datetime_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | require 'mspec/helpers' 4 | 5 | RSpec.describe Object, "#new_datetime" do 6 | it "returns a default DateTime instance" do 7 | expect(new_datetime).to eq(DateTime.new) 8 | end 9 | 10 | it "returns a DateTime instance with the specified year value" do 11 | d = new_datetime :year => 1970 12 | expect(d.year).to eq(1970) 13 | end 14 | 15 | it "returns a DateTime instance with the specified month value" do 16 | d = new_datetime :month => 11 17 | expect(d.mon).to eq(11) 18 | end 19 | 20 | it "returns a DateTime instance with the specified day value" do 21 | d = new_datetime :day => 23 22 | expect(d.day).to eq(23) 23 | end 24 | 25 | it "returns a DateTime instance with the specified hour value" do 26 | d = new_datetime :hour => 10 27 | expect(d.hour).to eq(10) 28 | end 29 | 30 | it "returns a DateTime instance with the specified minute value" do 31 | d = new_datetime :minute => 10 32 | expect(d.min).to eq(10) 33 | end 34 | 35 | it "returns a DateTime instance with the specified second value" do 36 | d = new_datetime :second => 2 37 | expect(d.sec).to eq(2) 38 | end 39 | 40 | it "returns a DateTime instance with the specified offset value" do 41 | d = new_datetime :offset => Rational(3,24) 42 | expect(d.offset).to eq(Rational(3,24)) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/mspec/expectations/should.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | NO_MATCHER_GIVEN = Object.new 3 | 4 | def should(matcher = NO_MATCHER_GIVEN, &block) 5 | MSpec.expectation 6 | state = MSpec.current.state 7 | raise "should outside example" unless state 8 | MSpec.actions :expectation, state 9 | 10 | if NO_MATCHER_GIVEN.equal?(matcher) 11 | SpecPositiveOperatorMatcher.new(self) 12 | else 13 | # The block was given to #should syntactically, but it was intended for a matcher like #raise_error 14 | matcher.block = block if block 15 | 16 | unless matcher.matches? self 17 | expected, actual = matcher.failure_message 18 | SpecExpectation.fail_with(expected, actual) 19 | end 20 | end 21 | end 22 | 23 | def should_not(matcher = NO_MATCHER_GIVEN, &block) 24 | MSpec.expectation 25 | state = MSpec.current.state 26 | raise "should_not outside example" unless state 27 | MSpec.actions :expectation, state 28 | 29 | if NO_MATCHER_GIVEN.equal?(matcher) 30 | SpecNegativeOperatorMatcher.new(self) 31 | else 32 | # The block was given to #should_not syntactically, but it was intended for the matcher 33 | matcher.block = block if block 34 | 35 | if matcher.matches? self 36 | expected, actual = matcher.negative_failure_message 37 | SpecExpectation.fail_with(expected, actual) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/mspec/runner/actions/profile.rb: -------------------------------------------------------------------------------- 1 | class ProfileAction 2 | def initialize 3 | @describe_name = nil 4 | @describe_time = nil 5 | @describes = [] 6 | @its = [] 7 | end 8 | 9 | def register 10 | MSpec.register :enter, self 11 | MSpec.register :before,self 12 | MSpec.register :after, self 13 | MSpec.register :finish,self 14 | end 15 | 16 | def enter(describe) 17 | if @describe_time 18 | @describes << [@describe_name, now - @describe_time] 19 | end 20 | 21 | @describe_name = describe 22 | @describe_time = now 23 | end 24 | 25 | def before(state) 26 | @it_name = state.it 27 | @it_time = now 28 | end 29 | 30 | def after(state = nil) 31 | @its << [@describe_name, @it_name, now - @it_time] 32 | end 33 | 34 | def finish 35 | puts "\nProfiling info:" 36 | 37 | desc = @describes.sort { |a,b| b.last <=> a.last } 38 | desc.delete_if { |a| a.last <= 0.001 } 39 | show = desc[0, 100] 40 | 41 | puts "Top #{show.size} describes:" 42 | 43 | show.each do |des, time| 44 | printf "%3.3f - %s\n", time, des 45 | end 46 | 47 | its = @its.sort { |a,b| b.last <=> a.last } 48 | its.delete_if { |a| a.last <= 0.001 } 49 | show = its[0, 100] 50 | 51 | puts "\nTop #{show.size} its:" 52 | show.each do |des, it, time| 53 | printf "%3.3f - %s %s\n", time, des, it 54 | end 55 | end 56 | 57 | def now 58 | Time.now.to_f 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/matchers/respond_to_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe RespondToMatcher do 6 | it "matches when actual does respond_to? expected" do 7 | expect(RespondToMatcher.new(:to_s).matches?(Object.new)).to eq(true) 8 | expect(RespondToMatcher.new(:inject).matches?([])).to eq(true) 9 | expect(RespondToMatcher.new(:[]).matches?(1)).to eq(true) 10 | expect(RespondToMatcher.new(:[]=).matches?("string")).to eq(true) 11 | end 12 | 13 | it "does not match when actual does not respond_to? expected" do 14 | expect(RespondToMatcher.new(:to_i).matches?(Object.new)).to eq(false) 15 | expect(RespondToMatcher.new(:inject).matches?(1)).to eq(false) 16 | expect(RespondToMatcher.new(:non_existent_method).matches?([])).to eq(false) 17 | expect(RespondToMatcher.new(:[]=).matches?(1)).to eq(false) 18 | end 19 | 20 | it "provides a useful failure message" do 21 | matcher = RespondToMatcher.new(:non_existent_method) 22 | matcher.matches?('string') 23 | expect(matcher.failure_message).to eq([ 24 | "Expected \"string\" (String)", "to respond to non_existent_method"]) 25 | end 26 | 27 | it "provides a useful negative failure message" do 28 | matcher = RespondToMatcher.new(:to_i) 29 | matcher.matches?(4.0) 30 | expect(matcher.negative_failure_message).to eq([ 31 | "Expected 4.0 (Float)", "not to respond to to_i"]) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mspec/expectations/expectations.rb: -------------------------------------------------------------------------------- 1 | class SpecExpectationNotMetError < StandardError 2 | end 3 | 4 | class SpecExpectationNotFoundError < StandardError 5 | def message 6 | "No behavior expectation was found in the example" 7 | end 8 | end 9 | 10 | class SkippedSpecError < StandardError 11 | end 12 | 13 | class SpecExpectation 14 | def self.fail_with(expected, actual) 15 | expected_to_s = expected.to_s 16 | actual_to_s = actual.to_s 17 | if expected_to_s.size + actual_to_s.size > 80 18 | message = "#{expected_to_s}\n#{actual_to_s}" 19 | else 20 | message = "#{expected_to_s} #{actual_to_s}" 21 | end 22 | raise SpecExpectationNotMetError, message 23 | end 24 | 25 | def self.fail_predicate(receiver, predicate, args, block, result, expectation) 26 | receiver_to_s = MSpec.format(receiver) 27 | before_method = predicate.to_s =~ /^[a-z]/ ? "." : " " 28 | predicate_to_s = "#{before_method}#{predicate}" 29 | predicate_to_s += " " unless args.empty? 30 | args_to_s = args.map { |arg| MSpec.format(arg) }.join(', ') 31 | args_to_s += " { ... }" if block 32 | result_to_s = MSpec.format(result) 33 | raise SpecExpectationNotMetError, "Expected #{receiver_to_s}#{predicate_to_s}#{args_to_s}\n#{expectation} but was #{result_to_s}" 34 | end 35 | 36 | def self.fail_single_arg_predicate(receiver, predicate, arg, result, expectation) 37 | fail_predicate(receiver, predicate, [arg], nil, result, expectation) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/matchers/have_private_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | class HPMMSpecs 6 | def self.private_method 7 | end 8 | 9 | private_class_method :private_method 10 | end 11 | 12 | RSpec.describe HavePrivateMethodMatcher do 13 | it "inherits from MethodMatcher" do 14 | expect(HavePrivateMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) 15 | end 16 | 17 | it "matches when mod has the private method" do 18 | matcher = HavePrivateMethodMatcher.new :private_method 19 | expect(matcher.matches?(HPMMSpecs)).to be_truthy 20 | end 21 | 22 | it "does not match when mod does not have the private method" do 23 | matcher = HavePrivateMethodMatcher.new :another_method 24 | expect(matcher.matches?(HPMMSpecs)).to be_falsey 25 | end 26 | 27 | it "provides a failure message for #should" do 28 | matcher = HavePrivateMethodMatcher.new :some_method 29 | matcher.matches?(HPMMSpecs) 30 | expect(matcher.failure_message).to eq([ 31 | "Expected HPMMSpecs to have private method 'some_method'", 32 | "but it does not" 33 | ]) 34 | end 35 | 36 | it "provides a failure message for #should_not" do 37 | matcher = HavePrivateMethodMatcher.new :private_method 38 | matcher.matches?(HPMMSpecs) 39 | expect(matcher.negative_failure_message).to eq([ 40 | "Expected HPMMSpecs NOT to have private method 'private_method'", 41 | "but it does" 42 | ]) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/matchers/include_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe IncludeMatcher do 6 | it "matches when actual includes expected" do 7 | expect(IncludeMatcher.new(2).matches?([1,2,3])).to eq(true) 8 | expect(IncludeMatcher.new("b").matches?("abc")).to eq(true) 9 | end 10 | 11 | it "does not match when actual does not include expected" do 12 | expect(IncludeMatcher.new(4).matches?([1,2,3])).to eq(false) 13 | expect(IncludeMatcher.new("d").matches?("abc")).to eq(false) 14 | end 15 | 16 | it "matches when actual includes all expected" do 17 | expect(IncludeMatcher.new(3, 2, 1).matches?([1,2,3])).to eq(true) 18 | expect(IncludeMatcher.new("a", "b", "c").matches?("abc")).to eq(true) 19 | end 20 | 21 | it "does not match when actual does not include all expected" do 22 | expect(IncludeMatcher.new(3, 2, 4).matches?([1,2,3])).to eq(false) 23 | expect(IncludeMatcher.new("a", "b", "c", "d").matches?("abc")).to eq(false) 24 | end 25 | 26 | it "provides a useful failure message" do 27 | matcher = IncludeMatcher.new(5, 2) 28 | matcher.matches?([1,2,3]) 29 | expect(matcher.failure_message).to eq(["Expected [1, 2, 3]", "to include 5"]) 30 | end 31 | 32 | it "provides a useful negative failure message" do 33 | matcher = IncludeMatcher.new(1, 2, 3) 34 | matcher.matches?([1,2,3]) 35 | expect(matcher.negative_failure_message).to eq(["Expected [1, 2, 3]", "not to include 3"]) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/mspec/runner/evaluate.rb: -------------------------------------------------------------------------------- 1 | class SpecEvaluate 2 | include MSpecMatchers 3 | 4 | def self.desc=(desc) 5 | @desc = desc 6 | end 7 | 8 | def self.desc 9 | @desc ||= "evaluates " 10 | end 11 | 12 | def initialize(ruby, desc) 13 | @ruby = ruby.rstrip 14 | @desc = desc || self.class.desc 15 | end 16 | 17 | # Formats the Ruby source code for reabable output in the -fs formatter 18 | # option. If the source contains no newline characters, wraps the source in 19 | # single quotes to set if off from the rest of the description string. If 20 | # the source does contain newline characters, sets the indent level to four 21 | # characters. 22 | def format(ruby, newline = true) 23 | if ruby.include?("\n") 24 | lines = ruby.each_line.to_a 25 | if /( *)/ =~ lines.first 26 | if $1.size > 4 27 | dedent = $1.size - 4 28 | ruby = lines.map { |l| l[dedent..-1] }.join 29 | else 30 | indent = " " * (4 - $1.size) 31 | ruby = lines.map { |l| "#{indent}#{l}" }.join 32 | end 33 | end 34 | "\n#{ruby}" 35 | else 36 | "'#{ruby.lstrip}'" 37 | end 38 | end 39 | 40 | def define(&block) 41 | ruby = @ruby 42 | desc = @desc 43 | evaluator = self 44 | 45 | specify "#{desc} #{format ruby}" do 46 | evaluator.instance_eval(ruby) 47 | evaluator.instance_eval(&block) 48 | end 49 | end 50 | end 51 | 52 | def evaluate(str, desc = nil, &block) 53 | SpecEvaluate.new(str, desc).define(&block) 54 | end 55 | -------------------------------------------------------------------------------- /spec/matchers/match_yaml_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe MatchYAMLMatcher do 6 | before :each do 7 | @matcher = MatchYAMLMatcher.new("--- \nfoo: bar\n") 8 | end 9 | 10 | it "compares YAML documents and matches if they're equivalent" do 11 | expect(@matcher.matches?("--- \nfoo: bar\n")).to eq(true) 12 | end 13 | 14 | it "compares YAML documents and does not match if they're not equivalent" do 15 | expect(@matcher.matches?("--- \nbar: foo\n")).to eq(false) 16 | expect(@matcher.matches?("--- \nfoo: \nbar\n")).to eq(false) 17 | end 18 | 19 | it "also receives objects that respond_to to_yaml" do 20 | matcher = MatchYAMLMatcher.new("some string") 21 | expect(matcher.matches?("some string")).to eq(true) 22 | 23 | matcher = MatchYAMLMatcher.new(['a', 'b']) 24 | expect(matcher.matches?("--- \n- a\n- b\n")).to eq(true) 25 | 26 | matcher = MatchYAMLMatcher.new("foo" => "bar") 27 | expect(matcher.matches?("--- \nfoo: bar\n")).to eq(true) 28 | end 29 | 30 | it "matches documents with trailing whitespace" do 31 | expect(@matcher.matches?("--- \nfoo: bar \n")).to eq(true) 32 | expect(@matcher.matches?("--- \nfoo: bar \n")).to eq(true) 33 | end 34 | 35 | it "fails with a descriptive error message" do 36 | expect(@matcher.matches?("foo")).to eq(false) 37 | expect(@matcher.failure_message).to eq(["Expected \"foo\"", " to match \"--- \\nfoo: bar\\n\""]) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/mspec/runner/actions/tagpurge.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/actions/filter' 2 | require 'mspec/runner/actions/taglist' 3 | 4 | # TagPurgeAction - removes all tags not matching any spec 5 | # descriptions. 6 | class TagPurgeAction < TagListAction 7 | attr_reader :matching 8 | 9 | def initialize 10 | @matching = [] 11 | @filter = nil 12 | @tags = nil 13 | end 14 | 15 | # Prints a banner about purging tags. 16 | def start 17 | print "\nRemoving tags not matching any specs\n\n" 18 | end 19 | 20 | # Creates a MatchFilter for all tags. 21 | def load 22 | @filter = nil 23 | @tags = MSpec.read_tags self 24 | desc = @tags.map { |t| t.description } 25 | @filter = MatchFilter.new(nil, *desc) unless desc.empty? 26 | end 27 | 28 | # Saves any matching tags 29 | def after(state) 30 | @matching << state.description if self === state.description 31 | end 32 | 33 | # Rewrites any matching tags. Prints non-matching tags. 34 | # Deletes the tag file if there were no tags (this cleans 35 | # up empty or malformed tag files). 36 | def unload 37 | if @filter 38 | matched = @tags.select { |t| @matching.any? { |s| s == t.description } } 39 | MSpec.write_tags matched 40 | 41 | (@tags - matched).each { |t| print t.description, "\n" } 42 | else 43 | MSpec.delete_tags 44 | end 45 | end 46 | 47 | def register 48 | super 49 | MSpec.register :unload, self 50 | end 51 | 52 | def unregister 53 | super 54 | MSpec.unregister :unload, self 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/matchers/have_singleton_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | class HSMMSpecs 6 | def self.singleton_method 7 | end 8 | end 9 | 10 | RSpec.describe HaveSingletonMethodMatcher do 11 | it "inherits from MethodMatcher" do 12 | expect(HaveSingletonMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) 13 | end 14 | 15 | it "matches when the class has a singleton method" do 16 | matcher = HaveSingletonMethodMatcher.new :singleton_method 17 | expect(matcher.matches?(HSMMSpecs)).to be_truthy 18 | end 19 | 20 | it "matches when the object has a singleton method" do 21 | obj = double("HSMMSpecs") 22 | def obj.singleton_method; end 23 | 24 | matcher = HaveSingletonMethodMatcher.new :singleton_method 25 | expect(matcher.matches?(obj)).to be_truthy 26 | end 27 | 28 | it "provides a failure message for #should" do 29 | matcher = HaveSingletonMethodMatcher.new :some_method 30 | matcher.matches?(HSMMSpecs) 31 | expect(matcher.failure_message).to eq([ 32 | "Expected HSMMSpecs to have singleton method 'some_method'", 33 | "but it does not" 34 | ]) 35 | end 36 | 37 | it "provides a failure message for #should_not" do 38 | matcher = HaveSingletonMethodMatcher.new :singleton_method 39 | matcher.matches?(HSMMSpecs) 40 | expect(matcher.negative_failure_message).to eq([ 41 | "Expected HSMMSpecs NOT to have singleton method 'singleton_method'", 42 | "but it does" 43 | ]) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/mspec/utils/version.rb: -------------------------------------------------------------------------------- 1 | class SpecVersion 2 | # If beginning implementations have a problem with this include, we can 3 | # manually implement the relational operators that are needed. 4 | include Comparable 5 | 6 | # SpecVersion handles comparison correctly for the context by filling in 7 | # missing version parts according to the value of +ceil+. If +ceil+ is 8 | # +false+, 0 digits fill in missing version parts. If +ceil+ is +true+, 9 9 | # digits fill in missing parts. (See e.g. VersionGuard and BugGuard.) 10 | def initialize(version, ceil = false) 11 | @version = version 12 | @ceil = ceil 13 | @integer = nil 14 | end 15 | 16 | def to_s 17 | @version 18 | end 19 | 20 | def to_str 21 | to_s 22 | end 23 | 24 | # Converts a string representation of a version major.minor.tiny 25 | # to an integer representation so that comparisons can be made. For example, 26 | # "2.2.10" < "2.2.2" would be false if compared as strings. 27 | def to_i 28 | unless @integer 29 | major, minor, tiny = @version.split "." 30 | if @ceil 31 | tiny = 99 unless tiny 32 | end 33 | parts = [major, minor, tiny].map { |x| x.to_i } 34 | @integer = ("1%02d%02d%02d" % parts).to_i 35 | end 36 | @integer 37 | end 38 | 39 | def to_int 40 | to_i 41 | end 42 | 43 | def <=>(other) 44 | if other.respond_to? :to_int 45 | other = Integer(other.to_int) 46 | else 47 | other = SpecVersion.new(String(other)).to_i 48 | end 49 | 50 | self.to_i <=> other 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/runner/actions/timer_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | require 'mspec/runner/actions/timer' 3 | require 'mspec/runner/mspec' 4 | require 'time' 5 | 6 | RSpec.describe TimerAction do 7 | before :each do 8 | @timer = TimerAction.new 9 | @start_time = Time.utc(2009, 3, 30, 14, 5, 19) 10 | @stop_time = Time.utc(2009, 3, 30, 14, 5, 52) 11 | end 12 | 13 | it "responds to #start by recording the current time" do 14 | expect(Time).to receive(:now) 15 | @timer.start 16 | end 17 | 18 | it "responds to #finish by recording the current time" do 19 | expect(Time).to receive(:now) 20 | @timer.finish 21 | end 22 | 23 | it "responds to #elapsed by returning the difference between stop and start" do 24 | allow(Time).to receive(:now).and_return(@start_time) 25 | @timer.start 26 | allow(Time).to receive(:now).and_return(@stop_time) 27 | @timer.finish 28 | expect(@timer.elapsed).to eq(33) 29 | end 30 | 31 | it "responds to #format by returning a readable string of elapsed time" do 32 | allow(Time).to receive(:now).and_return(@start_time) 33 | @timer.start 34 | allow(Time).to receive(:now).and_return(@stop_time) 35 | @timer.finish 36 | expect(@timer.format).to eq("Finished in 33.000000 seconds") 37 | end 38 | 39 | it "responds to #register by registering itself with MSpec for appropriate actions" do 40 | expect(MSpec).to receive(:register).with(:start, @timer) 41 | expect(MSpec).to receive(:register).with(:finish, @timer) 42 | @timer.register 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/mspec/matchers.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/matchers/base' 2 | require 'mspec/matchers/be_an_instance_of' 3 | require 'mspec/matchers/be_ancestor_of' 4 | require 'mspec/matchers/be_close' 5 | require 'mspec/matchers/be_computed_by' 6 | require 'mspec/matchers/be_empty' 7 | require 'mspec/matchers/be_false' 8 | require 'mspec/matchers/be_kind_of' 9 | require 'mspec/matchers/be_nan' 10 | require 'mspec/matchers/be_nil' 11 | require 'mspec/matchers/be_true' 12 | require 'mspec/matchers/be_true_or_false' 13 | require 'mspec/matchers/complain' 14 | require 'mspec/matchers/eql' 15 | require 'mspec/matchers/equal' 16 | require 'mspec/matchers/equal_element' 17 | require 'mspec/matchers/have_constant' 18 | require 'mspec/matchers/have_class_variable' 19 | require 'mspec/matchers/have_instance_method' 20 | require 'mspec/matchers/have_instance_variable' 21 | require 'mspec/matchers/have_method' 22 | require 'mspec/matchers/have_private_instance_method' 23 | require 'mspec/matchers/have_private_method' 24 | require 'mspec/matchers/have_protected_instance_method' 25 | require 'mspec/matchers/have_public_instance_method' 26 | require 'mspec/matchers/have_singleton_method' 27 | require 'mspec/matchers/include' 28 | require 'mspec/matchers/include_any_of' 29 | require 'mspec/matchers/infinity' 30 | require 'mspec/matchers/match_yaml' 31 | require 'mspec/matchers/raise_error' 32 | require 'mspec/matchers/output' 33 | require 'mspec/matchers/output_to_fd' 34 | require 'mspec/matchers/respond_to' 35 | require 'mspec/matchers/signed_zero' 36 | require 'mspec/matchers/block_caller' 37 | require 'mspec/matchers/skip' 38 | -------------------------------------------------------------------------------- /spec/utils/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/utils/version' 3 | 4 | RSpec.describe SpecVersion, "#to_s" do 5 | it "returns the string with which it was initialized" do 6 | expect(SpecVersion.new("1.8").to_s).to eq("1.8") 7 | expect(SpecVersion.new("2.118.9").to_s).to eq("2.118.9") 8 | end 9 | end 10 | 11 | RSpec.describe SpecVersion, "#to_str" do 12 | it "returns the same string as #to_s" do 13 | version = SpecVersion.new("2.118.9") 14 | expect(version.to_str).to eq(version.to_s) 15 | end 16 | end 17 | 18 | RSpec.describe SpecVersion, "#to_i with ceil = false" do 19 | it "returns an integer representation of the version string" do 20 | expect(SpecVersion.new("2.23.10").to_i).to eq(1022310) 21 | end 22 | 23 | it "replaces missing version parts with zeros" do 24 | expect(SpecVersion.new("1.8").to_i).to eq(1010800) 25 | expect(SpecVersion.new("1.8.6").to_i).to eq(1010806) 26 | end 27 | end 28 | 29 | RSpec.describe SpecVersion, "#to_i with ceil = true" do 30 | it "returns an integer representation of the version string" do 31 | expect(SpecVersion.new("1.8.6", true).to_i).to eq(1010806) 32 | end 33 | 34 | it "fills in 9s for missing tiny values" do 35 | expect(SpecVersion.new("1.8", true).to_i).to eq(1010899) 36 | expect(SpecVersion.new("1.8.6", true).to_i).to eq(1010806) 37 | end 38 | end 39 | 40 | RSpec.describe SpecVersion, "#to_int" do 41 | it "returns the same value as #to_i" do 42 | version = SpecVersion.new("4.16.87") 43 | expect(version.to_int).to eq(version.to_i) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/stats.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/base' 2 | 3 | class StatsPerFileFormatter < BaseFormatter 4 | def initialize(out = nil) 5 | super(out) 6 | @data = {} 7 | @root = File.expand_path(MSpecScript.get(:prefix) || '.') 8 | end 9 | 10 | def register 11 | super 12 | MSpec.register :load, self 13 | MSpec.register :unload, self 14 | end 15 | 16 | # Resets the tallies so the counts are only for this file. 17 | def load 18 | tally.counter.examples = 0 19 | tally.counter.errors = 0 20 | tally.counter.failures = 0 21 | tally.counter.tagged = 0 22 | end 23 | 24 | def unload 25 | file = format_file MSpec.file 26 | 27 | raise if @data.key?(file) 28 | @data[file] = { 29 | examples: tally.counter.examples, 30 | errors: tally.counter.errors, 31 | failures: tally.counter.failures, 32 | tagged: tally.counter.tagged, 33 | } 34 | end 35 | 36 | def finish 37 | width = @data.keys.max_by(&:size).size 38 | f = "%3d" 39 | @data.each_pair do |file, data| 40 | total = data[:examples] 41 | passing = total - data[:errors] - data[:failures] - data[:tagged] 42 | puts "#{file.ljust(width)} #{f % passing}/#{f % total}" 43 | end 44 | 45 | require 'yaml' 46 | yaml = YAML.dump(@data) 47 | File.write "results-#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}.yml", yaml 48 | end 49 | 50 | private def format_file(file) 51 | if file.start_with?(@root) 52 | file[@root.size+1..-1] 53 | else 54 | raise file 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/matchers/be_computed_by_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/matchers' 3 | 4 | RSpec.describe BeComputedByMatcher do 5 | it "matches when all entries in the Array compute" do 6 | array = [ [65, "A"], 7 | [90, "Z"] ] 8 | expect(BeComputedByMatcher.new(:chr).matches?(array)).to be_truthy 9 | end 10 | 11 | it "matches when all entries in the Array with arguments compute" do 12 | array = [ [1, 2, 3], 13 | [2, 4, 6] ] 14 | expect(BeComputedByMatcher.new(:+).matches?(array)).to be_truthy 15 | end 16 | 17 | it "does not match when any entry in the Array does not compute" do 18 | array = [ [65, "A" ], 19 | [91, "Z" ] ] 20 | expect(BeComputedByMatcher.new(:chr).matches?(array)).to be_falsey 21 | end 22 | 23 | it "accepts an argument list to apply to each method call" do 24 | array = [ [65, "1000001" ], 25 | [90, "1011010" ] ] 26 | expect(BeComputedByMatcher.new(:to_s, 2).matches?(array)).to be_truthy 27 | end 28 | 29 | it "does not match when any entry in the Array with arguments does not compute" do 30 | array = [ [1, 2, 3], 31 | [2, 4, 7] ] 32 | expect(BeComputedByMatcher.new(:+).matches?(array)).to be_falsey 33 | end 34 | 35 | it "provides a useful failure message" do 36 | array = [ [65, "A" ], 37 | [91, "Z" ] ] 38 | matcher = BeComputedByMatcher.new(:chr) 39 | matcher.matches?(array) 40 | expect(matcher.failure_message).to eq(["Expected \"Z\"", "to be computed by 91.chr (computed \"[\" instead)"]) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/guards/block_device_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | 4 | RSpec.describe Object, "#with_block_device" do 5 | before :each do 6 | ScratchPad.clear 7 | 8 | @guard = BlockDeviceGuard.new 9 | allow(BlockDeviceGuard).to receive(:new).and_return(@guard) 10 | end 11 | 12 | platform_is_not :freebsd, :windows do 13 | it "yields if block device is available" do 14 | expect(@guard).to receive(:`).and_return("block devices") 15 | with_block_device { ScratchPad.record :yield } 16 | expect(ScratchPad.recorded).to eq(:yield) 17 | end 18 | 19 | it "does not yield if block device is not available" do 20 | expect(@guard).to receive(:`).and_return(nil) 21 | with_block_device { ScratchPad.record :yield } 22 | expect(ScratchPad.recorded).not_to eq(:yield) 23 | end 24 | end 25 | 26 | platform_is :freebsd, :windows do 27 | it "does not yield, since platform does not support block devices" do 28 | expect(@guard).not_to receive(:`) 29 | with_block_device { ScratchPad.record :yield } 30 | expect(ScratchPad.recorded).not_to eq(:yield) 31 | end 32 | end 33 | 34 | it "sets the name of the guard to :with_block_device" do 35 | with_block_device { } 36 | expect(@guard.name).to eq(:with_block_device) 37 | end 38 | 39 | it "calls #unregister even when an exception is raised in the guard block" do 40 | expect(@guard).to receive(:match?).and_return(true) 41 | expect(@guard).to receive(:unregister) 42 | expect do 43 | with_block_device { raise Exception } 44 | end.to raise_error(Exception) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/mspec/runner/actions/taglist.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/actions/filter' 2 | 3 | # TagListAction - prints out the descriptions for any specs 4 | # tagged with +tags+. If +tags+ is an empty list, prints out 5 | # descriptions for any specs that are tagged. 6 | class TagListAction 7 | def initialize(tags = nil) 8 | @tags = tags.nil? || tags.empty? ? nil : Array(tags) 9 | @filter = nil 10 | end 11 | 12 | # Returns true. This enables us to match any tag when loading 13 | # tags from the file. 14 | def include?(arg) 15 | true 16 | end 17 | 18 | # Returns true if any tagged descriptions matches +string+. 19 | def ===(string) 20 | @filter === string 21 | end 22 | 23 | # Prints a banner about matching tagged specs. 24 | def start 25 | if @tags 26 | print "\nListing specs tagged with #{@tags.map { |t| "'#{t}'" }.join(", ") }\n\n" 27 | else 28 | print "\nListing all tagged specs\n\n" 29 | end 30 | end 31 | 32 | # Creates a MatchFilter for specific tags or for all tags. 33 | def load 34 | @filter = nil 35 | desc = MSpec.read_tags(@tags || self).map { |t| t.description } 36 | @filter = MatchFilter.new(nil, *desc) unless desc.empty? 37 | end 38 | 39 | # Prints the spec description if it matches the filter. 40 | def after(state) 41 | return unless self === state.description 42 | print state.description, "\n" 43 | end 44 | 45 | def register 46 | MSpec.register :start, self 47 | MSpec.register :load, self 48 | MSpec.register :after, self 49 | end 50 | 51 | def unregister 52 | MSpec.unregister :start, self 53 | MSpec.unregister :load, self 54 | MSpec.unregister :after, self 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/integration/tag_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | RSpec.describe "Running mspec tag" do 5 | before :all do 6 | FileUtils.rm_rf 'spec/fixtures/tags' 7 | end 8 | 9 | after :all do 10 | FileUtils.rm_rf 'spec/fixtures/tags' 11 | end 12 | 13 | it "tags the failing specs" do 14 | fixtures = "spec/fixtures" 15 | out, ret = run_mspec("tag", "--add fails --fail #{fixtures}/tagging_spec.rb") 16 | q = BACKTRACE_QUOTE 17 | expect(out).to eq <' 31 | CWD/spec/fixtures/tagging_spec.rb:3:in #{q}' 32 | 33 | 2) 34 | Tag#me érròrs in unicode FAILED 35 | Expected 1 == 2 36 | to be truthy but was false 37 | CWD/spec/fixtures/tagging_spec.rb:13:in #{q}block (2 levels) in ' 38 | CWD/spec/fixtures/tagging_spec.rb:3:in #{q}' 39 | 40 | Finished in D.DDDDDD seconds 41 | 42 | 1 file, 3 examples, 3 expectations, 2 failures, 0 errors, 0 tagged 43 | EOS 44 | expect(ret.success?).to eq(false) 45 | end 46 | 47 | it "does not run already tagged specs" do 48 | fixtures = "spec/fixtures" 49 | out, ret = run_mspec("run", "--excl-tag fails #{fixtures}/tagging_spec.rb") 50 | expect(out).to eq <= 1.9" do 12 | it "matches when mod has the class variable, given as string" do 13 | matcher = HaveClassVariableMatcher.new('@foo') 14 | expect(matcher.matches?(IVarModMock)).to be_truthy 15 | end 16 | 17 | it "matches when mod has the class variable, given as symbol" do 18 | matcher = HaveClassVariableMatcher.new(:@foo) 19 | expect(matcher.matches?(IVarModMock)).to be_truthy 20 | end 21 | 22 | it "does not match when mod hasn't got the class variable, given as string" do 23 | matcher = HaveClassVariableMatcher.new('@bar') 24 | expect(matcher.matches?(IVarModMock)).to be_falsey 25 | end 26 | 27 | it "does not match when mod hasn't got the class variable, given as symbol" do 28 | matcher = HaveClassVariableMatcher.new(:@bar) 29 | expect(matcher.matches?(IVarModMock)).to be_falsey 30 | end 31 | 32 | it "provides a failure message for #should" do 33 | matcher = HaveClassVariableMatcher.new(:@bar) 34 | matcher.matches?(IVarModMock) 35 | expect(matcher.failure_message).to eq([ 36 | "Expected IVarModMock to have class variable '@bar'", 37 | "but it does not" 38 | ]) 39 | end 40 | 41 | it "provides a failure messoge for #should_not" do 42 | matcher = HaveClassVariableMatcher.new(:@bar) 43 | matcher.matches?(IVarModMock) 44 | expect(matcher.negative_failure_message).to eq([ 45 | "Expected IVarModMock NOT to have class variable '@bar'", 46 | "but it does" 47 | ]) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/mspec/runner/exception.rb: -------------------------------------------------------------------------------- 1 | # Initialize $MSPEC_DEBUG 2 | $MSPEC_DEBUG ||= false 3 | 4 | class ExceptionState 5 | attr_reader :description, :describe, :it, :exception 6 | 7 | def initialize(state, location, exception) 8 | @exception = exception 9 | @failure = exception.class == SpecExpectationNotMetError || exception.class == SpecExpectationNotFoundError 10 | 11 | @description = location ? "An exception occurred during: #{location}" : "" 12 | if state 13 | @description += "\n" unless @description.empty? 14 | @description += state.description 15 | @describe = state.describe 16 | @it = state.it 17 | else 18 | @describe = @it = "" 19 | end 20 | end 21 | 22 | def failure? 23 | @failure 24 | end 25 | 26 | def message 27 | message = @exception.message 28 | message = "" if message.empty? 29 | 30 | if @failure 31 | message 32 | elsif raise_error_message = RaiseErrorMatcher::FAILURE_MESSAGE_FOR_EXCEPTION[@exception] 33 | raise_error_message.join("\n") 34 | else 35 | "#{@exception.class}: #{message}" 36 | end 37 | end 38 | 39 | def backtrace 40 | @backtrace_filter ||= MSpecScript.config[:backtrace_filter] || %r{(?:/bin/mspec|/lib/mspec/)} 41 | 42 | bt = @exception.backtrace || [] 43 | unless $MSPEC_DEBUG 44 | # Exclude e 21 | if File.directory? name 22 | # OK, another process/thread created the same directory 23 | else 24 | raise e 25 | end 26 | end 27 | end 28 | end 29 | end 30 | 31 | # Recursively removes all files and directories in +path+ 32 | # if +path+ is a directory. Removes the file if +path+ is 33 | # a file. 34 | def rm_r(*paths) 35 | paths.each do |path| 36 | path = File.expand_path path 37 | 38 | prefix = SPEC_TEMP_DIR 39 | unless path[0, prefix.size] == prefix 40 | raise ArgumentError, "#{path} is not prefixed by #{prefix}" 41 | end 42 | 43 | # File.symlink? needs to be checked first as 44 | # File.exist? returns false for dangling symlinks 45 | if File.symlink? path 46 | File.unlink path 47 | elsif File.directory? path 48 | Dir.entries(path).each { |x| rm_r "#{path}/#{x}" unless x =~ /^\.\.?$/ } 49 | Dir.rmdir path 50 | elsif File.exist? path 51 | File.delete path 52 | end 53 | end 54 | end 55 | 56 | # Creates a file +name+. Creates the directory for +name+ 57 | # if it does not exist. 58 | def touch(name, mode = "w") 59 | mkdir_p File.dirname(name) 60 | 61 | File.open(name, mode) do |f| 62 | yield f if block_given? 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/matchers/have_instance_variable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe HaveInstanceVariableMatcher do 6 | before :each do 7 | @object = Object.new 8 | def @object.instance_variables 9 | [:@foo] 10 | end 11 | end 12 | 13 | it "matches when object has the instance variable, given as string" do 14 | matcher = HaveInstanceVariableMatcher.new('@foo') 15 | expect(matcher.matches?(@object)).to be_truthy 16 | end 17 | 18 | it "matches when object has the instance variable, given as symbol" do 19 | matcher = HaveInstanceVariableMatcher.new(:@foo) 20 | expect(matcher.matches?(@object)).to be_truthy 21 | end 22 | 23 | it "does not match when object hasn't got the instance variable, given as string" do 24 | matcher = HaveInstanceVariableMatcher.new('@bar') 25 | expect(matcher.matches?(@object)).to be_falsey 26 | end 27 | 28 | it "does not match when object hasn't got the instance variable, given as symbol" do 29 | matcher = HaveInstanceVariableMatcher.new(:@bar) 30 | expect(matcher.matches?(@object)).to be_falsey 31 | end 32 | 33 | it "provides a failure message for #should" do 34 | matcher = HaveInstanceVariableMatcher.new(:@bar) 35 | matcher.matches?(@object) 36 | expect(matcher.failure_message).to eq([ 37 | "Expected #{@object.inspect} to have instance variable '@bar'", 38 | "but it does not" 39 | ]) 40 | end 41 | 42 | it "provides a failure messoge for #should_not" do 43 | matcher = HaveInstanceVariableMatcher.new(:@bar) 44 | matcher.matches?(@object) 45 | expect(matcher.negative_failure_message).to eq([ 46 | "Expected #{@object.inspect} NOT to have instance variable '@bar'", 47 | "but it does" 48 | ]) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/matchers/include_any_of_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe IncludeAnyOfMatcher do 6 | it "matches when actual includes expected" do 7 | expect(IncludeAnyOfMatcher.new(2).matches?([1,2,3])).to eq(true) 8 | expect(IncludeAnyOfMatcher.new("b").matches?("abc")).to eq(true) 9 | end 10 | 11 | it "does not match when actual does not include expected" do 12 | expect(IncludeAnyOfMatcher.new(4).matches?([1,2,3])).to eq(false) 13 | expect(IncludeAnyOfMatcher.new("d").matches?("abc")).to eq(false) 14 | end 15 | 16 | it "matches when actual includes all expected" do 17 | expect(IncludeAnyOfMatcher.new(3, 2, 1).matches?([1,2,3])).to eq(true) 18 | expect(IncludeAnyOfMatcher.new("a", "b", "c").matches?("abc")).to eq(true) 19 | end 20 | 21 | it "matches when actual includes any expected" do 22 | expect(IncludeAnyOfMatcher.new(3, 4, 5).matches?([1,2,3])).to eq(true) 23 | expect(IncludeAnyOfMatcher.new("c", "d", "e").matches?("abc")).to eq(true) 24 | end 25 | 26 | it "does not match when actual does not include any expected" do 27 | expect(IncludeAnyOfMatcher.new(4, 5).matches?([1,2,3])).to eq(false) 28 | expect(IncludeAnyOfMatcher.new("de").matches?("abc")).to eq(false) 29 | end 30 | 31 | it "provides a useful failure message" do 32 | matcher = IncludeAnyOfMatcher.new(5, 6) 33 | matcher.matches?([1,2,3]) 34 | expect(matcher.failure_message).to eq(["Expected [1, 2, 3]", "to include any of [5, 6]"]) 35 | end 36 | 37 | it "provides a useful negative failure message" do 38 | matcher = IncludeAnyOfMatcher.new(1, 2, 3) 39 | matcher.matches?([1,2]) 40 | expect(matcher.negative_failure_message).to eq(["Expected [1, 2]", "not to include any of [1, 2, 3]"]) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/expectations/should_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rbconfig' 3 | 4 | RSpec.describe "MSpec" do 5 | before :all do 6 | path = RbConfig::CONFIG['bindir'] 7 | exe = RbConfig::CONFIG['ruby_install_name'] 8 | file = File.expand_path('../../fixtures/should.rb', __FILE__) 9 | @out = `#{path}/#{exe} #{file}` 10 | end 11 | 12 | describe "#should" do 13 | it "records failures" do 14 | expect(@out).to include <<-EOS 15 | 1) 16 | MSpec expectation method #should causes a failure to be recorded FAILED 17 | Expected 1 == 2 18 | to be truthy but was false 19 | EOS 20 | end 21 | 22 | it "raises exceptions for examples with no expectations" do 23 | expect(@out).to include <<-EOS 24 | 2) 25 | MSpec expectation method #should registers that an expectation has been encountered FAILED 26 | No behavior expectation was found in the example 27 | EOS 28 | end 29 | end 30 | 31 | describe "#should_not" do 32 | it "records failures" do 33 | expect(@out).to include <<-EOS 34 | 3) 35 | MSpec expectation method #should_not causes a failure to be recorded FAILED 36 | Expected 1 == 1 37 | to be falsy but was true 38 | EOS 39 | end 40 | 41 | it "raises exceptions for examples with no expectations" do 42 | expect(@out).to include <<-EOS 43 | 4) 44 | MSpec expectation method #should_not registers that an expectation has been encountered FAILED 45 | No behavior expectation was found in the example 46 | EOS 47 | end 48 | end 49 | 50 | it "prints status information" do 51 | expect(@out).to include ".FF..FF." 52 | end 53 | 54 | it "prints out a summary" do 55 | expect(@out).to include "0 files, 8 examples, 6 expectations, 4 failures, 0 errors" 56 | end 57 | 58 | it "records expectations" do 59 | expect(@out).to include "I was called 6 times" 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/matchers/be_close_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | # Adapted from RSpec 1.0.8 6 | RSpec.describe BeCloseMatcher do 7 | it "matches when actual == expected" do 8 | expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.0)).to eq(true) 9 | end 10 | 11 | it "matches when actual < (expected + tolerance)" do 12 | expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.49)).to eq(true) 13 | end 14 | 15 | it "matches when actual > (expected - tolerance)" do 16 | expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.51)).to eq(true) 17 | end 18 | 19 | it "matches when actual == (expected + tolerance)" do 20 | expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.5)).to eq(true) 21 | expect(BeCloseMatcher.new(3, 2).matches?(5)).to eq(true) 22 | end 23 | 24 | it "matches when actual == (expected - tolerance)" do 25 | expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.5)).to eq(true) 26 | expect(BeCloseMatcher.new(3, 2).matches?(1)).to eq(true) 27 | end 28 | 29 | it "does not match when actual < (expected - tolerance)" do 30 | expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.49)).to eq(false) 31 | end 32 | 33 | it "does not match when actual > (expected + tolerance)" do 34 | expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.51)).to eq(false) 35 | end 36 | 37 | it "provides a useful failure message" do 38 | matcher = BeCloseMatcher.new(5.0, 0.5) 39 | matcher.matches?(6.5) 40 | expect(matcher.failure_message).to eq(["Expected 6.5", "to be within 5.0 +/- 0.5"]) 41 | end 42 | 43 | it "provides a useful negative failure message" do 44 | matcher = BeCloseMatcher.new(5.0, 0.5) 45 | matcher.matches?(4.9) 46 | expect(matcher.negative_failure_message).to eq(["Expected 4.9", "not to be within 5.0 +/- 0.5"]) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/guards/support_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | 4 | RSpec.describe Object, "#not_supported_on" do 5 | before :each do 6 | ScratchPad.clear 7 | end 8 | 9 | it "raises an Exception when passed :ruby" do 10 | stub_const "RUBY_ENGINE", "jruby" 11 | expect { 12 | not_supported_on(:ruby) { ScratchPad.record :yield } 13 | }.to raise_error(Exception) 14 | expect(ScratchPad.recorded).not_to eq(:yield) 15 | end 16 | 17 | it "does not yield when #implementation? returns true" do 18 | stub_const "RUBY_ENGINE", "jruby" 19 | not_supported_on(:jruby) { ScratchPad.record :yield } 20 | expect(ScratchPad.recorded).not_to eq(:yield) 21 | end 22 | 23 | it "yields when #standard? returns true" do 24 | stub_const "RUBY_ENGINE", "ruby" 25 | not_supported_on(:rubinius) { ScratchPad.record :yield } 26 | expect(ScratchPad.recorded).to eq(:yield) 27 | end 28 | 29 | it "yields when #implementation? returns false" do 30 | stub_const "RUBY_ENGINE", "jruby" 31 | not_supported_on(:rubinius) { ScratchPad.record :yield } 32 | expect(ScratchPad.recorded).to eq(:yield) 33 | end 34 | end 35 | 36 | RSpec.describe Object, "#not_supported_on" do 37 | before :each do 38 | @guard = SupportedGuard.new 39 | allow(SupportedGuard).to receive(:new).and_return(@guard) 40 | end 41 | 42 | it "sets the name of the guard to :not_supported_on" do 43 | not_supported_on(:rubinius) { } 44 | expect(@guard.name).to eq(:not_supported_on) 45 | end 46 | 47 | it "calls #unregister even when an exception is raised in the guard block" do 48 | expect(@guard).to receive(:match?).and_return(false) 49 | expect(@guard).to receive(:unregister) 50 | expect do 51 | not_supported_on(:rubinius) { raise Exception } 52 | end.to raise_error(Exception) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/matchers/have_instance_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | class HIMMSpecs 6 | def instance_method 7 | end 8 | 9 | class Subclass < HIMMSpecs 10 | def instance_sub_method 11 | end 12 | end 13 | end 14 | 15 | RSpec.describe HaveInstanceMethodMatcher do 16 | it "inherits from MethodMatcher" do 17 | expect(HaveInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) 18 | end 19 | 20 | it "matches when mod has the instance method" do 21 | matcher = HaveInstanceMethodMatcher.new :instance_method 22 | expect(matcher.matches?(HIMMSpecs)).to be_truthy 23 | expect(matcher.matches?(HIMMSpecs::Subclass)).to be_truthy 24 | end 25 | 26 | it "does not match when mod does not have the instance method" do 27 | matcher = HaveInstanceMethodMatcher.new :another_method 28 | expect(matcher.matches?(HIMMSpecs)).to be_falsey 29 | end 30 | 31 | it "does not match if the method is in a superclass and include_super is false" do 32 | matcher = HaveInstanceMethodMatcher.new :instance_method, false 33 | expect(matcher.matches?(HIMMSpecs::Subclass)).to be_falsey 34 | end 35 | 36 | it "provides a failure message for #should" do 37 | matcher = HaveInstanceMethodMatcher.new :some_method 38 | matcher.matches?(HIMMSpecs) 39 | expect(matcher.failure_message).to eq([ 40 | "Expected HIMMSpecs to have instance method 'some_method'", 41 | "but it does not" 42 | ]) 43 | end 44 | 45 | it "provides a failure messoge for #should_not" do 46 | matcher = HaveInstanceMethodMatcher.new :some_method 47 | matcher.matches?(HIMMSpecs) 48 | expect(matcher.negative_failure_message).to eq([ 49 | "Expected HIMMSpecs NOT to have instance method 'some_method'", 50 | "but it does" 51 | ]) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/guards/endian_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | 4 | RSpec.describe Object, "#big_endian" do 5 | before :each do 6 | @guard = BigEndianGuard.new 7 | allow(BigEndianGuard).to receive(:new).and_return(@guard) 8 | ScratchPad.clear 9 | end 10 | 11 | it "yields on big-endian platforms" do 12 | allow(@guard).to receive(:pattern).and_return([?\001]) 13 | big_endian { ScratchPad.record :yield } 14 | expect(ScratchPad.recorded).to eq(:yield) 15 | end 16 | 17 | it "does not yield on little-endian platforms" do 18 | allow(@guard).to receive(:pattern).and_return([?\000]) 19 | big_endian { ScratchPad.record :yield } 20 | expect(ScratchPad.recorded).not_to eq(:yield) 21 | end 22 | 23 | it "sets the name of the guard to :big_endian" do 24 | big_endian { } 25 | expect(@guard.name).to eq(:big_endian) 26 | end 27 | 28 | it "calls #unregister even when an exception is raised in the guard block" do 29 | allow(@guard).to receive(:pattern).and_return([?\001]) 30 | expect(@guard).to receive(:unregister) 31 | expect do 32 | big_endian { raise Exception } 33 | end.to raise_error(Exception) 34 | end 35 | end 36 | 37 | RSpec.describe Object, "#little_endian" do 38 | before :each do 39 | @guard = BigEndianGuard.new 40 | allow(BigEndianGuard).to receive(:new).and_return(@guard) 41 | ScratchPad.clear 42 | end 43 | 44 | it "yields on little-endian platforms" do 45 | allow(@guard).to receive(:pattern).and_return([?\000]) 46 | little_endian { ScratchPad.record :yield } 47 | expect(ScratchPad.recorded).to eq(:yield) 48 | end 49 | 50 | it "does not yield on big-endian platforms" do 51 | allow(@guard).to receive(:pattern).and_return([?\001]) 52 | little_endian { ScratchPad.record :yield } 53 | expect(ScratchPad.recorded).not_to eq(:yield) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/matchers/have_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | class HMMSpecs 6 | def instance_method 7 | end 8 | 9 | class Subclass < HMMSpecs 10 | def instance_sub_method 11 | end 12 | end 13 | end 14 | 15 | RSpec.describe HaveMethodMatcher do 16 | it "inherits from MethodMatcher" do 17 | expect(HaveMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) 18 | end 19 | 20 | it "matches when mod has the method" do 21 | matcher = HaveMethodMatcher.new :instance_method 22 | expect(matcher.matches?(HMMSpecs)).to be_truthy 23 | expect(matcher.matches?(HMMSpecs.new)).to be_truthy 24 | expect(matcher.matches?(HMMSpecs::Subclass)).to be_truthy 25 | expect(matcher.matches?(HMMSpecs::Subclass.new)).to be_truthy 26 | end 27 | 28 | it "does not match when mod does not have the method" do 29 | matcher = HaveMethodMatcher.new :another_method 30 | expect(matcher.matches?(HMMSpecs)).to be_falsey 31 | end 32 | 33 | it "does not match if the method is in a superclass and include_super is false" do 34 | matcher = HaveMethodMatcher.new :instance_method, false 35 | expect(matcher.matches?(HMMSpecs::Subclass)).to be_falsey 36 | end 37 | 38 | it "provides a failure message for #should" do 39 | matcher = HaveMethodMatcher.new :some_method 40 | matcher.matches?(HMMSpecs) 41 | expect(matcher.failure_message).to eq([ 42 | "Expected HMMSpecs to have method 'some_method'", 43 | "but it does not" 44 | ]) 45 | end 46 | 47 | it "provides a failure messoge for #should_not" do 48 | matcher = HaveMethodMatcher.new :some_method 49 | matcher.matches?(HMMSpecs) 50 | expect(matcher.negative_failure_message).to eq([ 51 | "Expected HMMSpecs NOT to have method 'some_method'", 52 | "but it does" 53 | ]) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/mspec/helpers/io.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/guards/feature' 2 | 3 | class IOStub 4 | def initialize 5 | @buffer = [] 6 | @output = '' 7 | end 8 | 9 | def write(*str) 10 | self << str.join('') 11 | end 12 | 13 | def << str 14 | @buffer << str 15 | self 16 | end 17 | 18 | def print(*str) 19 | write(str.join('') + $\.to_s) 20 | end 21 | 22 | def method_missing(name, *args, &block) 23 | to_s.send(name, *args, &block) 24 | end 25 | 26 | def == other 27 | to_s == other 28 | end 29 | 30 | def =~ other 31 | to_s =~ other 32 | end 33 | 34 | def puts(*str) 35 | if str.empty? 36 | write "\n" 37 | else 38 | write(str.collect { |s| s.to_s.chomp }.concat([nil]).join("\n")) 39 | end 40 | end 41 | 42 | def printf(format, *args) 43 | self << sprintf(format, *args) 44 | end 45 | 46 | def flush 47 | @output += @buffer.join('') 48 | @buffer.clear 49 | self 50 | end 51 | 52 | def to_s 53 | flush 54 | @output 55 | end 56 | 57 | alias_method :to_str, :to_s 58 | 59 | def inspect 60 | to_s.inspect 61 | end 62 | end 63 | 64 | # Creates a "bare" file descriptor (i.e. one that is not associated 65 | # with any Ruby object). The file descriptor can safely be passed 66 | # to IO.new without creating a Ruby object alias to the fd. 67 | def new_fd(name, mode = "w:utf-8") 68 | if mode.kind_of? Hash 69 | if mode.key? :mode 70 | mode = mode[:mode] 71 | else 72 | raise ArgumentError, "new_fd options Hash must include :mode" 73 | end 74 | end 75 | 76 | IO.sysopen name, mode 77 | end 78 | 79 | # Creates an IO instance for a temporary file name. The file 80 | # must be deleted. 81 | def new_io(name, mode = "w:utf-8") 82 | if Hash === mode # Avoid kwargs warnings on Ruby 2.7+ 83 | File.new(name, **mode) 84 | else 85 | File.new(name, mode) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/mspec/matchers/output_to_fd.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/helpers/tmp' 2 | 3 | # Lower-level output speccing mechanism for a single 4 | # output stream. Unlike OutputMatcher which provides 5 | # methods to capture the output, we actually replace 6 | # the FD itself so that there is no reliance on a 7 | # certain method being used. 8 | class OutputToFDMatcher 9 | def initialize(expected, to) 10 | @to, @expected = to, expected 11 | 12 | case @to 13 | when STDOUT 14 | @to_name = "STDOUT" 15 | when STDERR 16 | @to_name = "STDERR" 17 | when IO 18 | @to_name = @to.object_id.to_s 19 | else 20 | raise ArgumentError, "#{@to.inspect} is not a supported output target" 21 | end 22 | end 23 | 24 | def with_tmp 25 | path = tmp("mspec_output_to_#{$$}_#{Time.now.to_i}") 26 | File.open(path, 'w+') { |io| 27 | yield(io) 28 | } 29 | ensure 30 | File.delete path if path 31 | end 32 | 33 | def matches?(block) 34 | old_to = @to.dup 35 | with_tmp do |out| 36 | # Replacing with a file handle so that Readline etc. work 37 | @to.reopen out 38 | begin 39 | block.call 40 | ensure 41 | @to.reopen old_to 42 | old_to.close 43 | end 44 | 45 | out.rewind 46 | @actual = out.read 47 | 48 | case @expected 49 | when Regexp 50 | !(@actual =~ @expected).nil? 51 | else 52 | @actual == @expected 53 | end 54 | end 55 | end 56 | 57 | def failure_message() 58 | ["Expected (#{@to_name}): #{@expected.inspect}\n", 59 | "#{'but got'.rjust(@to_name.length + 10)}: #{@actual.inspect}\nBacktrace"] 60 | end 61 | 62 | def negative_failure_message() 63 | ["Expected output (#{@to_name}) to NOT be:\n", @actual.inspect] 64 | end 65 | end 66 | 67 | module MSpecMatchers 68 | private def output_to_fd(what, where = STDOUT) 69 | OutputToFDMatcher.new what, where 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/matchers/have_public_instance_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | class HPIMMSpecs 6 | def public_method 7 | end 8 | 9 | class Subclass < HPIMMSpecs 10 | def public_sub_method 11 | end 12 | end 13 | end 14 | 15 | RSpec.describe HavePublicInstanceMethodMatcher do 16 | it "inherits from MethodMatcher" do 17 | expect(HavePublicInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) 18 | end 19 | 20 | it "matches when mod has the public instance method" do 21 | matcher = HavePublicInstanceMethodMatcher.new :public_method 22 | expect(matcher.matches?(HPIMMSpecs)).to be_truthy 23 | expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy 24 | end 25 | 26 | it "does not match when mod does not have the public instance method" do 27 | matcher = HavePublicInstanceMethodMatcher.new :another_method 28 | expect(matcher.matches?(HPIMMSpecs)).to be_falsey 29 | end 30 | 31 | it "does not match if the method is in a superclass and include_super is false" do 32 | matcher = HavePublicInstanceMethodMatcher.new :public_method, false 33 | expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey 34 | end 35 | 36 | it "provides a failure message for #should" do 37 | matcher = HavePublicInstanceMethodMatcher.new :some_method 38 | matcher.matches?(HPIMMSpecs) 39 | expect(matcher.failure_message).to eq([ 40 | "Expected HPIMMSpecs to have public instance method 'some_method'", 41 | "but it does not" 42 | ]) 43 | end 44 | 45 | it "provides a failure messoge for #should_not" do 46 | matcher = HavePublicInstanceMethodMatcher.new :some_method 47 | matcher.matches?(HPIMMSpecs) 48 | expect(matcher.negative_failure_message).to eq([ 49 | "Expected HPIMMSpecs NOT to have public instance method 'some_method'", 50 | "but it does" 51 | ]) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/runner/formatters/multi_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | require 'mspec/runner/formatters/dotted' 3 | require 'mspec/runner/formatters/multi' 4 | require 'mspec/runner/example' 5 | require 'yaml' 6 | 7 | RSpec.describe MultiFormatter, "#aggregate_results" do 8 | before :each do 9 | @stdout, $stdout = $stdout, IOStub.new 10 | 11 | @file = double("file").as_null_object 12 | 13 | allow(File).to receive(:delete) 14 | allow(File).to receive(:read) 15 | 16 | @hash = { "files"=>1, "examples"=>1, "expectations"=>2, "failures"=>0, "errors"=>0 } 17 | allow(YAML).to receive(:load).and_return(@hash) 18 | 19 | @formatter = DottedFormatter.new.extend(MultiFormatter) 20 | allow(@formatter.timer).to receive(:format).and_return("Finished in 42 seconds") 21 | end 22 | 23 | after :each do 24 | $stdout = @stdout 25 | end 26 | 27 | it "outputs a summary without errors" do 28 | @formatter.aggregate_results(["a", "b"]) 29 | @formatter.finish 30 | expect($stdout).to eq(%[ 31 | 32 | Finished in 42 seconds 33 | 34 | 2 files, 2 examples, 4 expectations, 0 failures, 0 errors, 0 tagged 35 | ]) 36 | end 37 | 38 | it "outputs a summary with errors" do 39 | @hash["exceptions"] = [ 40 | "Some#method works real good FAILED\nExpected real good\n to equal fail\n\nfoo.rb:1\nfoo.rb:2", 41 | "Some#method never fails ERROR\nExpected 5\n to equal 3\n\nfoo.rb:1\nfoo.rb:2" 42 | ] 43 | @formatter.aggregate_results(["a"]) 44 | @formatter.finish 45 | expect($stdout).to eq(%[ 46 | 47 | 1) 48 | Some#method works real good FAILED 49 | Expected real good 50 | to equal fail 51 | 52 | foo.rb:1 53 | foo.rb:2 54 | 55 | 2) 56 | Some#method never fails ERROR 57 | Expected 5 58 | to equal 3 59 | 60 | foo.rb:1 61 | foo.rb:2 62 | 63 | Finished in 42 seconds 64 | 65 | 1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged 66 | ]) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/fixtures/should.rb: -------------------------------------------------------------------------------- 1 | $: << File.dirname(__FILE__) + '/../../lib' 2 | require 'mspec' 3 | require 'mspec/utils/script' 4 | 5 | # The purpose of these specs is to confirm that the #should 6 | # and #should_not methods are functioning appropriately. We 7 | # use a separate spec file that is invoked from the MSpec 8 | # specs but is run by MSpec. This avoids conflicting with 9 | # RSpec's #should and #should_not methods. 10 | 11 | raise "RSpec should not be loaded" if defined?(RSpec) 12 | 13 | class ShouldSpecsMonitor 14 | def initialize 15 | @called = 0 16 | end 17 | 18 | def expectation(state) 19 | @called += 1 20 | end 21 | 22 | def finish 23 | puts "I was called #{@called} times" 24 | end 25 | end 26 | 27 | # Simplistic runner 28 | formatter = DottedFormatter.new 29 | formatter.register 30 | 31 | monitor = ShouldSpecsMonitor.new 32 | MSpec.register :expectation, monitor 33 | MSpec.register :finish, monitor 34 | 35 | at_exit { MSpec.actions :finish } 36 | 37 | MSpec.actions :start 38 | MSpec.setup_env 39 | 40 | # Specs 41 | describe "MSpec expectation method #should" do 42 | it "accepts a matcher" do 43 | :sym.should be_kind_of(Symbol) 44 | end 45 | 46 | it "causes a failure to be recorded" do 47 | 1.should == 2 48 | end 49 | 50 | it "registers that an expectation has been encountered" do 51 | # an empty example block causes an exception because 52 | # no expectation was encountered 53 | end 54 | 55 | it "invokes the MSpec :expectation actions" do 56 | 1.should == 1 57 | end 58 | end 59 | 60 | describe "MSpec expectation method #should_not" do 61 | it "accepts a matcher" do 62 | "sym".should_not be_kind_of(Symbol) 63 | end 64 | 65 | it "causes a failure to be recorded" do 66 | 1.should_not == 1 67 | end 68 | 69 | it "registers that an expectation has been encountered" do 70 | end 71 | 72 | it "invokes the MSpec :expectation actions" do 73 | 1.should_not == 2 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/mspec/helpers/tmp.rb: -------------------------------------------------------------------------------- 1 | # Creates a temporary directory in the current working directory 2 | # for temporary files created while running the specs. All specs 3 | # should clean up any temporary files created so that the temp 4 | # directory is empty when the process exits. 5 | 6 | SPEC_TEMP_DIR_PID = Process.pid 7 | 8 | if spec_temp_dir = ENV["SPEC_TEMP_DIR"] 9 | spec_temp_dir = File.realdirpath(spec_temp_dir) 10 | else 11 | spec_temp_dir = "#{File.realpath(Dir.pwd)}/rubyspec_temp/#{SPEC_TEMP_DIR_PID}" 12 | end 13 | SPEC_TEMP_DIR = spec_temp_dir 14 | 15 | SPEC_TEMP_UNIQUIFIER = +"0" 16 | 17 | at_exit do 18 | begin 19 | if SPEC_TEMP_DIR_PID == Process.pid 20 | Dir.delete SPEC_TEMP_DIR if File.directory? SPEC_TEMP_DIR 21 | end 22 | rescue SystemCallError 23 | STDERR.puts <<-EOM 24 | 25 | ----------------------------------------------------- 26 | The rubyspec temp directory is not empty. Ensure that 27 | all specs are cleaning up temporary files: 28 | #{SPEC_TEMP_DIR} 29 | ----------------------------------------------------- 30 | 31 | EOM 32 | rescue Object => e 33 | STDERR.puts "failed to remove spec temp directory" 34 | STDERR.puts e.message 35 | end 36 | end 37 | 38 | def tmp(name, uniquify = true) 39 | if Dir.exist? SPEC_TEMP_DIR 40 | stat = File.stat(SPEC_TEMP_DIR) 41 | if stat.world_writable? and !stat.sticky? 42 | raise ArgumentError, "SPEC_TEMP_DIR (#{SPEC_TEMP_DIR}) is world writable but not sticky" 43 | end 44 | else 45 | platform_is_not :windows do 46 | umask = File.umask 47 | if (umask & 0002) == 0 # o+w 48 | raise ArgumentError, "File.umask #=> #{umask.to_s(8)} (world-writable)" 49 | end 50 | end 51 | mkdir_p SPEC_TEMP_DIR 52 | end 53 | 54 | if uniquify and !name.empty? 55 | slash = name.rindex "/" 56 | index = slash ? slash + 1 : 0 57 | name = +name 58 | name.insert index, "#{SPEC_TEMP_UNIQUIFIER.succ!}-" 59 | end 60 | 61 | File.join SPEC_TEMP_DIR, name 62 | end 63 | -------------------------------------------------------------------------------- /spec/matchers/have_private_instance_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | class HPIMMSpecs 6 | private 7 | 8 | def private_method 9 | end 10 | 11 | class Subclass < HPIMMSpecs 12 | private 13 | 14 | def private_sub_method 15 | end 16 | end 17 | end 18 | 19 | RSpec.describe HavePrivateInstanceMethodMatcher do 20 | it "inherits from MethodMatcher" do 21 | expect(HavePrivateInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) 22 | end 23 | 24 | it "matches when mod has the private instance method" do 25 | matcher = HavePrivateInstanceMethodMatcher.new :private_method 26 | expect(matcher.matches?(HPIMMSpecs)).to be_truthy 27 | expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy 28 | end 29 | 30 | it "does not match when mod does not have the private instance method" do 31 | matcher = HavePrivateInstanceMethodMatcher.new :another_method 32 | expect(matcher.matches?(HPIMMSpecs)).to be_falsey 33 | end 34 | 35 | it "does not match if the method is in a superclass and include_super is false" do 36 | matcher = HavePrivateInstanceMethodMatcher.new :private_method, false 37 | expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey 38 | end 39 | 40 | it "provides a failure message for #should" do 41 | matcher = HavePrivateInstanceMethodMatcher.new :some_method 42 | matcher.matches?(HPIMMSpecs) 43 | expect(matcher.failure_message).to eq([ 44 | "Expected HPIMMSpecs to have private instance method 'some_method'", 45 | "but it does not" 46 | ]) 47 | end 48 | 49 | it "provides a failure message for #should_not" do 50 | matcher = HavePrivateInstanceMethodMatcher.new :some_method 51 | matcher.matches?(HPIMMSpecs) 52 | expect(matcher.negative_failure_message).to eq([ 53 | "Expected HPIMMSpecs NOT to have private instance method 'some_method'", 54 | "but it does" 55 | ]) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/matchers/have_protected_instance_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | class HPIMMSpecs 6 | protected 7 | 8 | def protected_method 9 | end 10 | 11 | class Subclass < HPIMMSpecs 12 | protected 13 | 14 | def protected_sub_method 15 | end 16 | end 17 | end 18 | 19 | RSpec.describe HaveProtectedInstanceMethodMatcher do 20 | it "inherits from MethodMatcher" do 21 | expect(HaveProtectedInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) 22 | end 23 | 24 | it "matches when mod has the protected instance method" do 25 | matcher = HaveProtectedInstanceMethodMatcher.new :protected_method 26 | expect(matcher.matches?(HPIMMSpecs)).to be_truthy 27 | expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy 28 | end 29 | 30 | it "does not match when mod does not have the protected instance method" do 31 | matcher = HaveProtectedInstanceMethodMatcher.new :another_method 32 | expect(matcher.matches?(HPIMMSpecs)).to be_falsey 33 | end 34 | 35 | it "does not match if the method is in a superclass and include_super is false" do 36 | matcher = HaveProtectedInstanceMethodMatcher.new :protected_method, false 37 | expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey 38 | end 39 | 40 | it "provides a failure message for #should" do 41 | matcher = HaveProtectedInstanceMethodMatcher.new :some_method 42 | matcher.matches?(HPIMMSpecs) 43 | expect(matcher.failure_message).to eq([ 44 | "Expected HPIMMSpecs to have protected instance method 'some_method'", 45 | "but it does not" 46 | ]) 47 | end 48 | 49 | it "provides a failure messoge for #should_not" do 50 | matcher = HaveProtectedInstanceMethodMatcher.new :some_method 51 | matcher.matches?(HPIMMSpecs) 52 | expect(matcher.negative_failure_message).to eq([ 53 | "Expected HPIMMSpecs NOT to have protected instance method 'some_method'", 54 | "but it does" 55 | ]) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/matchers/output_to_fd_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/expectations/expectations' 3 | require 'mspec/matchers' 4 | 5 | RSpec.describe OutputToFDMatcher do 6 | # Figure out how in the hell to achieve this 7 | it "matches when running the block produces the expected output to the given FD" do 8 | expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { $stderr.print "Hi\n" })).to eq(true) 9 | end 10 | 11 | it "does not match if running the block does not produce the expected output to the FD" do 12 | expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { $stderr.puts("Hello\n") })).to eq(false) 13 | end 14 | 15 | it "propagate the exception if one is thrown while matching" do 16 | exc = RuntimeError.new("propagates") 17 | expect { 18 | expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { 19 | raise exc 20 | })).to eq(false) 21 | }.to raise_error(exc) 22 | end 23 | 24 | it "defaults to matching against STDOUT" do 25 | object = Object.new 26 | object.extend MSpecMatchers 27 | expect(object.send(:output_to_fd, "Hi\n").matches?(lambda { $stdout.print "Hi\n" })).to eq(true) 28 | end 29 | 30 | it "accepts any IO instance" do 31 | io = IO.new STDOUT.fileno 32 | expect(OutputToFDMatcher.new("Hi\n", io).matches?(lambda { io.print "Hi\n" })).to eq(true) 33 | end 34 | 35 | it "allows matching with a Regexp" do 36 | s = "Hi there\n" 37 | expect(OutputToFDMatcher.new(/Hi/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) 38 | expect(OutputToFDMatcher.new(/Hi?/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) 39 | expect(OutputToFDMatcher.new(/[hH]i?/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) 40 | expect(OutputToFDMatcher.new(/.*/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) 41 | expect(OutputToFDMatcher.new(/H.*?here/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) 42 | expect(OutputToFDMatcher.new(/Ahoy/, STDERR).matches?(lambda { $stderr.print s })).to eq(false) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.disable_monkey_patching! 3 | config.raise_errors_for_deprecations! 4 | end 5 | 6 | require 'mspec' 7 | 8 | # Remove this when MRI has intelligent warnings 9 | $VERBOSE = nil unless $VERBOSE 10 | 11 | class MOSConfig < Hash 12 | def initialize 13 | self[:loadpath] = [] 14 | self[:requires] = [] 15 | self[:flags] = [] 16 | self[:options] = [] 17 | self[:includes] = [] 18 | self[:excludes] = [] 19 | self[:patterns] = [] 20 | self[:xpatterns] = [] 21 | self[:tags] = [] 22 | self[:xtags] = [] 23 | self[:atags] = [] 24 | self[:astrings] = [] 25 | self[:target] = 'ruby' 26 | self[:command] = nil 27 | self[:ltags] = [] 28 | self[:files] = [] 29 | self[:launch] = [] 30 | end 31 | end 32 | 33 | def new_option 34 | config = MOSConfig.new 35 | return MSpecOptions.new("spec", 20, config), config 36 | end 37 | 38 | # Just to have an exception name output not be "Exception" 39 | class MSpecExampleError < Exception 40 | end 41 | 42 | def hide_deprecation_warnings 43 | allow(MSpec).to receive(:deprecate) 44 | end 45 | 46 | def run_mspec(command, args) 47 | cwd = Dir.pwd 48 | command = " #{command}" unless command.start_with?('-') 49 | cmd = "#{cwd}/bin/mspec#{command} -B spec/fixtures/config.mspec #{args}" 50 | out = `#{cmd} 2>&1` 51 | ret = $? 52 | out = out.sub(/\A\$.+\n/, '') # Remove printed command line 53 | out = out.sub(RUBY_DESCRIPTION, "RUBY_DESCRIPTION") 54 | out = out.gsub(/\d+\.\d{6}/, "D.DDDDDD") # Specs total time 55 | out = out.gsub(/\d{2}:\d{2}:\d{2}/, "00:00:00") # Progress bar time 56 | out = out.gsub(cwd, "CWD") 57 | return out, ret 58 | end 59 | 60 | def ensure_mspec_method(method) 61 | file, _line = method.source_location 62 | expect(file).to start_with(File.expand_path('../../lib/mspec', __FILE__ )) 63 | end 64 | 65 | PublicMSpecMatchers = Class.new { 66 | include MSpecMatchers 67 | public :raise_error 68 | }.new 69 | 70 | BACKTRACE_QUOTE = RUBY_VERSION >= "3.4" ? "'" : "`" 71 | -------------------------------------------------------------------------------- /spec/runner/formatters/describe_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | require 'mspec/runner/formatters/describe' 3 | require 'mspec/runner/example' 4 | 5 | RSpec.describe DescribeFormatter, "#finish" do 6 | before :each do 7 | allow(MSpec).to receive(:register) 8 | allow(MSpec).to receive(:unregister) 9 | 10 | @timer = double("timer").as_null_object 11 | allow(TimerAction).to receive(:new).and_return(@timer) 12 | allow(@timer).to receive(:format).and_return("Finished in 2.0 seconds") 13 | 14 | $stdout = @out = IOStub.new 15 | context = ContextState.new "Class#method" 16 | @state = ExampleState.new(context, "runs") 17 | 18 | @formatter = DescribeFormatter.new 19 | @formatter.register 20 | 21 | @tally = @formatter.tally 22 | @counter = @tally.counter 23 | 24 | @counter.files! 25 | @counter.examples! 26 | @counter.expectations! 2 27 | end 28 | 29 | after :each do 30 | $stdout = STDOUT 31 | end 32 | 33 | it "prints a summary of elapsed time" do 34 | @formatter.finish 35 | expect(@out).to match(/^Finished in 2.0 seconds$/) 36 | end 37 | 38 | it "prints a tally of counts" do 39 | @formatter.finish 40 | expect(@out).to match(/^1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged$/) 41 | end 42 | 43 | it "does not print exceptions" do 44 | @formatter.finish 45 | expect(@out).to eq(%[ 46 | 47 | Finished in 2.0 seconds 48 | 49 | 1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged 50 | ]) 51 | end 52 | 53 | it "prints a summary of failures and errors for each describe block" do 54 | exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken") 55 | allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") 56 | @formatter.exception exc 57 | @formatter.finish 58 | expect(@out).to eq(%[ 59 | 60 | Class#method 0 failures, 1 error 61 | 62 | Finished in 2.0 seconds 63 | 64 | 1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged 65 | ]) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/guards/conflict_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mspec/guards' 3 | 4 | RSpec.describe Object, "#conflicts_with" do 5 | before :each do 6 | hide_deprecation_warnings 7 | ScratchPad.clear 8 | end 9 | 10 | it "does not yield if Object.constants includes any of the arguments" do 11 | allow(Object).to receive(:constants).and_return(["SomeClass", "OtherClass"]) 12 | conflicts_with(:SomeClass, :AClass, :BClass) { ScratchPad.record :yield } 13 | expect(ScratchPad.recorded).not_to eq(:yield) 14 | end 15 | 16 | it "does not yield if Object.constants (as Symbols) includes any of the arguments" do 17 | allow(Object).to receive(:constants).and_return([:SomeClass, :OtherClass]) 18 | conflicts_with(:SomeClass, :AClass, :BClass) { ScratchPad.record :yield } 19 | expect(ScratchPad.recorded).not_to eq(:yield) 20 | end 21 | 22 | it "yields if Object.constants does not include any of the arguments" do 23 | allow(Object).to receive(:constants).and_return(["SomeClass", "OtherClass"]) 24 | conflicts_with(:AClass, :BClass) { ScratchPad.record :yield } 25 | expect(ScratchPad.recorded).to eq(:yield) 26 | end 27 | 28 | it "yields if Object.constants (as Symbols) does not include any of the arguments" do 29 | allow(Object).to receive(:constants).and_return([:SomeClass, :OtherClass]) 30 | conflicts_with(:AClass, :BClass) { ScratchPad.record :yield } 31 | expect(ScratchPad.recorded).to eq(:yield) 32 | end 33 | end 34 | 35 | RSpec.describe Object, "#conflicts_with" do 36 | before :each do 37 | hide_deprecation_warnings 38 | @guard = ConflictsGuard.new 39 | allow(ConflictsGuard).to receive(:new).and_return(@guard) 40 | end 41 | 42 | it "sets the name of the guard to :conflicts_with" do 43 | conflicts_with(:AClass, :BClass) { } 44 | expect(@guard.name).to eq(:conflicts_with) 45 | end 46 | 47 | it "calls #unregister even when an exception is raised in the guard block" do 48 | expect(@guard).to receive(:unregister) 49 | expect do 50 | conflicts_with(:AClass, :BClass) { raise Exception } 51 | end.to raise_error(Exception) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/mspec/matchers/complain.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/helpers/io' 2 | 3 | class ComplainMatcher 4 | def initialize(complaint = nil, options = nil) 5 | # the proper solution is to use double splat operator e.g. 6 | # def initialize(complaint = nil, **options) 7 | # but we are trying to minimize language features required to run MSpec 8 | if complaint.is_a?(Hash) 9 | @complaint = nil 10 | @options = complaint 11 | else 12 | @complaint = complaint 13 | @options = options || {} 14 | end 15 | end 16 | 17 | def matches?(proc) 18 | @saved_err = $stderr 19 | @verbose = $VERBOSE 20 | err = IOStub.new 21 | 22 | $stderr = err 23 | $VERBOSE = @options.key?(:verbose) ? @options[:verbose] : false 24 | begin 25 | proc.call 26 | ensure 27 | $VERBOSE = @verbose 28 | $stderr = @saved_err 29 | end 30 | 31 | @warning = err.to_s 32 | unless @complaint.nil? 33 | case @complaint 34 | when Regexp 35 | return false unless @warning =~ @complaint 36 | else 37 | return false unless @warning == @complaint 38 | end 39 | end 40 | 41 | return @warning.empty? ? false : true 42 | end 43 | 44 | def failure_message 45 | if @complaint.nil? 46 | ["Expected a warning", "but received none"] 47 | elsif @complaint.kind_of? Regexp 48 | ["Expected warning to match: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"] 49 | else 50 | ["Expected warning: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"] 51 | end 52 | end 53 | 54 | def negative_failure_message 55 | if @complaint.nil? 56 | ["Unexpected warning: ", @warning.chomp.inspect] 57 | elsif @complaint.kind_of? Regexp 58 | ["Expected warning not to match: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"] 59 | else 60 | ["Expected warning: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"] 61 | end 62 | end 63 | end 64 | 65 | module MSpecMatchers 66 | private def complain(complaint = nil, options = nil) 67 | ComplainMatcher.new(complaint, options) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/html.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/base' 2 | 3 | class HtmlFormatter < BaseFormatter 4 | def register 5 | super 6 | MSpec.register :start, self 7 | MSpec.register :enter, self 8 | MSpec.register :leave, self 9 | end 10 | 11 | def start 12 | print <<-EOH 13 | 15 | 16 | 17 | Spec Output For #{RUBY_ENGINE} (#{RUBY_VERSION}) 18 | 32 | 33 | 34 | EOH 35 | end 36 | 37 | def enter(describe) 38 | print "

#{describe}

\n
    \n" 39 | end 40 | 41 | def leave 42 | print "
\n
\n" 43 | end 44 | 45 | def exception(exception) 46 | super(exception) 47 | outcome = exception.failure? ? "FAILED" : "ERROR" 48 | print %[
  • - #{exception.it} (] 49 | print %[#{outcome} - #{@count})
  • \n] 50 | end 51 | 52 | def after(state = nil) 53 | super(state) 54 | print %[
  • - #{state.it}
  • \n] unless exception? 55 | end 56 | 57 | def finish 58 | success = @exceptions.empty? 59 | unless success 60 | print "
    \n" 61 | print %[
      ] 62 | count = 0 63 | @exceptions.each do |exc| 64 | outcome = exc.failure? ? "FAILED" : "ERROR" 65 | print %[\n
    1. #{escape(exc.description)} #{outcome}

      \n

      ] 66 | print escape(exc.message) 67 | print "

      \n
      \n"
      68 |         print escape(exc.backtrace)
      69 |         print "
      \n
    2. \n" 70 | end 71 | print "
    \n" 72 | end 73 | print %[

    #{@timer.format}

    \n] 74 | print %[

    #{@tally.format}

    \n] 75 | print "\n\n" 76 | end 77 | 78 | def escape(string) 79 | string.gsub("&", " ").gsub("<", "<").gsub(">", ">") 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/mspec/matchers/base.rb: -------------------------------------------------------------------------------- 1 | module MSpecMatchers 2 | end 3 | 4 | class MSpecEnv 5 | include MSpecMatchers 6 | end 7 | 8 | # Expectations are sometimes used in a module body 9 | class Module 10 | include MSpecMatchers 11 | end 12 | 13 | class SpecPositiveOperatorMatcher < BasicObject 14 | def initialize(actual) 15 | @actual = actual 16 | end 17 | 18 | def ==(expected) 19 | result = @actual == expected 20 | unless result 21 | ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be truthy") 22 | end 23 | end 24 | 25 | def !=(expected) 26 | result = @actual != expected 27 | unless result 28 | ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be truthy") 29 | end 30 | end 31 | 32 | def equal?(expected) 33 | result = @actual.equal?(expected) 34 | unless result 35 | ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be truthy") 36 | end 37 | end 38 | 39 | def method_missing(name, *args, &block) 40 | result = @actual.__send__(name, *args, &block) 41 | unless result 42 | ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be truthy") 43 | end 44 | end 45 | end 46 | 47 | class SpecNegativeOperatorMatcher < BasicObject 48 | def initialize(actual) 49 | @actual = actual 50 | end 51 | 52 | def ==(expected) 53 | result = @actual == expected 54 | if result 55 | ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be falsy") 56 | end 57 | end 58 | 59 | def !=(expected) 60 | result = @actual != expected 61 | if result 62 | ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be falsy") 63 | end 64 | end 65 | 66 | def equal?(expected) 67 | result = @actual.equal?(expected) 68 | if result 69 | ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be falsy") 70 | end 71 | end 72 | 73 | def method_missing(name, *args, &block) 74 | result = @actual.__send__(name, *args, &block) 75 | if result 76 | ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be falsy") 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /tool/tag_from_output.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Adds tags based on error and failures output (e.g., from a CI log), 4 | # without running any spec code. 5 | 6 | tag = ENV["TAG"] || "fails" 7 | 8 | tags_dir = %w[ 9 | spec/tags 10 | spec/tags/ruby 11 | ].find { |dir| Dir.exist?("#{dir}/language") } 12 | abort 'Could not find tags directory' unless tags_dir 13 | 14 | output = ARGF.readlines 15 | 16 | # Automatically strip datetime of GitHub Actions 17 | if output.first =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z / 18 | output = output.map { |line| line.split(' ', 2).last } 19 | end 20 | 21 | NUMBER = /^\d+\)$/ 22 | ERROR_OR_FAILED = / (ERROR|FAILED)$/ 23 | SPEC_FILE = /^(\/.+_spec\.rb)\:\d+/ 24 | 25 | output.slice_before(NUMBER).select { |number, *rest| 26 | number =~ NUMBER and rest.any? { |line| line =~ ERROR_OR_FAILED } 27 | }.each { |number, *rest| 28 | error_line = rest.find { |line| line =~ ERROR_OR_FAILED } 29 | description = error_line.match(ERROR_OR_FAILED).pre_match 30 | 31 | spec_file = rest.find { |line| line =~ SPEC_FILE } 32 | if spec_file 33 | spec_file = spec_file[SPEC_FILE, 1] or raise 34 | else 35 | if error_line =~ /^([\w:]+)[#\.](\w+) / 36 | mod, method = $1, $2 37 | file = "#{mod.downcase.gsub('::', '/')}/#{method}_spec.rb" 38 | spec_file = ['spec/ruby/core', 'spec/ruby/library', *Dir.glob('spec/ruby/library/*')].find { |dir| 39 | path = "#{dir}/#{file}" 40 | break path if File.exist?(path) 41 | } 42 | end 43 | 44 | unless spec_file 45 | warn "Could not find file for:\n#{error_line}" 46 | next 47 | end 48 | end 49 | 50 | prefix = spec_file.index('spec/ruby/') || spec_file.index('spec/truffle/') 51 | spec_file = spec_file[prefix..-1] 52 | 53 | tags_file = spec_file.sub('spec/ruby/', "#{tags_dir}/").sub('spec/truffle/', "#{tags_dir}/truffle/") 54 | tags_file = tags_file.sub(/_spec\.rb$/, '_tags.txt') 55 | 56 | dir = File.dirname(tags_file) 57 | Dir.mkdir(dir) unless Dir.exist?(dir) 58 | 59 | tag_line = "#{tag}:#{description}" 60 | lines = File.exist?(tags_file) ? File.readlines(tags_file, chomp: true) : [] 61 | unless lines.include?(tag_line) 62 | puts tags_file 63 | File.write(tags_file, (lines + [tag_line]).join("\n") + "\n") 64 | end 65 | } 66 | -------------------------------------------------------------------------------- /lib/mspec/runner/actions/constants_leak_checker.rb: -------------------------------------------------------------------------------- 1 | class ConstantsLockFile 2 | LOCK_FILE_NAME = '.mspec.constants' 3 | 4 | def self.lock_file 5 | @prefix ||= File.expand_path(MSpecScript.get(:prefix) || '.') 6 | "#{@prefix}/#{LOCK_FILE_NAME}" 7 | end 8 | 9 | def self.load 10 | if File.exist?(lock_file) 11 | File.readlines(lock_file).map(&:chomp) 12 | else 13 | [] 14 | end 15 | end 16 | 17 | def self.dump(ary) 18 | contents = ary.map(&:to_s).uniq.sort.join("\n") + "\n" 19 | File.write(lock_file, contents) 20 | end 21 | end 22 | 23 | class ConstantLeakError < StandardError 24 | end 25 | 26 | class ConstantsLeakCheckerAction 27 | def initialize(save) 28 | @save = save 29 | @check = !save 30 | @constants_locked = ConstantsLockFile.load 31 | @exclude_patterns = MSpecScript.get(:toplevel_constants_excludes) || [] 32 | end 33 | 34 | def register 35 | MSpec.register :start, self 36 | MSpec.register :before, self 37 | MSpec.register :after, self 38 | MSpec.register :finish, self 39 | end 40 | 41 | def start 42 | @constants_start = constants_now 43 | end 44 | 45 | def before(state) 46 | @constants_before = constants_now 47 | end 48 | 49 | def after(state) 50 | constants = remove_excludes(constants_now - @constants_before - @constants_locked) 51 | 52 | if @check && !constants.empty? 53 | MSpec.protect 'Constants leak check' do 54 | raise ConstantLeakError, "Top level constants leaked: #{constants.join(', ')}" 55 | end 56 | end 57 | end 58 | 59 | def finish 60 | constants = remove_excludes(constants_now - @constants_start - @constants_locked) 61 | 62 | if @save 63 | ConstantsLockFile.dump(@constants_locked + constants) 64 | end 65 | 66 | if @check && !constants.empty? 67 | MSpec.protect 'Global constants leak check' do 68 | raise ConstantLeakError, "Top level constants leaked in the whole test suite: #{constants.join(', ')}" 69 | end 70 | end 71 | end 72 | 73 | private 74 | 75 | def constants_now 76 | Object.constants.map(&:to_s) 77 | end 78 | 79 | def remove_excludes(constants) 80 | constants.reject { |name| 81 | @exclude_patterns.any? { |pattern| pattern === name } 82 | } 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/mspec/guards/version.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/utils/deprecate' 2 | require 'mspec/utils/version' 3 | require 'mspec/guards/guard' 4 | 5 | class VersionGuard < SpecGuard 6 | FULL_RUBY_VERSION = SpecVersion.new SpecGuard.ruby_version(:full) 7 | 8 | def initialize(version, requirement) 9 | version = SpecVersion.new(version) unless SpecVersion === version 10 | @version = version 11 | 12 | case requirement 13 | when String 14 | @requirement = SpecVersion.new requirement 15 | when Range 16 | MSpec.deprecate "an empty version range end", 'a specific version' if requirement.end.empty? 17 | a = SpecVersion.new requirement.begin 18 | b = SpecVersion.new requirement.end 19 | unless requirement.exclude_end? 20 | MSpec.deprecate "ruby_version_is with an inclusive range", 'an exclusive range ("2.1"..."2.3")' 21 | end 22 | @requirement = requirement.exclude_end? ? a...b : a..b 23 | else 24 | raise "version must be a String or Range but was a #{requirement.class}" 25 | end 26 | super(@version, @requirement) 27 | end 28 | 29 | def match? 30 | if Range === @requirement 31 | @requirement.include? @version 32 | else 33 | @version >= @requirement 34 | end 35 | end 36 | 37 | @kernel_version = nil 38 | def self.kernel_version 39 | if @kernel_version 40 | @kernel_version 41 | else 42 | if v = RUBY_PLATFORM[/darwin(\d+)/, 1] # build time version 43 | uname = v 44 | else 45 | begin 46 | require 'etc' 47 | etc = true 48 | rescue LoadError 49 | etc = false 50 | end 51 | if etc and Etc.respond_to?(:uname) 52 | uname = Etc.uname.fetch(:release) 53 | else 54 | uname = `uname -r`.chomp 55 | end 56 | end 57 | @kernel_version = uname 58 | end 59 | end 60 | end 61 | 62 | def version_is(base_version, requirement, &block) 63 | VersionGuard.new(base_version, requirement).run_if(:version_is, &block) 64 | end 65 | 66 | def ruby_version_is(requirement, &block) 67 | VersionGuard.new(VersionGuard::FULL_RUBY_VERSION, requirement).run_if(:ruby_version_is, &block) 68 | end 69 | 70 | def kernel_version_is(requirement, &block) 71 | VersionGuard.new(VersionGuard.kernel_version, requirement).run_if(:kernel_version_is, &block) 72 | end 73 | -------------------------------------------------------------------------------- /lib/mspec/matchers/equal_element.rb: -------------------------------------------------------------------------------- 1 | class EqualElementMatcher 2 | def initialize(element, attributes = nil, content = nil, options = {}) 3 | @element = element 4 | @attributes = attributes 5 | @content = content 6 | @options = options 7 | end 8 | 9 | def matches?(actual) 10 | @actual = actual 11 | 12 | matched = true 13 | 14 | if @options[:not_closed] 15 | matched &&= actual =~ /^#{Regexp.quote("<" + @element)}.*#{Regexp.quote(">" + (@content || ''))}$/ 16 | else 17 | matched &&= actual =~ /^#{Regexp.quote("<" + @element)}/ 18 | matched &&= actual =~ /#{Regexp.quote("")}$/ 19 | matched &&= actual =~ /#{Regexp.quote(">" + @content + ")/).size == 1) 29 | else 30 | matched &&= (actual.scan(%Q{ #{key}="#{value}"}).size == 1) 31 | end 32 | end 33 | end 34 | end 35 | 36 | !!matched 37 | end 38 | 39 | def failure_message 40 | ["Expected #{MSpec.format(@actual)}", 41 | "to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"] 42 | end 43 | 44 | def negative_failure_message 45 | ["Expected #{MSpec.format(@actual)}", 46 | "not to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"] 47 | end 48 | 49 | def attributes_for_failure_message 50 | if @attributes 51 | if @attributes.empty? 52 | "no attributes" 53 | else 54 | @attributes.inject([]) { |memo, n| memo << %Q{#{n[0]}="#{n[1]}"} }.join(" ") 55 | end 56 | else 57 | "any attributes" 58 | end 59 | end 60 | 61 | def content_for_failure_message 62 | if @content 63 | if @content.empty? 64 | "no content" 65 | else 66 | "#{@content.inspect} as content" 67 | end 68 | else 69 | "any content" 70 | end 71 | end 72 | end 73 | 74 | module MSpecMatchers 75 | private def equal_element(*args) 76 | EqualElementMatcher.new(*args) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/junit.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/runner/formatters/yaml' 2 | 3 | class JUnitFormatter < YamlFormatter 4 | def initialize(out = nil) 5 | super(out) 6 | @tests = [] 7 | end 8 | 9 | def after(state = nil) 10 | super(state) 11 | @tests << {:test => state, :exception => false} unless exception? 12 | end 13 | 14 | def exception(exception) 15 | super(exception) 16 | @tests << {:test => exception, :exception => true} 17 | end 18 | 19 | def finish 20 | switch 21 | 22 | time = @timer.elapsed 23 | tests = @tally.counter.examples 24 | errors = @tally.counter.errors 25 | failures = @tally.counter.failures 26 | 27 | print <<-XML 28 | 29 | 30 | 35 | 41 | XML 42 | @tests.each do |h| 43 | description = encode_for_xml h[:test].description 44 | 45 | print <<-XML 46 | 47 | XML 48 | if h[:exception] 49 | outcome = h[:test].failure? ? "failure" : "error" 50 | message = encode_for_xml h[:test].message 51 | backtrace = encode_for_xml h[:test].backtrace 52 | print <<-XML 53 | <#{outcome} message="error in #{description}" type="#{outcome}"> 54 | #{message} 55 | #{backtrace} 56 | 57 | XML 58 | end 59 | print <<-XML 60 | 61 | XML 62 | end 63 | 64 | print <<-XML 65 | 66 | 67 | XML 68 | end 69 | 70 | private 71 | LT = "<" 72 | GT = ">" 73 | QU = """ 74 | AP = "'" 75 | AM = "&" 76 | TARGET_ENCODING = "ISO-8859-1" 77 | 78 | def encode_for_xml(str) 79 | encode_as_latin1(str).gsub("<", LT).gsub(">", GT). 80 | gsub('"', QU).gsub("'", AP).gsub("&", AM). 81 | tr("\x00-\x08", "?") 82 | end 83 | 84 | def encode_as_latin1(str) 85 | str.encode(TARGET_ENCODING, :undef => :replace, :invalid => :replace) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/mspec/commands/mspec-ci.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/version' 2 | require 'mspec/utils/options' 3 | require 'mspec/utils/script' 4 | 5 | 6 | class MSpecCI < MSpecScript 7 | def options(argv = ARGV) 8 | options = MSpecOptions.new "mspec ci [options] (FILE|DIRECTORY|GLOB)+", 30, config 9 | 10 | options.doc " Ask yourself:" 11 | options.doc " 1. How to run the specs?" 12 | options.doc " 2. How to modify the guard behavior?" 13 | options.doc " 2. How to display the output?" 14 | options.doc " 3. What action to perform?" 15 | options.doc " 4. When to perform it?" 16 | 17 | options.doc "\n How to run the specs" 18 | options.chdir 19 | options.prefix 20 | options.configure { |f| load f } 21 | options.repeat 22 | options.pretend 23 | options.interrupt 24 | options.timeout 25 | 26 | options.doc "\n How to modify the guard behavior" 27 | options.unguarded 28 | options.verify 29 | 30 | options.doc "\n How to display their output" 31 | options.formatters 32 | options.verbose 33 | 34 | options.doc "\n What action to perform" 35 | options.actions 36 | 37 | options.doc "\n When to perform it" 38 | options.action_filters 39 | 40 | options.doc "\n Help!" 41 | options.debug 42 | options.version MSpec::VERSION 43 | options.help 44 | 45 | options.doc "\n Custom options" 46 | custom_options options 47 | 48 | options.doc "\n How might this work in the real world?" 49 | options.doc "\n 1. To simply run the known good specs" 50 | options.doc "\n $ mspec ci" 51 | options.doc "\n 2. To run a subset of the known good specs" 52 | options.doc "\n $ mspec ci path/to/specs" 53 | options.doc "\n 3. To start the debugger before the spec matching 'this crashes'" 54 | options.doc "\n $ mspec ci --spec-debug -S 'this crashes'" 55 | options.doc "" 56 | 57 | patterns = options.parse argv 58 | patterns = config[:ci_files] if patterns.empty? 59 | @files = files patterns 60 | end 61 | 62 | def run 63 | MSpec.register_tags_patterns config[:tags_patterns] 64 | MSpec.register_files @files 65 | 66 | tags = ["fails", "critical", "unstable", "incomplete", "unsupported"] 67 | tags += Array(config[:ci_xtags]) 68 | 69 | require 'mspec/runner/filters/tag' 70 | filter = TagFilter.new(:exclude, *tags) 71 | filter.register 72 | 73 | MSpec.process 74 | exit MSpec.exit_code 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/mspec/helpers/numeric.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'mspec/guards/platform' 3 | 4 | def nan_value 5 | 0/0.0 6 | end 7 | 8 | def infinity_value 9 | 1/0.0 10 | end 11 | 12 | def bignum_value(plus = 0) 13 | # Must be >= fixnum_max + 2, so -bignum_value is < fixnum_min 14 | # A fixed value has the advantage to be the same numeric value for all Rubies and is much easier to spec 15 | (2**64) + plus 16 | end 17 | 18 | def max_long 19 | 2**(PlatformGuard::C_LONG_SIZE - 1) - 1 20 | end 21 | 22 | def min_long 23 | -(2**(PlatformGuard::C_LONG_SIZE - 1)) 24 | end 25 | 26 | # This is a bit hairy, but we need to be able to write specs that cover the 27 | # boundary between Fixnum and Bignum for operations like Fixnum#<<. Since 28 | # this boundary is implementation-dependent, we use these helpers to write 29 | # specs based on the relationship between values rather than specific 30 | # values. 31 | if PlatformGuard.standard? or PlatformGuard.implementation? :topaz 32 | limits_available = begin 33 | require 'rbconfig/sizeof' 34 | defined?(RbConfig::LIMITS.[]) && ['FIXNUM_MAX', 'FIXNUM_MIN'].all? do |key| 35 | Integer === RbConfig::LIMITS[key] 36 | end 37 | rescue LoadError 38 | false 39 | end 40 | 41 | if limits_available 42 | def fixnum_max 43 | RbConfig::LIMITS['FIXNUM_MAX'] 44 | end 45 | 46 | def fixnum_min 47 | RbConfig::LIMITS['FIXNUM_MIN'] 48 | end 49 | elsif PlatformGuard.c_long_size? 32 50 | def fixnum_max 51 | (2**30) - 1 52 | end 53 | 54 | def fixnum_min 55 | -(2**30) 56 | end 57 | elsif PlatformGuard.c_long_size? 64 58 | def fixnum_max 59 | (2**62) - 1 60 | end 61 | 62 | def fixnum_min 63 | -(2**62) 64 | end 65 | end 66 | elsif PlatformGuard.implementation? :opal 67 | def fixnum_max 68 | Integer::MAX 69 | end 70 | 71 | def fixnum_min 72 | Integer::MIN 73 | end 74 | elsif PlatformGuard.implementation? :rubinius 75 | def fixnum_max 76 | Fixnum::MAX 77 | end 78 | 79 | def fixnum_min 80 | Fixnum::MIN 81 | end 82 | elsif PlatformGuard.implementation?(:jruby) || PlatformGuard.implementation?(:truffleruby) 83 | def fixnum_max 84 | 9223372036854775807 85 | end 86 | 87 | def fixnum_min 88 | -9223372036854775808 89 | end 90 | else 91 | def fixnum_max 92 | raise "unknown implementation for fixnum_max() helper" 93 | end 94 | 95 | def fixnum_min 96 | raise "unknown implementation for fixnum_min() helper" 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/mspec/runner/formatters/launchable.rb: -------------------------------------------------------------------------------- 1 | module LaunchableFormatter 2 | def self.extend_object(obj) 3 | super 4 | obj.init 5 | end 6 | 7 | def self.setDir(dir) 8 | @@path = File.join(dir, "#{rand.to_s}.json") 9 | self 10 | end 11 | 12 | def init 13 | @timer = nil 14 | @tests = [] 15 | end 16 | 17 | def before(state = nil) 18 | super 19 | @timer = TimerAction.new 20 | @timer.start 21 | end 22 | 23 | def after(state = nil) 24 | super 25 | @timer.finish 26 | file = MSpec.file 27 | return if file.nil? || state&.example.nil? || exception? 28 | 29 | @tests << {:test => state, :file => file, :exception => false, duration: @timer.elapsed} 30 | end 31 | 32 | def exception(exception) 33 | super 34 | @timer.finish 35 | file = MSpec.file 36 | return if file.nil? 37 | 38 | @tests << {:test => exception, :file => file, :exception => true, duration: @timer.elapsed} 39 | end 40 | 41 | def finish 42 | super 43 | 44 | require_relative '../../../../../../tool/lib/launchable' 45 | 46 | @writer = writer = Launchable::JsonStreamWriter.new(@@path) 47 | @writer.write_array('testCases') 48 | at_exit { 49 | @writer.close 50 | } 51 | 52 | repo_path = File.expand_path("#{__dir__}/../../../../../../") 53 | 54 | @tests.each do |t| 55 | testcase = t[:test].description 56 | relative_path = t[:file].delete_prefix("#{repo_path}/") 57 | # The test path is a URL-encoded representation. 58 | # https://github.com/launchableinc/cli/blob/v1.81.0/launchable/testpath.py#L18 59 | test_path = {file: relative_path, testcase: testcase}.map{|key, val| 60 | "#{encode_test_path_component(key)}=#{encode_test_path_component(val)}" 61 | }.join('#') 62 | 63 | status = 'TEST_PASSED' 64 | if t[:exception] 65 | message = t[:test].message 66 | backtrace = t[:test].backtrace 67 | e = "#{message}\n#{backtrace}" 68 | status = 'TEST_FAILED' 69 | end 70 | 71 | @writer.write_object( 72 | { 73 | testPath: test_path, 74 | status: status, 75 | duration: t[:duration], 76 | createdAt: Time.now.to_s, 77 | stderr: e, 78 | stdout: nil 79 | } 80 | ) 81 | end 82 | end 83 | 84 | private 85 | def encode_test_path_component component 86 | component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26').tr("\x00-\x08", "") 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/runner/formatters/unit_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | require 'mspec/runner/formatters/unit' 3 | require 'mspec/runner/example' 4 | require 'mspec/utils/script' 5 | 6 | RSpec.describe UnitdiffFormatter, "#finish" do 7 | before :each do 8 | @tally = double("tally").as_null_object 9 | allow(TallyAction).to receive(:new).and_return(@tally) 10 | @timer = double("timer").as_null_object 11 | allow(TimerAction).to receive(:new).and_return(@timer) 12 | 13 | $stdout = @out = IOStub.new 14 | context = ContextState.new "describe" 15 | @state = ExampleState.new(context, "it") 16 | allow(MSpec).to receive(:register) 17 | @formatter = UnitdiffFormatter.new 18 | @formatter.register 19 | end 20 | 21 | after :each do 22 | $stdout = STDOUT 23 | end 24 | 25 | it "prints a failure message for an exception" do 26 | exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken") 27 | @formatter.exception exc 28 | @formatter.after @state 29 | @formatter.finish 30 | expect(@out).to match(/^1\)\ndescribe it ERROR$/) 31 | end 32 | 33 | it "prints a backtrace for an exception" do 34 | exc = ExceptionState.new @state, nil, Exception.new("broken") 35 | allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") 36 | @formatter.exception exc 37 | @formatter.finish 38 | expect(@out).to match(%r[path/to/some/file.rb:35:in method$]) 39 | end 40 | 41 | it "prints a summary of elapsed time" do 42 | expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds") 43 | @formatter.finish 44 | expect(@out).to match(/^Finished in 2.0 seconds$/) 45 | end 46 | 47 | it "prints a tally of counts" do 48 | expect(@tally).to receive(:format).and_return("1 example, 0 failures") 49 | @formatter.finish 50 | expect(@out).to match(/^1 example, 0 failures$/) 51 | end 52 | 53 | it "prints errors, backtraces, elapsed time, and tallies" do 54 | exc = ExceptionState.new @state, nil, Exception.new("broken") 55 | allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") 56 | @formatter.exception exc 57 | @formatter.after @state 58 | expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds") 59 | expect(@tally).to receive(:format).and_return("1 example, 0 failures") 60 | @formatter.finish 61 | expect(@out).to eq(%[E 62 | 63 | Finished in 2.0 seconds 64 | 65 | 1) 66 | describe it ERROR 67 | Exception: broken: 68 | path/to/some/file.rb:35:in method 69 | 70 | 1 example, 0 failures 71 | ]) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/mspec/runner/parallel.rb: -------------------------------------------------------------------------------- 1 | class ParallelRunner 2 | def initialize(files, processes, formatter, argv) 3 | @files = files 4 | @processes = processes 5 | @formatter = formatter 6 | @argv = argv 7 | @last_files = {} 8 | @output_files = [] 9 | @success = true 10 | end 11 | 12 | def launch_children 13 | @children = @processes.times.map { |i| 14 | name = tmp "mspec-multi-#{i}" 15 | @output_files << name 16 | 17 | env = { 18 | "SPEC_TEMP_DIR" => "#{SPEC_TEMP_DIR}_#{i}", 19 | "MSPEC_MULTI" => i.to_s 20 | } 21 | command = @argv + ["-fy", "-o", name] 22 | $stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG 23 | IO.popen([env, *command, close_others: false], "rb+") 24 | } 25 | end 26 | 27 | def handle(child, message) 28 | case message 29 | when '.' 30 | @formatter.unload 31 | send_new_file_or_quit(child) 32 | else 33 | if message == nil 34 | msg = "A child mspec-run process died unexpectedly" 35 | else 36 | msg = "A child mspec-run process printed unexpected output on STDOUT" 37 | while chunk = (child.read_nonblock(4096) rescue nil) 38 | message += chunk 39 | end 40 | message.chomp!('.') 41 | msg += ": #{message.inspect}" 42 | end 43 | 44 | if last_file = @last_files[child] 45 | msg += " while running #{last_file}" 46 | end 47 | 48 | @success = false 49 | quit(child) 50 | abort "\n#{msg}" 51 | end 52 | end 53 | 54 | def quit(child) 55 | begin 56 | child.puts "QUIT" 57 | rescue Errno::EPIPE 58 | # The child process already died 59 | end 60 | _pid, status = Process.wait2(child.pid) 61 | @success &&= status.success? 62 | child.close 63 | @children.delete(child) 64 | end 65 | 66 | def send_new_file_or_quit(child) 67 | if @files.empty? 68 | quit(child) 69 | else 70 | file = @files.shift 71 | @last_files[child] = file 72 | child.puts file 73 | end 74 | end 75 | 76 | def run 77 | MSpec.register_files @files 78 | launch_children 79 | 80 | puts @children.map { |child| child.gets }.uniq 81 | @formatter.start 82 | begin 83 | @children.each { |child| send_new_file_or_quit(child) } 84 | 85 | until @children.empty? 86 | IO.select(@children)[0].each { |child| 87 | handle(child, child.read(1)) 88 | } 89 | end 90 | ensure 91 | @children.dup.each { |child| quit(child) } 92 | @formatter.aggregate_results(@output_files) 93 | @formatter.finish 94 | end 95 | 96 | @success 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/mspec/commands/mspec-run.rb: -------------------------------------------------------------------------------- 1 | require 'mspec/version' 2 | require 'mspec/utils/options' 3 | require 'mspec/utils/script' 4 | 5 | 6 | class MSpecRun < MSpecScript 7 | def initialize 8 | super 9 | 10 | config[:files] = [] 11 | end 12 | 13 | def options(argv = ARGV) 14 | options = MSpecOptions.new "mspec run [options] (FILE|DIRECTORY|GLOB)+", 30, config 15 | 16 | options.doc " Ask yourself:" 17 | options.doc " 1. What specs to run?" 18 | options.doc " 2. How to modify the execution?" 19 | options.doc " 3. How to modify the guard behavior?" 20 | options.doc " 4. How to display the output?" 21 | options.doc " 5. What action to perform?" 22 | options.doc " 6. When to perform it?" 23 | 24 | options.doc "\n What specs to run" 25 | options.filters 26 | 27 | options.doc "\n How to modify the execution" 28 | options.chdir 29 | options.prefix 30 | options.configure { |f| load f } 31 | options.env 32 | options.randomize 33 | options.repeat 34 | options.pretend 35 | options.interrupt 36 | options.timeout 37 | 38 | options.doc "\n How to modify the guard behavior" 39 | options.unguarded 40 | options.verify 41 | 42 | options.doc "\n How to display their output" 43 | options.formatters 44 | options.verbose 45 | 46 | options.doc "\n What action to perform" 47 | options.actions 48 | 49 | options.doc "\n When to perform it" 50 | options.action_filters 51 | 52 | options.doc "\n Launchable" 53 | options.launchable 54 | 55 | options.doc "\n Help!" 56 | options.debug 57 | options.version MSpec::VERSION 58 | options.help 59 | 60 | options.doc "\n Custom options" 61 | custom_options options 62 | 63 | options.doc "\n How might this work in the real world?" 64 | options.doc "\n 1. To simply run some specs" 65 | options.doc "\n $ mspec path/to/the/specs" 66 | options.doc " mspec path/to/the_file_spec.rb" 67 | options.doc "\n 2. To run specs tagged with 'fails'" 68 | options.doc "\n $ mspec -g fails path/to/the_file_spec.rb" 69 | options.doc "\n 3. To start the debugger before the spec matching 'this crashes'" 70 | options.doc "\n $ mspec --spec-debug -S 'this crashes' path/to/the_file_spec.rb" 71 | options.doc "\n 4. To run some specs matching 'this crashes'" 72 | options.doc "\n $ mspec -e 'this crashes' path/to/the_file_spec.rb" 73 | 74 | options.doc "" 75 | 76 | patterns = options.parse argv 77 | @files = files_from_patterns(patterns) 78 | end 79 | 80 | def run 81 | MSpec.register_tags_patterns config[:tags_patterns] 82 | MSpec.register_files @files 83 | 84 | MSpec.process 85 | exit MSpec.exit_code 86 | end 87 | end 88 | --------------------------------------------------------------------------------