summaryrefslogtreecommitdiff
path: root/spec/mspec/lib
diff options
context:
space:
mode:
Diffstat (limited to 'spec/mspec/lib')
-rw-r--r--spec/mspec/lib/mspec.rb14
-rw-r--r--[-rwxr-xr-x]spec/mspec/lib/mspec/commands/mkspec.rb22
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-ci.rb8
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-run.rb11
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-tag.rb6
-rw-r--r--[-rwxr-xr-x]spec/mspec/lib/mspec/commands/mspec.rb81
-rw-r--r--spec/mspec/lib/mspec/expectations/expectations.rb22
-rw-r--r--spec/mspec/lib/mspec/expectations/should.rb32
-rw-r--r--spec/mspec/lib/mspec/guards/bug.rb19
-rw-r--r--spec/mspec/lib/mspec/guards/conflict.rb6
-rw-r--r--spec/mspec/lib/mspec/guards/feature.rb4
-rw-r--r--spec/mspec/lib/mspec/guards/guard.rb2
-rw-r--r--spec/mspec/lib/mspec/guards/platform.rb68
-rw-r--r--spec/mspec/lib/mspec/guards/superuser.rb10
-rw-r--r--spec/mspec/lib/mspec/guards/version.rb65
-rw-r--r--spec/mspec/lib/mspec/helpers/datetime.rb3
-rw-r--r--spec/mspec/lib/mspec/helpers/flunk.rb2
-rw-r--r--spec/mspec/lib/mspec/helpers/fs.rb10
-rw-r--r--spec/mspec/lib/mspec/helpers/io.rb40
-rw-r--r--spec/mspec/lib/mspec/helpers/numeric.rb36
-rw-r--r--spec/mspec/lib/mspec/helpers/ruby_exe.rb115
-rw-r--r--spec/mspec/lib/mspec/helpers/scratch.rb4
-rw-r--r--spec/mspec/lib/mspec/helpers/tmp.rb29
-rw-r--r--spec/mspec/lib/mspec/helpers/warning.rb14
-rw-r--r--spec/mspec/lib/mspec/matchers.rb2
-rw-r--r--spec/mspec/lib/mspec/matchers/base.rb96
-rw-r--r--spec/mspec/lib/mspec/matchers/be_close.rb8
-rw-r--r--spec/mspec/lib/mspec/matchers/block_caller.rb30
-rw-r--r--spec/mspec/lib/mspec/matchers/complain.rb25
-rw-r--r--spec/mspec/lib/mspec/matchers/eql.rb8
-rw-r--r--spec/mspec/lib/mspec/matchers/equal.rb8
-rw-r--r--spec/mspec/lib/mspec/matchers/equal_element.rb4
-rw-r--r--spec/mspec/lib/mspec/matchers/have_instance_method.rb2
-rw-r--r--spec/mspec/lib/mspec/matchers/have_method.rb2
-rw-r--r--spec/mspec/lib/mspec/matchers/have_private_instance_method.rb2
-rw-r--r--spec/mspec/lib/mspec/matchers/have_private_method.rb2
-rw-r--r--spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb2
-rw-r--r--spec/mspec/lib/mspec/matchers/have_public_instance_method.rb2
-rw-r--r--spec/mspec/lib/mspec/matchers/have_singleton_method.rb2
-rw-r--r--spec/mspec/lib/mspec/matchers/include.rb4
-rw-r--r--spec/mspec/lib/mspec/matchers/include_any_of.rb29
-rw-r--r--spec/mspec/lib/mspec/matchers/match_yaml.rb6
-rw-r--r--spec/mspec/lib/mspec/matchers/method.rb2
-rw-r--r--spec/mspec/lib/mspec/matchers/output.rb10
-rw-r--r--spec/mspec/lib/mspec/matchers/raise_error.rb115
-rw-r--r--spec/mspec/lib/mspec/matchers/skip.rb5
-rw-r--r--spec/mspec/lib/mspec/mocks/mock.rb47
-rw-r--r--spec/mspec/lib/mspec/mocks/object.rb4
-rw-r--r--spec/mspec/lib/mspec/mocks/proxy.rb6
-rw-r--r--spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb84
-rw-r--r--spec/mspec/lib/mspec/runner/actions/filter.rb2
-rw-r--r--spec/mspec/lib/mspec/runner/actions/leakchecker.rb204
-rw-r--r--spec/mspec/lib/mspec/runner/actions/profile.rb60
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tag.rb2
-rw-r--r--spec/mspec/lib/mspec/runner/actions/taglist.rb2
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tally.rb14
-rw-r--r--spec/mspec/lib/mspec/runner/actions/timeout.rb145
-rw-r--r--spec/mspec/lib/mspec/runner/context.rb38
-rw-r--r--spec/mspec/lib/mspec/runner/evaluate.rb4
-rw-r--r--spec/mspec/lib/mspec/runner/example.rb14
-rw-r--r--spec/mspec/lib/mspec/runner/exception.rb31
-rw-r--r--spec/mspec/lib/mspec/runner/filters/regexp.rb24
-rw-r--r--spec/mspec/lib/mspec/runner/formatters.rb1
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/base.rb150
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/describe.rb1
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/dotted.rb110
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/file.rb9
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/html.rb10
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/junit.rb15
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/launchable.rb88
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/method.rb28
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/multi.rb44
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/profile.rb60
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/specdoc.rb14
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/spinner.rb20
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/stats.rb57
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/summary.rb11
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/unit.rb1
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/yaml.rb10
-rw-r--r--spec/mspec/lib/mspec/runner/mspec.rb104
-rw-r--r--spec/mspec/lib/mspec/runner/object.rb12
-rw-r--r--spec/mspec/lib/mspec/runner/parallel.rb98
-rw-r--r--spec/mspec/lib/mspec/runner/shared.rb10
-rw-r--r--spec/mspec/lib/mspec/runner/tag.rb2
-rw-r--r--spec/mspec/lib/mspec/utils/format.rb24
-rw-r--r--spec/mspec/lib/mspec/utils/name_map.rb71
-rw-r--r--spec/mspec/lib/mspec/utils/options.rb68
-rw-r--r--spec/mspec/lib/mspec/utils/script.rb59
-rw-r--r--spec/mspec/lib/mspec/utils/version.rb2
-rw-r--r--spec/mspec/lib/mspec/utils/warnings.rb65
90 files changed, 1856 insertions, 958 deletions
diff --git a/spec/mspec/lib/mspec.rb b/spec/mspec/lib/mspec.rb
index 42d590c99a..d24abd96f1 100644
--- a/spec/mspec/lib/mspec.rb
+++ b/spec/mspec/lib/mspec.rb
@@ -1,3 +1,4 @@
+require 'mspec/utils/format'
require 'mspec/matchers'
require 'mspec/expectations'
require 'mspec/mocks'
@@ -5,16 +6,3 @@ require 'mspec/runner'
require 'mspec/guards'
require 'mspec/helpers'
require 'mspec/version'
-
-# If the implementation on which the specs are run cannot
-# load pp from the standard library, add a pp.rb file that
-# defines the #pretty_inspect method on Object or Kernel.
-begin
- require 'pp'
-rescue LoadError
- module Kernel
- def pretty_inspect
- inspect
- end
- end
-end
diff --git a/spec/mspec/lib/mspec/commands/mkspec.rb b/spec/mspec/lib/mspec/commands/mkspec.rb
index 7a943aa1fe..f75e683b19 100755..100644
--- a/spec/mspec/lib/mspec/commands/mkspec.rb
+++ b/spec/mspec/lib/mspec/commands/mkspec.rb
@@ -1,5 +1,3 @@
-#!/usr/bin/env ruby
-
require 'rbconfig'
require 'mspec/version'
require 'mspec/utils/options'
@@ -19,7 +17,7 @@ class MkSpec
@map = NameMap.new true
end
- def options(argv=ARGV)
+ def options(argv = ARGV)
options = MSpecOptions.new "mkspec [options]", 32
options.on("-c", "--constant", "CONSTANT",
@@ -75,7 +73,7 @@ class MkSpec
parents = '../' * (sub.split('/').length + 1)
File.open(file, 'w') do |f|
- f.puts "require File.expand_path('../#{parents}spec_helper', __FILE__)"
+ f.puts "require_relative '#{parents}spec_helper'"
config[:requires].each do |lib|
f.puts "require '#{lib}'"
end
@@ -95,7 +93,9 @@ class MkSpec
def write_spec(file, meth, exists)
if exists
- out = `#{ruby} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e '#{meth}' #{file}`
+ command = "#{RbConfig.ruby} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e '#{meth}' #{file}"
+ puts "$ #{command}" if $DEBUG
+ out = `#{command}`
return if out.include?(meth)
end
@@ -133,18 +133,6 @@ EOS
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'
diff --git a/spec/mspec/lib/mspec/commands/mspec-ci.rb b/spec/mspec/lib/mspec/commands/mspec-ci.rb
index cb0193f42d..8951572f69 100644
--- a/spec/mspec/lib/mspec/commands/mspec-ci.rb
+++ b/spec/mspec/lib/mspec/commands/mspec-ci.rb
@@ -1,14 +1,10 @@
-#!/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)
+ def options(argv = ARGV)
options = MSpecOptions.new "mspec ci [options] (FILE|DIRECTORY|GLOB)+", 30, config
options.doc " Ask yourself:"
@@ -22,8 +18,10 @@ class MSpecCI < MSpecScript
options.chdir
options.prefix
options.configure { |f| load f }
+ options.repeat
options.pretend
options.interrupt
+ options.timeout
options.doc "\n How to modify the guard behavior"
options.unguarded
diff --git a/spec/mspec/lib/mspec/commands/mspec-run.rb b/spec/mspec/lib/mspec/commands/mspec-run.rb
index 249f9f5771..0fb338fa23 100644
--- a/spec/mspec/lib/mspec/commands/mspec-run.rb
+++ b/spec/mspec/lib/mspec/commands/mspec-run.rb
@@ -1,7 +1,3 @@
-#!/usr/bin/env ruby
-
-$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
-
require 'mspec/version'
require 'mspec/utils/options'
require 'mspec/utils/script'
@@ -14,7 +10,7 @@ class MSpecRun < MSpecScript
config[:files] = []
end
- def options(argv=ARGV)
+ def options(argv = ARGV)
options = MSpecOptions.new "mspec run [options] (FILE|DIRECTORY|GLOB)+", 30, config
options.doc " Ask yourself:"
@@ -32,10 +28,12 @@ class MSpecRun < MSpecScript
options.chdir
options.prefix
options.configure { |f| load f }
+ options.env
options.randomize
options.repeat
options.pretend
options.interrupt
+ options.timeout
options.doc "\n How to modify the guard behavior"
options.unguarded
@@ -51,6 +49,9 @@ class MSpecRun < MSpecScript
options.doc "\n When to perform it"
options.action_filters
+ options.doc "\n Launchable"
+ options.launchable
+
options.doc "\n Help!"
options.debug
options.version MSpec::VERSION
diff --git a/spec/mspec/lib/mspec/commands/mspec-tag.rb b/spec/mspec/lib/mspec/commands/mspec-tag.rb
index 8bc3382e91..9ce9f048c6 100644
--- a/spec/mspec/lib/mspec/commands/mspec-tag.rb
+++ b/spec/mspec/lib/mspec/commands/mspec-tag.rb
@@ -1,5 +1,3 @@
-#!/usr/bin/env ruby
-
require 'mspec/version'
require 'mspec/utils/options'
require 'mspec/utils/script'
@@ -15,7 +13,7 @@ class MSpecTag < MSpecScript
config[:ltags] = []
end
- def options(argv=ARGV)
+ def options(argv = ARGV)
options = MSpecOptions.new "mspec tag [options] (FILE|DIRECTORY|GLOB)+", 30, config
options.doc " Ask yourself:"
@@ -33,6 +31,7 @@ class MSpecTag < MSpecScript
options.pretend
options.unguarded
options.interrupt
+ options.timeout
options.doc "\n How to display their output"
options.formatters
@@ -113,6 +112,7 @@ class MSpecTag < MSpecScript
MSpec.register_mode :pretend
MSpec.register_mode :unguarded
config[:formatter] = false
+ config[:xtags] = []
else
raise ArgumentError, "No recognized action given"
end
diff --git a/spec/mspec/lib/mspec/commands/mspec.rb b/spec/mspec/lib/mspec/commands/mspec.rb
index 6cb1e87a58..a9d94ca354 100755..100644
--- a/spec/mspec/lib/mspec/commands/mspec.rb
+++ b/spec/mspec/lib/mspec/commands/mspec.rb
@@ -1,5 +1,3 @@
-#!/usr/bin/env ruby
-
require 'mspec/version'
require 'mspec/utils/options'
require 'mspec/utils/script'
@@ -21,10 +19,11 @@ class MSpecMain < MSpecScript
config[:launch] = []
end
- def options(argv=ARGV)
+ def options(argv = ARGV)
config[:command] = argv.shift if ["ci", "run", "tag"].include?(argv[0])
options = MSpecOptions.new "mspec [COMMAND] [options] (FILE|DIRECTORY|GLOB)+", 30, config
+ @options = options
options.doc " The mspec command sets up and invokes the sub-commands"
options.doc " (see below) to enable, for instance, running the specs"
@@ -37,11 +36,6 @@ class MSpecMain < MSpecScript
options.targets
- options.on("--warnings", "Don't suppress warnings") do
- config[:flags] << '-w'
- ENV['OUTPUT_WARNINGS'] = '1'
- end
-
options.on("-j", "--multi", "Run multiple (possibly parallel) subprocesses") do
config[:multi] = true
end
@@ -89,72 +83,12 @@ class MSpecMain < MSpecScript
def register; end
def multi_exec(argv)
- MSpec.register_files @files
-
require 'mspec/runner/formatters/multi'
- formatter = MultiFormatter.new
- if config[:formatter]
- warn "formatter options is ignored due to multi option"
- end
+ formatter = config_formatter.extend(MultiFormatter)
- output_files = []
+ require 'mspec/runner/parallel'
processes = cores(@files.size)
- 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 + ["-fy", "-o", name]
- $stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG
- IO.popen([env, *command, close_others: false], "rb+")
- }
-
- puts children.map { |child| child.gets }.uniq
- formatter.start
- last_files = {}
-
- 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
- reply.chomp!('.')
- msg = "A child mspec-run process printed unexpected output on STDOUT"
- if last_file = last_files[io]
- msg += " while running #{last_file}"
- end
- abort "\n#{msg}: #{reply.inspect}"
- end
-
- unless @files.empty?
- file = @files.shift
- last_files[io] = file
- io.puts file
- end
- }
- end
-
- success = true
- children.each { |child|
- child.puts "QUIT"
- _pid, status = Process.wait2(child.pid)
- success &&= status.success?
- child.close
- }
-
- formatter.aggregate_results(output_files)
- formatter.finish
- success
+ ParallelRunner.new(@files, processes, formatter, argv).run
end
def run
@@ -170,8 +104,9 @@ class MSpecMain < MSpecScript
if config[:multi]
exit multi_exec(argv)
else
- $stderr.puts "$ #{argv.join(' ')}"
- $stderr.flush
+ log = config[:options].include?('--error-output') ? $stdout : $stderr
+ log.puts "$ #{argv.join(' ')}"
+ log.flush
exec(*argv, close_others: false)
end
end
diff --git a/spec/mspec/lib/mspec/expectations/expectations.rb b/spec/mspec/lib/mspec/expectations/expectations.rb
index cfdc2b63a3..09852ab557 100644
--- a/spec/mspec/lib/mspec/expectations/expectations.rb
+++ b/spec/mspec/lib/mspec/expectations/expectations.rb
@@ -7,15 +7,33 @@ class SpecExpectationNotFoundError < StandardError
end
end
+class SkippedSpecError < StandardError
+end
+
class SpecExpectation
def self.fail_with(expected, actual)
expected_to_s = expected.to_s
actual_to_s = actual.to_s
if expected_to_s.size + actual_to_s.size > 80
- message = "#{expected_to_s.chomp}\n#{actual_to_s}"
+ message = "#{expected_to_s}\n#{actual_to_s}"
else
message = "#{expected_to_s} #{actual_to_s}"
end
- Kernel.raise SpecExpectationNotMetError, message
+ raise SpecExpectationNotMetError, message
+ end
+
+ def self.fail_predicate(receiver, predicate, args, block, result, expectation)
+ receiver_to_s = MSpec.format(receiver)
+ before_method = predicate.to_s =~ /^[a-z]/ ? "." : " "
+ predicate_to_s = "#{before_method}#{predicate}"
+ predicate_to_s += " " unless args.empty?
+ args_to_s = args.map { |arg| MSpec.format(arg) }.join(', ')
+ args_to_s += " { ... }" if block
+ result_to_s = MSpec.format(result)
+ raise SpecExpectationNotMetError, "Expected #{receiver_to_s}#{predicate_to_s}#{args_to_s}\n#{expectation} but was #{result_to_s}"
+ end
+
+ def self.fail_single_arg_predicate(receiver, predicate, arg, result, expectation)
+ fail_predicate(receiver, predicate, [arg], nil, result, expectation)
end
end
diff --git a/spec/mspec/lib/mspec/expectations/should.rb b/spec/mspec/lib/mspec/expectations/should.rb
index f6d83053f5..c1790e0ac8 100644
--- a/spec/mspec/lib/mspec/expectations/should.rb
+++ b/spec/mspec/lib/mspec/expectations/should.rb
@@ -1,29 +1,41 @@
class Object
NO_MATCHER_GIVEN = Object.new
- def should(matcher = NO_MATCHER_GIVEN)
+ def should(matcher = NO_MATCHER_GIVEN, &block)
MSpec.expectation
- MSpec.actions :expectation, MSpec.current.state
- unless matcher.equal? NO_MATCHER_GIVEN
+ state = MSpec.current.state
+ raise "should outside example" unless state
+ MSpec.actions :expectation, state
+
+ if NO_MATCHER_GIVEN.equal?(matcher)
+ SpecPositiveOperatorMatcher.new(self)
+ else
+ # The block was given to #should syntactically, but it was intended for a matcher like #raise_error
+ matcher.block = block if block
+
unless matcher.matches? self
expected, actual = matcher.failure_message
SpecExpectation.fail_with(expected, actual)
end
- else
- SpecPositiveOperatorMatcher.new(self)
end
end
- def should_not(matcher = NO_MATCHER_GIVEN)
+ def should_not(matcher = NO_MATCHER_GIVEN, &block)
MSpec.expectation
- MSpec.actions :expectation, MSpec.current.state
- unless matcher.equal? NO_MATCHER_GIVEN
+ state = MSpec.current.state
+ raise "should_not outside example" unless state
+ MSpec.actions :expectation, state
+
+ if NO_MATCHER_GIVEN.equal?(matcher)
+ SpecNegativeOperatorMatcher.new(self)
+ else
+ # The block was given to #should_not syntactically, but it was intended for the matcher
+ matcher.block = block if block
+
if matcher.matches? self
expected, actual = matcher.negative_failure_message
SpecExpectation.fail_with(expected, actual)
end
- else
- SpecNegativeOperatorMatcher.new(self)
end
end
end
diff --git a/spec/mspec/lib/mspec/guards/bug.rb b/spec/mspec/lib/mspec/guards/bug.rb
index b1bfc6413e..a6af0ef964 100644
--- a/spec/mspec/lib/mspec/guards/bug.rb
+++ b/spec/mspec/lib/mspec/guards/bug.rb
@@ -1,28 +1,29 @@
require 'mspec/guards/version'
class BugGuard < VersionGuard
- def initialize(bug, version)
+ def initialize(bug, requirement)
@bug = bug
- if String === version
+ if String === requirement
MSpec.deprecate "ruby_bug with a single version", 'an exclusive range ("2.1"..."2.3")'
- @version = SpecVersion.new version, true
+ super(FULL_RUBY_VERSION, requirement)
+ @requirement = SpecVersion.new requirement, true
else
- super(version)
+ super(FULL_RUBY_VERSION, requirement)
end
- @parameters = [@bug, @version]
end
def match?
return false if MSpec.mode? :no_ruby_bug
return false unless PlatformGuard.standard?
- if Range === @version
+
+ if Range === @requirement
super
else
- FULL_RUBY_VERSION <= @version
+ FULL_RUBY_VERSION <= @requirement
end
end
end
-def ruby_bug(bug, version, &block)
- BugGuard.new(bug, version).run_unless(:ruby_bug, &block)
+def ruby_bug(bug, requirement, &block)
+ BugGuard.new(bug, requirement).run_unless(:ruby_bug, &block)
end
diff --git a/spec/mspec/lib/mspec/guards/conflict.rb b/spec/mspec/lib/mspec/guards/conflict.rb
index 7a27671c1e..4930e5734d 100644
--- a/spec/mspec/lib/mspec/guards/conflict.rb
+++ b/spec/mspec/lib/mspec/guards/conflict.rb
@@ -1,6 +1,12 @@
require 'mspec/guards/guard'
+require 'mspec/utils/deprecate'
class ConflictsGuard < SpecGuard
+ def initialize(*args)
+ MSpec.deprecate 'conflicts_with', 'guard -> { condition } do'
+ super(*args)
+ end
+
def match?
# Always convert constants to symbols regardless of version.
constants = Object.constants.map { |x| x.to_sym }
diff --git a/spec/mspec/lib/mspec/guards/feature.rb b/spec/mspec/lib/mspec/guards/feature.rb
index 30984e0cc5..d4c6dd1cde 100644
--- a/spec/mspec/lib/mspec/guards/feature.rb
+++ b/spec/mspec/lib/mspec/guards/feature.rb
@@ -39,3 +39,7 @@ end
def with_feature(*features, &block)
FeatureGuard.new(*features).run_if(:with_feature, &block)
end
+
+def without_feature(*features, &block)
+ FeatureGuard.new(*features).run_unless(:without_feature, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/guard.rb b/spec/mspec/lib/mspec/guards/guard.rb
index 322a08145d..3a6372a660 100644
--- a/spec/mspec/lib/mspec/guards/guard.rb
+++ b/spec/mspec/lib/mspec/guards/guard.rb
@@ -111,7 +111,7 @@ class SpecGuard
def add(example)
record example.description
- MSpec.retrieve(:formatter).tally.counter.guards!
+ MSpec.formatter.tally.counter.guards!
end
def unregister
diff --git a/spec/mspec/lib/mspec/guards/platform.rb b/spec/mspec/lib/mspec/guards/platform.rb
index 96176b8753..fadd8d75ef 100644
--- a/spec/mspec/lib/mspec/guards/platform.rb
+++ b/spec/mspec/lib/mspec/guards/platform.rb
@@ -6,10 +6,8 @@ class PlatformGuard < SpecGuard
case name
when :rubinius
RUBY_ENGINE.start_with?('rbx')
- when :ruby, :jruby, :truffleruby, :ironruby, :macruby, :maglev, :topaz, :opal
- RUBY_ENGINE.start_with?(name.to_s)
else
- raise "unknown implementation #{name}"
+ RUBY_ENGINE.start_with?(name.to_s)
end
end
end
@@ -18,20 +16,23 @@ class PlatformGuard < SpecGuard
implementation? :ruby
end
- HOST_OS = begin
+ PLATFORM = if RUBY_ENGINE == "jruby"
require 'rbconfig'
- RbConfig::CONFIG['host_os'] || RUBY_PLATFORM
- rescue LoadError
+ "#{RbConfig::CONFIG['host_cpu']}-#{RbConfig::CONFIG['host_os']}"
+ else
RUBY_PLATFORM
- end.downcase
+ end
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)/
+ case os
+ when :windows
+ PLATFORM =~ /(mswin|mingw)/
+ when :wsl
+ wsl?
else
- HOST_OS.include?(os.to_s)
+ PLATFORM.include?(os.to_s)
end
end
end
@@ -40,8 +41,48 @@ class PlatformGuard < SpecGuard
os?(:windows)
end
+ def self.wasi?
+ os?(:wasi)
+ end
+
+ def self.wsl?
+ if defined?(@wsl_p)
+ @wsl_p
+ else
+ @wsl_p = `uname -r`.match?(/microsoft/i)
+ end
+ end
+
+ # In bits
+ WORD_SIZE = 1.size * 8
+ deprecate_constant :WORD_SIZE
+
+ # In bits
+ POINTER_SIZE = begin
+ require 'rbconfig/sizeof'
+ RbConfig::SIZEOF["void*"] * 8
+ rescue LoadError
+ [0].pack('j').size * 8
+ end
+
+ # In bits
+ C_LONG_SIZE = if defined?(RbConfig::SIZEOF[])
+ RbConfig::SIZEOF["long"] * 8
+ else
+ [0].pack('l!').size * 8
+ end
+
def self.wordsize?(size)
- size == 8 * 1.size
+ warn "#wordsize? is deprecated, use #c_long_size?"
+ size == WORD_SIZE
+ end
+
+ def self.pointer_size?(size)
+ size == POINTER_SIZE
+ end
+
+ def self.c_long_size?(size)
+ size == C_LONG_SIZE
end
def initialize(*args)
@@ -59,8 +100,13 @@ class PlatformGuard < SpecGuard
case key
when :os
match &&= PlatformGuard.os?(*value)
+ when :pointer_size
+ match &&= PlatformGuard.pointer_size? value
when :wordsize
+ warn ":wordsize is deprecated, use :c_long_size"
match &&= PlatformGuard.wordsize? value
+ when :c_long_size
+ match &&= PlatformGuard::c_long_size? value
end
end
match
diff --git a/spec/mspec/lib/mspec/guards/superuser.rb b/spec/mspec/lib/mspec/guards/superuser.rb
index e92ea7e862..24daf9b26c 100644
--- a/spec/mspec/lib/mspec/guards/superuser.rb
+++ b/spec/mspec/lib/mspec/guards/superuser.rb
@@ -6,10 +6,20 @@ class SuperUserGuard < SpecGuard
end
end
+class RealSuperUserGuard < SpecGuard
+ def match?
+ Process.uid == 0
+ end
+end
+
def as_superuser(&block)
SuperUserGuard.new.run_if(:as_superuser, &block)
end
+def as_real_superuser(&block)
+ RealSuperUserGuard.new.run_if(:as_real_superuser, &block)
+end
+
def as_user(&block)
SuperUserGuard.new.run_unless(:as_user, &block)
end
diff --git a/spec/mspec/lib/mspec/guards/version.rb b/spec/mspec/lib/mspec/guards/version.rb
index cb08fdac73..f5ea1988ae 100644
--- a/spec/mspec/lib/mspec/guards/version.rb
+++ b/spec/mspec/lib/mspec/guards/version.rb
@@ -5,33 +5,68 @@ require 'mspec/guards/guard'
class VersionGuard < SpecGuard
FULL_RUBY_VERSION = SpecVersion.new SpecGuard.ruby_version(:full)
- def initialize(version)
- case version
+ def initialize(version, requirement)
+ version = SpecVersion.new(version) unless SpecVersion === version
+ @version = version
+
+ case requirement
when String
- @version = SpecVersion.new version
+ @requirement = SpecVersion.new requirement
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 "an empty version range end", 'a specific version' if requirement.end.empty?
+ a = SpecVersion.new requirement.begin
+ b = SpecVersion.new requirement.end
+ unless requirement.exclude_end?
MSpec.deprecate "ruby_version_is with an inclusive range", 'an exclusive range ("2.1"..."2.3")'
end
- @version = version.exclude_end? ? a...b : a..b
+ @requirement = requirement.exclude_end? ? a...b : a..b
else
- raise "version must be a String or Range but was a #{version.class}"
+ raise "version must be a String or Range but was a #{requirement.class}"
end
- @parameters = [version]
+ super(@version, @requirement)
end
def match?
- if Range === @version
- @version.include? FULL_RUBY_VERSION
+ if Range === @requirement
+ @requirement.include? @version
+ else
+ @version >= @requirement
+ end
+ end
+
+ @kernel_version = nil
+ def self.kernel_version
+ if @kernel_version
+ @kernel_version
else
- FULL_RUBY_VERSION >= @version
+ if v = RUBY_PLATFORM[/darwin(\d+)/, 1] # build time version
+ uname = v
+ else
+ begin
+ require 'etc'
+ etc = true
+ rescue LoadError
+ etc = false
+ end
+ if etc and Etc.respond_to?(:uname)
+ uname = Etc.uname.fetch(:release)
+ else
+ uname = `uname -r`.chomp
+ end
+ end
+ @kernel_version = uname
end
end
end
-def ruby_version_is(*args, &block)
- VersionGuard.new(*args).run_if(:ruby_version_is, &block)
+def version_is(base_version, requirement, &block)
+ VersionGuard.new(base_version, requirement).run_if(:version_is, &block)
+end
+
+def ruby_version_is(requirement, &block)
+ VersionGuard.new(VersionGuard::FULL_RUBY_VERSION, requirement).run_if(:ruby_version_is, &block)
+end
+
+def kernel_version_is(requirement, &block)
+ VersionGuard.new(VersionGuard.kernel_version, requirement).run_if(:kernel_version_is, &block)
end
diff --git a/spec/mspec/lib/mspec/helpers/datetime.rb b/spec/mspec/lib/mspec/helpers/datetime.rb
index 1520b971ea..84ac86b686 100644
--- a/spec/mspec/lib/mspec/helpers/datetime.rb
+++ b/spec/mspec/lib/mspec/helpers/datetime.rb
@@ -6,7 +6,7 @@
#
# Possible keys are:
# :year, :month, :day, :hour, :minute, :second, :offset and :sg.
-def new_datetime(opts={})
+def new_datetime(opts = {})
require 'date'
value = {
@@ -25,6 +25,7 @@ def new_datetime(opts={})
end
def with_timezone(name, offset = nil, daylight_saving_zone = "")
+ skip "WASI doesn't have TZ concept" if PlatformGuard.wasi?
zone = name.dup
if offset
diff --git a/spec/mspec/lib/mspec/helpers/flunk.rb b/spec/mspec/lib/mspec/helpers/flunk.rb
index 68fb3cadac..84fb3ab39c 100644
--- a/spec/mspec/lib/mspec/helpers/flunk.rb
+++ b/spec/mspec/lib/mspec/helpers/flunk.rb
@@ -1,3 +1,3 @@
-def flunk(msg="This example is a failure")
+def flunk(msg = "This example is a failure")
SpecExpectation.fail_with "Failed:", msg
end
diff --git a/spec/mspec/lib/mspec/helpers/fs.rb b/spec/mspec/lib/mspec/helpers/fs.rb
index fb2c0f702c..67453eb302 100644
--- a/spec/mspec/lib/mspec/helpers/fs.rb
+++ b/spec/mspec/lib/mspec/helpers/fs.rb
@@ -1,12 +1,6 @@
# Copies a file
def cp(source, dest)
- File.open(dest, "wb") do |d|
- File.open(source, "rb") do |s|
- while data = s.read(1024)
- d.write data
- end
- end
- end
+ IO.copy_stream source, dest
end
# Creates each directory in path that does not exist.
@@ -61,7 +55,7 @@ end
# Creates a file +name+. Creates the directory for +name+
# if it does not exist.
-def touch(name, mode="w")
+def touch(name, mode = "w")
mkdir_p File.dirname(name)
File.open(name, mode) do |f|
diff --git a/spec/mspec/lib/mspec/helpers/io.rb b/spec/mspec/lib/mspec/helpers/io.rb
index 57dc0d53a4..2ad14f47a1 100644
--- a/spec/mspec/lib/mspec/helpers/io.rb
+++ b/spec/mspec/lib/mspec/helpers/io.rb
@@ -7,7 +7,7 @@ class IOStub
end
def write(*str)
- self << str.join
+ self << str.join('')
end
def << str
@@ -16,7 +16,7 @@ class IOStub
end
def print(*str)
- write(str.join + $\.to_s)
+ write(str.join('') + $\.to_s)
end
def method_missing(name, *args, &block)
@@ -64,9 +64,7 @@ end
# Creates a "bare" file descriptor (i.e. one that is not associated
# with any Ruby object). The file descriptor can safely be passed
# to IO.new without creating a Ruby object alias to the fd.
-def new_fd(name, mode="w:utf-8")
- mode = options_or_mode(mode)
-
+def new_fd(name, mode = "w:utf-8")
if mode.kind_of? Hash
if mode.key? :mode
mode = mode[:mode]
@@ -75,37 +73,15 @@ def new_fd(name, mode="w:utf-8")
end
end
- IO.sysopen name, fmode(mode)
+ IO.sysopen name, mode
end
# Creates an IO instance for a temporary file name. The file
# must be deleted.
-def new_io(name, mode="w:utf-8")
- 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
+def new_io(name, mode = "w:utf-8")
+ if Hash === mode # Avoid kwargs warnings on Ruby 2.7+
+ File.new(name, **mode)
else
- fmode(oom[:mode] || "r:utf-8")
+ File.new(name, mode)
end
end
diff --git a/spec/mspec/lib/mspec/helpers/numeric.rb b/spec/mspec/lib/mspec/helpers/numeric.rb
index 312aafae35..0b47855cd2 100644
--- a/spec/mspec/lib/mspec/helpers/numeric.rb
+++ b/spec/mspec/lib/mspec/helpers/numeric.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
require 'mspec/guards/platform'
def nan_value
@@ -8,8 +9,18 @@ def infinity_value
1/0.0
end
-def bignum_value(plus=0)
- 0x8000_0000_0000_0000 + plus
+def bignum_value(plus = 0)
+ # Must be >= fixnum_max + 2, so -bignum_value is < fixnum_min
+ # A fixed value has the advantage to be the same numeric value for all Rubies and is much easier to spec
+ (2**64) + plus
+end
+
+def max_long
+ 2**(PlatformGuard::C_LONG_SIZE - 1) - 1
+end
+
+def min_long
+ -(2**(PlatformGuard::C_LONG_SIZE - 1))
end
# This is a bit hairy, but we need to be able to write specs that cover the
@@ -18,7 +29,24 @@ end
# specs based on the relationship between values rather than specific
# values.
if PlatformGuard.standard? or PlatformGuard.implementation? :topaz
- if PlatformGuard.wordsize? 32
+ limits_available = begin
+ require 'rbconfig/sizeof'
+ defined?(RbConfig::LIMITS.[]) && ['FIXNUM_MAX', 'FIXNUM_MIN'].all? do |key|
+ Integer === RbConfig::LIMITS[key]
+ end
+ rescue LoadError
+ false
+ end
+
+ if limits_available
+ def fixnum_max
+ RbConfig::LIMITS['FIXNUM_MAX']
+ end
+
+ def fixnum_min
+ RbConfig::LIMITS['FIXNUM_MIN']
+ end
+ elsif PlatformGuard.c_long_size? 32
def fixnum_max
(2**30) - 1
end
@@ -26,7 +54,7 @@ if PlatformGuard.standard? or PlatformGuard.implementation? :topaz
def fixnum_min
-(2**30)
end
- elsif PlatformGuard.wordsize? 64
+ elsif PlatformGuard.c_long_size? 64
def fixnum_max
(2**62) - 1
end
diff --git a/spec/mspec/lib/mspec/helpers/ruby_exe.rb b/spec/mspec/lib/mspec/helpers/ruby_exe.rb
index f74ed014ce..2e499d6f9a 100644
--- a/spec/mspec/lib/mspec/helpers/ruby_exe.rb
+++ b/spec/mspec/lib/mspec/helpers/ruby_exe.rb
@@ -2,11 +2,12 @@ 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
+# same Ruby interpreter with the same flags as the one running
# the specs and getting the output from running the code.
+#
# If +code+ is a file that exists, it will be run.
-# Otherwise, +code+ should be Ruby code that will be run with
-# the -e command line option. For example:
+# Otherwise, +code+ will be written to a temporary file and be run.
+# For example:
#
# ruby_exe('path/to/some/file.rb')
#
@@ -14,27 +15,24 @@ require 'mspec/helpers/tmp'
#
# `#{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 four
+# keys: :options, :args :env and :exception.
#
-# The ruby_exe helper also accepts an options hash with three
-# keys: :options, :args and :env. For example:
+# For example:
#
# ruby_exe('file.rb', :options => "-w",
-# :args => "> file.txt",
+# :args => "arg1 arg2",
# :env => { :FOO => "bar" })
#
# will be executed as
#
-# `#{RUBY_EXE} -w #{'file.rb'} > file.txt`
+# `#{RUBY_EXE} -w file.rb arg1 arg2`
#
# with access to ENV["FOO"] with value "bar".
#
+# When `exception: false` and Ruby command fails then exception will not be
+# raised.
+#
# If +nil+ is passed for the first argument, the command line
# will be built only from the options hash.
#
@@ -49,38 +47,18 @@ require 'mspec/helpers/tmp'
# 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_ENGINE
-# 3. cwd/(RUBY_ENGINE + $(EXEEXT) || $(exeext) || '')
-# 4. $(bindir)/$(RUBY_INSTALL_NAME)
+# to invoke the mspec-run script.
#
# The value will only be used if the file exists and is executable.
-# The flags will then be appended to the resulting value.
-#
-# 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_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.
+# The flags will then be appended to the resulting value, such that
+# the RUBY_EXE constant contains both the executable and the flags.
#
# Additionally, the flags passed to mspec
# (with -T on the command line or in the config with set :flags)
# will be appended to RUBY_EXE so that the interpreter
# is always called with those flags.
+#
+# Failure of a Ruby command leads to raising exception by default.
def ruby_exe_options(option)
case option
@@ -101,12 +79,12 @@ def ruby_exe_options(option)
end
when :name
require 'rbconfig'
- bin = RUBY_ENGINE + (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
+ bin = RUBY_ENGINE + (RbConfig::CONFIG['EXEEXT'] || '')
File.join(".", bin)
when :install_name
require 'rbconfig'
bin = RbConfig::CONFIG["RUBY_INSTALL_NAME"] || RbConfig::CONFIG["ruby_install_name"]
- bin << (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
+ bin << (RbConfig::CONFIG['EXEEXT'] || '')
File.join(RbConfig::CONFIG['bindir'], bin)
end
end
@@ -129,7 +107,13 @@ def resolve_ruby_exe
raise Exception, "Unable to find a suitable ruby executable."
end
+unless Object.const_defined?(:RUBY_EXE) and RUBY_EXE
+ RUBY_EXE = resolve_ruby_exe
+end
+
def ruby_exe(code = :not_given, opts = {})
+ skip "WASI doesn't provide subprocess" if PlatformGuard.wasi?
+
if opts[:dir]
raise "ruby_exe(..., dir: dir) is no longer supported, use Dir.chdir"
end
@@ -153,10 +137,47 @@ def ruby_exe(code = :not_given, opts = {})
code = tmpfile
end
+ expected_status = opts.fetch(:exit_status, 0)
+
begin
- platform_is_not :opal do
- `#{ruby_cmd(code, opts)}`
+ command = ruby_cmd(code, opts)
+
+ # Try to avoid the extra shell for 2>&1
+ # This is notably useful for TimeoutAction which can then signal the ruby subprocess and not the shell
+ popen_options = []
+ if command.end_with?(' 2>&1')
+ command = command[0...-5]
+ popen_options = [{ err: [:child, :out] }]
end
+
+ output = IO.popen(command, *popen_options) do |io|
+ pid = io.pid
+ MSpec.subprocesses << pid
+ begin
+ io.read
+ ensure
+ MSpec.subprocesses.delete(pid)
+ end
+ end
+
+ status = Process.last_status
+
+ exit_status = if status.exited?
+ status.exitstatus
+ elsif status.signaled?
+ signame = Signal.signame status.termsig
+ raise "No signal name?" unless signame
+ :"SIG#{signame}"
+ else
+ raise SpecExpectationNotMetError, "#{exit_status.inspect} is neither exited? nor signaled?"
+ end
+ if exit_status != expected_status
+ formatted_output = output.lines.map { |line| " #{line}" }.join
+ raise SpecExpectationNotMetError,
+ "Expected exit status is #{expected_status.inspect} but actual is #{exit_status.inspect} for command ruby_exe(#{command.inspect})\nOutput:\n#{formatted_output}"
+ end
+
+ output
ensure
saved_env.each { |key, value| ENV[key] = value }
env.keys.each do |key|
@@ -178,9 +199,7 @@ def ruby_cmd(code, opts = {})
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
+ command = [RUBY_EXE, opts[:options], body, opts[:args]].compact.join(' ')
+ STDERR.puts "\nruby_cmd: #{command}" if ENV["DEBUG_MSPEC_RUBY_CMD"] == "true"
+ command
end
diff --git a/spec/mspec/lib/mspec/helpers/scratch.rb b/spec/mspec/lib/mspec/helpers/scratch.rb
index a6b0c02748..0da3315cd8 100644
--- a/spec/mspec/lib/mspec/helpers/scratch.rb
+++ b/spec/mspec/lib/mspec/helpers/scratch.rb
@@ -14,4 +14,8 @@ module ScratchPad
def self.recorded
@record
end
+
+ def self.inspect
+ "<ScratchPad @record=#{@record.inspect}>"
+ end
end
diff --git a/spec/mspec/lib/mspec/helpers/tmp.rb b/spec/mspec/lib/mspec/helpers/tmp.rb
index 4e1273dcfe..e903dd9f50 100644
--- a/spec/mspec/lib/mspec/helpers/tmp.rb
+++ b/spec/mspec/lib/mspec/helpers/tmp.rb
@@ -3,11 +3,16 @@
# 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_DIR_PID = Process.pid
-SPEC_TEMP_UNIQUIFIER = "0"
+if spec_temp_dir = ENV["SPEC_TEMP_DIR"]
+ spec_temp_dir = File.realdirpath(spec_temp_dir)
+else
+ spec_temp_dir = "#{File.realpath(Dir.pwd)}/rubyspec_temp/#{SPEC_TEMP_DIR_PID}"
+end
+SPEC_TEMP_DIR = spec_temp_dir
-SPEC_TEMP_DIR_PID = Process.pid
+SPEC_TEMP_UNIQUIFIER = +"0"
at_exit do
begin
@@ -30,12 +35,26 @@ all specs are cleaning up temporary files:
end
end
-def tmp(name, uniquify=true)
- Dir.mkdir SPEC_TEMP_DIR unless Dir.exist? SPEC_TEMP_DIR
+def tmp(name, uniquify = true)
+ if Dir.exist? SPEC_TEMP_DIR
+ stat = File.stat(SPEC_TEMP_DIR)
+ if stat.world_writable? and !stat.sticky?
+ raise ArgumentError, "SPEC_TEMP_DIR (#{SPEC_TEMP_DIR}) is world writable but not sticky"
+ end
+ else
+ platform_is_not :windows do
+ umask = File.umask
+ if (umask & 0002) == 0 # o+w
+ raise ArgumentError, "File.umask #=> #{umask.to_s(8)} (world-writable)"
+ end
+ end
+ mkdir_p SPEC_TEMP_DIR
+ end
if uniquify and !name.empty?
slash = name.rindex "/"
index = slash ? slash + 1 : 0
+ name = +name
name.insert index, "#{SPEC_TEMP_UNIQUIFIER.succ!}-"
end
diff --git a/spec/mspec/lib/mspec/helpers/warning.rb b/spec/mspec/lib/mspec/helpers/warning.rb
index 9e093074e5..e3d72b78bd 100644
--- a/spec/mspec/lib/mspec/helpers/warning.rb
+++ b/spec/mspec/lib/mspec/helpers/warning.rb
@@ -1,3 +1,7 @@
+require 'mspec/guards/version'
+
+# You might be looking for #silence_warnings, use #suppress_warning instead.
+# MSpec calls it #suppress_warning for consistency with EnvUtil.suppress_warning in CRuby test/.
def suppress_warning
verbose = $VERBOSE
$VERBOSE = nil
@@ -5,3 +9,13 @@ def suppress_warning
ensure
$VERBOSE = verbose
end
+
+if ruby_version_is("2.7")
+ def suppress_keyword_warning(&block)
+ suppress_warning(&block)
+ end
+else
+ def suppress_keyword_warning
+ yield
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers.rb b/spec/mspec/lib/mspec/matchers.rb
index 8eab73198a..356e4a9f32 100644
--- a/spec/mspec/lib/mspec/matchers.rb
+++ b/spec/mspec/lib/mspec/matchers.rb
@@ -25,6 +25,7 @@ require 'mspec/matchers/have_protected_instance_method'
require 'mspec/matchers/have_public_instance_method'
require 'mspec/matchers/have_singleton_method'
require 'mspec/matchers/include'
+require 'mspec/matchers/include_any_of'
require 'mspec/matchers/infinity'
require 'mspec/matchers/match_yaml'
require 'mspec/matchers/raise_error'
@@ -33,3 +34,4 @@ require 'mspec/matchers/output_to_fd'
require 'mspec/matchers/respond_to'
require 'mspec/matchers/signed_zero'
require 'mspec/matchers/block_caller'
+require 'mspec/matchers/skip'
diff --git a/spec/mspec/lib/mspec/matchers/base.rb b/spec/mspec/lib/mspec/matchers/base.rb
index fc2d36c84a..3534520d88 100644
--- a/spec/mspec/lib/mspec/matchers/base.rb
+++ b/spec/mspec/lib/mspec/matchers/base.rb
@@ -10,98 +10,86 @@ class Module
include MSpecMatchers
end
-class SpecPositiveOperatorMatcher
+class SpecPositiveOperatorMatcher < BasicObject
def initialize(actual)
@actual = actual
end
def ==(expected)
- unless @actual == expected
- SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
- "to equal #{expected.pretty_inspect}")
+ result = @actual == expected
+ unless result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be truthy")
end
end
- def <(expected)
- unless @actual < expected
- SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
- "to be less than #{expected.pretty_inspect}")
+ def !=(expected)
+ result = @actual != expected
+ unless result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be truthy")
end
end
- def <=(expected)
- unless @actual <= expected
- SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
- "to be less than or equal to #{expected.pretty_inspect}")
+ def equal?(expected)
+ result = @actual.equal?(expected)
+ unless result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be truthy")
end
end
- def >(expected)
- unless @actual > expected
- SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
- "to be greater than #{expected.pretty_inspect}")
+ def raise(exception = ::Exception, message = nil, options = nil, &block)
+ matcher = ::RaiseErrorMatcher.new(exception, message, options, &block)
+ unless matcher.matches? @actual
+ expected, actual = matcher.failure_message
+ ::SpecExpectation.fail_with(expected, actual)
end
end
- def >=(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}")
+ def method_missing(name, *args, &block)
+ result = @actual.__send__(name, *args, &block)
+ unless result
+ ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be truthy")
end
end
end
-class SpecNegativeOperatorMatcher
+class SpecNegativeOperatorMatcher < BasicObject
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}")
+ result = @actual == expected
+ if result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be falsy")
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}")
+ def !=(expected)
+ result = @actual != expected
+ if result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be falsy")
end
end
- def >(expected)
- if @actual > expected
- SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
- "not to be greater than #{expected.pretty_inspect}")
+ def equal?(expected)
+ result = @actual.equal?(expected)
+ if result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be falsy")
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}")
+ def raise(exception = ::Exception, message = nil, options = nil, &block)
+ matcher = ::RaiseErrorMatcher.new(exception, message, options, &block)
+ if matcher.matches? @actual
+ expected, actual = matcher.negative_failure_message
+ ::SpecExpectation.fail_with(expected, actual)
end
end
- def =~(expected)
- if @actual =~ expected
- SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
- "not to match #{expected.pretty_inspect}")
+ def method_missing(name, *args, &block)
+ result = @actual.__send__(name, *args, &block)
+ if result
+ ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be falsy")
end
end
end
diff --git a/spec/mspec/lib/mspec/matchers/be_close.rb b/spec/mspec/lib/mspec/matchers/be_close.rb
index 2cf0fba41f..d6a6626f31 100644
--- a/spec/mspec/lib/mspec/matchers/be_close.rb
+++ b/spec/mspec/lib/mspec/matchers/be_close.rb
@@ -1,4 +1,6 @@
TOLERANCE = 0.00003 unless Object.const_defined?(:TOLERANCE)
+# To account for GC, context switches, other processes, load, etc.
+TIME_TOLERANCE = 20.0 unless Object.const_defined?(:TIME_TOLERANCE)
class BeCloseMatcher
def initialize(expected, tolerance)
@@ -8,15 +10,15 @@ class BeCloseMatcher
def matches?(actual)
@actual = actual
- (@actual - @expected).abs < @tolerance
+ (@actual - @expected).abs <= @tolerance
end
def failure_message
- ["Expected #{@expected}", "to be within +/- #{@tolerance} of #{@actual}"]
+ ["Expected #{@actual}", "to be within #{@expected} +/- #{@tolerance}"]
end
def negative_failure_message
- ["Expected #{@expected}", "not to be within +/- #{@tolerance} of #{@actual}"]
+ ["Expected #{@actual}", "not to be within #{@expected} +/- #{@tolerance}"]
end
end
diff --git a/spec/mspec/lib/mspec/matchers/block_caller.rb b/spec/mspec/lib/mspec/matchers/block_caller.rb
index 017bce3cb7..30fab4fc68 100644
--- a/spec/mspec/lib/mspec/matchers/block_caller.rb
+++ b/spec/mspec/lib/mspec/matchers/block_caller.rb
@@ -1,22 +1,24 @@
class BlockingMatcher
def matches?(block)
- started = false
- blocking = true
-
- thread = Thread.new do
- started = true
+ t = Thread.new do
block.call
-
- blocking = false
end
- while !started and status = thread.status and status != "sleep"
- Thread.pass
+ loop do
+ case t.status
+ when "sleep" # blocked
+ t.kill
+ t.join
+ return true
+ when false # terminated normally, so never blocked
+ t.join
+ return false
+ when nil # terminated exceptionally
+ t.value
+ else
+ Thread.pass
+ end
end
- thread.kill
- thread.join
-
- blocking
end
def failure_message
@@ -29,7 +31,7 @@ class BlockingMatcher
end
module MSpecMatchers
- private def block_caller(timeout = 0.1)
+ private def block_caller
BlockingMatcher.new
end
end
diff --git a/spec/mspec/lib/mspec/matchers/complain.rb b/spec/mspec/lib/mspec/matchers/complain.rb
index 4bcb255040..19310c0bbb 100644
--- a/spec/mspec/lib/mspec/matchers/complain.rb
+++ b/spec/mspec/lib/mspec/matchers/complain.rb
@@ -1,22 +1,31 @@
require 'mspec/helpers/io'
class ComplainMatcher
- def initialize(complaint)
- @complaint = complaint
+ def initialize(complaint = nil, options = nil)
+ # the proper solution is to use double splat operator e.g.
+ # def initialize(complaint = nil, **options)
+ # but we are trying to minimize language features required to run MSpec
+ if complaint.is_a?(Hash)
+ @complaint = nil
+ @options = complaint
+ else
+ @complaint = complaint
+ @options = options || {}
+ end
end
def matches?(proc)
@saved_err = $stderr
@verbose = $VERBOSE
+ err = IOStub.new
+
+ $stderr = err
+ $VERBOSE = @options.key?(:verbose) ? @options[:verbose] : false
begin
- err = $stderr = IOStub.new
- $VERBOSE = false
- Thread.current[:in_mspec_complain_matcher] = true
proc.call
ensure
$VERBOSE = @verbose
$stderr = @saved_err
- Thread.current[:in_mspec_complain_matcher] = false
end
@warning = err.to_s
@@ -54,7 +63,7 @@ class ComplainMatcher
end
module MSpecMatchers
- private def complain(complaint=nil)
- ComplainMatcher.new(complaint)
+ private def complain(complaint = nil, options = nil)
+ ComplainMatcher.new(complaint, options)
end
end
diff --git a/spec/mspec/lib/mspec/matchers/eql.rb b/spec/mspec/lib/mspec/matchers/eql.rb
index a855789550..bcab88ebee 100644
--- a/spec/mspec/lib/mspec/matchers/eql.rb
+++ b/spec/mspec/lib/mspec/matchers/eql.rb
@@ -9,13 +9,13 @@ class EqlMatcher
end
def failure_message
- ["Expected #{@actual.pretty_inspect}",
- "to have same value and type as #{@expected.pretty_inspect}"]
+ ["Expected #{MSpec.format(@actual)}",
+ "to have same value and type as #{MSpec.format(@expected)}"]
end
def negative_failure_message
- ["Expected #{@actual.pretty_inspect}",
- "not to have same value or type as #{@expected.pretty_inspect}"]
+ ["Expected #{MSpec.format(@actual)}",
+ "not to have same value or type as #{MSpec.format(@expected)}"]
end
end
diff --git a/spec/mspec/lib/mspec/matchers/equal.rb b/spec/mspec/lib/mspec/matchers/equal.rb
index 5dc77d27ea..5ba4856d82 100644
--- a/spec/mspec/lib/mspec/matchers/equal.rb
+++ b/spec/mspec/lib/mspec/matchers/equal.rb
@@ -9,13 +9,13 @@ class EqualMatcher
end
def failure_message
- ["Expected #{@actual.pretty_inspect}",
- "to be identical to #{@expected.pretty_inspect}"]
+ ["Expected #{MSpec.format(@actual)}",
+ "to be identical to #{MSpec.format(@expected)}"]
end
def negative_failure_message
- ["Expected #{@actual.pretty_inspect}",
- "not to be identical to #{@expected.pretty_inspect}"]
+ ["Expected #{MSpec.format(@actual)}",
+ "not to be identical to #{MSpec.format(@expected)}"]
end
end
diff --git a/spec/mspec/lib/mspec/matchers/equal_element.rb b/spec/mspec/lib/mspec/matchers/equal_element.rb
index 1e9dfbcca1..8da2567fcf 100644
--- a/spec/mspec/lib/mspec/matchers/equal_element.rb
+++ b/spec/mspec/lib/mspec/matchers/equal_element.rb
@@ -37,12 +37,12 @@ class EqualElementMatcher
end
def failure_message
- ["Expected #{@actual.pretty_inspect}",
+ ["Expected #{MSpec.format(@actual)}",
"to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
end
def negative_failure_message
- ["Expected #{@actual.pretty_inspect}",
+ ["Expected #{MSpec.format(@actual)}",
"not to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
end
diff --git a/spec/mspec/lib/mspec/matchers/have_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_instance_method.rb
index 636aaf3e47..9a5a31aa0f 100644
--- a/spec/mspec/lib/mspec/matchers/have_instance_method.rb
+++ b/spec/mspec/lib/mspec/matchers/have_instance_method.rb
@@ -18,7 +18,7 @@ class HaveInstanceMethodMatcher < MethodMatcher
end
module MSpecMatchers
- private def have_instance_method(method, include_super=true)
+ private def have_instance_method(method, include_super = true)
HaveInstanceMethodMatcher.new method, include_super
end
end
diff --git a/spec/mspec/lib/mspec/matchers/have_method.rb b/spec/mspec/lib/mspec/matchers/have_method.rb
index 35dae03af0..e962e69e0a 100644
--- a/spec/mspec/lib/mspec/matchers/have_method.rb
+++ b/spec/mspec/lib/mspec/matchers/have_method.rb
@@ -18,7 +18,7 @@ class HaveMethodMatcher < MethodMatcher
end
module MSpecMatchers
- private def have_method(method, include_super=true)
+ private def have_method(method, include_super = true)
HaveMethodMatcher.new method, include_super
end
end
diff --git a/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb
index 4eb7133055..d32db76c6a 100644
--- a/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb
+++ b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb
@@ -18,7 +18,7 @@ class HavePrivateInstanceMethodMatcher < MethodMatcher
end
module MSpecMatchers
- private def have_private_instance_method(method, include_super=true)
+ private def have_private_instance_method(method, include_super = true)
HavePrivateInstanceMethodMatcher.new method, include_super
end
end
diff --git a/spec/mspec/lib/mspec/matchers/have_private_method.rb b/spec/mspec/lib/mspec/matchers/have_private_method.rb
index 3433d982cc..c74165cfc7 100644
--- a/spec/mspec/lib/mspec/matchers/have_private_method.rb
+++ b/spec/mspec/lib/mspec/matchers/have_private_method.rb
@@ -18,7 +18,7 @@ class HavePrivateMethodMatcher < MethodMatcher
end
module MSpecMatchers
- private def have_private_method(method, include_super=true)
+ private def have_private_method(method, include_super = true)
HavePrivateMethodMatcher.new method, include_super
end
end
diff --git a/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb
index 641d4d0dc2..1deb2f995d 100644
--- a/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb
+++ b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb
@@ -18,7 +18,7 @@ class HaveProtectedInstanceMethodMatcher < MethodMatcher
end
module MSpecMatchers
- private def have_protected_instance_method(method, include_super=true)
+ private def have_protected_instance_method(method, include_super = true)
HaveProtectedInstanceMethodMatcher.new method, include_super
end
end
diff --git a/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb
index 501c0a418e..0e620532c0 100644
--- a/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb
+++ b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb
@@ -18,7 +18,7 @@ class HavePublicInstanceMethodMatcher < MethodMatcher
end
module MSpecMatchers
- private def have_public_instance_method(method, include_super=true)
+ private def have_public_instance_method(method, include_super = true)
HavePublicInstanceMethodMatcher.new method, include_super
end
end
diff --git a/spec/mspec/lib/mspec/matchers/have_singleton_method.rb b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb
index 95d78709ff..b60dd2536b 100644
--- a/spec/mspec/lib/mspec/matchers/have_singleton_method.rb
+++ b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb
@@ -18,7 +18,7 @@ class HaveSingletonMethodMatcher < MethodMatcher
end
module MSpecMatchers
- private def have_singleton_method(method, include_super=true)
+ private def have_singleton_method(method, include_super = true)
HaveSingletonMethodMatcher.new method, include_super
end
end
diff --git a/spec/mspec/lib/mspec/matchers/include.rb b/spec/mspec/lib/mspec/matchers/include.rb
index 0b7eaf3ce2..3f07f35548 100644
--- a/spec/mspec/lib/mspec/matchers/include.rb
+++ b/spec/mspec/lib/mspec/matchers/include.rb
@@ -15,11 +15,11 @@ class IncludeMatcher
end
def failure_message
- ["Expected #{@actual.inspect}", "to include #{@element.inspect}"]
+ ["Expected #{MSpec.format(@actual)}", "to include #{MSpec.format(@element)}"]
end
def negative_failure_message
- ["Expected #{@actual.inspect}", "not to include #{@element.inspect}"]
+ ["Expected #{MSpec.format(@actual)}", "not to include #{MSpec.format(@element)}"]
end
end
diff --git a/spec/mspec/lib/mspec/matchers/include_any_of.rb b/spec/mspec/lib/mspec/matchers/include_any_of.rb
new file mode 100644
index 0000000000..ce097ccf0f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/include_any_of.rb
@@ -0,0 +1,29 @@
+class IncludeAnyOfMatcher
+ def initialize(*expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @expected.each do |e|
+ if @actual.include?(e)
+ return true
+ end
+ end
+ return false
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to include any of #{@expected.inspect}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to include any of #{@expected.inspect}"]
+ end
+end
+
+module MSpecMatchers
+ private def include_any_of(*expected)
+ IncludeAnyOfMatcher.new(*expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/match_yaml.rb b/spec/mspec/lib/mspec/matchers/match_yaml.rb
index 920d85a14f..30561627c3 100644
--- a/spec/mspec/lib/mspec/matchers/match_yaml.rb
+++ b/spec/mspec/lib/mspec/matchers/match_yaml.rb
@@ -30,7 +30,11 @@ class MatchYAMLMatcher
def valid_yaml?(obj)
require 'yaml'
begin
- YAML.load(obj)
+ if YAML.respond_to?(:unsafe_load)
+ YAML.unsafe_load(obj)
+ else
+ YAML.load(obj)
+ end
rescue
false
else
diff --git a/spec/mspec/lib/mspec/matchers/method.rb b/spec/mspec/lib/mspec/matchers/method.rb
index e8cdfa62ff..2b54419faa 100644
--- a/spec/mspec/lib/mspec/matchers/method.rb
+++ b/spec/mspec/lib/mspec/matchers/method.rb
@@ -1,5 +1,5 @@
class MethodMatcher
- def initialize(method, include_super=true)
+ def initialize(method, include_super = true)
@include_super = include_super
@method = method.to_sym
end
diff --git a/spec/mspec/lib/mspec/matchers/output.rb b/spec/mspec/lib/mspec/matchers/output.rb
index b89b6ca0f6..5bb5d55027 100644
--- a/spec/mspec/lib/mspec/matchers/output.rb
+++ b/spec/mspec/lib/mspec/matchers/output.rb
@@ -42,12 +42,12 @@ class OutputMatcher
expected_out = "\n"
actual_out = "\n"
unless @out.nil?
- expected_out += " $stdout: #{@out.inspect}\n"
- actual_out += " $stdout: #{@stdout.inspect}\n"
+ expected_out += " $stdout: #{MSpec.format(@out)}\n"
+ actual_out += " $stdout: #{MSpec.format(@stdout.to_s)}\n"
end
unless @err.nil?
- expected_out += " $stderr: #{@err.inspect}\n"
- actual_out += " $stderr: #{@stderr.inspect}\n"
+ expected_out += " $stderr: #{MSpec.format(@err)}\n"
+ actual_out += " $stderr: #{MSpec.format(@stderr.to_s)}\n"
end
["Expected:#{expected_out}", " got:#{actual_out}"]
end
@@ -61,7 +61,7 @@ class OutputMatcher
end
module MSpecMatchers
- private def output(stdout=nil, stderr=nil)
+ private def output(stdout = nil, stderr = nil)
OutputMatcher.new(stdout, stderr)
end
end
diff --git a/spec/mspec/lib/mspec/matchers/raise_error.rb b/spec/mspec/lib/mspec/matchers/raise_error.rb
index 2f9afdc687..8cba842ce3 100644
--- a/spec/mspec/lib/mspec/matchers/raise_error.rb
+++ b/spec/mspec/lib/mspec/matchers/raise_error.rb
@@ -1,71 +1,105 @@
-require 'mspec/utils/deprecate'
-
class RaiseErrorMatcher
- def initialize(exception, message, &block)
+ FAILURE_MESSAGE_FOR_EXCEPTION = {}.compare_by_identity
+ UNDEF_CAUSE = Object.new
+
+ attr_writer :block
+
+ def initialize(exception, message = nil, options = nil, &block)
+ if message.is_a? Hash
+ @message = nil
+ options = message
+ else
+ @message = message
+ end
+ @cause = options ? options.fetch(:cause, UNDEF_CAUSE) : UNDEF_CAUSE
@exception = exception
- @message = message
@block = block
@actual = nil
end
+ # This #matches? method is unusual because it doesn't always return a boolean but instead
+ # re-raises the original exception if proc.call raises an exception and #matching_exception? is false.
+ # The reasoning is the original exception class matters and we don't want to change it by raising another exception,
+ # so instead we attach the #failure_message and extract it in ExceptionState#message.
def matches?(proc)
@result = proc.call
return false
- rescue Exception => actual
+ rescue Object => actual
@actual = actual
+
if matching_exception?(actual)
+ # The block has its own expectations and will throw an exception if it fails
+ @block[actual] if @block
return true
else
+ FAILURE_MESSAGE_FOR_EXCEPTION[actual] = failure_message
raise actual
end
end
- def matching_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
+ def matching_class?(exc)
+ @exception === exc
+ end
+
+ def matching_message?(exc)
+ case @message
+ when String
+ @message == exc.message
+ when Regexp
+ @message =~ exc.message
+ else
+ true
end
+ end
- # The block has its own expectations and will throw an exception if it fails
- @block[exc] if @block
+ def matching_cause?(exc)
+ case @cause
+ when UNDEF_CAUSE
+ true
+ else
+ @cause == exc.cause
+ end
+ end
- return true
+ def matching_exception?(exc)
+ matching_class?(exc) and matching_message?(exc) and matching_cause?(exc)
end
- def exception_class_and_message(exception_class, message)
- if message
- "#{exception_class} (#{message})"
- else
- "#{exception_class}"
+ def exception_class_and_message_and_cause(exception_class, message, cause)
+ string = "#{exception_class}"
+ prefixed = false
+ prefix = -> { prefixed ? ", " : prefixed = "(" }
+
+ if message != nil
+ string << "#{prefix.()}#{message.inspect}"
end
+
+ if cause != UNDEF_CAUSE
+ string << "#{prefix.()}cause: #{cause.inspect}"
+ end
+
+ string << ")" if prefixed
+
+ string
end
def format_expected_exception
- exception_class_and_message(@exception, @message)
+ exception_class_and_message_and_cause(@exception, @message, @cause)
end
def format_exception(exception)
- exception_class_and_message(exception.class, exception.message)
- end
-
- def format_result(result)
- result.pretty_inspect.chomp
- rescue => e
- "#pretty_inspect raised #{e.class}; A #<#{result.class}>"
+ exception_class_and_message_and_cause(exception.class,
+ @message == nil ? nil : exception.message,
+ @cause == UNDEF_CAUSE ? UNDEF_CAUSE : exception.cause)
end
def failure_message
message = ["Expected #{format_expected_exception}"]
if @actual
- message << "but got #{format_exception(@actual)}"
+ message << "but got: #{format_exception(@actual)}"
else
- message << "but no exception was raised (#{format_result(@result)} was returned)"
+ message << "but no exception was raised (#{MSpec.format(@result)} was returned)"
end
message
@@ -74,14 +108,25 @@ class RaiseErrorMatcher
def negative_failure_message
message = ["Expected to not get #{format_expected_exception}", ""]
unless @actual.class == @exception
- message[1] = "but got #{format_exception(@actual)}"
+ message[1] = "but got: #{format_exception(@actual)}"
end
message
end
end
module MSpecMatchers
- private def raise_error(exception=Exception, message=nil, &block)
- RaiseErrorMatcher.new(exception, message, &block)
+ private def raise_error(exception = Exception, message = nil, options = nil, &block)
+ RaiseErrorMatcher.new(exception, message, options, &block)
+ end
+
+ # CRuby < 4.1 has inconsistent coercion errors:
+ # https://bugs.ruby-lang.org/issues/21864
+ # This matcher ignores the message on CRuby < 4.1
+ # and checks the message for all other cases, including other Rubies
+ private def raise_consistent_error(exception = Exception, message = nil, options = nil, &block)
+ if RUBY_ENGINE == "ruby" and ruby_version_is ""..."4.1"
+ message = nil
+ end
+ RaiseErrorMatcher.new(exception, message, options, &block)
end
end
diff --git a/spec/mspec/lib/mspec/matchers/skip.rb b/spec/mspec/lib/mspec/matchers/skip.rb
new file mode 100644
index 0000000000..7c175d358d
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/skip.rb
@@ -0,0 +1,5 @@
+module MSpecMatchers
+ private def skip(reason = 'no reason')
+ raise SkippedSpecError, reason
+ end
+end
diff --git a/spec/mspec/lib/mspec/mocks/mock.rb b/spec/mspec/lib/mspec/mocks/mock.rb
index b11d469186..c61ba35ea7 100644
--- a/spec/mspec/lib/mspec/mocks/mock.rb
+++ b/spec/mspec/lib/mspec/mocks/mock.rb
@@ -18,20 +18,16 @@ module Mock
@stubs ||= Hash.new { |h,k| h[k] = [] }
end
- def self.replaced_name(obj, sym)
- :"__mspec_#{obj.__id__}_#{sym}__"
+ def self.replaced_name(key)
+ :"__mspec_#{key.last}__"
end
def self.replaced_key(obj, sym)
- [replaced_name(obj, sym), sym]
+ [obj.__id__, 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)
+ def self.replaced?(key)
+ mocks.include?(key) or stubs.include?(key)
end
def self.clear_replaced(key)
@@ -40,22 +36,28 @@ module Mock
end
def self.mock_respond_to?(obj, sym, include_private = false)
- name = replaced_name(obj, :respond_to?)
- if replaced? name
+ key = replaced_key(obj, :respond_to?)
+ if replaced? key
+ name = replaced_name(key)
obj.__send__ name, sym, include_private
else
obj.respond_to? sym, include_private
end
end
- def self.install_method(obj, sym, type=nil)
+ 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
+ if type == :stub and mocks.key?(key)
+ # Defining a stub and there is already a mock, ignore the stub
+ return
+ end
+
+ if (sym == :respond_to? or mock_respond_to?(obj, sym, true)) and !replaced?(key)
+ meta.__send__ :alias_method, replaced_name(key), sym
end
suppress_warning {
@@ -73,6 +75,11 @@ module Mock
MSpec.actions :expectation, MSpec.current.state
end
+ if proxy.mock? and stubs.key?(key)
+ # Defining a mock and there is already a stub, remove the stub
+ stubs.delete key
+ end
+
if proxy.stub?
stubs[key].unshift proxy
else
@@ -87,6 +94,10 @@ module Mock
obj.instance_variable_get(:@name) || obj.inspect
end
+ def self.inspect_args(args)
+ "(#{Array(args).map(&:inspect).join(', ')})"
+ end
+
def self.verify_count
mocks.each do |key, proxies|
obj = objects[key]
@@ -106,7 +117,7 @@ module Mock
end
unless pass
SpecExpectation.fail_with(
- "Mock '#{name_or_inspect obj}' expected to receive '#{key.last}' " + \
+ "Mock '#{name_or_inspect obj}' expected to receive #{key.last}#{inspect_args proxy.arguments} " + \
"#{qualifier.to_s.sub('_', ' ')} #{count} times",
"but received it #{proxy.calls} times")
end
@@ -120,7 +131,7 @@ module Mock
key = replaced_key obj, sym
[mocks, stubs].each do |proxies|
- proxies[key].each do |proxy|
+ proxies.fetch(key, []).each do |proxy|
pass = case proxy.arguments
when :any_args
true
@@ -166,7 +177,7 @@ module Mock
mock_respond_to? obj, *args
else
SpecExpectation.fail_with("Mock '#{name_or_inspect obj}': method #{sym}\n",
- "called with unexpected arguments (#{Array(compare).join(' ')})")
+ "called with unexpected arguments #{inspect_args args}")
end
end
@@ -177,7 +188,7 @@ module Mock
next
end
- replaced = key.first
+ replaced = replaced_name(key)
sym = key.last
meta = obj.singleton_class
diff --git a/spec/mspec/lib/mspec/mocks/object.rb b/spec/mspec/lib/mspec/mocks/object.rb
index 19a50ac4e1..fcaa1caef0 100644
--- a/spec/mspec/lib/mspec/mocks/object.rb
+++ b/spec/mspec/lib/mspec/mocks/object.rb
@@ -15,7 +15,7 @@ class Object
end
end
-def mock(name, options={})
+def mock(name, options = {})
MockObject.new name, options
end
@@ -23,6 +23,6 @@ def mock_int(val)
MockIntObject.new(val)
end
-def mock_numeric(name, options={})
+def mock_numeric(name, options = {})
NumericMockObject.new name, options
end
diff --git a/spec/mspec/lib/mspec/mocks/proxy.rb b/spec/mspec/lib/mspec/mocks/proxy.rb
index f5acc89d62..8473132b0b 100644
--- a/spec/mspec/lib/mspec/mocks/proxy.rb
+++ b/spec/mspec/lib/mspec/mocks/proxy.rb
@@ -1,5 +1,5 @@
class MockObject
- def initialize(name, options={})
+ def initialize(name, options = {})
@name = name
@null = options[:null_object]
end
@@ -11,7 +11,7 @@ class MockObject
end
class NumericMockObject < Numeric
- def initialize(name, options={})
+ def initialize(name, options = {})
@name = name
@null = options[:null_object]
end
@@ -50,7 +50,7 @@ end
class MockProxy
attr_reader :raising, :yielding
- def initialize(type=nil)
+ def initialize(type = nil)
@multiple_returns = nil
@returning = nil
@raising = nil
diff --git a/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb b/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb
new file mode 100644
index 0000000000..abfb6dd0ee
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb
@@ -0,0 +1,84 @@
+class ConstantsLockFile
+ LOCK_FILE_NAME = '.mspec.constants'
+
+ def self.lock_file
+ @prefix ||= File.expand_path(MSpecScript.get(:prefix) || '.')
+ "#{@prefix}/#{LOCK_FILE_NAME}"
+ end
+
+ def self.load
+ if File.exist?(lock_file)
+ File.readlines(lock_file).map(&:chomp)
+ else
+ []
+ end
+ end
+
+ def self.dump(ary)
+ contents = ary.map(&:to_s).uniq.sort.join("\n") + "\n"
+ File.write(lock_file, contents)
+ end
+end
+
+class ConstantLeakError < StandardError
+end
+
+class ConstantsLeakCheckerAction
+ def initialize(save)
+ @save = save
+ @check = !save
+ @constants_locked = ConstantsLockFile.load
+ @exclude_patterns = MSpecScript.get(:toplevel_constants_excludes) || []
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ def start
+ @constants_start = constants_now
+ end
+
+ def before(state)
+ @constants_before = constants_now
+ end
+
+ def after(state)
+ constants = remove_excludes(constants_now - @constants_before - @constants_locked)
+
+ if @check && !constants.empty?
+ MSpec.protect 'Constants leak check' do
+ raise ConstantLeakError, "Top level constants leaked: #{constants.join(', ')}"
+ end
+ end
+ end
+
+ def finish
+ constants = remove_excludes(constants_now - @constants_start - @constants_locked)
+
+ if @save
+ ConstantsLockFile.dump(@constants_locked + constants)
+ end
+
+ if @check && !constants.empty?
+ MSpec.protect 'Global constants leak check' do
+ raise ConstantLeakError, "Top level constants leaked in the whole test suite: #{constants.join(', ')}"
+ end
+ end
+ end
+
+ private
+
+ def constants_now
+ Object.constants.map(&:to_s)
+ end
+
+ def remove_excludes(constants)
+ constants.reject { |name|
+ @exclude_patterns.any? { |pattern| pattern === name }
+ }
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/filter.rb b/spec/mspec/lib/mspec/runner/actions/filter.rb
index 35899c8dc8..b0ad7080da 100644
--- a/spec/mspec/lib/mspec/runner/actions/filter.rb
+++ b/spec/mspec/lib/mspec/runner/actions/filter.rb
@@ -10,7 +10,7 @@ require 'mspec/runner/filters/match'
# trigger the action.
class ActionFilter
- def initialize(tags=nil, descs=nil)
+ def initialize(tags = nil, descs = nil)
@tags = Array(tags)
descs = Array(descs)
@sfilter = descs.empty? ? nil : MatchFilter.new(nil, *descs)
diff --git a/spec/mspec/lib/mspec/runner/actions/leakchecker.rb b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
index e947cda9ff..0a8c9c3252 100644
--- a/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
+++ b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
@@ -24,29 +24,36 @@
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
+class LeakError < StandardError
+end
+
class LeakChecker
+ attr_reader :leaks
+
def initialize
@fd_info = find_fds
@tempfile_info = find_tempfiles
@thread_info = find_threads
@env_info = find_env
@argv_info = find_argv
+ @globals_info = find_globals
@encoding_info = find_encodings
end
- def check(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?
+ def check(state)
+ @state = state
+ @leaks = []
+ check_fd_leak
+ check_tempfile_leak
+ check_thread_leak
+ check_process_leak
+ check_env
+ check_argv
+ check_globals
+ check_encodings
+ check_tracepoints
+ GC.start unless @leaks.empty?
+ @leaks.empty?
end
private
@@ -66,8 +73,7 @@ class LeakChecker
end
end
- def check_fd_leak(test_name)
- leaked = false
+ def check_fd_leak
live1 = @fd_info
if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
m[:close]
@@ -76,12 +82,11 @@ class LeakChecker
fd_closed = live1 - live2
if !fd_closed.empty?
fd_closed.each {|fd|
- puts "Closed file descriptor: #{test_name}: #{fd}"
+ leak "Closed file descriptor: #{fd}"
}
end
fd_leaked = live2 - live1
if !fd_leaked.empty?
- leaked = true
h = {}
ObjectSpace.each_object(IO) {|io|
inspect = io.inspect
@@ -105,19 +110,18 @@ class LeakChecker
str << s
}
end
- puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
+ leak "Leaked file descriptor: #{fd}#{str}"
}
#system("lsof -p #$$") if !fd_leaked.empty?
h.each {|fd, list|
next if list.length <= 1
if 1 < list.count {|io, autoclose, inspect| autoclose }
str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
- puts "Multiple autoclose IO object for a file descriptor:#{str}"
+ leak "Multiple autoclose IO object for a file descriptor:#{str}"
end
}
end
@fd_info = live2
- return leaked
end
def extend_tempfile_counter
@@ -128,19 +132,19 @@ class LeakChecker
attr_accessor :count
end
- def new(data)
+ def new(...)
LeakChecker::TempfileCounter.count += 1
- super(data)
+ super
end
}
LeakChecker.const_set(:TempfileCounter, m)
- class << Tempfile::Remover
+ class << Tempfile
prepend LeakChecker::TempfileCounter
end
end
- def find_tempfiles(prev_count=-1)
+ def find_tempfiles(prev_count = -1)
return [prev_count, []] unless defined? Tempfile
extend_tempfile_counter
count = TempfileCounter.count
@@ -152,132 +156,141 @@ class LeakChecker
end
end
- def check_tempfile_leak(test_name)
+ def check_tempfile_leak
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}"
+ leak "Leaked tempfile: #{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?
+ t != Thread.current && t.alive? &&
+ !(t.thread_variable?(:"\0__detached_thread__") && t.thread_variable_get(:"\0__detached_thread__"))
}
end
- def check_thread_leak(test_name)
+ def check_thread_leak
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}"
+ leak "Finished thread: #{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}"
+ leak "Leaked thread: #{str}"
}
end
@thread_info = live2
- return leaked
end
- def check_process_leak(test_name)
+ def check_process_leak
subprocesses_leaked = Process.waitall
subprocesses_leaked.each { |pid, status|
- puts "Leaked subprocess: #{pid}: #{status}"
+ leak "Leaked subprocess: #{pid}: #{status}"
}
- return !subprocesses_leaked.empty?
end
def find_env
ENV.to_h
end
- def check_env(test_name)
+ def check_env
old_env = @env_info
new_env = find_env
- return false if old_env == new_env
+ return if old_env == new_env
+
(old_env.keys | new_env.keys).sort.each {|k|
if old_env.has_key?(k)
if new_env.has_key?(k)
if old_env[k] != new_env[k]
- puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
+ leak "Environment variable changed : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
end
else
- puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
+ leak "Environment variable changed: #{k.inspect} deleted"
end
else
if new_env.has_key?(k)
- puts "Environment variable changed: #{test_name} : #{k.inspect} added"
+ leak "Environment variable changed: #{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)
+ def check_argv
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}"
+ leak "ARGV changed: #{old_argv.inspect} to #{new_argv.inspect}"
@argv_info = new_argv
- leaked = true
end
- return leaked
+ end
+
+ def find_globals
+ { verbose: $VERBOSE, debug: $DEBUG }
+ end
+
+ def check_globals
+ old_globals = @globals_info
+ new_globals = find_globals
+ if new_globals != old_globals
+ leak "Globals changed: #{old_globals.inspect} to #{new_globals.inspect}"
+ @globals_info = new_globals
+ end
end
def find_encodings
[Encoding.default_internal, Encoding.default_external]
end
- def check_encodings(test_name)
+ def check_encodings
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}"
+ leak "Encoding.default_internal changed: #{old_internal.inspect} to #{new_internal.inspect}"
end
if new_external != old_external
- leaked = true
- puts "Encoding.default_external changed: #{test_name} : #{old_external} to #{new_external}"
+ leak "Encoding.default_external changed: #{old_external.inspect} to #{new_external.inspect}"
end
@encoding_info = [new_internal, new_external]
- return leaked
end
- def puts(*args)
- if @no_leaks
- @no_leaks = false
- print "\n"
+ def check_tracepoints
+ ObjectSpace.each_object(TracePoint) do |tp|
+ if tp.enabled?
+ leak "TracePoint is still enabled: #{tp.inspect}"
+ end
+ end
+ end
+
+ def leak(message)
+ if @leaks.empty?
+ $stderr.puts "\n"
+ $stderr.puts @state.description
end
- super(*args)
+ @leaks << message
+ $stderr.puts message
end
end
@@ -288,14 +301,77 @@ class LeakCheckerAction
end
def start
+ disable_nss_modules
@checker = LeakChecker.new
end
def after(state)
- unless @checker.check(state.description)
+ unless @checker.check(state)
+ leak_messages = @checker.leaks
+ location = state.description
if state.example
- puts state.example.source_location.join(':')
+ location = "#{location}\n#{state.example.source_location.join(':')}"
+ end
+ MSpec.protect(location) do
+ raise LeakError, leak_messages.join("\n")
end
end
end
+
+ private
+
+ # This function is intended to disable all NSS modules when ruby is compiled
+ # against glibc. NSS modules allow the system administrator to load custom
+ # shared objects into all processes using glibc, and use them to customise
+ # the behaviour of username, groupname, hostname, etc lookups. This is
+ # normally configured in the file /etc/nsswitch.conf.
+ # These modules often do things like open cache files or connect to system
+ # daemons like sssd or dbus, which of course means they have open file
+ # descriptors of their own. This can cause the leak-checking functionality
+ # in this file to report that such descriptors have been leaked, and fail
+ # the test suite.
+ # This function uses glibc's __nss_configure_lookup function to override any
+ # configuration in /etc/nsswitch.conf, and just use the built in files/dns
+ # name lookup functionality (which is of course perfectly sufficient for
+ # running ruby/spec).
+ def disable_nss_modules
+ begin
+ require 'fiddle'
+ rescue LoadError
+ # Make sure it's possible to run the test suite on a ruby implementation
+ # which does not (yet?) have Fiddle.
+ return
+ end
+
+ begin
+ libc = Fiddle.dlopen(nil)
+ # Older versions of fiddle don't have Fiddle::Type (and instead rely on Fiddle::TYPE_)
+ # Even older versions of fiddle don't have CONST_STRING,
+ string_type = defined?(Fiddle::TYPE_CONST_STRING) ? Fiddle::TYPE_CONST_STRING : Fiddle::TYPE_VOIDP
+ nss_configure_lookup = Fiddle::Function.new(
+ libc['__nss_configure_lookup'],
+ [string_type, string_type],
+ Fiddle::TYPE_INT
+ )
+ rescue Fiddle::DLError
+ # We're not running with glibc - no need to do this.
+ return
+ end
+
+ nss_configure_lookup.call 'passwd', 'files'
+ nss_configure_lookup.call 'shadow', 'files'
+ nss_configure_lookup.call 'group', 'files'
+ nss_configure_lookup.call 'hosts', 'files dns'
+ nss_configure_lookup.call 'services', 'files'
+ nss_configure_lookup.call 'netgroup', 'files'
+ nss_configure_lookup.call 'automount', 'files'
+ nss_configure_lookup.call 'aliases', 'files'
+ nss_configure_lookup.call 'ethers', 'files'
+ nss_configure_lookup.call 'gshadow', 'files'
+ nss_configure_lookup.call 'initgroups', 'files'
+ nss_configure_lookup.call 'networks', 'files dns'
+ nss_configure_lookup.call 'protocols', 'files'
+ nss_configure_lookup.call 'publickey', 'files'
+ nss_configure_lookup.call 'rpc', 'files'
+ end
end
diff --git a/spec/mspec/lib/mspec/runner/actions/profile.rb b/spec/mspec/lib/mspec/runner/actions/profile.rb
new file mode 100644
index 0000000000..c743d6e3e8
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/profile.rb
@@ -0,0 +1,60 @@
+class ProfileAction
+ def initialize
+ @describe_name = nil
+ @describe_time = nil
+ @describes = []
+ @its = []
+ end
+
+ def register
+ MSpec.register :enter, self
+ MSpec.register :before,self
+ MSpec.register :after, self
+ MSpec.register :finish,self
+ end
+
+ def enter(describe)
+ if @describe_time
+ @describes << [@describe_name, now - @describe_time]
+ end
+
+ @describe_name = describe
+ @describe_time = now
+ end
+
+ def before(state)
+ @it_name = state.it
+ @it_time = now
+ end
+
+ def after(state = nil)
+ @its << [@describe_name, @it_name, now - @it_time]
+ end
+
+ def finish
+ puts "\nProfiling info:"
+
+ desc = @describes.sort { |a,b| b.last <=> a.last }
+ desc.delete_if { |a| a.last <= 0.001 }
+ show = desc[0, 100]
+
+ puts "Top #{show.size} describes:"
+
+ show.each do |des, time|
+ printf "%3.3f - %s\n", time, des
+ end
+
+ its = @its.sort { |a,b| b.last <=> a.last }
+ its.delete_if { |a| a.last <= 0.001 }
+ show = its[0, 100]
+
+ puts "\nTop #{show.size} its:"
+ show.each do |des, it, time|
+ printf "%3.3f - %s %s\n", time, des, it
+ end
+ end
+
+ def now
+ Time.now.to_f
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tag.rb b/spec/mspec/lib/mspec/runner/actions/tag.rb
index 760152b2a3..d40d562451 100644
--- a/spec/mspec/lib/mspec/runner/actions/tag.rb
+++ b/spec/mspec/lib/mspec/runner/actions/tag.rb
@@ -22,7 +22,7 @@ require 'mspec/runner/actions/filter'
# spec description strings
class TagAction < ActionFilter
- def initialize(action, outcome, tag, comment, tags=nil, descs=nil)
+ def initialize(action, outcome, tag, comment, tags = nil, descs = nil)
super tags, descs
@action = action
@outcome = outcome
diff --git a/spec/mspec/lib/mspec/runner/actions/taglist.rb b/spec/mspec/lib/mspec/runner/actions/taglist.rb
index c1aba53794..3097e655d5 100644
--- a/spec/mspec/lib/mspec/runner/actions/taglist.rb
+++ b/spec/mspec/lib/mspec/runner/actions/taglist.rb
@@ -4,7 +4,7 @@ require 'mspec/runner/actions/filter'
# tagged with +tags+. If +tags+ is an empty list, prints out
# descriptions for any specs that are tagged.
class TagListAction
- def initialize(tags=nil)
+ def initialize(tags = nil)
@tags = tags.nil? || tags.empty? ? nil : Array(tags)
@filter = nil
end
diff --git a/spec/mspec/lib/mspec/runner/actions/tally.rb b/spec/mspec/lib/mspec/runner/actions/tally.rb
index 33f937293c..d6ada53bab 100644
--- a/spec/mspec/lib/mspec/runner/actions/tally.rb
+++ b/spec/mspec/lib/mspec/runner/actions/tally.rb
@@ -5,31 +5,31 @@ class Tally
@files = @examples = @expectations = @failures = @errors = @guards = @tagged = 0
end
- def files!(add=1)
+ def files!(add = 1)
@files += add
end
- def examples!(add=1)
+ def examples!(add = 1)
@examples += add
end
- def expectations!(add=1)
+ def expectations!(add = 1)
@expectations += add
end
- def failures!(add=1)
+ def failures!(add = 1)
@failures += add
end
- def errors!(add=1)
+ def errors!(add = 1)
@errors += add
end
- def guards!(add=1)
+ def guards!(add = 1)
@guards += add
end
- def tagged!(add=1)
+ def tagged!(add = 1)
@tagged += add
end
diff --git a/spec/mspec/lib/mspec/runner/actions/timeout.rb b/spec/mspec/lib/mspec/runner/actions/timeout.rb
new file mode 100644
index 0000000000..1200926872
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/timeout.rb
@@ -0,0 +1,145 @@
+class TimeoutAction
+ def initialize(timeout)
+ @timeout = timeout
+ @queue = Queue.new
+ @started = now
+ @fail = false
+ @error_message = "took longer than the configured timeout of #{@timeout}s"
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ private def now
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ end
+
+ private def fetch_item
+ @queue.pop(true)
+ rescue ThreadError
+ nil
+ end
+
+ def start
+ @thread = Thread.new do
+ loop do
+ if action = fetch_item
+ action.call
+ else
+ wakeup_at = @started + @timeout
+ left = wakeup_at - now
+ sleep left if left > 0
+ Thread.pass # Let the main thread run
+
+ if @queue.empty?
+ elapsed = now - @started
+ if elapsed > @timeout
+ if @current_state
+ STDERR.puts "\nExample #{@error_message}:"
+ STDERR.puts "#{@current_state.description}"
+ else
+ STDERR.puts "\nSome code outside an example #{@error_message}"
+ end
+ STDERR.flush
+
+ show_backtraces
+ if MSpec.subprocesses.empty?
+ exit! 2
+ else
+ # Do not exit but signal the subprocess so we can get their output
+ MSpec.subprocesses.each do |pid|
+ kill_wait_one_second :SIGTERM, pid
+ hard_kill :SIGKILL, pid
+ end
+ @fail = true
+ @current_state = nil
+ break # stop this thread, will fail in #after
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def before(state = nil)
+ time = now
+ @queue << -> do
+ @current_state = state
+ @started = time
+ end
+ end
+
+ def after(state = nil)
+ @queue << -> do
+ @current_state = nil
+ end
+
+ if @fail
+ STDERR.puts "\n\nThe last example #{@error_message}. See above for the subprocess stacktrace."
+ exit! 2
+ end
+ end
+
+ def finish
+ @thread.kill
+ @thread.join
+ end
+
+ private def hard_kill(signal, pid)
+ begin
+ Process.kill signal, pid
+ rescue Errno::ESRCH
+ # Process already terminated
+ end
+ end
+
+ private def kill_wait_one_second(signal, pid)
+ begin
+ Process.kill signal, pid
+ sleep 1
+ rescue Errno::ESRCH
+ # Process already terminated
+ end
+ end
+
+ private def show_backtraces
+ java_stacktraces = -> pid {
+ if RUBY_ENGINE == 'truffleruby' || RUBY_ENGINE == 'jruby'
+ STDERR.puts 'Java stacktraces:'
+ kill_wait_one_second :SIGQUIT, pid
+ end
+ }
+
+ if MSpec.subprocesses.empty?
+ java_stacktraces.call Process.pid
+
+ STDERR.puts "\nRuby backtraces:"
+ if defined?(Truffle::Debug.show_backtraces)
+ Truffle::Debug.show_backtraces
+ else
+ Thread.list.each do |thread|
+ unless thread == Thread.current
+ STDERR.puts thread.inspect, thread.backtrace, ''
+ end
+ end
+ end
+ else
+ MSpec.subprocesses.each do |pid|
+ STDERR.puts "\nFor subprocess #{pid}"
+ java_stacktraces.call pid
+
+ if RUBY_ENGINE == 'truffleruby'
+ STDERR.puts "\nRuby backtraces:"
+ kill_wait_one_second :SIGALRM, pid
+ else
+ STDERR.puts "Don't know how to print backtraces of a subprocess on #{RUBY_ENGINE}"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/context.rb b/spec/mspec/lib/mspec/runner/context.rb
index 30d8a4ad1b..bcd83b2465 100644
--- a/spec/mspec/lib/mspec/runner/context.rb
+++ b/spec/mspec/lib/mspec/runner/context.rb
@@ -12,15 +12,14 @@
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
+ MOCK_VERIFY = -> { Mock.verify_count }
+ MOCK_CLEANUP = -> { Mock.cleanup }
+ EXPECTATION_MISSING = -> { raise SpecExpectationNotFoundError }
+
+ def initialize(description, options = nil)
+ raise "#describe options should be a Hash or nil" unless Hash === options or options.nil?
+ @to_s = description.to_s
+ @shared = options && options[:shared]
@parsed = false
@before = { :all => [], :each => [] }
@@ -28,13 +27,10 @@ class ContextState
@pre = {}
@post = {}
@examples = []
+ @state = nil
@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.
@@ -46,7 +42,7 @@ class ContextState
# Returns true if this is a shared +ContextState+. Essentially, when
# created with: describe "Something", :shared => true { ... }
def shared?
- return @options[:shared]
+ @shared
end
# Set the parent (enclosing) +ContextState+ for this state. Creates
@@ -127,6 +123,7 @@ class ContextState
# Creates an ExampleState instance for the block and stores it
# in a list of examples to evaluate unless the example is filtered.
def it(desc, &block)
+ raise "nested #it" if @state
example = ExampleState.new(self, desc, block)
MSpec.actions :add, example
return if MSpec.guarded?
@@ -174,7 +171,7 @@ class ContextState
# 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)
+ def protect(what, blocks, check = true)
return true if check and MSpec.mode? :pretend
Array(blocks).all? { |block| MSpec.protect what, &block }
end
@@ -205,7 +202,7 @@ class ContextState
if protect "before :all", pre(:all)
@examples.each do |state|
MSpec.repeat do
- @state = state
+ @state = state
example = state.example
MSpec.actions :before, state
@@ -213,21 +210,22 @@ class ContextState
MSpec.clear_expectations
if example
passed = protect nil, example
+ passed = protect nil, -> { MSpec.actions :passed, state, example } if passed
MSpec.actions :example, state, example
- protect nil, @expectation_missing unless MSpec.expectation? or !passed
+ protect nil, EXPECTATION_MISSING if !MSpec.expectation? and passed
end
end
protect "after :each", post(:each)
- protect "Mock.verify_count", @mock_verify
+ protect "Mock.verify_count", MOCK_VERIFY
- protect "Mock.cleanup", @mock_cleanup
+ protect "Mock.cleanup", MOCK_CLEANUP
MSpec.actions :after, state
@state = nil
end
end
protect "after :all", post(:all)
else
- protect "Mock.cleanup", @mock_cleanup
+ protect "Mock.cleanup", MOCK_CLEANUP
end
MSpec.actions :leave
diff --git a/spec/mspec/lib/mspec/runner/evaluate.rb b/spec/mspec/lib/mspec/runner/evaluate.rb
index ecf7460a90..396a84c118 100644
--- a/spec/mspec/lib/mspec/runner/evaluate.rb
+++ b/spec/mspec/lib/mspec/runner/evaluate.rb
@@ -19,7 +19,7 @@ class SpecEvaluate
# 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)
+ def format(ruby, newline = true)
if ruby.include?("\n")
lines = ruby.each_line.to_a
if /( *)/ =~ lines.first
@@ -49,6 +49,6 @@ class SpecEvaluate
end
end
-def evaluate(str, desc=nil, &block)
+def evaluate(str, desc = nil, &block)
SpecEvaluate.new(str, desc).define(&block)
end
diff --git a/spec/mspec/lib/mspec/runner/example.rb b/spec/mspec/lib/mspec/runner/example.rb
index 19eb29b079..0d9f0d618c 100644
--- a/spec/mspec/lib/mspec/runner/example.rb
+++ b/spec/mspec/lib/mspec/runner/example.rb
@@ -3,12 +3,12 @@ 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
+ attr_reader :context, :it, :example
- def initialize(context, it, example=nil)
- @context = context
- @it = it
- @example = example
+ def initialize(context, it, example = nil)
+ @context = context
+ @it = it
+ @example = example
end
def context=(context)
@@ -25,8 +25,8 @@ class ExampleState
end
def filtered?
- incl = MSpec.retrieve(:include) || []
- excl = MSpec.retrieve(:exclude) || []
+ incl = MSpec.include
+ excl = MSpec.exclude
included = incl.empty? || incl.any? { |f| f === description }
included &&= excl.empty? || !excl.any? { |f| f === description }
!included
diff --git a/spec/mspec/lib/mspec/runner/exception.rb b/spec/mspec/lib/mspec/runner/exception.rb
index 0d9bb43105..23375733e6 100644
--- a/spec/mspec/lib/mspec/runner/exception.rb
+++ b/spec/mspec/lib/mspec/runner/exception.rb
@@ -6,6 +6,7 @@ class ExceptionState
def initialize(state, location, exception)
@exception = exception
+ @failure = exception.class == SpecExpectationNotMetError || exception.class == SpecExpectationNotFoundError
@description = location ? "An exception occurred during: #{location}" : ""
if state
@@ -19,25 +20,35 @@ class ExceptionState
end
def failure?
- [SpecExpectationNotMetError, SpecExpectationNotFoundError].any? { |e| @exception.is_a? e }
+ @failure
end
def message
- if @exception.message.empty?
- "<No message>"
- elsif @exception.class == SpecExpectationNotMetError ||
- @exception.class == SpecExpectationNotFoundError
- @exception.message
+ message = @exception.message
+ message = "<No message>" if message.empty?
+
+ if @failure
+ message
+ elsif raise_error_message = RaiseErrorMatcher::FAILURE_MESSAGE_FOR_EXCEPTION[@exception]
+ raise_error_message.join("\n")
else
- "#{@exception.class}: #{@exception.message}"
+ "#{@exception.class}: #{message}"
end
end
def backtrace
- @backtrace_filter ||= MSpecScript.config[:backtrace_filter]
+ @backtrace_filter ||= MSpecScript.config[:backtrace_filter] || %r{(?:/bin/mspec|/lib/mspec/)}
bt = @exception.backtrace || []
-
- bt.select { |line| $MSPEC_DEBUG or @backtrace_filter !~ line }.join("\n")
+ unless $MSPEC_DEBUG
+ # Exclude <internal: entries inside MSpec code, so only after the first ignored entry
+ first_excluded_line = bt.index { |line| @backtrace_filter =~ line }
+ if first_excluded_line
+ bt = bt[0...first_excluded_line] + bt[first_excluded_line..-1].reject { |line|
+ @backtrace_filter =~ line || /^<internal:/ =~ line
+ }
+ end
+ end
+ bt.join("\n")
end
end
diff --git a/spec/mspec/lib/mspec/runner/filters/regexp.rb b/spec/mspec/lib/mspec/runner/filters/regexp.rb
index 2bd1448d3f..097ec6a755 100644
--- a/spec/mspec/lib/mspec/runner/filters/regexp.rb
+++ b/spec/mspec/lib/mspec/runner/filters/regexp.rb
@@ -1,7 +1,23 @@
-require 'mspec/runner/filters/match'
+class RegexpFilter
+ def initialize(what, *regexps)
+ @what = what
+ @regexps = to_regexp(*regexps)
+ end
+
+ def ===(string)
+ @regexps.any? { |regexp| regexp === string }
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
-class RegexpFilter < MatchFilter
- def to_regexp(*strings)
- strings.map { |str| Regexp.new str }
+ def to_regexp(*regexps)
+ regexps.map { |str| Regexp.new str }
end
+ private :to_regexp
end
diff --git a/spec/mspec/lib/mspec/runner/formatters.rb b/spec/mspec/lib/mspec/runner/formatters.rb
index d085031a12..66f515ddff 100644
--- a/spec/mspec/lib/mspec/runner/formatters.rb
+++ b/spec/mspec/lib/mspec/runner/formatters.rb
@@ -7,6 +7,7 @@ require 'mspec/runner/formatters/summary'
require 'mspec/runner/formatters/unit'
require 'mspec/runner/formatters/spinner'
require 'mspec/runner/formatters/method'
+require 'mspec/runner/formatters/stats'
require 'mspec/runner/formatters/yaml'
require 'mspec/runner/formatters/profile'
require 'mspec/runner/formatters/junit'
diff --git a/spec/mspec/lib/mspec/runner/formatters/base.rb b/spec/mspec/lib/mspec/runner/formatters/base.rb
new file mode 100644
index 0000000000..882e15c8c2
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/base.rb
@@ -0,0 +1,150 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/actions/tally'
+require 'mspec/utils/options'
+
+if ENV['CHECK_LEAKS']
+ require 'mspec/runner/actions/leakchecker'
+end
+
+if ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS']
+ require 'mspec/runner/actions/constants_leak_checker'
+end
+
+class BaseFormatter
+ attr_reader :exceptions, :timer, :tally
+
+ def initialize(out = nil)
+ @current_state = nil
+ @exception = false
+ @failure = false
+ @exceptions = []
+
+ @count = 0 # For subclasses
+
+ if out
+ @out = File.open out, "w"
+ else
+ @out = $stdout
+ end
+
+ err = MSpecOptions.latest && MSpecOptions.latest.config[:error_output]
+ if err
+ @err = (err == 'stderr') ? $stderr : File.open(err, "w")
+ else
+ @err = @out
+ end
+ end
+
+ # Creates the +TimerAction+ and +TallyAction+ instances and registers them.
+ def register
+ (@timer = TimerAction.new).register
+ (@tally = TallyAction.new).register
+ @counter = @tally.counter
+
+ if ENV['CHECK_LEAKS']
+ LeakCheckerAction.new.register
+ end
+
+ if (ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS']) && ENV['CHECK_CONSTANT_LEAKS'] != 'false'
+ save = ENV['CHECK_LEAKS'] == 'save' || ENV['CHECK_CONSTANT_LEAKS'] == 'save'
+ ConstantsLeakCheckerAction.new(save).register
+ end
+
+ MSpec.register :abort, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :exception, self
+ MSpec.register :finish, self
+ end
+
+ def abort
+ if @current_state
+ puts "\naborting example: #{@current_state.description}"
+ end
+ end
+
+ # Returns true if any exception is raised while running
+ # an example. This flag is reset before each example
+ # is evaluated.
+ def exception?
+ @exception
+ end
+
+ # Returns true if all exceptions during the evaluation
+ # of an example are failures rather than errors. See
+ # <tt>ExceptionState#failure</tt>. This flag is reset
+ # before each example is evaluated.
+ def failure?
+ @failure
+ end
+
+ # Callback for the MSpec :before event. Resets the
+ # +#exception?+ and +#failure+ flags.
+ def before(state = nil)
+ @current_state = state
+ @failure = @exception = false
+ end
+
+ # Callback for the MSpec :exception event. Stores the
+ # +ExceptionState+ object to generate the list of backtraces
+ # after all the specs are run. Also updates the internal
+ # +#exception?+ and +#failure?+ flags.
+ def exception(exception)
+ @count += 1
+ @failure = @exception ? @failure && exception.failure? : exception.failure?
+ @exception = true
+ @exceptions << exception
+ end
+
+ # Callback for the MSpec :after event.
+ def after(state = nil)
+ @current_state = nil
+ end
+
+ # Callback for the MSpec :start event. Calls :after event.
+ # Defined here, in the base class, and used by MultiFormatter.
+ def start
+ after
+ end
+
+ # Callback for the MSpec :unload event. Calls :after event.
+ # Defined here, in the base class, and used by MultiFormatter.
+ def unload
+ after
+ end
+
+ # Callback for the MSpec :finish event. Prints a description
+ # and backtrace for every exception that occurred while
+ # evaluating the examples.
+ def finish
+ print "\n"
+
+ if MSpecOptions.latest && MSpecOptions.latest.config[:print_skips]
+ print "\nSkips:\n" unless MSpec.skips.empty?
+ MSpec.skips.each do |skip, block|
+ print "#{skip.message} in #{(block.source_location || ['?']).join(':')}\n"
+ end
+ end
+
+ count = 0
+ @exceptions.each do |exc|
+ count += 1
+ print_exception(exc, count)
+ end
+ print "\n#{@timer.format}\n\n#{@tally.format}\n"
+ end
+
+ def print_exception(exc, count)
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ @err.print "\n#{count})\n#{exc.description} #{outcome}\n"
+ @err.print exc.message, "\n"
+ @err.print exc.backtrace, "\n"
+ end
+
+ # A convenience method to allow printing to different outputs.
+ def print(*args)
+ @out.print(*args)
+ @out.flush
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/describe.rb b/spec/mspec/lib/mspec/runner/formatters/describe.rb
index 176bd79279..fc4122d13b 100644
--- a/spec/mspec/lib/mspec/runner/formatters/describe.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/describe.rb
@@ -1,5 +1,4 @@
require 'mspec/runner/formatters/dotted'
-require 'mspec/runner/actions/tally'
class DescribeFormatter < DottedFormatter
# Callback for the MSpec :finish event. Prints a summary of
diff --git a/spec/mspec/lib/mspec/runner/formatters/dotted.rb b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
index 61c8e4c27c..672cdf81dc 100644
--- a/spec/mspec/lib/mspec/runner/formatters/dotted.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
@@ -1,77 +1,9 @@
-require 'mspec/expectations/expectations'
-require 'mspec/runner/actions/timer'
-require 'mspec/runner/actions/tally'
-require 'mspec/runner/actions/leakchecker' if ENV['CHECK_LEAKS']
+require 'mspec/runner/formatters/base'
-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.
+class DottedFormatter < BaseFormatter
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
+ super
+ MSpec.register :after, self
end
# Callback for the MSpec :after event. Prints an indicator
@@ -80,38 +12,12 @@ class DottedFormatter
# F = An SpecExpectationNotMetError was raised
# E = Any exception other than SpecExpectationNotMetError
def after(state = nil)
- @current_state = nil
+ super(state)
- unless exception?
- print "."
- else
+ if exception?
print failure? ? "F" : "E"
+ else
+ print "."
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
index 6db72af4ff..65cfb1f75b 100644
--- a/spec/mspec/lib/mspec/runner/formatters/file.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/file.rb
@@ -14,6 +14,11 @@ class FileFormatter < DottedFormatter
MSpec.register :unload, self
end
- alias_method :load, :before
- alias_method :unload, :after
+ def load(state = nil)
+ before(state)
+ end
+
+ def unload(state = nil)
+ after(state)
+ end
end
diff --git a/spec/mspec/lib/mspec/runner/formatters/html.rb b/spec/mspec/lib/mspec/runner/formatters/html.rb
index fd64cd0d20..e37e89a088 100644
--- a/spec/mspec/lib/mspec/runner/formatters/html.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/html.rb
@@ -1,7 +1,6 @@
-require 'mspec/expectations/expectations'
-require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/base'
-class HtmlFormatter < DottedFormatter
+class HtmlFormatter < BaseFormatter
def register
super
MSpec.register :start, self
@@ -44,13 +43,14 @@ EOH
end
def exception(exception)
- super
+ super(exception)
outcome = exception.failure? ? "FAILED" : "ERROR"
print %[<li class="fail">- #{exception.it} (<a href="#details-#{@count}">]
print %[#{outcome} - #{@count}</a>)</li>\n]
end
- def after(state)
+ def after(state = nil)
+ super(state)
print %[<li class="pass">- #{state.it}</li>\n] unless exception?
end
diff --git a/spec/mspec/lib/mspec/runner/formatters/junit.rb b/spec/mspec/lib/mspec/runner/formatters/junit.rb
index 76d46c2414..6351ccbce9 100644
--- a/spec/mspec/lib/mspec/runner/formatters/junit.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/junit.rb
@@ -1,19 +1,18 @@
-require 'mspec/expectations/expectations'
require 'mspec/runner/formatters/yaml'
class JUnitFormatter < YamlFormatter
- def initialize(out=nil)
- super
+ def initialize(out = nil)
+ super(out)
@tests = []
end
def after(state = nil)
- super
+ super(state)
@tests << {:test => state, :exception => false} unless exception?
end
def exception(exception)
- super
+ super(exception)
@tests << {:test => exception, :exception => true}
end
@@ -25,7 +24,7 @@ class JUnitFormatter < YamlFormatter
errors = @tally.counter.errors
failures = @tally.counter.failures
- printf <<-XML
+ print <<-XML
<?xml version="1.0" encoding="UTF-8" ?>
<testsuites
@@ -43,8 +42,8 @@ class JUnitFormatter < YamlFormatter
@tests.each do |h|
description = encode_for_xml h[:test].description
- printf <<-XML, "Spec", description, 0.0
- <testcase classname="%s" name="%s" time="%f">
+ print <<-XML
+ <testcase classname="Spec" name="#{description}" time="0.0">
XML
if h[:exception]
outcome = h[:test].failure? ? "failure" : "error"
diff --git a/spec/mspec/lib/mspec/runner/formatters/launchable.rb b/spec/mspec/lib/mspec/runner/formatters/launchable.rb
new file mode 100644
index 0000000000..f738781c71
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/launchable.rb
@@ -0,0 +1,88 @@
+module LaunchableFormatter
+ def self.extend_object(obj)
+ super
+ obj.init
+ end
+
+ def self.setDir(dir)
+ @@path = File.join(dir, "#{rand.to_s}.json")
+ self
+ end
+
+ def init
+ @timer = nil
+ @tests = []
+ end
+
+ def before(state = nil)
+ super
+ @timer = TimerAction.new
+ @timer.start
+ end
+
+ def after(state = nil)
+ super
+ @timer.finish
+ file = MSpec.file
+ return if file.nil? || state&.example.nil? || exception?
+
+ @tests << {:test => state, :file => file, :exception => false, duration: @timer.elapsed}
+ end
+
+ def exception(exception)
+ super
+ @timer.finish
+ file = MSpec.file
+ return if file.nil?
+
+ @tests << {:test => exception, :file => file, :exception => true, duration: @timer.elapsed}
+ end
+
+ def finish
+ super
+
+ require_relative '../../../../../../tool/lib/launchable'
+
+ @writer = writer = Launchable::JsonStreamWriter.new(@@path)
+ @writer.write_array('testCases')
+ at_exit {
+ @writer.close
+ }
+
+ repo_path = File.expand_path("#{__dir__}/../../../../../../")
+
+ @tests.each do |t|
+ testcase = t[:test].description
+ relative_path = t[:file].delete_prefix("#{repo_path}/")
+ # The test path is a URL-encoded representation.
+ # https://github.com/launchableinc/cli/blob/v1.81.0/launchable/testpath.py#L18
+ test_path = {file: relative_path, testcase: testcase}.map{|key, val|
+ "#{encode_test_path_component(key)}=#{encode_test_path_component(val)}"
+ }.join('#')
+
+ status = 'TEST_PASSED'
+ if t[:exception]
+ message = t[:test].message
+ backtrace = t[:test].backtrace
+ e = "#{message}\n#{backtrace}"
+ status = 'TEST_FAILED'
+ end
+
+ @writer.write_object(
+ {
+ testPath: test_path,
+ status: status,
+ duration: t[:duration],
+ createdAt: Time.now.to_s,
+ stderr: e,
+ stdout: nil
+ }
+ )
+ end
+ end
+
+ private
+ def encode_test_path_component component
+ component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26').tr("\x00-\x08", "")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/method.rb b/spec/mspec/lib/mspec/runner/formatters/method.rb
index ff115193fd..925858c845 100644
--- a/spec/mspec/lib/mspec/runner/formatters/method.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/method.rb
@@ -1,18 +1,18 @@
-require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/base'
-class MethodFormatter < DottedFormatter
+class MethodFormatter < BaseFormatter
attr_accessor :methods
- def initialize(out=nil)
- super
+ def initialize(out = nil)
+ super(out)
@methods = Hash.new do |h, k|
- hash = {}
- hash[:examples] = 0
- hash[:expectations] = 0
- hash[:failures] = 0
- hash[:errors] = 0
- hash[:exceptions] = []
- h[k] = hash
+ h[k] = {
+ examples: 0,
+ expectations: 0,
+ failures: 0,
+ errors: 0,
+ exceptions: []
+ }
end
end
@@ -34,7 +34,7 @@ class MethodFormatter < DottedFormatter
# Resets the tallies so the counts are only for this
# example.
def before(state)
- super
+ super(state)
# The pattern for a method name is not correctly
# restrictive but it is simplistic and useful
@@ -60,7 +60,9 @@ class MethodFormatter < DottedFormatter
# Callback for the MSpec :after event. Sets or adds to
# tallies for the example block.
- def after(state)
+ def after(state = nil)
+ super(state)
+
h = methods[@key]
h[:examples] += tally.counter.examples
h[:expectations] += tally.counter.expectations
diff --git a/spec/mspec/lib/mspec/runner/formatters/multi.rb b/spec/mspec/lib/mspec/runner/formatters/multi.rb
index f69055025f..fa1da3766b 100644
--- a/spec/mspec/lib/mspec/runner/formatters/multi.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/multi.rb
@@ -1,13 +1,24 @@
-require 'mspec/runner/formatters/spinner'
+module MultiFormatter
+ def self.extend_object(obj)
+ super
+ obj.multi_initialize
+ end
-class MultiFormatter < SpinnerFormatter
- def initialize(out=nil)
- super(out)
- @counter = @tally = Tally.new
+ def multi_initialize
+ @tally = TallyAction.new
+ @counter = @tally.counter
@timer = TimerAction.new
@timer.start
end
+ def register
+ super
+
+ MSpec.register :start, self
+ MSpec.register :unload, self
+ MSpec.unregister :before, self
+ end
+
def aggregate_results(files)
require 'yaml'
@@ -15,23 +26,22 @@ class MultiFormatter < SpinnerFormatter
@exceptions = []
files.each do |file|
- d = File.open(file, "r") { |f| YAML.load f }
+ contents = File.read(file)
+ d = YAML.load(contents)
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']
+ if d # The file might be empty if the child process died
+ @exceptions += Array(d['exceptions'])
+ @counter.files! d['files']
+ @counter.examples! d['examples']
+ @counter.expectations! d['expectations']
+ @counter.errors! d['errors']
+ @counter.failures! d['failures']
+ end
end
end
def print_exception(exc, count)
- print "\n#{count})\n#{exc}\n"
- end
-
- def finish
- super(false)
+ @err.print "\n#{count})\n#{exc}\n"
end
end
diff --git a/spec/mspec/lib/mspec/runner/formatters/profile.rb b/spec/mspec/lib/mspec/runner/formatters/profile.rb
index 498cd4a3b7..38ef5b12ed 100644
--- a/spec/mspec/lib/mspec/runner/formatters/profile.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/profile.rb
@@ -1,9 +1,9 @@
-require 'mspec/expectations/expectations'
require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/actions/profile'
class ProfileFormatter < DottedFormatter
- def initialize(out=nil)
- super
+ def initialize(out = nil)
+ super(out)
@describe_name = nil
@describe_time = nil
@@ -12,59 +12,7 @@ class ProfileFormatter < DottedFormatter
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
-
+ (@profile = ProfileAction.new).register
super
end
end
diff --git a/spec/mspec/lib/mspec/runner/formatters/specdoc.rb b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
index 29adde3c5c..d3a5c3d729 100644
--- a/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
@@ -1,7 +1,6 @@
-require 'mspec/expectations/expectations'
-require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/base'
-class SpecdocFormatter < DottedFormatter
+class SpecdocFormatter < BaseFormatter
def register
super
MSpec.register :enter, self
@@ -16,7 +15,7 @@ class SpecdocFormatter < DottedFormatter
# Callback for the MSpec :before event. Prints the
# +it+ block string.
def before(state)
- super
+ super(state)
print "- #{state.it}"
end
@@ -25,17 +24,18 @@ class SpecdocFormatter < DottedFormatter
# 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
+ # block description string so that each description
# string has an associated 'ERROR' or 'FAILED'
def exception(exception)
print "\n- #{exception.it}" if exception?
- super
+ super(exception)
print " (#{exception.failure? ? 'FAILED' : 'ERROR'} - #{@count})"
end
# Callback for the MSpec :after event. Prints a
# newline to finish the description string output.
- def after(state)
+ def after(state = nil)
+ super(state)
print "\n"
end
end
diff --git a/spec/mspec/lib/mspec/runner/formatters/spinner.rb b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
index f6f35cc476..817d8c02be 100644
--- a/spec/mspec/lib/mspec/runner/formatters/spinner.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
@@ -1,14 +1,13 @@
-require 'mspec/expectations/expectations'
-require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/base'
-class SpinnerFormatter < DottedFormatter
+class SpinnerFormatter < BaseFormatter
attr_reader :length
Spins = %w!| / - \\!
HOUR = 3600
MIN = 60
- def initialize(out=nil)
+ def initialize(out = nil)
super(nil)
@which = 0
@@ -28,7 +27,6 @@ class SpinnerFormatter < DottedFormatter
MSpec.register :start, self
MSpec.register :unload, self
- MSpec.unregister :before, self
end
def length=(length)
@@ -80,7 +78,7 @@ class SpinnerFormatter < DottedFormatter
# Callback for the MSpec :start event. Stores the total
# number of files that will be processed.
def start
- @total = MSpec.retrieve(:files).size
+ @total = MSpec.files_array.size
compute_progress
print progress_line
end
@@ -102,16 +100,12 @@ class SpinnerFormatter < DottedFormatter
clear_progress_line
print_exception(exception, @count)
+ exceptions.clear
end
# Callback for the MSpec :after event. Updates the spinner.
- def after(state)
+ def after(state = nil)
+ super(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/stats.rb b/spec/mspec/lib/mspec/runner/formatters/stats.rb
new file mode 100644
index 0000000000..8cff96d145
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/stats.rb
@@ -0,0 +1,57 @@
+require 'mspec/runner/formatters/base'
+
+class StatsPerFileFormatter < BaseFormatter
+ def initialize(out = nil)
+ super(out)
+ @data = {}
+ @root = File.expand_path(MSpecScript.get(:prefix) || '.')
+ end
+
+ def register
+ super
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ # Resets the tallies so the counts are only for this file.
+ def load
+ tally.counter.examples = 0
+ tally.counter.errors = 0
+ tally.counter.failures = 0
+ tally.counter.tagged = 0
+ end
+
+ def unload
+ file = format_file MSpec.file
+
+ raise if @data.key?(file)
+ @data[file] = {
+ examples: tally.counter.examples,
+ errors: tally.counter.errors,
+ failures: tally.counter.failures,
+ tagged: tally.counter.tagged,
+ }
+ end
+
+ def finish
+ width = @data.keys.max_by(&:size).size
+ f = "%3d"
+ @data.each_pair do |file, data|
+ total = data[:examples]
+ passing = total - data[:errors] - data[:failures] - data[:tagged]
+ puts "#{file.ljust(width)} #{f % passing}/#{f % total}"
+ end
+
+ require 'yaml'
+ yaml = YAML.dump(@data)
+ File.write "results-#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}.yml", yaml
+ end
+
+ private def format_file(file)
+ if file.start_with?(@root)
+ file[@root.size+1..-1]
+ else
+ raise file
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/summary.rb b/spec/mspec/lib/mspec/runner/formatters/summary.rb
index 0c9207194c..41819d2158 100644
--- a/spec/mspec/lib/mspec/runner/formatters/summary.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/summary.rb
@@ -1,11 +1,4 @@
-require 'mspec/expectations/expectations'
-require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/base'
-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
+class SummaryFormatter < BaseFormatter
end
diff --git a/spec/mspec/lib/mspec/runner/formatters/unit.rb b/spec/mspec/lib/mspec/runner/formatters/unit.rb
index cebc18a49b..d03ae79e9f 100644
--- a/spec/mspec/lib/mspec/runner/formatters/unit.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/unit.rb
@@ -1,4 +1,3 @@
-require 'mspec/expectations/expectations'
require 'mspec/runner/formatters/dotted'
class UnitdiffFormatter < DottedFormatter
diff --git a/spec/mspec/lib/mspec/runner/formatters/yaml.rb b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
index 090a9b1b9d..6c05cc902f 100644
--- a/spec/mspec/lib/mspec/runner/formatters/yaml.rb
+++ b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
@@ -1,8 +1,7 @@
-require 'mspec/expectations/expectations'
-require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/base'
-class YamlFormatter < DottedFormatter
- def initialize(out=nil)
+class YamlFormatter < BaseFormatter
+ def initialize(out = nil)
super(nil)
if out.nil?
@@ -16,9 +15,6 @@ class YamlFormatter < DottedFormatter
@out = @finish
end
- def after(state)
- end
-
def finish
switch
diff --git a/spec/mspec/lib/mspec/runner/mspec.rb b/spec/mspec/lib/mspec/runner/mspec.rb
index d47657326b..0e016c67a7 100644
--- a/spec/mspec/lib/mspec/runner/mspec.rb
+++ b/spec/mspec/lib/mspec/runner/mspec.rb
@@ -10,7 +10,6 @@ class MSpecEnv
end
module MSpec
-
@exit = nil
@abort = nil
@start = nil
@@ -20,26 +19,35 @@ module MSpec
@after = nil
@leave = nil
@finish = nil
- @exclude = nil
- @include = nil
+ @exclude = []
+ @include = []
@leave = nil
@load = nil
@unload = nil
@tagged = nil
@current = nil
+ @passed = nil
@example = nil
@modes = []
@shared = {}
@guarded = []
@features = {}
@exception = nil
- @randomize = nil
- @repeat = nil
+ @randomize = false
+ @repeat = 1
@expectation = nil
@expectations = false
+ @skips = []
+ @subprocesses = []
+
+ class << self
+ attr_reader :file, :include, :exclude, :skips, :subprocesses
+ attr_writer :repeat, :randomize
+ attr_accessor :formatter
+ end
- def self.describe(mod, options=nil, &block)
- state = ContextState.new mod, options
+ def self.describe(description, options = nil, &block)
+ state = ContextState.new description, options
state.parent = current
MSpec.register_current state
@@ -50,17 +58,21 @@ module MSpec
def self.process
STDOUT.puts RUBY_DESCRIPTION
+ STDOUT.flush
actions :start
files
actions :finish
end
+ def self.files_array
+ @files
+ end
+
def self.each_file(&block)
if ENV["MSPEC_MULTI"]
- STDOUT.print "."
- STDOUT.flush
- while file = STDIN.gets and file = file.chomp
+ while file = STDIN.gets
+ file = file.chomp
return if file == "QUIT"
yield file
begin
@@ -74,7 +86,7 @@ module MSpec
# The parent closed the connection without QUIT
abort "the parent did not send QUIT"
else
- return unless files = retrieve(:files)
+ return unless files = @files
shuffle files if randomize?
files.each(&block)
end
@@ -83,10 +95,11 @@ module MSpec
def self.files
each_file do |file|
setup_env
- store :file, file
+ @file = file
actions :load
protect("loading #{file}") { Kernel.load file }
actions :unload
+ raise "#{file} was executed but did not reset the current example: #{@current}" if @current
end
end
@@ -101,11 +114,14 @@ module MSpec
def self.protect(location, &block)
begin
- @env.instance_eval(&block)
+ @env.instance_exec(&block)
return true
rescue SystemExit => e
raise e
- rescue Exception => exc
+ rescue SkippedSpecError => e
+ @skips << [e, block]
+ return false
+ rescue Object => exc
register_exit 1
actions :exception, ExceptionState.new(current && current.state, location, exc)
return false
@@ -128,22 +144,24 @@ module MSpec
# Sets the toplevel ContextState to +state+.
def self.register_current(state)
- store :current, state
+ @current = state
end
# Sets the toplevel ContextState to +nil+.
def self.clear_current
- store :current, nil
+ @current = nil
end
# Returns the toplevel ContextState.
def self.current
- retrieve :current
+ @current
end
# Stores the shared ContextState keyed by description.
def self.register_shared(state)
- @shared[state.to_s] = state
+ name = state.to_s
+ raise "duplicated shared #describe: #{name}" if @shared.key?(name)
+ @shared[name] = state
end
# Returns the shared ContextState matching description.
@@ -153,17 +171,17 @@ module MSpec
# Stores the exit code used by the runner scripts.
def self.register_exit(code)
- store :exit, code
+ @exit = code
end
# Retrieves the stored exit code.
def self.exit_code
- retrieve(:exit).to_i
+ @exit.to_i
end
# Stores the list of files to be evaluated.
def self.register_files(files)
- store :files, files
+ @files = files
end
# Stores one or more substitution patterns for transforming
@@ -174,7 +192,7 @@ module MSpec
#
# See also +tags_file+.
def self.register_tags_patterns(patterns)
- store :tags_patterns, patterns
+ @tags_patterns = patterns
end
# Registers an operating mode. Modes recognized by MSpec:
@@ -185,30 +203,30 @@ module MSpec
# :report - specs that are guarded are reported
# :unguarded - all guards are forced off
def self.register_mode(mode)
- modes = retrieve :modes
+ modes = @modes
modes << mode unless modes.include? mode
end
# Clears all registered modes.
def self.clear_modes
- store :modes, []
+ @modes = []
end
# Returns +true+ if +mode+ is registered.
def self.mode?(mode)
- retrieve(:modes).include? mode
+ @modes.include? mode
end
def self.enable_feature(feature)
- retrieve(:features)[feature] = true
+ @features[feature] = true
end
def self.disable_feature(feature)
- retrieve(:features)[feature] = false
+ @features[feature] = false
end
def self.feature_enabled?(feature)
- retrieve(:features)[feature] || false
+ @features[feature] || false
end
def self.retrieve(symbol)
@@ -227,6 +245,7 @@ module MSpec
# :before before a single spec is run
# :add while a describe block is adding examples to run later
# :expectation before a 'should', 'should_receive', etc.
+ # :passed after an example block is run and passes, passed the block, run before :example action
# :example after an example block is run, passed the block
# :exception after an exception is rescued
# :after after a single spec is run
@@ -257,21 +276,17 @@ module MSpec
end
end
- def self.randomize(flag=true)
- @randomize = flag
- end
-
def self.randomize?
- @randomize == true
- end
-
- def self.repeat=(times)
- @repeat = times
+ @randomize
end
def self.repeat
- (@repeat || 1).times do
+ if @repeat == 1
yield
+ else
+ @repeat.times do
+ yield
+ end
end
end
@@ -287,17 +302,17 @@ module MSpec
# Records that an expectation has been encountered in an example.
def self.expectation
- store :expectations, true
+ @expectations = true
end
# Returns true if an expectation has been encountered
def self.expectation?
- retrieve :expectations
+ @expectations
end
# Resets the flag that an expectation has been encountered in an example.
def self.clear_expectations
- store :expectations, false
+ @expectations = false
end
# Transforms a spec filename into a tags filename by applying each
@@ -311,9 +326,9 @@ module MSpec
#
# See also +register_tags_patterns+.
def self.tags_file
- patterns = retrieve(:tags_patterns) ||
+ patterns = @tags_patterns ||
[[%r(spec/), 'spec/tags/'], [/_spec.rb$/, '_tags.txt']]
- patterns.inject(retrieve(:file).dup) do |file, pattern|
+ patterns.inject(@file.dup) do |file, pattern|
file.gsub(*pattern)
end
end
@@ -350,6 +365,7 @@ module MSpec
# Writes each tag in +tags+ to the tag file. Overwrites the
# tag file if it exists.
def self.write_tags(tags)
+ return delete_tags if tags.empty?
file = tags_file
make_tag_dir(file)
File.open(file, "w:utf-8") do |f|
@@ -381,7 +397,7 @@ module MSpec
desc = tag.escape(tag.description)
file = tags_file
if File.exist? file
- lines = IO.readlines(file)
+ lines = File.readlines(file)
File.open(file, "w:utf-8") do |f|
lines.each do |line|
line = line.chomp
diff --git a/spec/mspec/lib/mspec/runner/object.rb b/spec/mspec/lib/mspec/runner/object.rb
index 2ea8197165..58d98cc4df 100644
--- a/spec/mspec/lib/mspec/runner/object.rb
+++ b/spec/mspec/lib/mspec/runner/object.rb
@@ -1,18 +1,18 @@
class Object
- private def before(at=:each, &block)
+ private def before(at = :each, &block)
MSpec.current.before at, &block
end
- private def after(at=:each, &block)
+ private def after(at = :each, &block)
MSpec.current.after at, &block
end
- private def describe(mod, msg=nil, options=nil, &block)
- MSpec.describe mod, msg, &block
+ private def describe(description, options = nil, &block)
+ MSpec.describe description, options, &block
end
- private def it(msg, &block)
- MSpec.current.it msg, &block
+ private def it(desc, &block)
+ MSpec.current.it desc, &block
end
private def it_should_behave_like(desc)
diff --git a/spec/mspec/lib/mspec/runner/parallel.rb b/spec/mspec/lib/mspec/runner/parallel.rb
new file mode 100644
index 0000000000..6a9ecd155d
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/parallel.rb
@@ -0,0 +1,98 @@
+class ParallelRunner
+ def initialize(files, processes, formatter, argv)
+ @files = files
+ @processes = processes
+ @formatter = formatter
+ @argv = argv
+ @last_files = {}
+ @output_files = []
+ @success = true
+ end
+
+ def launch_children
+ @children = @processes.times.map { |i|
+ name = tmp "mspec-multi-#{i}"
+ @output_files << name
+
+ env = {
+ "SPEC_TEMP_DIR" => "#{SPEC_TEMP_DIR}_#{i}",
+ "MSPEC_MULTI" => i.to_s
+ }
+ command = @argv + ["-fy", "-o", name]
+ $stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG
+ IO.popen([env, *command, close_others: false], "rb+")
+ }
+ end
+
+ def handle(child, message)
+ case message
+ when '.'
+ @formatter.unload
+ send_new_file_or_quit(child)
+ else
+ if message == nil
+ msg = "A child mspec-run process died unexpectedly"
+ else
+ msg = "A child mspec-run process printed unexpected output on STDOUT"
+ while chunk = (child.read_nonblock(4096) rescue nil)
+ message += chunk
+ end
+ message.chomp!('.')
+ msg += ": #{message.inspect}"
+ end
+
+ if last_file = @last_files[child]
+ msg += " while running #{last_file}"
+ end
+
+ @success = false
+ quit(child)
+ abort "\n#{msg}"
+ end
+ end
+
+ def quit(child)
+ begin
+ child.puts "QUIT"
+ rescue Errno::EPIPE
+ # The child process already died
+ end
+ _pid, status = Process.wait2(child.pid)
+ @success &&= status.success?
+ child.close
+ @children.delete(child)
+ end
+
+ def send_new_file_or_quit(child)
+ if @files.empty?
+ quit(child)
+ else
+ file = @files.shift
+ @last_files[child] = file
+ child.puts file
+ end
+ end
+
+ def run
+ MSpec.register_files @files
+ launch_children
+
+ puts @children.map { |child| child.gets }.uniq
+ @formatter.start
+ begin
+ @children.each { |child| send_new_file_or_quit(child) }
+
+ until @children.empty?
+ IO.select(@children)[0].each { |child|
+ handle(child, child.read(1))
+ }
+ end
+ ensure
+ @children.dup.each { |child| quit(child) }
+ @formatter.aggregate_results(@output_files)
+ @formatter.finish
+ end
+
+ @success
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/shared.rb b/spec/mspec/lib/mspec/runner/shared.rb
index b606de473b..283711c1d7 100644
--- a/spec/mspec/lib/mspec/runner/shared.rb
+++ b/spec/mspec/lib/mspec/runner/shared.rb
@@ -1,10 +1,14 @@
require 'mspec/runner/mspec'
-def it_behaves_like(desc, meth, obj=nil)
- send :before, :all do
+def it_behaves_like(desc, meth, obj = nil)
+ before :all do
@method = meth
@object = obj
end
+ after :all do
+ @method = nil
+ @object = nil
+ end
- send :it_should_behave_like, desc.to_s
+ it_should_behave_like desc.to_s
end
diff --git a/spec/mspec/lib/mspec/runner/tag.rb b/spec/mspec/lib/mspec/runner/tag.rb
index e2275ad3a6..820df9159e 100644
--- a/spec/mspec/lib/mspec/runner/tag.rb
+++ b/spec/mspec/lib/mspec/runner/tag.rb
@@ -1,7 +1,7 @@
class SpecTag
attr_accessor :tag, :comment, :description
- def initialize(string=nil)
+ def initialize(string = nil)
parse(string) if string
end
diff --git a/spec/mspec/lib/mspec/utils/format.rb b/spec/mspec/lib/mspec/utils/format.rb
new file mode 100644
index 0000000000..425dd4d11c
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/format.rb
@@ -0,0 +1,24 @@
+# If the implementation on which the specs are run cannot
+# load pp from the standard library, add a pp.rb file that
+# defines the #pretty_inspect method on Object or Kernel.
+begin
+ require 'pp'
+rescue LoadError
+ module Kernel
+ def pretty_inspect
+ inspect
+ end
+ end
+end
+
+module MSpec
+ def self.format(obj)
+ if String === obj and obj.include?("\n")
+ "\n#{obj.inspect.gsub('\n', "\n")}"
+ else
+ obj.pretty_inspect.chomp
+ end
+ rescue => e
+ "#<#{obj.class}>(#pretty_inspect raised #{e.inspect})"
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/name_map.rb b/spec/mspec/lib/mspec/utils/name_map.rb
index c1de081af0..9b04112e2e 100644
--- a/spec/mspec/lib/mspec/utils/name_map.rb
+++ b/spec/mspec/lib/mspec/utils/name_map.rb
@@ -8,10 +8,9 @@ class NameMap
'*' => 'multiply',
'/' => 'divide',
'%' => 'modulo',
- '<<' => {'Bignum' => 'left_shift',
- 'Fixnum' => 'left_shift',
- 'IO' => 'output',
- :default => 'append' },
+ '<<' => {'Integer' => 'left_shift',
+ 'IO' => 'output',
+ :default => 'append' },
'>>' => 'right_shift',
'<' => 'lt',
'<=' => 'lte',
@@ -25,33 +24,22 @@ class NameMap
'[]=' => 'element_set',
'**' => 'exponent',
'!' => 'not',
- '~' => {'Bignum' => 'complement',
- 'Fixnum' => 'complement',
- 'Regexp' => 'match',
- 'String' => 'match' },
+ '~' => {'Integer' => 'complement',
+ :default => 'match' },
'!=' => 'not_equal',
'!~' => 'not_match',
'=~' => 'match',
- '&' => {'Bignum' => 'bit_and',
- 'Fixnum' => 'bit_and',
+ '&' => {'Integer' => 'bit_and',
'Array' => 'intersection',
- 'TrueClass' => 'and',
- 'FalseClass' => 'and',
- 'NilClass' => 'and',
- 'Set' => 'intersection' },
- '|' => {'Bignum' => 'bit_or',
- 'Fixnum' => 'bit_or',
+ 'Set' => 'intersection',
+ :default => 'and' },
+ '|' => {'Integer' => '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'},
+ 'Set' => 'union',
+ :default => 'or' },
+ '^' => {'Integer' => 'bit_xor',
+ 'Set' => 'exclusion',
+ :default => 'xor' },
}
EXCLUDED = %w[
@@ -63,7 +51,11 @@ class NameMap
SpecVersion
]
- def initialize(filter=false)
+ ALWAYS_PRIVATE = %w[
+ initialize initialize_copy initialize_clone initialize_dup respond_to_missing?
+ ].map(&:to_sym)
+
+ def initialize(filter = false)
@seen = {}
@filter = filter
end
@@ -74,10 +66,17 @@ class NameMap
end
def class_or_module(c)
- const = Object.const_get(c, false)
+ begin
+ const = Object.const_get(c, false)
+ rescue NameError, RuntimeError
+ # Either the constant doesn't exist or it is
+ # explicitly raising an error, like `SortedSet`.
+ return nil
+ end
+ return nil unless Module === const
+
filtered = @filter && EXCLUDED.include?(const.name)
- return const if Module === const and !filtered
- rescue NameError
+ return const unless filtered
end
def namespace(mod, const)
@@ -85,7 +84,7 @@ class NameMap
"#{mod}::#{const}"
end
- def map(hash, constants, mod=nil)
+ def map(hash, constants, mod = nil)
@seen = {} unless mod
constants.each do |const|
@@ -98,7 +97,8 @@ class NameMap
hash["#{name}."] = ms.sort unless ms.empty?
ms = m.public_instance_methods(false) +
- m.protected_instance_methods(false)
+ m.protected_instance_methods(false) +
+ (m.private_instance_methods(false) & ALWAYS_PRIVATE)
ms.map! { |x| x.to_s }
hash["#{name}#"] = ms.sort unless ms.empty?
@@ -119,7 +119,12 @@ class NameMap
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]
+ mapping = MAP[m]
+ if mapping.is_a?(Hash)
+ name = mapping[c.split('::').last] || mapping.fetch(:default)
+ else
+ name = mapping
+ end
else
name = m.gsub(/[?!=]/, '')
end
diff --git a/spec/mspec/lib/mspec/utils/options.rb b/spec/mspec/lib/mspec/utils/options.rb
index 9f8dd01dbf..adeafa1f81 100644
--- a/spec/mspec/lib/mspec/utils/options.rb
+++ b/spec/mspec/lib/mspec/utils/options.rb
@@ -32,9 +32,13 @@ class MSpecOptions
# Raised if an unrecognized option is encountered.
class ParseError < Exception; end
+ class << self
+ attr_accessor :latest
+ end
+
attr_accessor :config, :banner, :width, :options
- def initialize(banner="", width=30, config=nil)
+ def initialize(banner = "", width = 30, config = nil)
@banner = banner
@config = config
@width = width
@@ -46,7 +50,7 @@ class MSpecOptions
@extra << x
}
- yield self if block_given?
+ MSpecOptions.latest = self
end
# Registers an option. Acceptable formats for arguments are:
@@ -94,7 +98,7 @@ class MSpecOptions
@options.find { |o| o.match? opt }
end
- # Processes an option. Calles the #on_extra block (or default) for
+ # Processes an option. Calls the #on_extra block (or default) for
# unrecognized options. For registered options, possibly fetches an
# argument and invokes the option's block if it is not nil.
def process(argv, entry, opt, arg)
@@ -123,7 +127,7 @@ class MSpecOptions
# Parses an array of command line entries, calling blocks for
# registered options.
- def parse(argv=ARGV)
+ def parse(argv = ARGV)
argv = Array(argv).dup
while entry = argv.shift
@@ -200,6 +204,13 @@ class MSpecOptions
"Load FILE containing configuration options", &block)
end
+ def env
+ on("--env", "KEY=VALUE", "Set environment variable") do |env|
+ key, value = env.split('=', 2)
+ ENV[key] = value
+ end
+ end
+
def targets
on("-t", "--target", "TARGET",
"Implementation to run the specs, where TARGET is:") do |t|
@@ -274,6 +285,8 @@ class MSpecOptions
config[:formatter] = SpinnerFormatter
when 't', 'method'
config[:formatter] = MethodFormatter
+ when 'e', 'stats'
+ config[:formatter] = StatsPerFileFormatter
when 'y', 'yaml'
config[:formatter] = YamlFormatter
when 'p', 'profile'
@@ -281,7 +294,7 @@ class MSpecOptions
when 'j', 'junit'
config[:formatter] = JUnitFormatter
else
- abort "Unknown format: #{o}\n#{@parser}" unless File.exist?(o)
+ abort "Unknown format: #{o}" unless File.exist?(o)
require File.expand_path(o)
if Object.const_defined?(:CUSTOM_MSPEC_FORMATTER)
config[:formatter] = CUSTOM_MSPEC_FORMATTER
@@ -300,6 +313,7 @@ class MSpecOptions
doc " m, summary SummaryFormatter"
doc " a, *, spin SpinnerFormatter"
doc " t, method MethodFormatter"
+ doc " e, stats StatsPerFileFormatter"
doc " y, yaml YamlFormatter"
doc " p, profile ProfileFormatter"
doc " j, junit JUnitFormatter\n"
@@ -308,6 +322,11 @@ class MSpecOptions
"Write formatter output to FILE") do |f|
config[:output] = f
end
+
+ on("--error-output", "FILE",
+ "Write error output of failing specs to FILE, or $stderr if value is 'stderr'.") do |f|
+ config[:error_output] = f
+ end
end
def filters
@@ -377,14 +396,14 @@ class MSpecOptions
def randomize
on("-H", "--random",
"Randomize the list of spec files") do
- MSpec.randomize
+ MSpec.randomize = true
end
end
def repeat
on("-R", "--repeat", "NUMBER",
"Repeatedly run an example NUMBER times") do |o|
- MSpec.repeat = o.to_i
+ MSpec.repeat = Integer(o)
end
end
@@ -392,11 +411,11 @@ class MSpecOptions
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 }
+ @width = MSpec.files_array.inject(0) { |max, f| f.size > max ? f.size : max }
end
def obj.load
- file = MSpec.retrieve :file
- STDERR.print "\n#{file.ljust(@width)}"
+ file = MSpec.file
+ STDERR.print "\n#{file.ljust(@width)}\n"
end
MSpec.register :start, obj
MSpec.register :load, obj
@@ -411,14 +430,26 @@ class MSpecOptions
end
MSpec.register :load, obj
end
+
+ on("--print-skips", "Print skips") do
+ config[:print_skips] = true
+ end
end
def interrupt
- on("--int-spec", "Control-C interupts the current spec only") do
+ on("--int-spec", "Control-C interrupts the current spec only") do
config[:abort] = false
end
end
+ def timeout
+ on("--timeout", "TIMEOUT", "Abort if a spec takes longer than TIMEOUT seconds") do |timeout|
+ require 'mspec/runner/actions/timeout'
+ timeout = Float(timeout)
+ TimeoutAction.new(timeout).register
+ end
+ end
+
def verify
on("--report-on", "GUARD", "Report specs guarded by GUARD") do |g|
MSpec.register_mode :report_on
@@ -453,15 +484,22 @@ class MSpecOptions
def debug
on("-d", "--debug",
- "Set MSpec debugging flag for more verbose output") do
+ "Disable MSpec backtrace filtering") do
$MSPEC_DEBUG = true
end
end
+ def launchable
+ on("--launchable-test-reports", "DIR",
+ "DIR The directory for reporting test results in Launchable JSON format") do |o|
+ require 'mspec/runner/formatters/launchable'
+ config[:launchable] = LaunchableFormatter.setDir(o)
+ end
+ end
+
def all
- # Generated with:
- # puts File.read(__FILE__).scan(/def (\w+).*\n\s*on\(/)
configure {}
+ env
targets
formatters
filters
@@ -473,9 +511,11 @@ class MSpecOptions
repeat
verbose
interrupt
+ timeout
verify
action_filters
actions
debug
+ launchable
end
end
diff --git a/spec/mspec/lib/mspec/utils/script.rb b/spec/mspec/lib/mspec/utils/script.rb
index 24cd069bb4..15fd23fabf 100644
--- a/spec/mspec/lib/mspec/utils/script.rb
+++ b/spec/mspec/lib/mspec/utils/script.rb
@@ -3,7 +3,6 @@ require 'mspec/guards/version'
require 'mspec/utils/warnings'
# MSpecScript provides a skeleton for all the MSpec runner scripts.
-
class MSpecScript
# Returns the config object. Maintained at the class
# level to easily enable simple config files. See the
@@ -38,10 +37,19 @@ class MSpecScript
config[key]
end
+ class << self
+ attr_accessor :child_process
+ end
+
+ # True if the current process is the one going to run the specs with `MSpec.process`.
+ # False for e.g. `mspec` which exec's to `mspec-run`.
+ # This is useful in .mspec config files.
+ def self.child_process?
+ MSpecScript.child_process
+ end
+
def initialize
- ruby_version_is ""..."2.2" do
- abort "MSpec needs Ruby 2.2 or more recent"
- end
+ check_version!
config[:formatter] = nil
config[:includes] = []
@@ -125,14 +133,9 @@ class MSpecScript
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])
+ if formatter = config_formatter
formatter.register
- MSpec.store :formatter, formatter
+ MSpec.formatter = formatter
end
MatchFilter.new(:include, *config[:includes]).register unless config[:includes].empty?
@@ -149,6 +152,23 @@ class MSpecScript
custom_register
end
+ # Makes a formatter specified by :formatter option.
+ def config_formatter
+ if config[:formatter].nil?
+ config[:formatter] = STDOUT.tty? ? SpinnerFormatter : @files.size < 50 ? DottedFormatter : FileFormatter
+ end
+
+ if config[:formatter]
+ config[:formatter] = config[:formatter].new(config[:output])
+ end
+
+ if config[:launchable]
+ config[:formatter].extend config[:launchable]
+ end
+
+ config[:formatter]
+ end
+
# Callback for enabling custom actions, etc. This version is a
# no-op. Provide an implementation specific version in a config
# file. Called by #register.
@@ -189,7 +209,11 @@ class MSpecScript
end
patterns.each do |pattern|
- expanded = File.expand_path(pattern)
+ begin
+ expanded = File.realpath(pattern)
+ rescue Errno::ENOENT, Errno::ENOTDIR
+ next
+ end
if File.file?(expanded) && expanded.end_with?('.rb')
return [expanded]
elsif File.directory?(expanded)
@@ -260,10 +284,11 @@ class MSpecScript
# Instantiates an instance and calls the series of methods to
# invoke the script.
- def self.main
+ def self.main(child_process = true)
+ MSpecScript.child_process = child_process
+
script = new
script.load_default
- script.try_load '~/.mspecrc'
script.options
script.signals
script.register
@@ -271,4 +296,10 @@ class MSpecScript
require 'mspec'
script.run
end
+
+ private def check_version!
+ ruby_version_is ""..."2.6" do
+ warn "MSpec is supported for Ruby 2.6 and above only"
+ end
+ end
end
diff --git a/spec/mspec/lib/mspec/utils/version.rb b/spec/mspec/lib/mspec/utils/version.rb
index 787a76b053..9c1c58b8df 100644
--- a/spec/mspec/lib/mspec/utils/version.rb
+++ b/spec/mspec/lib/mspec/utils/version.rb
@@ -42,7 +42,7 @@ class SpecVersion
def <=>(other)
if other.respond_to? :to_int
- other = Integer other
+ other = Integer(other.to_int)
else
other = SpecVersion.new(String(other)).to_i
end
diff --git a/spec/mspec/lib/mspec/utils/warnings.rb b/spec/mspec/lib/mspec/utils/warnings.rb
index 4d23474236..23efc696a5 100644
--- a/spec/mspec/lib/mspec/utils/warnings.rb
+++ b/spec/mspec/lib/mspec/utils/warnings.rb
@@ -1,61 +1,10 @@
require 'mspec/guards/version'
-if RUBY_ENGINE == "ruby" and ruby_version_is("2.4")
- ruby_version_is "2.4"..."2.5" do
- # Kernel#warn does not delegate to Warning.warn in 2.4
- module Kernel
- remove_method :warn
- 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)
- if Thread.current[:in_mspec_complain_matcher]
- return $stderr.write(message)
- end
-
- case message
- # $VERBOSE = true warnings
- when /(.+\.rb):(\d+):.+possibly useless use of (<|<=|==|>=|>) in void context/
- # Make sure there is a .should otherwise it is missing
- line_nb = Integer($2)
- unless File.exist?($1) and /\.should(_not)? (<|<=|==|>=|>)/ === File.readlines($1)[line_nb-1]
- $stderr.write message
- end
- when /possibly useless use of (\+|-) in void context/
- when /assigned but unused variable/
- when /method redefined/
- when /previous definition of/
- when /instance variable @.+ not initialized/
- when /statement not reached/
- when /shadowing outer local variable/
- when /setting Encoding.default_(in|ex)ternal/
- when /unknown (un)?pack directive/
- when /(un)?trust(ed\?)? is deprecated/
- when /\.exists\? is a deprecated name/
- when /Float .+ out of range/
- when /passing a block to String#(bytes|chars|codepoints|lines) is deprecated/
- when /core\/string\/modulo_spec\.rb:\d+: warning: too many arguments for format string/
- when /regexp\/shared\/new_ascii(_8bit)?\.rb:\d+: warning: Unknown escape .+ is ignored/
-
- # $VERBOSE = false warnings
- 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']
+# Always enable deprecation warnings when running MSpec, as ruby/spec tests for them,
+# and like in most test frameworks, deprecation warnings should be enabled by default,
+# so that deprecations are noticed before the breaking change.
+# Disable experimental warnings, we want to test new experimental features in ruby/spec.
+if Object.const_defined?(:Warning) and Warning.respond_to?(:[]=)
+ Warning[:deprecated] = true
+ Warning[:experimental] = false
end