diff options
Diffstat (limited to 'spec/mspec')
74 files changed, 848 insertions, 292 deletions
diff --git a/spec/mspec/Gemfile b/spec/mspec/Gemfile index 617a995cad..e57acd6435 100644 --- a/spec/mspec/Gemfile +++ b/spec/mspec/Gemfile @@ -1,4 +1,4 @@ source 'https://rubygems.org' -gem "rake", "~> 12.3" +gem "rake" gem "rspec", "~> 3.0" diff --git a/spec/mspec/Gemfile.lock b/spec/mspec/Gemfile.lock index cd39906044..075337d671 100644 --- a/spec/mspec/Gemfile.lock +++ b/spec/mspec/Gemfile.lock @@ -2,7 +2,7 @@ GEM remote: https://rubygems.org/ specs: diff-lcs (1.4.4) - rake (12.3.3) + rake (13.4.2) rspec (3.10.0) rspec-core (~> 3.10.0) rspec-expectations (~> 3.10.0) @@ -22,5 +22,5 @@ PLATFORMS ruby DEPENDENCIES - rake (~> 12.3) + rake rspec (~> 3.0) diff --git a/spec/mspec/lib/mspec/commands/mkspec.rb b/spec/mspec/lib/mspec/commands/mkspec.rb index d10cc35d18..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' @@ -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 a31db1d7dc..8951572f69 100644 --- a/spec/mspec/lib/mspec/commands/mspec-ci.rb +++ b/spec/mspec/lib/mspec/commands/mspec-ci.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' @@ -22,6 +18,7 @@ class MSpecCI < MSpecScript options.chdir options.prefix options.configure { |f| load f } + options.repeat options.pretend options.interrupt options.timeout diff --git a/spec/mspec/lib/mspec/commands/mspec-run.rb b/spec/mspec/lib/mspec/commands/mspec-run.rb index 4d8f4d9984..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' @@ -32,6 +28,7 @@ class MSpecRun < MSpecScript options.chdir options.prefix options.configure { |f| load f } + options.env options.randomize options.repeat options.pretend @@ -52,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 e1d04d1446..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' @@ -114,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 9c38cebcda..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' @@ -38,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 diff --git a/spec/mspec/lib/mspec/guards/platform.rb b/spec/mspec/lib/mspec/guards/platform.rb index e87b08a4c1..fadd8d75ef 100644 --- a/spec/mspec/lib/mspec/guards/platform.rb +++ b/spec/mspec/lib/mspec/guards/platform.rb @@ -53,16 +53,27 @@ class PlatformGuard < SpecGuard 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 - WORD_SIZE + [0].pack('j').size * 8 + end + + # In bits + C_LONG_SIZE = if defined?(RbConfig::SIZEOF[]) + RbConfig::SIZEOF["long"] * 8 + else + [0].pack('l!').size * 8 end def self.wordsize?(size) + warn "#wordsize? is deprecated, use #c_long_size?" size == WORD_SIZE end @@ -70,6 +81,10 @@ class PlatformGuard < SpecGuard size == POINTER_SIZE end + def self.c_long_size?(size) + size == C_LONG_SIZE + end + def initialize(*args) if args.last.is_a?(Hash) @options, @platforms = args.last, args[0..-2] @@ -85,10 +100,13 @@ class PlatformGuard < SpecGuard case key when :os match &&= PlatformGuard.os?(*value) - when :wordsize - match &&= PlatformGuard.wordsize? 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 20f8c06d38..f5ea1988ae 100644 --- a/spec/mspec/lib/mspec/guards/version.rb +++ b/spec/mspec/lib/mspec/guards/version.rb @@ -33,6 +33,30 @@ class VersionGuard < SpecGuard @version >= @requirement end end + + @kernel_version = nil + def self.kernel_version + if @kernel_version + @kernel_version + else + if v = RUBY_PLATFORM[/darwin(\d+)/, 1] # build time version + uname = v + else + begin + require 'etc' + etc = true + rescue LoadError + etc = false + end + if etc and Etc.respond_to?(:uname) + uname = Etc.uname.fetch(:release) + else + uname = `uname -r`.chomp + end + end + @kernel_version = uname + end + end end def version_is(base_version, requirement, &block) @@ -42,3 +66,7 @@ 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/io.rb b/spec/mspec/lib/mspec/helpers/io.rb index 29c6c37a1a..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) diff --git a/spec/mspec/lib/mspec/helpers/numeric.rb b/spec/mspec/lib/mspec/helpers/numeric.rb index c1ed81a233..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 @@ -15,11 +16,11 @@ def bignum_value(plus = 0) end def max_long - 2**(0.size * 8 - 1) - 1 + 2**(PlatformGuard::C_LONG_SIZE - 1) - 1 end def min_long - -(2**(0.size * 8 - 1)) + -(2**(PlatformGuard::C_LONG_SIZE - 1)) end # This is a bit hairy, but we need to be able to write specs that cover the @@ -28,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 @@ -36,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 922178dab0..2e499d6f9a 100644 --- a/spec/mspec/lib/mspec/helpers/ruby_exe.rb +++ b/spec/mspec/lib/mspec/helpers/ruby_exe.rb @@ -140,19 +140,44 @@ def ruby_exe(code = :not_given, opts = {}) expected_status = opts.fetch(:exit_status, 0) begin - platform_is_not :opal do - command = ruby_cmd(code, opts) - output = `#{command}` - - exit_status = Process.last_status.exitstatus - 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}" + 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 - output + 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| diff --git a/spec/mspec/lib/mspec/helpers/tmp.rb b/spec/mspec/lib/mspec/helpers/tmp.rb index b2a38ee983..e903dd9f50 100644 --- a/spec/mspec/lib/mspec/helpers/tmp.rb +++ b/spec/mspec/lib/mspec/helpers/tmp.rb @@ -12,7 +12,7 @@ else end SPEC_TEMP_DIR = spec_temp_dir -SPEC_TEMP_UNIQUIFIER = "0" +SPEC_TEMP_UNIQUIFIER = +"0" at_exit do begin @@ -36,11 +36,25 @@ all specs are cleaning up temporary files: end def tmp(name, uniquify = true) - mkdir_p SPEC_TEMP_DIR unless Dir.exist? SPEC_TEMP_DIR + 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/matchers/base.rb b/spec/mspec/lib/mspec/matchers/base.rb index d9d7f6fec0..4056e73a81 100644 --- a/spec/mspec/lib/mspec/matchers/base.rb +++ b/spec/mspec/lib/mspec/matchers/base.rb @@ -1,3 +1,5 @@ +require 'mspec/utils/deprecate' + module MSpecMatchers end @@ -36,6 +38,14 @@ class SpecPositiveOperatorMatcher < BasicObject end end + def raise(exception = ::Exception, message = nil, options = nil, &block) + matcher = ::RaiseErrorMatcher.new(exception, message, options, &block) + unless matcher.matches? @actual + expected, actual = matcher.failure_message + ::SpecExpectation.fail_with(expected, actual) + end + end + def method_missing(name, *args, &block) result = @actual.__send__(name, *args, &block) unless result @@ -70,6 +80,14 @@ class SpecNegativeOperatorMatcher < BasicObject end end + def raise(exception = ::Exception, message = nil, options = nil, &block) + matcher = ::RaiseErrorMatcher.new(exception, message, options, &block) + if matcher.matches? @actual + expected, actual = matcher.negative_failure_message + ::SpecExpectation.fail_with(expected, actual) + end + end + def method_missing(name, *args, &block) result = @actual.__send__(name, *args, &block) if result diff --git a/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb index fdf3736ac2..230cea2e9b 100644 --- a/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb +++ b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb @@ -21,6 +21,7 @@ end module MSpecMatchers private def be_an_instance_of(expected) + MSpec.deprecate __method__, '.should.instance_of?' BeAnInstanceOfMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb index 05f72099e4..b428a153bf 100644 --- a/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb +++ b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def be_ancestor_of(expected) + MSpec.deprecate __method__, '.ancestors.should.include?' BeAncestorOfMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/be_empty.rb b/spec/mspec/lib/mspec/matchers/be_empty.rb index 5abd5c9485..aac175ffb4 100644 --- a/spec/mspec/lib/mspec/matchers/be_empty.rb +++ b/spec/mspec/lib/mspec/matchers/be_empty.rb @@ -15,6 +15,7 @@ end module MSpecMatchers private def be_empty + MSpec.deprecate __method__, '.should.empty?' BeEmptyMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/be_false.rb b/spec/mspec/lib/mspec/matchers/be_false.rb index 9e9a2608e1..4fa0bba8a3 100644 --- a/spec/mspec/lib/mspec/matchers/be_false.rb +++ b/spec/mspec/lib/mspec/matchers/be_false.rb @@ -15,6 +15,7 @@ end module MSpecMatchers private def be_false + MSpec.deprecate __method__, '.should == false' BeFalseMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/be_kind_of.rb b/spec/mspec/lib/mspec/matchers/be_kind_of.rb index a69906f210..d0b23086aa 100644 --- a/spec/mspec/lib/mspec/matchers/be_kind_of.rb +++ b/spec/mspec/lib/mspec/matchers/be_kind_of.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def be_kind_of(expected) + MSpec.deprecate __method__, '.should.is_a?' BeKindOfMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/be_nan.rb b/spec/mspec/lib/mspec/matchers/be_nan.rb index b279d8f1cf..8ba2a3cafe 100644 --- a/spec/mspec/lib/mspec/matchers/be_nan.rb +++ b/spec/mspec/lib/mspec/matchers/be_nan.rb @@ -15,6 +15,7 @@ end module MSpecMatchers private def be_nan + MSpec.deprecate __method__, '.should.nan?' BeNaNMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/be_nil.rb b/spec/mspec/lib/mspec/matchers/be_nil.rb index 049b1e3a53..c79a50a3f9 100644 --- a/spec/mspec/lib/mspec/matchers/be_nil.rb +++ b/spec/mspec/lib/mspec/matchers/be_nil.rb @@ -15,6 +15,7 @@ end module MSpecMatchers private def be_nil + MSpec.deprecate __method__, '.should == nil' BeNilMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/be_true.rb b/spec/mspec/lib/mspec/matchers/be_true.rb index 52f5013752..91020cc3fe 100644 --- a/spec/mspec/lib/mspec/matchers/be_true.rb +++ b/spec/mspec/lib/mspec/matchers/be_true.rb @@ -15,6 +15,7 @@ end module MSpecMatchers private def be_true + MSpec.deprecate __method__, '.should == true' BeTrueMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/complain.rb b/spec/mspec/lib/mspec/matchers/complain.rb index 887e72b4b0..19310c0bbb 100644 --- a/spec/mspec/lib/mspec/matchers/complain.rb +++ b/spec/mspec/lib/mspec/matchers/complain.rb @@ -19,7 +19,6 @@ class ComplainMatcher @verbose = $VERBOSE err = IOStub.new - Thread.current[:in_mspec_complain_matcher] = true $stderr = err $VERBOSE = @options.key?(:verbose) ? @options[:verbose] : false begin @@ -27,7 +26,6 @@ class ComplainMatcher ensure $VERBOSE = @verbose $stderr = @saved_err - Thread.current[:in_mspec_complain_matcher] = false end @warning = err.to_s diff --git a/spec/mspec/lib/mspec/matchers/eql.rb b/spec/mspec/lib/mspec/matchers/eql.rb index bcab88ebee..3b132f9ae3 100644 --- a/spec/mspec/lib/mspec/matchers/eql.rb +++ b/spec/mspec/lib/mspec/matchers/eql.rb @@ -21,6 +21,7 @@ end module MSpecMatchers private def eql(expected) + MSpec.deprecate __method__, '.should.eql?' EqlMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/equal.rb b/spec/mspec/lib/mspec/matchers/equal.rb index 5ba4856d82..1ca5172e0b 100644 --- a/spec/mspec/lib/mspec/matchers/equal.rb +++ b/spec/mspec/lib/mspec/matchers/equal.rb @@ -21,6 +21,7 @@ end module MSpecMatchers private def equal(expected) + MSpec.deprecate __method__, '.should.equal?' EqualMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/have_class_variable.rb b/spec/mspec/lib/mspec/matchers/have_class_variable.rb index dd43ced621..576f43793b 100644 --- a/spec/mspec/lib/mspec/matchers/have_class_variable.rb +++ b/spec/mspec/lib/mspec/matchers/have_class_variable.rb @@ -7,6 +7,7 @@ end module MSpecMatchers private def have_class_variable(variable) + MSpec.deprecate __method__, '.should.class_variable_defined?' HaveClassVariableMatcher.new(variable) end end diff --git a/spec/mspec/lib/mspec/matchers/have_constant.rb b/spec/mspec/lib/mspec/matchers/have_constant.rb index 6ec7c75b85..c796d742e0 100644 --- a/spec/mspec/lib/mspec/matchers/have_constant.rb +++ b/spec/mspec/lib/mspec/matchers/have_constant.rb @@ -7,6 +7,7 @@ end module MSpecMatchers private def have_constant(variable) + MSpec.deprecate __method__, '.should.const_defined?' HaveConstantMatcher.new(variable) end end diff --git a/spec/mspec/lib/mspec/matchers/have_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_instance_method.rb index 9a5a31aa0f..76dde482d5 100644 --- a/spec/mspec/lib/mspec/matchers/have_instance_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_instance_method.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def have_instance_method(method, include_super = true) + MSpec.deprecate __method__, '.should.method_defined?' HaveInstanceMethodMatcher.new method, include_super end end diff --git a/spec/mspec/lib/mspec/matchers/have_instance_variable.rb b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb index de51b3209d..f49595ff7e 100644 --- a/spec/mspec/lib/mspec/matchers/have_instance_variable.rb +++ b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb @@ -7,6 +7,7 @@ end module MSpecMatchers private def have_instance_variable(variable) + MSpec.deprecate __method__, '.should.instance_variable_defined?' HaveInstanceVariableMatcher.new(variable) end end diff --git a/spec/mspec/lib/mspec/matchers/have_method.rb b/spec/mspec/lib/mspec/matchers/have_method.rb index e962e69e0a..3db01a7235 100644 --- a/spec/mspec/lib/mspec/matchers/have_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_method.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def have_method(method, include_super = true) + MSpec.deprecate __method__, '.should.respond_to?' 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 d32db76c6a..bae01e846e 100644 --- a/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def have_private_instance_method(method, include_super = true) + MSpec.deprecate __method__, '.private_instance_methods(false).should.include?' 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 c74165cfc7..32efd5a155 100644 --- a/spec/mspec/lib/mspec/matchers/have_private_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_private_method.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def have_private_method(method, include_super = true) + MSpec.deprecate __method__, '.private_methods(false).should.include?' 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 1deb2f995d..9f09e8b529 100644 --- a/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def have_protected_instance_method(method, include_super = true) + MSpec.deprecate __method__, '.protected_instance_methods(false).should.include?' 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 0e620532c0..69abadfafa 100644 --- a/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def have_public_instance_method(method, include_super = true) + MSpec.deprecate __method__, '.public_instance_methods(false).should.include?' 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 b60dd2536b..2d2707c528 100644 --- a/spec/mspec/lib/mspec/matchers/have_singleton_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def have_singleton_method(method, include_super = true) + MSpec.deprecate __method__, '.singleton_methods(false).should.include?' 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 3f07f35548..05d5079f13 100644 --- a/spec/mspec/lib/mspec/matchers/include.rb +++ b/spec/mspec/lib/mspec/matchers/include.rb @@ -26,6 +26,7 @@ end # Cannot override #include at the toplevel in MRI module MSpecMatchers private def include(*expected) + MSpec.deprecate __method__, '.should.include?' IncludeMatcher.new(*expected) end end diff --git a/spec/mspec/lib/mspec/matchers/infinity.rb b/spec/mspec/lib/mspec/matchers/infinity.rb index 8bfa6dbd10..0a4c95c7cc 100644 --- a/spec/mspec/lib/mspec/matchers/infinity.rb +++ b/spec/mspec/lib/mspec/matchers/infinity.rb @@ -19,10 +19,12 @@ end module MSpecMatchers private def be_positive_infinity + MSpec.deprecate __method__, '.should.infinite? == 1' InfinityMatcher.new(1) end private def be_negative_infinity + MSpec.deprecate __method__, '.should.infinite? == -1' InfinityMatcher.new(-1) end end diff --git a/spec/mspec/lib/mspec/matchers/raise_error.rb b/spec/mspec/lib/mspec/matchers/raise_error.rb index 54378bb34c..17ea47148b 100644 --- a/spec/mspec/lib/mspec/matchers/raise_error.rb +++ b/spec/mspec/lib/mspec/matchers/raise_error.rb @@ -1,11 +1,18 @@ class RaiseErrorMatcher FAILURE_MESSAGE_FOR_EXCEPTION = {}.compare_by_identity + UNDEF_CAUSE = Object.new attr_writer :block - def initialize(exception, message, &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 @@ -45,24 +52,45 @@ class RaiseErrorMatcher end end + def matching_cause?(exc) + case @cause + when UNDEF_CAUSE + true + else + @cause == exc.cause + end + end + def matching_exception?(exc) - matching_class?(exc) and matching_message?(exc) + 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) + exception_class_and_message_and_cause(exception.class, + @message == nil ? nil : exception.message, + @cause == UNDEF_CAUSE ? UNDEF_CAUSE : exception.cause) end def failure_message @@ -87,7 +115,19 @@ class RaiseErrorMatcher 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) + MSpec.deprecate __method__, '.should.raise' + RaiseErrorMatcher.new(exception, message, options, &block) + end + + # CRuby < 4.1 has inconsistent coercion errors: + # https://bugs.ruby-lang.org/issues/21864 + # This matcher ignores the message on CRuby < 4.1 + # and checks the message for all other cases, including other Rubies + private def raise_consistent_error(exception = Exception, message = nil, options = nil, &block) + if RUBY_ENGINE == "ruby" and ruby_version_is ""..."4.1" + message = nil + end + RaiseErrorMatcher.new(exception, message, options, &block) end end diff --git a/spec/mspec/lib/mspec/matchers/respond_to.rb b/spec/mspec/lib/mspec/matchers/respond_to.rb index 6b35ae2d3c..a36bf8aee2 100644 --- a/spec/mspec/lib/mspec/matchers/respond_to.rb +++ b/spec/mspec/lib/mspec/matchers/respond_to.rb @@ -19,6 +19,7 @@ end module MSpecMatchers private def respond_to(expected) + MSpec.deprecate __method__, '.should.respond_to?' RespondToMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/mocks/mock.rb b/spec/mspec/lib/mspec/mocks/mock.rb index 28a083cc15..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,8 +36,9 @@ 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 @@ -59,8 +56,8 @@ module Mock return end - if (sym == :respond_to? or mock_respond_to?(obj, sym, true)) and !replaced?(key.first) - meta.__send__ :alias_method, key.first, sym + 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 { @@ -191,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/runner/actions/leakchecker.rb b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb index 596b120d9f..0a8c9c3252 100644 --- a/spec/mspec/lib/mspec/runner/actions/leakchecker.rb +++ b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb @@ -132,14 +132,14 @@ 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 @@ -173,7 +173,8 @@ class LeakChecker 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 @@ -300,6 +301,7 @@ class LeakCheckerAction end def start + disable_nss_modules @checker = LeakChecker.new end @@ -315,4 +317,61 @@ class LeakCheckerAction 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/timeout.rb b/spec/mspec/lib/mspec/runner/actions/timeout.rb index 543b7366d7..1200926872 100644 --- a/spec/mspec/lib/mspec/runner/actions/timeout.rb +++ b/spec/mspec/lib/mspec/runner/actions/timeout.rb @@ -3,6 +3,8 @@ class TimeoutAction @timeout = timeout @queue = Queue.new @started = now + @fail = false + @error_message = "took longer than the configured timeout of #{@timeout}s" end def register @@ -37,15 +39,26 @@ class TimeoutAction elapsed = now - @started if elapsed > @timeout if @current_state - STDERR.puts "\nExample took longer than the configured timeout of #{@timeout}s:" + STDERR.puts "\nExample #{@error_message}:" STDERR.puts "#{@current_state.description}" else - STDERR.puts "\nSome code outside an example took longer than the configured timeout of #{@timeout}s" + STDERR.puts "\nSome code outside an example #{@error_message}" end STDERR.flush show_backtraces - exit 2 + 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 @@ -65,6 +78,11 @@ class TimeoutAction @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 @@ -72,20 +90,54 @@ class TimeoutAction @thread.join end - private def show_backtraces - if RUBY_ENGINE == 'truffleruby' - STDERR.puts 'Java stacktraces:' - Process.kill :SIGQUIT, Process.pid + 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 - STDERR.puts "\nRuby backtraces:" - if defined?(Truffle::Debug.show_backtraces) - Truffle::Debug.show_backtraces + 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 - Thread.list.each do |thread| - unless thread == Thread.current - STDERR.puts thread.inspect, thread.backtrace, '' + 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 diff --git a/spec/mspec/lib/mspec/runner/context.rb b/spec/mspec/lib/mspec/runner/context.rb index 62483590bb..bcd83b2465 100644 --- a/spec/mspec/lib/mspec/runner/context.rb +++ b/spec/mspec/lib/mspec/runner/context.rb @@ -210,6 +210,7 @@ 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 if !MSpec.expectation? and passed end diff --git a/spec/mspec/lib/mspec/runner/formatters/base.rb b/spec/mspec/lib/mspec/runner/formatters/base.rb index c7c50c40d8..882e15c8c2 100644 --- a/spec/mspec/lib/mspec/runner/formatters/base.rb +++ b/spec/mspec/lib/mspec/runner/formatters/base.rb @@ -5,6 +5,9 @@ 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 @@ -40,8 +43,11 @@ class BaseFormatter @counter = @tally.counter if ENV['CHECK_LEAKS'] - save = ENV['CHECK_LEAKS'] == 'save' 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 @@ -113,6 +119,14 @@ class BaseFormatter # 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 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/multi.rb b/spec/mspec/lib/mspec/runner/formatters/multi.rb index a723ae8eb9..fa1da3766b 100644 --- a/spec/mspec/lib/mspec/runner/formatters/multi.rb +++ b/spec/mspec/lib/mspec/runner/formatters/multi.rb @@ -42,6 +42,6 @@ module MultiFormatter end def print_exception(exc, count) - print "\n#{count})\n#{exc}\n" + @err.print "\n#{count})\n#{exc}\n" end end diff --git a/spec/mspec/lib/mspec/runner/mspec.rb b/spec/mspec/lib/mspec/runner/mspec.rb index 19cf59b7d2..0e016c67a7 100644 --- a/spec/mspec/lib/mspec/runner/mspec.rb +++ b/spec/mspec/lib/mspec/runner/mspec.rb @@ -26,6 +26,7 @@ module MSpec @unload = nil @tagged = nil @current = nil + @passed = nil @example = nil @modes = [] @shared = {} @@ -36,9 +37,11 @@ module MSpec @repeat = 1 @expectation = nil @expectations = false + @skips = [] + @subprocesses = [] class << self - attr_reader :file, :include, :exclude + attr_reader :file, :include, :exclude, :skips, :subprocesses attr_writer :repeat, :randomize attr_accessor :formatter end @@ -116,6 +119,7 @@ module MSpec rescue SystemExit => e raise e rescue SkippedSpecError => e + @skips << [e, block] return false rescue Object => exc register_exit 1 @@ -241,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 @@ -360,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| @@ -391,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/shared.rb b/spec/mspec/lib/mspec/runner/shared.rb index 1d68365474..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 + 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/utils/name_map.rb b/spec/mspec/lib/mspec/utils/name_map.rb index a389b9d1de..9b04112e2e 100644 --- a/spec/mspec/lib/mspec/utils/name_map.rb +++ b/spec/mspec/lib/mspec/utils/name_map.rb @@ -51,6 +51,10 @@ class NameMap SpecVersion ] + ALWAYS_PRIVATE = %w[ + initialize initialize_copy initialize_clone initialize_dup respond_to_missing? + ].map(&:to_sym) + def initialize(filter = false) @seen = {} @filter = filter @@ -62,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) @@ -86,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? diff --git a/spec/mspec/lib/mspec/utils/options.rb b/spec/mspec/lib/mspec/utils/options.rb index 23a4c9a2a0..adeafa1f81 100644 --- a/spec/mspec/lib/mspec/utils/options.rb +++ b/spec/mspec/lib/mspec/utils/options.rb @@ -204,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| @@ -423,6 +430,10 @@ class MSpecOptions end MSpec.register :load, obj end + + on("--print-skips", "Print skips") do + config[:print_skips] = true + end end def interrupt @@ -473,13 +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 configure {} + env targets formatters filters @@ -496,5 +516,6 @@ class MSpecOptions 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 b9f8b17fdc..15fd23fabf 100644 --- a/spec/mspec/lib/mspec/utils/script.rb +++ b/spec/mspec/lib/mspec/utils/script.rb @@ -159,8 +159,14 @@ class MSpecScript end if config[:formatter] - config[:formatter].new(config[:output]) + 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 @@ -283,7 +289,6 @@ class MSpecScript script = new script.load_default - script.try_load '~/.mspecrc' script.options script.signals script.register diff --git a/spec/mspec/lib/mspec/utils/warnings.rb b/spec/mspec/lib/mspec/utils/warnings.rb index 0d3d36fada..23efc696a5 100644 --- a/spec/mspec/lib/mspec/utils/warnings.rb +++ b/spec/mspec/lib/mspec/utils/warnings.rb @@ -8,46 +8,3 @@ if Object.const_defined?(:Warning) and Warning.respond_to?(:[]=) Warning[:deprecated] = true Warning[:experimental] = false end - -if Object.const_defined?(:Warning) and Warning.respond_to?(:warn) - def Warning.warn(message, category: nil) - # Suppress any warning inside the method to prevent recursion - verbose = $VERBOSE - $VERBOSE = nil - - 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/ - else - $stderr.write message - end - ensure - $VERBOSE = verbose - end -else - $VERBOSE = nil unless ENV['OUTPUT_WARNINGS'] -end diff --git a/spec/mspec/spec/commands/mkspec_spec.rb b/spec/mspec/spec/commands/mkspec_spec.rb index 825add7212..32262723de 100644 --- a/spec/mspec/spec/commands/mkspec_spec.rb +++ b/spec/mspec/spec/commands/mkspec_spec.rb @@ -194,7 +194,7 @@ RSpec.describe MkSpec, "#write_spec" do end it "checks if specs exist for the method if the spec file exists" do - name = Regexp.escape(@script.ruby) + name = Regexp.escape(RbConfig.ruby) expect(@script).to receive(:`).with( %r"#{name} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e 'Object#inspect' spec/core/tcejbo/inspect_spec.rb") @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true) diff --git a/spec/mspec/spec/commands/mspec_ci_spec.rb b/spec/mspec/spec/commands/mspec_ci_spec.rb index bcbc5b4224..b8dc9d062f 100644 --- a/spec/mspec/spec/commands/mspec_ci_spec.rb +++ b/spec/mspec/spec/commands/mspec_ci_spec.rb @@ -78,6 +78,11 @@ RSpec.describe MSpecCI, "#options" do @script.options [] end + it "enables the repeat option" do + expect(@options).to receive(:repeat) + @script.options @argv + end + it "calls #custom_options" do expect(@script).to receive(:custom_options).with(@options) @script.options [] diff --git a/spec/mspec/spec/commands/mspec_run_spec.rb b/spec/mspec/spec/commands/mspec_run_spec.rb index 62acd49d7f..f96be2b43e 100644 --- a/spec/mspec/spec/commands/mspec_run_spec.rb +++ b/spec/mspec/spec/commands/mspec_run_spec.rb @@ -105,6 +105,11 @@ RSpec.describe MSpecRun, "#options" do @script.options @argv end + it "enables the repeat option" do + expect(@options).to receive(:repeat) + @script.options @argv + end + it "exits if there are no files to process and './spec' is not a directory" do expect(File).to receive(:directory?).with("./spec").and_return(false) expect(@options).to receive(:parse).and_return([]) diff --git a/spec/mspec/spec/commands/mspec_spec.rb b/spec/mspec/spec/commands/mspec_spec.rb index 82201c2075..d19bebb2d6 100644 --- a/spec/mspec/spec/commands/mspec_spec.rb +++ b/spec/mspec/spec/commands/mspec_spec.rb @@ -92,33 +92,6 @@ RSpec.describe MSpecMain, "#run" do end end -RSpec.describe "The --warnings option" do - before :each do - @options, @config = new_option - allow(MSpecOptions).to receive(:new).and_return(@options) - @script = MSpecMain.new - allow(@script).to receive(:config).and_return(@config) - end - - it "is enabled by #options" do - allow(@options).to receive(:on) - expect(@options).to receive(:on).with("--warnings", an_instance_of(String)) - @script.options - end - - it "sets flags to -w" do - @config[:flags] = [] - @script.options ["--warnings"] - expect(@config[:flags]).to include("-w") - end - - it "set OUTPUT_WARNINGS = '1' in the environment" do - ENV['OUTPUT_WARNINGS'] = '0' - @script.options ["--warnings"] - expect(ENV['OUTPUT_WARNINGS']).to eq('1') - end -end - RSpec.describe "The -j, --multi option" do before :each do @options, @config = new_option diff --git a/spec/mspec/spec/fixtures/should.rb b/spec/mspec/spec/fixtures/should.rb index f494775c5f..6eb156da25 100644 --- a/spec/mspec/spec/fixtures/should.rb +++ b/spec/mspec/spec/fixtures/should.rb @@ -40,7 +40,7 @@ MSpec.setup_env # Specs describe "MSpec expectation method #should" do it "accepts a matcher" do - :sym.should be_kind_of(Symbol) + 0.4.should be_close(0.5, 0.2) end it "causes a failure to be recorded" do @@ -59,7 +59,7 @@ end describe "MSpec expectation method #should_not" do it "accepts a matcher" do - "sym".should_not be_kind_of(Symbol) + 0.1.should_not be_close(0.5, 0.2) end it "causes a failure to be recorded" do diff --git a/spec/mspec/spec/guards/platform_spec.rb b/spec/mspec/spec/guards/platform_spec.rb index 88a7ad86f2..bd37432800 100644 --- a/spec/mspec/spec/guards/platform_spec.rb +++ b/spec/mspec/spec/guards/platform_spec.rb @@ -81,44 +81,44 @@ RSpec.describe Object, "#platform_is_not" do end end -RSpec.describe Object, "#platform_is :wordsize => SIZE_SPEC" do +RSpec.describe Object, "#platform_is :c_long_size => SIZE_SPEC" do before :each do - @guard = PlatformGuard.new :darwin, :wordsize => 32 + @guard = PlatformGuard.new :darwin, :c_long_size => 32 allow(PlatformGuard).to receive(:os?).and_return(true) allow(PlatformGuard).to receive(:new).and_return(@guard) ScratchPad.clear end - it "yields when #wordsize? returns true" do - allow(PlatformGuard).to receive(:wordsize?).and_return(true) - platform_is(:wordsize => 32) { ScratchPad.record :yield } + it "yields when #c_long_size? returns true" do + allow(PlatformGuard).to receive(:c_long_size?).and_return(true) + platform_is(:c_long_size => 32) { ScratchPad.record :yield } expect(ScratchPad.recorded).to eq(:yield) end - it "doesn not yield when #wordsize? returns false" do - allow(PlatformGuard).to receive(:wordsize?).and_return(false) - platform_is(:wordsize => 32) { ScratchPad.record :yield } + it "doesn not yield when #c_long_size? returns false" do + allow(PlatformGuard).to receive(:c_long_size?).and_return(false) + platform_is(:c_long_size => 32) { ScratchPad.record :yield } expect(ScratchPad.recorded).not_to eq(:yield) end end -RSpec.describe Object, "#platform_is_not :wordsize => SIZE_SPEC" do +RSpec.describe Object, "#platform_is_not :c_long_size => SIZE_SPEC" do before :each do - @guard = PlatformGuard.new :darwin, :wordsize => 32 + @guard = PlatformGuard.new :darwin, :c_long_size => 32 allow(PlatformGuard).to receive(:os?).and_return(true) allow(PlatformGuard).to receive(:new).and_return(@guard) ScratchPad.clear end - it "yields when #wordsize? returns false" do - allow(PlatformGuard).to receive(:wordsize?).and_return(false) - platform_is_not(:wordsize => 32) { ScratchPad.record :yield } + it "yields when #c_long_size? returns false" do + allow(PlatformGuard).to receive(:c_long_size?).and_return(false) + platform_is_not(:c_long_size => 32) { ScratchPad.record :yield } expect(ScratchPad.recorded).to eq(:yield) end - it "doesn not yield when #wordsize? returns true" do - allow(PlatformGuard).to receive(:wordsize?).and_return(true) - platform_is_not(:wordsize => 32) { ScratchPad.record :yield } + it "doesn not yield when #c_long_size? returns true" do + allow(PlatformGuard).to receive(:c_long_size?).and_return(true) + platform_is_not(:c_long_size => 32) { ScratchPad.record :yield } expect(ScratchPad.recorded).not_to eq(:yield) end end @@ -184,13 +184,13 @@ RSpec.describe PlatformGuard, ".standard?" do end end -RSpec.describe PlatformGuard, ".wordsize?" do +RSpec.describe PlatformGuard, ".c_long_size?" do it "returns true when arg is 32 and 1.size is 4" do - expect(PlatformGuard.wordsize?(32)).to eq(1.size == 4) + expect(PlatformGuard.c_long_size?(32)).to eq(1.size == 4) end it "returns true when arg is 64 and 1.size is 8" do - expect(PlatformGuard.wordsize?(64)).to eq(1.size == 8) + expect(PlatformGuard.c_long_size?(64)).to eq(1.size == 8) end end diff --git a/spec/mspec/spec/helpers/ruby_exe_spec.rb b/spec/mspec/spec/helpers/ruby_exe_spec.rb index 79ce55ca75..56bade1ba9 100644 --- a/spec/mspec/spec/helpers/ruby_exe_spec.rb +++ b/spec/mspec/spec/helpers/ruby_exe_spec.rb @@ -145,9 +145,9 @@ RSpec.describe Object, "#ruby_exe" do stub_const 'RUBY_EXE', 'ruby_spec_exe -w -Q' @script = RubyExeSpecs.new - allow(@script).to receive(:`).and_return('OUTPUT') + allow(IO).to receive(:popen).and_return('OUTPUT') - status_successful = double(Process::Status, exitstatus: 0) + status_successful = double(Process::Status, exited?: true, exitstatus: 0) allow(Process).to receive(:last_status).and_return(status_successful) end @@ -155,7 +155,7 @@ RSpec.describe Object, "#ruby_exe" do code = "code" options = {} output = "output" - allow(@script).to receive(:`).and_return(output) + expect(IO).to receive(:popen).and_return(output) expect(@script.ruby_exe(code, options)).to eq output end @@ -168,7 +168,7 @@ RSpec.describe Object, "#ruby_exe" do code = "code" options = {} expect(@script).to receive(:ruby_cmd).and_return("ruby_cmd") - expect(@script).to receive(:`).with("ruby_cmd") + expect(IO).to receive(:popen).with("ruby_cmd") @script.ruby_exe(code, options) end @@ -176,7 +176,7 @@ RSpec.describe Object, "#ruby_exe" do code = "code" options = {} - status_failed = double(Process::Status, exitstatus: 4) + status_failed = double(Process::Status, exited?: true, exitstatus: 4) allow(Process).to receive(:last_status).and_return(status_failed) expect { @@ -184,16 +184,16 @@ RSpec.describe Object, "#ruby_exe" do }.to raise_error(%r{Expected exit status is 0 but actual is 4 for command ruby_exe\(.+\)}) end - it "shows in the exception message if exitstatus is nil (e.g., signal)" do + it "shows in the exception message if a signal killed the process" do code = "code" options = {} - status_failed = double(Process::Status, exitstatus: nil) + status_failed = double(Process::Status, exited?: false, signaled?: true, termsig: Signal.list.fetch('TERM')) allow(Process).to receive(:last_status).and_return(status_failed) expect { @script.ruby_exe(code, options) - }.to raise_error(%r{Expected exit status is 0 but actual is nil for command ruby_exe\(.+\)}) + }.to raise_error(%r{Expected exit status is 0 but actual is :SIGTERM for command ruby_exe\(.+\)}) end describe "with :dir option" do @@ -227,7 +227,7 @@ RSpec.describe Object, "#ruby_exe" do expect(ENV).to receive(:[]=).with("ABC", "xyz") expect(ENV).to receive(:[]=).with("ABC", "123") - expect(@script).to receive(:`).and_raise(Exception) + expect(IO).to receive(:popen).and_raise(Exception) expect do @script.ruby_exe nil, :env => { :ABC => "xyz" } end.to raise_error(Exception) @@ -236,7 +236,7 @@ RSpec.describe Object, "#ruby_exe" do describe "with :exit_status option" do before do - status_failed = double(Process::Status, exitstatus: 4) + status_failed = double(Process::Status, exited?: true, exitstatus: 4) allow(Process).to receive(:last_status).and_return(status_failed) end @@ -248,7 +248,7 @@ RSpec.describe Object, "#ruby_exe" do it "does not raise exception when command ends with expected status" do output = "output" - allow(@script).to receive(:`).and_return(output) + expect(IO).to receive(:popen).and_return(output) expect(@script.ruby_exe("path", exit_status: 4)).to eq output end diff --git a/spec/mspec/spec/integration/run_spec.rb b/spec/mspec/spec/integration/run_spec.rb index 90dc051543..ea0735e9b2 100644 --- a/spec/mspec/spec/integration/run_spec.rb +++ b/spec/mspec/spec/integration/run_spec.rb @@ -1,20 +1,21 @@ require 'spec_helper' RSpec.describe "Running mspec" do + q = BACKTRACE_QUOTE a_spec_output = <<EOS 1) Foo#bar errors FAILED Expected 1 == 2 to be truthy but was false -CWD/spec/fixtures/a_spec.rb:8:in `block (2 levels) in <top (required)>' -CWD/spec/fixtures/a_spec.rb:2:in `<top (required)>' +CWD/spec/fixtures/a_spec.rb:8:in #{q}block (2 levels) in <top (required)>' +CWD/spec/fixtures/a_spec.rb:2:in #{q}<top (required)>' 2) Foo#bar fails ERROR RuntimeError: failure -CWD/spec/fixtures/a_spec.rb:12:in `block (2 levels) in <top (required)>' -CWD/spec/fixtures/a_spec.rb:2:in `<top (required)>' +CWD/spec/fixtures/a_spec.rb:12:in #{q}block (2 levels) in <top (required)>' +CWD/spec/fixtures/a_spec.rb:2:in #{q}<top (required)>' Finished in D.DDDDDD seconds EOS diff --git a/spec/mspec/spec/integration/tag_spec.rb b/spec/mspec/spec/integration/tag_spec.rb index 33df1cfd40..ae08e9d45f 100644 --- a/spec/mspec/spec/integration/tag_spec.rb +++ b/spec/mspec/spec/integration/tag_spec.rb @@ -13,6 +13,7 @@ RSpec.describe "Running mspec tag" do it "tags the failing specs" do fixtures = "spec/fixtures" out, ret = run_mspec("tag", "--add fails --fail #{fixtures}/tagging_spec.rb") + q = BACKTRACE_QUOTE expect(out).to eq <<EOS RUBY_DESCRIPTION .FF @@ -26,15 +27,15 @@ Tag#me érròrs in unicode Tag#me errors FAILED Expected 1 == 2 to be truthy but was false -CWD/spec/fixtures/tagging_spec.rb:9:in `block (2 levels) in <top (required)>' -CWD/spec/fixtures/tagging_spec.rb:3:in `<top (required)>' +CWD/spec/fixtures/tagging_spec.rb:9:in #{q}block (2 levels) in <top (required)>' +CWD/spec/fixtures/tagging_spec.rb:3:in #{q}<top (required)>' 2) Tag#me érròrs in unicode FAILED Expected 1 == 2 to be truthy but was false -CWD/spec/fixtures/tagging_spec.rb:13:in `block (2 levels) in <top (required)>' -CWD/spec/fixtures/tagging_spec.rb:3:in `<top (required)>' +CWD/spec/fixtures/tagging_spec.rb:13:in #{q}block (2 levels) in <top (required)>' +CWD/spec/fixtures/tagging_spec.rb:3:in #{q}<top (required)>' Finished in D.DDDDDD seconds diff --git a/spec/mspec/spec/matchers/raise_error_spec.rb b/spec/mspec/spec/matchers/raise_error_spec.rb index 8613eee118..957baa087d 100644 --- a/spec/mspec/spec/matchers/raise_error_spec.rb +++ b/spec/mspec/spec/matchers/raise_error_spec.rb @@ -19,7 +19,7 @@ RSpec.describe RaiseErrorMatcher do ensure_mspec_method(-> {}.method(:should)) run = false - -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error { |error| + -> { raise ExpectedException }.should.raise { |error| expect(error.class).to eq(ExpectedException) run = true } @@ -30,7 +30,7 @@ RSpec.describe RaiseErrorMatcher do ensure_mspec_method(-> {}.method(:should)) run = false - -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error do |error| + -> { raise ExpectedException }.should.raise do |error| expect(error.class).to eq(ExpectedException) run = true end @@ -84,10 +84,10 @@ RSpec.describe RaiseErrorMatcher do matcher.matches?(Proc.new { raise exc }) rescue UnexpectedException => e expect(matcher.failure_message).to eq( - ["Expected ExpectedException (message)", "but got: UnexpectedException (message)"] + ['Expected ExpectedException("message")', 'but got: UnexpectedException("message")'] ) expect(ExceptionState.new(nil, nil, e).message).to eq( - "Expected ExpectedException (message)\nbut got: UnexpectedException (message)" + "Expected ExpectedException(\"message\")\nbut got: UnexpectedException(\"message\")" ) else raise "no exception" @@ -103,10 +103,10 @@ RSpec.describe RaiseErrorMatcher do matcher.matches?(Proc.new { raise exc }) rescue ExpectedException => e expect(matcher.failure_message).to eq( - ["Expected ExpectedException (expected)", "but got: ExpectedException (unexpected)"] + ['Expected ExpectedException("expected")', 'but got: ExpectedException("unexpected")'] ) expect(ExceptionState.new(nil, nil, e).message).to eq( - "Expected ExpectedException (expected)\nbut got: ExpectedException (unexpected)" + "Expected ExpectedException(\"expected\")\nbut got: ExpectedException(\"unexpected\")" ) else raise "no exception" @@ -122,10 +122,10 @@ RSpec.describe RaiseErrorMatcher do matcher.matches?(Proc.new { raise exc }) rescue UnexpectedException => e expect(matcher.failure_message).to eq( - ["Expected ExpectedException (expected)", "but got: UnexpectedException (unexpected)"] + ['Expected ExpectedException("expected")', 'but got: UnexpectedException("unexpected")'] ) expect(ExceptionState.new(nil, nil, e).message).to eq( - "Expected ExpectedException (expected)\nbut got: UnexpectedException (unexpected)" + "Expected ExpectedException(\"expected\")\nbut got: UnexpectedException(\"unexpected\")" ) else raise "no exception" @@ -137,7 +137,7 @@ RSpec.describe RaiseErrorMatcher do matcher = RaiseErrorMatcher.new(ExpectedException, "expected") matcher.matches?(proc) expect(matcher.failure_message).to eq( - ["Expected ExpectedException (expected)", "but no exception was raised (120 was returned)"] + ['Expected ExpectedException("expected")', "but no exception was raised (120 was returned)"] ) end @@ -146,7 +146,7 @@ RSpec.describe RaiseErrorMatcher do matcher = RaiseErrorMatcher.new(ExpectedException, "expected") matcher.matches?(proc) expect(matcher.failure_message).to eq( - ["Expected ExpectedException (expected)", "but no exception was raised (nil was returned)"] + ['Expected ExpectedException("expected")', "but no exception was raised (nil was returned)"] ) end @@ -159,7 +159,7 @@ RSpec.describe RaiseErrorMatcher do matcher = RaiseErrorMatcher.new(ExpectedException, "expected") matcher.matches?(proc) expect(matcher.failure_message).to eq( - ["Expected ExpectedException (expected)", "but no exception was raised (#<Object>(#pretty_inspect raised #<ArgumentError: bad>) was returned)"] + ['Expected ExpectedException("expected")', 'but no exception was raised (#<Object>(#pretty_inspect raised #<ArgumentError: bad>) was returned)'] ) end @@ -168,7 +168,7 @@ RSpec.describe RaiseErrorMatcher do matcher = RaiseErrorMatcher.new(ExpectedException, "expected") matcher.matches?(proc) expect(matcher.negative_failure_message).to eq( - ["Expected to not get ExpectedException (expected)", ""] + ['Expected to not get ExpectedException("expected")', ""] ) end @@ -177,7 +177,58 @@ RSpec.describe RaiseErrorMatcher do matcher = RaiseErrorMatcher.new(Exception, nil) matcher.matches?(proc) expect(matcher.negative_failure_message).to eq( - ["Expected to not get Exception", "but got: UnexpectedException (unexpected)"] + ['Expected to not get Exception', 'but got: UnexpectedException'] ) end + + it "matches cause if given" do + cause = RuntimeError.new("foo") + proc = -> do + raise cause + rescue + raise "bar" + end + + matcher = RaiseErrorMatcher.new(RuntimeError, cause: cause) + expect(matcher.matches?(proc)).to eq(true) + end + + it "matches message and cause if given" do + cause = RuntimeError.new("foo") + proc = -> do + raise cause + rescue + raise "bar" + end + + matcher = RaiseErrorMatcher.new(RuntimeError, "bar", cause: cause) + expect(matcher.matches?(proc)).to eq(true) + end + + it "provides useful negative failure message when cause does not match" do + cause = RuntimeError.new("bar") + proc = -> do + raise "foo" + end + + matcher = RaiseErrorMatcher.new(RuntimeError, cause: cause) + + begin + matcher.matches?(proc) + rescue RuntimeError + expect(matcher.failure_message).to eq( + ['Expected RuntimeError(cause: #<RuntimeError: bar>)', 'but got: RuntimeError(cause: nil)'] + ) + end + + matcher = RaiseErrorMatcher.new(RuntimeError, "foo", cause: cause) + + begin + matcher.matches?(proc) + rescue RuntimeError + expect(matcher.failure_message).to eq( + ['Expected RuntimeError("foo", cause: #<RuntimeError: bar>)', 'but got: RuntimeError("foo", cause: nil)'] + ) + end + end end diff --git a/spec/mspec/spec/mocks/mock_spec.rb b/spec/mspec/spec/mocks/mock_spec.rb index 73f9bdfa14..7426e0ff88 100644 --- a/spec/mspec/spec/mocks/mock_spec.rb +++ b/spec/mspec/spec/mocks/mock_spec.rb @@ -22,14 +22,14 @@ end RSpec.describe Mock, ".replaced_name" do it "returns the name for a method that is being replaced by a mock method" do m = double('a fake id') - expect(Mock.replaced_name(m, :method_call)).to eq(:"__mspec_#{m.object_id}_method_call__") + expect(Mock.replaced_name(Mock.replaced_key(m, :method_call))).to eq(:"__mspec_method_call__") end end RSpec.describe Mock, ".replaced_key" do it "returns a key used internally by Mock" do m = double('a fake id') - expect(Mock.replaced_key(m, :method_call)).to eq([:"__mspec_#{m.object_id}_method_call__", :method_call]) + expect(Mock.replaced_key(m, :method_call)).to eq([m.object_id, :method_call]) end end @@ -42,16 +42,16 @@ RSpec.describe Mock, ".replaced?" do it "returns true if a method has been stubbed on an object" do Mock.install_method @mock, :method_call - expect(Mock.replaced?(Mock.replaced_name(@mock, :method_call))).to be_truthy + expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_truthy end it "returns true if a method has been mocked on an object" do Mock.install_method @mock, :method_call, :stub - expect(Mock.replaced?(Mock.replaced_name(@mock, :method_call))).to be_truthy + expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_truthy end it "returns false if a method has not been stubbed or mocked" do - expect(Mock.replaced?(Mock.replaced_name(@mock, :method_call))).to be_falsey + expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_falsey end end @@ -197,11 +197,11 @@ RSpec.describe Mock, ".install_method" do Mock.install_method @mock, :method_call expect(@mock).to respond_to(:method_call) - expect(@mock).not_to respond_to(Mock.replaced_name(@mock, :method_call)) + expect(@mock).not_to respond_to(Mock.replaced_name(Mock.replaced_key(@mock, :method_call))) Mock.install_method @mock, :method_call, :stub expect(@mock).to respond_to(:method_call) - expect(@mock).not_to respond_to(Mock.replaced_name(@mock, :method_call)) + expect(@mock).not_to respond_to(Mock.replaced_name(Mock.replaced_key(@mock, :method_call))) end end @@ -493,7 +493,7 @@ RSpec.describe Mock, ".cleanup" do it "removes the replaced method if the mock method overrides an existing method" do def @mock.already_here() :hey end expect(@mock).to respond_to(:already_here) - replaced_name = Mock.replaced_name(@mock, :already_here) + replaced_name = Mock.replaced_name(Mock.replaced_key(@mock, :already_here)) Mock.install_method @mock, :already_here expect(@mock).to respond_to(replaced_name) @@ -521,10 +521,9 @@ RSpec.describe Mock, ".cleanup" do replaced_key = Mock.replaced_key(@mock, :method_call) expect(Mock).to receive(:clear_replaced).with(replaced_key) - replaced_name = Mock.replaced_name(@mock, :method_call) - expect(Mock.replaced?(replaced_name)).to be_truthy + expect(Mock.replaced?(replaced_key)).to be_truthy Mock.cleanup - expect(Mock.replaced?(replaced_name)).to be_falsey + expect(Mock.replaced?(replaced_key)).to be_falsey end end diff --git a/spec/mspec/spec/runner/context_spec.rb b/spec/mspec/spec/runner/context_spec.rb index a864428aec..9ebc708c0c 100644 --- a/spec/mspec/spec/runner/context_spec.rb +++ b/spec/mspec/spec/runner/context_spec.rb @@ -914,7 +914,7 @@ RSpec.describe ContextState, "#it_should_behave_like" do it "raises an Exception if unable to find the shared ContextState" do expect(MSpec).to receive(:retrieve_shared).and_return(nil) - expect { @state.it_should_behave_like "this" }.to raise_error(Exception) + expect { @state.it_should_behave_like :this }.to raise_error(Exception) end describe "for nested ContextState instances" do diff --git a/spec/mspec/spec/spec_helper.rb b/spec/mspec/spec/spec_helper.rb index 3a749581ee..8ea38b644f 100644 --- a/spec/mspec/spec/spec_helper.rb +++ b/spec/mspec/spec/spec_helper.rb @@ -62,7 +62,4 @@ def ensure_mspec_method(method) expect(file).to start_with(File.expand_path('../../lib/mspec', __FILE__ )) end -PublicMSpecMatchers = Class.new { - include MSpecMatchers - public :raise_error -}.new +BACKTRACE_QUOTE = RUBY_VERSION >= "3.4" ? "'" : "`" diff --git a/spec/mspec/spec/utils/fixtures/this_file_raises.rb b/spec/mspec/spec/utils/fixtures/this_file_raises.rb new file mode 100644 index 0000000000..8e37a587bf --- /dev/null +++ b/spec/mspec/spec/utils/fixtures/this_file_raises.rb @@ -0,0 +1 @@ +raise "This is a BAD file" diff --git a/spec/mspec/spec/utils/fixtures/this_file_raises2.rb b/spec/mspec/spec/utils/fixtures/this_file_raises2.rb new file mode 100644 index 0000000000..8efc10199a --- /dev/null +++ b/spec/mspec/spec/utils/fixtures/this_file_raises2.rb @@ -0,0 +1 @@ +raise "This is a BAD file 2" diff --git a/spec/mspec/spec/utils/name_map_spec.rb b/spec/mspec/spec/utils/name_map_spec.rb index a18a481500..a42dc9ffec 100644 --- a/spec/mspec/spec/utils/name_map_spec.rb +++ b/spec/mspec/spec/utils/name_map_spec.rb @@ -21,6 +21,9 @@ module NameMapSpecs def f; end end + autoload :BadFile, "#{__dir__}/fixtures/this_file_raises.rb" + autoload :BadFile2, "#{__dir__}/fixtures/this_file_raises2.rb" + def self.n; end def n; end end @@ -84,6 +87,15 @@ RSpec.describe NameMap, "#class_or_module" do expect(@map.class_or_module("Hell")).to eq(nil) expect(@map.class_or_module("Bush::Brain")).to eq(nil) end + + it "returns nil if accessing the constant raises RuntimeError" do + expect { NameMapSpecs::BadFile }.to raise_error(RuntimeError) + expect(@map.class_or_module("NameMapSpecs::BadFile")).to eq(nil) + end + + it "returns nil if accessing the constant raises RuntimeError when not triggering the autoload before" do + expect(@map.class_or_module("NameMapSpecs::BadFile2")).to eq(nil) + end end RSpec.describe NameMap, "#dir_name" do diff --git a/spec/mspec/spec/utils/script_spec.rb b/spec/mspec/spec/utils/script_spec.rb index d9f6eac9a9..c35bda8b47 100644 --- a/spec/mspec/spec/utils/script_spec.rb +++ b/spec/mspec/spec/utils/script_spec.rb @@ -96,11 +96,6 @@ RSpec.describe MSpecScript, ".main" do MSpecScript.main end - it "attempts to load the '~/.mspecrc' script" do - expect(@script).to receive(:try_load).with('~/.mspecrc') - MSpecScript.main - end - it "calls the #options method on the script" do expect(@script).to receive(:options) MSpecScript.main diff --git a/spec/mspec/tool/check_require_spec_helper.rb b/spec/mspec/tool/check_require_spec_helper.rb new file mode 100755 index 0000000000..07126e68dc --- /dev/null +++ b/spec/mspec/tool/check_require_spec_helper.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby + +# This script is used to check that each *_spec.rb file has +# a relative_require for spec_helper which should live higher +# up in the ruby/spec repo directory tree. +# +# Prints errors to $stderr and returns a non-zero exit code when +# errors are found. +# +# Related to https://github.com/ruby/spec/pull/992 + +def check_file(fn) + File.foreach(fn) do |line| + return $1 if line =~ /^\s*require_relative\s*['"](.*spec_helper)['"]/ + end + nil +end + +rootdir = ARGV[0] || "." +fglob = File.join(rootdir, "**", "*_spec.rb") +specfiles = Dir.glob(fglob) +raise "No spec files found in #{fglob.inspect}. Give an argument to specify the root-directory of ruby/spec" if specfiles.empty? + +errors = 0 +specfiles.sort.each do |fn| + result = check_file(fn) + if result.nil? + warn "Missing require_relative for *spec_helper for file: #{fn}" + errors += 1 + end +end + +puts "# Found #{errors} files with require_relative spec_helper issues." +exit 1 if errors > 0 diff --git a/spec/mspec/tool/remove_old_guards.rb b/spec/mspec/tool/remove_old_guards.rb index 67485446bb..bc5612c78d 100644..100755 --- a/spec/mspec/tool/remove_old_guards.rb +++ b/spec/mspec/tool/remove_old_guards.rb @@ -1,6 +1,23 @@ +#!/usr/bin/env ruby + # Removes old version guards in ruby/spec. # Run it from the ruby/spec repository root. # The argument is the new minimum supported version. +# +# cd spec +# ../mspec/tool/remove_old_guards.rb <ruby-version> +# +# where <ruby-version> is a version guard with which should be removed +# +# Example: +# tool/remove_old_guards.rb 3.1 +# +# As a result guards like +# ruby_version_is "3.1" do +# # ... +# end +# +# will be removed. def dedent(line) if line.start_with?(" ") @@ -14,6 +31,12 @@ def each_spec_file(&block) Dir["*/**/*.rb"].each(&block) end +def each_file(&block) + Dir["**/*"].each { |path| + yield path if File.file?(path) + } +end + def remove_guards(guard, keep) each_spec_file do |file| contents = File.binread(file) @@ -46,9 +69,54 @@ def remove_guards(guard, keep) end end -def search(regexp) +def remove_empty_files + each_spec_file do |file| + unless file.include?("fixtures/") + lines = File.readlines(file) + if lines.all? { |line| line.chomp.empty? or line.start_with?('require', '#') } + puts "Removing empty file #{file}" + File.delete(file) + end + end + end +end + +def remove_unused_shared_specs + shared_groups = {} + # Dir["**/shared/**/*.rb"].each do |shared| + each_spec_file do |shared| + next if File.basename(shared) == 'constants.rb' + contents = File.binread(shared) + found = false + contents.scan(/^\s*describe (:[\w_?]+), shared: true do$/) { + shared_groups[$1] = 0 + found = true + } + if !found and shared.include?('shared/') and !shared.include?('fixtures/') and !shared.end_with?('/constants.rb') + puts "no shared describe in #{shared} ?" + end + end + each_spec_file do |file| contents = File.binread(file) + contents.scan(/(?:it_behaves_like|it_should_behave_like) (:[\w_?]+)[,\s]/) do + puts $1 unless shared_groups.key?($1) + shared_groups[$1] += 1 + end + end + + shared_groups.each_pair do |group, value| + if value == 0 + puts "Shared describe #{group} seems unused" + elsif value == 1 + puts "Shared describe #{group} seems used only once" if $VERBOSE + end + end +end + +def search(regexp) + each_file do |file| + contents = File.binread(file) if contents =~ regexp puts file contents.each_line do |line| @@ -60,11 +128,18 @@ def search(regexp) end end +abort "usage: #{$0} <ruby-version>" if ARGV.empty? + version = Regexp.escape(ARGV.fetch(0)) version += "(?:\\.0)?" if version.count(".") < 2 remove_guards(/ruby_version_is (["'])#{version}\1 do/, true) remove_guards(/ruby_version_is (["'])[0-9.]*\1 *... *(["'])#{version}\2 do/, false) -remove_guards(/ruby_bug "#\d+", (["'])[0-9.]*\1 *... *(["'])#{version}\2 do/, true) +remove_guards(/ruby_bug ["']#\d+["'], (["'])[0-9.]*\1 *... *(["'])#{version}\2 do/, true) + +remove_empty_files +remove_unused_shared_specs +puts "Search:" search(/(["'])#{version}\1/) search(/^\s*#.+#{version}/) +search(/RUBY_VERSION_IS_#{version.tr('.', '_')}/) diff --git a/spec/mspec/tool/sync/sync-rubyspec.rb b/spec/mspec/tool/sync/sync-rubyspec.rb index 13f1d8004d..86c43d0dc8 100644 --- a/spec/mspec/tool/sync/sync-rubyspec.rb +++ b/spec/mspec/tool/sync/sync-rubyspec.rb @@ -3,7 +3,7 @@ IMPLS = { truffleruby: { - git: "https://github.com/oracle/truffleruby.git", + git: "https://github.com/truffleruby/truffleruby.git", from_commit: "f10ab6988d", }, jruby: { @@ -23,6 +23,8 @@ MSPEC = ARGV.delete('--mspec') CHECK_LAST_MERGE = !MSPEC && ENV['CHECK_LAST_MERGE'] != 'false' TEST_MASTER = ENV['TEST_MASTER'] != 'false' +ONLY_FILTER = ENV['ONLY_FILTER'] == 'true' + MSPEC_REPO = File.expand_path("../../..", __FILE__) raise MSPEC_REPO if !Dir.exist?(MSPEC_REPO) or !Dir.exist?("#{MSPEC_REPO}/.git") @@ -32,6 +34,13 @@ raise RUBYSPEC_REPO unless Dir.exist?(RUBYSPEC_REPO) SOURCE_REPO = MSPEC ? MSPEC_REPO : RUBYSPEC_REPO +# LAST_MERGE is a commit of ruby/spec or ruby/mspec +# which is the spec/mspec commit that was last imported in the Ruby implementation +# (i.e. the commit in "Update to ruby/spec@commit"). +# It is normally automatically computed, but can be manually set when +# e.g. the last update of specs wasn't merged in the Ruby implementation. +LAST_MERGE = ENV["LAST_MERGE"] + NOW = Time.now BRIGHT_RED = "\e[31;1m" @@ -140,8 +149,8 @@ def rebase_commits(impl) else sh "git", "checkout", impl.name - if ENV["LAST_MERGE"] - last_merge = `git log -n 1 --format='%H %ct' #{ENV["LAST_MERGE"]}` + if LAST_MERGE + last_merge = `git log -n 1 --format='%H %ct' #{LAST_MERGE}` else last_merge = `git log --grep='^#{impl.last_merge_message}' -n 1 --format='%H %ct'` end @@ -181,30 +190,20 @@ def test_new_specs Dir.chdir(SOURCE_REPO) do workflow = YAML.load_file(".github/workflows/ci.yml") job_name = MSPEC ? "test" : "specs" - versions = workflow.dig("jobs", job_name, "strategy", "matrix", "ruby") + versions = workflow.dig("jobs", job_name, "strategy", "matrix", "ruby").map(&:to_s) versions = versions.grep(/^\d+\./) # Test on MRI min_version, max_version = versions.minmax test_command = MSPEC ? "bundle install && bundle exec rspec" : "../mspec/bin/mspec -j" run_test = -> version { - command = "chruby #{version} && #{test_command}" + command = "chruby ruby-#{version} && #{test_command}" sh ENV["SHELL"], "-c", command } run_test[min_version] run_test[max_version] - run_test["ruby-master"] if TEST_MASTER - end -end - -def verify_commits(impl) - puts - Dir.chdir(SOURCE_REPO) do - puts "Manually check commit messages:" - print "Press enter >" - STDIN.gets - system "git", "log", "master..." + run_test["master"] if TEST_MASTER end end @@ -230,15 +229,16 @@ def main(impls) impl = RubyImplementation.new(impl, data) update_repo(impl) filter_commits(impl) - rebase_commits(impl) - if new_commits?(impl) - test_new_specs - verify_commits(impl) - fast_forward_master(impl) - check_ci - else - STDERR.puts "#{BRIGHT_YELLOW}No new commits#{RESET}" - fast_forward_master(impl) + unless ONLY_FILTER + rebase_commits(impl) + if new_commits?(impl) + test_new_specs + fast_forward_master(impl) + check_ci + else + STDERR.puts "#{BRIGHT_YELLOW}No new commits#{RESET}" + fast_forward_master(impl) + end end end end diff --git a/spec/mspec/tool/tag_from_output.rb b/spec/mspec/tool/tag_from_output.rb index ebe13434c2..41aa70f932 100755 --- a/spec/mspec/tool/tag_from_output.rb +++ b/spec/mspec/tool/tag_from_output.rb @@ -3,6 +3,8 @@ # Adds tags based on error and failures output (e.g., from a CI log), # without running any spec code. +tag = ENV["TAG"] || "fails" + tags_dir = %w[ spec/tags spec/tags/ruby @@ -11,9 +13,14 @@ abort 'Could not find tags directory' unless tags_dir output = ARGF.readlines +# Automatically strip datetime of GitHub Actions +if output.first =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z / + output = output.map { |line| line.split(' ', 2).last } +end + NUMBER = /^\d+\)$/ ERROR_OR_FAILED = / (ERROR|FAILED)$/ -SPEC_FILE = /^(\/.+_spec\.rb)\:\d+/ +SPEC_FILE = /^((?:\/|[CD]:\/).+_spec\.rb)\:\d+/ output.slice_before(NUMBER).select { |number, *rest| number =~ NUMBER and rest.any? { |line| line =~ ERROR_OR_FAILED } @@ -22,11 +29,24 @@ output.slice_before(NUMBER).select { |number, *rest| description = error_line.match(ERROR_OR_FAILED).pre_match spec_file = rest.find { |line| line =~ SPEC_FILE } - unless spec_file - warn "Could not find file for:\n#{error_line}" - next + if spec_file + spec_file = spec_file[SPEC_FILE, 1] or raise + else + if error_line =~ /^([\w:]+)[#\.](\w+) / + mod, method = $1, $2 + file = "#{mod.downcase.gsub('::', '/')}/#{method}_spec.rb" + spec_file = ['spec/ruby/core', 'spec/ruby/library', *Dir.glob('spec/ruby/library/*')].find { |dir| + path = "#{dir}/#{file}" + break path if File.exist?(path) + } + end + + unless spec_file + warn "Could not find file for:\n#{error_line}" + next + end end - spec_file = spec_file[SPEC_FILE, 1] + prefix = spec_file.index('spec/ruby/') || spec_file.index('spec/truffle/') spec_file = spec_file[prefix..-1] @@ -36,7 +56,7 @@ output.slice_before(NUMBER).select { |number, *rest| dir = File.dirname(tags_file) Dir.mkdir(dir) unless Dir.exist?(dir) - tag_line = "fails:#{description}" + tag_line = "#{tag}:#{description}" lines = File.exist?(tags_file) ? File.readlines(tags_file, chomp: true) : [] unless lines.include?(tag_line) puts tags_file |
