oktest 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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