# -*- coding: utf-8 -*- ## # Minimal Test framework for mruby # module MTest ## # Assertion base class class Assertion < Exception; end ## # Assertion raised when skipping a test class Skip < Assertion; end module Assertions def mu_pp obj obj.inspect end def diff exp, act return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" end def _assertions= n @_assertions = n end def _assertions @_assertions = 0 unless @_assertions @_assertions end ## # Fails unless +test+ is a true value. def assert test, msg = nil msg ||= "Failed assertion, no message given." self._assertions += 1 unless test msg = msg.call if Proc === msg raise MTest::Assertion, msg end true end alias assert_true assert ## # Fails unless +test+ is a false value def assert_false test, msg = nil msg = message(msg) { "Expected #{mu_pp(test)} to be false" } assert test == false, msg end ## # Fails unless the block returns a true value. def assert_block msg = nil msg = message(msg) { "Expected block to return true value" } assert yield, msg end ## # Fails unless +obj+ is empty. def assert_empty obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" } assert_respond_to obj, :empty? assert obj.empty?, msg end ## # Fails +obj+ is not empty. def assert_not_empty obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to be not empty" } assert_respond_to obj, :empty? assert !obj.empty?, msg end ## # Fails unless exp == act printing the difference between # the two, if possible. # # If there is no visible difference but the assertion fails, you # should suspect that your #== is buggy, or your inspect output is # missing crucial details. # # For floats use assert_in_delta. # # See also: MiniTest::Assertions.diff def assert_equal exp, act, msg = nil msg = message(msg, "") { diff exp, act } assert(exp == act, msg) end ## # Fails exp == act def assert_not_equal exp, act, msg = nil msg = message(msg) { "Expected #{mu_pp(exp)} to be not equal #{mu_pp(act)}" } assert(exp != act, msg) end ## # For comparing Floats. Fails unless +exp+ and +act+ are within +delta+ # of each other. # # assert_in_delta Math::PI, (22.0 / 7.0), 0.01 def assert_in_delta exp, act, delta = 0.001, msg = nil n = (exp - act).abs msg = message(msg) { "Expected #{exp} - #{act} (#{n}) to be < #{delta}" } assert delta >= n, msg end ## # For comparing Floats. Fails unless +exp+ and +act+ have a relative # error less than +epsilon+. def assert_in_epsilon a, b, epsilon = 0.001, msg = nil assert_in_delta a, b, [a, b].min * epsilon, msg end ## # Fails unless +collection+ includes +obj+. def assert_include collection, obj, msg = nil msg = message(msg) { "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}" } assert_respond_to collection, :include? assert collection.include?(obj), msg end ## # Fails +collection+ includes +obj+ def assert_not_include collection, obj, msg = nil msg = message(msg) { "Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}" } assert_respond_to collection, :include? assert !collection.include?(obj), msg end ## # Fails unless +obj+ is an instance of +cls+. def assert_instance_of cls, obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}" } assert obj.instance_of?(cls), msg end ## # Fails unless +obj+ is a kind of +cls+. def assert_kind_of cls, obj, msg = nil # TODO: merge with instance_of msg = message(msg) { "Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" } assert obj.kind_of?(cls), msg end ## # Fails unless +exp+ is =~ +act+. def assert_match exp, act, msg = nil if Object.const_defined?(:Regexp) msg = message(msg) { "Expected #{mu_pp(exp)} to match #{mu_pp(act)}" } assert_respond_to act, :"=~" exp = Regexp.new Regexp.escape exp if String === exp and String === act assert exp =~ act, msg else raise MTest::Skip, "assert_match is not defined, because Regexp is not impl." end end ## # Fails unless +obj+ is nil def assert_nil obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" } assert obj.nil?, msg end ## # For testing equality operators and so-forth. # # assert_operator 5, :<=, 4 def assert_operator o1, op, o2, msg = nil msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" } assert o1.__send__(op, o2), msg end ## # Fails if stdout or stderr do not output the expected results. # Pass in nil if you don't care about that streams output. Pass in # "" if you require it to be silent. # # See also: #assert_silent def assert_output stdout = nil, stderr = nil out, err = capture_io do yield end x = assert_equal stdout, out, "In stdout" if stdout y = assert_equal stderr, err, "In stderr" if stderr (!stdout || x) && (!stderr || y) end ## # Fails unless the block raises one of +exp+ def assert_raise *exp msg = "#{exp.pop}\n" if String === exp.last begin yield rescue MTest::Skip => e return e if exp.include? MTest::Skip raise e rescue Exception => e excepted = exp.any? do |ex| if ex.instance_of?(Module) e.kind_of?(ex) else e.instance_of?(ex) end end assert excepted, exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not") return e end exp = exp.first if exp.size == 1 flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised." end ## # Fails unless +obj+ responds to +meth+. def assert_respond_to obj, meth, msg = nil msg = message(msg, '') { "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}" } assert obj.respond_to?(meth), msg end ## # Fails unless +exp+ and +act+ are #equal? def assert_same exp, act, msg = nil msg = message(msg) { data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id] "Expected %s (oid=%d) to be the same as %s (oid=%d)" % data } assert exp.equal?(act), msg end ## # +send_ary+ is a receiver, message and arguments. # # Fails unless the call returns a true value # TODO: I should prolly remove this from specs def assert_send send_ary, m = nil recv, msg, *args = send_ary m = message(m) { "Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" } assert recv.__send__(msg, *args), m end ## # Fails if the block outputs anything to stderr or stdout. # # See also: #assert_output def assert_silent assert_output "", "" do yield end end ## # Fails unless the block throws +sym+ def assert_throws sym, msg = nil default = "Expected #{mu_pp(sym)} to have been thrown" caught = true catch(sym) do begin yield rescue ArgumentError => e # 1.9 exception default += ", not #{e.message.split(' ').last}" rescue NameError => e # 1.8 exception default += ", not #{e.name.inspect}" end caught = false end assert caught, message(msg) { default } end ## # Returns a proc that will output +msg+ along with the default message. def message msg = nil, ending = ".", &default Proc.new{ custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty? "#{custom_message}#{default.call}#{ending}" } end ## # used for counting assertions def pass msg = nil assert true end ## # Skips the current test. Gets listed at the end of the run but # doesn't cause a failure exit code. # disable backtrace for mruby def skip msg = nil msg ||= "Skipped, no message given" raise MTest::Skip, msg end ## # Returns details for exception +e+ # disable backtrace for mruby def exception_details e, msg [ "#{msg}", "Class: <#{e.class}>", "Message: <#{e.message.inspect}>", # "---Backtrace---", # "#{MiniTest::filter_backtrace(e.backtrace).join("\n")}", # "---------------", ].join "\n" end ## # Fails with +msg+ def flunk msg = nil msg ||= "Epic Fail!" assert false, msg end end class Unit attr_accessor :report, :failures, :errors, :skips attr_accessor :test_count, :assertion_count attr_accessor :start_time attr_accessor :help attr_accessor :verbose attr_writer :options def options @options ||= {} end @@out = $stdout @@runner = nil def self.output @@out end def self.output= stream @@out = stream end def self.runnner= runner @@runner = runnner end def self.runner @@runner = self.new unless @@runner @@runner end def output self.class.output end def puts *a output.puts(*a) end def print *a output.print(*a) end def puke klass, meth, e e = case e when MTest::Skip @skips += 1 "Skipped:\n#{meth}(#{klass}) #{e.inspect}\n" when MTest::Assertion @failures += 1 "Failure:\n#{meth}(#{klass}) #{e.inspect}\n" else @errors += 1 "Error:\n#{meth}(#{klass}): #{e.class}, #{e.inspect}\n" end @report << e e[0, 1] end def initialize @report = [] @errors = @failures = @skips = 0 @verbose = false end def run args = [] self.class.runner._run(args) end def mrbtest suites = TestCase.send "test_suites" return if suites.empty? @test_cound, @assertion_count = 0, 0 results = _run_suites suites @test_count = results.map{ |r| r[0] }.inject(0) { |sum, tc| sum + tc } @assertion_count = results.map{ |r| r[1] }.inject(0) { |sum, ac| sum + ac } $ok_test += (test_count.to_i - failures.to_i - errors.to_i - skips.to_i) $ko_test += failures.to_i $kill_test += errors.to_i report.each_with_index do |msg, i| $asserts << "MTest #{i+1}) #{msg}" end TestCase.reset end def _run args = [] _run_tests @test_count ||= 0 @test_count > 0 ? failures + errors : nil end def _run_tests suites = TestCase.send "test_suites" return if suites.empty? start = Time.now puts puts "# Running tests:" puts @test_count, @assertion_count = 0, 0 results = _run_suites suites @test_count = results.map{ |r| r[0] }.inject(0) { |sum, tc| sum + tc } @assertion_count = results.map{ |r| r[1] }.inject(0) { |sum, ac| sum + ac } t = Time.now - start puts puts puts sprintf("Finished tests in %.6fs, %.4f tests/s, %.4f assertions/s.", t, test_count / t, assertion_count / t) report.each_with_index do |msg, i| puts sprintf("\n%3d) %s", i+1, msg) end puts status end def _run_suites suites suites.map { |suite| _run_suite suite } end def _run_suite suite header = "test_suite_header" puts send(header, suite) if respond_to? header assertions = suite.send("test_methods").map do |method| inst = suite.new method inst._assertions = 0 print "#{suite}##{method} = " if @verbose @start_time = Time.now result = inst.run self time = Time.now - @start_time print sprintf("%.2f s = ", time) if @verbose print result puts if @verbose inst._assertions end return assertions.size, assertions.inject(0) { |sum, n| sum + n } end def status io = self.output format = "%d tests, %d assertions, %d failures, %d errors, %d skips" io.puts sprintf(format, test_count, assertion_count, failures, errors, skips) end class TestCase attr_reader :__name__ @@test_suites = {} def run runner result = "" begin @passed = nil self.setup self.run_setup_hooks self.__send__ self.__name__ result = "." unless io? @passed = true rescue Exception => e @passed = false result = runner.puke self.class, self.__name__, e ensure begin self.run_teardown_hooks self.teardown rescue Exception => e result = runner.puke self.class, self.__name__, e end end result end def initialize name = self.to_s @__name__ = name @__io__ = nil @passed = nil end def io @__io__ = true MTest::Unit.output end def io? @__io__ end def self.reset @@test_suites = {} end reset def self.inherited klass @@test_suites[klass] = true klass.reset_setup_teardown_hooks end def self.test_order :random end def self.test_suites hash = {} @@test_suites.keys.each{ |ts| hash[ts.to_s] = ts } hash.keys.sort.map{ |key| hash[key] } end def self.test_methods # :nodoc: methods = [] self.new.methods(true).each do |m| methods << m.to_s if m.to_s.index('test') == 0 end case self.test_order when :random then max = methods.size # TODO: methods.sort.sort_by { rand max } methods when :alpha, :sorted then methods.sort else raise "Unknown test_order: #{self.test_order.inspect}" end end def passed? @passed end def setup; end def teardown; end def self.reset_setup_teardown_hooks @setup_hooks = [] @teardown_hooks = [] end reset_setup_teardown_hooks def self.add_setup_hook arg=nil, &block hook = arg || block @setup_hooks << hook end def self.setup_hooks # :nodoc: if superclass.respond_to? :setup_hooks then superclass.setup_hooks else [] end + @setup_hooks end def run_setup_hooks # :nodoc: self.class.setup_hooks.each do |hook| if hook.respond_to?(:arity) && hook.arity == 1 hook.call(self) else hook.call end end end def self.add_teardown_hook arg=nil, &block hook = arg || block @teardown_hooks << hook end def self.teardown_hooks # :nodoc: if superclass.respond_to? :teardown_hooks then superclass.teardown_hooks else [] end + @teardown_hooks end def run_teardown_hooks # :nodoc: self.class.teardown_hooks.reverse.each do |hook| if hook.respond_to?(:arity) && hook.arity == 1 hook.call(self) else hook.call end end end include MTest::Assertions end end end if __FILE__ == $0 class Test4MTest < MTest::Unit::TestCase def setup puts '*setup' end def teardown puts '*teardown' end def test_sample puts '*test_sample' assert(true, 'true sample test') assert(true) end end MTest::Unit.new.run end