diff options
Diffstat (limited to 'spec/mspec/lib')
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 |
