# frozen_string_literal: true module Test class << self ## # Filter object for backtraces. attr_accessor :backtrace_filter end class BacktraceFilter # :nodoc: def filter bt return ["No backtrace"] unless bt new_bt = [] pattern = %r[/(?:lib\/test/|core_assertions\.rb:)] unless $DEBUG then bt.each do |line| break if pattern.match?(line) new_bt << line end new_bt = bt.reject { |line| pattern.match?(line) } if new_bt.empty? new_bt = bt.dup if new_bt.empty? else new_bt = bt.dup end new_bt end end self.backtrace_filter = BacktraceFilter.new def self.filter_backtrace bt # :nodoc: backtrace_filter.filter bt end module Unit module Assertions def assert_raises(*exp, &b) raise NoMethodError, "use assert_raise", caller end def _assertions= n # :nodoc: @_assertions = n end def _assertions # :nodoc: @_assertions ||= 0 end ## # Returns a proc that will output +msg+ along with the default message. def message msg = nil, ending = nil, &default proc { ending ||= (ending_pattern = /(? 0 and b > 0 assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) end rescue LoadError pend end # :call-seq: # assert_nothing_raised( *args, &block ) # #If any exceptions are given as arguments, the assertion will #fail if one of those exceptions are raised. Otherwise, the test fails #if any exceptions are raised. # #The final argument may be a failure message. # # assert_nothing_raised RuntimeError do # raise Exception #Assertion passes, Exception is not a RuntimeError # end # # assert_nothing_raised do # raise Exception #Assertion fails # end def assert_nothing_raised(*args) self._assertions += 1 if Module === args.last msg = nil else msg = args.pop end begin yield rescue Test::Unit::PendedError, *(Test::Unit::AssertionFailedError if args.empty?) raise rescue *(args.empty? ? Exception : args) => e msg = message(msg) { "Exception raised:\n<#{mu_pp(e)}>\n""Backtrace:\n" << Test.filter_backtrace(e.backtrace).map{|frame| " #{frame}"}.join("\n") } raise Test::Unit::AssertionFailedError, msg.call, e.backtrace end end def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil) fname ||= caller_locations(2, 1)[0] mesg ||= fname.to_s verbose, $VERBOSE = $VERBOSE, verbose case when Array === fname fname, line = *fname when defined?(fname.path) && defined?(fname.lineno) fname, line = fname.path, fname.lineno else line = 1 end yield(code, fname, line, message(mesg) { if code.end_with?("\n") "```\n#{code}```\n" else "```\n#{code}\n```\n""no-newline" end }) ensure $VERBOSE = verbose end def assert_valid_syntax(code, *args, **opt) prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| yield if defined?(yield) assert_nothing_raised(SyntaxError, mesg) do assert_equal(:ok, syntax_check(src, fname, line), mesg) end end end def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) if child_env child_env = [child_env] else child_env = [] end out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) assert !status.signaled?, FailDesc[status, message, out] end def assert_ruby_status(args, test_stdin="", message=nil, **opt) out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) desc = FailDesc[status, message, out] assert(!status.signaled?, desc) message ||= "ruby exit status is not success:" assert(status.success?, desc) end ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") def separated_runner(token, out = nil) include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) }) out = out ? IO.new(out, 'w') : STDOUT at_exit { out.puts "#{token}", [Marshal.dump($!)].pack('m'), "#{token}", "#{token}assertions=#{self._assertions}" } if defined?(Test::Unit::Runner) Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) elsif defined?(Test::Unit::AutoRunner) Test::Unit::AutoRunner.need_auto_run = false end end def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) unless file and line loc, = caller_locations(1,1) file ||= loc.path line ||= loc.lineno end capture_stdout = true unless /mswin|mingw/ =~ RbConfig::CONFIG['host_os'] capture_stdout = false opt[:out] = Test::Unit::Runner.output if defined?(Test::Unit::Runner) res_p, res_c = IO.pipe opt[:ios] = [res_c] end token_dump, token_re = new_test_token src = <\n\K.*\n(?=#{token_re}<\/error>$)/m].unpack1("m")) rescue => marshal_error ignore_stderr = nil res = nil end if res and !(SystemExit === res) if bt = res.backtrace bt.each do |l| l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} end bt.concat(caller) else res.set_backtrace(caller) end raise res end # really is it succeed? unless ignore_stderr # the body of assert_separately must not output anything to detect error assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) end assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) raise marshal_error if marshal_error end # Run Ractor-related test without influencing the main test suite def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt) return unless defined?(Ractor) require = "require #{require.inspect}" if require if require_relative dir = File.dirname(caller_locations[0,1][0].absolute_path) full_path = File.expand_path(require_relative, dir) require = "#{require}; require #{full_path.inspect}" end assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt) #{require} previous_verbose = $VERBOSE $VERBOSE = nil Ractor.new {} # trigger initial warning $VERBOSE = previous_verbose #{src} RUBY end # :call-seq: # assert_throw( tag, failure_message = nil, &block ) # #Fails unless the given block throws +tag+, returns the caught #value otherwise. # #An optional failure message may be provided as the final argument. # # tag = Object.new # assert_throw(tag, "#{tag} was not thrown!") do # throw tag # end def assert_throw(tag, msg = nil) ret = catch(tag) do begin yield(tag) rescue UncaughtThrowError => e thrown = e.tag end msg = message(msg) { "Expected #{mu_pp(tag)} to have been thrown"\ "#{%Q[, not #{thrown}] if thrown}" } assert(false, msg) end assert(true) ret end # :call-seq: # assert_raise( *args, &block ) # #Tests if the given block raises an exception. Acceptable exception #types may be given as optional arguments. If the last argument is a #String, it will be used as the error message. # # assert_raise do #Fails, no Exceptions are raised # end # # assert_raise NameError do # puts x #Raises NameError, so assertion succeeds # end def assert_raise(*exp, &b) case exp.last when String, Proc msg = exp.pop end begin yield rescue Test::Unit::PendedError => e return e if exp.include? Test::Unit::PendedError raise e rescue Exception => e expected = exp.any? { |ex| if ex.instance_of? Module then e.kind_of? ex else e.instance_of? ex end } assert expected, proc { flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) } return e ensure unless e exp = exp.first if exp.size == 1 flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) end end end # :call-seq: # assert_raise_with_message(exception, expected, msg = nil, &block) # #Tests if the given block raises an exception with the expected #message. # # assert_raise_with_message(RuntimeError, "foo") do # nil #Fails, no Exceptions are raised # end # # assert_raise_with_message(RuntimeError, "foo") do # raise ArgumentError, "foo" #Fails, different Exception is raised # end # # assert_raise_with_message(RuntimeError, "foo") do # raise "bar" #Fails, RuntimeError is raised but the message differs # end # # assert_raise_with_message(RuntimeError, "foo") do # raise "foo" #Raises RuntimeError with the message, so assertion succeeds # end def assert_raise_with_message(exception, expected, msg = nil, &block) case expected when String assert = :assert_equal when Regexp assert = :assert_match else raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" end ex = m = nil EnvUtil.with_default_internal(expected.encoding) do ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do yield end m = ex.message end msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} if assert == :assert_equal assert_equal(expected, m, msg) else msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } assert expected =~ m, msg block.binding.eval("proc{|_|$~=_}").call($~) end ex end TEST_DIR = File.join(__dir__, "test/unit") #:nodoc: # :call-seq: # assert(test, [failure_message]) # #Tests if +test+ is true. # #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used #as the failure message. Otherwise, the result of calling +msg+ will be #used as the message if the assertion fails. # #If no +msg+ is given, a default message will be used. # # assert(false, "This was expected to be true") def assert(test, *msgs) case msg = msgs.first when String, Proc when nil msgs.shift else bt = caller.reject { |s| s.start_with?(TEST_DIR) } raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt end unless msgs.empty? super end # :call-seq: # assert_respond_to( object, method, failure_message = nil ) # #Tests if the given Object responds to +method+. # #An optional failure message may be provided as the final argument. # # assert_respond_to("hello", :reverse) #Succeeds # assert_respond_to("hello", :does_not_exist) #Fails def assert_respond_to(obj, (meth, *priv), msg = nil) unless priv.empty? msg = message(msg) { "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" } return assert obj.respond_to?(meth, *priv), msg end #get rid of overcounting if caller_locations(1, 1)[0].path.start_with?(TEST_DIR) return if obj.respond_to?(meth) end super(obj, meth, msg) end # :call-seq: # assert_not_respond_to( object, method, failure_message = nil ) # #Tests if the given Object does not respond to +method+. # #An optional failure message may be provided as the final argument. # # assert_not_respond_to("hello", :reverse) #Fails # assert_not_respond_to("hello", :does_not_exist) #Succeeds def assert_not_respond_to(obj, (meth, *priv), msg = nil) unless priv.empty? msg = message(msg) { "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" } return assert !obj.respond_to?(meth, *priv), msg end #get rid of overcounting if caller_locations(1, 1)[0].path.start_with?(TEST_DIR) return unless obj.respond_to?(meth) end refute_respond_to(obj, meth, msg) end # pattern_list is an array which contains regexp, string and :*. # :* means any sequence. # # pattern_list is anchored. # Use [:*, regexp/string, :*] for non-anchored match. def assert_pattern_list(pattern_list, actual, message=nil) rest = actual anchored = true pattern_list.each_with_index {|pattern, i| if pattern == :* anchored = false else if anchored match = rest.rindex(pattern, 0) else match = rest.index(pattern) end if match post_match = $~ ? $~.post_match : rest[match+pattern.size..-1] else msg = message(msg) { expect_msg = "Expected #{mu_pp pattern}\n" if /\n[^\n]/ =~ rest actual_mesg = +"to match\n" rest.scan(/.*\n+/) { actual_mesg << ' ' << $&.inspect << "+\n" } actual_mesg.sub!(/\+\n\z/, '') else actual_mesg = "to match " + mu_pp(rest) end actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" expect_msg + actual_mesg } assert false, msg end rest = post_match anchored = true end } if anchored assert_equal("", rest) end end def assert_warning(pat, msg = nil) result = nil stderr = EnvUtil.with_default_internal(pat.encoding) { EnvUtil.verbose_warning { result = yield } } msg = message(msg) {diff pat, stderr} assert(pat === stderr, msg) result end def assert_warn(*args) assert_warning(*args) {$VERBOSE = false; yield} end def assert_deprecated_warning(mesg = /deprecated/) assert_warning(mesg) do Warning[:deprecated] = true if Warning.respond_to?(:[]=) yield end end def assert_deprecated_warn(mesg = /deprecated/) assert_warn(mesg) do Warning[:deprecated] = true if Warning.respond_to?(:[]=) yield end end class << (AssertFile = Struct.new(:failure_message).new) include Assertions include CoreAssertions def assert_file_predicate(predicate, *args) if /\Anot_/ =~ predicate predicate = $' neg = " not" end result = File.__send__(predicate, *args) result = !result if neg mesg = "Expected file ".dup << args.shift.inspect mesg << "#{neg} to be #{predicate}" mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? mesg << " #{failure_message}" if failure_message assert(result, mesg) end alias method_missing assert_file_predicate def for(message) clone.tap {|a| a.failure_message = message} end end class AllFailures attr_reader :failures def initialize @count = 0 @failures = {} end def for(key) @count += 1 yield key rescue Exception => e @failures[key] = [@count, e] end def foreach(*keys) keys.each do |key| @count += 1 begin yield key rescue Exception => e @failures[key] = [@count, e] end end end def message i = 0 total = @count.to_s fmt = "%#{total.size}d" @failures.map {|k, (n, v)| v = v.message "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}" }.join("\n") end def pass? @failures.empty? end end # threads should respond to shift method. # Array can be used. def assert_join_threads(threads, message = nil) errs = [] values = [] while th = threads.shift begin values << th.value rescue Exception errs << [th, $!] th = nil end end values ensure if th&.alive? th.raise(Timeout::Error.new) th.join rescue errs << [th, $!] end if !errs.empty? msg = "exceptions on #{errs.length} threads:\n" + errs.map {|t, err| "#{t.inspect}:\n" + (err.respond_to?(:full_message) ? err.full_message(highlight: false, order: :top) : err.message) }.join("\n---\n") if message msg = "#{message}\n#{msg}" end raise Test::Unit::AssertionFailedError, msg end end def assert_all?(obj, m = nil, &blk) failed = [] obj.each do |*a, &b| unless blk.call(*a, &b) failed << (a.size > 1 ? a : a[0]) end end assert(failed.empty?, message(m) {failed.pretty_inspect}) end def assert_all_assertions(msg = nil) all = AllFailures.new yield all ensure assert(all.pass?, message(msg) {all.message.chomp(".")}) end alias all_assertions assert_all_assertions def assert_all_assertions_foreach(msg = nil, *keys, &block) all = AllFailures.new all.foreach(*keys, &block) ensure assert(all.pass?, message(msg) {all.message.chomp(".")}) end alias all_assertions_foreach assert_all_assertions_foreach %w[ CLOCK_THREAD_CPUTIME_ID CLOCK_PROCESS_CPUTIME_ID CLOCK_MONOTONIC ].find do |c| if Process.const_defined?(c) [c.to_sym, Process.const_get(c)].find do |clk| begin Process.clock_gettime(clk) rescue # Constants may be defined but not implemented, e.g., mingw. else PERFORMANCE_CLOCK = clk end end end end # Expect +seq+ to respond to +first+ and +each+ methods, e.g., # Array, Range, Enumerator::ArithmeticSequence and other # Enumerable-s, and each elements should be size factors. # # :yield: each elements of +seq+. def assert_linear_performance(seq, rehearsal: nil, pre: ->(n) {n}) pend "No PERFORMANCE_CLOCK found" unless defined?(PERFORMANCE_CLOCK) # Timeout testing generally doesn't work when RJIT compilation happens. rjit_enabled = defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? measure = proc do |arg, message| st = Process.clock_gettime(PERFORMANCE_CLOCK) yield(*arg) t = (Process.clock_gettime(PERFORMANCE_CLOCK) - st) assert_operator 0, :<=, t, message unless rjit_enabled t end first = seq.first *arg = pre.call(first) times = (0..(rehearsal || (2 * first))).map do measure[arg, "rehearsal"].nonzero? end times.compact! tmin, tmax = times.minmax tbase = 10 ** Math.log10(tmax * ([(tmax / tmin), 2].max ** 2)).ceil info = "(tmin: #{tmin}, tmax: #{tmax}, tbase: #{tbase})" seq.each do |i| next if i == first t = tbase * i.fdiv(first) *arg = pre.call(i) message = "[#{i}]: in #{t}s #{info}" Timeout.timeout(t, Timeout::Error, message) do measure[arg, message] end end end def diff(exp, act) require 'pp' q = PP.new(+"") q.guard_inspect_key do q.group(2, "expected: ") do q.pp exp end q.text q.newline q.group(2, "actual: ") do q.pp act end q.flush end q.output end def new_test_token token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" return token.dump, Regexp.quote(token) end end end end