oktest 1.0.2 → 1.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 9f8f2b588e8b3d85d2dfc1bbf975b491484b400a
4
- data.tar.gz: 7b8ede816f90a30f76263b713456fb90ae1e86b5
2
+ SHA256:
3
+ metadata.gz: 240a20810f76b97272c48007e0a8a3193f0217c0aa31d7e9ccf363e05aa577aa
4
+ data.tar.gz: 035ac9fb077d8f5e67355c9d6a177e67684e11e46db351edc71992f280d4ab08
5
5
  SHA512:
6
- metadata.gz: 2888eb6c1bfde26a01bd15baaf40b5de1cfd0d6b604e9a6645dd778dc69412d92c3c89cb37abb7f73b94ac7adcaf151600b1ef17a5a89d253eb8f40fecc4d07b
7
- data.tar.gz: b22ff5554db672ee5d7d048acf45fd6428106763ebd6d1277eb49fe1d15cabd45927564de2fa602e58a53825d040bfb684473fdebc9de4ba8dbc7f500db9e7d2
6
+ metadata.gz: 333dea58ea3e17034bda9706a426fc63e089fda74ea2b2626915b635b3cdf5e72cce076823b22acb2d7390a189408adf0286eac89650a80939d057f51ba2ff09
7
+ data.tar.gz: 4136c2c8613ca829d0d72deb8c857995a482e19b0b9c7c282e9a45b3948b9ac3dced2ba665544a98c62d79260e3bbf2da231f577c4221fd23bd81087dea168cf
data/README.md CHANGED
@@ -4,12 +4,13 @@
4
4
 
5
5
  Oktest.rb is a new-style testing library for Ruby.
6
6
 
