oktest 1.0.2 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/oktest.rb CHANGED
@@ -1,16 +1,18 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  ###
4
- ### $Release: 1.0.2 $
4
+ ### $Release: 1.2.1 $
5
5
  ### $Copyright: copyright(c) 2011-2021 kuwata-lab.com all rights reserved $
6
6
  ### $License: MIT License $
7
7
  ###
8
8
 
9
+ require 'set'
10
+
9
11
 
10
12
  module Oktest
11
13
 
12
14
 
13
- VERSION = '$Release: 1.0.2 $'.split()[1]
15
+ VERSION = '$Release: 1.2.1 $'.split()[1]
14
16
 
15
17
 
16
18
  class OktestError < StandardError
@@ -114,6 +116,10 @@ module Oktest
114
116
 
115
117
  def ===(expected)
116
118
  __done()
119
+ #; [!mjh4d] raises error when combination of 'not_ok()' and matcher object.
120
+ if @bool == false && @actual.is_a?(Matcher)
121
+ raise OktestError, "negative `===` is not available with matcher object."
122
+ end
117
123
  #; [!42f6a] raises assertion error when failed.
118
124
  #; [!vhvyu] is avaialbe with NOT.
119
125
  __assert(@bool == (@actual === expected)) {
@@ -264,10 +270,10 @@ module Oktest
264
270
 
265
271
  def raise!(errcls=nil, errmsg=nil, &b)
266
272
  #; [!8k6ee] compares error class by '.is_a?' instead of '=='.
267
- return raise?(errcls, errmsg, subclass: true, &b)
273
+ return raise?(errcls, errmsg, _subclass: true, &b)
268
274
  end
269
275
 
270
- def raise?(errcls=nil, errmsg=nil, subclass: false, &b)
276
+ def raise?(errcls=nil, errmsg=nil, _subclass: false, &b)
271
277
  __done()
272
278
  #; [!2rnni] 1st argument can be error message string or rexp.
273
279
  if errmsg.nil? && ! errcls.nil? && ! (errcls.is_a?(Class) && errcls <= Exception)
@@ -289,8 +295,8 @@ module Oktest
289
295
  #; [!lq6jv] compares error class with '==' operator, not '.is_a?'.
290
296
  elsif exc.class == errcls # not `exc.is_a?(errcls)`
291
297
  nil
292
- #; [!hwg0z] compares error class with '.is_a?' if 'subclass: true' specified.
293
- elsif subclass && exc.class < errcls
298
+ #; [!hwg0z] compares error class with '.is_a?' if '_subclass: true' specified.
299
+ elsif _subclass && exc.class < errcls
294
300
  nil
295
301
  #; [!4n3ed] reraises if exception is not matched to specified error class.
296
302
  else
@@ -328,8 +334,8 @@ module Oktest
328
334
  #; [!smprc] compares error class with '==' operator, not '.is_a?'.
329
335
  elsif exc.class == errcls # not `exc.is_a?(errcls)`
330
336
  __assert(false) { "#{errcls.inspect} should not be raised but got #{exc.inspect}." }
331
- #; [!34nd8] compares error class with '.is_a?' if 'subclass: true' specified.
332
- elsif subclass && exc.class < errcls
337
+ #; [!34nd8] compares error class with '.is_a?' if '_subclass: true' specified.
338
+ elsif _subclass && exc.class < errcls
333
339
  __assert(false) { "#{errcls.inspect} should not be raised but got #{exc.inspect}." }
334
340
  #; [!shxne] reraises exception if different from specified error class.
335
341
  else
@@ -548,6 +554,261 @@ module Oktest
548
554
  end
549
555
 
550
556
 
557
+ class Matcher
558
+
559
+ def initialize(actual)
560
+ @actual = actual
561
+ end
562
+
563
+ def ===(expected)
564
+ #; [!spybn] raises NotImplementedError.
565
+ raise NotImplementedError.new("#{self.class.name}#===(): not implemented yet.")
566
+ end
567
+
568
+ def ==(expected)
569
+ #; [!ymt1b] raises OktestError.
570
+ raise OktestError, "JSON(): use `===` instead of `==`."
571
+ end
572
+
573
+ def fail(errmsg)
574
+ #; [!8qpsd] raises assertion error.
575
+ raise Oktest::FAIL_EXCEPTION, errmsg
576
+ end
577
+
578
+ end
579
+
580
+
581
+ class JsonMatcher < Matcher
582
+
583
+ def ===(expected)
584
+ #; [!4uf1o] raises assertion error when JSON not matched.
585
+ _compare([], @actual, expected)
586
+ #; [!0g0u4] returns true when JSON matched.
587
+ return true
588
+ end
589
+
590
+ private
591
+
592
+ def _compare?(path, a, e)
593
+ #; [!nkvqo] returns true when nothing raised.
594
+ #; [!57m2j] returns false when assertion error raised.
595
+ _compare(path, a, e)
596
+ return true
597
+ rescue FAIL_EXCEPTION
598
+ return false
599
+ end
600
+
601
+ def _compare(path, a, e)
602
+ if a.is_a?(Hash) && e.is_a?(Hash)
603
+ _compare_hash(path, a, e)
604
+ elsif a.is_a?(Array) && e.is_a?(Array)
605
+ _compare_array(path, a, e)
606
+ elsif e.is_a?(Enumerator)
607
+ _compare_enumerator(path, a, e)
608
+ elsif e.is_a?(OR)
609
+ _compare_or(path, a, e)
610
+ elsif e.is_a?(AND)
611
+ _compare_and(path, a, e)
612
+ else
613
+ _compare_value(path, a, e)
614
+ end
615
+ end
616
+
617
+ def _compare_value(path, a, e)
618
+ #; [!1ukbv] scalar value matches to integer, string, bool, and so son.
619
+ #; [!8o55d] class object matches to instance object.
620
+ #; [!s625d] regexp object matches to string value.
621
+ #; [!aqkk0] range object matches to scalar value.
622
+ #; [!a7bfs] Set object matches to enum value.
623
+ e === a or fail <<"END"
624
+ $<JSON>#{_path(path)}: $<expected> === $<actual> : failed.
625
+ $<actual>: #{a.inspect}
626
+ $<expected>: #{e.inspect}
627
+ END
628
+ #; [!4ymj2] fails when actual value is not matched to item class of range object.
629
+ if e.is_a?(Range)
630
+ expected_class = (e.begin || e.end).class
631
+ a.is_a?(expected_class) or fail <<"END"
632
+ $<JSON>#{_path(path)}: expected #{expected_class.name} value, but got #{a.class.name} value.
633
+ $<actual>: #{a.inspect}
634
+ $<expected>: #{e.inspect}
635
+ END
636
+ end
637
+ end
638
+
639
+ def _compare_array(path, a, e)
640
+ #; [!bz74w] fails when array lengths are different.
641
+ a.length == e.length or fail <<"END"
642
+ $<JSON>#{_path(path)}: $<actual>.length == $<expected>.length : failed.
643
+ $<actual>.length: #{a.length}
644
+ $<expected>.length: #{e.length}
645
+ $<actual>: #{a.inspect}
646
+ $<expected>: #{e.inspect}
647
+ END
648
+ #; [!lh6d6] compares array items recursively.
649
+ path.push(nil)
650
+ i = -1
651
+ a.zip(e) do |a2, e2|
652
+ path[-1] = (i += 1)
653
+ _compare(path, a2, e2)
654
+ end
655
+ path.pop()
656
+ end
657
+
658
+ def _compare_hash(path, a, e)
659
+ #; [!rkv0z] compares two hashes with converting keys into string.
660
+ a2 = {}; a.each {|k, v| a2[k.to_s] = v }
661
+ e2 = {}; e.each {|k, v| e2[k.to_s] = v }
662
+ #; [!fmxyg] compares hash objects recursively.
663
+ path.push(nil)
664
+ a2.each_key do |k|
665
+ path[-1] = k
666
+ if e2.key?(k)
667
+ _compare(path, a2[k], e2[k])
668
+ #; [!jbyv6] key 'aaa?' represents optional key.
669
+ elsif e2.key?("#{k}?")
670
+ _compare(path, a2[k], e2["#{k}?"]) unless a2[k].nil?
671
+ #; [!uc4ag] key '*' matches to any key name.
672
+ elsif e2.key?("*")
673
+ _compare(path, a2[k], e2["*"])
674
+ #; [!mpbvu] fails when unexpected key exists in actual hash.
675
+ else
676
+ fail <<"END"
677
+ $<JSON>#{_path(path)}: unexpected key.
678
+ $<actual>: #{a2[k].inspect}
679
+ END
680
+ end
681
+ end
682
+ path.pop()
683
+ #; [!4oasq] fails when expected key not exist in actual hash.
684
+ (e2.keys - a2.keys).each do |k|
685
+ k =~ /\?\z/ || k == "*" or fail <<"END"
686
+ $<JSON>#{_path(path)}: key \"#{k}\" expected but not found.
687
+ $<actual>.keys: #{a2.keys.sort.inspect[1...-1]}
688
+ $<expected>.keys: #{e2.keys.sort.inspect[1...-1]}
689
+ END
690
+ end
691
+ end
692
+
693
+ def _compare_enumerator(path, a, e)
694
+ #; [!ljrmc] fails when expected is an Enumerator object and actual is not an array.
695
+ e2 = e.first
696
+ a.is_a?(Array) or fail <<"END"
697
+ $<JSON>#{_path(path)}: Array value expected but got #{a.class.name} value.
698
+ $<actual>: #{a.inspect}
699
+ $<expected>: [#{e2.inspect}].each
700
+ END
701
+ #; [!sh5cg] Enumerator object matches to repeat of rule.
702
+ path.push(nil)
703
+ a.each_with_index do |a2, i|
704
+ path[-1] = i
705
+ _compare(path, a2, e2)
706
+ end
707
+ path.pop()
708
+ end
709
+
710
+ def _compare_or(path, a, e)
711
+ #; [!eqr3b] `OR()` matches to any of arguments.
712
+ #; [!5ybfg] `OR()` can contain `AND()`.
713
+ passed = e.items.any? {|e2| _compare?(path, a, e2) }
714
+ passed or fail <<"END"
715
+ $<JSON>#{_path(path)}: $<expected> === $<actual> : failed.
716
+ $<actual>: #{a.inspect}
717
+ $<expected>: OR(#{e.items.collect(&:inspect).join(', ')})
718
+ END
719
+ end
720
+
721
+ def _compare_and(path, a, e)
722
+ #; [!4hk96] `AND()` matches to all of arguments.
723
+ #; [!scx22] `AND()` can contain `OR()`.
724
+ failed = e.items.find {|e2| ! _compare?(path, a, e2) }
725
+ ! failed or fail <<"END"
726
+ $<JSON>#{_path(path)}: $<expected> === $<actual> : failed.
727
+ $<actual>: #{a.inspect}
728
+ $<expected>: AND(#{failed.inspect})
729
+ END
730
+ end
731
+
732
+ def _path(path)
733
+ #return path.collect {|x| "/#{x}" }.join()
734
+ return path.collect {|x| "[#{x.inspect}]" }.join()
735
+ end
736
+
737
+ protected
738
+
739
+ class OR
740
+ def initialize(*items)
741
+ @items = items
742
+ end
743
+ attr_reader :items
744
+ def inspect()
745
+ #; [!2mu33] returns 'OR(...)' string.
746
+ return "OR(#{@items.collect(&:inspect).join(', ')})"
747
+ end
748
+ end
749
+
750
+ class AND
751
+ def initialize(*items)
752
+ @items = items
753
+ end
754
+ attr_reader :items
755
+ def inspect()
756
+ #; [!w43ag] returns 'AND(...)' string.
757
+ return "AND(#{@items.collect(&:inspect).join(', ')})"
758
+ end
759
+ end
760
+
761
+ class Enum < Set
762
+ alias === include? # Ruby 2.4 or older doesn't have 'Set#==='.
763
+ def inspect()
764
+ #; [!fam11] returns 'Enum(...)' string.
765
+ return "Enum(#{self.collect(&:inspect).join(', ')})"
766
+ end
767
+ end
768
+
769
+ class Length
770
+ def initialize(expected)
771
+ @expected = expected
772
+ end
773
+ def ===(actual)
774
+ #; [!03ozi] compares length of actual value with expected value.
775
+ return @expected === actual.length
776
+ end
777
+ def inspect()
778
+ #; [!nwv3e] returns 'Length(n)' string.
779
+ return "Length(#{@expected.inspect})"
780
+ end
781
+ end
782
+
783
+ class Any
784
+ def ===(actual)
785
+ #; [!mzion] returns true in any case.
786
+ true
787
+ end
788
+ def inspect()
789
+ #; [!6f0yv] returns 'Any()' string.
790
+ return "Any()"
791
+ end
792
+ end
793
+
794
+ end
795
+
796
+
797
+ module UtilHelper
798
+
799
+ def partial_regexp(pattern, begin_='\A', end_='\z', mark="{== ==}")
800
+ #; [!9drtn] is available in both topic and spec blocks.
801
+ return Util.partial_regexp(pattern, begin_, end_, mark)
802
+ end
803
+
804
+ def partial_regexp!(pattern, begin_='\A', end_='\z', mark="{== ==}")
805
+ #; [!wo4hp] is available in both topic and spec blocks.
806
+ return Util.partial_regexp!(pattern, begin_, end_, mark)
807
+ end
808
+
809
+ end
810
+
811
+
551
812
  class Context
552
813
  ## * Context class is separated from ScopeNode, TopicNode, and SpecLeaf.
553
814
  ## * `topic()` and `spec()` creates subclass of Context class,
@@ -555,6 +816,8 @@ module Oktest
555
816
  ## * `scope()` instanciates those subclasses, and run spec blocks
556
817
  ## in that instance objects.
557
818
 
819
+ extend UtilHelper
820
+
558
821
  class << self
559
822
  attr_accessor :__node
560
823
  end
@@ -587,26 +850,29 @@ module Oktest
587
850
  return to
588
851
  end
589
852
 
590
- def self.spec(desc, tag: nil, &block)
853
+ def self.spec(desc, tag: nil, fixture: nil, &block)
591
854
  node = @__node
592
855
  node.is_a?(Node) or raise "internal error: node=#{node.inspect}" # for debug
856
+ #; [!4vkbl] error when `fixture:` keyword arg is not a Hash object.
857
+ fixture.nil? || fixture.is_a?(Hash) or
858
+ raise ArgumentError, "spec(fixture: #{fixture.inspect}): fixture argument should be a Hash object, but got #{fixture.class.name} object."
593
859
  #; [!ala78] provides raising TodoException block if block not given.
594
860
  block ||= proc { raise TodoException, "not implemented yet" }
595
861
  #; [!x48db] keeps called location only when block has parameters.
596
862
  if block.parameters.empty?
597
863
  location = nil
598
864
  else
599
- location = caller(1).first # caller() makes performance slower, but necessary.
865
+ location = caller_locations(1, 1).first
600
866
  end
601
867
  #; [!c8c8o] creates new spec object.
602
- spec = SpecLeaf.new(node, desc, tag: tag, location: location, &block)
868
+ spec = SpecLeaf.new(node, desc, tag: tag, fixture: fixture, location: location, &block)
603
869
  return spec
604
870
  end
605
871
 
606
872
  def self.fixture(name, &block)
607
873
  #; [!8wfrq] registers fixture factory block.
608
874
  #; [!y3ks3] retrieves block parameter names.
609
- location = caller(1).first # caller() makes performance slower, but necessary.
875
+ location = caller_locations(1, 1).first
610
876
  @__node.register_fixture_block(name, location, &block)
611
877
  self
612
878
  end
@@ -730,6 +996,7 @@ module Oktest
730
996
  context = @context_class.new()
731
997
  #; [!9hbxn] context object has 'ok()' method.
732
998
  context.extend SpecHelper
999
+ context.extend UtilHelper
733
1000
  return context
734
1001
  end
735
1002
 
@@ -758,7 +1025,7 @@ module Oktest
758
1025
  return @hooks[key]
759
1026
  end
760
1027
 
761
- def _repr(depth=0, buf="")
1028
+ def _repr(depth=0, buf=String.new)
762
1029
  #; [!bt5j8] builds debug string.
763
1030
  if depth < 0
764
1031
  id_str = "%x" % self.object_id
@@ -810,7 +1077,7 @@ module Oktest
810
1077
  end
811
1078
 
812
1079
  attr_reader :target
813
- attr_accessor :_prefix
1080
+ attr_writer :_prefix
814
1081
 
815
1082
  def _prefix
816
1083
  @_prefix || '*'
@@ -833,16 +1100,17 @@ module Oktest
833
1100
 
834
1101
  class SpecLeaf < Item
835
1102
 
836
- def initialize(parent, desc, tag: nil, location: nil, &block)
1103
+ def initialize(parent, desc, tag: nil, fixture: nil, location: nil, &block)
837
1104
  #@parent = parent # not keep parent node to avoid recursive reference
838
1105
  @desc = desc
839
1106
  @tag = tag
1107
+ @fixture = fixture
840
1108
  @location = location # necessary when raising fixture not found error
841
1109
  @block = block
842
1110
  parent.add_child(self) if parent
843
1111
  end
844
1112
 
845
- attr_reader :desc, :tag, :location, :block
1113
+ attr_reader :desc, :tag, :fixture, :location, :block
846
1114
 
847
1115
  def _prefix
848
1116
  '-'
@@ -863,7 +1131,7 @@ module Oktest
863
1131
  nil
864
1132
  end
865
1133
 
866
- def _repr(depth=0, buf="") #:nodoc:
1134
+ def _repr(depth=0, buf=String.new) #:nodoc:
867
1135
  #; [!6nsgy] builds debug string.
868
1136
  buf << " " * depth << "- #{@desc}"
869
1137
  buf << " (tag: #{@tag.inspect})" if @tag
@@ -892,8 +1160,8 @@ module Oktest
892
1160
 
893
1161
  def self.scope(tag: nil, &block)
894
1162
  #; [!kem4y] detects test script filename.
895
- location = caller(1).first # caller() makes performance slower, but necessary.
896
- filename = location =~ /:\d+/ ? $` : nil
1163
+ location = caller_locations(1, 1).first
1164
+ filename = location.path.to_s
897
1165
  #; [!6ullm] changes test script filename from absolute path to relative path.
898
1166
  if filename
899
1167
  pwd = Dir.pwd()
@@ -929,7 +1197,7 @@ module Oktest
929
1197
  #; [!bc3l2] records invoked location.
930
1198
  #; [!mqtdy] not record invoked location when `Config.ok_location == false`.
931
1199
  if Config.ok_location
932
- location = caller(1).first # caller() makes performance slower, but necessary.
1200
+ location = caller_locations(1, 1).first
933
1201
  else
934
1202
  location = nil
935
1203
  end
@@ -944,7 +1212,7 @@ module Oktest
944
1212
  #; [!agmx8] records invoked location.
945
1213
  #; [!a9508] not record invoked location when `Config.ok_location == false`.
946
1214
  if Config.ok_location
947
- location = caller(1).first # caller() makes performance slower, but necessary.
1215
+ location = caller_locations(1, 1).first
948
1216
  else
949
1217
  location = nil
950
1218
  end
@@ -961,7 +1229,7 @@ module Oktest
961
1229
  raise SkipException, reason if condition
962
1230
  end
963
1231
 
964
- def fixture(name, *args)
1232
+ def fixture(name, *args, **kwargs)
965
1233
  #; [!zgfg9] finds fixture block in current or parent node.
966
1234
  node = self.class.__node
967
1235
  while node && (tuple = node.get_fixture_block(name)) == nil
@@ -970,17 +1238,17 @@ module Oktest
970
1238
  #; [!wxcsp] raises error when fixture not found.
971
1239
  unless tuple
972
1240
  exc = FixtureNotFoundError.new("`#{name.inspect}`: fixture not found.")
973
- exc.set_backtrace([caller(1).first])
1241
+ exc.set_backtrace([caller_locations(1, 1).first.to_s])
974
1242
  raise exc
975
1243
  end
976
1244
  #; [!m4ava] calls fixture block and returns result of it.
977
1245
  #; [!l2mcx] accepts block arguments.
978
1246
  block, _, _ = tuple
979
- return block.call(*args)
1247
+ return block.call(*args, **kwargs)
980
1248
  end
981
1249
 
982
1250
  def TODO()
983
- location = caller(1).first # ex: "foo_test.rb:123:in ...."
1251
+ location = caller_locations(1, 1).first # ex: "foo_test.rb:123:in ...."
984
1252
  @__TODO = location
985
1253
  end
986
1254
 
@@ -1116,6 +1384,41 @@ module Oktest
1116
1384
  return Benry::Recorder.new
1117
1385
  end
1118
1386
 
1387
+ def JSON(actual)
1388
+ #; [!n0k03] creates JsonMatcher object.
1389
+ return JsonMatcher.new(actual)
1390
+ end
1391
+
1392
+ def Enum(*values)
1393
+ #; [!fbfr0] creates Enum object which is a subclass of Set.
1394
+ return JsonMatcher::Enum.new(values)
1395
+ end
1396
+
1397
+ def Bool()
1398
+ #; [!vub5j] creates a set of true and false.
1399
+ return Enum(true, false)
1400
+ end
1401
+
1402
+ def OR(*args)
1403
+ #; [!9e8im] creates `OR` object.
1404
+ return JsonMatcher::OR.new(*args)
1405
+ end
1406
+
1407
+ def AND(*args)
1408
+ #; [!38jln] creates `AND` object.
1409
+ return JsonMatcher::AND.new(*args)
1410
+ end
1411
+
1412
+ def Length(n)
1413
+ #; [!qqas3] creates Length object.
1414
+ return JsonMatcher::Length.new(n)
1415
+ end
1416
+
1417
+ def Any()
1418
+ #; [!dlo1o] creates an 'Any' object.
1419
+ return JsonMatcher::Any.new
1420
+ end
1421
+
1119
1422
  end
1120
1423
 
1121
1424
 
@@ -1215,11 +1518,25 @@ module Oktest
1215
1518
  @reporter.exit_all(self)
1216
1519
  end
1217
1520
 
1521
+ def _spec_first(node)
1522
+ a1, a2 = node.each_child.partition {|c|
1523
+ c.is_a?(SpecLeaf) || (c.is_a?(TopicNode) && c._prefix != '*')
1524
+ }
1525
+ return a1+a2
1526
+ end
1527
+ private :_spec_first
1528
+
1218
1529
  def visit_scope(scope, depth, parent)
1219
1530
  @reporter.enter_scope(scope) unless scope.equal?(THE_GLOBAL_SCOPE)
1220
1531
  #; [!5anr7] calls before_all and after_all blocks.
1221
1532
  call_before_all_block(scope)
1222
- scope.each_child {|c| c.accept_visitor(self, depth+1, scope) }
1533
+ #; [!c5cw0] run specs and case_when in advance of specs and topics when SimpleReporter.
1534
+ if @reporter.order_policy() == :spec_first
1535
+ _spec_first(scope).each {|c| c.accept_visitor(self, depth+1, scope) }
1536
+ else
1537
+ scope.each_child {|c| c.accept_visitor(self, depth+1, scope) }
1538
+ end
1539
+ #
1223
1540
  call_after_all_block(scope)
1224
1541
  @reporter.exit_scope(scope) unless scope.equal?(THE_GLOBAL_SCOPE)
1225
1542
  end
@@ -1228,7 +1545,13 @@ module Oktest
1228
1545
  @reporter.enter_topic(topic, depth)
1229
1546
  #; [!i3yfv] calls 'before_all' and 'after_all' blocks.
1230
1547
  call_before_all_block(topic)
1231
- topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1548
+ #; [!p3a5o] run specs and case_when in advance of specs and topics when SimpleReporter.
1549
+ if @reporter.order_policy() == :spec_first
1550
+ _spec_first(topic).each {|c| c.accept_visitor(self, depth+1, topic) }
1551
+ else
1552
+ topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1553
+ end
1554
+ #
1232
1555
  call_after_all_block(topic)
1233
1556
  @reporter.exit_topic(topic, depth)
1234
1557
  end
@@ -1246,7 +1569,8 @@ module Oktest
1246
1569
  begin
1247
1570
  params = Util.required_param_names_of_block(spec.block)
1248
1571
  values = params.nil? || params.empty? ? [] \
1249
- : get_fixture_values(params, node, spec, context)
1572
+ : get_fixture_values(params, node, spec, context, spec.location,
1573
+ spec.fixture ? spec.fixture.dup : {})
1250
1574
  spec.run_block_in_context_object(context, *values)
1251
1575
  rescue NoMemoryError => exc; raise exc
1252
1576
  rescue SignalException => exc; raise exc
@@ -1271,7 +1595,7 @@ module Oktest
1271
1595
  exc = TODO_EXCEPTION.new("#{exc.class} raised because not implemented yet")
1272
1596
  end
1273
1597
  location = context.__TODO
1274
- exc.set_backtrace([location])
1598
+ exc.set_backtrace([location.to_s])
1275
1599
  end
1276
1600
  #; [!dihkr] calls 'at_end' blocks, even when exception raised.
1277
1601
  begin
@@ -1285,8 +1609,8 @@ module Oktest
1285
1609
 
1286
1610
  private
1287
1611
 
1288
- def get_fixture_values(names, node, spec, context)
1289
- return THE_FIXTURE_MANAGER.get_fixture_values(names, node, spec, context)
1612
+ def get_fixture_values(names, node, spec, context, location=nil, resolved=nil)
1613
+ return THE_FIXTURE_MANAGER.get_fixture_values(names, node, spec, context, location, resolved)
1290
1614
  end
1291
1615
 
1292
1616
  def _call_blocks_parent_first(node, name, context)
@@ -1342,51 +1666,53 @@ module Oktest
1342
1666
 
1343
1667
  class FixtureManager
1344
1668
 
1345
- def get_fixture_values(names, node, spec, context, location=nil, _resolved={}, _resolving=[])
1669
+ def get_fixture_values(names, node, spec, context, location, resolved={}, _resolving=[])
1346
1670
  #; [!w6ffs] resolves 'this_topic' fixture name as target objec of current topic.
1347
- _resolved[:this_topic] = node.target if !_resolved.key?(:this_topic) && node.topic?
1671
+ resolved[:this_topic] = node.target if !resolved.key?(:this_topic) && node.topic?
1348
1672
  #; [!ja2ew] resolves 'this_spec' fixture name as description of current spec.
1349
- _resolved[:this_spec] = spec.desc if !_resolved.key?(:this_spec)
1673
+ resolved[:this_spec] = spec.desc if !resolved.key?(:this_spec)
1350
1674
  #; [!v587k] resolves fixtures.
1351
- location ||= spec.location
1352
1675
  return names.collect {|name|
1353
1676
  #; [!np4p9] raises error when loop exists in dependency.
1354
1677
  ! _resolving.include?(name) or
1355
1678
  raise _looped_dependency_error(name, _resolving, location)
1356
- get_fixture_value(name, node, spec, context, location, _resolved, _resolving)
1679
+ get_fixture_value(name, node, spec, context, location, resolved, _resolving)
1357
1680
  }
1358
1681
  end
1359
1682
 
1360
- def get_fixture_value(name, node, spec, context, location=nil, _resolved={}, _resolving=[])
1361
- return _resolved[name] if _resolved.key?(name)
1362
- location ||= spec.location
1683
+ def get_fixture_value(name, node, spec, context, location, resolved={}, _resolving=[])
1684
+ return resolved[name] if resolved.key?(name)
1363
1685
  tuple = node.get_fixture_block(name)
1364
1686
  if tuple
1365
1687
  block, param_names, location = tuple
1366
1688
  #; [!2esaf] resolves fixture dependencies.
1367
1689
  if param_names
1368
1690
  _resolving << name
1369
- args = get_fixture_values(param_names, node, spec, context, location, _resolved, _resolving)
1691
+ args = get_fixture_values(param_names, node, spec, context, location, resolved, _resolving)
1370
1692
  (popped = _resolving.pop) == name or
1371
1693
  raise "** assertion failed: name=#{name.inspect}, resolvng[-1]=#{popped.inspect}"
1372
- #; [!4xghy] calls fixture block with context object as self.
1373
- val = context.instance_exec(*args, &block)
1374
1694
  else
1375
- val = context.instance_eval(&block)
1695
+ args = []
1376
1696
  end
1697
+ #; [!gyyst] overwrites keyword params by fixture values.
1698
+ kwnames = Util.keyword_param_names_of_block(block)
1699
+ kwargs = {}
1700
+ kwnames.each {|name| kwargs[name] = resolved[name] if resolved.key?(name) }
1701
+ #; [!4xghy] calls fixture block with context object as self.
1702
+ val = context.instance_exec(*args, **kwargs, &block)
1377
1703
  #; [!8t3ul] caches fixture value to call fixture block only once per spec.
1378
- _resolved[name] = val
1704
+ resolved[name] = val
1379
1705
  return val
1380
1706
  elsif node.parent
1381
1707
  #; [!4chb9] traverses parent topics if fixture not found in current topic.
1382
- return get_fixture_value(name, node.parent, spec, context, location, _resolved, _resolving)
1708
+ return get_fixture_value(name, node.parent, spec, context, location, resolved, _resolving)
1383
1709
  elsif ! node.equal?(THE_GLOBAL_SCOPE)
1384
1710
  #; [!wt3qk] suports global scope.
1385
- return get_fixture_value(name, THE_GLOBAL_SCOPE, spec, context, location, _resolved, _resolving)
1711
+ return get_fixture_value(name, THE_GLOBAL_SCOPE, spec, context, location, resolved, _resolving)
1386
1712
  else
1387
1713
  #; [!nr79z] raises error when fixture not found.
1388
1714
  exc = FixtureNotFoundError.new("#{name}: fixture not found. (spec: #{spec.desc})")
1389
- exc.set_backtrace([location]) if location
1715
+ exc.set_backtrace([location.to_s]) if location
1390
1716
  raise exc
1391
1717
  end
1392
1718
  end
@@ -1401,7 +1727,7 @@ module Oktest
1401
1727
  loop = s1.empty? ? s2 : "#{s1}->#{s2}"
1402
1728
  #location = $1 if location =~ /(.*:\d+)/
1403
1729
  exc = LoopedDependencyError.new("fixture dependency is looped: #{loop}")
1404
- exc.set_backtrace([location])
1730
+ exc.set_backtrace([location.to_s])
1405
1731
  return exc
1406
1732
  end
1407
1733
 
@@ -1429,6 +1755,7 @@ module Oktest
1429
1755
  def exit_spec(spec, depth, status, error, parent); end
1430
1756
  #
1431
1757
  def counts; {}; end
1758
+ def order_policy(); nil; end # :spec_first or nil
1432
1759
 
1433
1760
  end
1434
1761
 
@@ -1439,7 +1766,7 @@ module Oktest
1439
1766
  CHARS = { :PASS=>'.', :FAIL=>'f', :ERROR=>'E', :SKIP=>'s', :TODO=>'t' }
1440
1767
 
1441
1768
 
1442
- def initialize
1769
+ def initialize()
1443
1770
  @exceptions = []
1444
1771
  @counts = {}
1445
1772
  end
@@ -1544,7 +1871,7 @@ module Oktest
1544
1871
  end
1545
1872
  lines = []
1546
1873
  msg.each_line {|line| lines << line }
1547
- puts lines.shift.chomp
1874
+ puts Color.errmsg(lines.shift.chomp)
1548
1875
  puts lines.join.chomp unless lines.empty?
1549
1876
  puts exc.diff if exc.respond_to?(:diff) && exc.diff # for oktest.rb
1550
1877
  end
@@ -1582,6 +1909,16 @@ module Oktest
1582
1909
 
1583
1910
  LABELS = { :PASS=>'pass', :FAIL=>'Fail', :ERROR=>'ERROR', :SKIP=>'Skip', :TODO=>'TODO' }
1584
1911
 
1912
+ def enter_scope(scope)
1913
+ puts "## #{scope.filename}"
1914
+ end
1915
+
1916
+ def exit_scope(scope)
1917
+ #; [!ibdu7] reports errors even when no topics.
1918
+ super
1919
+ print_exceptions()
1920
+ end
1921
+
1585
1922
  def enter_topic(topic, depth)
1586
1923
  super
1587
1924
  puts "#{' ' * (depth - 1)}#{topic._prefix} #{Color.topic(topic.target)}"
@@ -1606,15 +1943,73 @@ module Oktest
1606
1943
  $stdout.flush
1607
1944
  end
1608
1945
  label = Color.status(status, LABELS[status] || '???')
1609
- msg = "#{' ' * (depth - 1)}- [#{label}] #{spec.desc}"
1946
+ msg = ["#{' ' * (depth - 1)}- [#{label}] #{spec.desc}"]
1610
1947
  msg << " " << Color.reason("(reason: #{error.message})") if status == :SKIP
1611
- puts msg
1948
+ puts msg.join()
1612
1949
  end
1613
1950
 
1614
1951
  end
1615
1952
 
1616
1953
 
1617
1954
  class SimpleReporter < BaseReporter
1955
+ #; [!jxa1b] reports topics and progress.
1956
+
1957
+ def initialize()
1958
+ super
1959
+ @_nl = true
1960
+ end
1961
+
1962
+ def order_policy()
1963
+ :spec_first
1964
+ end
1965
+
1966
+ def _nl()
1967
+ (puts(); @_nl = true) unless @_nl
1968
+ end
1969
+ private :_nl
1970
+
1971
+ def _nl_off()
1972
+ @_nl = false
1973
+ end
1974
+ private :_nl_off
1975
+
1976
+ def enter_scope(scope)
1977
+ _nl()
1978
+ puts "## #{scope.filename}"
1979
+ end
1980
+
1981
+ def exit_scope(scope)
1982
+ _nl()
1983
+ print_exceptions()
1984
+ end
1985
+
1986
+ def enter_topic(topic, depth)
1987
+ super
1988
+ return if topic._prefix == '-'
1989
+ _nl()
1990
+ print "#{' ' * (depth - 1)}#{topic._prefix} #{Color.topic(topic.target)}: "
1991
+ $stdout.flush()
1992
+ _nl_off()
1993
+ end
1994
+
1995
+ def exit_topic(topic, depth)
1996
+ super
1997
+ return if topic._prefix == '-'
1998
+ _nl()
1999
+ print_exceptions()
2000
+ end
2001
+
2002
+ def exit_spec(spec, depth, status, error, parent)
2003
+ super
2004
+ print Color.status(status, CHARS[status] || '?')
2005
+ $stdout.flush
2006
+ _nl_off()
2007
+ end
2008
+
2009
+ end
2010
+
2011
+
2012
+ class CompactReporter < BaseReporter
1618
2013
  #; [!xfd5o] reports filename.
1619
2014
 
1620
2015
  def enter_scope(scope)
@@ -1681,14 +2076,13 @@ module Oktest
1681
2076
  REPORTER_CLASSES = {
1682
2077
  'verbose' => VerboseReporter, 'v' => VerboseReporter,
1683
2078
  'simple' => SimpleReporter, 's' => SimpleReporter,
2079
+ 'compact' => CompactReporter, 'c' => CompactReporter,
1684
2080
  'plain' => PlainReporter, 'p' => PlainReporter,
1685
2081
  'quiet' => QuietReporter, 'q' => QuietReporter,
1686
2082
  }
1687
2083
 
1688
2084
 
1689
2085
  def self.run(reporter: nil, style: nil)
1690
- #; [!kfi8b] do nothing when 'Oktest.scope()' not called.
1691
- return unless THE_GLOBAL_SCOPE.has_child?
1692
2086
  #; [!6xn3t] creates reporter object according to 'style:' keyword arg.
1693
2087
  klass = (style ? REPORTER_CLASSES[style] : REPORTER_CLASS) or
1694
2088
  raise ArgumentError, "#{style.inspect}: unknown style."
@@ -1737,6 +2131,13 @@ module Oktest
1737
2131
  return param_names
1738
2132
  end
1739
2133
 
2134
+ def keyword_param_names_of_block(block)
2135
+ #; [!p6qqp] returns keyword param names of proc object.
2136
+ names = []
2137
+ block.parameters.each {|kind, name| names << name if kind == :key }
2138
+ return names
2139
+ end
2140
+
1740
2141
  def strfold(str, width=80, mark='...')
1741
2142
  #; [!wb7m8] returns string as it is if string is not long.
1742
2143
  return str if str.bytesize <= width
@@ -1837,6 +2238,70 @@ module Oktest
1837
2238
  end
1838
2239
  end
1839
2240
 
2241
+ class PartialRegexp < Regexp
2242
+ attr_accessor :pattern_string, :begin, :end, :mark
2243
+ def inspect()
2244
+ #; [!uyh31] returns function call style string if @pattern_string is set.
2245
+ if @pattern_string
2246
+ c = @pattern_string.end_with?("\n") ? "" : ".chomp"
2247
+ p = @pattern_string.chomp
2248
+ b = @begin == '\A' ? "'\\A'" : @begin.inspect
2249
+ e = @end == '\z' ? "'\\z'" : @end.inspect
2250
+ m = mark == "{== ==}" ? "" : ", #{mark.inspect}"
2251
+ return "partial_regexp(<<PREXP#{c}, #{b}, #{e}#{m})\n#{p}\nPREXP\n"
2252
+ #; [!ts9v4] returns regexp literal style string if @pattern_string is not set.
2253
+ else
2254
+ s = super
2255
+ s = s.gsub(/([^\\](?:\\\\)*)((?:\\n)+)/) {
2256
+ $1 + ("\\n\n" * ($2.length / 2))
2257
+ }
2258
+ if s =~ /\n/
2259
+ s = s.sub(/\A\/(\\A)?/, "/\\1\n")
2260
+ s = s + "x" # `/.../x` means multiline regexp
2261
+ end
2262
+ return s
2263
+ end
2264
+ end
2265
+ end
2266
+
2267
+ def partial_regexp!(pattern, begin_='\A', end_='\z', mark="{== ==}")
2268
+ #; [!peyu4] returns PartialRegexp object which inspect string is function call styel.
2269
+ regexp = partial_regexp(pattern, begin_, end_, mark)
2270
+ regexp.pattern_string = pattern
2271
+ regexp.begin = begin_; regexp.end = end_; regexp.mark = mark
2272
+ return regexp
2273
+ end
2274
+
2275
+ def partial_regexp(pattern, begin_='\A', end_='\z', mark="{== ==}")
2276
+ mark_rexp = PARTIAL_REGEXP_CACHE[mark]
2277
+ if mark_rexp.nil?
2278
+ #; [!ostkw] raises error if mark has no space or has more than two spaces.
2279
+ pair = mark.split()
2280
+ pair.length == 2 or
2281
+ raise ArgumentError.new("#{mark.inspect}: mark should contain only one space (ex: `{== ==}`).")
2282
+ open = Regexp.escape(pair[0])
2283
+ close = Regexp.escape(pair[1])
2284
+ mark_rexp = Regexp.compile("#{open}(.*?)#{close}")
2285
+ PARTIAL_REGEXP_CACHE[mark] = mark_rexp
2286
+ end
2287
+ #; [!wn524] returns PartialRegexp object which inspect string is regexp literal style.
2288
+ pos = 0
2289
+ buf = []
2290
+ buf << begin_ if begin_
2291
+ pattern.scan(mark_rexp) do
2292
+ m = Regexp.last_match
2293
+ text = pattern[pos, m.begin(0) - pos]
2294
+ buf << Regexp.escape(text) << $1.strip()
2295
+ pos = m.end(0)
2296
+ end
2297
+ rest = pos == 0 ? pattern : pattern[pos..-1]
2298
+ buf << Regexp.escape(rest) unless rest.empty?
2299
+ buf << end_ if end_
2300
+ return PartialRegexp.compile(buf.join())
2301
+ end
2302
+
2303
+ PARTIAL_REGEXP_CACHE = {} # :nodoc:
2304
+
1840
2305
  end
1841
2306
 
1842
2307
 
@@ -1979,24 +2444,35 @@ module Oktest
1979
2444
  module_function
1980
2445
 
1981
2446
  def normal s; return s; end
1982
- def bold s; return "\e[0;1m#{s}\e[22m"; end
1983
- def black s; return "\e[1;30m#{s}\e[0m"; end
1984
- def red s; return "\e[1;31m#{s}\e[0m"; end
1985
- def green s; return "\e[1;32m#{s}\e[0m"; end
1986
- def yellow s; return "\e[1;33m#{s}\e[0m"; end
1987
- def blue s; return "\e[1;34m#{s}\e[0m"; end
1988
- def magenta s; return "\e[1;35m#{s}\e[0m"; end
1989
- def cyan s; return "\e[1;36m#{s}\e[0m"; end
1990
- def white s; return "\e[1;37m#{s}\e[0m"; end
2447
+ def bold s; return "\e[0;1m#{s}\e[0m"; end
2448
+
2449
+ def black s; return "\e[0;30m#{s}\e[0m"; end
2450
+ def red s; return "\e[0;31m#{s}\e[0m"; end
2451
+ def green s; return "\e[0;32m#{s}\e[0m"; end
2452
+ def yellow s; return "\e[0;33m#{s}\e[0m"; end
2453
+ def blue s; return "\e[0;34m#{s}\e[0m"; end
2454
+ def magenta s; return "\e[0;35m#{s}\e[0m"; end
2455
+ def cyan s; return "\e[0;36m#{s}\e[0m"; end
2456
+ def white s; return "\e[0;37m#{s}\e[0m"; end
2457
+
2458
+ def black_b s; return "\e[1;30m#{s}\e[0m"; end # bold
2459
+ def red_b s; return "\e[1;31m#{s}\e[0m"; end # bold
2460
+ def green_b s; return "\e[1;32m#{s}\e[0m"; end # bold
2461
+ def yellow_b s; return "\e[1;33m#{s}\e[0m"; end # bold
2462
+ def blue_b s; return "\e[1;34m#{s}\e[0m"; end # bold
2463
+ def magenta_b s; return "\e[1;35m#{s}\e[0m"; end # bold
2464
+ def cyan_b s; return "\e[1;36m#{s}\e[0m"; end # bold
2465
+ def white_b s; return "\e[1;37m#{s}\e[0m"; end # bold
1991
2466
 
1992
2467
  def topic s; Config.color_enabled ? bold(s) : s; end
1993
2468
  def spec s; Config.color_enabled ? normal(s) : s; end
1994
- def pass s; Config.color_enabled ? blue(s) : s; end
2469
+ def pass s; Config.color_enabled ? cyan(s) : s; end
1995
2470
  def fail s; Config.color_enabled ? red(s) : s; end
1996
- def error s; Config.color_enabled ? red(s) : s; end
2471
+ def error s; Config.color_enabled ? red_b(s) : s; end # bold
1997
2472
  def skip s; Config.color_enabled ? yellow(s) : s; end
1998
2473
  def todo s; Config.color_enabled ? yellow(s) : s; end
1999
2474
  def reason s; Config.color_enabled ? yellow(s) : s; end
2475
+ def errmsg s; Config.color_enabled ? red(s) : s; end
2000
2476
 
2001
2477
  def status(status, s)
2002
2478
  #; [!yev5y] returns string containing color escape sequence.
@@ -2131,6 +2607,11 @@ END
2131
2607
  color_enabled = nil
2132
2608
  opts = Options.new
2133
2609
  parser = option_parser(opts)
2610
+ #; [!v5xie] parses $OKTEST_RB environment variable.
2611
+ if ENV.key?('OKTEST_RB')
2612
+ parser.parse(ENV['OKTEST_RB'].split())
2613
+ end
2614
+ #
2134
2615
  filenames = parser.parse(args)
2135
2616
  #; [!9973n] '-h' or '--help' option prints help message.
2136
2617
  if opts.help
@@ -2142,8 +2623,8 @@ END
2142
2623
  puts VERSION
2143
2624
  return 0
2144
2625
  end
2145
- #; [!dk8eg] '-C' or '--create' option prints test code skeleton.
2146
- if opts.create
2626
+ #; [!dk8eg] '-S' or '--skeleton' option prints test code skeleton.
2627
+ if opts.skeleton
2147
2628
  print SKELETON
2148
2629
  return 0
2149
2630
  end
@@ -2185,7 +2666,8 @@ END
2185
2666
  Config.auto_run = false
2186
2667
  #; [!18qpe] runs test scripts.
2187
2668
  #; [!0qd92] '-s verbose' or '-sv' option prints test results in verbose mode.
2188
- #; [!ef5v7] '-s simple' or '-ss' option prints test results in simple mode.
2669
+ #; [!zfdr5] '-s simple' or '-ss' option prints test results in simple mode.
2670
+ #; [!ef5v7] '-s compact' or '-sc' option prints test results in compact mode.
2189
2671
  #; [!244te] '-s plain' or '-sp' option prints test results in plain mode.
2190
2672
  #; [!ai61w] '-s quiet' or '-sq' option prints test results in quiet mode.
2191
2673
  n_errors = Oktest.run(:style=>opts.style)
@@ -2201,7 +2683,7 @@ END
2201
2683
  private
2202
2684
 
2203
2685
  class Options #:nodoc:
2204
- attr_accessor :help, :version, :style, :filter, :color, :create, :generate, :faster
2686
+ attr_accessor :help, :version, :style, :filter, :color, :skeleton, :generate, :faster
2205
2687
  end
2206
2688
 
2207
2689
  def option_parser(opts)
@@ -2216,6 +2698,7 @@ END
2216
2698
  }
2217
2699
  parser.on('-F PATTERN') {|val|
2218
2700
  #; [!71h2x] '-F ...' option will be error.
2701
+ #; [!j01y7] if filerting by '-F' matched nothing, then prints zero result.
2219
2702
  val =~ /\A(topic|spec|tag|sid)(=|!=)/ or
2220
2703
  raise OptionParser::InvalidArgument, val
2221
2704
  opts.filter = val
@@ -2227,7 +2710,7 @@ END
2227
2710
  #; [!dptgn] '--color' is same as '--color=on'.
2228
2711
  opts.color = val || 'on'
2229
2712
  }
2230
- parser.on('-C', '--create') { opts.create = true }
2713
+ parser.on('-S', '--skeleton') { opts.skeleton = true }
2231
2714
  parser.on('-G', '--generate[=styleoption]') {|val|
2232
2715
  val.nil? || val == 'unaryop' or
2233
2716
  raise OptionParser::InvalidArgument, val
@@ -2242,16 +2725,16 @@ END
2242
2725
  return HELP_MESSAGE % {command: command}
2243
2726
  end
2244
2727
 
2245
- HELP_MESSAGE = <<'END'
2728
+ HELP_MESSAGE = <<'END'.gsub(/^#.*\n/, '')
2246
2729
  Usage: %{command} [<options>] [<file-or-directory>...]
2247
2730
  -h, --help : show help
2248
2731
  --version : print version
2249
- -s <STYLE> : report style (verbose/simple/plain/quiet, or v/s/p/q)
2732
+ -s <REPORT-STYLE> : verbose/simple/compact/plain/quiet, or v/s/c/p/q
2250
2733
  -F <PATTERN> : filter topic or spec with pattern (see below)
2251
2734
  --color[={on|off}] : enable/disable output coloring forcedly
2252
- -C, --create : print test code skeleton
2735
+ -S, --skeleton : print test code skeleton
2253
2736
  -G, --generate : generate test code skeleton from ruby file
2254
- --faster : make 'ok{}' faster (for very large project)
2737
+ # --faster : make 'ok{}' faster (for very large project)
2255
2738
 
2256
2739
  Filter examples:
2257
2740
  $ oktest -F topic=Hello # filter by topic