oktest 1.0.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/oktest.rb CHANGED
@@ -1,16 +1,18 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  ###
4
- ### $Release: 1.0.1 $
4
+ ### $Release: 1.2.0 $
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.1 $'.split()[1]
15
+ VERSION = '$Release: 1.2.0 $'.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
@@ -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
@@ -970,7 +1238,7 @@ 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.
@@ -980,7 +1248,7 @@ module Oktest
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,10 @@ 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
+
1585
1916
  def enter_topic(topic, depth)
1586
1917
  super
1587
1918
  puts "#{' ' * (depth - 1)}#{topic._prefix} #{Color.topic(topic.target)}"
@@ -1606,15 +1937,73 @@ module Oktest
1606
1937
  $stdout.flush
1607
1938
  end
1608
1939
  label = Color.status(status, LABELS[status] || '???')
1609
- msg = "#{' ' * (depth - 1)}- [#{label}] #{spec.desc}"
1940
+ msg = ["#{' ' * (depth - 1)}- [#{label}] #{spec.desc}"]
1610
1941
  msg << " " << Color.reason("(reason: #{error.message})") if status == :SKIP
1611
- puts msg
1942
+ puts msg.join()
1612
1943
  end
1613
1944
 
1614
1945
  end
1615
1946
 
1616
1947
 
1617
1948
  class SimpleReporter < BaseReporter
1949
+ #; [!jxa1b] reports topics and progress.
1950
+
1951
+ def initialize()
1952
+ super
1953
+ @_nl = true
1954
+ end
1955
+
1956
+ def order_policy()
1957
+ :spec_first
1958
+ end
1959
+
1960
+ def _nl()
1961
+ (puts(); @_nl = true) unless @_nl
1962
+ end
1963
+ private :_nl
1964
+
1965
+ def _nl_off()
1966
+ @_nl = false
1967
+ end
1968
+ private :_nl_off
1969
+
1970
+ def enter_scope(scope)
1971
+ _nl()
1972
+ puts "## #{scope.filename}"
1973
+ end
1974
+
1975
+ def exit_scope(scope)
1976
+ _nl()
1977
+ print_exceptions()
1978
+ end
1979
+
1980
+ def enter_topic(topic, depth)
1981
+ super
1982
+ return if topic._prefix == '-'
1983
+ _nl()
1984
+ print "#{' ' * (depth - 1)}#{topic._prefix} #{Color.topic(topic.target)}: "
1985
+ $stdout.flush()
1986
+ _nl_off()
1987
+ end
1988
+
1989
+ def exit_topic(topic, depth)
1990
+ super
1991
+ return if topic._prefix == '-'
1992
+ _nl()
1993
+ print_exceptions()
1994
+ end
1995
+
1996
+ def exit_spec(spec, depth, status, error, parent)
1997
+ super
1998
+ print Color.status(status, CHARS[status] || '?')
1999
+ $stdout.flush
2000
+ _nl_off()
2001
+ end
2002
+
2003
+ end
2004
+
2005
+
2006
+ class CompactReporter < BaseReporter
1618
2007
  #; [!xfd5o] reports filename.
1619
2008
 
1620
2009
  def enter_scope(scope)
@@ -1681,14 +2070,13 @@ module Oktest
1681
2070
  REPORTER_CLASSES = {
1682
2071
  'verbose' => VerboseReporter, 'v' => VerboseReporter,
1683
2072
  'simple' => SimpleReporter, 's' => SimpleReporter,
2073
+ 'compact' => CompactReporter, 'c' => CompactReporter,
1684
2074
  'plain' => PlainReporter, 'p' => PlainReporter,
1685
2075
  'quiet' => QuietReporter, 'q' => QuietReporter,
1686
2076
  }
1687
2077
 
1688
2078
 
1689
2079
  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
2080
  #; [!6xn3t] creates reporter object according to 'style:' keyword arg.
1693
2081
  klass = (style ? REPORTER_CLASSES[style] : REPORTER_CLASS) or