7
- * `ok {actual} == expected` style assertion.
8
- * **Fixture injection** inspired by dependency injection.
7
+ * `ok {actual} == expected` style [assertion](#assertions).
8
+ * [Fixture injection](#fixture-injection) inspired by dependency injection.
9
9
  * Structured test specifications like RSpec.
10
- * Filtering testcases by pattern or tags.
10
+ * [JSON Matcher](#json-matcher) similar to JSON Schema.
11
+ * [Filtering](#tag-and-filtering) testcases by pattern or tags.
11
12
  * Blue/red color instead of green/red for accesability.
12
- * Small code size (about 2400 lines) and good performance.
13
+ * Small code size (less than 3000 lines) and good performance.
13
14
 
14
15
  ```ruby
15
16
  ### Oktest ### Test::Unit
@@ -82,6 +83,11 @@ Oktest.rb requires Ruby 2.3 or later.
82
83
  * <a href="#dummy_attrs"><code>dummy_attrs()</code></a>
83
84
  * <a href="#dummy_ivars"><code>dummy_ivars()</code></a>
84
85
  * <a href="#recorder"><code>recorder()</code></a>
86
+ * <a href="#json-matcher">JSON Matcher</a>
87
+ * <a href="#simple-example">Simple Example</a>
88
+ * <a href="#nested-example">Nested Example</a>
89
+ * <a href="#complex-example">Complex Example</a>
90
+ * <a href="#helper-methods-for-json-matcher">Helper Methods for JSON Matcher</a>
85
91
  * <a href="#tips">Tips</a>
86
92
  * <a href="#ok--in-minitest"><code>ok {}</code> in MiniTest</a>
87
93
  * <a href="#testing-rack-application">Testing Rack Application</a>
@@ -165,6 +171,7 @@ Result:
165
171
 
166
172
  ```terminal
167
173
  $ oktest test/example01_test.rb # or: ruby test/example01_test.rb
174
+ ## test/example01_test.rb
168
175
  * Hello
169
176
  * #hello()
170
177
  - [pass] returns greeting message.
@@ -206,6 +213,7 @@ Result:
206
213
 
207
214
  ```terminal
208
215
  $ oktest test/example02_test.rb # or: ruby test/example02_test.rb
216
+ ## test/example02_test.rb
209
217
  * other examples
210
218
  - [Fail] example of assertion failure
211
219
  - [ERROR] example of something error
@@ -260,6 +268,7 @@ Result:
260
268
 
261
269
  ```terminal
262
270
  $ oktest test/example03_test.rb # or: ruby test/example03_test.rb
271
+ ## oktest test/example03_test.rb
263
272
  * other examples
264
273
  - [Skip] example of skip (reason: requires Ruby3)
265
274
  - [TODO] example of todo
@@ -280,6 +289,7 @@ Verbose mode (default):
280
289
 
281
290
  ```terminal
282
291
  $ oktest test/example01_test.rb -s verbose # or -sv
292
+ ## test/example01_test.rb
283
293
  * Hello
284
294
  * #hello()
285
295
  - [pass] returns greeting message.
@@ -291,6 +301,16 @@ Simple mode:
291
301
 
292
302
  ```terminal
293
303
  $ oktest test/example01_test.rb -s simple # or -ss
304
+ ## test/example01_test.rb
305
+ * Hello:
306
+ * #hello(): ..
307
+ ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s
308
+ ```
309
+
310
+ Compact mode:
311
+
312
+ ```terminal
313
+ $ oktest test/example01_test.rb -s compact # or -sc
294
314
  test/example01_test.rb: ..
295
315
  ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s
296
316
  ```
@@ -298,7 +318,7 @@ test/example01_test.rb: ..
298
318
  Plain mode:
299
319
 
300
320
  ```terminal
301
- $ oktest test/example01_test.rb -s simple # or -ss
321
+ $ oktest test/example01_test.rb -s plain # or -sp
302
322
  ..
303
323
  ## total:2 (pass:2, fail:0, error:0, skip:0, todo:0) in 0.000s
304
324
  ```
@@ -324,7 +344,7 @@ How to run test scripts under `test` directory:
324
344
  $ ls test/
325
345
  example01_test.rb example02_test.rb example03_test.rb
326
346
 
327
- $ oktest -s simple test # or: ruby -r oktest -e 'Oktest.main' -- test -s simple
347
+ $ oktest -s compact test # or: ruby -r oktest -e 'Oktest.main' -- test -s compact
328
348
  test/example01_test.rb: ..
329
349
  test/example02_test.rb: fE
330
350
  ----------------------------------------------------------------------
@@ -457,6 +477,7 @@ Result:
457
477
 
458
478
  ```terminal
459
479
  $ ruby test/example05_test.rb
480
+ ## test/example05_test.rb
460
481
  * Integer
461
482
  * #abs()
462
483
  - When value is negative...
@@ -1022,8 +1043,8 @@ Oktest.scope do
1022
1043
  after { puts "===== Inner: after =====" } # !!!!!
1023
1044
 
1024
1045
  spec "example" do
1025
- ok {1+1} == 2
1026
- end
1046
+ ok {1+1} == 2
1047
+ end
1027
1048
 
1028
1049
  end
1029
1050
 
@@ -1589,6 +1610,216 @@ end
1589
1610
 
1590
1611
 
1591
1612
 
1613
+ ## JSON Matcher
1614
+
1615
+ Oktest.rb provides easy way to assert JSON data.
1616
+ This is very convenient feature, but don't abuse it.
1617
+
1618
+
1619
+ ### Simple Example
1620
+
1621
+ <!--
1622
+ test/example41_test.rb:
1623
+ -->
1624
+ ```ruby
1625
+ require 'oktest'
1626
+ require 'set' # !!!!!
1627
+
1628
+ Oktest.scope do
1629
+ topic 'JSON Example' do
1630
+
1631
+ spec "simple example" do
1632
+ actual = {
1633
+ "name": "Alice",
1634
+ "id": 1001,
1635
+ "age": 18,
1636
+ "email": "alice@example.com",
1637
+ "gender": "F",
1638
+ "deleted": false,
1639
+ "tags": ["aaa", "bbb", "ccc"],
1640
+ #"twitter": "@alice",
1641
+ }
1642
+ ## assertion
1643
+ ok {JSON(actual)} === { # requires `JSON()` and `===`
1644
+ "name": "Alice", # scalar value
1645
+ "id": 1000..9999, # range object
1646
+ "age": Integer, # class object
1647
+ "email": /^\w+@example\.com$/, # regexp
1648
+ "gender": Set.new(["M", "F"]), # Set object ("M" or "F")
1649
+ "deleted": Set.new([true, false]), # boolean (true or false)
1650
+ "tags": [/^\w+$/].each, # Enumerator object (!= Array obj)
1651
+ "twitter?": /^@\w+$/, # key 'xxx?' means optional value
1652
+ }
1653
+ end
1654
+
1655
+ end
1656
+ end
1657
+ ```
1658
+
1659
+ (Note: Ruby 2.4 or older doesn't have `Set#===()`, so above code will occur error
1660
+ in Ruby 2.4 or older. Please add the folllowing hack in your test script.)
1661
+
1662
+ ```ruby
1663
+ require 'set'
1664
+ unless Set.instance_methods(false).include?(:===) # for Ruby 2.4 or older
1665
+ class Set; alias === include?; end
1666
+ end
1667
+ ```
1668
+
1669
+ Notice that `Enumerator` has different meaning from `Array` in JSON matcher.
1670
+
1671
+ ```ruby
1672
+ actual = {"tags": ["foo", "bar", "baz"]}
1673
+
1674
+ ## Array
1675
+ ok {JSON(actual)} == {"tags": ["foo", "bar", "baz"]}
1676
+
1677
+ ## Enumerator
1678
+ ok {JSON(actual)} == {"tags": [/^\w+$/].each}
1679
+ ```
1680
+
1681
+
1682
+ ### Nested Example
1683
+
1684
+ <!--
1685
+ test/example42_test.rb:
1686
+ -->
1687
+ ```ruby
1688
+ require 'oktest'
1689
+ require 'set' # !!!!!
1690
+
1691
+ Oktest.scope do
1692
+ topic 'JSON Example' do
1693
+
1694
+ spec "nested example" do
1695
+ actual = {
1696
+ "teams": [
1697
+ {
1698
+ "team": "Section 9",
1699
+ "members": [
1700
+ {"id": 2500, "name": "Aramaki", "gender": "M"},
1701
+ {"id": 2501, "name": "Motoko" , "gender": "F"},
1702
+ {"id": 2502, "name": "Batou" , "gender": "M"},
1703
+ ],
1704
+ "leader": "Aramaki",
1705
+ },
1706
+ {
1707
+ "team": "SOS Brigade",
1708
+ "members": [
1709
+ {"id": 1001, "name": "Haruhi", "gender": "F"},
1710
+ {"id": 1002, "name": "Mikuru", "gender": "F"},
1711
+ {"id": 1003, "name": "Yuki" , "gender": "F"},
1712
+ {"id": 1004, "name": "Itsuki", "gender": "M"},
1713
+ {"id": 1005, "name": "Kyon" , "gender": "M"},
1714
+ ],
1715
+ },
1716
+ ],
1717
+ }
1718
+ ## assertion
1719
+ ok {JSON(actual)} === { # requires `JSON()` and `===`
1720
+ "teams": [
1721
+ {
1722
+ "team": String,
1723
+ "members": [
1724
+ {"id": 1000..9999, "name": String, "gender": Set.new(["M", "F"])}
1725
+ ].each, # Enumerator object (!= Array obj)
1726
+ "leader?": String, # key 'xxx?' means optional value
1727
+ }
1728
+ ].each, # Enumerator object (!= Array obj)
1729
+ }
1730
+ end
1731
+
1732
+ end
1733
+ end
1734
+ ```
1735
+
1736
+
1737
+ ### Complex Example
1738
+
1739
+ * `OR(x, y, z)` matches to `x`, `y`, or `z`.
1740
+ * `AND(x, y, z)` matches to `x`, `y`, and `z`.
1741
+ * Key `"*"` matches to any key of hash object.
1742
+ * `Any()` matches to anything.
1743
+
1744
+ <!--
1745
+ test/example43_test.rb:
1746
+ -->
1747
+ ```ruby
1748
+ require 'oktest'
1749
+ require 'set'
1750
+
1751
+ Oktest.scope do
1752
+ topic 'JSON Example' do
1753
+
1754
+ spec "OR() example" do
1755
+ ok {JSON({"val": "123"})} === {"val": OR(String, Integer)} # OR()
1756
+ ok {JSON({"val": 123 })} === {"val": OR(String, Integer)} # OR()
1757
+ end
1758
+
1759
+ spec "AND() example" do
1760
+ ok {JSON({"val": "123"})} === {"val": AND(String, /^\d+$/)} # AND()
1761
+ ok {JSON({"val": 123 })} === {"val": AND(Integer, 1..1000)} # AND()
1762
+ end
1763
+
1764
+ spec "`*` and `ANY` example" do
1765
+ ok {JSON({"name": "Bob", "age": 20})} === {"*": Any()} # '*' and Any()
1766
+ end
1767
+
1768
+ spec "complex exapmle" do
1769
+ actual = {
1770
+ "item": "awesome item",
1771
+ "colors": ["red", "#cceeff", "green", "#fff"],
1772
+ "memo": "this is awesome.",
1773
+ "url": "https://example.com/awesome",
1774
+ }
1775
+ ## assertion
1776
+ color_names = ["red", "blue", "green", "white", "black"]
1777
+ color_pat = /^\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/
1778
+ ok {JSON(actual)} === {
1779
+ "colors": [
1780
+ AND(String, OR(Set.new(color_names), color_pat)), # AND() and OR()
1781
+ ].each,
1782
+ "*": Any(), # match to any key (`"*"`) and value (`ANY`)
1783
+ }
1784
+ end
1785
+
1786
+ end
1787
+ end
1788
+ ```
1789
+
1790
+ (Note: `/^\d+$/` implies String value, and `1..100` implies Integer value.)
1791
+
1792
+ ```ruby
1793
+ ## No need to write:
1794
+ ## ok {JSON{...}} === {"val": AND(String, /^\d+$/)}
1795
+ ## ok {JSON{...}} === {"val": AND(Integer, 1..100)}
1796
+ ok {JSON({"val": "A"})} === {"val": /^\d+$/} # implies String value
1797
+ ok {JSON({"val": 99 })} === {"val": 1..100} # implies Integer value
1798
+ ```
1799
+
1800
+
1801
+ ### Helper Methods for JSON Matcher
1802
+
1803
+ Oktest.rb provides some helper methods and objects:
1804
+
1805
+ * `Enum(x, y, z)` is almost same as `Set.new([x, y, z])`.
1806
+ * `Bool()` is same as `Enum(true, false)`.
1807
+ * `Length(3)` matches to length 3, and `Length(1..3)` matches to length 1..3.
1808
+
1809
+ <!--
1810
+ test/example44_test.rb:
1811
+ -->
1812
+ ```ruby
1813
+ actual = {"gender": "M", "deleted": false, "code": "ABCD1234"}
1814
+ ok {JSON(actual)} == {
1815
+ "gender": Enum("M", "F"), # same as Set.new(["M", "F"])
1816
+ "deleted": Bool(), # same as Enum(true, false)
1817
+ "code": Length(6..10), # code length should be 6..10
1818
+ }
1819
+ ```
1820
+
1821
+
1822
+
1592
1823
  ## Tips
1593
1824
 
1594
1825
 
@@ -1597,7 +1828,7 @@ end
1597
1828
  If you want to use `ok {actual} == expected` style assertion in MiniTest,
1598
1829
  install `minitest-ok` gem instead of `otest` gem.
1599
1830
 
1600
- test/example41_test.rb:
1831
+ test/example51_test.rb:
1601
1832
 
1602
1833
  ```ruby
1603
1834
  require 'minitest/spec'
@@ -1620,7 +1851,7 @@ See [minitest-ok README](https://github.com/kwatch/minitest-ok) for details.
1620
1851
 
1621
1852
  `rack-test_app` gem will help you to test Rack application very well.
1622
1853
 
1623
- test/example42_test.rb:
1854
+ test/example52_test.rb:
1624
1855
 
1625
1856
  ```ruby
1626
1857
  require 'rack'
@@ -1693,7 +1924,7 @@ end
1693
1924
 
1694
1925
  Oktest.rb provides `Traverser` class which implements Visitor pattern.
1695
1926
 
1696
- test/example44_test.rb:
1927
+ test/example54_test.rb:
1697
1928
 
1698
1929
  ```ruby
1699
1930
  require 'oktest'
@@ -1750,8 +1981,8 @@ MyTraverser.new.start()
1750
1981
  Result:
1751
1982
 
1752
1983
  ```terminal
1753
- $ ruby test/example44_test.rb
1754
- # scope: test/example44_test.rb
1984
+ $ ruby test/example54_test.rb
1985
+ # scope: test/example54_test.rb
1755
1986
  + topic: Example Topic
1756
1987
  - spec: sample #1
1757
1988
  - spec: sample #2
data/Rakefile.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  ###
4
- ### $Release: 1.0.2 $
4
+ ### $Release: 1.1.0 $
5
5
  ### $Copyright: copyright(c) 2011-2021 kuwata-lab.com all rights reserved $
6
6
  ### $License: MIT License $
7
7
  ###
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.1.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.2 $'.split()[1]
15
+ VERSION = '$Release: 1.1.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,246 @@ 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
+
551
797
  class Context
552
798
  ## * Context class is separated from ScopeNode, TopicNode, and SpecLeaf.
553
799
  ## * `topic()` and `spec()` creates subclass of Context class,
@@ -1116,6 +1362,41 @@ module Oktest
1116
1362
  return Benry::Recorder.new
1117
1363
  end
1118
1364
 
1365
+ def JSON(actual)
1366
+ #; [!n0k03] creates JsonMatcher object.
1367
+ return JsonMatcher.new(actual)
1368
+ end
1369
+
1370
+ def Enum(*values)
1371
+ #; [!fbfr0] creates Enum object which is a subclass of Set.
1372
+ return JsonMatcher::Enum.new(values)
1373
+ end
1374
+
1375
+ def Bool()
1376
+ #; [!vub5j] creates a set of true and false.
1377
+ return Enum(true, false)
1378
+ end
1379
+
1380
+ def OR(*args)
1381
+ #; [!9e8im] creates `OR` object.
1382
+ return JsonMatcher::OR.new(*args)
1383
+ end
1384
+
1385
+ def AND(*args)
1386
+ #; [!38jln] creates `AND` object.
1387
+ return JsonMatcher::AND.new(*args)
1388
+ end
1389
+
1390
+ def Length(n)
1391
+ #; [!qqas3] creates Length object.
1392
+ return JsonMatcher::Length.new(n)
1393
+ end
1394
+
1395
+ def Any()
1396
+ #; [!dlo1o] creates an 'Any' object.
1397
+ return JsonMatcher::Any.new
1398
+ end
1399
+
1119
1400
  end
1120
1401
 
1121
1402
 
@@ -1215,11 +1496,25 @@ module Oktest
1215
1496
  @reporter.exit_all(self)
1216
1497
  end
1217
1498
 
1499
+ def _spec_first(node)
1500
+ a1, a2 = node.each_child.partition {|c|
1501
+ c.is_a?(SpecLeaf) || (c.is_a?(TopicNode) && c._prefix != '*')
1502
+ }
1503
+ return a1+a2
1504
+ end
1505
+ private :_spec_first
1506
+
1218
1507
  def visit_scope(scope, depth, parent)
1219
1508
  @reporter.enter_scope(scope) unless scope.equal?(THE_GLOBAL_SCOPE)
1220
1509
  #; [!5anr7] calls before_all and after_all blocks.
1221
1510
  call_before_all_block(scope)
1222
- scope.each_child {|c| c.accept_visitor(self, depth+1, scope) }
1511
+ #; [!c5cw0] run specs and case_when in advance of specs and topics when SimpleReporter.
1512
+ if @reporter.order_policy() == :spec_first
1513
+ _spec_first(scope).each {|c| c.accept_visitor(self, depth+1, scope) }
1514
+ else
1515
+ scope.each_child {|c| c.accept_visitor(self, depth+1, scope) }
1516
+ end
1517
+ #
1223
1518
  call_after_all_block(scope)
1224
1519
  @reporter.exit_scope(scope) unless scope.equal?(THE_GLOBAL_SCOPE)
1225
1520
  end
@@ -1228,7 +1523,13 @@ module Oktest
1228
1523
  @reporter.enter_topic(topic, depth)
1229
1524
  #; [!i3yfv] calls 'before_all' and 'after_all' blocks.
1230
1525
  call_before_all_block(topic)
1231
- topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1526
+ #; [!p3a5o] run specs and case_when in advance of specs and topics when SimpleReporter.
1527
+ if @reporter.order_policy() == :spec_first
1528
+ _spec_first(topic).each {|c| c.accept_visitor(self, depth+1, topic) }
1529
+ else
1530
+ topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1531
+ end
1532
+ #
1232
1533
  call_after_all_block(topic)
1233
1534
  @reporter.exit_topic(topic, depth)
1234
1535
  end
@@ -1429,6 +1730,7 @@ module Oktest
1429
1730
  def exit_spec(spec, depth, status, error, parent); end
1430
1731
  #
1431
1732
  def counts; {}; end
1733
+ def order_policy(); nil; end # :spec_first or nil
1432
1734
 
1433
1735
  end
1434
1736
 
@@ -1439,7 +1741,7 @@ module Oktest
1439
1741
  CHARS = { :PASS=>'.', :FAIL=>'f', :ERROR=>'E', :SKIP=>'s', :TODO=>'t' }
1440
1742
 
1441
1743
 
1442
- def initialize
1744
+ def initialize()
1443
1745
  @exceptions = []
1444
1746
  @counts = {}
1445
1747
  end
@@ -1582,6 +1884,10 @@ module Oktest
1582
1884
 
1583
1885
  LABELS = { :PASS=>'pass', :FAIL=>'Fail', :ERROR=>'ERROR', :SKIP=>'Skip', :TODO=>'TODO' }
1584
1886
 
1887
+ def enter_scope(scope)
1888
+ puts "## #{scope.filename}"
1889
+ end
1890
+
1585
1891
  def enter_topic(topic, depth)
1586
1892
  super
1587
1893
  puts "#{' ' * (depth - 1)}#{topic._prefix} #{Color.topic(topic.target)}"
@@ -1615,6 +1921,64 @@ module Oktest
1615
1921
 
1616
1922
 
1617
1923
  class SimpleReporter < BaseReporter
1924
+ #; [!jxa1b] reports topics and progress.
1925
+
1926
+ def initialize()
1927
+ super
1928
+ @_nl = true
1929
+ end
1930
+
1931
+ def order_policy()
1932
+ :spec_first
1933
+ end
1934
+
1935
+ def _nl()
1936
+ (puts(); @_nl = true) unless @_nl
1937
+ end
1938
+ private :_nl
1939
+
1940
+ def _nl_off()
1941
+ @_nl = false
1942
+ end
1943
+ private :_nl_off
1944
+
1945
+ def enter_scope(scope)
1946
+ _nl()
1947
+ puts "## #{scope.filename}"
1948
+ end
1949
+
1950
+ def exit_scope(scope)
1951
+ _nl()
1952
+ print_exceptions()
1953
+ end
1954
+
1955
+ def enter_topic(topic, depth)
1956
+ super
1957
+ return if topic._prefix == '-'
1958
+ _nl()
1959
+ print "#{' ' * (depth - 1)}#{topic._prefix} #{Color.topic(topic.target)}: "
1960
+ $stdout.flush()
1961
+ _nl_off()
1962
+ end
1963
+
1964
+ def exit_topic(topic, depth)
1965
+ super
1966
+ return if topic._prefix == '-'
1967
+ _nl()
1968
+ print_exceptions()
1969
+ end
1970
+
1971
+ def exit_spec(spec, depth, status, error, parent)
1972
+ super
1973
+ print Color.status(status, CHARS[status] || '?')
1974
+ $stdout.flush
1975
+ _nl_off()
1976
+ end
1977
+
1978
+ end
1979
+
1980
+
1981
+ class CompactReporter < BaseReporter
1618
1982
  #; [!xfd5o] reports filename.
1619
1983
 
1620
1984
  def enter_scope(scope)
@@ -1681,6 +2045,7 @@ module Oktest
1681
2045
  REPORTER_CLASSES = {
1682
2046
  'verbose' => VerboseReporter, 'v' => VerboseReporter,
1683
2047
  'simple' => SimpleReporter, 's' => SimpleReporter,
2048
+ 'compact' => CompactReporter, 'c' => CompactReporter,
1684
2049
  'plain' => PlainReporter, 'p' => PlainReporter,
1685
2050
  'quiet' => QuietReporter, 'q' => QuietReporter,
1686
2051
  }
@@ -2185,7 +2550,8 @@ END
2185
2550
  Config.auto_run = false
2186
2551
  #; [!18qpe] runs test scripts.
2187
2552
  #; [!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.
2553
+ #; [!zfdr5] '-s simple' or '-ss' option prints test results in simple mode.
2554
+ #; [!ef5v7] '-s compact' or '-sc' option prints test results in compact mode.
2189
2555
  #; [!244te] '-s plain' or '-sp' option prints test results in plain mode.
2190
2556
  #; [!ai61w] '-s quiet' or '-sq' option prints test results in quiet mode.
2191
2557
  n_errors = Oktest.run(:style=>opts.style)
@@ -2246,7 +2612,7 @@ END
2246
2612
  Usage: %{command} [<options>] [<file-or-directory>...]
2247
2613
  -h, --help : show help
2248
2614
  --version : print version
2249
- -s <STYLE> : report style (verbose/simple/plain/quiet, or v/s/p/q)
2615
+ -s <REPORT-STYLE> : verbose/simple/compact/plain/quiet, or v/s/c/p/q
2250
2616
  -F <PATTERN> : filter topic or spec with pattern (see below)
2251
2617
  --color[={on|off}] : enable/disable output coloring forcedly
2252
2618
  -C, --create : print test code skeleton