1# -*- coding: utf-8 -*-
2
3##
4# Minimal Test framework for mruby
5#
6module MTest
7
8
9  ##
10  # Assertion base class
11
12  class Assertion < Exception; end
13
14  ##
15  # Assertion raised when skipping a test
16
17  class Skip < Assertion; end
18
19  module Assertions
20    def mu_pp obj
21      obj.inspect
22    end
23
24    def diff exp, act
25      return "Expected: #{mu_pp exp}\n  Actual: #{mu_pp act}"
26    end
27
28    def _assertions= n
29      @_assertions = n
30    end
31
32    def _assertions
33      @_assertions = 0 unless @_assertions
34      @_assertions
35    end
36
37    ##
38    # Fails unless +test+ is a true value.
39
40    def assert test, msg = nil
41      msg ||= "Failed assertion, no message given."
42      self._assertions += 1
43      unless test
44        msg = msg.call if Proc === msg
45        raise MTest::Assertion, msg
46      end
47      true
48    end
49
50    alias assert_true assert
51
52    ##
53    # Fails unless +test+ is a false value
54    def assert_false test, msg = nil
55      msg = message(msg) { "Expected #{mu_pp(test)} to be false" }
56      assert test == false, msg
57    end
58
59    ##
60    # Fails unless the block returns a true value.
61
62    def assert_block msg = nil
63      msg = message(msg) { "Expected block to return true value" }
64      assert yield, msg
65    end
66
67    ##
68    # Fails unless +obj+ is empty.
69
70    def assert_empty obj, msg = nil
71      msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
72      assert_respond_to obj, :empty?
73      assert obj.empty?, msg
74    end
75
76    ##
77    # Fails +obj+ is not empty.
78
79    def assert_not_empty obj, msg = nil
80      msg = message(msg) { "Expected #{mu_pp(obj)} to be not empty" }
81      assert_respond_to obj, :empty?
82      assert !obj.empty?, msg
83    end
84
85    ##
86    # Fails unless <tt>exp == act</tt> printing the difference between
87    # the two, if possible.
88    #
89    # If there is no visible difference but the assertion fails, you
90    # should suspect that your #== is buggy, or your inspect output is
91    # missing crucial details.
92    #
93    # For floats use assert_in_delta.
94    #
95    # See also: MiniTest::Assertions.diff
96
97    def assert_equal exp, act, msg = nil
98      msg = message(msg, "") { diff exp, act }
99      assert(exp == act, msg)
100    end
101
102    ##
103    # Fails exp == act
104    def assert_not_equal exp, act, msg = nil
105      msg = message(msg) {
106        "Expected #{mu_pp(exp)} to be not equal #{mu_pp(act)}"
107      }
108      assert(exp != act, msg)
109    end
110
111    ##
112    # For comparing Floats.  Fails unless +exp+ and +act+ are within +delta+
113    # of each other.
114    #
115    #   assert_in_delta Math::PI, (22.0 / 7.0), 0.01
116
117    def assert_in_delta exp, act, delta = 0.001, msg = nil
118      n = (exp - act).abs
119      msg = message(msg) { "Expected #{exp} - #{act} (#{n}) to be < #{delta}" }
120      assert delta >= n, msg
121    end
122
123    ##
124    # For comparing Floats.  Fails unless +exp+ and +act+ have a relative
125    # error less than +epsilon+.
126
127    def assert_in_epsilon a, b, epsilon = 0.001, msg = nil
128      assert_in_delta a, b, [a, b].min * epsilon, msg
129    end
130
131    ##
132    # Fails unless +collection+ includes +obj+.
133
134    def assert_include collection, obj, msg = nil
135      msg = message(msg) {
136        "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
137      }
138      assert_respond_to collection, :include?
139      assert collection.include?(obj), msg
140    end
141
142    ##
143    # Fails +collection+ includes +obj+
144    def assert_not_include collection, obj, msg = nil
145      msg = message(msg) {
146        "Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}"
147      }
148      assert_respond_to collection, :include?
149      assert !collection.include?(obj), msg
150    end
151
152    ##
153    # Fails unless +obj+ is an instance of +cls+.
154
155    def assert_instance_of cls, obj, msg = nil
156      msg = message(msg) {
157        "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}"
158      }
159
160      assert obj.instance_of?(cls), msg
161    end
162
163    ##
164    # Fails unless +obj+ is a kind of +cls+.
165
166    def assert_kind_of cls, obj, msg = nil # TODO: merge with instance_of
167      msg = message(msg) {
168        "Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" }
169
170      assert obj.kind_of?(cls), msg
171    end
172
173    ##
174    # Fails unless +exp+ is <tt>=~</tt> +act+.
175
176    def assert_match exp, act, msg = nil
177      if Object.const_defined?(:Regexp)
178        msg = message(msg) { "Expected #{mu_pp(exp)} to match #{mu_pp(act)}" }
179        assert_respond_to act, :"=~"
180        exp = Regexp.new Regexp.escape exp if String === exp and String === act
181        assert exp =~ act, msg
182      else
183        raise MTest::Skip, "assert_match is not defined, because Regexp is not impl."
184      end
185    end
186
187    ##
188    # Fails unless +obj+ is nil
189
190    def assert_nil obj, msg = nil
191      msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
192      assert obj.nil?, msg
193    end
194
195    ##
196    # For testing equality operators and so-forth.
197    #
198    #   assert_operator 5, :<=, 4
199
200    def assert_operator o1, op, o2, msg = nil
201      msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
202      assert o1.__send__(op, o2), msg
203    end
204
205    ##
206    # Fails if stdout or stderr do not output the expected results.
207    # Pass in nil if you don't care about that streams output. Pass in
208    # "" if you require it to be silent.
209    #
210    # See also: #assert_silent
211
212    def assert_output stdout = nil, stderr = nil
213      out, err = capture_io do
214        yield
215      end
216
217      x = assert_equal stdout, out, "In stdout" if stdout
218      y = assert_equal stderr, err, "In stderr" if stderr
219
220      (!stdout || x) && (!stderr || y)
221    end
222
223    ##
224    # Fails unless the block raises one of +exp+
225
226    def assert_raise *exp
227      msg = "#{exp.pop}\n" if String === exp.last
228
229      begin
230        yield
231      rescue MTest::Skip => e
232        return e if exp.include? MTest::Skip
233        raise e
234      rescue Exception => e
235        excepted = exp.any? do |ex|
236          if ex.instance_of?(Module)
237            e.kind_of?(ex)
238          else
239            e.instance_of?(ex)
240          end
241        end
242
243        assert excepted, exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not")
244
245        return e
246      end
247      exp = exp.first if exp.size == 1
248      flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised."
249    end
250
251    ##
252    # Fails unless +obj+ responds to +meth+.
253
254    def assert_respond_to obj, meth, msg = nil
255      msg = message(msg, '') {
256        "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
257      }
258      assert obj.respond_to?(meth), msg
259    end
260
261    ##
262    # Fails unless +exp+ and +act+ are #equal?
263
264    def assert_same exp, act, msg = nil
265      msg = message(msg) {
266        data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
267        "Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
268      }
269      assert exp.equal?(act), msg
270    end
271
272    ##
273    # +send_ary+ is a receiver, message and arguments.
274    #
275    # Fails unless the call returns a true value
276    # TODO: I should prolly remove this from specs
277
278    def assert_send send_ary, m = nil
279      recv, msg, *args = send_ary
280      m = message(m) {
281        "Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" }
282      assert recv.__send__(msg, *args), m
283    end
284
285    ##
286    # Fails if the block outputs anything to stderr or stdout.
287    #
288    # See also: #assert_output
289
290    def assert_silent
291      assert_output "", "" do
292        yield
293      end
294    end
295
296    ##
297    # Fails unless the block throws +sym+
298
299    def assert_throws sym, msg = nil
300      default = "Expected #{mu_pp(sym)} to have been thrown"
301      caught = true
302      catch(sym) do
303        begin
304          yield
305        rescue ArgumentError => e     # 1.9 exception
306          default += ", not #{e.message.split(' ').last}"
307        rescue NameError => e         # 1.8 exception
308          default += ", not #{e.name.inspect}"
309        end
310        caught = false
311      end
312
313      assert caught, message(msg) { default }
314    end
315
316    ##
317    # Returns a proc that will output +msg+ along with the default message.
318
319    def message msg = nil, ending = ".", &default
320      Proc.new{
321        custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
322        "#{custom_message}#{default.call}#{ending}"
323      }
324    end
325
326    ##
327    # used for counting assertions
328
329    def pass msg = nil
330      assert true
331    end
332
333    ##
334    # Skips the current test. Gets listed at the end of the run but
335    # doesn't cause a failure exit code.
336
337    # disable backtrace for mruby
338
339    def skip msg = nil
340      msg ||= "Skipped, no message given"
341      raise MTest::Skip, msg
342    end
343
344    ##
345    # Returns details for exception +e+
346
347    # disable backtrace for mruby
348
349    def exception_details e, msg
350      [
351       "#{msg}",
352       "Class: <#{e.class}>",
353       "Message: <#{e.message.inspect}>",
354#       "---Backtrace---",
355#       "#{MiniTest::filter_backtrace(e.backtrace).join("\n")}",
356#       "---------------",
357      ].join "\n"
358    end
359
360    ##
361    # Fails with +msg+
362
363    def flunk msg = nil
364      msg ||= "Epic Fail!"
365      assert false, msg
366    end
367
368  end
369
370  class Unit
371    attr_accessor :report, :failures, :errors, :skips
372    attr_accessor :test_count, :assertion_count
373    attr_accessor :start_time
374    attr_accessor :help
375    attr_accessor :verbose
376    attr_writer   :options
377
378    def options
379      @options ||= {}
380    end
381
382    @@out = $stdout
383    @@runner = nil
384
385    def self.output
386      @@out
387    end
388
389    def self.output= stream
390      @@out = stream
391    end
392
393    def self.runnner= runner
394      @@runner = runnner
395    end
396
397    def self.runner
398      @@runner = self.new  unless @@runner
399      @@runner
400    end
401
402    def output
403      self.class.output
404    end
405
406    def puts *a
407      output.puts(*a)
408    end
409
410    def print *a
411      output.print(*a)
412    end
413
414    def puke klass, meth, e
415      e = case e
416          when MTest::Skip
417            @skips += 1
418            "Skipped:\n#{meth}(#{klass}) #{e.inspect}\n"
419          when MTest::Assertion
420            @failures += 1
421            "Failure:\n#{meth}(#{klass}) #{e.inspect}\n"
422          else
423            @errors += 1
424            "Error:\n#{meth}(#{klass}): #{e.class}, #{e.inspect}\n"
425          end
426      @report << e
427      e[0, 1]
428    end
429
430    def initialize
431      @report = []
432      @errors = @failures = @skips = 0
433      @verbose = false
434    end
435
436    def run args = []
437      self.class.runner._run(args)
438    end
439
440    def mrbtest
441      suites = TestCase.send "test_suites"
442      return if suites.empty?
443
444      @test_cound, @assertion_count = 0, 0
445
446      results = _run_suites suites
447
448      @test_count      = results.map{ |r| r[0] }.inject(0) { |sum, tc| sum + tc }
449      @assertion_count = results.map{ |r| r[1] }.inject(0) { |sum, ac| sum + ac }
450
451      $ok_test += (test_count.to_i - failures.to_i - errors.to_i - skips.to_i)
452      $ko_test += failures.to_i
453      $kill_test += errors.to_i
454      report.each_with_index do |msg, i|
455        $asserts << "MTest #{i+1}) #{msg}"
456      end
457
458      TestCase.reset
459    end
460
461    def _run args = []
462      _run_tests
463      @test_count ||= 0
464      @test_count > 0 ? failures + errors : nil
465    end
466
467    def _run_tests
468      suites = TestCase.send "test_suites"
469      return if suites.empty?
470
471      start = Time.now
472
473      puts
474      puts "# Running tests:"
475      puts
476
477      @test_count, @assertion_count = 0, 0
478
479      results = _run_suites suites
480
481      @test_count      = results.map{ |r| r[0] }.inject(0) { |sum, tc| sum + tc }
482      @assertion_count = results.map{ |r| r[1] }.inject(0) { |sum, ac| sum + ac }
483
484      t = Time.now - start
485
486      puts
487      puts
488      puts sprintf("Finished tests in %.6fs, %.4f tests/s, %.4f assertions/s.",
489        t, test_count / t, assertion_count / t)
490
491      report.each_with_index do |msg, i|
492        puts sprintf("\n%3d) %s", i+1, msg)
493      end
494
495      puts
496
497      status
498    end
499
500    def _run_suites suites
501      suites.map { |suite| _run_suite suite }
502    end
503
504    def _run_suite suite
505      header = "test_suite_header"
506      puts send(header, suite) if respond_to? header
507
508      assertions = suite.send("test_methods").map do |method|
509        inst = suite.new method
510        inst._assertions = 0
511
512        print "#{suite}##{method} = " if @verbose
513
514        @start_time = Time.now
515        result = inst.run self
516        time = Time.now - @start_time
517
518        print sprintf("%.2f s = ", time) if @verbose
519        print result
520        puts if @verbose
521
522        inst._assertions
523      end
524
525      return assertions.size, assertions.inject(0) { |sum, n| sum + n }
526    end
527
528    def status io = self.output
529      format = "%d tests, %d assertions, %d failures, %d errors, %d skips"
530      io.puts sprintf(format, test_count, assertion_count, failures, errors, skips)
531    end
532
533    class TestCase
534      attr_reader :__name__
535
536      @@test_suites = {}
537
538      def run runner
539        result = ""
540        begin
541          @passed = nil
542          self.setup
543          self.run_setup_hooks
544          self.__send__ self.__name__
545          result = "." unless io?
546          @passed = true
547        rescue Exception => e
548          @passed = false
549          result = runner.puke self.class, self.__name__, e
550        ensure
551          begin
552            self.run_teardown_hooks
553            self.teardown
554          rescue Exception => e
555            result = runner.puke self.class, self.__name__, e
556          end
557        end
558        result
559      end
560
561      def initialize name = self.to_s
562        @__name__ = name
563        @__io__ = nil
564        @passed = nil
565      end
566
567      def io
568        @__io__ = true
569        MTest::Unit.output
570      end
571
572      def io?
573        @__io__
574      end
575
576      def self.reset
577        @@test_suites = {}
578      end
579
580      reset
581
582      def self.inherited klass
583        @@test_suites[klass] = true
584        klass.reset_setup_teardown_hooks
585      end
586
587      def self.test_order
588        :random
589      end
590
591      def self.test_suites
592        hash = {}
593        @@test_suites.keys.each{ |ts| hash[ts.to_s] = ts }
594        hash.keys.sort.map{ |key| hash[key] }
595      end
596
597      def self.test_methods # :nodoc:
598        methods = []
599        self.new.methods(true).each do |m|
600          methods << m.to_s  if m.to_s.index('test') == 0
601        end
602
603        case self.test_order
604        when :random then
605          max = methods.size
606          # TODO: methods.sort.sort_by { rand max }
607          methods
608        when :alpha, :sorted then
609          methods.sort
610        else
611          raise "Unknown test_order: #{self.test_order.inspect}"
612        end
613      end
614
615
616      def passed?
617        @passed
618      end
619
620      def setup; end
621      def teardown; end
622      def self.reset_setup_teardown_hooks
623        @setup_hooks = []
624        @teardown_hooks = []
625      end
626      reset_setup_teardown_hooks
627
628      def self.add_setup_hook arg=nil, &block
629        hook = arg || block
630        @setup_hooks << hook
631      end
632
633      def self.setup_hooks # :nodoc:
634        if superclass.respond_to? :setup_hooks then
635          superclass.setup_hooks
636        else
637          []
638        end + @setup_hooks
639      end
640
641      def run_setup_hooks # :nodoc:
642        self.class.setup_hooks.each do |hook|
643          if hook.respond_to?(:arity) && hook.arity == 1
644            hook.call(self)
645          else
646            hook.call
647          end
648        end
649      end
650
651      def self.add_teardown_hook arg=nil, &block
652        hook = arg || block
653        @teardown_hooks << hook
654      end
655
656      def self.teardown_hooks # :nodoc:
657        if superclass.respond_to? :teardown_hooks then
658          superclass.teardown_hooks
659        else
660          []
661        end + @teardown_hooks
662      end
663
664      def run_teardown_hooks # :nodoc:
665        self.class.teardown_hooks.reverse.each do |hook|
666          if hook.respond_to?(:arity) && hook.arity == 1
667            hook.call(self)
668          else
669            hook.call
670          end
671        end
672      end
673
674
675      include MTest::Assertions
676    end
677  end
678end
679
680if __FILE__ == $0
681  class Test4MTest < MTest::Unit::TestCase
682    def setup
683      puts '*setup'
684    end
685
686    def teardown
687      puts '*teardown'
688    end
689
690    def test_sample
691      puts '*test_sample'
692      assert(true, 'true sample test')
693      assert(true)
694    end
695  end
696
697  MTest::Unit.new.run
698end
699