diff options
Diffstat (limited to 'spec/mspec')
42 files changed, 529 insertions, 203 deletions
diff --git a/spec/mspec/bin/mspec b/spec/mspec/bin/mspec index f833257bb0..5bd753c06d 100755 --- a/spec/mspec/bin/mspec +++ b/spec/mspec/bin/mspec @@ -4,4 +4,4 @@ $:.unshift File.expand_path('../../lib', __FILE__) require 'mspec/commands/mspec' -MSpecMain.main +MSpecMain.main(false) diff --git a/spec/mspec/lib/mspec/commands/mkspec.rb b/spec/mspec/lib/mspec/commands/mkspec.rb index d10cc35d18..a31cb2191c 100755 --- a/spec/mspec/lib/mspec/commands/mkspec.rb +++ b/spec/mspec/lib/mspec/commands/mkspec.rb @@ -95,7 +95,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 +135,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.rb b/spec/mspec/lib/mspec/commands/mspec.rb index 9d82949ff1..f5341c699d 100755 --- a/spec/mspec/lib/mspec/commands/mspec.rb +++ b/spec/mspec/lib/mspec/commands/mspec.rb @@ -25,6 +25,7 @@ class MSpecMain < MSpecScript 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 +38,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 @@ -110,8 +106,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 8d01dc22ab..09852ab557 100644 --- a/spec/mspec/lib/mspec/expectations/expectations.rb +++ b/spec/mspec/lib/mspec/expectations/expectations.rb @@ -32,4 +32,8 @@ class SpecExpectation 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/guards/platform.rb b/spec/mspec/lib/mspec/guards/platform.rb index 2d5c2de6b6..e87b08a4c1 100644 --- a/spec/mspec/lib/mspec/guards/platform.rb +++ b/spec/mspec/lib/mspec/guards/platform.rb @@ -41,6 +41,10 @@ class PlatformGuard < SpecGuard os?(:windows) end + def self.wasi? + os?(:wasi) + end + def self.wsl? if defined?(@wsl_p) @wsl_p 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/datetime.rb b/spec/mspec/lib/mspec/helpers/datetime.rb index 3a40cc0902..84ac86b686 100644 --- a/spec/mspec/lib/mspec/helpers/datetime.rb +++ b/spec/mspec/lib/mspec/helpers/datetime.rb @@ -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/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 db1fde64d8..c1ed81a233 100644 --- a/spec/mspec/lib/mspec/helpers/numeric.rb +++ b/spec/mspec/lib/mspec/helpers/numeric.rb @@ -9,7 +9,9 @@ def infinity_value end def bignum_value(plus = 0) - 0x8000_0000_0000_0000 + plus + # 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 diff --git a/spec/mspec/lib/mspec/helpers/ruby_exe.rb b/spec/mspec/lib/mspec/helpers/ruby_exe.rb index 4948ec09f1..2e499d6f9a 100644 --- a/spec/mspec/lib/mspec/helpers/ruby_exe.rb +++ b/spec/mspec/lib/mspec/helpers/ruby_exe.rb @@ -112,6 +112,8 @@ unless Object.const_defined?(:RUBY_EXE) and 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 @@ -138,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..4c0eddab75 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 @@ -41,6 +41,7 @@ def tmp(name, uniquify = true) 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 94d3b71e55..d9d7f6fec0 100644 --- a/spec/mspec/lib/mspec/matchers/base.rb +++ b/spec/mspec/lib/mspec/matchers/base.rb @@ -16,15 +16,24 @@ class SpecPositiveOperatorMatcher < BasicObject end def ==(expected) - method_missing(:==, expected) + result = @actual == expected + unless result + ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be truthy") + end end def !=(expected) - method_missing(:!=, expected) + result = @actual != expected + unless result + ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be truthy") + end end def equal?(expected) - method_missing(:equal?, expected) + result = @actual.equal?(expected) + unless result + ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be truthy") + end end def method_missing(name, *args, &block) @@ -41,15 +50,24 @@ class SpecNegativeOperatorMatcher < BasicObject end def ==(expected) - method_missing(:==, expected) + result = @actual == expected + if result + ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be falsy") + end end def !=(expected) - method_missing(:!=, expected) + result = @actual != expected + if result + ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be falsy") + end end def equal?(expected) - method_missing(:equal?, expected) + result = @actual.equal?(expected) + if result + ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be falsy") + end end def method_missing(name, *args, &block) 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/output.rb b/spec/mspec/lib/mspec/matchers/output.rb index 20721df743..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 diff --git a/spec/mspec/lib/mspec/matchers/raise_error.rb b/spec/mspec/lib/mspec/matchers/raise_error.rb index 0ee8953519..54378bb34c 100644 --- a/spec/mspec/lib/mspec/matchers/raise_error.rb +++ b/spec/mspec/lib/mspec/matchers/raise_error.rb @@ -1,4 +1,6 @@ class RaiseErrorMatcher + FAILURE_MESSAGE_FOR_EXCEPTION = {}.compare_by_identity + attr_writer :block def initialize(exception, message, &block) @@ -15,7 +17,7 @@ class RaiseErrorMatcher def matches?(proc) @result = proc.call return false - rescue Exception => actual + rescue Object => actual @actual = actual if matching_exception?(actual) @@ -23,7 +25,7 @@ class RaiseErrorMatcher @block[actual] if @block return true else - actual.instance_variable_set(:@mspec_raise_error_message, failure_message) + FAILURE_MESSAGE_FOR_EXCEPTION[actual] = failure_message raise actual 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..71797b9815 100644 --- a/spec/mspec/lib/mspec/runner/actions/leakchecker.rb +++ b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb @@ -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 c85bf49ad3..1200926872 100644 --- a/spec/mspec/lib/mspec/runner/actions/timeout.rb +++ b/spec/mspec/lib/mspec/runner/actions/timeout.rb @@ -3,11 +3,14 @@ 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 MSpec.register :start, self MSpec.register :before, self + MSpec.register :after, self MSpec.register :finish, self end @@ -35,9 +38,27 @@ class TimeoutAction if @queue.empty? elapsed = now - @started if elapsed > @timeout - STDERR.puts "\n#{@current_state.description}" + 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 - abort "Example took longer than the configured timeout of #{@timeout}s" + + 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 @@ -53,8 +74,72 @@ class TimeoutAction 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 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/exception.rb b/spec/mspec/lib/mspec/runner/exception.rb index e07f02f684..23375733e6 100644 --- a/spec/mspec/lib/mspec/runner/exception.rb +++ b/spec/mspec/lib/mspec/runner/exception.rb @@ -29,7 +29,7 @@ class ExceptionState if @failure message - elsif raise_error_message = @exception.instance_variable_get(:@mspec_raise_error_message) + elsif raise_error_message = RaiseErrorMatcher::FAILURE_MESSAGE_FOR_EXCEPTION[@exception] raise_error_message.join("\n") else "#{@exception.class}: #{message}" diff --git a/spec/mspec/lib/mspec/runner/formatters/base.rb b/spec/mspec/lib/mspec/runner/formatters/base.rb index 6c075c4d0a..e3b5bb23e0 100644 --- a/spec/mspec/lib/mspec/runner/formatters/base.rb +++ b/spec/mspec/lib/mspec/runner/formatters/base.rb @@ -1,9 +1,13 @@ 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 @@ -18,10 +22,17 @@ class BaseFormatter @count = 0 # For subclasses - if out.nil? + 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 - @out = File.open out, "w" + @err = @out end end @@ -32,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'] + save = ENV['CHECK_LEAKS'] == 'save' || ENV['CHECK_CONSTANT_LEAKS'] == 'save' ConstantsLeakCheckerAction.new(save).register end @@ -105,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 @@ -115,9 +137,9 @@ class BaseFormatter 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" + @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. diff --git a/spec/mspec/lib/mspec/runner/mspec.rb b/spec/mspec/lib/mspec/runner/mspec.rb index 8331086196..97c3f365bc 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,8 +119,9 @@ module MSpec rescue SystemExit => e raise e rescue SkippedSpecError => e + @skips << [e, block] return false - rescue Exception => exc + rescue Object => exc register_exit 1 actions :exception, ExceptionState.new(current && current.state, location, exc) return false @@ -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 @@ -391,7 +396,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..bf70e651a2 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 @@ -86,7 +90,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 bef1dbdd2e..3b5962dbe6 100644 --- a/spec/mspec/lib/mspec/utils/options.rb +++ b/spec/mspec/lib/mspec/utils/options.rb @@ -32,6 +32,10 @@ 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) @@ -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: @@ -311,6 +315,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 @@ -414,6 +423,10 @@ class MSpecOptions end MSpec.register :load, obj end + + on("--print-skips", "Print skips") do + config[:print_skips] = true + end end def interrupt @@ -464,7 +477,7 @@ 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 diff --git a/spec/mspec/lib/mspec/utils/script.rb b/spec/mspec/lib/mspec/utils/script.rb index a77476ee2e..e86beaab86 100644 --- a/spec/mspec/lib/mspec/utils/script.rb +++ b/spec/mspec/lib/mspec/utils/script.rb @@ -37,6 +37,17 @@ 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 check_version! @@ -267,10 +278,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 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_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/helpers/numeric_spec.rb b/spec/mspec/spec/helpers/numeric_spec.rb index e65f3e8610..64495b7276 100644 --- a/spec/mspec/spec/helpers/numeric_spec.rb +++ b/spec/mspec/spec/helpers/numeric_spec.rb @@ -4,11 +4,17 @@ require 'mspec/helpers' RSpec.describe Object, "#bignum_value" do it "returns a value that is an instance of Bignum on any platform" do - expect(bignum_value).to eq(0x8000_0000_0000_0000) + expect(bignum_value).to be > fixnum_max end it "returns the default value incremented by the argument" do - expect(bignum_value(42)).to eq(0x8000_0000_0000_002a) + expect(bignum_value(42)).to eq(bignum_value + 42) + end +end + +RSpec.describe Object, "-bignum_value" do + it "returns a value that is an instance of Bignum on any platform" do + expect(-bignum_value).to be < fixnum_min 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/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..5cabfe5626 100644 --- a/spec/mspec/spec/spec_helper.rb +++ b/spec/mspec/spec/spec_helper.rb @@ -66,3 +66,5 @@ PublicMSpecMatchers = Class.new { include MSpecMatchers public :raise_error }.new + +BACKTRACE_QUOTE = RUBY_VERSION >= "3.4" ? "'" : "`" 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 718e351e11..3fd95e6b31 100644 --- a/spec/mspec/tool/remove_old_guards.rb +++ b/spec/mspec/tool/remove_old_guards.rb @@ -21,20 +21,24 @@ def remove_guards(guard, keep) puts file lines = contents.lines.to_a while first = lines.find_index { |line| line =~ guard } + comment = first + while comment > 0 and lines[comment-1] =~ /^(\s*)#/ + comment -= 1 + end indent = lines[first][/^(\s*)/, 1].length last = (first+1...lines.size).find { |i| space = lines[i][/^(\s*)end$/, 1] and space.length == indent } raise file unless last if keep - lines[first..last] = lines[first+1..last-1].map { |l| dedent(l) } + lines[comment..last] = lines[first+1..last-1].map { |l| dedent(l) } else - if first > 0 and lines[first-1] == "\n" - first -= 1 + if comment > 0 and lines[comment-1] == "\n" + comment -= 1 elsif lines[last+1] == "\n" last += 1 end - lines[first..last] = [] + lines[comment..last] = [] end end File.binwrite file, lines.join @@ -42,6 +46,51 @@ def remove_guards(guard, keep) end end +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_spec_file do |file| contents = File.binread(file) @@ -60,7 +109,11 @@ 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}/) diff --git a/spec/mspec/tool/sync/sync-rubyspec.rb b/spec/mspec/tool/sync/sync-rubyspec.rb index b4c79d2afc..13f1d8004d 100644 --- a/spec/mspec/tool/sync/sync-rubyspec.rb +++ b/spec/mspec/tool/sync/sync-rubyspec.rb @@ -20,7 +20,7 @@ IMPLS = { MSPEC = ARGV.delete('--mspec') -CHECK_LAST_MERGE = ENV['CHECK_LAST_MERGE'] != 'false' +CHECK_LAST_MERGE = !MSPEC && ENV['CHECK_LAST_MERGE'] != 'false' TEST_MASTER = ENV['TEST_MASTER'] != 'false' MSPEC_REPO = File.expand_path("../../..", __FILE__) diff --git a/spec/mspec/tool/tag_from_output.rb b/spec/mspec/tool/tag_from_output.rb index ebe13434c2..b6b4603855 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,6 +13,11 @@ 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+/ @@ -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 |