aboutsummaryrefslogtreecommitdiffstats
path: root/spec/mspec/lib/mspec
diff options
context:
space:
mode:
Diffstat (limited to 'spec/mspec/lib/mspec')
-rwxr-xr-xspec/mspec/lib/mspec/commands/mkspec.rb155
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-ci.rb79
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-run.rb87
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-tag.rb133
-rwxr-xr-xspec/mspec/lib/mspec/commands/mspec.rb163
-rw-r--r--spec/mspec/lib/mspec/expectations.rb2
-rw-r--r--spec/mspec/lib/mspec/expectations/expectations.rb21
-rw-r--r--spec/mspec/lib/mspec/expectations/should.rb29
-rw-r--r--spec/mspec/lib/mspec/guards.rb12
-rw-r--r--spec/mspec/lib/mspec/guards/block_device.rb18
-rw-r--r--spec/mspec/lib/mspec/guards/bug.rb30
-rw-r--r--spec/mspec/lib/mspec/guards/conflict.rb19
-rw-r--r--spec/mspec/lib/mspec/guards/endian.rb27
-rw-r--r--spec/mspec/lib/mspec/guards/feature.rb43
-rw-r--r--spec/mspec/lib/mspec/guards/guard.rb118
-rw-r--r--spec/mspec/lib/mspec/guards/platform.rb78
-rw-r--r--spec/mspec/lib/mspec/guards/quarantine.rb13
-rw-r--r--spec/mspec/lib/mspec/guards/superuser.rb17
-rw-r--r--spec/mspec/lib/mspec/guards/support.rb16
-rw-r--r--spec/mspec/lib/mspec/guards/version.rb39
-rw-r--r--spec/mspec/lib/mspec/helpers.rb12
-rw-r--r--spec/mspec/lib/mspec/helpers/argf.rb37
-rw-r--r--spec/mspec/lib/mspec/helpers/argv.rb46
-rw-r--r--spec/mspec/lib/mspec/helpers/datetime.rb51
-rw-r--r--spec/mspec/lib/mspec/helpers/fixture.rb26
-rw-r--r--spec/mspec/lib/mspec/helpers/flunk.rb5
-rw-r--r--spec/mspec/lib/mspec/helpers/fs.rb62
-rw-r--r--spec/mspec/lib/mspec/helpers/io.rb113
-rw-r--r--spec/mspec/lib/mspec/helpers/mock_to_path.rb8
-rw-r--r--spec/mspec/lib/mspec/helpers/numeric.rb72
-rw-r--r--spec/mspec/lib/mspec/helpers/ruby_exe.rb178
-rw-r--r--spec/mspec/lib/mspec/helpers/scratch.rb17
-rw-r--r--spec/mspec/lib/mspec/helpers/tmp.rb45
-rw-r--r--spec/mspec/lib/mspec/matchers.rb35
-rw-r--r--spec/mspec/lib/mspec/matchers/base.rb95
-rw-r--r--spec/mspec/lib/mspec/matchers/be_an_instance_of.rb26
-rw-r--r--spec/mspec/lib/mspec/matchers/be_ancestor_of.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/be_close.rb27
-rw-r--r--spec/mspec/lib/mspec/matchers/be_computed_by.rb37
-rw-r--r--spec/mspec/lib/mspec/matchers/be_empty.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_false.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_kind_of.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/be_nan.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_nil.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_true.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_true_or_false.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/block_caller.rb35
-rw-r--r--spec/mspec/lib/mspec/matchers/complain.rb56
-rw-r--r--spec/mspec/lib/mspec/matchers/eql.rb26
-rw-r--r--spec/mspec/lib/mspec/matchers/equal.rb26
-rw-r--r--spec/mspec/lib/mspec/matchers/equal_element.rb78
-rw-r--r--spec/mspec/lib/mspec/matchers/have_class_variable.rb12
-rw-r--r--spec/mspec/lib/mspec/matchers/have_constant.rb12
-rw-r--r--spec/mspec/lib/mspec/matchers/have_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_instance_variable.rb12
-rw-r--r--spec/mspec/lib/mspec/matchers/have_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_private_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_private_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_public_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_singleton_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/include.rb32
-rw-r--r--spec/mspec/lib/mspec/matchers/infinity.rb28
-rw-r--r--spec/mspec/lib/mspec/matchers/match_yaml.rb47
-rw-r--r--spec/mspec/lib/mspec/matchers/method.rb10
-rw-r--r--spec/mspec/lib/mspec/matchers/output.rb67
-rw-r--r--spec/mspec/lib/mspec/matchers/output_to_fd.rb71
-rw-r--r--spec/mspec/lib/mspec/matchers/raise_error.rb79
-rw-r--r--spec/mspec/lib/mspec/matchers/respond_to.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/signed_zero.rb28
-rw-r--r--spec/mspec/lib/mspec/matchers/variable.rb24
-rw-r--r--spec/mspec/lib/mspec/mocks.rb3
-rw-r--r--spec/mspec/lib/mspec/mocks/mock.rb197
-rw-r--r--spec/mspec/lib/mspec/mocks/object.rb28
-rw-r--r--spec/mspec/lib/mspec/mocks/proxy.rb186
-rw-r--r--spec/mspec/lib/mspec/runner.rb12
-rw-r--r--spec/mspec/lib/mspec/runner/actions.rb6
-rw-r--r--spec/mspec/lib/mspec/runner/actions/filter.rb40
-rw-r--r--spec/mspec/lib/mspec/runner/actions/leakchecker.rb301
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tag.rb133
-rw-r--r--spec/mspec/lib/mspec/runner/actions/taglist.rb56
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tagpurge.rb56
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tally.rb133
-rw-r--r--spec/mspec/lib/mspec/runner/actions/timer.rb22
-rw-r--r--spec/mspec/lib/mspec/runner/context.rb239
-rw-r--r--spec/mspec/lib/mspec/runner/evaluate.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/example.rb34
-rw-r--r--spec/mspec/lib/mspec/runner/exception.rb43
-rw-r--r--spec/mspec/lib/mspec/runner/filters.rb4
-rw-r--r--spec/mspec/lib/mspec/runner/filters/match.rb18
-rw-r--r--spec/mspec/lib/mspec/runner/filters/profile.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/filters/regexp.rb7
-rw-r--r--spec/mspec/lib/mspec/runner/filters/tag.rb29
-rw-r--r--spec/mspec/lib/mspec/runner/formatters.rb12
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/describe.rb24
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/dotted.rb117
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/file.rb19
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/html.rb81
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/junit.rb89
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/method.rb93
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/multi.rb36
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/profile.rb70
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/specdoc.rb41
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/spinner.rb117
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/summary.rb11
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/unit.rb21
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/yaml.rb42
-rw-r--r--spec/mspec/lib/mspec/runner/mspec.rb391
-rw-r--r--spec/mspec/lib/mspec/runner/object.rb28
-rw-r--r--spec/mspec/lib/mspec/runner/shared.rb12
-rw-r--r--spec/mspec/lib/mspec/runner/tag.rb38
-rw-r--r--spec/mspec/lib/mspec/utils/deprecate.rb6
-rw-r--r--spec/mspec/lib/mspec/utils/name_map.rb128
-rw-r--r--spec/mspec/lib/mspec/utils/options.rb489
-rw-r--r--spec/mspec/lib/mspec/utils/ruby_name.rb8
-rw-r--r--spec/mspec/lib/mspec/utils/script.rb267
-rw-r--r--spec/mspec/lib/mspec/utils/version.rb52
-rw-r--r--spec/mspec/lib/mspec/utils/warnings.rb32
-rw-r--r--spec/mspec/lib/mspec/version.rb5
119 files changed, 6878 insertions, 0 deletions
diff --git a/spec/mspec/lib/mspec/commands/mkspec.rb b/spec/mspec/lib/mspec/commands/mkspec.rb
new file mode 100755
index 0000000000..7a943aa1fe
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mkspec.rb
@@ -0,0 +1,155 @@
+#!/usr/bin/env ruby
+
+require 'rbconfig'
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/name_map'
+require 'mspec/helpers/fs'
+
+class MkSpec
+ attr_reader :config
+
+ def initialize
+ @config = {
+ :constants => [],
+ :requires => [],
+ :base => "core",
+ :version => nil
+ }
+ @map = NameMap.new true
+ end
+
+ def options(argv=ARGV)
+ options = MSpecOptions.new "mkspec [options]", 32
+
+ options.on("-c", "--constant", "CONSTANT",
+ "Class or Module to generate spec stubs for") do |name|
+ config[:constants] << name
+ end
+ options.on("-b", "--base", "DIR",
+ "Directory to generate specs into") do |directory|
+ config[:base] = File.expand_path directory
+ end
+ options.on("-r", "--require", "LIBRARY",
+ "A library to require") do |file|
+ config[:requires] << file
+ end
+ options.on("-V", "--version-guard", "VERSION",
+ "Specify version for ruby_version_is guards") do |version|
+ config[:version] = version
+ end
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n How might this work in the real world?\n"
+ options.doc " 1. To create spec stubs for every class or module in Object\n"
+ options.doc " $ mkspec\n"
+ options.doc " 2. To create spec stubs for Fixnum\n"
+ options.doc " $ mkspec -c Fixnum\n"
+ options.doc " 3. To create spec stubs for Complex in 'superspec/complex'\n"
+ options.doc " $ mkspec -c Complex -r complex -b superspec"
+ options.doc ""
+
+ options.parse argv
+ end
+
+ def create_directory(mod)
+ subdir = @map.dir_name mod, config[:base]
+
+ if File.exist? subdir
+ unless File.directory? subdir
+ puts "#{subdir} already exists and is not a directory."
+ return nil
+ end
+ else
+ mkdir_p subdir
+ end
+
+ subdir
+ end
+
+ def write_requires(dir, file)
+ prefix = config[:base] + '/'
+ raise dir unless dir.start_with? prefix
+ sub = dir[prefix.size..-1]
+ parents = '../' * (sub.split('/').length + 1)
+
+ File.open(file, 'w') do |f|
+ f.puts "require File.expand_path('../#{parents}spec_helper', __FILE__)"
+ config[:requires].each do |lib|
+ f.puts "require '#{lib}'"
+ end
+ end
+ end
+
+ def write_version(f)
+ f.puts ""
+ if version = config[:version]
+ f.puts "ruby_version_is #{version} do"
+ yield " "
+ f.puts "end"
+ else
+ yield ""
+ end
+ end
+
+ def write_spec(file, meth, exists)
+ if exists
+ out = `#{ruby} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e '#{meth}' #{file}`
+ return if out.include?(meth)
+ end
+
+ File.open file, 'a' do |f|
+ write_version(f) do |indent|
+ f.puts <<-EOS
+#{indent}describe "#{meth}" do
+#{indent} it "needs to be reviewed for spec completeness"
+#{indent}end
+EOS
+ end
+ end
+
+ puts file
+ end
+
+ def create_file(dir, mod, meth, name)
+ file = File.join dir, @map.file_name(meth, mod)
+ exists = File.exist? file
+
+ write_requires dir, file unless exists
+ write_spec file, name, exists
+ end
+
+ def run
+ config[:requires].each { |lib| require lib }
+ constants = config[:constants]
+ constants = Object.constants if constants.empty?
+
+ @map.map({}, constants).each do |mod, methods|
+ name = mod.chop
+ next unless dir = create_directory(name)
+
+ methods.each { |method| create_file dir, name, method, mod + method }
+ end
+ end
+
+ ##
+ # Determine and return the path of the ruby executable.
+
+ def ruby
+ ruby = File.join(RbConfig::CONFIG['bindir'],
+ RbConfig::CONFIG['ruby_install_name'])
+
+ ruby.gsub! File::SEPARATOR, File::ALT_SEPARATOR if File::ALT_SEPARATOR
+
+ return ruby
+ end
+
+ def self.main
+ ENV['MSPEC_RUNNER'] = '1'
+
+ script = new
+ script.options
+ script.run
+ end
+end
diff --git a/spec/mspec/lib/mspec/commands/mspec-ci.rb b/spec/mspec/lib/mspec/commands/mspec-ci.rb
new file mode 100644
index 0000000000..225d2bb96d
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec-ci.rb
@@ -0,0 +1,79 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
+
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+
+
+class MSpecCI < MSpecScript
+ def options(argv=ARGV)
+ options = MSpecOptions.new "mspec ci [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " Ask yourself:"
+ options.doc " 1. How to run the specs?"
+ options.doc " 2. How to modify the guard behavior?"
+ options.doc " 2. How to display the output?"
+ options.doc " 3. What action to perform?"
+ options.doc " 4. When to perform it?"
+
+ options.doc "\n How to run the specs"
+ options.chdir
+ options.prefix
+ options.configure { |f| load f }
+ options.name
+ options.pretend
+ options.interrupt
+
+ options.doc "\n How to modify the guard behavior"
+ options.unguarded
+ options.verify
+
+ options.doc "\n How to display their output"
+ options.formatters
+ options.verbose
+
+ options.doc "\n What action to perform"
+ options.actions
+
+ options.doc "\n When to perform it"
+ options.action_filters
+
+ options.doc "\n Help!"
+ options.debug
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ options.doc "\n How might this work in the real world?"
+ options.doc "\n 1. To simply run the known good specs"
+ options.doc "\n $ mspec ci"
+ options.doc "\n 2. To run a subset of the known good specs"
+ options.doc "\n $ mspec ci path/to/specs"
+ options.doc "\n 3. To start the debugger before the spec matching 'this crashes'"
+ options.doc "\n $ mspec ci --spec-debug -S 'this crashes'"
+ options.doc ""
+
+ patterns = options.parse argv
+ patterns = config[:ci_files] if patterns.empty?
+ @files = files patterns
+ end
+
+ def run
+ MSpec.register_tags_patterns config[:tags_patterns]
+ MSpec.register_files @files
+
+ tags = ["fails", "critical", "unstable", "incomplete", "unsupported"]
+ tags += Array(config[:ci_xtags])
+
+ require 'mspec/runner/filters/tag'
+ filter = TagFilter.new(:exclude, *tags)
+ filter.register
+
+ MSpec.process
+ exit MSpec.exit_code
+ end
+end
diff --git a/spec/mspec/lib/mspec/commands/mspec-run.rb b/spec/mspec/lib/mspec/commands/mspec-run.rb
new file mode 100644
index 0000000000..45b26e88ad
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec-run.rb
@@ -0,0 +1,87 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
+
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+
+
+class MSpecRun < MSpecScript
+ def initialize
+ super
+
+ config[:files] = []
+ end
+
+ def options(argv=ARGV)
+ options = MSpecOptions.new "mspec run [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " Ask yourself:"
+ options.doc " 1. What specs to run?"
+ options.doc " 2. How to modify the execution?"
+ options.doc " 3. How to modify the guard behavior?"
+ options.doc " 4. How to display the output?"
+ options.doc " 5. What action to perform?"
+ options.doc " 6. When to perform it?"
+
+ options.doc "\n What specs to run"
+ options.filters
+
+ options.doc "\n How to modify the execution"
+ options.chdir
+ options.prefix
+ options.configure { |f| load f }
+ options.name
+ options.randomize
+ options.repeat
+ options.pretend
+ options.interrupt
+
+ options.doc "\n How to modify the guard behavior"
+ options.unguarded
+ options.verify
+
+ options.doc "\n How to display their output"
+ options.formatters
+ options.verbose
+
+ options.doc "\n What action to perform"
+ options.actions
+
+ options.doc "\n When to perform it"
+ options.action_filters
+
+ options.doc "\n Help!"
+ options.debug
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ options.doc "\n How might this work in the real world?"
+ options.doc "\n 1. To simply run some specs"
+ options.doc "\n $ mspec path/to/the/specs"
+ options.doc " mspec path/to/the_file_spec.rb"
+ options.doc "\n 2. To run specs tagged with 'fails'"
+ options.doc "\n $ mspec -g fails path/to/the_file_spec.rb"
+ options.doc "\n 3. To start the debugger before the spec matching 'this crashes'"
+ options.doc "\n $ mspec --spec-debug -S 'this crashes' path/to/the_file_spec.rb"
+ options.doc "\n 4. To run some specs matching 'this crashes'"
+ options.doc "\n $ mspec -e 'this crashes' path/to/the_file_spec.rb"
+
+ options.doc ""
+
+ patterns = options.parse argv
+ @files = files_from_patterns(patterns)
+ end
+
+ def run
+ MSpec.register_tags_patterns config[:tags_patterns]
+ MSpec.register_files @files
+
+ MSpec.process
+ exit MSpec.exit_code
+ end
+end
diff --git a/spec/mspec/lib/mspec/commands/mspec-tag.rb b/spec/mspec/lib/mspec/commands/mspec-tag.rb
new file mode 100644
index 0000000000..7582015916
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec-tag.rb
@@ -0,0 +1,133 @@
+#!/usr/bin/env ruby
+
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+
+
+class MSpecTag < MSpecScript
+ def initialize
+ super
+
+ config[:tagger] = :add
+ config[:tag] = 'fails:'
+ config[:outcome] = :fail
+ config[:ltags] = []
+ end
+
+ def options(argv=ARGV)
+ options = MSpecOptions.new "mspec tag [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " Ask yourself:"
+ options.doc " 1. What specs to run?"
+ options.doc " 2. How to modify the execution?"
+ options.doc " 3. How to display the output?"
+ options.doc " 4. What tag action to perform?"
+ options.doc " 5. When to perform it?"
+
+ options.doc "\n What specs to run"
+ options.filters
+
+ options.doc "\n How to modify the execution"
+ options.configure { |f| load f }
+ options.name
+ options.pretend
+ options.unguarded
+ options.interrupt
+
+ options.doc "\n How to display their output"
+ options.formatters
+ options.verbose
+
+ options.doc "\n What action to perform and when to perform it"
+ options.on("-N", "--add", "TAG",
+ "Add TAG with format 'tag' or 'tag(comment)' (see -Q, -F, -L)") do |o|
+ config[:tagger] = :add
+ config[:tag] = "#{o}:"
+ end
+ options.on("-R", "--del", "TAG",
+ "Delete TAG (see -Q, -F, -L)") do |o|
+ config[:tagger] = :del
+ config[:tag] = "#{o}:"
+ config[:outcome] = :pass
+ end
+ options.on("-Q", "--pass", "Apply action to specs that pass (default for --del)") do
+ config[:outcome] = :pass
+ end
+ options.on("-F", "--fail", "Apply action to specs that fail (default for --add)") do
+ config[:outcome] = :fail
+ end
+ options.on("-L", "--all", "Apply action to all specs") do
+ config[:outcome] = :all
+ end
+ options.on("--list", "TAG", "Display descriptions of any specs tagged with TAG") do |t|
+ config[:tagger] = :list
+ config[:ltags] << t
+ end
+ options.on("--list-all", "Display descriptions of any tagged specs") do
+ config[:tagger] = :list_all
+ end
+ options.on("--purge", "Remove all tags not matching any specs") do
+ config[:tagger] = :purge
+ end
+
+ options.doc "\n Help!"
+ options.debug
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ options.doc "\n How might this work in the real world?"
+ options.doc "\n 1. To add the 'fails' tag to failing specs"
+ options.doc "\n $ mspec tag path/to/the_file_spec.rb"
+ options.doc "\n 2. To remove the 'fails' tag from passing specs"
+ options.doc "\n $ mspec tag --del fails path/to/the_file_spec.rb"
+ options.doc "\n 3. To display the descriptions for all specs tagged with 'fails'"
+ options.doc "\n $ mspec tag --list fails path/to/the/specs"
+ options.doc ""
+
+ patterns = options.parse argv
+ if patterns.empty?
+ puts options
+ puts "No files specified."
+ exit 1
+ end
+ @files = files patterns
+ end
+
+ def register
+ require 'mspec/runner/actions'
+
+ case config[:tagger]
+ when :add, :del
+ tag = SpecTag.new config[:tag]
+ tagger = TagAction.new(config[:tagger], config[:outcome], tag.tag, tag.comment,
+ config[:atags], config[:astrings])
+ when :list, :list_all
+ tagger = TagListAction.new config[:tagger] == :list_all ? nil : config[:ltags]
+ MSpec.register_mode :pretend
+ config[:formatter] = false
+ when :purge
+ tagger = TagPurgeAction.new
+ MSpec.register_mode :pretend
+ MSpec.register_mode :unguarded
+ config[:formatter] = false
+ else
+ raise ArgumentError, "No recognized action given"
+ end
+ tagger.register
+
+ super
+ end
+
+ def run
+ MSpec.register_tags_patterns config[:tags_patterns]
+ MSpec.register_files @files
+
+ MSpec.process
+ exit MSpec.exit_code
+ end
+end
+
diff --git a/spec/mspec/lib/mspec/commands/mspec.rb b/spec/mspec/lib/mspec/commands/mspec.rb
new file mode 100755
index 0000000000..6f1ae8cb6e
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec.rb
@@ -0,0 +1,163 @@
+#!/usr/bin/env ruby
+
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+require 'mspec/helpers/tmp'
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/timer'
+
+
+class MSpecMain < MSpecScript
+ def initialize
+ super
+
+ config[:loadpath] = []
+ config[:requires] = []
+ config[:target] = ENV['RUBY'] || 'ruby'
+ config[:flags] = []
+ config[:command] = nil
+ config[:options] = []
+ config[:launch] = []
+ end
+
+ def options(argv=ARGV)
+ config[:command] = argv.shift if ["ci", "run", "tag"].include?(argv[0])
+
+ options = MSpecOptions.new "mspec [COMMAND] [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " The mspec command sets up and invokes the sub-commands"
+ options.doc " (see below) to enable, for instance, running the specs"
+ options.doc " with different implementations like ruby, jruby, rbx, etc.\n"
+
+ options.configure do |f|
+ load f
+ config[:options] << '-B' << f
+ end
+
+ options.targets
+
+ options.on("--warnings", "Don't supress warnings") do
+ config[:flags] << '-w'
+ ENV['OUTPUT_WARNINGS'] = '1'
+ end
+
+ options.on("-j", "--multi", "Run multiple (possibly parallel) subprocesses") do
+ config[:multi] = true
+ config[:options] << "-fy"
+ end
+
+ options.version MSpec::VERSION do
+ if config[:command]
+ config[:options] << "-v"
+ else
+ puts "#{File.basename $0} #{MSpec::VERSION}"
+ exit
+ end
+ end
+
+ options.help do
+ if config[:command]
+ config[:options] << "-h"
+ else
+ puts options
+ exit 1
+ end
+ end
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ # The rest of the help output
+ options.doc "\n where COMMAND is one of:\n"
+ options.doc " run - Run the specified specs (default)"
+ options.doc " ci - Run the known good specs"
+ options.doc " tag - Add or remove tags\n"
+ options.doc " mspec COMMAND -h for more options\n"
+ options.doc " example: $ mspec run -h\n"
+
+ options.on_extra { |o| config[:options] << o }
+ options.parse(argv)
+
+ if config[:multi]
+ options = MSpecOptions.new "mspec", 30, config
+ options.all
+ patterns = options.parse(config[:options])
+ @files = files_from_patterns(patterns)
+ end
+ end
+
+ def register; end
+
+ def multi_exec(argv)
+ MSpec.register_files @files
+
+ require 'mspec/runner/formatters/multi'
+ formatter = MultiFormatter.new
+
+ output_files = []
+ processes = [cores, @files.size].min
+ children = processes.times.map { |i|
+ name = tmp "mspec-multi-#{i}"
+ output_files << name
+
+ env = {
+ "SPEC_TEMP_DIR" => "rubyspec_temp_#{i}",
+ "MSPEC_MULTI" => i.to_s
+ }
+ command = argv + ["-o", name]
+ $stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG
+ IO.popen([env, *command], "rb+")
+ }
+
+ puts children.map { |child| child.gets }.uniq
+ formatter.start
+
+ until @files.empty?
+ IO.select(children)[0].each { |io|
+ reply = io.read(1)
+ case reply
+ when '.'
+ formatter.unload
+ when nil
+ raise "Worker died!"
+ else
+ while chunk = (io.read_nonblock(4096) rescue nil)
+ reply += chunk
+ end
+ raise reply
+ end
+ io.puts @files.shift unless @files.empty?
+ }
+ end
+
+ ok = true
+ children.each { |child|
+ child.puts "QUIT"
+ Process.wait(child.pid)
+ ok &&= $?.success?
+ }
+
+ formatter.aggregate_results(output_files)
+ formatter.finish
+ ok
+ end
+
+ def run
+ argv = config[:target].split(/\s+/)
+
+ argv.concat config[:launch]
+ argv.concat config[:flags]
+ argv.concat config[:loadpath]
+ argv.concat config[:requires]
+ argv << "#{MSPEC_HOME}/bin/mspec-#{ config[:command] || "run" }"
+ argv.concat config[:options]
+
+ if config[:multi]
+ exit multi_exec(argv)
+ else
+ $stderr.puts "$ #{argv.join(' ')}"
+ exec(*argv)
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/expectations.rb b/spec/mspec/lib/mspec/expectations.rb
new file mode 100644
index 0000000000..d07f959b27
--- /dev/null
+++ b/spec/mspec/lib/mspec/expectations.rb
@@ -0,0 +1,2 @@
+require 'mspec/expectations/expectations'
+require 'mspec/expectations/should'
diff --git a/spec/mspec/lib/mspec/expectations/expectations.rb b/spec/mspec/lib/mspec/expectations/expectations.rb
new file mode 100644
index 0000000000..cfdc2b63a3
--- /dev/null
+++ b/spec/mspec/lib/mspec/expectations/expectations.rb
@@ -0,0 +1,21 @@
+class SpecExpectationNotMetError < StandardError
+end
+
+class SpecExpectationNotFoundError < StandardError
+ def message
+ "No behavior expectation was found in the example"
+ end
+end
+
+class SpecExpectation
+ def self.fail_with(expected, actual)
+ expected_to_s = expected.to_s
+ actual_to_s = actual.to_s
+ if expected_to_s.size + actual_to_s.size > 80
+ message = "#{expected_to_s.chomp}\n#{actual_to_s}"
+ else
+ message = "#{expected_to_s} #{actual_to_s}"
+ end
+ Kernel.raise SpecExpectationNotMetError, message
+ end
+end
diff --git a/spec/mspec/lib/mspec/expectations/should.rb b/spec/mspec/lib/mspec/expectations/should.rb
new file mode 100644
index 0000000000..f6d83053f5
--- /dev/null
+++ b/spec/mspec/lib/mspec/expectations/should.rb
@@ -0,0 +1,29 @@
+class Object
+ NO_MATCHER_GIVEN = Object.new
+
+ def should(matcher = NO_MATCHER_GIVEN)
+ MSpec.expectation
+ MSpec.actions :expectation, MSpec.current.state
+ unless matcher.equal? NO_MATCHER_GIVEN
+ unless matcher.matches? self
+ expected, actual = matcher.failure_message
+ SpecExpectation.fail_with(expected, actual)
+ end
+ else
+ SpecPositiveOperatorMatcher.new(self)
+ end
+ end
+
+ def should_not(matcher = NO_MATCHER_GIVEN)
+ MSpec.expectation
+ MSpec.actions :expectation, MSpec.current.state
+ unless matcher.equal? NO_MATCHER_GIVEN
+ if matcher.matches? self
+ expected, actual = matcher.negative_failure_message
+ SpecExpectation.fail_with(expected, actual)
+ end
+ else
+ SpecNegativeOperatorMatcher.new(self)
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards.rb b/spec/mspec/lib/mspec/guards.rb
new file mode 100644
index 0000000000..0d7d300c64
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards.rb
@@ -0,0 +1,12 @@
+require 'mspec/utils/ruby_name'
+require 'mspec/guards/block_device'
+require 'mspec/guards/bug'
+require 'mspec/guards/conflict'
+require 'mspec/guards/endian'
+require 'mspec/guards/feature'
+require 'mspec/guards/guard'
+require 'mspec/guards/platform'
+require 'mspec/guards/quarantine'
+require 'mspec/guards/support'
+require 'mspec/guards/superuser'
+require 'mspec/guards/version'
diff --git a/spec/mspec/lib/mspec/guards/block_device.rb b/spec/mspec/lib/mspec/guards/block_device.rb
new file mode 100644
index 0000000000..327f6e564e
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/block_device.rb
@@ -0,0 +1,18 @@
+require 'mspec/guards/guard'
+
+class BlockDeviceGuard < SpecGuard
+ def match?
+ platform_is_not :freebsd, :windows, :opal do
+ block = `find /dev /devices -type b 2> /dev/null`
+ return !(block.nil? || block.empty?)
+ end
+
+ false
+ end
+end
+
+class Object
+ def with_block_device(&block)
+ BlockDeviceGuard.new.run_if(:with_block_device, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/bug.rb b/spec/mspec/lib/mspec/guards/bug.rb
new file mode 100644
index 0000000000..31de6e080d
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/bug.rb
@@ -0,0 +1,30 @@
+require 'mspec/guards/version'
+
+class BugGuard < VersionGuard
+ def initialize(bug, version)
+ @bug = bug
+ if String === version
+ MSpec.deprecate "ruby_bug with a single version", 'an exclusive range ("2.1"..."2.3")'
+ @version = SpecVersion.new version, true
+ else
+ super(version)
+ end
+ @parameters = [@bug, @version]
+ end
+
+ def match?
+ return false if MSpec.mode? :no_ruby_bug
+ return false unless PlatformGuard.standard?
+ if Range === @version
+ super
+ else
+ FULL_RUBY_VERSION <= @version
+ end
+ end
+end
+
+class Object
+ def ruby_bug(bug, version, &block)
+ BugGuard.new(bug, version).run_unless(:ruby_bug, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/conflict.rb b/spec/mspec/lib/mspec/guards/conflict.rb
new file mode 100644
index 0000000000..c1d33e3512
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/conflict.rb
@@ -0,0 +1,19 @@
+require 'mspec/guards/guard'
+
+class ConflictsGuard < SpecGuard
+ def match?
+ # Always convert constants to symbols regardless of version.
+ constants = Object.constants.map { |x| x.to_sym }
+ @parameters.any? { |mod| constants.include? mod }
+ end
+end
+
+class Object
+ # In some cases, libraries will modify another Ruby method's
+ # behavior. The specs for the method's behavior will then fail
+ # if that library is loaded. This guard will not run if any of
+ # the specified constants exist in Object.constants.
+ def conflicts_with(*modules, &block)
+ ConflictsGuard.new(*modules).run_unless(:conflicts_with, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/endian.rb b/spec/mspec/lib/mspec/guards/endian.rb
new file mode 100644
index 0000000000..6bb01263c7
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/endian.rb
@@ -0,0 +1,27 @@
+require 'mspec/guards/guard'
+
+# Despite that these are inverses, the two classes are
+# used to simplify MSpec guard reporting modes
+
+class EndianGuard < SpecGuard
+ def pattern
+ @pattern ||= [1].pack('L')
+ end
+ private :pattern
+end
+
+class BigEndianGuard < EndianGuard
+ def match?
+ pattern[-1] == ?\001
+ end
+end
+
+class Object
+ def big_endian(&block)
+ BigEndianGuard.new.run_if(:big_endian, &block)
+ end
+
+ def little_endian(&block)
+ BigEndianGuard.new.run_unless(:little_endian, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/feature.rb b/spec/mspec/lib/mspec/guards/feature.rb
new file mode 100644
index 0000000000..346212bda0
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/feature.rb
@@ -0,0 +1,43 @@
+require 'mspec/guards/guard'
+
+class FeatureGuard < SpecGuard
+ def self.enabled?(*features)
+ new(*features).match?
+ end
+
+ def match?
+ @parameters.all? { |f| MSpec.feature_enabled? f }
+ end
+end
+
+class Object
+ # Provides better documentation in the specs by
+ # naming sets of features that work together as
+ # a whole. Examples include :encoding, :fiber,
+ # :continuation, :fork.
+ #
+ # Usage example:
+ #
+ # with_feature :encoding do
+ # # specs for a method that provides aspects
+ # # of the encoding feature
+ # end
+ #
+ # Multiple features must all be enabled for the
+ # guard to run:
+ #
+ # with_feature :one, :two do
+ # # these specs will run if features :one AND
+ # # :two are enabled.
+ # end
+ #
+ # The implementation must explicitly enable a feature
+ # by adding code like the following to the .mspec
+ # configuration file:
+ #
+ # MSpec.enable_feature :encoding
+ #
+ def with_feature(*features, &block)
+ FeatureGuard.new(*features).run_if(:with_feature, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/guard.rb b/spec/mspec/lib/mspec/guards/guard.rb
new file mode 100644
index 0000000000..c95d8f7923
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/guard.rb
@@ -0,0 +1,118 @@
+require 'mspec/runner/mspec'
+require 'mspec/runner/actions/tally'
+require 'mspec/utils/ruby_name'
+
+class SpecGuard
+ def self.report
+ @report ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ def self.clear
+ @report = nil
+ end
+
+ def self.finish
+ report.keys.sort.each do |key|
+ desc = report[key]
+ size = desc.size
+ spec = size == 1 ? "spec" : "specs"
+ print "\n\n#{size} #{spec} omitted by guard: #{key}:\n"
+ desc.each { |description| print "\n", description; }
+ end
+
+ print "\n\n"
+ end
+
+ def self.guards
+ @guards ||= []
+ end
+
+ def self.clear_guards
+ @guards = []
+ end
+
+ # Returns a partial Ruby version string based on +which+.
+ # For example, if RUBY_VERSION = 8.2.3:
+ #
+ # :major => "8"
+ # :minor => "8.2"
+ # :tiny => "8.2.3"
+ # :teeny => "8.2.3"
+ # :full => "8.2.3"
+ def self.ruby_version(which = :minor)
+ case which
+ when :major
+ n = 1
+ when :minor
+ n = 2
+ when :tiny, :teeny, :full
+ n = 3
+ end
+
+ RUBY_VERSION.split('.')[0,n].join('.')
+ end
+
+ attr_accessor :name
+
+ def initialize(*args)
+ @parameters = args
+ end
+
+ def yield?(invert = false)
+ return true if MSpec.mode? :unguarded
+
+ allow = match? ^ invert
+
+ if !allow and reporting?
+ MSpec.guard
+ MSpec.register :finish, SpecGuard
+ MSpec.register :add, self
+ return true
+ elsif MSpec.mode? :verify
+ return true
+ end
+
+ allow
+ end
+
+ def run_if(name, &block)
+ @name = name
+ yield if yield?(false)
+ ensure
+ unregister
+ end
+
+ def run_unless(name, &block)
+ @name = name
+ yield if yield?(true)
+ ensure
+ unregister
+ end
+
+ def reporting?
+ MSpec.mode?(:report) or
+ (MSpec.mode?(:report_on) and SpecGuard.guards.include?(name))
+ end
+
+ def report_key
+ "#{name} #{@parameters.join(", ")}"
+ end
+
+ def record(description)
+ SpecGuard.report[report_key] << description
+ end
+
+ def add(example)
+ record example.description
+ MSpec.retrieve(:formatter).tally.counter.guards!
+ end
+
+ def unregister
+ MSpec.unguard
+ MSpec.unregister :add, self
+ end
+
+ def match?
+ raise "must be implemented by the subclass"
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/platform.rb b/spec/mspec/lib/mspec/guards/platform.rb
new file mode 100644
index 0000000000..875aef6c9c
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/platform.rb
@@ -0,0 +1,78 @@
+require 'mspec/guards/guard'
+
+class PlatformGuard < SpecGuard
+ def self.implementation?(*args)
+ args.any? do |name|
+ case name
+ when :rubinius
+ RUBY_NAME.start_with?('rbx')
+ when :ruby, :jruby, :truffleruby, :ironruby, :macruby, :maglev, :topaz, :opal
+ RUBY_NAME.start_with?(name.to_s)
+ else
+ raise "unknown implementation #{name}"
+ end
+ end
+ end
+
+ def self.standard?
+ implementation? :ruby
+ end
+
+ HOST_OS = begin
+ require 'rbconfig'
+ RbConfig::CONFIG['host_os'] || RUBY_PLATFORM
+ rescue LoadError
+ RUBY_PLATFORM
+ end.downcase
+
+ def self.os?(*oses)
+ oses.any? do |os|
+ raise ":java is not a valid OS" if os == :java
+ if os == :windows
+ HOST_OS =~ /(mswin|mingw)/
+ else
+ HOST_OS.include?(os.to_s)
+ end
+ end
+ end
+
+ def self.windows?
+ os?(:windows)
+ end
+
+ def self.wordsize?(size)
+ size == 8 * 1.size
+ end
+
+ def initialize(*args)
+ if args.last.is_a?(Hash)
+ @options, @platforms = args.last, args[0..-2]
+ else
+ @options, @platforms = {}, args
+ end
+ @parameters = args
+ end
+
+ def match?
+ match = @platforms.empty? ? true : PlatformGuard.os?(*@platforms)
+ @options.each do |key, value|
+ case key
+ when :os
+ match &&= PlatformGuard.os?(*value)
+ when :wordsize
+ match &&= PlatformGuard.wordsize? value
+ end
+ end
+ match
+ end
+end
+
+class Object
+ def platform_is(*args, &block)
+ PlatformGuard.new(*args).run_if(:platform_is, &block)
+ end
+
+ def platform_is_not(*args, &block)
+ PlatformGuard.new(*args).run_unless(:platform_is_not, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/quarantine.rb b/spec/mspec/lib/mspec/guards/quarantine.rb
new file mode 100644
index 0000000000..4724613a0f
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/quarantine.rb
@@ -0,0 +1,13 @@
+require 'mspec/guards/guard'
+
+class QuarantineGuard < SpecGuard
+ def match?
+ true
+ end
+end
+
+class Object
+ def quarantine!(&block)
+ QuarantineGuard.new.run_unless(:quarantine!, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/superuser.rb b/spec/mspec/lib/mspec/guards/superuser.rb
new file mode 100644
index 0000000000..6e447198a7
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/superuser.rb
@@ -0,0 +1,17 @@
+require 'mspec/guards/guard'
+
+class SuperUserGuard < SpecGuard
+ def match?
+ Process.euid == 0
+ end
+end
+
+class Object
+ def as_superuser(&block)
+ SuperUserGuard.new.run_if(:as_superuser, &block)
+ end
+
+ def as_user(&block)
+ SuperUserGuard.new.run_unless(:as_user, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/support.rb b/spec/mspec/lib/mspec/guards/support.rb
new file mode 100644
index 0000000000..f1760ece2e
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/support.rb
@@ -0,0 +1,16 @@
+require 'mspec/guards/platform'
+
+class SupportedGuard < SpecGuard
+ def match?
+ if @parameters.include? :ruby
+ raise Exception, "improper use of not_supported_on guard"
+ end
+ !PlatformGuard.standard? and PlatformGuard.implementation?(*@parameters)
+ end
+end
+
+class Object
+ def not_supported_on(*args, &block)
+ SupportedGuard.new(*args).run_unless(:not_supported_on, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards/version.rb b/spec/mspec/lib/mspec/guards/version.rb
new file mode 100644
index 0000000000..110853e082
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/version.rb
@@ -0,0 +1,39 @@
+require 'mspec/utils/deprecate'
+require 'mspec/utils/version'
+require 'mspec/guards/guard'
+
+class VersionGuard < SpecGuard
+ FULL_RUBY_VERSION = SpecVersion.new SpecGuard.ruby_version(:full)
+
+ def initialize(version)
+ case version
+ when String
+ @version = SpecVersion.new version
+ when Range
+ MSpec.deprecate "an empty version range end", 'a specific version' if version.end.empty?
+ a = SpecVersion.new version.begin
+ b = SpecVersion.new version.end
+ unless version.exclude_end?
+ MSpec.deprecate "ruby_version_is with an inclusive range", 'an exclusive range ("2.1"..."2.3")'
+ end
+ @version = version.exclude_end? ? a...b : a..b
+ else
+ raise "version must be a String or Range but was a #{version.class}"
+ end
+ @parameters = [version]
+ end
+
+ def match?
+ if Range === @version
+ @version.include? FULL_RUBY_VERSION
+ else
+ FULL_RUBY_VERSION >= @version
+ end
+ end
+end
+
+class Object
+ def ruby_version_is(*args, &block)
+ VersionGuard.new(*args).run_if(:ruby_version_is, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers.rb b/spec/mspec/lib/mspec/helpers.rb
new file mode 100644
index 0000000000..f2d1c9fb21
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers.rb
@@ -0,0 +1,12 @@
+require 'mspec/helpers/argf'
+require 'mspec/helpers/argv'
+require 'mspec/helpers/datetime'
+require 'mspec/helpers/fixture'
+require 'mspec/helpers/flunk'
+require 'mspec/helpers/fs'
+require 'mspec/helpers/io'
+require 'mspec/helpers/mock_to_path'
+require 'mspec/helpers/numeric'
+require 'mspec/helpers/ruby_exe'
+require 'mspec/helpers/scratch'
+require 'mspec/helpers/tmp'
diff --git a/spec/mspec/lib/mspec/helpers/argf.rb b/spec/mspec/lib/mspec/helpers/argf.rb
new file mode 100644
index 0000000000..1ba48b9378
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/argf.rb
@@ -0,0 +1,37 @@
+class Object
+ # Convenience helper for specs using ARGF.
+ # Set @argf to an instance of ARGF.class with the given +argv+.
+ # That instance must be used instead of ARGF as ARGF is global
+ # and it is not always possible to reset its state correctly.
+ #
+ # The helper yields to the block and then close
+ # the files open by the instance. Example:
+ #
+ # describe "That" do
+ # it "does something" do
+ # argf ['a', 'b'] do
+ # # do something
+ # end
+ # end
+ # end
+ def argf(argv)
+ if argv.empty? or argv.length > 2
+ raise "Only 1 or 2 filenames are allowed for the argf helper so files can be properly closed: #{argv.inspect}"
+ end
+ @argf ||= nil
+ raise "Cannot nest calls to the argf helper" if @argf
+
+ @argf = ARGF.class.new(*argv)
+ @__mspec_saved_argf_file__ = @argf.file
+ begin
+ yield
+ ensure
+ file1 = @__mspec_saved_argf_file__
+ file2 = @argf.file # Either the first file or the second
+ file1.close if !file1.closed? and file1 != STDIN
+ file2.close if !file2.closed? and file2 != STDIN
+ @argf = nil
+ @__mspec_saved_argf_file__ = nil
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/argv.rb b/spec/mspec/lib/mspec/helpers/argv.rb
new file mode 100644
index 0000000000..c8cbbf2ac3
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/argv.rb
@@ -0,0 +1,46 @@
+class Object
+ # Convenience helper for altering ARGV. Saves the
+ # value of ARGV and sets it to +args+. If a block
+ # is given, yields to the block and then restores
+ # the value of ARGV. The previously saved value of
+ # ARGV can be restored by passing +:restore+. The
+ # former is useful in a single spec. The latter is
+ # useful in before/after actions. For example:
+ #
+ # describe "This" do
+ # before do
+ # argv ['a', 'b']
+ # end
+ #
+ # after do
+ # argv :restore
+ # end
+ #
+ # it "does something" do
+ # # do something
+ # end
+ # end
+ #
+ # describe "That" do
+ # it "does something" do
+ # argv ['a', 'b'] do
+ # # do something
+ # end
+ # end
+ # end
+ def argv(args)
+ if args == :restore
+ ARGV.replace(@__mspec_saved_argv__ || [])
+ else
+ @__mspec_saved_argv__ = ARGV.dup
+ ARGV.replace args
+ if block_given?
+ begin
+ yield
+ ensure
+ argv :restore
+ end
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/datetime.rb b/spec/mspec/lib/mspec/helpers/datetime.rb
new file mode 100644
index 0000000000..4cb57bdaa1
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/datetime.rb
@@ -0,0 +1,51 @@
+class Object
+ # The new_datetime helper makes writing DateTime specs more simple by
+ # providing default constructor values and accepting a Hash of only the
+ # constructor values needed for the particular spec. For example:
+ #
+ # new_datetime :hour => 1, :minute => 20
+ #
+ # Possible keys are:
+ # :year, :month, :day, :hour, :minute, :second, :offset and :sg.
+
+ def new_datetime(opts={})
+ require 'date'
+
+ value = {
+ :year => -4712,
+ :month => 1,
+ :day => 1,
+ :hour => 0,
+ :minute => 0,
+ :second => 0,
+ :offset => 0,
+ :sg => Date::ITALY
+ }.merge opts
+
+ DateTime.new value[:year], value[:month], value[:day], value[:hour],
+ value[:minute], value[:second], value[:offset], value[:sg]
+ end
+
+ def with_timezone(name, offset = nil, daylight_saving_zone = "")
+ zone = name.dup
+
+ if offset
+ # TZ convention is backwards
+ offset = -offset
+
+ zone += offset.to_s
+ zone += ":00:00"
+ end
+ zone += daylight_saving_zone
+
+ old = ENV["TZ"]
+ ENV["TZ"] = zone
+
+ begin
+ yield
+ ensure
+ ENV["TZ"] = old
+ end
+ end
+
+end
diff --git a/spec/mspec/lib/mspec/helpers/fixture.rb b/spec/mspec/lib/mspec/helpers/fixture.rb
new file mode 100644
index 0000000000..718c1b7a94
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/fixture.rb
@@ -0,0 +1,26 @@
+class Object
+ # Returns the name of a fixture file by adjoining the directory
+ # of the +file+ argument with "fixtures" and the contents of the
+ # +args+ array. For example,
+ #
+ # +file+ == "some/example_spec.rb"
+ #
+ # and
+ #
+ # +args+ == ["subdir", "file.txt"]
+ #
+ # then the result is the expanded path of
+ #
+ # "some/fixtures/subdir/file.txt".
+ def fixture(file, *args)
+ path = File.dirname(file)
+ path = path[0..-7] if path[-7..-1] == "/shared"
+ fixtures = path[-9..-1] == "/fixtures" ? "" : "fixtures"
+ if File.respond_to?(:realpath)
+ path = File.realpath(path)
+ else
+ path = File.expand_path(path)
+ end
+ File.join(path, fixtures, args)
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/flunk.rb b/spec/mspec/lib/mspec/helpers/flunk.rb
new file mode 100644
index 0000000000..35bd939b85
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/flunk.rb
@@ -0,0 +1,5 @@
+class Object
+ def flunk(msg="This example is a failure")
+ SpecExpectation.fail_with "Failed:", msg
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/fs.rb b/spec/mspec/lib/mspec/helpers/fs.rb
new file mode 100644
index 0000000000..ee33f5fec0
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/fs.rb
@@ -0,0 +1,62 @@
+class Object
+ # Copies a file
+ def cp(source, dest)
+ File.open(dest, "w") do |d|
+ File.open(source, "r") do |s|
+ while data = s.read(1024)
+ d.write data
+ end
+ end
+ end
+ end
+
+ # Creates each directory in path that does not exist.
+ def mkdir_p(path)
+ parts = File.expand_path(path).split %r[/|\\]
+ name = parts.shift
+ parts.each do |part|
+ name = File.join name, part
+
+ if File.file? name
+ raise ArgumentError, "path component of #{path} is a file"
+ end
+
+ Dir.mkdir name unless File.directory? name
+ end
+ end
+
+ # Recursively removes all files and directories in +path+
+ # if +path+ is a directory. Removes the file if +path+ is
+ # a file.
+ def rm_r(*paths)
+ paths.each do |path|
+ path = File.expand_path path
+
+ prefix = SPEC_TEMP_DIR
+ unless path[0, prefix.size] == prefix
+ raise ArgumentError, "#{path} is not prefixed by #{prefix}"
+ end
+
+ # File.symlink? needs to be checked first as
+ # File.exist? returns false for dangling symlinks
+ if File.symlink? path
+ File.unlink path
+ elsif File.directory? path
+ Dir.entries(path).each { |x| rm_r "#{path}/#{x}" unless x =~ /^\.\.?$/ }
+ Dir.rmdir path
+ elsif File.exist? path
+ File.delete path
+ end
+ end
+ end
+
+ # Creates a file +name+. Creates the directory for +name+
+ # if it does not exist.
+ def touch(name, mode="w")
+ mkdir_p File.dirname(name)
+
+ File.open(name, mode) do |f|
+ yield f if block_given?
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/io.rb b/spec/mspec/lib/mspec/helpers/io.rb
new file mode 100644
index 0000000000..83d14441a7
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/io.rb
@@ -0,0 +1,113 @@
+require 'mspec/guards/feature'
+
+class IOStub
+ def initialize
+ @buffer = []
+ @output = ''
+ end
+
+ def write(*str)
+ self << str.join
+ end
+
+ def << str
+ @buffer << str
+ self
+ end
+
+ def print(*str)
+ write(str.join + $\.to_s)
+ end
+
+ def method_missing(name, *args, &block)
+ to_s.send(name, *args, &block)
+ end
+
+ def == other
+ to_s == other
+ end
+
+ def =~ other
+ to_s =~ other
+ end
+
+ def puts(*str)
+ if str.empty?
+ write "\n"
+ else
+ write(str.collect { |s| s.to_s.chomp }.concat([nil]).join("\n"))
+ end
+ end
+
+ def printf(format, *args)
+ self << sprintf(format, *args)
+ end
+
+ def flush
+ @output += @buffer.join('')
+ @buffer.clear
+ self
+ end
+
+ def to_s
+ flush
+ @output
+ end
+
+ alias_method :to_str, :to_s
+
+ def inspect
+ to_s.inspect
+ end
+end
+
+class Object
+ # Creates a "bare" file descriptor (i.e. one that is not associated
+ # with any Ruby object). The file descriptor can safely be passed
+ # to IO.new without creating a Ruby object alias to the fd.
+ def new_fd(name, mode="w:utf-8")
+ mode = options_or_mode(mode)
+
+ if mode.kind_of? Hash
+ if mode.key? :mode
+ mode = mode[:mode]
+ else
+ raise ArgumentError, "new_fd options Hash must include :mode"
+ end
+ end
+
+ IO.sysopen name, fmode(mode)
+ end
+
+ # Creates an IO instance for a temporary file name. The file
+ # must be deleted.
+ def new_io(name, mode="w:utf-8")
+ IO.new new_fd(name, options_or_mode(mode)), options_or_mode(mode)
+ end
+
+ # This helper simplifies passing file access modes regardless of
+ # whether the :encoding feature is enabled. Only the access specifier
+ # itself will be returned if :encoding is not enabled. Otherwise,
+ # the full mode string will be returned (i.e. the helper is a no-op).
+ def fmode(mode)
+ if FeatureGuard.enabled? :encoding
+ mode
+ else
+ mode.split(':').first
+ end
+ end
+
+ # This helper simplifies passing file access modes or options regardless of
+ # whether the :encoding feature is enabled. Only the access specifier itself
+ # will be returned if :encoding is not enabled. Otherwise, the full mode
+ # string or option will be returned (i.e. the helper is a no-op).
+ def options_or_mode(oom)
+ return fmode(oom) if oom.kind_of? String
+
+ if FeatureGuard.enabled? :encoding
+ oom
+ else
+ fmode(oom[:mode] || "r:utf-8")
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/mock_to_path.rb b/spec/mspec/lib/mspec/helpers/mock_to_path.rb
new file mode 100644
index 0000000000..683bb1d9d6
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/mock_to_path.rb
@@ -0,0 +1,8 @@
+class Object
+ def mock_to_path(path)
+ # Cannot use our Object#mock here since it conflicts with RSpec
+ obj = MockObject.new('path')
+ obj.should_receive(:to_path).and_return(path)
+ obj
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/numeric.rb b/spec/mspec/lib/mspec/helpers/numeric.rb
new file mode 100644
index 0000000000..ff30cf2b83
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/numeric.rb
@@ -0,0 +1,72 @@
+require 'mspec/guards/platform'
+
+class Object
+ def nan_value
+ 0/0.0
+ end
+
+ def infinity_value
+ 1/0.0
+ end
+
+ def bignum_value(plus=0)
+ 0x8000_0000_0000_0000 + plus
+ end
+
+ # This is a bit hairy, but we need to be able to write specs that cover the
+ # boundary between Fixnum and Bignum for operations like Fixnum#<<. Since
+ # this boundary is implementation-dependent, we use these helpers to write
+ # specs based on the relationship between values rather than specific
+ # values.
+ if PlatformGuard.standard? or PlatformGuard.implementation? :topaz
+ if PlatformGuard.wordsize? 32
+ def fixnum_max
+ (2**30) - 1
+ end
+
+ def fixnum_min
+ -(2**30)
+ end
+ elsif PlatformGuard.wordsize? 64
+ def fixnum_max
+ (2**62) - 1
+ end
+
+ def fixnum_min
+ -(2**62)
+ end
+ end
+ elsif PlatformGuard.implementation? :opal
+ def fixnum_max
+ Integer::MAX
+ end
+
+ def fixnum_min
+ Integer::MIN
+ end
+ elsif PlatformGuard.implementation? :rubinius
+ def fixnum_max
+ Fixnum::MAX
+ end
+
+ def fixnum_min
+ Fixnum::MIN
+ end
+ elsif PlatformGuard.implementation?(:jruby) || PlatformGuard.implementation?(:truffleruby)
+ def fixnum_max
+ 9223372036854775807
+ end
+
+ def fixnum_min
+ -9223372036854775808
+ end
+ else
+ def fixnum_max
+ raise "unknown implementation for fixnum_max() helper"
+ end
+
+ def fixnum_min
+ raise "unknown implementation for fixnum_min() helper"
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/ruby_exe.rb b/spec/mspec/lib/mspec/helpers/ruby_exe.rb
new file mode 100644
index 0000000000..a025be6c81
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/ruby_exe.rb
@@ -0,0 +1,178 @@
+require 'mspec/utils/ruby_name'
+require 'mspec/guards/platform'
+require 'mspec/helpers/tmp'
+
+# The ruby_exe helper provides a wrapper for invoking the
+# same Ruby interpreter with the same falgs as the one running
+# the specs and getting the output from running the code.
+# If +code+ is a file that exists, it will be run.
+# Otherwise, +code+ should be Ruby code that will be run with
+# the -e command line option. For example:
+#
+# ruby_exe('path/to/some/file.rb')
+#
+# will be executed as
+#
+# `#{RUBY_EXE} 'path/to/some/file.rb'`
+#
+# while
+#
+# ruby_exe('puts "hello, world."')
+#
+# will be executed as
+#
+# `#{RUBY_EXE} -e 'puts "hello, world."'`
+#
+# The ruby_exe helper also accepts an options hash with three
+# keys: :options, :args and :env. For example:
+#
+# ruby_exe('file.rb', :options => "-w",
+# :args => "> file.txt",
+# :env => { :FOO => "bar" })
+#
+# will be executed as
+#
+# `#{RUBY_EXE} -w #{'file.rb'} > file.txt`
+#
+# with access to ENV["FOO"] with value "bar".
+#
+# If +nil+ is passed for the first argument, the command line
+# will be built only from the options hash.
+#
+# The RUBY_EXE constant is setup by mspec automatically
+# and is used by ruby_exe and ruby_cmd. The mspec runner script
+# will set ENV['RUBY_EXE'] to the name of the executable used
+# to invoke the mspec-run script. The value of RUBY_EXE will be
+# constructed as follows:
+#
+# 1. the value of ENV['RUBY_EXE']
+# 2. an explicit value based on RUBY_NAME
+# 3. cwd/(RUBY_NAME + $(EXEEXT) || $(exeext) || '')
+# 4. $(bindir)/$(RUBY_INSTALL_NAME)
+#
+# The value will only be used if the file exists and is executable.
+# The flags will then be appended to the resulting value.
+#
+# These 4 ways correspond to the following scenarios:
+#
+# 1. Using the MSpec runner scripts, the name of the
+# executable is explicitly passed by ENV['RUBY_EXE']
+# so there is no ambiguity.
+#
+# Otherwise, if using RSpec (or something else)
+#
+# 2. Running the specs while developing an alternative
+# Ruby implementation. This explicitly names the
+# executable in the development directory based on
+# the value of RUBY_NAME, which is probably initialized
+# from the value of RUBY_ENGINE.
+# 3. Running the specs within the source directory for
+# some implementation. (E.g. a local build directory.)
+# 4. Running the specs against some installed Ruby
+# implementation.
+#
+# Additionally, the flags passed to mspec
+# (with -T on the command line or in the config with set :flags)
+# will be appended to RUBY_EXE so that the interpreter
+# is always called with those flags.
+
+class Object
+ def ruby_exe_options(option)
+ case option
+ when :env
+ ENV['RUBY_EXE']
+ when :engine
+ case RUBY_NAME
+ when 'rbx'
+ "bin/rbx"
+ when 'jruby'
+ "bin/jruby"
+ when 'maglev'
+ "maglev-ruby"
+ when 'topaz'
+ "topaz"
+ when 'ironruby'
+ "ir"
+ end
+ when :name
+ require 'rbconfig'
+ bin = RUBY_NAME + (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
+ File.join(".", bin)
+ when :install_name
+ require 'rbconfig'
+ bin = RbConfig::CONFIG["RUBY_INSTALL_NAME"] || RbConfig::CONFIG["ruby_install_name"]
+ bin << (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
+ File.join(RbConfig::CONFIG['bindir'], bin)
+ end
+ end
+
+ def resolve_ruby_exe
+ [:env, :engine, :name, :install_name].each do |option|
+ next unless exe = ruby_exe_options(option)
+
+ if File.file?(exe) and File.executable?(exe)
+ exe = File.expand_path(exe)
+ exe = exe.tr('/', '\\') if PlatformGuard.windows?
+ flags = ENV['RUBY_FLAGS']
+ if flags and !flags.empty?
+ return exe + ' ' + flags
+ else
+ return exe
+ end
+ end
+ end
+ raise Exception, "Unable to find a suitable ruby executable."
+ end
+
+ def ruby_exe(code, opts = {})
+ if opts[:dir]
+ raise "ruby_exe(..., dir: dir) is no longer supported, use Dir.chdir"
+ end
+
+ env = opts[:env] || {}
+ saved_env = {}
+ env.each do |key, value|
+ key = key.to_s
+ saved_env[key] = ENV[key] if ENV.key? key
+ ENV[key] = value
+ end
+
+ escape = opts.delete(:escape)
+ if code and !File.exist?(code) and escape != false
+ tmpfile = tmp("rubyexe.rb")
+ File.open(tmpfile, "w") { |f| f.write(code) }
+ code = tmpfile
+ end
+
+ begin
+ platform_is_not :opal do
+ `#{ruby_cmd(code, opts)}`
+ end
+ ensure
+ saved_env.each { |key, value| ENV[key] = value }
+ env.keys.each do |key|
+ key = key.to_s
+ ENV.delete key unless saved_env.key? key
+ end
+ File.delete tmpfile if tmpfile
+ end
+ end
+
+ def ruby_cmd(code, opts = {})
+ body = code
+
+ if opts[:escape]
+ raise "escape: true is no longer supported in ruby_cmd, use ruby_exe or a fixture"
+ end
+
+ if code and !File.exist?(code)
+ body = "-e #{code.inspect}"
+ end
+
+ [RUBY_EXE, opts[:options], body, opts[:args]].compact.join(' ')
+ end
+
+ unless Object.const_defined?(:RUBY_EXE) and RUBY_EXE
+ RUBY_EXE = resolve_ruby_exe
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/scratch.rb b/spec/mspec/lib/mspec/helpers/scratch.rb
new file mode 100644
index 0000000000..a6b0c02748
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/scratch.rb
@@ -0,0 +1,17 @@
+module ScratchPad
+ def self.clear
+ @record = nil
+ end
+
+ def self.record(arg)
+ @record = arg
+ end
+
+ def self.<<(arg)
+ @record << arg
+ end
+
+ def self.recorded
+ @record
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/tmp.rb b/spec/mspec/lib/mspec/helpers/tmp.rb
new file mode 100644
index 0000000000..742eb57fdc
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/tmp.rb
@@ -0,0 +1,45 @@
+# Creates a temporary directory in the current working directory
+# for temporary files created while running the specs. All specs
+# should clean up any temporary files created so that the temp
+# directory is empty when the process exits.
+
+SPEC_TEMP_DIR = File.expand_path(ENV["SPEC_TEMP_DIR"] || "rubyspec_temp")
+
+SPEC_TEMP_UNIQUIFIER = "0"
+
+SPEC_TEMP_DIR_PID = Process.pid
+
+at_exit do
+ begin
+ if SPEC_TEMP_DIR_PID == Process.pid
+ Dir.delete SPEC_TEMP_DIR if File.directory? SPEC_TEMP_DIR
+ end
+ rescue SystemCallError
+ STDERR.puts <<-EOM
+
+-----------------------------------------------------
+The rubyspec temp directory is not empty. Ensure that
+all specs are cleaning up temporary files:
+ #{SPEC_TEMP_DIR}
+-----------------------------------------------------
+
+ EOM
+ rescue Object => e
+ STDERR.puts "failed to remove spec temp directory"
+ STDERR.puts e.message
+ end
+end
+
+class Object
+ def tmp(name, uniquify=true)
+ Dir.mkdir SPEC_TEMP_DIR unless Dir.exist? SPEC_TEMP_DIR
+
+ if uniquify and !name.empty?
+ slash = name.rindex "/"
+ index = slash ? slash + 1 : 0
+ name.insert index, "#{SPEC_TEMP_UNIQUIFIER.succ!}-"
+ end
+
+ File.join SPEC_TEMP_DIR, name
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers.rb b/spec/mspec/lib/mspec/matchers.rb
new file mode 100644
index 0000000000..8eab73198a
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers.rb
@@ -0,0 +1,35 @@
+require 'mspec/matchers/base'
+require 'mspec/matchers/be_an_instance_of'
+require 'mspec/matchers/be_ancestor_of'
+require 'mspec/matchers/be_close'
+require 'mspec/matchers/be_computed_by'
+require 'mspec/matchers/be_empty'
+require 'mspec/matchers/be_false'
+require 'mspec/matchers/be_kind_of'
+require 'mspec/matchers/be_nan'
+require 'mspec/matchers/be_nil'
+require 'mspec/matchers/be_true'
+require 'mspec/matchers/be_true_or_false'
+require 'mspec/matchers/complain'
+require 'mspec/matchers/eql'
+require 'mspec/matchers/equal'
+require 'mspec/matchers/equal_element'
+require 'mspec/matchers/have_constant'
+require 'mspec/matchers/have_class_variable'
+require 'mspec/matchers/have_instance_method'
+require 'mspec/matchers/have_instance_variable'
+require 'mspec/matchers/have_method'
+require 'mspec/matchers/have_private_instance_method'
+require 'mspec/matchers/have_private_method'
+require 'mspec/matchers/have_protected_instance_method'
+require 'mspec/matchers/have_public_instance_method'
+require 'mspec/matchers/have_singleton_method'
+require 'mspec/matchers/include'
+require 'mspec/matchers/infinity'
+require 'mspec/matchers/match_yaml'
+require 'mspec/matchers/raise_error'
+require 'mspec/matchers/output'
+require 'mspec/matchers/output_to_fd'
+require 'mspec/matchers/respond_to'
+require 'mspec/matchers/signed_zero'
+require 'mspec/matchers/block_caller'
diff --git a/spec/mspec/lib/mspec/matchers/base.rb b/spec/mspec/lib/mspec/matchers/base.rb
new file mode 100644
index 0000000000..30fb1f93dc
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/base.rb
@@ -0,0 +1,95 @@
+class SpecPositiveOperatorMatcher
+ def initialize(actual)
+ @actual = actual
+ end
+
+ def ==(expected)
+ unless @actual == expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "to equal #{expected.pretty_inspect}")
+ end
+ end
+
+ def <(expected)
+ unless @actual < expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "to be less than #{expected.pretty_inspect}")
+ end
+ end
+
+ def <=(expected)
+ unless @actual <= expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "to be less than or equal to #{expected.pretty_inspect}")
+ end
+ end
+
+ def >(expected)
+ unless @actual > expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "to be greater than #{expected.pretty_inspect}")
+ end
+ end
+
+ def >=(expected)
+ unless @actual >= expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "to be greater than or equal to #{expected.pretty_inspect}")
+ end
+ end
+
+ def =~(expected)
+ unless @actual =~ expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "to match #{expected.pretty_inspect}")
+ end
+ end
+end
+
+class SpecNegativeOperatorMatcher
+ def initialize(actual)
+ @actual = actual
+ end
+
+ def ==(expected)
+ if @actual == expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "not to equal #{expected.pretty_inspect}")
+ end
+ end
+
+ def <(expected)
+ if @actual < expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "not to be less than #{expected.pretty_inspect}")
+ end
+ end
+
+ def <=(expected)
+ if @actual <= expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "not to be less than or equal to #{expected.pretty_inspect}")
+ end
+ end
+
+ def >(expected)
+ if @actual > expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "not to be greater than #{expected.pretty_inspect}")
+ end
+ end
+
+ def >=(expected)
+ if @actual >= expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "not to be greater than or equal to #{expected.pretty_inspect}")
+ end
+ end
+
+ def =~(expected)
+ if @actual =~ expected
+ SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
+ "not to match #{expected.pretty_inspect}")
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb
new file mode 100644
index 0000000000..6e31afcddd
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb
@@ -0,0 +1,26 @@
+class BeAnInstanceOfMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.instance_of?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})",
+ "to be an instance of #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})",
+ "not to be an instance of #{@expected}"]
+ end
+end
+
+class Object
+ def be_an_instance_of(expected)
+ BeAnInstanceOfMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb
new file mode 100644
index 0000000000..792c64089a
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb
@@ -0,0 +1,24 @@
+class BeAncestorOfMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @expected.ancestors.include? @actual
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be an ancestor of #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be an ancestor of #{@expected}"]
+ end
+end
+
+class Object
+ def be_ancestor_of(expected)
+ BeAncestorOfMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_close.rb b/spec/mspec/lib/mspec/matchers/be_close.rb
new file mode 100644
index 0000000000..5d79654099
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_close.rb
@@ -0,0 +1,27 @@
+TOLERANCE = 0.00003 unless Object.const_defined?(:TOLERANCE)
+
+class BeCloseMatcher
+ def initialize(expected, tolerance)
+ @expected = expected
+ @tolerance = tolerance
+ end
+
+ def matches?(actual)
+ @actual = actual
+ (@actual - @expected).abs < @tolerance
+ end
+
+ def failure_message
+ ["Expected #{@expected}", "to be within +/- #{@tolerance} of #{@actual}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@expected}", "not to be within +/- #{@tolerance} of #{@actual}"]
+ end
+end
+
+class Object
+ def be_close(expected, tolerance)
+ BeCloseMatcher.new(expected, tolerance)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_computed_by.rb b/spec/mspec/lib/mspec/matchers/be_computed_by.rb
new file mode 100644
index 0000000000..c927eb7697
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_computed_by.rb
@@ -0,0 +1,37 @@
+class BeComputedByMatcher
+ def initialize(sym, *args)
+ @method = sym
+ @args = args
+ end
+
+ def matches?(array)
+ array.each do |line|
+ @receiver = line.shift
+ @value = line.pop
+ @arguments = line
+ @arguments += @args
+ @actual = @receiver.send(@method, *@arguments)
+ return false unless @actual == @value
+ end
+
+ return true
+ end
+
+ def method_call
+ method_call = "#{@receiver.inspect}.#{@method}"
+ unless @arguments.empty?
+ method_call = "#{method_call} from #{@arguments.map { |x| x.inspect }.join(", ")}"
+ end
+ method_call
+ end
+
+ def failure_message
+ ["Expected #{@value.inspect}", "to be computed by #{method_call} (computed #{@actual.inspect} instead)"]
+ end
+end
+
+class Object
+ def be_computed_by(sym, *args)
+ BeComputedByMatcher.new(sym, *args)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_empty.rb b/spec/mspec/lib/mspec/matchers/be_empty.rb
new file mode 100644
index 0000000000..8a401b63fd
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_empty.rb
@@ -0,0 +1,20 @@
+class BeEmptyMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual.empty?
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be empty"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be empty"]
+ end
+end
+
+class Object
+ def be_empty
+ BeEmptyMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_false.rb b/spec/mspec/lib/mspec/matchers/be_false.rb
new file mode 100644
index 0000000000..0a6e8cfd63
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_false.rb
@@ -0,0 +1,20 @@
+class BeFalseMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual == false
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be false"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be false"]
+ end
+end
+
+class Object
+ def be_false
+ BeFalseMatcher.new
+ end
+end \ No newline at end of file
diff --git a/spec/mspec/lib/mspec/matchers/be_kind_of.rb b/spec/mspec/lib/mspec/matchers/be_kind_of.rb
new file mode 100644
index 0000000000..a734f6159c
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_kind_of.rb
@@ -0,0 +1,24 @@
+class BeKindOfMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.is_a?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "to be kind of #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "not to be kind of #{@expected}"]
+ end
+end
+
+class Object
+ def be_kind_of(expected)
+ BeKindOfMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_nan.rb b/spec/mspec/lib/mspec/matchers/be_nan.rb
new file mode 100644
index 0000000000..aa19391211
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_nan.rb
@@ -0,0 +1,20 @@
+class BeNaNMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual.kind_of?(Float) && @actual.nan?
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be NaN"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be NaN"]
+ end
+end
+
+class Object
+ def be_nan
+ BeNaNMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_nil.rb b/spec/mspec/lib/mspec/matchers/be_nil.rb
new file mode 100644
index 0000000000..ecea6feffa
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_nil.rb
@@ -0,0 +1,20 @@
+class BeNilMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual.nil?
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be nil"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be nil"]
+ end
+end
+
+class Object
+ def be_nil
+ BeNilMatcher.new
+ end
+end \ No newline at end of file
diff --git a/spec/mspec/lib/mspec/matchers/be_true.rb b/spec/mspec/lib/mspec/matchers/be_true.rb
new file mode 100644
index 0000000000..de8e237d35
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_true.rb
@@ -0,0 +1,20 @@
+class BeTrueMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual == true
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be true"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be true"]
+ end
+end
+
+class Object
+ def be_true
+ BeTrueMatcher.new
+ end
+end \ No newline at end of file
diff --git a/spec/mspec/lib/mspec/matchers/be_true_or_false.rb b/spec/mspec/lib/mspec/matchers/be_true_or_false.rb
new file mode 100644
index 0000000000..b2262779ed
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_true_or_false.rb
@@ -0,0 +1,20 @@
+class BeTrueOrFalseMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual == true || @actual == false
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be true or false"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be true or false"]
+ end
+end
+
+class Object
+ def be_true_or_false
+ BeTrueOrFalseMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/block_caller.rb b/spec/mspec/lib/mspec/matchers/block_caller.rb
new file mode 100644
index 0000000000..5451950712
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/block_caller.rb
@@ -0,0 +1,35 @@
+class BlockingMatcher
+ def matches?(block)
+ started = false
+ blocking = true
+
+ thread = Thread.new do
+ started = true
+ block.call
+
+ blocking = false
+ end
+
+ while !started and status = thread.status and status != "sleep"
+ Thread.pass
+ end
+ thread.kill
+ thread.join
+
+ blocking
+ end
+
+ def failure_message
+ ['Expected the given Proc', 'to block the caller']
+ end
+
+ def negative_failure_message
+ ['Expected the given Proc', 'to not block the caller']
+ end
+end
+
+class Object
+ def block_caller(timeout = 0.1)
+ BlockingMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/complain.rb b/spec/mspec/lib/mspec/matchers/complain.rb
new file mode 100644
index 0000000000..1313215156
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/complain.rb
@@ -0,0 +1,56 @@
+require 'mspec/helpers/io'
+
+class ComplainMatcher
+ def initialize(complaint)
+ @complaint = complaint
+ end
+
+ def matches?(proc)
+ @saved_err = $stderr
+ @stderr = $stderr = IOStub.new
+ @verbose = $VERBOSE
+ $VERBOSE = false
+
+ proc.call
+
+ unless @complaint.nil?
+ case @complaint
+ when Regexp
+ return false unless $stderr =~ @complaint
+ else
+ return false unless $stderr == @complaint
+ end
+ end
+
+ return $stderr.empty? ? false : true
+ ensure
+ $VERBOSE = @verbose
+ $stderr = @saved_err
+ end
+
+ def failure_message
+ if @complaint.nil?
+ ["Expected a warning", "but received none"]
+ elsif @complaint.kind_of? Regexp
+ ["Expected warning to match: #{@complaint.inspect}", "but got: #{@stderr.chomp.inspect}"]
+ else
+ ["Expected warning: #{@complaint.inspect}", "but got: #{@stderr.chomp.inspect}"]
+ end
+ end
+
+ def negative_failure_message
+ if @complaint.nil?
+ ["Unexpected warning: ", @stderr.chomp.inspect]
+ elsif @complaint.kind_of? Regexp
+ ["Expected warning not to match: #{@complaint.inspect}", "but got: #{@stderr.chomp.inspect}"]
+ else
+ ["Expected warning: #{@complaint.inspect}", "but got: #{@stderr.chomp.inspect}"]
+ end
+ end
+end
+
+class Object
+ def complain(complaint=nil)
+ ComplainMatcher.new(complaint)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/eql.rb b/spec/mspec/lib/mspec/matchers/eql.rb
new file mode 100644
index 0000000000..82117d862c
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/eql.rb
@@ -0,0 +1,26 @@
+class EqlMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.eql?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.pretty_inspect}",
+ "to have same value and type as #{@expected.pretty_inspect}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.pretty_inspect}",
+ "not to have same value or type as #{@expected.pretty_inspect}"]
+ end
+end
+
+class Object
+ def eql(expected)
+ EqlMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/equal.rb b/spec/mspec/lib/mspec/matchers/equal.rb
new file mode 100644
index 0000000000..ee6431fd4f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/equal.rb
@@ -0,0 +1,26 @@
+class EqualMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.equal?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.pretty_inspect}",
+ "to be identical to #{@expected.pretty_inspect}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.pretty_inspect}",
+ "not to be identical to #{@expected.pretty_inspect}"]
+ end
+end
+
+class Object
+ def equal(expected)
+ EqualMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/equal_element.rb b/spec/mspec/lib/mspec/matchers/equal_element.rb
new file mode 100644
index 0000000000..8d032fd088
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/equal_element.rb
@@ -0,0 +1,78 @@
+class EqualElementMatcher
+ def initialize(element, attributes = nil, content = nil, options = {})
+ @element = element
+ @attributes = attributes
+ @content = content
+ @options = options
+ end
+
+ def matches?(actual)
+ @actual = actual
+
+ matched = true
+
+ if @options[:not_closed]
+ matched &&= actual =~ /^#{Regexp.quote("<" + @element)}.*#{Regexp.quote(">" + (@content || ''))}$/
+ else
+ matched &&= actual =~ /^#{Regexp.quote("<" + @element)}/
+ matched &&= actual =~ /#{Regexp.quote("</" + @element + ">")}$/
+ matched &&= actual =~ /#{Regexp.quote(">" + @content + "</")}/ if @content
+ end
+
+ if @attributes
+ if @attributes.empty?
+ matched &&= actual.scan(/\w+\=\"(.*)\"/).size == 0
+ else
+ @attributes.each do |key, value|
+ if value == true
+ matched &&= (actual.scan(/#{Regexp.quote(key)}(\s|>)/).size == 1)
+ else
+ matched &&= (actual.scan(%Q{ #{key}="#{value}"}).size == 1)
+ end
+ end
+ end
+ end
+
+ !!matched
+ end
+
+ def failure_message
+ ["Expected #{@actual.pretty_inspect}",
+ "to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.pretty_inspect}",
+ "not to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
+ end
+
+ def attributes_for_failure_message
+ if @attributes
+ if @attributes.empty?
+ "no attributes"
+ else
+ @attributes.inject([]) { |memo, n| memo << %Q{#{n[0]}="#{n[1]}"} }.join(" ")
+ end
+ else
+ "any attributes"
+ end
+ end
+
+ def content_for_failure_message
+ if @content
+ if @content.empty?
+ "no content"
+ else
+ "#{@content.inspect} as content"
+ end
+ else
+ "any content"
+ end
+ end
+end
+
+class Object
+ def equal_element(*args)
+ EqualElementMatcher.new(*args)
+ end
+end \ No newline at end of file
diff --git a/spec/mspec/lib/mspec/matchers/have_class_variable.rb b/spec/mspec/lib/mspec/matchers/have_class_variable.rb
new file mode 100644
index 0000000000..45cd0b5ae1
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_class_variable.rb
@@ -0,0 +1,12 @@
+require 'mspec/matchers/variable'
+
+class HaveClassVariableMatcher < VariableMatcher
+ self.variables_method = :class_variables
+ self.description = 'class variable'
+end
+
+class Object
+ def have_class_variable(variable)
+ HaveClassVariableMatcher.new(variable)
+ end
+end \ No newline at end of file
diff --git a/spec/mspec/lib/mspec/matchers/have_constant.rb b/spec/mspec/lib/mspec/matchers/have_constant.rb
new file mode 100644
index 0000000000..df95219e53
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_constant.rb
@@ -0,0 +1,12 @@
+require 'mspec/matchers/variable'
+
+class HaveConstantMatcher < VariableMatcher
+ self.variables_method = :constants
+ self.description = 'constant'
+end
+
+class Object
+ def have_constant(variable)
+ HaveConstantMatcher.new(variable)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_instance_method.rb
new file mode 100644
index 0000000000..00dcbd39eb
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+class Object
+ def have_instance_method(method, include_super=true)
+ HaveInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_instance_variable.rb b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb
new file mode 100644
index 0000000000..e83eb9408c
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb
@@ -0,0 +1,12 @@
+require 'mspec/matchers/variable'
+
+class HaveInstanceVariableMatcher < VariableMatcher
+ self.variables_method = :instance_variables
+ self.description = 'instance variable'
+end
+
+class Object
+ def have_instance_variable(variable)
+ HaveInstanceVariableMatcher.new(variable)
+ end
+end \ No newline at end of file
diff --git a/spec/mspec/lib/mspec/matchers/have_method.rb b/spec/mspec/lib/mspec/matchers/have_method.rb
new file mode 100644
index 0000000000..2fc3e66f69
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ @mod.methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+class Object
+ def have_method(method, include_super=true)
+ HaveMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb
new file mode 100644
index 0000000000..87d9767a69
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HavePrivateInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.private_instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have private instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have private instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+class Object
+ def have_private_instance_method(method, include_super=true)
+ HavePrivateInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_private_method.rb b/spec/mspec/lib/mspec/matchers/have_private_method.rb
new file mode 100644
index 0000000000..d99d4ccb7f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_private_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HavePrivateMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.private_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have private method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have private method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+class Object
+ def have_private_method(method, include_super=true)
+ HavePrivateMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb
new file mode 100644
index 0000000000..92f38e9acb
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveProtectedInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.protected_instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have protected instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have protected instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+class Object
+ def have_protected_instance_method(method, include_super=true)
+ HaveProtectedInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb
new file mode 100644
index 0000000000..035547d28f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HavePublicInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.public_instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have public instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have public instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+class Object
+ def have_public_instance_method(method, include_super=true)
+ HavePublicInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_singleton_method.rb b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb
new file mode 100644
index 0000000000..5f3acb84e2
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveSingletonMethodMatcher < MethodMatcher
+ def matches?(obj)
+ @obj = obj
+ obj.singleton_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@obj} to have singleton method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@obj} NOT to have singleton method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+class Object
+ def have_singleton_method(method, include_super=true)
+ HaveSingletonMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/include.rb b/spec/mspec/lib/mspec/matchers/include.rb
new file mode 100644
index 0000000000..b4e54158d1
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/include.rb
@@ -0,0 +1,32 @@
+class IncludeMatcher
+ def initialize(*expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @expected.each do |e|
+ @element = e
+ unless @actual.include?(e)
+ return false
+ end
+ end
+ return true
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to include #{@element.inspect}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to include #{@element.inspect}"]
+ end
+end
+
+# Cannot override #include at the toplevel in MRI
+module MSpec
+ def include(*expected)
+ IncludeMatcher.new(*expected)
+ end
+ module_function :include
+end
diff --git a/spec/mspec/lib/mspec/matchers/infinity.rb b/spec/mspec/lib/mspec/matchers/infinity.rb
new file mode 100644
index 0000000000..0949fd47eb
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/infinity.rb
@@ -0,0 +1,28 @@
+class InfinityMatcher
+ def initialize(expected_sign)
+ @expected_sign = expected_sign
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.kind_of?(Float) && @actual.infinite? == @expected_sign
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}Infinity"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}Infinity"]
+ end
+end
+
+class Object
+ def be_positive_infinity
+ InfinityMatcher.new(1)
+ end
+
+ def be_negative_infinity
+ InfinityMatcher.new(-1)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/match_yaml.rb b/spec/mspec/lib/mspec/matchers/match_yaml.rb
new file mode 100644
index 0000000000..542dece2b4
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/match_yaml.rb
@@ -0,0 +1,47 @@
+class MatchYAMLMatcher
+
+ def initialize(expected)
+ if valid_yaml?(expected)
+ @expected = expected
+ else
+ @expected = expected.to_yaml
+ end
+ end
+
+ def matches?(actual)
+ @actual = actual
+ clean_yaml(@actual) == clean_yaml(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", " to match #{@expected.inspect}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", " to match #{@expected.inspect}"]
+ end
+
+ protected
+
+ def clean_yaml(yaml)
+ yaml.gsub(/([^-]|^---)\s+\n/, "\\1\n").sub(/\n\.\.\.\n$/, "\n")
+ end
+
+ def valid_yaml?(obj)
+ require 'yaml'
+ begin
+ YAML.load(obj)
+ rescue
+ false
+ else
+ true
+ end
+ end
+end
+
+class Object
+ def match_yaml(expected)
+ MatchYAMLMatcher.new(expected)
+ end
+end
+
diff --git a/spec/mspec/lib/mspec/matchers/method.rb b/spec/mspec/lib/mspec/matchers/method.rb
new file mode 100644
index 0000000000..e8cdfa62ff
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/method.rb
@@ -0,0 +1,10 @@
+class MethodMatcher
+ def initialize(method, include_super=true)
+ @include_super = include_super
+ @method = method.to_sym
+ end
+
+ def matches?(mod)
+ raise Exception, "define #matches? in the subclass"
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/output.rb b/spec/mspec/lib/mspec/matchers/output.rb
new file mode 100644
index 0000000000..551e7506cf
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/output.rb
@@ -0,0 +1,67 @@
+require 'mspec/helpers/io'
+
+class OutputMatcher
+ def initialize(stdout, stderr)
+ @out = stdout
+ @err = stderr
+ end
+
+ def matches?(proc)
+ @saved_out = $stdout
+ @saved_err = $stderr
+ @stdout = $stdout = IOStub.new
+ @stderr = $stderr = IOStub.new
+
+ proc.call
+
+ unless @out.nil?
+ case @out
+ when Regexp
+ return false unless $stdout =~ @out
+ else
+ return false unless $stdout == @out
+ end
+ end
+
+ unless @err.nil?
+ case @err
+ when Regexp
+ return false unless $stderr =~ @err
+ else
+ return false unless $stderr == @err
+ end
+ end
+
+ return true
+ ensure
+ $stdout = @saved_out
+ $stderr = @saved_err
+ end
+
+ def failure_message
+ expected_out = "\n"
+ actual_out = "\n"
+ unless @out.nil?
+ expected_out += " $stdout: #{@out.inspect}\n"
+ actual_out += " $stdout: #{@stdout.inspect}\n"
+ end
+ unless @err.nil?
+ expected_out += " $stderr: #{@err.inspect}\n"
+ actual_out += " $stderr: #{@stderr.inspect}\n"
+ end
+ ["Expected:#{expected_out}", " got:#{actual_out}"]
+ end
+
+ def negative_failure_message
+ out = ""
+ out += " $stdout: #{@stdout.chomp.dump}\n" unless @out.nil?
+ out += " $stderr: #{@stderr.chomp.dump}\n" unless @err.nil?
+ ["Expected output not to be:\n", out]
+ end
+end
+
+class Object
+ def output(stdout=nil, stderr=nil)
+ OutputMatcher.new(stdout, stderr)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/output_to_fd.rb b/spec/mspec/lib/mspec/matchers/output_to_fd.rb
new file mode 100644
index 0000000000..5daaf5545c
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/output_to_fd.rb
@@ -0,0 +1,71 @@
+require 'mspec/helpers/tmp'
+
+# Lower-level output speccing mechanism for a single
+# output stream. Unlike OutputMatcher which provides
+# methods to capture the output, we actually replace
+# the FD itself so that there is no reliance on a
+# certain method being used.
+class OutputToFDMatcher
+ def initialize(expected, to)
+ @to, @expected = to, expected
+
+ case @to
+ when STDOUT
+ @to_name = "STDOUT"
+ when STDERR
+ @to_name = "STDERR"
+ when IO
+ @to_name = @to.object_id.to_s
+ else
+ raise ArgumentError, "#{@to.inspect} is not a supported output target"
+ end
+ end
+
+ def with_tmp
+ path = tmp("mspec_output_to_#{$$}_#{Time.now.to_i}")
+ File.open(path, 'w+') { |io|
+ yield(io)
+ }
+ ensure
+ File.delete path if path
+ end
+
+ def matches?(block)
+ old_to = @to.dup
+ with_tmp do |out|
+ # Replacing with a file handle so that Readline etc. work
+ @to.reopen out
+ begin
+ block.call
+ ensure
+ @to.reopen old_to
+ old_to.close
+ end
+
+ out.rewind
+ @actual = out.read
+
+ case @expected
+ when Regexp
+ !(@actual =~ @expected).nil?
+ else
+ @actual == @expected
+ end
+ end
+ end
+
+ def failure_message()
+ ["Expected (#{@to_name}): #{@expected.inspect}\n",
+ "#{'but got'.rjust(@to_name.length + 10)}: #{@actual.inspect}\nBacktrace"]
+ end
+
+ def negative_failure_message()
+ ["Expected output (#{@to_name}) to NOT be:\n", @actual.inspect]
+ end
+end
+
+class Object
+ def output_to_fd(what, where = STDOUT)
+ OutputToFDMatcher.new what, where
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/raise_error.rb b/spec/mspec/lib/mspec/matchers/raise_error.rb
new file mode 100644
index 0000000000..a5d6e01ec9
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/raise_error.rb
@@ -0,0 +1,79 @@
+require 'mspec/utils/deprecate'
+
+class RaiseErrorMatcher
+ def initialize(exception, message, &block)
+ @exception = exception
+ @message = message
+ @block = block
+ end
+
+ def matches?(proc)
+ @result = proc.call
+ return false
+ rescue Exception => @actual
+ if matching_exception?(@actual)
+ return true
+ else
+ raise @actual
+ end
+ end
+
+ def matching_exception?(exc)
+ return false unless @exception === exc
+ if @message then
+ case @message
+ when String
+ return false if @message != exc.message
+ when Regexp
+ return false if @message !~ exc.message
+ end
+ end
+
+ # The block has its own expectations and will throw an exception if it fails
+ @block[exc] if @block
+
+ return true
+ end
+
+ def exception_class_and_message(exception_class, message)
+ if message
+ "#{exception_class} (#{message})"
+ else
+ "#{exception_class}"
+ end
+ end
+
+ def format_expected_exception
+ exception_class_and_message(@exception, @message)
+ end
+
+ def format_exception(exception)
+ exception_class_and_message(exception.class, exception.message)
+ end
+
+ def failure_message
+ message = ["Expected #{format_expected_exception}"]
+
+ if @actual then
+ message << "but got #{format_exception(@actual)}"
+ else
+ message << "but no exception was raised (#{@result.pretty_inspect.chomp} was returned)"
+ end
+
+ message
+ end
+
+ def negative_failure_message
+ message = ["Expected to not get #{format_expected_exception}", ""]
+ unless @actual.class == @exception
+ message[1] = "but got #{format_exception(@actual)}"
+ end
+ message
+ end
+end
+
+class Object
+ def raise_error(exception=Exception, message=nil, &block)
+ RaiseErrorMatcher.new(exception, message, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/respond_to.rb b/spec/mspec/lib/mspec/matchers/respond_to.rb
new file mode 100644
index 0000000000..2aa3ab14d1
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/respond_to.rb
@@ -0,0 +1,24 @@
+class RespondToMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.respond_to?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "to respond to #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "not to respond to #{@expected}"]
+ end
+end
+
+class Object
+ def respond_to(expected)
+ RespondToMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/signed_zero.rb b/spec/mspec/lib/mspec/matchers/signed_zero.rb
new file mode 100644
index 0000000000..3fd1472fc8
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/signed_zero.rb
@@ -0,0 +1,28 @@
+class SignedZeroMatcher
+ def initialize(expected_sign)
+ @expected_sign = expected_sign
+ end
+
+ def matches?(actual)
+ @actual = actual
+ (1.0/actual).infinite? == @expected_sign
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}0.0"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}0.0"]
+ end
+end
+
+class Object
+ def be_positive_zero
+ SignedZeroMatcher.new(1)
+ end
+
+ def be_negative_zero
+ SignedZeroMatcher.new(-1)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/variable.rb b/spec/mspec/lib/mspec/matchers/variable.rb
new file mode 100644
index 0000000000..4d801ea337
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/variable.rb
@@ -0,0 +1,24 @@
+class VariableMatcher
+ class << self
+ attr_accessor :variables_method, :description
+ end
+
+ def initialize(variable)
+ @variable = variable.to_sym
+ end
+
+ def matches?(object)
+ @object = object
+ @object.send(self.class.variables_method).include? @variable
+ end
+
+ def failure_message
+ ["Expected #{@object} to have #{self.class.description} '#{@variable}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@object} NOT to have #{self.class.description} '#{@variable}'",
+ "but it does"]
+ end
+end
diff --git a/spec/mspec/lib/mspec/mocks.rb b/spec/mspec/lib/mspec/mocks.rb
new file mode 100644
index 0000000000..6a029c7b53
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks.rb
@@ -0,0 +1,3 @@
+require 'mspec/mocks/mock'
+require 'mspec/mocks/proxy'
+require 'mspec/mocks/object'
diff --git a/spec/mspec/lib/mspec/mocks/mock.rb b/spec/mspec/lib/mspec/mocks/mock.rb
new file mode 100644
index 0000000000..1557f2008e
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks/mock.rb
@@ -0,0 +1,197 @@
+require 'mspec/expectations/expectations'
+
+class Object
+ alias_method :__mspec_object_id__, :object_id
+end
+
+module Mock
+ def self.reset
+ @mocks = @stubs = @objects = nil
+ end
+
+ def self.objects
+ @objects ||= {}
+ end
+
+ def self.mocks
+ @mocks ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ def self.stubs
+ @stubs ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ def self.replaced_name(obj, sym)
+ :"__mspec_#{obj.__mspec_object_id__}_#{sym}__"
+ end
+
+ def self.replaced_key(obj, sym)
+ [replaced_name(obj, sym), sym]
+ end
+
+ def self.has_key?(keys, sym)
+ !!keys.find { |k| k.first == sym }
+ end
+
+ def self.replaced?(sym)
+ has_key?(mocks.keys, sym) or has_key?(stubs.keys, sym)
+ end
+
+ def self.clear_replaced(key)
+ mocks.delete key
+ stubs.delete key
+ end
+
+ def self.mock_respond_to?(obj, sym, include_private = false)
+ name = replaced_name(obj, :respond_to?)
+ if replaced? name
+ obj.__send__ name, sym, include_private
+ else
+ obj.respond_to? sym, include_private
+ end
+ end
+
+ def self.install_method(obj, sym, type=nil)
+ meta = obj.singleton_class
+
+ key = replaced_key obj, sym
+ sym = sym.to_sym
+
+ if (sym == :respond_to? or mock_respond_to?(obj, sym, true)) and !replaced?(key.first)
+ meta.__send__ :alias_method, key.first, sym
+ end
+
+ meta.class_eval {
+ define_method(sym) do |*args, &block|
+ Mock.verify_call self, sym, *args, &block
+ end
+ }
+
+ proxy = MockProxy.new type
+
+ if proxy.mock?
+ MSpec.expectation
+ MSpec.actions :expectation, MSpec.current.state
+ end
+
+ if proxy.stub?
+ stubs[key].unshift proxy
+ else
+ mocks[key] << proxy
+ end
+ objects[key] = obj
+
+ proxy
+ end
+
+ def self.name_or_inspect(obj)
+ obj.instance_variable_get(:@name) || obj.inspect
+ end
+
+ def self.verify_count
+ mocks.each do |key, proxies|
+ obj = objects[key]
+ proxies.each do |proxy|
+ qualifier, count = proxy.count
+ pass = case qualifier
+ when :at_least
+ proxy.calls >= count
+ when :at_most
+ proxy.calls <= count
+ when :exactly
+ proxy.calls == count
+ when :any_number_of_times
+ true
+ else
+ false
+ end
+ unless pass
+ SpecExpectation.fail_with(
+ "Mock '#{name_or_inspect obj}' expected to receive '#{key.last}' " + \
+ "#{qualifier.to_s.sub('_', ' ')} #{count} times",
+ "but received it #{proxy.calls} times")
+ end
+ end
+ end
+ end
+
+ def self.verify_call(obj, sym, *args, &block)
+ compare = *args
+ compare = compare.first if compare.length <= 1
+
+ key = replaced_key obj, sym
+ [mocks, stubs].each do |proxies|
+ proxies[key].each do |proxy|
+ pass = case proxy.arguments
+ when :any_args
+ true
+ when :no_args
+ compare.nil?
+ else
+ proxy.arguments == compare
+ end
+
+ if proxy.yielding?
+ if block
+ proxy.yielding.each do |args_to_yield|
+ if block.arity == -1 || block.arity == args_to_yield.size
+ block.call(*args_to_yield)
+ else
+ SpecExpectation.fail_with(
+ "Mock '#{name_or_inspect obj}' asked to yield " + \
+ "|#{proxy.yielding.join(', ')}| on #{sym}\n",
+ "but a block with arity #{block.arity} was passed")
+ end
+ end
+ else
+ SpecExpectation.fail_with(
+ "Mock '#{name_or_inspect obj}' asked to yield " + \
+ "|[#{proxy.yielding.join('], [')}]| on #{sym}\n",
+ "but no block was passed")
+ end
+ end
+
+ if pass
+ proxy.called
+
+ if proxy.raising?
+ raise proxy.raising
+ else
+ return proxy.returning
+ end
+ end
+ end
+ end
+
+ if sym.to_sym == :respond_to?
+ mock_respond_to? obj, compare
+ else
+ SpecExpectation.fail_with("Mock '#{name_or_inspect obj}': method #{sym}\n",
+ "called with unexpected arguments (#{Array(compare).join(' ')})")
+ end
+ end
+
+ def self.cleanup
+ objects.each do |key, obj|
+ if obj.kind_of? MockIntObject
+ clear_replaced key
+ next
+ end
+
+ replaced = key.first
+ sym = key.last
+ meta = obj.singleton_class
+
+ if mock_respond_to? obj, replaced, true
+ meta.__send__ :alias_method, sym, replaced
+ meta.__send__ :remove_method, replaced
+ else
+ meta.__send__ :remove_method, sym
+ end
+
+ clear_replaced key
+ end
+ ensure
+ reset
+ end
+end
diff --git a/spec/mspec/lib/mspec/mocks/object.rb b/spec/mspec/lib/mspec/mocks/object.rb
new file mode 100644
index 0000000000..f4652a4671
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks/object.rb
@@ -0,0 +1,28 @@
+require 'mspec/mocks/proxy'
+
+class Object
+ def stub!(sym)
+ Mock.install_method self, sym, :stub
+ end
+
+ def should_receive(sym)
+ Mock.install_method self, sym
+ end
+
+ def should_not_receive(sym)
+ proxy = Mock.install_method self, sym
+ proxy.exactly(0).times
+ end
+
+ def mock(name, options={})
+ MockObject.new name, options
+ end
+
+ def mock_int(val)
+ MockIntObject.new(val)
+ end
+
+ def mock_numeric(name, options={})
+ NumericMockObject.new name, options
+ end
+end
diff --git a/spec/mspec/lib/mspec/mocks/proxy.rb b/spec/mspec/lib/mspec/mocks/proxy.rb
new file mode 100644
index 0000000000..f5acc89d62
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks/proxy.rb
@@ -0,0 +1,186 @@
+class MockObject
+ def initialize(name, options={})
+ @name = name
+ @null = options[:null_object]
+ end
+
+ def method_missing(sym, *args, &block)
+ @null ? self : super
+ end
+ private :method_missing
+end
+
+class NumericMockObject < Numeric
+ def initialize(name, options={})
+ @name = name
+ @null = options[:null_object]
+ end
+
+ def method_missing(sym, *args, &block)
+ @null ? self : super
+ end
+
+ def singleton_method_added(val)
+ end
+end
+
+class MockIntObject
+ def initialize(val)
+ @value = val
+ @calls = 0
+
+ key = [self, :to_int]
+
+ Mock.objects[key] = self
+ Mock.mocks[key] << self
+ end
+
+ attr_reader :calls
+
+ def to_int
+ @calls += 1
+ @value.to_int
+ end
+
+ def count
+ [:at_least, 1]
+ end
+end
+
+class MockProxy
+ attr_reader :raising, :yielding
+
+ def initialize(type=nil)
+ @multiple_returns = nil
+ @returning = nil
+ @raising = nil
+ @yielding = []
+ @arguments = :any_args
+ @type = type || :mock
+ end
+
+ def mock?
+ @type == :mock
+ end
+
+ def stub?
+ @type == :stub
+ end
+
+ def count
+ @count ||= mock? ? [:exactly, 1] : [:any_number_of_times, 0]
+ end
+
+ def arguments
+ @arguments
+ end
+
+ def returning
+ if @multiple_returns
+ if @returning.size == 1
+ @multiple_returns = false
+ return @returning = @returning.shift
+ end
+ return @returning.shift
+ end
+ @returning
+ end
+
+ def times
+ self
+ end
+
+ def calls
+ @calls ||= 0
+ end
+
+ def called
+ @calls = calls + 1
+ end
+
+ def exactly(n)
+ @count = [:exactly, n_times(n)]
+ self
+ end
+
+ def at_least(n)
+ @count = [:at_least, n_times(n)]
+ self
+ end
+
+ def at_most(n)
+ @count = [:at_most, n_times(n)]
+ self
+ end
+
+ def once
+ exactly 1
+ end
+
+ def twice
+ exactly 2
+ end
+
+ def any_number_of_times
+ @count = [:any_number_of_times, 0]
+ self
+ end
+
+ def with(*args)
+ raise ArgumentError, "you must specify the expected arguments" if args.empty?
+ if args.length == 1
+ @arguments = args.first
+ else
+ @arguments = args
+ end
+ self
+ end
+
+ def and_return(*args)
+ case args.size
+ when 0
+ @returning = nil
+ when 1
+ @returning = args[0]
+ else
+ @multiple_returns = true
+ @returning = args
+ count[1] = args.size if count[1] < args.size
+ end
+ self
+ end
+
+ def and_raise(exception)
+ if exception.kind_of? String
+ @raising = RuntimeError.new exception
+ else
+ @raising = exception
+ end
+ end
+
+ def raising?
+ @raising != nil
+ end
+
+ def and_yield(*args)
+ @yielding << args
+ self
+ end
+
+ def yielding?
+ !@yielding.empty?
+ end
+
+ private
+
+ def n_times(n)
+ case n
+ when :once
+ 1
+ when :twice
+ 2
+ else
+ Integer n
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner.rb b/spec/mspec/lib/mspec/runner.rb
new file mode 100644
index 0000000000..df57b9f69b
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner.rb
@@ -0,0 +1,12 @@
+require 'mspec/mocks'
+require 'mspec/runner/mspec'
+require 'mspec/runner/context'
+require 'mspec/runner/evaluate'
+require 'mspec/runner/example'
+require 'mspec/runner/exception'
+require 'mspec/runner/object'
+require 'mspec/runner/formatters'
+require 'mspec/runner/actions'
+require 'mspec/runner/filters'
+require 'mspec/runner/shared'
+require 'mspec/runner/tag'
diff --git a/spec/mspec/lib/mspec/runner/actions.rb b/spec/mspec/lib/mspec/runner/actions.rb
new file mode 100644
index 0000000000..0a5a05fbd1
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions.rb
@@ -0,0 +1,6 @@
+require 'mspec/runner/actions/tally'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/tag'
+require 'mspec/runner/actions/taglist'
+require 'mspec/runner/actions/tagpurge'
diff --git a/spec/mspec/lib/mspec/runner/actions/filter.rb b/spec/mspec/lib/mspec/runner/actions/filter.rb
new file mode 100644
index 0000000000..35899c8dc8
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/filter.rb
@@ -0,0 +1,40 @@
+require 'mspec/runner/filters/match'
+
+# ActionFilter is a base class for actions that are triggered by
+# specs that match the filter. The filter may be specified by
+# strings that match spec descriptions or by tags for strings
+# that match spec descriptions.
+#
+# Unlike TagFilter and RegexpFilter, ActionFilter instances do
+# not affect the specs that are run. The filter is only used to
+# trigger the action.
+
+class ActionFilter
+ def initialize(tags=nil, descs=nil)
+ @tags = Array(tags)
+ descs = Array(descs)
+ @sfilter = descs.empty? ? nil : MatchFilter.new(nil, *descs)
+ @tfilter = nil
+ end
+
+ def ===(string)
+ @sfilter === string or @tfilter === string
+ end
+
+ def load
+ return if @tags.empty?
+
+ desc = MSpec.read_tags(@tags).map { |t| t.description }
+ return if desc.empty?
+
+ @tfilter = MatchFilter.new(nil, *desc)
+ end
+
+ def register
+ MSpec.register :load, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/leakchecker.rb b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
new file mode 100644
index 0000000000..e947cda9ff
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
@@ -0,0 +1,301 @@
+# Adapted from ruby's test/lib/leakchecker.rb.
+# Ruby's 2-clause BSDL follows.
+
+# Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+class LeakChecker
+ def initialize
+ @fd_info = find_fds
+ @tempfile_info = find_tempfiles
+ @thread_info = find_threads
+ @env_info = find_env
+ @argv_info = find_argv
+ @encoding_info = find_encodings
+ end
+
+ def check(test_name)
+ @no_leaks = true
+ leaks = [
+ check_fd_leak(test_name),
+ check_tempfile_leak(test_name),
+ check_thread_leak(test_name),
+ check_process_leak(test_name),
+ check_env(test_name),
+ check_argv(test_name),
+ check_encodings(test_name)
+ ]
+ GC.start if leaks.any?
+ return leaks.none?
+ end
+
+ private
+ def find_fds
+ fd_dir = "/proc/self/fd"
+ if File.directory?(fd_dir)
+ fds = Dir.open(fd_dir) {|d|
+ a = d.grep(/\A\d+\z/, &:to_i)
+ if d.respond_to? :fileno
+ a -= [d.fileno]
+ end
+ a
+ }
+ fds.sort
+ else
+ []
+ end
+ end
+
+ def check_fd_leak(test_name)
+ leaked = false
+ live1 = @fd_info
+ if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
+ m[:close]
+ end
+ live2 = find_fds
+ fd_closed = live1 - live2
+ if !fd_closed.empty?
+ fd_closed.each {|fd|
+ puts "Closed file descriptor: #{test_name}: #{fd}"
+ }
+ end
+ fd_leaked = live2 - live1
+ if !fd_leaked.empty?
+ leaked = true
+ h = {}
+ ObjectSpace.each_object(IO) {|io|
+ inspect = io.inspect
+ begin
+ autoclose = io.autoclose?
+ fd = io.fileno
+ rescue IOError # closed IO object
+ next
+ end
+ (h[fd] ||= []) << [io, autoclose, inspect]
+ }
+ fd_leaked.each {|fd|
+ str = ''
+ if h[fd]
+ str << ' :'
+ h[fd].map {|io, autoclose, inspect|
+ s = ' ' + inspect
+ s << "(not-autoclose)" if !autoclose
+ s
+ }.sort.each {|s|
+ str << s
+ }
+ end
+ puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
+ }
+ #system("lsof -p #$$") if !fd_leaked.empty?
+ h.each {|fd, list|
+ next if list.length <= 1
+ if 1 < list.count {|io, autoclose, inspect| autoclose }
+ str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
+ puts "Multiple autoclose IO object for a file descriptor:#{str}"
+ end
+ }
+ end
+ @fd_info = live2
+ return leaked
+ end
+
+ def extend_tempfile_counter
+ return if defined? LeakChecker::TempfileCounter
+ m = Module.new {
+ @count = 0
+ class << self
+ attr_accessor :count
+ end
+
+ def new(data)
+ LeakChecker::TempfileCounter.count += 1
+ super(data)
+ end
+ }
+ LeakChecker.const_set(:TempfileCounter, m)
+
+ class << Tempfile::Remover
+ prepend LeakChecker::TempfileCounter
+ end
+ end
+
+ def find_tempfiles(prev_count=-1)
+ return [prev_count, []] unless defined? Tempfile
+ extend_tempfile_counter
+ count = TempfileCounter.count
+ if prev_count == count
+ [prev_count, []]
+ else
+ tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t| t.path }
+ [count, tempfiles]
+ end
+ end
+
+ def check_tempfile_leak(test_name)
+ return false unless defined? Tempfile
+ count1, initial_tempfiles = @tempfile_info
+ count2, current_tempfiles = find_tempfiles(count1)
+ leaked = false
+ tempfiles_leaked = current_tempfiles - initial_tempfiles
+ if !tempfiles_leaked.empty?
+ leaked = true
+ list = tempfiles_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Leaked tempfile: #{test_name}: #{str}"
+ }
+ tempfiles_leaked.each {|t| t.close! }
+ end
+ @tempfile_info = [count2, initial_tempfiles]
+ return leaked
+ end
+
+ def find_threads
+ Thread.list.find_all {|t|
+ t != Thread.current && t.alive?
+ }
+ end
+
+ def check_thread_leak(test_name)
+ live1 = @thread_info
+ live2 = find_threads
+ thread_finished = live1 - live2
+ leaked = false
+ if !thread_finished.empty?
+ list = thread_finished.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Finished thread: #{test_name}: #{str}"
+ }
+ end
+ thread_leaked = live2 - live1
+ if !thread_leaked.empty?
+ leaked = true
+ list = thread_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Leaked thread: #{test_name}: #{str}"
+ }
+ end
+ @thread_info = live2
+ return leaked
+ end
+
+ def check_process_leak(test_name)
+ subprocesses_leaked = Process.waitall
+ subprocesses_leaked.each { |pid, status|
+ puts "Leaked subprocess: #{pid}: #{status}"
+ }
+ return !subprocesses_leaked.empty?
+ end
+
+ def find_env
+ ENV.to_h
+ end
+
+ def check_env(test_name)
+ old_env = @env_info
+ new_env = find_env
+ return false if old_env == new_env
+ (old_env.keys | new_env.keys).sort.each {|k|
+ if old_env.has_key?(k)
+ if new_env.has_key?(k)
+ if old_env[k] != new_env[k]
+ puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
+ end
+ else
+ puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
+ end
+ else
+ if new_env.has_key?(k)
+ puts "Environment variable changed: #{test_name} : #{k.inspect} added"
+ else
+ flunk "unreachable"
+ end
+ end
+ }
+ @env_info = new_env
+ return true
+ end
+
+ def find_argv
+ ARGV.map { |e| e.dup }
+ end
+
+ def check_argv(test_name)
+ old_argv = @argv_info
+ new_argv = find_argv
+ leaked = false
+ if new_argv != old_argv
+ puts "ARGV changed: #{test_name} : #{old_argv.inspect} to #{new_argv.inspect}"
+ @argv_info = new_argv
+ leaked = true
+ end
+ return leaked
+ end
+
+ def find_encodings
+ [Encoding.default_internal, Encoding.default_external]
+ end
+
+ def check_encodings(test_name)
+ old_internal, old_external = @encoding_info
+ new_internal, new_external = find_encodings
+ leaked = false
+ if new_internal != old_internal
+ leaked = true
+ puts "Encoding.default_internal changed: #{test_name} : #{old_internal} to #{new_internal}"
+ end
+ if new_external != old_external
+ leaked = true
+ puts "Encoding.default_external changed: #{test_name} : #{old_external} to #{new_external}"
+ end
+ @encoding_info = [new_internal, new_external]
+ return leaked
+ end
+
+ def puts(*args)
+ if @no_leaks
+ @no_leaks = false
+ print "\n"
+ end
+ super(*args)
+ end
+end
+
+class LeakCheckerAction
+ def register
+ MSpec.register :start, self
+ MSpec.register :after, self
+ end
+
+ def start
+ @checker = LeakChecker.new
+ end
+
+ def after(state)
+ unless @checker.check(state.description)
+ if state.example
+ puts state.example.source_location.join(':')
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tag.rb b/spec/mspec/lib/mspec/runner/actions/tag.rb
new file mode 100644
index 0000000000..760152b2a3
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tag.rb
@@ -0,0 +1,133 @@
+require 'mspec/runner/actions/filter'
+
+# TagAction - Write tagged spec description string to a
+# tag file associated with each spec file.
+#
+# The action is triggered by specs whose descriptions
+# match the filter created with 'tags' and/or 'desc'
+#
+# The action fires in the :after event, after the spec
+# had been run. The action fires if the outcome of
+# running the spec matches 'outcome'.
+#
+# The arguments are:
+#
+# action: :add, :del
+# outcome: :pass, :fail, :all
+# tag: the tag to create/delete
+# comment: the comment to create
+# tags: zero or more tags to get matching
+# spec description strings from
+# desc: zero or more strings to match the
+# spec description strings
+
+class TagAction < ActionFilter
+ def initialize(action, outcome, tag, comment, tags=nil, descs=nil)
+ super tags, descs
+ @action = action
+ @outcome = outcome
+ @tag = tag
+ @comment = comment
+ @report = []
+ @exception = false
+ end
+
+ # Returns true if there are no _tag_ or _description_ filters. This
+ # means that a TagAction matches any example by default. Otherwise,
+ # returns true if either the _tag_ or the _description_ filter
+ # matches +string+.
+ def ===(string)
+ return true unless @sfilter or @tfilter
+ @sfilter === string or @tfilter === string
+ end
+
+ # Callback for the MSpec :before event. Resets the +#exception?+
+ # flag to false.
+ def before(state)
+ @exception = false
+ end
+
+ # Callback for the MSpec :exception event. Sets the +#exception?+
+ # flag to true.
+ def exception(exception)
+ @exception = true
+ end
+
+ # Callback for the MSpec :after event. Performs the tag action
+ # depending on the type of action and the outcome of evaluating
+ # the example. See +TagAction+ for a description of the actions.
+ def after(state)
+ if self === state.description and outcome?
+ tag = SpecTag.new
+ tag.tag = @tag
+ tag.comment = @comment
+ tag.description = state.description
+
+ case @action
+ when :add
+ changed = MSpec.write_tag tag
+ when :del
+ changed = MSpec.delete_tag tag
+ end
+
+ @report << state.description if changed
+ end
+ end
+
+ # Returns true if the result of evaluating the example matches
+ # the _outcome_ registered for this tag action. See +TagAction+
+ # for a description of the _outcome_ types.
+ def outcome?
+ @outcome == :all or
+ (@outcome == :pass and not exception?) or
+ (@outcome == :fail and exception?)
+ end
+
+ # Returns true if an exception was raised while evaluating the
+ # current example.
+ def exception?
+ @exception
+ end
+
+ def report
+ @report.join("\n") + "\n"
+ end
+ private :report
+
+ # Callback for the MSpec :finish event. Prints the actions
+ # performed while evaluating the examples.
+ def finish
+ case @action
+ when :add
+ if @report.empty?
+ print "\nTagAction: no specs were tagged with '#{@tag}'\n"
+ else
+ print "\nTagAction: specs tagged with '#{@tag}':\n\n"
+ print report
+ end
+ when :del
+ if @report.empty?
+ print "\nTagAction: no tags '#{@tag}' were deleted\n"
+ else
+ print "\nTagAction: tag '#{@tag}' deleted for specs:\n\n"
+ print report
+ end
+ end
+ end
+
+ def register
+ super
+ MSpec.register :before, self
+ MSpec.register :exception, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ def unregister
+ super
+ MSpec.unregister :before, self
+ MSpec.unregister :exception, self
+ MSpec.unregister :after, self
+ MSpec.unregister :finish, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/taglist.rb b/spec/mspec/lib/mspec/runner/actions/taglist.rb
new file mode 100644
index 0000000000..c1aba53794
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/taglist.rb
@@ -0,0 +1,56 @@
+require 'mspec/runner/actions/filter'
+
+# TagListAction - prints out the descriptions for any specs
+# tagged with +tags+. If +tags+ is an empty list, prints out
+# descriptions for any specs that are tagged.
+class TagListAction
+ def initialize(tags=nil)
+ @tags = tags.nil? || tags.empty? ? nil : Array(tags)
+ @filter = nil
+ end
+
+ # Returns true. This enables us to match any tag when loading
+ # tags from the file.
+ def include?(arg)
+ true
+ end
+
+ # Returns true if any tagged descriptions matches +string+.
+ def ===(string)
+ @filter === string
+ end
+
+ # Prints a banner about matching tagged specs.
+ def start
+ if @tags
+ print "\nListing specs tagged with #{@tags.map { |t| "'#{t}'" }.join(", ") }\n\n"
+ else
+ print "\nListing all tagged specs\n\n"
+ end
+ end
+
+ # Creates a MatchFilter for specific tags or for all tags.
+ def load
+ @filter = nil
+ desc = MSpec.read_tags(@tags || self).map { |t| t.description }
+ @filter = MatchFilter.new(nil, *desc) unless desc.empty?
+ end
+
+ # Prints the spec description if it matches the filter.
+ def after(state)
+ return unless self === state.description
+ print state.description, "\n"
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :load, self
+ MSpec.register :after, self
+ end
+
+ def unregister
+ MSpec.unregister :start, self
+ MSpec.unregister :load, self
+ MSpec.unregister :after, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tagpurge.rb b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb
new file mode 100644
index 0000000000..f4587de6bc
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb
@@ -0,0 +1,56 @@
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/taglist'
+
+# TagPurgeAction - removes all tags not matching any spec
+# descriptions.
+class TagPurgeAction < TagListAction
+ attr_reader :matching
+
+ def initialize
+ @matching = []
+ @filter = nil
+ @tags = nil
+ end
+
+ # Prints a banner about purging tags.
+ def start
+ print "\nRemoving tags not matching any specs\n\n"
+ end
+
+ # Creates a MatchFilter for all tags.
+ def load
+ @filter = nil
+ @tags = MSpec.read_tags self
+ desc = @tags.map { |t| t.description }
+ @filter = MatchFilter.new(nil, *desc) unless desc.empty?
+ end
+
+ # Saves any matching tags
+ def after(state)
+ @matching << state.description if self === state.description
+ end
+
+ # Rewrites any matching tags. Prints non-matching tags.
+ # Deletes the tag file if there were no tags (this cleans
+ # up empty or malformed tag files).
+ def unload
+ if @filter
+ matched = @tags.select { |t| @matching.any? { |s| s == t.description } }
+ MSpec.write_tags matched
+
+ (@tags - matched).each { |t| print t.description, "\n" }
+ else
+ MSpec.delete_tags
+ end
+ end
+
+ def register
+ super
+ MSpec.register :unload, self
+ end
+
+ def unregister
+ super
+ MSpec.unregister :unload, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tally.rb b/spec/mspec/lib/mspec/runner/actions/tally.rb
new file mode 100644
index 0000000000..33f937293c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tally.rb
@@ -0,0 +1,133 @@
+class Tally
+ attr_accessor :files, :examples, :expectations, :failures, :errors, :guards, :tagged
+
+ def initialize
+ @files = @examples = @expectations = @failures = @errors = @guards = @tagged = 0
+ end
+
+ def files!(add=1)
+ @files += add
+ end
+
+ def examples!(add=1)
+ @examples += add
+ end
+
+ def expectations!(add=1)
+ @expectations += add
+ end
+
+ def failures!(add=1)
+ @failures += add
+ end
+
+ def errors!(add=1)
+ @errors += add
+ end
+
+ def guards!(add=1)
+ @guards += add
+ end
+
+ def tagged!(add=1)
+ @tagged += add
+ end
+
+ def file
+ pluralize files, "file"
+ end
+
+ def example
+ pluralize examples, "example"
+ end
+
+ def expectation
+ pluralize expectations, "expectation"
+ end
+
+ def failure
+ pluralize failures, "failure"
+ end
+
+ def error
+ pluralize errors, "error"
+ end
+
+ def guard
+ pluralize guards, "guard"
+ end
+
+ def tag
+ "#{tagged} tagged"
+ end
+
+ def format
+ results = [ file, example, expectation, failure, error, tag ]
+ if [:report, :report_on, :verify].any? { |m| MSpec.mode? m }
+ results << guard
+ end
+ results.join(", ")
+ end
+
+ alias_method :to_s, :format
+
+ def pluralize(count, singular)
+ "#{count} #{singular}#{'s' unless count == 1}"
+ end
+ private :pluralize
+end
+
+class TallyAction
+ attr_reader :counter
+
+ def initialize
+ @counter = Tally.new
+ end
+
+ def register
+ MSpec.register :load, self
+ MSpec.register :exception, self
+ MSpec.register :example, self
+ MSpec.register :tagged, self
+ MSpec.register :expectation, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ MSpec.unregister :exception, self
+ MSpec.unregister :example, self
+ MSpec.unregister :tagged, self
+ MSpec.unregister :expectation, self
+ end
+
+ def load
+ @counter.files!
+ end
+
+ # Callback for the MSpec :expectation event. Increments the
+ # tally of expectations (e.g. #should, #should_receive, etc.).
+ def expectation(state)
+ @counter.expectations!
+ end
+
+ # Callback for the MSpec :exception event. Increments the
+ # tally of errors and failures.
+ def exception(exception)
+ exception.failure? ? @counter.failures! : @counter.errors!
+ end
+
+ # Callback for the MSpec :example event. Increments the tally
+ # of examples.
+ def example(state, block)
+ @counter.examples!
+ end
+
+ def tagged(state)
+ @counter.examples!
+ @counter.tagged!
+ end
+
+ def format
+ @counter.format
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/timer.rb b/spec/mspec/lib/mspec/runner/actions/timer.rb
new file mode 100644
index 0000000000..e7ebfebe0d
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/timer.rb
@@ -0,0 +1,22 @@
+class TimerAction
+ def register
+ MSpec.register :start, self
+ MSpec.register :finish, self
+ end
+
+ def start
+ @start = Time.now
+ end
+
+ def finish
+ @stop = Time.now
+ end
+
+ def elapsed
+ @stop - @start
+ end
+
+ def format
+ "Finished in %f seconds" % elapsed
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/context.rb b/spec/mspec/lib/mspec/runner/context.rb
new file mode 100644
index 0000000000..2b470f226a
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/context.rb
@@ -0,0 +1,239 @@
+# Holds the state of the +describe+ block that is being
+# evaluated. Every example (i.e. +it+ block) is evaluated
+# in a context, which may include state set up in <tt>before
+# :each</tt> or <tt>before :all</tt> blocks.
+#
+#--
+# A note on naming: this is named _ContextState_ rather
+# than _DescribeState_ because +describe+ is the keyword
+# in the DSL for refering to the context in which an example
+# is evaluated, just as +it+ refers to the example itself.
+#++
+class ContextState
+ attr_reader :state, :parent, :parents, :children, :examples, :to_s
+
+ def initialize(mod, options=nil)
+ @to_s = mod.to_s
+ if options.is_a? Hash
+ @options = options
+ else
+ @to_s += "#{".:#".include?(options[0,1]) ? "" : " "}#{options}" if options
+ @options = { }
+ end
+ @options[:shared] ||= false
+
+ @parsed = false
+ @before = { :all => [], :each => [] }
+ @after = { :all => [], :each => [] }
+ @pre = {}
+ @post = {}
+ @examples = []
+ @parent = nil
+ @parents = [self]
+ @children = []
+
+ @mock_verify = Proc.new { Mock.verify_count }
+ @mock_cleanup = Proc.new { Mock.cleanup }
+ @expectation_missing = Proc.new { raise SpecExpectationNotFoundError }
+ end
+
+ # Remove caching when a ContextState is dup'd for shared specs.
+ def initialize_copy(other)
+ @pre = {}
+ @post = {}
+ end
+
+ # Returns true if this is a shared +ContextState+. Essentially, when
+ # created with: describe "Something", :shared => true { ... }
+ def shared?
+ return @options[:shared]
+ end
+
+ # Set the parent (enclosing) +ContextState+ for this state. Creates
+ # the +parents+ list.
+ def parent=(parent)
+ @description = nil
+
+ if shared?
+ @parent = nil
+ else
+ @parent = parent
+ parent.child self if parent
+
+ @parents = [self]
+ state = parent
+ while state
+ @parents.unshift state
+ state = state.parent
+ end
+ end
+ end
+
+ # Add the ContextState instance +child+ to the list of nested
+ # describe blocks.
+ def child(child)
+ @children << child
+ end
+
+ # Adds a nested ContextState in a shared ContextState to a containing
+ # ContextState.
+ #
+ # Normal adoption is from the parent's perspective. But adopt is a good
+ # verb and it's reasonable for the child to adopt the parent as well. In
+ # this case, manipulating state from inside the child avoids needlessly
+ # exposing the state to manipulate it externally in the dup. (See
+ # #it_should_behave_like)
+ def adopt(parent)
+ self.parent = parent
+
+ @examples = @examples.map do |example|
+ example = example.dup
+ example.context = self
+ example
+ end
+
+ children = @children
+ @children = []
+
+ children.each { |child| child.dup.adopt self }
+ end
+
+ # Returns a list of all before(+what+) blocks from self and any parents.
+ def pre(what)
+ @pre[what] ||= parents.inject([]) { |l, s| l.push(*s.before(what)) }
+ end
+
+ # Returns a list of all after(+what+) blocks from self and any parents.
+ # The list is in reverse order. In other words, the blocks defined in
+ # inner describes are in the list before those defined in outer describes,
+ # and in a particular describe block those defined later are in the list
+ # before those defined earlier.
+ def post(what)
+ @post[what] ||= parents.inject([]) { |l, s| l.unshift(*s.after(what)) }
+ end
+
+ # Records before(:each) and before(:all) blocks.
+ def before(what, &block)
+ return if MSpec.guarded?
+ block ? @before[what].push(block) : @before[what]
+ end
+
+ # Records after(:each) and after(:all) blocks.
+ def after(what, &block)
+ return if MSpec.guarded?
+ block ? @after[what].unshift(block) : @after[what]
+ end
+
+ # Creates an ExampleState instance for the block and stores it
+ # in a list of examples to evaluate unless the example is filtered.
+ def it(desc, &block)
+ example = ExampleState.new(self, desc, block)
+ MSpec.actions :add, example
+ return if MSpec.guarded?
+ @examples << example
+ end
+
+ # Evaluates the block and resets the toplevel +ContextState+ to #parent.
+ def describe(&block)
+ @parsed = protect @to_s, block, false
+ MSpec.register_current parent
+ MSpec.register_shared self if shared?
+ end
+
+ # Returns a description string generated from self and all parents
+ def description
+ @description ||= parents.map { |p| p.to_s }.compact.join(" ")
+ end
+
+ # Injects the before/after blocks and examples from the shared
+ # describe block into this +ContextState+ instance.
+ def it_should_behave_like(desc)
+ return if MSpec.guarded?
+
+ unless state = MSpec.retrieve_shared(desc)
+ raise Exception, "Unable to find shared 'describe' for #{desc}"
+ end
+
+ state.before(:all).each { |b| before :all, &b }
+ state.before(:each).each { |b| before :each, &b }
+ state.after(:each).each { |b| after :each, &b }
+ state.after(:all).each { |b| after :all, &b }
+
+ state.examples.each do |example|
+ example = example.dup
+ example.context = self
+ @examples << example
+ end
+
+ state.children.each do |child|
+ child.dup.adopt self
+ end
+ end
+
+ # Evaluates each block in +blocks+ using the +MSpec.protect+ method
+ # so that exceptions are handled and tallied. Returns true and does
+ # NOT evaluate any blocks if +check+ is true and
+ # <tt>MSpec.mode?(:pretend)</tt> is true.
+ def protect(what, blocks, check=true)
+ return true if check and MSpec.mode? :pretend
+ Array(blocks).all? { |block| MSpec.protect what, &block }
+ end
+
+ # Removes filtered examples. Returns true if there are examples
+ # left to evaluate.
+ def filter_examples
+ filtered, @examples = @examples.partition do |ex|
+ ex.filtered?
+ end
+
+ filtered.each do |ex|
+ MSpec.actions :tagged, ex
+ end
+
+ !@examples.empty?
+ end
+
+ # Evaluates the examples in a +ContextState+. Invokes the MSpec events
+ # for :enter, :before, :after, :leave.
+ def process
+ MSpec.register_current self
+
+ if @parsed and filter_examples
+ MSpec.shuffle @examples if MSpec.randomize?
+ MSpec.actions :enter, description
+
+ if protect "before :all", pre(:all)
+ @examples.each do |state|
+ MSpec.repeat do
+ @state = state
+ example = state.example
+ MSpec.actions :before, state
+
+ if protect "before :each", pre(:each)
+ MSpec.clear_expectations
+ if example
+ passed = protect nil, example
+ MSpec.actions :example, state, example
+ protect nil, @expectation_missing unless MSpec.expectation? or !passed
+ end
+ end
+ protect "after :each", post(:each)
+ protect "Mock.verify_count", @mock_verify
+
+ protect "Mock.cleanup", @mock_cleanup
+ MSpec.actions :after, state
+ @state = nil
+ end
+ end
+ protect "after :all", post(:all)
+ else
+ protect "Mock.cleanup", @mock_cleanup
+ end
+
+ MSpec.actions :leave
+ end
+
+ MSpec.register_current nil
+ children.each { |child| child.process }
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/evaluate.rb b/spec/mspec/lib/mspec/runner/evaluate.rb
new file mode 100644
index 0000000000..fded84421f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/evaluate.rb
@@ -0,0 +1,54 @@
+class SpecEvaluate
+ def self.desc=(desc)
+ @desc = desc
+ end
+
+ def self.desc
+ @desc ||= "evaluates "
+ end
+
+ def initialize(ruby, desc)
+ @ruby = ruby.rstrip
+ @desc = desc || self.class.desc
+ end
+
+ # Formats the Ruby source code for reabable output in the -fs formatter
+ # option. If the source contains no newline characters, wraps the source in
+ # single quotes to set if off from the rest of the description string. If
+ # the source does contain newline characters, sets the indent level to four
+ # characters.
+ def format(ruby, newline=true)
+ if ruby.include?("\n")
+ lines = ruby.each_line.to_a
+ if /( *)/ =~ lines.first
+ if $1.size > 4
+ dedent = $1.size - 4
+ ruby = lines.map { |l| l[dedent..-1] }.join
+ else
+ indent = " " * (4 - $1.size)
+ ruby = lines.map { |l| "#{indent}#{l}" }.join
+ end
+ end
+ "\n#{ruby}"
+ else
+ "'#{ruby.lstrip}'"
+ end
+ end
+
+ def define(&block)
+ ruby = @ruby
+ desc = @desc
+ evaluator = self
+
+ specify "#{desc} #{format ruby}" do
+ evaluator.instance_eval(ruby)
+ evaluator.instance_eval(&block)
+ end
+ end
+end
+
+class Object
+ def evaluate(str, desc=nil, &block)
+ SpecEvaluate.new(str, desc).define(&block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/example.rb b/spec/mspec/lib/mspec/runner/example.rb
new file mode 100644
index 0000000000..19eb29b079
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/example.rb
@@ -0,0 +1,34 @@
+require 'mspec/runner/mspec'
+
+# Holds some of the state of the example (i.e. +it+ block) that is
+# being evaluated. See also +ContextState+.
+class ExampleState
+ attr_reader :context, :it, :example
+
+ def initialize(context, it, example=nil)
+ @context = context
+ @it = it
+ @example = example
+ end
+
+ def context=(context)
+ @description = nil
+ @context = context
+ end
+
+ def describe
+ @context.description
+ end
+
+ def description
+ @description ||= "#{describe} #{@it}"
+ end
+
+ def filtered?
+ incl = MSpec.retrieve(:include) || []
+ excl = MSpec.retrieve(:exclude) || []
+ included = incl.empty? || incl.any? { |f| f === description }
+ included &&= excl.empty? || !excl.any? { |f| f === description }
+ !included
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/exception.rb b/spec/mspec/lib/mspec/runner/exception.rb
new file mode 100644
index 0000000000..0d9bb43105
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/exception.rb
@@ -0,0 +1,43 @@
+# Initialize $MSPEC_DEBUG
+$MSPEC_DEBUG ||= false
+
+class ExceptionState
+ attr_reader :description, :describe, :it, :exception
+
+ def initialize(state, location, exception)
+ @exception = exception
+
+ @description = location ? "An exception occurred during: #{location}" : ""
+ if state
+ @description += "\n" unless @description.empty?
+ @description += state.description
+ @describe = state.describe
+ @it = state.it
+ else
+ @describe = @it = ""
+ end
+ end
+
+ def failure?
+ [SpecExpectationNotMetError, SpecExpectationNotFoundError].any? { |e| @exception.is_a? e }
+ end
+
+ def message
+ if @exception.message.empty?
+ "<No message>"
+ elsif @exception.class == SpecExpectationNotMetError ||
+ @exception.class == SpecExpectationNotFoundError
+ @exception.message
+ else
+ "#{@exception.class}: #{@exception.message}"
+ end
+ end
+
+ def backtrace
+ @backtrace_filter ||= MSpecScript.config[:backtrace_filter]
+
+ bt = @exception.backtrace || []
+
+ bt.select { |line| $MSPEC_DEBUG or @backtrace_filter !~ line }.join("\n")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters.rb b/spec/mspec/lib/mspec/runner/filters.rb
new file mode 100644
index 0000000000..d0420faca6
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters.rb
@@ -0,0 +1,4 @@
+require 'mspec/runner/filters/match'
+require 'mspec/runner/filters/regexp'
+require 'mspec/runner/filters/tag'
+require 'mspec/runner/filters/profile'
diff --git a/spec/mspec/lib/mspec/runner/filters/match.rb b/spec/mspec/lib/mspec/runner/filters/match.rb
new file mode 100644
index 0000000000..539fd02d01
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/match.rb
@@ -0,0 +1,18 @@
+class MatchFilter
+ def initialize(what, *strings)
+ @what = what
+ @strings = strings
+ end
+
+ def ===(string)
+ @strings.any? { |s| string.include?(s) }
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/profile.rb b/spec/mspec/lib/mspec/runner/filters/profile.rb
new file mode 100644
index 0000000000..a59722c451
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/profile.rb
@@ -0,0 +1,54 @@
+class ProfileFilter
+ def initialize(what, *files)
+ @what = what
+ @methods = load(*files)
+ @pattern = /([^ .#]+[.#])([^ ]+)/
+ end
+
+ def find(name)
+ return name if File.exist?(File.expand_path(name))
+
+ ["spec/profiles", "spec", "profiles", "."].each do |dir|
+ file = File.join dir, name
+ return file if File.exist? file
+ end
+ end
+
+ def parse(file)
+ pattern = /(\S+):\s*/
+ key = ""
+ file.inject(Hash.new { |h,k| h[k] = [] }) do |hash, line|
+ line.chomp!
+ if line[0,2] == "- "
+ hash[key] << line[2..-1].gsub(/[ '"]/, "")
+ elsif m = pattern.match(line)
+ key = m[1]
+ end
+ hash
+ end
+ end
+
+ def load(*files)
+ files.inject({}) do |hash, file|
+ next hash unless name = find(file)
+
+ File.open name, "r" do |f|
+ hash.merge parse(f)
+ end
+ end
+ end
+
+ def ===(string)
+ return false unless m = @pattern.match(string)
+ return false unless l = @methods[m[1]]
+ l.include? m[2]
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/regexp.rb b/spec/mspec/lib/mspec/runner/filters/regexp.rb
new file mode 100644
index 0000000000..2bd1448d3f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/regexp.rb
@@ -0,0 +1,7 @@
+require 'mspec/runner/filters/match'
+
+class RegexpFilter < MatchFilter
+ def to_regexp(*strings)
+ strings.map { |str| Regexp.new str }
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/tag.rb b/spec/mspec/lib/mspec/runner/filters/tag.rb
new file mode 100644
index 0000000000..c641c01606
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/tag.rb
@@ -0,0 +1,29 @@
+class TagFilter
+ def initialize(what, *tags)
+ @what = what
+ @tags = tags
+ end
+
+ def load
+ @descriptions = MSpec.read_tags(@tags).map { |t| t.description }
+ MSpec.register @what, self
+ end
+
+ def unload
+ MSpec.unregister @what, self
+ end
+
+ def ===(string)
+ @descriptions.include?(string)
+ end
+
+ def register
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ MSpec.unregister :unload, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters.rb b/spec/mspec/lib/mspec/runner/formatters.rb
new file mode 100644
index 0000000000..d085031a12
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters.rb
@@ -0,0 +1,12 @@
+require 'mspec/runner/formatters/describe'
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/file'
+require 'mspec/runner/formatters/specdoc'
+require 'mspec/runner/formatters/html'
+require 'mspec/runner/formatters/summary'
+require 'mspec/runner/formatters/unit'
+require 'mspec/runner/formatters/spinner'
+require 'mspec/runner/formatters/method'
+require 'mspec/runner/formatters/yaml'
+require 'mspec/runner/formatters/profile'
+require 'mspec/runner/formatters/junit'
diff --git a/spec/mspec/lib/mspec/runner/formatters/describe.rb b/spec/mspec/lib/mspec/runner/formatters/describe.rb
new file mode 100644
index 0000000000..176bd79279
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/describe.rb
@@ -0,0 +1,24 @@
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/actions/tally'
+
+class DescribeFormatter < DottedFormatter
+ # Callback for the MSpec :finish event. Prints a summary of
+ # the number of errors and failures for each +describe+ block.
+ def finish
+ describes = Hash.new { |h,k| h[k] = Tally.new }
+
+ @exceptions.each do |exc|
+ desc = describes[exc.describe]
+ exc.failure? ? desc.failures! : desc.errors!
+ end
+
+ print "\n"
+ describes.each do |d, t|
+ text = d.size > 40 ? "#{d[0,37]}..." : d.ljust(40)
+ print "\n#{text} #{t.failure}, #{t.error}"
+ end
+ print "\n" unless describes.empty?
+
+ print "\n#{@timer.format}\n\n#{@tally.format}\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/dotted.rb b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
new file mode 100644
index 0000000000..61c8e4c27c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
@@ -0,0 +1,117 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/actions/tally'
+require 'mspec/runner/actions/leakchecker' if ENV['CHECK_LEAKS']
+
+class DottedFormatter
+ attr_reader :exceptions, :timer, :tally
+
+ def initialize(out=nil)
+ @exception = @failure = false
+ @exceptions = []
+ @count = 0 # For subclasses
+ if out.nil?
+ @out = $stdout
+ else
+ @out = File.open out, "w"
+ end
+
+ @current_state = nil
+ end
+
+ # Creates the +TimerAction+ and +TallyAction+ instances and
+ # registers them. Registers +self+ for the +:exception+,
+ # +:before+, +:after+, and +:finish+ actions.
+ def register
+ (@timer = TimerAction.new).register
+ (@tally = TallyAction.new).register
+ LeakCheckerAction.new.register if ENV['CHECK_LEAKS']
+ @counter = @tally.counter
+
+ MSpec.register :exception, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ MSpec.register :abort, self
+ end
+
+ def abort
+ if @current_state
+ puts "\naborting example: #{@current_state.description}"
+ end
+ end
+
+ # Returns true if any exception is raised while running
+ # an example. This flag is reset before each example
+ # is evaluated.
+ def exception?
+ @exception
+ end
+
+ # Returns true if all exceptions during the evaluation
+ # of an example are failures rather than errors. See
+ # <tt>ExceptionState#failure</tt>. This flag is reset
+ # before each example is evaluated.
+ def failure?
+ @failure
+ end
+
+ # Callback for the MSpec :before event. Resets the
+ # +#exception?+ and +#failure+ flags.
+ def before(state=nil)
+ @current_state = state
+ @failure = @exception = false
+ end
+
+ # Callback for the MSpec :exception event. Stores the
+ # +ExceptionState+ object to generate the list of backtraces
+ # after all the specs are run. Also updates the internal
+ # +#exception?+ and +#failure?+ flags.
+ def exception(exception)
+ @count += 1
+ @failure = @exception ? @failure && exception.failure? : exception.failure?
+ @exception = true
+ @exceptions << exception
+ end
+
+ # Callback for the MSpec :after event. Prints an indicator
+ # for the result of evaluating this example as follows:
+ # . = No failure or error
+ # F = An SpecExpectationNotMetError was raised
+ # E = Any exception other than SpecExpectationNotMetError
+ def after(state = nil)
+ @current_state = nil
+
+ unless exception?
+ print "."
+ else
+ print failure? ? "F" : "E"
+ end
+ end
+
+ # Callback for the MSpec :finish event. Prints a description
+ # and backtrace for every exception that occurred while
+ # evaluating the examples.
+ def finish
+ print "\n"
+ count = 0
+ @exceptions.each do |exc|
+ count += 1
+ print_exception(exc, count)
+ end
+ print "\n#{@timer.format}\n\n#{@tally.format}\n"
+ end
+
+ def print_exception(exc, count)
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print "\n#{count})\n#{exc.description} #{outcome}\n"
+ print exc.message, "\n"
+ print exc.backtrace, "\n"
+ end
+
+ # A convenience method to allow printing to different outputs.
+ def print(*args)
+ @out.print(*args)
+ @out.flush
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/file.rb b/spec/mspec/lib/mspec/runner/formatters/file.rb
new file mode 100644
index 0000000000..6db72af4ff
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/file.rb
@@ -0,0 +1,19 @@
+require 'mspec/runner/formatters/dotted'
+
+class FileFormatter < DottedFormatter
+ # Unregisters DottedFormatter#before, #after methods and
+ # registers #load, #unload, which perform the same duties
+ # as #before, #after in DottedFormatter.
+ def register
+ super
+
+ MSpec.unregister :before, self
+ MSpec.unregister :after, self
+
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ alias_method :load, :before
+ alias_method :unload, :after
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/html.rb b/spec/mspec/lib/mspec/runner/formatters/html.rb
new file mode 100644
index 0000000000..060d2732f0
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/html.rb
@@ -0,0 +1,81 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class HtmlFormatter < DottedFormatter
+ def register
+ super
+ MSpec.register :start, self
+ MSpec.register :enter, self
+ MSpec.register :leave, self
+ end
+
+ def start
+ print <<-EOH
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>Spec Output For #{RUBY_NAME} (#{RUBY_VERSION})</title>
+<style type="text/css">
+ul {
+ list-style: none;
+}
+.fail {
+ color: red;
+}
+.pass {
+ color: green;
+}
+#details :target {
+ background-color: #ffffe0;
+}
+</style>
+</head>
+<body>
+EOH
+ end
+
+ def enter(describe)
+ print "<div><p>#{describe}</p>\n<ul>\n"
+ end
+
+ def leave
+ print "</ul>\n</div>\n"
+ end
+
+ def exception(exception)
+ super
+ outcome = exception.failure? ? "FAILED" : "ERROR"
+ print %[<li class="fail">- #{exception.it} (<a href="#details-#{@count}">]
+ print %[#{outcome} - #{@count}</a>)</li>\n]
+ end
+
+ def after(state)
+ print %[<li class="pass">- #{state.it}</li>\n] unless exception?
+ end
+
+ def finish
+ success = @exceptions.empty?
+ unless success
+ print "<hr>\n"
+ print %[<ol id="details">]
+ count = 0
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print %[\n<li id="details-#{count += 1}"><p>#{escape(exc.description)} #{outcome}</p>\n<p>]
+ print escape(exc.message)
+ print "</p>\n<pre>\n"
+ print escape(exc.backtrace)
+ print "</pre>\n</li>\n"
+ end
+ print "</ol>\n"
+ end
+ print %[<p>#{@timer.format}</p>\n]
+ print %[<p class="#{success ? "pass" : "fail"}">#{@tally.format}</p>\n]
+ print "</body>\n</html>\n"
+ end
+
+ def escape(string)
+ string.gsub("&", "&nbsp;").gsub("<", "&lt;").gsub(">", "&gt;")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/junit.rb b/spec/mspec/lib/mspec/runner/formatters/junit.rb
new file mode 100644
index 0000000000..647deee7e1
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/junit.rb
@@ -0,0 +1,89 @@
+require 'mspec/expectations/expectations'
+require 'mspec/utils/ruby_name'
+require 'mspec/runner/formatters/yaml'
+
+class JUnitFormatter < YamlFormatter
+ def initialize(out=nil)
+ super
+ @tests = []
+ end
+
+ def after(state = nil)
+ super
+ @tests << {:test => state, :exception => false} unless exception?
+ end
+
+ def exception(exception)
+ super
+ @tests << {:test => exception, :exception => true}
+ end
+
+ def finish
+ switch
+
+ time = @timer.elapsed
+ tests = @tally.counter.examples
+ errors = @tally.counter.errors
+ failures = @tally.counter.failures
+
+ printf <<-XML
+
+<?xml version="1.0" encoding="UTF-8" ?>
+ <testsuites
+ testCount="#{tests}"
+ errorCount="#{errors}"
+ failureCount="#{failures}"
+ timeCount="#{time}" time="#{time}">
+ <testsuite
+ tests="#{tests}"
+ errors="#{errors}"
+ failures="#{failures}"
+ time="#{time}"
+ name="Spec Output For #{::RUBY_NAME} (#{::RUBY_VERSION})">
+ XML
+ @tests.each do |h|
+ description = encode_for_xml h[:test].description
+
+ printf <<-XML, "Spec", description, 0.0
+ <testcase classname="%s" name="%s" time="%f">
+ XML
+ if h[:exception]
+ outcome = h[:test].failure? ? "failure" : "error"
+ message = encode_for_xml h[:test].message
+ backtrace = encode_for_xml h[:test].backtrace
+ print <<-XML
+ <#{outcome} message="error in #{description}" type="#{outcome}">
+ #{message}
+ #{backtrace}
+ </#{outcome}>
+ XML
+ end
+ print <<-XML
+ </testcase>
+ XML
+ end
+
+ print <<-XML
+ </testsuite>
+ </testsuites>
+ XML
+ end
+
+ private
+ LT = "&lt;"
+ GT = "&gt;"
+ QU = "&quot;"
+ AP = "&apos;"
+ AM = "&amp;"
+ TARGET_ENCODING = "ISO-8859-1"
+
+ def encode_for_xml(str)
+ encode_as_latin1(str).gsub("<", LT).gsub(">", GT).
+ gsub('"', QU).gsub("'", AP).gsub("&", AM).
+ tr("\x00-\x08", "?")
+ end
+
+ def encode_as_latin1(str)
+ str.encode(TARGET_ENCODING, :undef => :replace, :invalid => :replace)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/method.rb b/spec/mspec/lib/mspec/runner/formatters/method.rb
new file mode 100644
index 0000000000..ff115193fd
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/method.rb
@@ -0,0 +1,93 @@
+require 'mspec/runner/formatters/dotted'
+
+class MethodFormatter < DottedFormatter
+ attr_accessor :methods
+
+ def initialize(out=nil)
+ super
+ @methods = Hash.new do |h, k|
+ hash = {}
+ hash[:examples] = 0
+ hash[:expectations] = 0
+ hash[:failures] = 0
+ hash[:errors] = 0
+ hash[:exceptions] = []
+ h[k] = hash
+ end
+ end
+
+ # Returns the type of method as a "class", "instance",
+ # or "unknown".
+ def method_type(sep)
+ case sep
+ when '.', '::'
+ "class"
+ when '#'
+ "instance"
+ else
+ "unknown"
+ end
+ end
+
+ # Callback for the MSpec :before event. Parses the
+ # describe string into class and method if possible.
+ # Resets the tallies so the counts are only for this
+ # example.
+ def before(state)
+ super
+
+ # The pattern for a method name is not correctly
+ # restrictive but it is simplistic and useful
+ # for our purpose.
+ /^([A-Za-z_]+\w*)(\.|#|::)([^ ]+)/ =~ state.describe
+ @key = $1 && $2 && $3 ? "#{$1}#{$2}#{$3}" : state.describe
+
+ unless methods.key? @key
+ h = methods[@key]
+ h[:class] = "#{$1}"
+ h[:method] = "#{$3}"
+ h[:type] = method_type $2
+ h[:description] = state.description
+ end
+
+ tally.counter.examples = 0
+ tally.counter.expectations = 0
+ tally.counter.failures = 0
+ tally.counter.errors = 0
+
+ @exceptions = []
+ end
+
+ # Callback for the MSpec :after event. Sets or adds to
+ # tallies for the example block.
+ def after(state)
+ h = methods[@key]
+ h[:examples] += tally.counter.examples
+ h[:expectations] += tally.counter.expectations
+ h[:failures] += tally.counter.failures
+ h[:errors] += tally.counter.errors
+ @exceptions.each do |exc|
+ h[:exceptions] << "#{exc.message}\n#{exc.backtrace}\n"
+ end
+ end
+
+ # Callback for the MSpec :finish event. Prints out the
+ # summary information in YAML format for all the methods.
+ def finish
+ print "---\n"
+
+ methods.each do |key, hash|
+ print key.inspect, ":\n"
+ print " class: ", hash[:class].inspect, "\n"
+ print " method: ", hash[:method].inspect, "\n"
+ print " type: ", hash[:type], "\n"
+ print " description: ", hash[:description].inspect, "\n"
+ print " examples: ", hash[:examples], "\n"
+ print " expectations: ", hash[:expectations], "\n"
+ print " failures: ", hash[:failures], "\n"
+ print " errors: ", hash[:errors], "\n"
+ print " exceptions:\n"
+ hash[:exceptions].each { |exc| print " - ", exc.inspect, "\n" }
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/multi.rb b/spec/mspec/lib/mspec/runner/formatters/multi.rb
new file mode 100644
index 0000000000..bcc5411e6f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/multi.rb
@@ -0,0 +1,36 @@
+require 'mspec/runner/formatters/spinner'
+require 'yaml'
+
+class MultiFormatter < SpinnerFormatter
+ def initialize(out=nil)
+ super(out)
+ @counter = @tally = Tally.new
+ @timer = TimerAction.new
+ @timer.start
+ end
+
+ def aggregate_results(files)
+ @timer.finish
+ @exceptions = []
+
+ files.each do |file|
+ d = File.open(file, "r") { |f| YAML.load f }
+ File.delete file
+
+ @exceptions += Array(d['exceptions'])
+ @tally.files! d['files']
+ @tally.examples! d['examples']
+ @tally.expectations! d['expectations']
+ @tally.errors! d['errors']
+ @tally.failures! d['failures']
+ end
+ end
+
+ def print_exception(exc, count)
+ print "\n#{count})\n#{exc}\n"
+ end
+
+ def finish
+ super(false)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/profile.rb b/spec/mspec/lib/mspec/runner/formatters/profile.rb
new file mode 100644
index 0000000000..498cd4a3b7
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/profile.rb
@@ -0,0 +1,70 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class ProfileFormatter < DottedFormatter
+ def initialize(out=nil)
+ super
+
+ @describe_name = nil
+ @describe_time = nil
+ @describes = []
+ @its = []
+ end
+
+ def register
+ super
+ MSpec.register :enter, self
+ end
+
+ # Callback for the MSpec :enter event. Prints the
+ # +describe+ block string.
+ def enter(describe)
+ if @describe_time
+ @describes << [@describe_name, Time.now.to_f - @describe_time]
+ end
+
+ @describe_name = describe
+ @describe_time = Time.now.to_f
+ end
+
+ # Callback for the MSpec :before event. Prints the
+ # +it+ block string.
+ def before(state)
+ super
+
+ @it_name = state.it
+ @it_time = Time.now.to_f
+ end
+
+ # Callback for the MSpec :after event. Prints a
+ # newline to finish the description string output.
+ def after(state)
+ @its << [@describe_name, @it_name, Time.now.to_f - @it_time]
+ super
+ end
+
+ def finish
+ puts "\nProfiling info:"
+
+ desc = @describes.sort { |a,b| b.last <=> a.last }
+ desc.delete_if { |a| a.last <= 0.001 }
+ show = desc[0, 100]
+
+ puts "Top #{show.size} describes:"
+
+ show.each do |des, time|
+ printf "%3.3f - %s\n", time, des
+ end
+
+ its = @its.sort { |a,b| b.last <=> a.last }
+ its.delete_if { |a| a.last <= 0.001 }
+ show = its[0, 100]
+
+ puts "\nTop #{show.size} its:"
+ show.each do |des, it, time|
+ printf "%3.3f - %s %s\n", time, des, it
+ end
+
+ super
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/specdoc.rb b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
new file mode 100644
index 0000000000..29adde3c5c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
@@ -0,0 +1,41 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class SpecdocFormatter < DottedFormatter
+ def register
+ super
+ MSpec.register :enter, self
+ end
+
+ # Callback for the MSpec :enter event. Prints the
+ # +describe+ block string.
+ def enter(describe)
+ print "\n#{describe}\n"
+ end
+
+ # Callback for the MSpec :before event. Prints the
+ # +it+ block string.
+ def before(state)
+ super
+ print "- #{state.it}"
+ end
+
+ # Callback for the MSpec :exception event. Prints
+ # either 'ERROR - X' or 'FAILED - X' where _X_ is
+ # the sequential number of the exception raised. If
+ # there has already been an exception raised while
+ # evaluating this example, it prints another +it+
+ # block description string so that each discription
+ # string has an associated 'ERROR' or 'FAILED'
+ def exception(exception)
+ print "\n- #{exception.it}" if exception?
+ super
+ print " (#{exception.failure? ? 'FAILED' : 'ERROR'} - #{@count})"
+ end
+
+ # Callback for the MSpec :after event. Prints a
+ # newline to finish the description string output.
+ def after(state)
+ print "\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/spinner.rb b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
new file mode 100644
index 0000000000..f6f35cc476
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
@@ -0,0 +1,117 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class SpinnerFormatter < DottedFormatter
+ attr_reader :length
+
+ Spins = %w!| / - \\!
+ HOUR = 3600
+ MIN = 60
+
+ def initialize(out=nil)
+ super(nil)
+
+ @which = 0
+ @loaded = 0
+ self.length = 40
+ @percent = 0
+ @start = Time.now
+
+ term = ENV['TERM']
+ @color = (term != "dumb")
+ @fail_color = "32"
+ @error_color = "32"
+ end
+
+ def register
+ super
+
+ MSpec.register :start, self
+ MSpec.register :unload, self
+ MSpec.unregister :before, self
+ end
+
+ def length=(length)
+ @length = length
+ @ratio = 100.0 / length
+ @position = length / 2 - 2
+ end
+
+ def compute_etr
+ return @etr = "00:00:00" if @percent == 0
+ elapsed = Time.now - @start
+ remain = (100 * elapsed / @percent) - elapsed
+
+ hour = remain >= HOUR ? (remain / HOUR).to_i : 0
+ remain -= hour * HOUR
+ min = remain >= MIN ? (remain / MIN).to_i : 0
+ sec = remain - min * MIN
+
+ @etr = "%02d:%02d:%02d" % [hour, min, sec]
+ end
+
+ def compute_percentage
+ @percent = @loaded * 100 / @total
+ bar = ("=" * (@percent / @ratio)).ljust @length
+ label = "%d%%" % @percent
+ bar[@position, label.size] = label
+ @bar = bar
+ end
+
+ def compute_progress
+ compute_percentage
+ compute_etr
+ end
+
+ def progress_line
+ @which = (@which + 1) % Spins.size
+ data = [Spins[@which], @bar, @etr, @counter.failures, @counter.errors]
+ if @color
+ "\r[%s | %s | %s] \e[0;#{@fail_color}m%6dF \e[0;#{@error_color}m%6dE\e[0m " % data
+ else
+ "\r[%s | %s | %s] %6dF %6dE " % data
+ end
+ end
+
+ def clear_progress_line
+ print "\r#{' '*progress_line.length}"
+ end
+
+ # Callback for the MSpec :start event. Stores the total
+ # number of files that will be processed.
+ def start
+ @total = MSpec.retrieve(:files).size
+ compute_progress
+ print progress_line
+ end
+
+ # Callback for the MSpec :unload event. Increments the number
+ # of files that have been run.
+ def unload
+ @loaded += 1
+ compute_progress
+ print progress_line
+ end
+
+ # Callback for the MSpec :exception event. Changes the color
+ # used to display the tally of errors and failures
+ def exception(exception)
+ super
+ @fail_color = "31" if exception.failure?
+ @error_color = "33" unless exception.failure?
+
+ clear_progress_line
+ print_exception(exception, @count)
+ end
+
+ # Callback for the MSpec :after event. Updates the spinner.
+ def after(state)
+ print progress_line
+ end
+
+ def finish(printed_exceptions = true)
+ # We already printed the exceptions
+ @exceptions = [] if printed_exceptions
+ super()
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/summary.rb b/spec/mspec/lib/mspec/runner/formatters/summary.rb
new file mode 100644
index 0000000000..0c9207194c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/summary.rb
@@ -0,0 +1,11 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class SummaryFormatter < DottedFormatter
+ # Callback for the MSpec :after event. Overrides the
+ # callback provided by +DottedFormatter+ and does not
+ # print any output for each example evaluated.
+ def after(state)
+ # do nothing
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/unit.rb b/spec/mspec/lib/mspec/runner/formatters/unit.rb
new file mode 100644
index 0000000000..69b68dc0d5
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/unit.rb
@@ -0,0 +1,21 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class UnitdiffFormatter < DottedFormatter
+ def finish
+ print "\n\n#{@timer.format}\n"
+ count = 0
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print "\n#{count += 1})\n#{exc.description} #{outcome}\n"
+ print exc.message, ": \n"
+ print exc.backtrace, "\n"
+ end
+ print "\n#{@tally.format}\n"
+ end
+
+ def backtrace(exc)
+ exc.backtrace && exc.backtrace.join("\n")
+ end
+ private :backtrace
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/yaml.rb b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
new file mode 100644
index 0000000000..090a9b1b9d
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
@@ -0,0 +1,42 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class YamlFormatter < DottedFormatter
+ def initialize(out=nil)
+ super(nil)
+
+ if out.nil?
+ @finish = $stdout
+ else
+ @finish = File.open out, "w"
+ end
+ end
+
+ def switch
+ @out = @finish
+ end
+
+ def after(state)
+ end
+
+ def finish
+ switch
+
+ print "---\n"
+ print "exceptions:\n"
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ str = "#{exc.description} #{outcome}\n"
+ str << exc.message << "\n" << exc.backtrace
+ print "- ", str.inspect, "\n"
+ end
+
+ print "time: ", @timer.elapsed, "\n"
+ print "files: ", @tally.counter.files, "\n"
+ print "examples: ", @tally.counter.examples, "\n"
+ print "expectations: ", @tally.counter.expectations, "\n"
+ print "failures: ", @tally.counter.failures, "\n"
+ print "errors: ", @tally.counter.errors, "\n"
+ print "tagged: ", @tally.counter.tagged, "\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/mspec.rb b/spec/mspec/lib/mspec/runner/mspec.rb
new file mode 100644
index 0000000000..0ff0de36ca
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/mspec.rb
@@ -0,0 +1,391 @@
+require 'mspec/runner/context'
+require 'mspec/runner/exception'
+require 'mspec/runner/tag'
+
+module MSpec
+
+ @exit = nil
+ @abort = nil
+ @start = nil
+ @enter = nil
+ @before = nil
+ @add = nil
+ @after = nil
+ @leave = nil
+ @finish = nil
+ @exclude = nil
+ @include = nil
+ @leave = nil
+ @load = nil
+ @unload = nil
+ @tagged = nil
+ @current = nil
+ @example = nil
+ @modes = []
+ @shared = {}
+ @guarded = []
+ @features = {}
+ @exception = nil
+ @randomize = nil
+ @repeat = nil
+ @expectation = nil
+ @expectations = false
+
+ def self.describe(mod, options=nil, &block)
+ state = ContextState.new mod, options
+ state.parent = current
+
+ MSpec.register_current state
+ state.describe(&block)
+
+ state.process unless state.shared? or current
+ end
+
+ def self.process
+ STDOUT.puts RUBY_DESCRIPTION
+
+ actions :start
+ files
+ actions :finish
+ end
+
+ def self.each_file(&block)
+ if ENV["MSPEC_MULTI"]
+ STDOUT.print "."
+ STDOUT.flush
+ while (file = STDIN.gets.chomp) != "QUIT"
+ yield file
+ STDOUT.print "."
+ STDOUT.flush
+ end
+ else
+ return unless files = retrieve(:files)
+ shuffle files if randomize?
+ files.each(&block)
+ end
+ end
+
+ def self.files
+ each_file do |file|
+ setup_env
+ store :file, file
+ actions :load
+ protect("loading #{file}") { Kernel.load file }
+ actions :unload
+ end
+ end
+
+ def self.setup_env
+ @env = Object.new
+ @env.extend MSpec
+ end
+
+ def self.actions(action, *args)
+ actions = retrieve(action)
+ actions.each { |obj| obj.send action, *args } if actions
+ end
+
+ def self.protect(location, &block)
+ begin
+ @env.instance_eval(&block)
+ return true
+ rescue SystemExit => e
+ raise e
+ rescue Exception => exc
+ register_exit 1
+ actions :exception, ExceptionState.new(current && current.state, location, exc)
+ return false
+ end
+ end
+
+ # Guards can be nested, so a stack is necessary to know when we have
+ # exited the toplevel guard.
+ def self.guard
+ @guarded << true
+ end
+
+ def self.unguard
+ @guarded.pop
+ end
+
+ def self.guarded?
+ !@guarded.empty?
+ end
+
+ # Sets the toplevel ContextState to +state+.
+ def self.register_current(state)
+ store :current, state
+ end
+
+ # Sets the toplevel ContextState to +nil+.
+ def self.clear_current
+ store :current, nil
+ end
+
+ # Returns the toplevel ContextState.
+ def self.current
+ retrieve :current
+ end
+
+ # Stores the shared ContextState keyed by description.
+ def self.register_shared(state)
+ @shared[state.to_s] = state
+ end
+
+ # Returns the shared ContextState matching description.
+ def self.retrieve_shared(desc)
+ @shared[desc.to_s]
+ end
+
+ # Stores the exit code used by the runner scripts.
+ def self.register_exit(code)
+ store :exit, code
+ end
+
+ # Retrieves the stored exit code.
+ def self.exit_code
+ retrieve(:exit).to_i
+ end
+
+ # Stores the list of files to be evaluated.
+ def self.register_files(files)
+ store :files, files
+ end
+
+ # Stores one or more substitution patterns for transforming
+ # a spec filename into a tags filename, where each pattern
+ # has the form:
+ #
+ # [Regexp, String]
+ #
+ # See also +tags_file+.
+ def self.register_tags_patterns(patterns)
+ store :tags_patterns, patterns
+ end
+
+ # Registers an operating mode. Modes recognized by MSpec:
+ #
+ # :pretend - actions execute but specs are not run
+ # :verify - specs are run despite guards and the result is
+ # verified to match the expectation of the guard
+ # :report - specs that are guarded are reported
+ # :unguarded - all guards are forced off
+ def self.register_mode(mode)
+ modes = retrieve :modes
+ modes << mode unless modes.include? mode
+ end
+
+ # Clears all registered modes.
+ def self.clear_modes
+ store :modes, []
+ end
+
+ # Returns +true+ if +mode+ is registered.
+ def self.mode?(mode)
+ retrieve(:modes).include? mode
+ end
+
+ def self.enable_feature(feature)
+ retrieve(:features)[feature] = true
+ end
+
+ def self.disable_feature(feature)
+ retrieve(:features)[feature] = false
+ end
+
+ def self.feature_enabled?(feature)
+ retrieve(:features)[feature] || false
+ end
+
+ def self.retrieve(symbol)
+ instance_variable_get :"@#{symbol}"
+ end
+
+ def self.store(symbol, value)
+ instance_variable_set :"@#{symbol}", value
+ end
+
+ # This method is used for registering actions that are
+ # run at particular points in the spec cycle:
+ # :start before any specs are run
+ # :load before a spec file is loaded
+ # :enter before a describe block is run
+ # :before before a single spec is run
+ # :add while a describe block is adding examples to run later
+ # :expectation before a 'should', 'should_receive', etc.
+ # :example after an example block is run, passed the block
+ # :exception after an exception is rescued
+ # :after after a single spec is run
+ # :leave after a describe block is run
+ # :unload after a spec file is run
+ # :finish after all specs are run
+ #
+ # Objects registered as actions above should respond to
+ # a method of the same name. For example, if an object
+ # is registered as a :start action, it should respond to
+ # a #start method call.
+ #
+ # Additionally, there are two "action" lists for
+ # filtering specs:
+ # :include return true if the spec should be run
+ # :exclude return true if the spec should NOT be run
+ #
+ def self.register(symbol, action)
+ unless value = retrieve(symbol)
+ value = store symbol, []
+ end
+ value << action unless value.include? action
+ end
+
+ def self.unregister(symbol, action)
+ if value = retrieve(symbol)
+ value.delete action
+ end
+ end
+
+ def self.randomize(flag=true)
+ @randomize = flag
+ end
+
+ def self.randomize?
+ @randomize == true
+ end
+
+ def self.repeat=(times)
+ @repeat = times
+ end
+
+ def self.repeat
+ (@repeat || 1).times do
+ yield
+ end
+ end
+
+ def self.shuffle(ary)
+ return if ary.empty?
+
+ size = ary.size
+ size.times do |i|
+ r = rand(size - i - 1)
+ ary[i], ary[r] = ary[r], ary[i]
+ end
+ end
+
+ # Records that an expectation has been encountered in an example.
+ def self.expectation
+ store :expectations, true
+ end
+
+ # Returns true if an expectation has been encountered
+ def self.expectation?
+ retrieve :expectations
+ end
+
+ # Resets the flag that an expectation has been encountered in an example.
+ def self.clear_expectations
+ store :expectations, false
+ end
+
+ # Transforms a spec filename into a tags filename by applying each
+ # substitution pattern in :tags_pattern. The default patterns are:
+ #
+ # [%r(/spec/), '/spec/tags/'], [/_spec.rb$/, '_tags.txt']
+ #
+ # which will perform the following transformation:
+ #
+ # path/to/spec/class/method_spec.rb => path/to/spec/tags/class/method_tags.txt
+ #
+ # See also +register_tags_patterns+.
+ def self.tags_file
+ patterns = retrieve(:tags_patterns) ||
+ [[%r(spec/), 'spec/tags/'], [/_spec.rb$/, '_tags.txt']]
+ patterns.inject(retrieve(:file).dup) do |file, pattern|
+ file.gsub(*pattern)
+ end
+ end
+
+ # Returns a list of tags matching any tag string in +keys+ based
+ # on the return value of <tt>keys.include?("tag_name")</tt>
+ def self.read_tags(keys)
+ tags = []
+ file = tags_file
+ if File.exist? file
+ File.open(file, "r:utf-8") do |f|
+ f.each_line do |line|
+ line.chomp!
+ next if line.empty?
+ tag = SpecTag.new line
+ tags << tag if keys.include? tag.tag
+ end
+ end
+ end
+ tags
+ end
+
+ def self.make_tag_dir(path)
+ parent = File.dirname(path)
+ return if File.exist? parent
+ begin
+ Dir.mkdir(parent)
+ rescue SystemCallError
+ make_tag_dir(parent)
+ Dir.mkdir(parent)
+ end
+ end
+
+ # Writes each tag in +tags+ to the tag file. Overwrites the
+ # tag file if it exists.
+ def self.write_tags(tags)
+ file = tags_file
+ make_tag_dir(file)
+ File.open(file, "w:utf-8") do |f|
+ tags.each { |t| f.puts t }
+ end
+ end
+
+ # Writes +tag+ to the tag file if it does not already exist.
+ # Returns +true+ if the tag is written, +false+ otherwise.
+ def self.write_tag(tag)
+ tags = read_tags([tag.tag])
+ tags.each do |t|
+ if t.tag == tag.tag and t.description == tag.description
+ return false
+ end
+ end
+
+ file = tags_file
+ make_tag_dir(file)
+ File.open(file, "a:utf-8") { |f| f.puts tag.to_s }
+ return true
+ end
+
+ # Deletes +tag+ from the tag file if it exists. Returns +true+
+ # if the tag is deleted, +false+ otherwise. Deletes the tag
+ # file if it is empty.
+ def self.delete_tag(tag)
+ deleted = false
+ desc = tag.escape(tag.description)
+ file = tags_file
+ if File.exist? file
+ lines = IO.readlines(file)
+ File.open(file, "w:utf-8") do |f|
+ lines.each do |line|
+ line = line.chomp
+ if line.start_with?(tag.tag) and line.end_with?(desc)
+ deleted = true
+ else
+ f.puts line unless line.empty?
+ end
+ end
+ end
+ File.delete file unless File.size? file
+ end
+ return deleted
+ end
+
+ # Removes the tag file associated with a spec file.
+ def self.delete_tags
+ file = tags_file
+ File.delete file if File.exist? file
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/object.rb b/spec/mspec/lib/mspec/runner/object.rb
new file mode 100644
index 0000000000..018e356149
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/object.rb
@@ -0,0 +1,28 @@
+class Object
+ def before(at=:each, &block)
+ MSpec.current.before at, &block
+ end
+
+ def after(at=:each, &block)
+ MSpec.current.after at, &block
+ end
+
+ def describe(mod, msg=nil, options=nil, &block)
+ MSpec.describe mod, msg, &block
+ end
+
+ def it(msg, &block)
+ MSpec.current.it msg, &block
+ end
+
+ def it_should_behave_like(desc)
+ MSpec.current.it_should_behave_like desc
+ end
+
+ # For ReadRuby compatiability
+ def doc(*a)
+ end
+
+ alias_method :context, :describe
+ alias_method :specify, :it
+end
diff --git a/spec/mspec/lib/mspec/runner/shared.rb b/spec/mspec/lib/mspec/runner/shared.rb
new file mode 100644
index 0000000000..336e35f6ac
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/shared.rb
@@ -0,0 +1,12 @@
+require 'mspec/runner/mspec'
+
+class Object
+ def it_behaves_like(desc, meth, obj=nil)
+ send :before, :all do
+ @method = meth
+ @object = obj
+ end
+
+ send :it_should_behave_like, desc.to_s
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/tag.rb b/spec/mspec/lib/mspec/runner/tag.rb
new file mode 100644
index 0000000000..e2275ad3a6
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/tag.rb
@@ -0,0 +1,38 @@
+class SpecTag
+ attr_accessor :tag, :comment, :description
+
+ def initialize(string=nil)
+ parse(string) if string
+ end
+
+ def parse(string)
+ m = /^([^()#:]+)(\(([^)]+)?\))?:(.*)$/.match string
+ @tag, @comment, description = m.values_at(1, 3, 4) if m
+ @description = unescape description
+ end
+
+ def unescape(str)
+ return unless str
+ if str[0] == ?" and str[-1] == ?"
+ str[1..-2].gsub('\n', "\n")
+ else
+ str
+ end
+ end
+
+ def escape(str)
+ if str.include? "\n"
+ %["#{str.gsub("\n", '\n')}"]
+ else
+ str
+ end
+ end
+
+ def to_s
+ "#{@tag}#{ "(#{@comment})" if @comment }:#{escape @description}"
+ end
+
+ def ==(o)
+ @tag == o.tag and @comment == o.comment and @description == o.description
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/deprecate.rb b/spec/mspec/lib/mspec/utils/deprecate.rb
new file mode 100644
index 0000000000..1db843b329
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/deprecate.rb
@@ -0,0 +1,6 @@
+module MSpec
+ def self.deprecate(what, replacement)
+ user_caller = caller.find { |line| !line.include?('lib/mspec') }
+ $stderr.puts "\n#{what} is deprecated, use #{replacement} instead.\nfrom #{user_caller}"
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/name_map.rb b/spec/mspec/lib/mspec/utils/name_map.rb
new file mode 100644
index 0000000000..c1de081af0
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/name_map.rb
@@ -0,0 +1,128 @@
+class NameMap
+ MAP = {
+ '`' => 'backtick',
+ '+' => 'plus',
+ '-' => 'minus',
+ '+@' => 'uplus',
+ '-@' => 'uminus',
+ '*' => 'multiply',
+ '/' => 'divide',
+ '%' => 'modulo',
+ '<<' => {'Bignum' => 'left_shift',
+ 'Fixnum' => 'left_shift',
+ 'IO' => 'output',
+ :default => 'append' },
+ '>>' => 'right_shift',
+ '<' => 'lt',
+ '<=' => 'lte',
+ '>' => 'gt',
+ '>=' => 'gte',
+ '=' => 'assignment',
+ '==' => 'equal_value',
+ '===' => 'case_compare',
+ '<=>' => 'comparison',
+ '[]' => 'element_reference',
+ '[]=' => 'element_set',
+ '**' => 'exponent',
+ '!' => 'not',
+ '~' => {'Bignum' => 'complement',
+ 'Fixnum' => 'complement',
+ 'Regexp' => 'match',
+ 'String' => 'match' },
+ '!=' => 'not_equal',
+ '!~' => 'not_match',
+ '=~' => 'match',
+ '&' => {'Bignum' => 'bit_and',
+ 'Fixnum' => 'bit_and',
+ 'Array' => 'intersection',
+ 'TrueClass' => 'and',
+ 'FalseClass' => 'and',
+ 'NilClass' => 'and',
+ 'Set' => 'intersection' },
+ '|' => {'Bignum' => 'bit_or',
+ 'Fixnum' => 'bit_or',
+ 'Array' => 'union',
+ 'TrueClass' => 'or',
+ 'FalseClass' => 'or',
+ 'NilClass' => 'or',
+ 'Set' => 'union' },
+ '^' => {'Bignum' => 'bit_xor',
+ 'Fixnum' => 'bit_xor',
+ 'TrueClass' => 'xor',
+ 'FalseClass' => 'xor',
+ 'NilClass' => 'xor',
+ 'Set' => 'exclusion'},
+ }
+
+ EXCLUDED = %w[
+ MSpecScript
+ MkSpec
+ MSpecOption
+ MSpecOptions
+ NameMap
+ SpecVersion
+ ]
+
+ def initialize(filter=false)
+ @seen = {}
+ @filter = filter
+ end
+
+ def exception?(name)
+ return false unless c = class_or_module(name)
+ c == Errno or c.ancestors.include? Exception
+ end
+
+ def class_or_module(c)
+ const = Object.const_get(c, false)
+ filtered = @filter && EXCLUDED.include?(const.name)
+ return const if Module === const and !filtered
+ rescue NameError
+ end
+
+ def namespace(mod, const)
+ return const.to_s if mod.nil? or %w[Object Class Module].include? mod
+ "#{mod}::#{const}"
+ end
+
+ def map(hash, constants, mod=nil)
+ @seen = {} unless mod
+
+ constants.each do |const|
+ name = namespace mod, const
+ m = class_or_module name
+ next unless m and !@seen[m]
+ @seen[m] = true
+
+ ms = m.methods(false).map { |x| x.to_s }
+ hash["#{name}."] = ms.sort unless ms.empty?
+
+ ms = m.public_instance_methods(false) +
+ m.protected_instance_methods(false)
+ ms.map! { |x| x.to_s }
+ hash["#{name}#"] = ms.sort unless ms.empty?
+
+ map hash, m.constants(false), name
+ end
+
+ hash
+ end
+
+ def dir_name(c, base)
+ return File.join(base, 'exception') if exception? c
+
+ c.split('::').inject(base) do |dir, name|
+ name.gsub!(/Class/, '') unless name == 'Class'
+ File.join dir, name.downcase
+ end
+ end
+
+ def file_name(m, c)
+ if MAP.key?(m)
+ name = MAP[m].is_a?(Hash) ? MAP[m][c.split('::').last] || MAP[m][:default] : MAP[m]
+ else
+ name = m.gsub(/[?!=]/, '')
+ end
+ "#{name}_spec.rb"
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/options.rb b/spec/mspec/lib/mspec/utils/options.rb
new file mode 100644
index 0000000000..122ef6e135
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/options.rb
@@ -0,0 +1,489 @@
+require 'mspec/version'
+
+MSPEC_HOME = File.expand_path('../../../..', __FILE__)
+
+class MSpecOption
+ attr_reader :short, :long, :arg, :description, :block
+
+ def initialize(short, long, arg, description, block)
+ @short = short
+ @long = long
+ @arg = arg
+ @description = description
+ @block = block
+ end
+
+ def arg?
+ @arg != nil
+ end
+
+ def match?(opt)
+ opt == @short or opt == @long
+ end
+end
+
+# MSpecOptions provides a parser for command line options. It also
+# provides a composable set of options from which the runner scripts
+# can select for their particular functionality.
+class MSpecOptions
+ # Raised if incorrect or incomplete formats are passed to #on.
+ class OptionError < Exception; end
+
+ # Raised if an unrecognized option is encountered.
+ class ParseError < Exception; end
+
+ attr_accessor :config, :banner, :width, :options
+
+ def initialize(banner="", width=30, config=nil)
+ @banner = banner
+ @config = config
+ @width = width
+ @options = []
+ @doc = []
+ @extra = []
+ @on_extra = lambda { |x|
+ raise ParseError, "Unrecognized option: #{x}" if x[0] == ?-
+ @extra << x
+ }
+
+ yield self if block_given?
+ end
+
+ # Registers an option. Acceptable formats for arguments are:
+ #
+ # on "-a", "description"
+ # on "-a", "--abdc", "description"
+ # on "-a", "ARG", "description"
+ # on "--abdc", "ARG", "description"
+ # on "-a", "--abdc", "ARG", "description"
+ #
+ # If an block is passed, it will be invoked when the option is
+ # matched. Not passing a block is permitted, but nonsensical.
+ def on(*args, &block)
+ raise OptionError, "option and description are required" if args.size < 2
+
+ description = args.pop
+ short, long, argument = nil
+ args.each do |arg|
+ if arg[0] == ?-
+ if arg[1] == ?-
+ long = arg
+ else
+ short = arg
+ end
+ else
+ argument = arg
+ end
+ end
+
+ add short, long, argument, description, block
+ end
+
+ # Adds documentation text for an option and adds an +MSpecOption+
+ # instance to the list of registered options.
+ def add(short, long, arg, description, block)
+ s = short ? short.dup : " "
+ s += (short ? ", " : " ") if long
+ doc " #{s}#{long} #{arg}".ljust(@width-1) + " #{description}"
+ @options << MSpecOption.new(short, long, arg, description, block)
+ end
+
+ # Searches all registered options to find a match for +opt+. Returns
+ # +nil+ if no registered options match.
+ def match?(opt)
+ @options.find { |o| o.match? opt }
+ end
+
+ # Processes an option. Calles the #on_extra block (or default) for
+ # unrecognized options. For registered options, possibly fetches an
+ # argument and invokes the option's block if it is not nil.
+ def process(argv, entry, opt, arg)
+ unless option = match?(opt)
+ @on_extra[entry]
+ else
+ if option.arg?
+ arg = argv.shift if arg.nil?
+ raise ParseError, "No argument provided for #{opt}" unless arg
+ option.block[arg] if option.block
+ else
+ option.block[] if option.block
+ end
+ end
+ option
+ end
+
+ # Splits a string at +n+ characters into the +opt+ and the +rest+.
+ # The +arg+ is set to +nil+ if +rest+ is an empty string.
+ def split(str, n)
+ opt = str[0, n]
+ rest = str[n, str.size]
+ arg = rest == "" ? nil : rest
+ return opt, arg, rest
+ end
+
+ # Parses an array of command line entries, calling blocks for
+ # registered options.
+ def parse(argv=ARGV)
+ argv = Array(argv).dup
+
+ while entry = argv.shift
+ # collect everything that is not an option
+ if entry[0] != ?- or entry.size < 2
+ @on_extra[entry]
+ next
+ end
+
+ # this is a long option
+ if entry[1] == ?-
+ opt, arg = entry.split "="
+ process argv, entry, opt, arg
+ next
+ end
+
+ # disambiguate short option group from short option with argument
+ opt, arg, rest = split entry, 2
+
+ # process first option
+ option = process argv, entry, opt, arg
+ next unless option and !option.arg?
+
+ # process the rest of the options
+ while rest.size > 0
+ opt, arg, rest = split rest, 1
+ opt = "-" + opt
+ option = process argv, opt, opt, arg
+ break if !option or option.arg?
+ end
+ end
+
+ @extra
+ rescue ParseError => e
+ puts self
+ puts e
+ exit 1
+ end
+
+ # Adds a string of documentation text inline in the text generated
+ # from the options. See #on and #add.
+ def doc(str)
+ @doc << str
+ end
+
+ # Convenience method for providing -v, --version options.
+ def version(version, &block)
+ show = block || lambda { puts "#{File.basename $0} #{version}"; exit }
+ on "-v", "--version", "Show version", &show
+ end
+
+ # Convenience method for providing -h, --help options.
+ def help(&block)
+ help = block || lambda { puts self; exit 1 }
+ on "-h", "--help", "Show this message", &help
+ end
+
+ # Stores a block that will be called with unrecognized options
+ def on_extra(&block)
+ @on_extra = block
+ end
+
+ # Returns a string representation of the options and doc strings.
+ def to_s
+ @banner + "\n\n" + @doc.join("\n") + "\n"
+ end
+
+ # The methods below provide groups of options that
+ # are composed by the particular runners to provide
+ # their functionality
+
+ def configure(&block)
+ on("-B", "--config", "FILE",
+ "Load FILE containing configuration options", &block)
+ end
+
+ def name
+ on("-n", "--name", "RUBY_NAME",
+ "Set the value of RUBY_NAME (used to determine the implementation)") do |n|
+ Object.const_set :RUBY_NAME, n
+ end
+ end
+
+ def targets
+ on("-t", "--target", "TARGET",
+ "Implementation to run the specs, where TARGET is:") do |t|
+ case t
+ when 'r', 'ruby'
+ config[:target] = 'ruby'
+ when 'x', 'rubinius'
+ config[:target] = './bin/rbx'
+ when 'X', 'rbx'
+ config[:target] = 'rbx'
+ when 'j', 'jruby'
+ config[:target] = 'jruby'
+ when 'i','ironruby'
+ config[:target] = 'ir'
+ when 'm','maglev'
+ config[:target] = 'maglev-ruby'
+ when 't','topaz'
+ config[:target] = 'topaz'
+ when 'o','opal'
+ mspec_lib = File.expand_path('../../../', __FILE__)
+ config[:target] = "./bin/opal -syaml -sfileutils -rnodejs -rnodejs/require -rnodejs/yaml -rprocess -Derror -I#{mspec_lib} -I./lib/ -I. "
+ else
+ config[:target] = t
+ end
+ end
+
+ doc ""
+ doc " r or ruby invokes ruby in PATH"
+ doc " x or rubinius invokes ./bin/rbx"
+ doc " X or rbx invokes rbx in PATH"
+ doc " j or jruby invokes jruby in PATH"
+ doc " i or ironruby invokes ir in PATH"
+ doc " m or maglev invokes maglev-ruby in PATH"
+ doc " t or topaz invokes topaz in PATH"
+ doc " o or opal invokes ./bin/opal with options"
+ doc " full path to EXE invokes EXE directly\n"
+
+ on("-T", "--target-opt", "OPT",
+ "Pass OPT as a flag to the target implementation") do |t|
+ config[:flags] << t
+ end
+ on("-I", "--include", "DIR",
+ "Pass DIR through as the -I option to the target") do |d|
+ config[:loadpath] << "-I#{d}"
+ end
+ on("-r", "--require", "LIBRARY",
+ "Pass LIBRARY through as the -r option to the target") do |f|
+ config[:requires] << "-r#{f}"
+ end
+ end
+
+ def formatters
+ on("-f", "--format", "FORMAT",
+ "Formatter for reporting, where FORMAT is one of:") do |o|
+ require 'mspec/runner/formatters'
+ case o
+ when 's', 'spec', 'specdoc'
+ config[:formatter] = SpecdocFormatter
+ when 'h', 'html'
+ config[:formatter] = HtmlFormatter
+ when 'd', 'dot', 'dotted'
+ config[:formatter] = DottedFormatter
+ when 'b', 'describe'
+ config[:formatter] = DescribeFormatter
+ when 'f', 'file'
+ config[:formatter] = FileFormatter
+ when 'u', 'unit', 'unitdiff'
+ config[:formatter] = UnitdiffFormatter
+ when 'm', 'summary'
+ config[:formatter] = SummaryFormatter
+ when 'a', '*', 'spin'
+ config[:formatter] = SpinnerFormatter
+ when 't', 'method'
+ config[:formatter] = MethodFormatter
+ when 'y', 'yaml'
+ config[:formatter] = YamlFormatter
+ when 'p', 'profile'
+ config[:formatter] = ProfileFormatter
+ when 'j', 'junit'
+ config[:formatter] = JUnitFormatter
+ else
+ abort "Unknown format: #{o}\n#{@parser}" unless File.exist?(o)
+ require File.expand_path(o)
+ if Object.const_defined?(:CUSTOM_MSPEC_FORMATTER)
+ config[:formatter] = CUSTOM_MSPEC_FORMATTER
+ else
+ abort "You must define CUSTOM_MSPEC_FORMATTER in your custom formatter file"
+ end
+ end
+ end
+
+ doc ""
+ doc " s, spec, specdoc SpecdocFormatter"
+ doc " h, html, HtmlFormatter"
+ doc " d, dot, dotted DottedFormatter"
+ doc " f, file FileFormatter"
+ doc " u, unit, unitdiff UnitdiffFormatter"
+ doc " m, summary SummaryFormatter"
+ doc " a, *, spin SpinnerFormatter"
+ doc " t, method MethodFormatter"
+ doc " y, yaml YamlFormatter"
+ doc " p, profile ProfileFormatter"
+ doc " j, junit JUnitFormatter\n"
+
+ on("-o", "--output", "FILE",
+ "Write formatter output to FILE") do |f|
+ config[:output] = f
+ end
+ end
+
+ def filters
+ on("-e", "--example", "STR",
+ "Run examples with descriptions matching STR") do |o|
+ config[:includes] << o
+ end
+ on("-E", "--exclude", "STR",
+ "Exclude examples with descriptions matching STR") do |o|
+ config[:excludes] << o
+ end
+ on("-p", "--pattern", "PATTERN",
+ "Run examples with descriptions matching PATTERN") do |o|
+ config[:patterns] << Regexp.new(o)
+ end
+ on("-P", "--excl-pattern", "PATTERN",
+ "Exclude examples with descriptions matching PATTERN") do |o|
+ config[:xpatterns] << Regexp.new(o)
+ end
+ on("-g", "--tag", "TAG",
+ "Run examples with descriptions matching ones tagged with TAG") do |o|
+ config[:tags] << o
+ end
+ on("-G", "--excl-tag", "TAG",
+ "Exclude examples with descriptions matching ones tagged with TAG") do |o|
+ config[:xtags] << o
+ end
+ on("-w", "--profile", "FILE",
+ "Run examples for methods listed in the profile FILE") do |f|
+ config[:profiles] << f
+ end
+ on("-W", "--excl-profile", "FILE",
+ "Exclude examples for methods listed in the profile FILE") do |f|
+ config[:xprofiles] << f
+ end
+ end
+
+ def chdir
+ on("-C", "--chdir", "DIR",
+ "Change the working directory to DIR before running specs") do |d|
+ Dir.chdir d
+ end
+ end
+
+ def prefix
+ on("--prefix", "STR", "Prepend STR when resolving spec file names") do |p|
+ config[:prefix] = p
+ end
+ end
+
+ def pretend
+ on("-Z", "--dry-run",
+ "Invoke formatters and other actions, but don't execute the specs") do
+ MSpec.register_mode :pretend
+ end
+ end
+
+ def unguarded
+ on("--unguarded", "Turn off all guards") do
+ MSpec.register_mode :unguarded
+ end
+ on("--no-ruby_bug", "Turn off the ruby_bug guard") do
+ MSpec.register_mode :no_ruby_bug
+ end
+ end
+
+ def randomize
+ on("-H", "--random",
+ "Randomize the list of spec files") do
+ MSpec.randomize
+ end
+ end
+
+ def repeat
+ on("-R", "--repeat", "NUMBER",
+ "Repeatedly run an example NUMBER times") do |o|
+ MSpec.repeat = o.to_i
+ end
+ end
+
+ def verbose
+ on("-V", "--verbose", "Output the name of each file processed") do
+ obj = Object.new
+ def obj.start
+ @width = MSpec.retrieve(:files).inject(0) { |max, f| f.size > max ? f.size : max }
+ end
+ def obj.load
+ file = MSpec.retrieve :file
+ print "\n#{file.ljust(@width)}"
+ end
+ MSpec.register :start, obj
+ MSpec.register :load, obj
+ end
+
+ on("-m", "--marker", "MARKER",
+ "Output MARKER for each file processed") do |o|
+ obj = Object.new
+ obj.instance_variable_set :@marker, o
+ def obj.load
+ print @marker
+ end
+ MSpec.register :load, obj
+ end
+ end
+
+ def interrupt
+ on("--int-spec", "Control-C interupts the current spec only") do
+ config[:abort] = false
+ end
+ end
+
+ def verify
+ on("--report-on", "GUARD", "Report specs guarded by GUARD") do |g|
+ MSpec.register_mode :report_on
+ SpecGuard.guards << g.to_sym
+ end
+ on("-O", "--report", "Report guarded specs") do
+ MSpec.register_mode :report
+ end
+ on("-Y", "--verify",
+ "Verify that guarded specs pass and fail as expected") do
+ MSpec.register_mode :verify
+ end
+ end
+
+ def action_filters
+ on("-K", "--action-tag", "TAG",
+ "Spec descriptions marked with TAG will trigger the specified action") do |o|
+ config[:atags] << o
+ end
+ on("-S", "--action-string", "STR",
+ "Spec descriptions matching STR will trigger the specified action") do |o|
+ config[:astrings] << o
+ end
+ end
+
+ def actions
+ on("--spec-debug",
+ "Invoke the debugger when a spec description matches (see -K, -S)") do
+ config[:debugger] = true
+ end
+ end
+
+ def debug
+ on("-d", "--debug",
+ "Set MSpec debugging flag for more verbose output") do
+ $MSPEC_DEBUG = true
+ end
+ end
+
+ def all
+ # Generated with:
+ # puts File.read(__FILE__).scan(/def (\w+).*\n\s*on\(/)
+ configure {}
+ name
+ targets
+ formatters
+ filters
+ chdir
+ prefix
+ pretend
+ unguarded
+ randomize
+ repeat
+ verbose
+ interrupt
+ verify
+ action_filters
+ actions
+ debug
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/ruby_name.rb b/spec/mspec/lib/mspec/utils/ruby_name.rb
new file mode 100644
index 0000000000..e381e387f6
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/ruby_name.rb
@@ -0,0 +1,8 @@
+unless Object.const_defined?(:RUBY_NAME) and RUBY_NAME
+ if Object.const_defined?(:RUBY_ENGINE) and RUBY_ENGINE
+ RUBY_NAME = RUBY_ENGINE
+ else
+ require 'rbconfig'
+ RUBY_NAME = RbConfig::CONFIG["RUBY_INSTALL_NAME"] || RbConfig::CONFIG["ruby_install_name"]
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/script.rb b/spec/mspec/lib/mspec/utils/script.rb
new file mode 100644
index 0000000000..28be854a85
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/script.rb
@@ -0,0 +1,267 @@
+require 'mspec/guards/guard'
+require 'mspec/utils/warnings'
+
+# MSpecScript provides a skeleton for all the MSpec runner scripts.
+
+class MSpecScript
+ # Returns the config object. Maintained at the class
+ # level to easily enable simple config files. See the
+ # class method +set+.
+ def self.config
+ @config ||= {
+ :path => ['.', 'spec'],
+ :config_ext => '.mspec'
+ }
+ end
+
+ # Associates +value+ with +key+ in the config object. Enables
+ # simple config files of the form:
+ #
+ # class MSpecScript
+ # set :target, "ruby"
+ # set :files, ["one_spec.rb", "two_spec.rb"]
+ # end
+ def self.set(key, value)
+ config[key] = value
+ end
+
+ # Gets the value of +key+ from the config object. Simplifies
+ # getting values in a config file:
+ #
+ # class MSpecScript
+ # set :a, 1
+ # set :b, 2
+ # set :c, get(:a) + get(:b)
+ # end
+ def self.get(key)
+ config[key]
+ end
+
+ def initialize
+ config[:formatter] = nil
+ config[:includes] = []
+ config[:excludes] = []
+ config[:patterns] = []
+ config[:xpatterns] = []
+ config[:tags] = []
+ config[:xtags] = []
+ config[:profiles] = []
+ config[:xprofiles] = []
+ config[:atags] = []
+ config[:astrings] = []
+ config[:ltags] = []
+ config[:abort] = true
+ @loaded = []
+ end
+
+ # Returns the config object maintained by the instance's class.
+ # See the class methods +set+ and +config+.
+ def config
+ MSpecScript.config
+ end
+
+ # Returns +true+ if the file was located in +config[:path]+,
+ # possibly appending +config[:config_ext]. Returns +false+
+ # otherwise.
+ def try_load(target)
+ names = [target]
+ unless target[-6..-1] == config[:config_ext]
+ names << target + config[:config_ext]
+ end
+
+ names.each do |name|
+ config[:path].each do |dir|
+ file = File.expand_path name, dir
+ if @loaded.include?(file)
+ return true
+ elsif File.exist? file
+ value = Kernel.load(file)
+ @loaded << file
+ return value
+ end
+ end
+ end
+
+ false
+ end
+
+ def load(target)
+ try_load(target) or abort "Could not load config file #{target}"
+ end
+
+ # Attempts to load a default config file. First tries to load
+ # 'default.mspec'. If that fails, attempts to load a config
+ # file name constructed from the value of RUBY_ENGINE and the
+ # first two numbers in RUBY_VERSION. For example, on MRI 1.8.6,
+ # the file name would be 'ruby.1.8.mspec'.
+ def load_default
+ try_load 'default.mspec'
+
+ if Object.const_defined?(:RUBY_ENGINE)
+ engine = RUBY_ENGINE
+ else
+ engine = 'ruby'
+ end
+ try_load "#{engine}.#{SpecGuard.ruby_version}.mspec"
+ try_load "#{engine}.mspec"
+ end
+
+ # Callback for enabling custom options. This version is a no-op.
+ # Provide an implementation specific version in a config file.
+ # Called by #options after the MSpec-provided options are added.
+ def custom_options(options)
+ options.doc " No custom options registered"
+ end
+
+ # Registers all filters and actions.
+ def register
+ require 'mspec/runner/formatters/dotted'
+ require 'mspec/runner/formatters/spinner'
+ require 'mspec/runner/formatters/file'
+ require 'mspec/runner/filters'
+
+ if config[:formatter].nil?
+ config[:formatter] = STDOUT.tty? ? SpinnerFormatter : @files.size < 50 ? DottedFormatter : FileFormatter
+ end
+
+ if config[:formatter]
+ formatter = config[:formatter].new(config[:output])
+ formatter.register
+ MSpec.store :formatter, formatter
+ end
+
+ MatchFilter.new(:include, *config[:includes]).register unless config[:includes].empty?
+ MatchFilter.new(:exclude, *config[:excludes]).register unless config[:excludes].empty?
+ RegexpFilter.new(:include, *config[:patterns]).register unless config[:patterns].empty?
+ RegexpFilter.new(:exclude, *config[:xpatterns]).register unless config[:xpatterns].empty?
+ TagFilter.new(:include, *config[:tags]).register unless config[:tags].empty?
+ TagFilter.new(:exclude, *config[:xtags]).register unless config[:xtags].empty?
+ ProfileFilter.new(:include, *config[:profiles]).register unless config[:profiles].empty?
+ ProfileFilter.new(:exclude, *config[:xprofiles]).register unless config[:xprofiles].empty?
+
+ DebugAction.new(config[:atags], config[:astrings]).register if config[:debugger]
+
+ custom_register
+ end
+
+ # Callback for enabling custom actions, etc. This version is a
+ # no-op. Provide an implementation specific version in a config
+ # file. Called by #register.
+ def custom_register
+ end
+
+ # Sets up signal handlers. Only a handler for SIGINT is
+ # registered currently.
+ def signals
+ if config[:abort]
+ Signal.trap "INT" do
+ MSpec.actions :abort
+ puts "\nProcess aborted!"
+ exit! 1
+ end
+ end
+ end
+
+ # Attempts to resolve +partial+ as a file or directory name in the
+ # following order:
+ #
+ # 1. +partial+
+ # 2. +partial+ + "_spec.rb"
+ # 3. <tt>File.join(config[:prefix], partial)</tt>
+ # 4. <tt>File.join(config[:prefix], partial + "_spec.rb")</tt>
+ #
+ # If it is a file name, returns the name as an entry in an array.
+ # If it is a directory, returns all *_spec.rb files in the
+ # directory and subdirectories.
+ #
+ # If unable to resolve +partial+, +Kernel.abort+ is called.
+ def entries(partial)
+ file = partial + "_spec.rb"
+ patterns = [partial, file]
+ if config[:prefix]
+ patterns << File.join(config[:prefix], partial)
+ patterns << File.join(config[:prefix], file)
+ end
+
+ patterns.each do |pattern|
+ expanded = File.expand_path(pattern)
+ if File.file?(expanded)
+ return [expanded]
+ elsif File.directory?(expanded)
+ return Dir["#{expanded}/**/*_spec.rb"].sort
+ end
+ end
+
+ abort "Could not find spec file #{partial}"
+ end
+
+ # Resolves each entry in +list+ to a set of files.
+ #
+ # If the entry has a leading '^' character, the list of files
+ # is subtracted from the list of files accumulated to that point.
+ #
+ # If the entry has a leading ':' character, the corresponding
+ # key is looked up in the config object and the entries in the
+ # value retrieved are processed through #entries.
+ def files(list)
+ list.inject([]) do |files, item|
+ case item[0]
+ when ?^
+ files -= entries(item[1..-1])
+ when ?:
+ key = item[1..-1].to_sym
+ files += files(Array(config[key]))
+ else
+ files += entries(item)
+ end
+ files
+ end
+ end
+
+ def files_from_patterns(patterns)
+ unless $0.end_with?("_spec.rb")
+ if patterns.empty?
+ patterns = config[:files]
+ end
+ if patterns.empty? and File.directory? "./spec"
+ patterns = ["spec/"]
+ end
+ if patterns.empty?
+ puts "No files specified."
+ exit 1
+ end
+ end
+ files patterns
+ end
+
+ def cores
+ require 'etc'
+ Etc.nprocessors
+ end
+
+ def setup_env
+ ENV['MSPEC_RUNNER'] = '1'
+
+ unless ENV['RUBY_EXE']
+ ENV['RUBY_EXE'] = config[:target] if config[:target]
+ end
+
+ unless ENV['RUBY_FLAGS']
+ ENV['RUBY_FLAGS'] = config[:flags].join(" ") if config[:flags]
+ end
+ end
+
+ # Instantiates an instance and calls the series of methods to
+ # invoke the script.
+ def self.main
+ script = new
+ script.load_default
+ script.try_load '~/.mspecrc'
+ script.options
+ script.signals
+ script.register
+ script.setup_env
+ require 'mspec'
+ script.run
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/version.rb b/spec/mspec/lib/mspec/utils/version.rb
new file mode 100644
index 0000000000..787a76b053
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/version.rb
@@ -0,0 +1,52 @@
+class SpecVersion
+ # If beginning implementations have a problem with this include, we can
+ # manually implement the relational operators that are needed.
+ include Comparable
+
+ # SpecVersion handles comparison correctly for the context by filling in
+ # missing version parts according to the value of +ceil+. If +ceil+ is
+ # +false+, 0 digits fill in missing version parts. If +ceil+ is +true+, 9
+ # digits fill in missing parts. (See e.g. VersionGuard and BugGuard.)
+ def initialize(version, ceil = false)
+ @version = version
+ @ceil = ceil
+ @integer = nil
+ end
+
+ def to_s
+ @version
+ end
+
+ def to_str
+ to_s
+ end
+
+ # Converts a string representation of a version major.minor.tiny
+ # to an integer representation so that comparisons can be made. For example,
+ # "2.2.10" < "2.2.2" would be false if compared as strings.
+ def to_i
+ unless @integer
+ major, minor, tiny = @version.split "."
+ if @ceil
+ tiny = 99 unless tiny
+ end
+ parts = [major, minor, tiny].map { |x| x.to_i }
+ @integer = ("1%02d%02d%02d" % parts).to_i
+ end
+ @integer
+ end
+
+ def to_int
+ to_i
+ end
+
+ def <=>(other)
+ if other.respond_to? :to_int
+ other = Integer other
+ else
+ other = SpecVersion.new(String(other)).to_i
+ end
+
+ self.to_i <=> other
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/warnings.rb b/spec/mspec/lib/mspec/utils/warnings.rb
new file mode 100644
index 0000000000..74c7f88a52
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/warnings.rb
@@ -0,0 +1,32 @@
+require 'mspec/guards/version'
+
+if RUBY_ENGINE == "ruby" and RUBY_VERSION >= "2.4.0"
+ ruby_version_is "2.4"..."2.5" do
+ # Kernel#warn does not delegate to Warning.warn in 2.4
+ module Kernel
+ def warn(*messages)
+ return if $VERBOSE == nil or messages.empty?
+ msg = messages.join("\n")
+ msg += "\n" unless msg.end_with?("\n")
+ Warning.warn(msg)
+ end
+ private :warn
+ end
+ end
+
+ def Warning.warn(message)
+ case message
+ when /constant ::(Fixnum|Bignum) is deprecated/
+ when /\/(argf|io|stringio)\/.+(ARGF|IO)#(lines|chars|bytes|codepoints) is deprecated/
+ when /Thread\.exclusive is deprecated.+\n.+thread\/exclusive_spec\.rb/
+ when /hash\/shared\/index\.rb:\d+: warning: Hash#index is deprecated; use Hash#key/
+ when /env\/shared\/key\.rb:\d+: warning: ENV\.index is deprecated; use ENV\.key/
+ when /exponent(_spec)?\.rb:\d+: warning: in a\*\*b, b may be too big/
+ when /enumerator\/(new|initialize_spec)\.rb:\d+: warning: Enumerator\.new without a block is deprecated/
+ else
+ $stderr.write message
+ end
+ end
+else
+ $VERBOSE = nil unless ENV['OUTPUT_WARNINGS']
+end
diff --git a/spec/mspec/lib/mspec/version.rb b/spec/mspec/lib/mspec/version.rb
new file mode 100644
index 0000000000..9126f5366e
--- /dev/null
+++ b/spec/mspec/lib/mspec/version.rb
@@ -0,0 +1,5 @@
+require 'mspec/utils/version'
+
+module MSpec
+ VERSION = SpecVersion.new "1.8.0"
+end