1694
2082
  raise ArgumentError, "#{style.inspect}: unknown style."
@@ -1737,6 +2125,13 @@ module Oktest
1737
2125
  return param_names
1738
2126
  end
1739
2127
 
2128
+ def keyword_param_names_of_block(block)
2129
+ #; [!p6qqp] returns keyword param names of proc object.
2130
+ names = []
2131
+ block.parameters.each {|kind, name| names << name if kind == :key }
2132
+ return names
2133
+ end
2134
+
1740
2135
  def strfold(str, width=80, mark='...')
1741
2136
  #; [!wb7m8] returns string as it is if string is not long.
1742
2137
  return str if str.bytesize <= width
@@ -1837,6 +2232,70 @@ module Oktest
1837
2232
  end
1838
2233
  end
1839
2234
 
2235
+ class PartialRegexp < Regexp
2236
+ attr_accessor :pattern_string, :begin, :end, :mark
2237
+ def inspect()
2238
+ #; [!uyh31] returns function call style string if @pattern_string is set.
2239
+ if @pattern_string
2240
+ c = @pattern_string.end_with?("\n") ? "" : ".chomp"
2241
+ p = @pattern_string.chomp
2242
+ b = @begin == '\A' ? "'\\A'" : @begin.inspect
2243
+ e = @end == '\z' ? "'\\z'" : @end.inspect
2244
+ m = mark == "{== ==}" ? "" : ", #{mark.inspect}"
2245
+ return "partial_regexp(<<PREXP#{c}, #{b}, #{e}#{m})\n#{p}\nPREXP\n"
2246
+ #; [!ts9v4] returns regexp literal style string if @pattern_string is not set.
2247
+ else
2248
+ s = super
2249
+ s = s.gsub(/([^\\](?:\\\\)*)((?:\\n)+)/) {
2250
+ $1 + ("\\n\n" * ($2.length / 2))
2251
+ }
2252
+ if s =~ /\n/
2253
+ s = s.sub(/\A\/(\\A)?/, "/\\1\n")
2254
+ s = s + "x" # `/.../x` means multiline regexp
2255
+ end
2256
+ return s
2257
+ end
2258
+ end
2259
+ end
2260
+
2261
+ def partial_regexp!(pattern, begin_='\A', end_='\z', mark="{== ==}")
2262
+ #; [!peyu4] returns PartialRegexp object which inspect string is function call styel.
2263
+ regexp = partial_regexp(pattern, begin_, end_, mark)
2264
+ regexp.pattern_string = pattern
2265
+ regexp.begin = begin_; regexp.end = end_; regexp.mark = mark
2266
+ return regexp
2267
+ end
2268
+
2269
+ def partial_regexp(pattern, begin_='\A', end_='\z', mark="{== ==}")
2270
+ mark_rexp = PARTIAL_REGEXP_CACHE[mark]
2271
+ if mark_rexp.nil?
2272
+ #; [!ostkw] raises error if mark has no space or has more than two spaces.
2273
+ pair = mark.split()
2274
+ pair.length == 2 or
2275
+ raise ArgumentError.new("#{mark.inspect}: mark should contain only one space (ex: `{== ==}`).")
2276
+ open = Regexp.escape(pair[0])
2277
+ close = Regexp.escape(pair[1])
2278
+ mark_rexp = Regexp.compile("#{open}(.*?)#{close}")
2279
+ PARTIAL_REGEXP_CACHE[mark] = mark_rexp
2280
+ end
2281
+ #; [!wn524] returns PartialRegexp object which inspect string is regexp literal style.
2282
+ pos = 0
2283
+ buf = []
2284
+ buf << begin_ if begin_
2285
+ pattern.scan(mark_rexp) do
2286
+ m = Regexp.last_match
2287
+ text = pattern[pos, m.begin(0) - pos]
2288
+ buf << Regexp.escape(text) << $1.strip()
2289
+ pos = m.end(0)
2290
+ end
2291
+ rest = pos == 0 ? pattern : pattern[pos..-1]
2292
+ buf << Regexp.escape(rest) unless rest.empty?
2293
+ buf << end_ if end_
2294
+ return PartialRegexp.compile(buf.join())
2295
+ end
2296
+
2297
+ PARTIAL_REGEXP_CACHE = {} # :nodoc:
2298
+
1840
2299
  end
