diff options
Diffstat (limited to 'spec/mspec')
28 files changed, 343 insertions, 183 deletions
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 9c38cebcda..f5341c699d 100755 --- a/spec/mspec/lib/mspec/commands/mspec.rb +++ b/spec/mspec/lib/mspec/commands/mspec.rb @@ -38,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 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/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/ruby_exe.rb b/spec/mspec/lib/mspec/helpers/ruby_exe.rb index 7fde001cda..2e499d6f9a 100644 --- a/spec/mspec/lib/mspec/helpers/ruby_exe.rb +++ b/spec/mspec/lib/mspec/helpers/ruby_exe.rb @@ -140,28 +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}` - 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}" + 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/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/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 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/formatters/base.rb b/spec/mspec/lib/mspec/runner/formatters/base.rb index 54a83c9c32..e3b5bb23e0 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'] + save = ENV['CHECK_LEAKS'] == 'save' || ENV['CHECK_CONSTANT_LEAKS'] == 'save' ConstantsLeakCheckerAction.new(save).register end diff --git a/spec/mspec/lib/mspec/runner/mspec.rb b/spec/mspec/lib/mspec/runner/mspec.rb index 889e085175..97c3f365bc 100644 --- a/spec/mspec/lib/mspec/runner/mspec.rb +++ b/spec/mspec/lib/mspec/runner/mspec.rb @@ -38,9 +38,10 @@ module MSpec @expectation = nil @expectations = false @skips = [] + @subprocesses = [] class << self - attr_reader :file, :include, :exclude, :skips + attr_reader :file, :include, :exclude, :skips, :subprocesses attr_writer :repeat, :randomize attr_accessor :formatter end @@ -395,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/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 612caf6771..3b5962dbe6 100644 --- a/spec/mspec/lib/mspec/utils/options.rb +++ b/spec/mspec/lib/mspec/utils/options.rb @@ -477,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 b9f8b17fdc..e86beaab86 100644 --- a/spec/mspec/lib/mspec/utils/script.rb +++ b/spec/mspec/lib/mspec/utils/script.rb @@ -283,7 +283,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_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/ruby_exe_spec.rb b/spec/mspec/spec/helpers/ruby_exe_spec.rb index 61225a2756..56bade1ba9 100644 --- a/spec/mspec/spec/helpers/ruby_exe_spec.rb +++ b/spec/mspec/spec/helpers/ruby_exe_spec.rb @@ -145,7 +145,7 @@ 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, exited?: true, exitstatus: 0) allow(Process).to receive(:last_status).and_return(status_successful) @@ -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 @@ -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) @@ -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 67485446bb..3fd95e6b31 100644 --- a/spec/mspec/tool/remove_old_guards.rb +++ b/spec/mspec/tool/remove_old_guards.rb @@ -46,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) @@ -64,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/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 |