1841
2300
 
1842
2301
 
@@ -1979,24 +2438,35 @@ module Oktest
1979
2438
  module_function
1980
2439
 
1981
2440
  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
2441
+ def bold s; return "\e[0;1m#{s}\e[0m"; end
2442
+
2443
+ def black s; return "\e[0;30m#{s}\e[0m"; end
2444
+ def red s; return "\e[0;31m#{s}\e[0m"; end
2445
+ def green s; return "\e[0;32m#{s}\e[0m"; end
2446
+ def yellow s; return "\e[0;33m#{s}\e[0m"; end
2447
+ def blue s; return "\e[0;34m#{s}\e[0m"; end
2448
+ def magenta s; return "\e[0;35m#{s}\e[0m"; end
2449
+ def cyan s; return "\e[0;36m#{s}\e[0m"; end
2450
+ def white s; return "\e[0;37m#{s}\e[0m"; end
2451
+
2452
+ def black_b s; return "\e[1;30m#{s}\e[0m"; end # bold
2453
+ def red_b s; return "\e[1;31m#{s}\e[0m"; end # bold
2454
+ def green_b s; return "\e[1;32m#{s}\e[0m"; end # bold
2455
+ def yellow_b s; return "\e[1;33m#{s}\e[0m"; end # bold
2456
+ def blue_b s; return "\e[1;34m#{s}\e[0m"; end # bold
2457
+ def magenta_b s; return "\e[1;35m#{s}\e[0m"; end # bold
2458
+ def cyan_b s; return "\e[1;36m#{s}\e[0m"; end # bold
2459
+ def white_b s; return "\e[1;37m#{s}\e[0m"; end # bold
1991
2460
 
1992
2461
  def topic s; Config.color_enabled ? bold(s) : s; end
1993
2462
  def spec s; Config.color_enabled ? normal(s) : s; end
1994
- def pass s; Config.color_enabled ? blue(s) : s; end
2463
+ def pass s; Config.color_enabled ? cyan(s) : s; end
1995
2464
  def fail s; Config.color_enabled ? red(s) : s; end
1996
- def error s; Config.color_enabled ? red(s) : s; end
2465
+ def error s; Config.color_enabled ? red_b(s) : s; end # bold
1997
2466
  def skip s; Config.color_enabled ? yellow(s) : s; end
1998
2467
  def todo s; Config.color_enabled ? yellow(s) : s; end
1999
2468
  def reason s; Config.color_enabled ? yellow(s) : s; end
2469
+ def errmsg s; Config.color_enabled ? red(s) : s; end
2000
2470
 
2001
2471
  def status(status, s)
2002
2472
  #; [!yev5y] returns string containing color escape sequence.
@@ -2131,6 +2601,11 @@ END
2131
2601
  color_enabled = nil
2132
2602
  opts = Options.new
2133
2603
  parser = option_parser(opts)
2604
+ #; [!v5xie] parses $OKTEST_RB environment variable.
2605
+ if ENV.key?('OKTEST_RB')
2606
+ parser.parse(ENV['OKTEST_RB'].split())
2607
+ end
2608
+ #
2134
2609
  filenames = parser.parse(args)
2135
2610
  #; [!9973n] '-h' or '--help' option prints help message.
2136
2611
  if opts.help
@@ -2142,8 +2617,8 @@ END
2142
2617
  puts VERSION
2143
2618
  return 0
2144
2619
  end
2145
- #; [!dk8eg] '-C' or '--create' option prints test code skeleton.
2146
- if opts.create
2620
+ #; [!dk8eg] '-S' or '--skeleton' option prints test code skeleton.
2621
+ if opts.skeleton
2147
2622
  print SKELETON
2148
2623
  return 0
2149
2624
  end
@@ -2154,7 +2629,7 @@ END
2154
2629
  return 0
2155
2630
  end
2156
2631
  #; [!65vdx] prints help message if no arguments specified.
2157
- if filenames.empty?
2632
+ if filenames.empty? && !THE_GLOBAL_SCOPE.has_child?
2158
2633
  puts help_message()
2159
2634
  return 0
2160
2635
  end
@@ -2185,7 +2660,8 @@ END
2185
2660
  Config.auto_run = false
2186
2661
  #; [!18qpe] runs test scripts.
2187
2662
  #; [!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.
2663
+ #; [!zfdr5] '-s simple' or '-ss' option prints test results in simple mode.
2664
+ #; [!ef5v7] '-s compact' or '-sc' option prints test results in compact mode.
2189
2665
  #; [!244te] '-s plain' or '-sp' option prints test results in plain mode.
2190
2666
  #; [!ai61w] '-s quiet' or '-sq' option prints test results in quiet mode.
2191
2667
  n_errors = Oktest.run(:style=>opts.style)
@@ -2201,7 +2677,7 @@ END
2201
2677
  private
2202
2678
 
2203
2679
  class Options #:nodoc:
2204
- attr_accessor :help, :version, :style, :filter, :color, :create, :generate, :faster
2680
+ attr_accessor :help, :version, :style, :filter, :color, :skeleton, :generate, :faster
2205
2681
  end
2206
2682
 
2207
2683
  def option_parser(opts)
@@ -2216,6 +2692,7 @@ END
2216
2692
  }
2217
2693
  parser.on('-F PATTERN') {|val|
2218
2694
  #; [!71h2x] '-F ...' option will be error.
2695
+ #; [!j01y7] if filerting by '-F' matched nothing, then prints zero result.
2219
2696
  val =~ /\A(topic|spec|tag|sid)(=|!=)/ or
2220
2697
  raise OptionParser::InvalidArgument, val
2221
2698
  opts.filter = val
@@ -2227,7 +2704,7 @@ END
2227
2704
  #; [!dptgn] '--color' is same as '--color=on'.
2228
2705
  opts.color = val || 'on'
2229
2706
  }
2230
- parser.on('-C', '--create') { opts.create = true }
2707
+ parser.on('-S', '--skeleton') { opts.skeleton = true }
2231
2708
  parser.on('-G', '--generate[=styleoption]') {|val|
2232
2709
  val.nil? || val == 'unaryop' or
2233
2710
  raise OptionParser::InvalidArgument, val
@@ -2242,16 +2719,16 @@ END
2242
2719
  return HELP_MESSAGE % {command: command}
2243
2720
  end
2244
2721
 
2245
- HELP_MESSAGE = <<'END'
2722
+ HELP_MESSAGE = <<'END'.gsub(/^#.*\n/, '')
2246
2723
  Usage: %{command} [<options>] [<file-or-directory>...]
2247
2724
  -h, --help : show help
2248
2725
  --version : print version
2249
- -s <STYLE> : report style (verbose/simple/plain/quiet, or v/s/p/q)
2726
+ -s <REPORT-STYLE> : verbose/simple/compact/plain/quiet, or v/s/c/p/q
2250
2727
  -F <PATTERN> : filter topic or spec with pattern (see below)
2251
2728
  --color[={on|off}] : enable/disable output coloring forcedly
2252
- -C, --create : print test code skeleton
2729
+ -S, --skeleton : print test code skeleton
2253
2730
  -G, --generate : generate test code skeleton from ruby file
2254
- --faster : make 'ok{}' faster (for very large project)
2731
+ # --faster : make 'ok{}' faster (for very large project)
2255
2732
 
2256
2733
  Filter examples:
2257
2734
  $ oktest -F topic=Hello # filter by topic