trackler 2.0.3.8 → 2.0.3.9

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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/common/exercises/book-store/description.md +67 -0
  3. data/common/exercises/book-store/metadata.yml +4 -0
  4. data/lib/trackler/version.rb +1 -1
  5. data/tracks/crystal/exercises/difference-of-squares/src/difference_of_squares.cr +1 -0
  6. data/tracks/ecmascript/exercises/custom-set/package.json +1 -0
  7. data/tracks/ecmascript/exercises/palindrome-products/palindrome-products.js +18 -18
  8. data/tracks/fsharp/docs/INSTALLATION.md +2 -3
  9. data/tracks/fsharp/docs/TESTS.md +1 -1
  10. data/tracks/fsharp/exercises/accumulate/HINTS.md +3 -0
  11. data/tracks/fsharp/exercises/diamond/HINTS.md +2 -0
  12. data/tracks/fsharp/exercises/grains/HINTS.md +1 -1
  13. data/tracks/fsharp/exercises/hello-world/HINTS.md +1 -1
  14. data/tracks/fsharp/exercises/parallel-letter-frequency/HINTS.md +3 -0
  15. data/tracks/fsharp/exercises/poker/HINTS.md +2 -0
  16. data/tracks/fsharp/exercises/raindrops/HINTS.md +2 -0
  17. data/tracks/fsharp/exercises/space-age/HINTS.md +3 -0
  18. data/tracks/javascript/exercises/bowling/bowling.spec.js +83 -63
  19. data/tracks/ocaml/config.json +5 -0
  20. data/tracks/ocaml/exercises/leap/test.ml +7 -13
  21. data/tracks/ocaml/exercises/robot-name/.merlin +3 -0
  22. data/tracks/ocaml/exercises/robot-name/Makefile +11 -0
  23. data/tracks/ocaml/exercises/robot-name/example.ml +26 -0
  24. data/tracks/ocaml/exercises/robot-name/robot_name.mli +7 -0
  25. data/tracks/ocaml/exercises/robot-name/test.ml +64 -0
  26. data/tracks/ocaml/tools/test-generator/Makefile +3 -0
  27. data/tracks/ocaml/tools/test-generator/README.md +1 -0
  28. data/tracks/ocaml/tools/test-generator/src/canonical_data_checker.ml +23 -0
  29. data/tracks/ocaml/tools/test-generator/src/controller.ml +12 -0
  30. data/tracks/ocaml/tools/test-generator/src/parser.ml +29 -22
  31. data/tracks/ocaml/tools/test-generator/src/utils.ml +8 -0
  32. data/tracks/ocaml/tools/test-generator/test/clock.json +437 -0
  33. data/tracks/ocaml/tools/test-generator/test/parser_test.ml +18 -4
  34. data/tracks/ocaml/tools/test-generator/test/with-methods-key.json +22 -0
  35. data/tracks/perl6/config.json +5 -0
  36. data/tracks/perl6/exercises/wordy/Example.p6 +16 -0
  37. data/tracks/perl6/exercises/wordy/cases.json +89 -0
  38. data/tracks/perl6/exercises/wordy/wordy.t +29 -0
  39. data/tracks/ruby/exercises/acronym/{.version → .meta/.version} +0 -0
  40. data/tracks/ruby/exercises/alphametics/{.version → .meta/.version} +0 -0
  41. data/tracks/ruby/exercises/anagram/{.version → .meta/.version} +0 -0
  42. data/tracks/ruby/exercises/binary/{.version → .meta/.version} +0 -0
  43. data/tracks/ruby/exercises/bowling/{.version → .meta/.version} +0 -0
  44. data/tracks/ruby/exercises/bracket-push/{.version → .meta/.version} +0 -0
  45. data/tracks/ruby/exercises/clock/{.version → .meta/.version} +0 -0
  46. data/tracks/ruby/exercises/connect/{.version → .meta/.version} +0 -0
  47. data/tracks/ruby/exercises/custom-set/{.version → .meta/.version} +0 -0
  48. data/tracks/ruby/exercises/difference-of-squares/{.version → .meta/.version} +0 -0
  49. data/tracks/ruby/exercises/dominoes/{.version → .meta/.version} +0 -0
  50. data/tracks/ruby/exercises/gigasecond/{.version → .meta/.version} +0 -0
  51. data/tracks/ruby/exercises/hamming/{.version → .meta/.version} +0 -0
  52. data/tracks/ruby/exercises/hello-world/{.version → .meta/.version} +0 -0
  53. data/tracks/ruby/exercises/isogram/{.version → .meta/.version} +0 -0
  54. data/tracks/ruby/exercises/largest-series-product/{.version → .meta/.version} +0 -0
  55. data/tracks/ruby/exercises/leap/{.version → .meta/.version} +0 -0
  56. data/tracks/ruby/exercises/nth-prime/{.version → .meta/.version} +0 -0
  57. data/tracks/ruby/exercises/pangram/{.version → .meta/.version} +0 -0
  58. data/tracks/ruby/exercises/queen-attack/{.version → .meta/.version} +0 -0
  59. data/tracks/ruby/exercises/raindrops/{.version → .meta/.version} +0 -0
  60. data/tracks/ruby/exercises/rna-transcription/{.version → .meta/.version} +0 -0
  61. data/tracks/ruby/exercises/roman-numerals/{.version → .meta/.version} +0 -0
  62. data/tracks/ruby/exercises/run-length-encoding/{.version → .meta/.version} +0 -0
  63. data/tracks/ruby/exercises/sieve/{.version → .meta/.version} +0 -0
  64. data/tracks/ruby/exercises/tournament/{.version → .meta/.version} +0 -0
  65. data/tracks/ruby/exercises/transpose/{.version → .meta/.version} +0 -0
  66. data/tracks/ruby/exercises/triangle/{.version → .meta/.version} +0 -0
  67. data/tracks/ruby/exercises/two-bucket/{.version → .meta/.version} +0 -0
  68. data/tracks/ruby/exercises/word-count/{.version → .meta/.version} +0 -0
  69. data/tracks/ruby/lib/generator.rb +15 -4
  70. data/tracks/rust/config.json +1 -0
  71. data/tracks/rust/exercises/largest-series-product/.gitignore +7 -0
  72. data/tracks/rust/exercises/largest-series-product/Cargo.toml +3 -0
  73. data/tracks/rust/exercises/largest-series-product/HINTS.md +6 -0
  74. data/tracks/rust/exercises/largest-series-product/example.rs +22 -0
  75. data/tracks/rust/exercises/largest-series-product/tests/largest-series-product.rs +105 -0
  76. data/tracks/rust/problems.md +1 -0
  77. data/tracks/scala/.gitignore +1 -0
  78. data/tracks/scala/exercises/accumulate/src/test/scala/{accumulate_test.scala → AccumulateTest.scala} +0 -0
  79. data/tracks/scala/exercises/allergies/src/test/scala/{allergies_test.scala → AllergiesTest.scala} +0 -0
  80. data/tracks/scala/exercises/anagram/src/test/scala/{anagram_test.scala → AnagramTest.scala} +0 -0
  81. data/tracks/scala/exercises/binary/src/test/scala/{binary_test.scala → BinaryTest.scala} +0 -0
  82. data/tracks/scala/exercises/bob/src/test/scala/{bob_test.scala → BobTest.scala} +0 -0
  83. data/tracks/scala/exercises/difference-of-squares/src/test/scala/{squares_test.scala → SquaresTest.scala} +0 -0
  84. data/tracks/scala/exercises/etl/src/test/scala/{etl_test.scala → EtlTest.scala} +0 -0
  85. data/tracks/scala/exercises/gigasecond/src/test/scala/{gigasecond_test.scala → GigasecondTest.scala} +0 -0
  86. data/tracks/scala/exercises/grade-school/src/test/scala/{grade_school_test.scala → GradeSchoolTest.scala} +0 -0
  87. data/tracks/scala/exercises/grains/src/test/scala/{grains_test.scala → GrainsTest.scala} +0 -0
  88. data/tracks/scala/exercises/hamming/src/test/scala/{hamming_test.scala → HammingTest.scala} +0 -0
  89. data/tracks/scala/exercises/hello-world/src/test/scala/HelloWorldTest.scala +2 -0
  90. data/tracks/scala/exercises/leap/src/test/scala/{leap_test.scala → LeapTest.scala} +0 -0
  91. data/tracks/scala/exercises/meetup/src/test/scala/{meetup_test.scala → MeetupTest.scala} +0 -0
  92. data/tracks/scala/exercises/nth-prime/example.scala +3 -1
  93. data/tracks/scala/exercises/nth-prime/src/test/scala/PrimeTest.scala +11 -6
  94. data/tracks/scala/exercises/nucleotide-count/src/test/scala/{nucleotide_count_test.scala → NucleotideCountTest.scala} +0 -0
  95. data/tracks/scala/exercises/palindrome-products/src/test/scala/PalindromeProductsTest.scala +7 -0
  96. data/tracks/scala/exercises/pangram/src/test/scala/{PangramsTest.scala → PangramTest.scala} +0 -0
  97. data/tracks/scala/exercises/parallel-letter-frequency/HINTS.md +26 -0
  98. data/tracks/scala/exercises/phone-number/src/test/scala/{phone_number_test.scala → PhoneNumberTest.scala} +0 -0
  99. data/tracks/scala/exercises/prime-factors/src/test/scala/{primefactors_test.scala → PrimefactorsTest.scala} +10 -0
  100. data/tracks/scala/exercises/raindrops/src/test/scala/{raindrops_test.scala → RaindropsTest.scala} +15 -0
  101. data/tracks/scala/exercises/rna-transcription/src/test/scala/{transcription_test.scala → RnaTranscriptionTest.scala} +8 -0
  102. data/tracks/scala/exercises/robot-name/src/test/scala/{robot_name_test.scala → RobotNameTest.scala} +0 -0
  103. data/tracks/scala/exercises/roman-numerals/src/test/scala/{roman_numerals_test.scala → RomanNumeralsTest.scala} +17 -0
  104. data/tracks/scala/exercises/saddle-points/src/test/scala/{SaddlePointsSpecs.scala → SaddlePointsTest.scala} +3 -0
  105. data/tracks/scala/exercises/scrabble-score/src/test/scala/{scrabble_score_test.scala → ScrabbleScoreTest.scala} +0 -0
  106. data/tracks/scala/exercises/sgf-parsing/src/test/scala/SgfTest.scala +1 -0
  107. data/tracks/scala/exercises/space-age/src/test/scala/{space_age_test.scala → SpaceAgeTest.scala} +0 -0
  108. data/tracks/scala/exercises/sublist/src/test/scala/{sublist_test.scala → SublistTest.scala} +0 -0
  109. data/tracks/scala/exercises/triangle/src/test/scala/{triangle_test.scala → TriangleTest.scala} +7 -0
  110. data/tracks/scala/exercises/word-count/src/test/scala/{word_count_test.scala → WordCountTest.scala} +0 -0
  111. data/tracks/scala/exercises/zipper/src/test/scala/ZipperTest.scala +7 -0
  112. data/tracks/swift/config.json +19 -0
  113. data/tracks/swift/docs/TESTS.md +114 -23
  114. data/tracks/swift/exercises/beer-song/BeerSongExample.swift +32 -0
  115. data/tracks/swift/exercises/beer-song/BeerSongTest.swift +44 -0
  116. data/tracks/swift/exercises/hello-world/{helloWorldExample.swift → HelloWorldExample.swift} +4 -0
  117. data/tracks/swift/exercises/hello-world/{helloWorldTest/helloWorldTest.swift → helloWorldTest.swift} +1 -1
  118. data/tracks/swift/exercises/sublist/SubListTest.swift +95 -0
  119. data/tracks/swift/exercises/sublist/SublistExample.swift +72 -0
  120. data/tracks/swift/img/page_assets/001-splash.png +0 -0
  121. data/tracks/swift/img/page_assets/002-templateChooser.png +0 -0
  122. data/tracks/swift/img/page_assets/003-nameProject.jpg +0 -0
  123. data/tracks/swift/img/page_assets/004-saveProject.jpg +0 -0
  124. data/tracks/swift/img/page_assets/005-folderLayout.png +0 -0
  125. data/tracks/swift/img/page_assets/006-newProjectInitial.jpg +0 -0
  126. data/tracks/swift/img/page_assets/007-fileInspectorUpdate.png +0 -0
  127. data/tracks/swift/img/page_assets/008-templateChooserSwift.png +0 -0
  128. data/tracks/swift/img/page_assets/009-importTestSource.png +0 -0
  129. data/tracks/swift/img/page_assets/010-testsImportExample.png +0 -0
  130. data/tracks/swift/img/page_assets/011-finalLayoutExample.png +0 -0
  131. data/tracks/swift/xcodeProject/xSwift.xcodeproj/project.pbxproj +36 -56
  132. metadata +103 -65
  133. data/tracks/swift/exercises/hello-world/helloWorld.swift +0 -1
  134. data/tracks/swift/exercises/hello-world/helloWorld.xcodeproj/project.pbxproj +0 -256
  135. data/tracks/swift/exercises/hello-world/helloWorld.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  136. data/tracks/swift/exercises/hello-world/helloWorldTest/Info.plist +0 -24
@@ -0,0 +1,3 @@
1
+ S *
2
+ B _build/**
3
+ PKG core oUnit
@@ -0,0 +1,11 @@
1
+ test: test.native
2
+ @./test.native
3
+
4
+ test.native: *.ml *.mli
5
+ @corebuild -r -quiet -pkg oUnit test.native
6
+
7
+ clean:
8
+ rm -rf _build
9
+ rm -f test.native
10
+
11
+ .PHONY: clean
@@ -0,0 +1,26 @@
1
+ open Core.Std
2
+
3
+ type robot = {mutable index : int}
4
+
5
+ let index = ref (-1)
6
+
7
+ let unique_ids: int array =
8
+ let ids = Array.init (26*26*1000) ~f:Fn.id in
9
+ Array.permute ids;
10
+ ids
11
+
12
+ let new_robot () =
13
+ index := !index + 1;
14
+ {index = unique_ids.(!index)}
15
+
16
+ let name r =
17
+ let n = r.index in
18
+ let letters_part = n / 1000 in
19
+ let letter_A = Char.to_int 'A' in
20
+ let first_letter = Char.of_int_exn (letter_A + (letters_part / 26)) in
21
+ let second_letter = Char.of_int_exn (letter_A + (letters_part % 26)) in
22
+ sprintf "%c%c%03d" first_letter second_letter (n % 1000)
23
+
24
+ let reset r =
25
+ index := !index + 1;
26
+ r.index <- unique_ids.(!index);
@@ -0,0 +1,7 @@
1
+ type robot
2
+
3
+ val new_robot : unit -> robot
4
+
5
+ val name : robot -> string
6
+
7
+ val reset : robot -> unit
@@ -0,0 +1,64 @@
1
+ open Core.Std
2
+ open OUnit2
3
+ open Robot_name
4
+
5
+ let assert_matches_spec name =
6
+ let is_valid_letter ch = 'A' <= ch && ch <= 'Z' in
7
+ let is_valid_digit ch = '0' <= ch && ch <= '9' in
8
+ assert_equal ~printer:Int.to_string 5 (String.length name);
9
+ assert_bool ("First character must be from A to Z") (is_valid_letter @@ name.[0]);
10
+ assert_bool ("Second character must be from A to Z") (is_valid_letter @@ name.[1]);
11
+ assert_bool ("Third character must be from 0 to 9") (is_valid_digit @@ name.[2]);
12
+ assert_bool ("Fourth character must be from 0 to 9") (is_valid_digit @@ name.[3]);
13
+ assert_bool ("Fifth character must be from 0 to 9") (is_valid_digit @@ name.[4]);;
14
+
15
+ let basic_tests = [
16
+ "a robot has a name of 2 letters followed by 3 numbers" >:: (fun _ctxt ->
17
+ let n = name (new_robot ()) in
18
+ assert_matches_spec n
19
+ );
20
+
21
+ "resetting a robot's name gives it a different name" >:: (fun _ctxt ->
22
+ let r = new_robot () in
23
+ let n1 = name r in
24
+ reset r;
25
+ let n2 = name r in
26
+ assert_bool ("'" ^ n1 ^ "' was repeated") (n1 <> n2)
27
+ );
28
+
29
+ "after reset the robot's name still matches the specification" >:: (fun _ctxt ->
30
+ let r = new_robot () in
31
+ reset r;
32
+ let n = name r in
33
+ assert_matches_spec n
34
+ );
35
+ ]
36
+
37
+ (*
38
+ Optionally: make this test pass.
39
+
40
+ There are 26 * 26 * 10 * 10 * 10 = 676,000 possible Robot names.
41
+ This test generates all possible Robot names, and checks that there are
42
+ no duplicates. It's harder to make pass than the other tests, so it is left
43
+ as optional.
44
+
45
+ To enable it, uncomment the code in the run_test_tt_main
46
+ line at the bottom of this module.
47
+ *)
48
+ let unique_name_tests = [
49
+ "all possible robot names are distinct" >:: (fun _ctxt ->
50
+ let rs = Array.init (26 * 26 * 1000) ~f:(fun _ -> new_robot ()) in
51
+ let (repeated, _) = Array.fold rs ~init:(String.Set.empty, String.Set.empty) ~f:(fun (repeated, seen) r ->
52
+ let n = name r in
53
+ if Set.mem seen n
54
+ then (Set.add repeated n, seen)
55
+ else (repeated, Set.add seen n)
56
+ ) in
57
+ let first_few_repeats = Array.slice (Set.to_array repeated) 0 (min 20 (Set.length repeated)) in
58
+ let failure_message = "first few repeats: " ^ (String.concat_array first_few_repeats ~sep:",") in
59
+ assert_bool failure_message (Set.is_empty repeated)
60
+ );
61
+ ]
62
+
63
+ let () =
64
+ run_test_tt_main ("robot-name tests" >::: List.concat [basic_tests (* ; unique_name_tests *)])
@@ -1,6 +1,9 @@
1
1
  test: test_gen.native
2
2
  @./all_tests.native
3
3
 
4
+ canonical_data_checker.native: all_tests.native src/*.ml interfaces/*.mli test/*.ml
5
+ @ocamlbuild -use-ocamlfind -tag thread -tag short_paths -cflags -strict-sequence -r -pkg core -pkg yojson -pkg ppx_deriving -pkg ppx_deriving.eq -pkg ppx_deriving.show -Is src,interfaces canonical_data_checker.native
6
+
4
7
  test_gen.byte: all_tests.native src/*.ml interfaces/*.mli test/*.ml
5
8
  @ocamlbuild -use-ocamlfind -tag thread -tag short_paths -cflags -strict-sequence -r -pkg core -pkg yojson -pkg ppx_deriving -pkg ppx_deriving.eq -pkg ppx_deriving.show -Is src,interfaces test_gen.byte
6
9
 
@@ -0,0 +1 @@
1
+ Placeholder
@@ -0,0 +1,23 @@
1
+ open Core.Std
2
+
3
+ let is_directory =
4
+ Command.Spec.Arg_type.create
5
+ (fun n ->
6
+ match Sys.is_directory n with
7
+ | `Yes -> n
8
+ | `No | `Unknown ->
9
+ eprintf "'%s' is not a regular folder.\n%!" n;
10
+ exit 1
11
+ )
12
+
13
+ let command =
14
+ Command.basic
15
+ ~summary:"Reports errors in canonical data."
16
+ Command.Spec.(
17
+ empty
18
+ +> flag "-c" (optional_with_default "../../../x-common/exercises" is_directory) ~doc:"string Directory containing canonical data."
19
+ )
20
+ (fun canonical_data_folder () -> Controller.check_canonical_data canonical_data_folder)
21
+
22
+ let () =
23
+ Command.run ~version:"0.1" command
@@ -53,3 +53,15 @@ let run ~(templates_folder: string) ~(canonical_data_folder: string) ~(output_fo
53
53
  let canonical_data_files = find_canonical_data_files canonical_data_folder in
54
54
  let combined = combine_files template_files canonical_data_files in
55
55
  output_tests combined output_folder
56
+
57
+ let check_canonical_data canonical_data_folder =
58
+ let ok_count = ref 0 in
59
+ let canonical_data_files = find_canonical_data_files canonical_data_folder in
60
+ let canonical_data_files = List.sort canonical_data_files ~cmp:(fun (s1, _) (s2, _) -> String.compare s1 s2) in
61
+ let total_count = List.length canonical_data_files in
62
+ List.iter canonical_data_files ~f:(fun (slug, text) ->
63
+ match parse_json_text text with
64
+ | Error e -> print_endline @@ slug ^ ": " ^ (show_error e)
65
+ | _ -> ok_count := !ok_count + 1
66
+ );
67
+ print_endline @@ "There are " ^ (Int.to_string total_count) ^ " exercises with canonical data, " ^ (Int.to_string !ok_count) ^ " can be parsed."
@@ -5,19 +5,22 @@ open Yojson.Safe.Util
5
5
  open Model
6
6
 
7
7
  type error =
8
- TestMustHaveKeyCalledCases | ExpectingListOfCases | ExpectingMapForCase |
9
- BadDescription | BadExpected | UnrecognizedJson [@@deriving eq, show]
8
+ TestMustHaveKeyCalledCases of string | ExpectingListOfCases | ExpectingMapForCase |
9
+ NoDescription | BadDescription | NoExpected of string | BadExpected | UnrecognizedJson [@@deriving eq, show]
10
10
 
11
- let to_int_unsafe = function
12
- | `Int x -> x
13
- | _ -> failwith "need an int here"
11
+ let to_int_safe = function
12
+ | `Int x -> Some x
13
+ | _ -> None
14
14
 
15
- let to_list_safe xs = match xs with
15
+ let to_list_safe xs = let open Option.Monad_infix in match xs with
16
16
  | [] -> Some (StringList [])
17
17
  | `String x :: _ -> Some (StringList (List.map xs ~f:to_string))
18
- | `Int x :: _ -> Some (IntList (List.map xs ~f:to_int_unsafe))
18
+ | `Int x :: _ -> List.map xs ~f:to_int_safe |> sequence_option >>= (fun xs -> Some (IntList xs))
19
19
  | _ -> None
20
20
 
21
+ let q xs = let open Option.Monad_infix in
22
+ List.map xs ~f:(fun (k,v) -> (to_int_safe v |> Option.map ~f:(fun v -> (k,v))))
23
+
21
24
  let to_parameter (s: json) = match s with
22
25
  | `Null -> Some (Null)
23
26
  | `String x -> Some (String x)
@@ -25,7 +28,9 @@ let to_parameter (s: json) = match s with
25
28
  | `Int x -> Some (Int x)
26
29
  | `Bool x -> Some (Bool x)
27
30
  | `List x -> to_list_safe x
28
- | `Assoc x -> Some (IntStringMap (List.map x ~f:(fun (k,v) -> (k,to_int_unsafe v))))
31
+ | `Assoc xs -> let open Option.Monad_infix in
32
+ let xs = List.map xs ~f:(fun (k,v) -> (to_int_safe v |> Option.map ~f:(fun v -> (k,v)))) in
33
+ sequence_option xs >>= fun xs -> Some (IntStringMap xs)
29
34
  | _ -> None
30
35
 
31
36
  let parse_parameters (parameters: (string * json) list): parameter elements =
@@ -36,9 +41,9 @@ let parse_case_assoc (parameters: (string * json) list): (case, error) Result.t
36
41
  let test_parameters = List.Assoc.remove parameters "description" in
37
42
  let test_parameters = List.Assoc.remove test_parameters "expected" in
38
43
  let open Result.Monad_infix in
39
- find "description" BadDescription >>=
44
+ find "description" NoDescription >>=
40
45
  to_string_note BadDescription >>= fun description ->
41
- find "expected" BadExpected >>= fun expectedJson ->
46
+ find "expected" (NoExpected description) >>= fun expectedJson ->
42
47
  to_parameter expectedJson |> Result.of_option ~error:BadExpected >>= fun expected ->
43
48
  Ok {description = description; parameters = parse_parameters test_parameters; expected = expected}
44
49
 
@@ -48,7 +53,7 @@ let parse_case (s: json): (case, error) Result.t = match s with
48
53
 
49
54
  let parse_cases (text: string): (json, error) Result.t =
50
55
  match from_string text |> member "cases" with
51
- | `Null -> Error TestMustHaveKeyCalledCases
56
+ | `Null -> Error (TestMustHaveKeyCalledCases "xx")
52
57
  | json -> Ok json
53
58
 
54
59
  let parse_single (text: string): (tests, error) Result.t =
@@ -59,17 +64,18 @@ let parse_single (text: string): (tests, error) Result.t =
59
64
  Result.return (Single ts)
60
65
 
61
66
  let is_suite (json: json) =
62
- let keys = List.sort (keys json) ~cmp:String.compare in
67
+ let keys = List.filter (keys json) ~f:(fun k -> k <> "methods") in
68
+ let keys = List.sort keys ~cmp:String.compare in
63
69
  not (List.is_empty keys || keys = ["cases"] || keys = ["#"; "cases"])
64
70
 
65
71
  let merge_result = function
66
72
  | (_, Error x) -> Error x
67
73
  | (n, Ok c) -> Ok {name = n; cases = c}
68
74
 
69
- let parse_cases_from_suite suite =
75
+ let parse_cases_from_suite name suite =
70
76
  let open Result.Monad_infix in
71
- member_note UnrecognizedJson "cases" suite >>=
72
- to_list_note UnrecognizedJson >>= fun tests ->
77
+ member_note (TestMustHaveKeyCalledCases name) "cases" suite >>=
78
+ to_list_note ExpectingListOfCases >>= fun tests ->
73
79
  List.map tests ~f:parse_case |> sequence
74
80
 
75
81
  let parse_json_text (text: string): (tests, error) Result.t =
@@ -78,18 +84,19 @@ let parse_json_text (text: string): (tests, error) Result.t =
78
84
  if is_suite json
79
85
  then
80
86
  to_assoc_note UnrecognizedJson json >>= fun tests ->
81
- Result.return (List.map tests ~f:(fun (name, suite) -> merge_result (name, parse_cases_from_suite suite))) >>= fun tests ->
87
+ let tests = List.filter tests ~f:(fun (n, _) -> n <> "#") in
88
+ let tests = List.map tests ~f:(fun (name, suite) -> merge_result (name, parse_cases_from_suite name suite)) in
82
89
  sequence tests >>= fun tests ->
83
90
  Ok (Suite tests)
84
91
  else
85
92
  parse_single text
86
93
 
87
94
  let show_error = function
88
- | TestMustHaveKeyCalledCases -> "Cannot parse this json - " ^
89
- "expecting an object with a key: 'cases'"
95
+ | TestMustHaveKeyCalledCases name -> "Test named '" ^ name ^ "' is expected to have an object with a key: 'cases'"
90
96
  | ExpectingMapForCase -> "Expected a json map for a test case"
91
- | ExpectingListOfCases -> "Expected a top level map with key cases, " ^
92
- "and a list of cases as its value."
93
- | BadDescription -> "Case is missing a description or it is not a string."
94
- | BadExpected -> "Case is missing an expected key or it is not a string."
97
+ | ExpectingListOfCases -> "Expected a top level map with key cases, and a list of cases as its value."
98
+ | NoDescription -> "Case is missing a description."
99
+ | BadDescription -> "Description is not a string."
100
+ | NoExpected s -> "Case '" ^ s ^ "' is missing an expected key."
101
+ | BadExpected -> "Do not understand type of Expected key."
95
102
  | UnrecognizedJson -> "Cannot understand this json."
@@ -7,9 +7,17 @@ let map2 (f: 'a -> 'b -> 'c) (r1: ('a, 'e) Result.t) (r2: ('b, 'e) Result.t): ('
7
7
  | (_, Error x) -> Error x
8
8
  | (Ok a, Ok b) -> Ok (f a b)
9
9
 
10
+ let map2_option (f: 'a -> 'b -> 'c) (r1: 'a option) (r2: 'b option): 'c option = match (r1, r2) with
11
+ | (None, _) -> None
12
+ | (_, None) -> None
13
+ | (Some a, Some b) -> Some (f a b)
14
+
10
15
  let sequence (rs: (('a, 'e) Result.t) list): (('a list), 'e) Result.t =
11
16
  List.fold_right rs ~init:(Ok []) ~f:(map2 (fun x xs -> x :: xs))
12
17
 
18
+ let sequence_option (rs: ('a option) list): ('a list) option =
19
+ List.fold_right rs ~init:(Some []) ~f:(map2_option (fun x xs -> x :: xs))
20
+
13
21
  let to_list_option json =
14
22
  try Some (to_list json) with Type_error _ -> None
15
23
 
@@ -0,0 +1,437 @@
1
+
2
+ {
3
+ "#": [
4
+ "Most languages require constructing a clock with initial values,",
5
+ "adding a positive or negative number of minutes, and testing equality",
6
+ "in some language-native way. Some languages require separate add and",
7
+ "subtract functions. Negative and out of range values are generally",
8
+ "expected to wrap around rather than represent errors."
9
+ ],
10
+ "create": {
11
+ "description": [
12
+ "Test creating a new clock with an initial time."
13
+ ],
14
+ "cases": [
15
+ {
16
+ "description": "on the hour",
17
+ "hour": 8,
18
+ "minute": 0,
19
+ "expected": "08:00"
20
+ },
21
+ {
22
+ "description": "past the hour",
23
+ "hour": 11,
24
+ "minute": 9,
25
+ "expected": "11:09"
26
+ },
27
+ {
28
+ "description": "midnight is zero hours",
29
+ "hour": 24,
30
+ "minute": 0,
31
+ "expected": "00:00"
32
+ },
33
+ {
34
+ "description": "hour rolls over",
35
+ "hour": 25,
36
+ "minute": 0,
37
+ "expected": "01:00"
38
+ },
39
+ {
40
+ "description": "hour rolls over continuously",
41
+ "hour": 100,
42
+ "minute": 0,
43
+ "expected": "04:00"
44
+ },
45
+ {
46
+ "description": "sixty minutes is next hour",
47
+ "hour": 1,
48
+ "minute": 60,
49
+ "expected": "02:00"
50
+ },
51
+ {
52
+ "description": "minutes roll over",
53
+ "hour": 0,
54
+ "minute": 160,
55
+ "expected": "02:40"
56
+ },
57
+ {
58
+ "description": "minutes roll over continuously",
59
+ "hour": 0,
60
+ "minute": 1723,
61
+ "expected": "04:43"
62
+ },
63
+ {
64
+ "description": "hour and minutes roll over",
65
+ "hour": 25,
66
+ "minute": 160,
67
+ "expected": "03:40"
68
+ },
69
+ {
70
+ "description": "hour and minutes roll over continuously",
71
+ "hour": 201,
72
+ "minute": 3001,
73
+ "expected": "11:01"
74
+ },
75
+ {
76
+ "description": "hour and minutes roll over to exactly midnight",
77
+ "hour": 72,
78
+ "minute": 8640,
79
+ "expected": "00:00"
80
+ },
81
+ {
82
+ "description": "negative hour",
83
+ "hour": -1,
84
+ "minute": 15,
85
+ "expected": "23:15"
86
+ },
87
+ {
88
+ "description": "negative hour rolls over",
89
+ "hour": -25,
90
+ "minute": 0,
91
+ "expected": "23:00"
92
+ },
93
+ {
94
+ "description": "negative hour rolls over continuously",
95
+ "hour": -91,
96
+ "minute": 0,
97
+ "expected": "05:00"
98
+ },
99
+ {
100
+ "description": "negative minutes",
101
+ "hour": 1,
102
+ "minute": -40,
103
+ "expected": "00:20"
104
+ },
105
+ {
106
+ "description": "negative minutes roll over",
107
+ "hour": 1,
108
+ "minute": -160,
109
+ "expected": "22:20"
110
+ },
111
+ {
112
+ "description": "negative minutes roll over continuously",
113
+ "hour": 1,
114
+ "minute": -4820,
115
+ "expected": "16:40"
116
+ },
117
+ {
118
+ "description": "negative hour and minutes both roll over",
119
+ "hour": -25,
120
+ "minute": -160,
121
+ "expected": "20:20"
122
+ },
123
+ {
124
+ "description": "negative hour and minutes both roll over continuously",
125
+ "hour": -121,
126
+ "minute": -5810,
127
+ "expected": "22:10"
128
+ }
129
+ ]
130
+ },
131
+ "add": {
132
+ "description": [
133
+ "Test adding and subtracting minutes."
134
+ ],
135
+ "cases": [
136
+ {
137
+ "description": "add minutes",
138
+ "hour": 10,
139
+ "minute": 0,
140
+ "add": 3,
141
+ "expected": "10:03"
142
+ },
143
+ {
144
+ "description": "add no minutes",
145
+ "hour": 6,
146
+ "minute": 41,
147
+ "add": 0,
148
+ "expected": "06:41"
149
+ },
150
+ {
151
+ "description": "add to next hour",
152
+ "hour": 0,
153
+ "minute": 45,
154
+ "add": 40,
155
+ "expected": "01:25"
156
+ },
157
+ {
158
+ "description": "add more than one hour",
159
+ "hour": 10,
160
+ "minute": 0,
161
+ "add": 61,
162
+ "expected": "11:01"
163
+ },
164
+ {
165
+ "description": "add more than two hours with carry",
166
+ "hour": 0,
167
+ "minute": 45,
168
+ "add": 160,
169
+ "expected": "03:25"
170
+ },
171
+ {
172
+ "description": "add across midnight",
173
+ "hour": 23,
174
+ "minute": 59,
175
+ "add": 2,
176
+ "expected": "00:01"
177
+ },
178
+ {
179
+ "description": "add more than one day (1500 min = 25 hrs)",
180
+ "hour": 5,
181
+ "minute": 32,
182
+ "add": 1500,
183
+ "expected": "06:32"
184
+ },
185
+ {
186
+ "description": "add more than two days",
187
+ "hour": 1,
188
+ "minute": 1,
189
+ "add": 3500,
190
+ "expected": "11:21"
191
+ },
192
+ {
193
+ "description": "subtract minutes",
194
+ "hour": 10,
195
+ "minute": 3,
196
+ "add": -3,
197
+ "expected": "10:00"
198
+ },
199
+ {
200
+ "description": "subtract to previous hour",
201
+ "hour": 10,
202
+ "minute": 3,
203
+ "add": -30,
204
+ "expected": "09:33"
205
+ },
206
+ {
207
+ "description": "subtract more than an hour",
208
+ "hour": 10,
209
+ "minute": 3,
210
+ "add": -70,
211
+ "expected": "08:53"
212
+ },
213
+ {
214
+ "description": "subtract across midnight",
215
+ "hour": 0,
216
+ "minute": 3,
217
+ "add": -4,
218
+ "expected": "23:59"
219
+ },
220
+ {
221
+ "description": "subtract more than two hours",
222
+ "hour": 0,
223
+ "minute": 0,
224
+ "add": -160,
225
+ "expected": "21:20"
226
+ },
227
+ {
228
+ "description": "subtract more than two hours with borrow",
229
+ "hour": 6,
230
+ "minute": 15,
231
+ "add": -160,
232
+ "expected": "03:35"
233
+ },
234
+ {
235
+ "description": "subtract more than one day (1500 min = 25 hrs)",
236
+ "hour": 5,
237
+ "minute": 32,
238
+ "add": -1500,
239
+ "expected": "04:32"
240
+ },
241
+ {
242
+ "description": "subtract more than two days",
243
+ "hour": 2,
244
+ "minute": 20,
245
+ "add": -3000,
246
+ "expected": "00:20"
247
+ }
248
+ ]
249
+ },
250
+ "equal": {
251
+ "description": [
252
+ "Construct two separate clocks, set times, test if they are equal."
253
+ ],
254
+ "cases": [
255
+ {
256
+ "description": "clocks with same time",
257
+ "clock1": {
258
+ "hour": 15,
259
+ "minute": 37
260
+ },
261
+ "clock2": {
262
+ "hour": 15,
263
+ "minute": 37
264
+ },
265
+ "expected": true
266
+ },
267
+ {
268
+ "description": "clocks a minute apart",
269
+ "clock1": {
270
+ "hour": 15,
271
+ "minute": 36
272
+ },
273
+ "clock2": {
274
+ "hour": 15,
275
+ "minute": 37
276
+ },
277
+ "expected": false
278
+ },
279
+ {
280
+ "description": "clocks an hour apart",
281
+ "clock1": {
282
+ "hour": 14,
283
+ "minute": 37
284
+ },
285
+ "clock2": {
286
+ "hour": 15,
287
+ "minute": 37
288
+ },
289
+ "expected": false
290
+ },
291
+ {
292
+ "description": "clocks with hour overflow",
293
+ "clock1": {
294
+ "hour": 10,
295
+ "minute": 37
296
+ },
297
+ "clock2": {
298
+ "hour": 34,
299
+ "minute": 37
300
+ },
301
+ "expected": true
302
+ },
303
+ {
304
+ "description": "clocks with hour overflow by several days",
305
+ "clock1": {
306
+ "hour": 3,
307
+ "minute": 11
308
+ },
309
+ "clock2": {
310
+ "hour": 99,
311
+ "minute": 11
312
+ },
313
+ "expected": true
314
+ },
315
+ {
316
+ "description": "clocks with negative hour",
317
+ "clock1": {
318
+ "hour": 22,
319
+ "minute": 40
320
+ },
321
+ "clock2": {
322
+ "hour": -2,
323
+ "minute": 40
324
+ },
325
+ "expected": true
326
+ },
327
+ {
328
+ "description": "clocks with negative hour that wraps",
329
+ "clock1": {
330
+ "hour": 17,
331
+ "minute": 3
332
+ },
333
+ "clock2": {
334
+ "hour": -31,
335
+ "minute": 3
336
+ },
337
+ "expected": true
338
+ },
339
+ {
340
+ "description": "clocks with negative hour that wraps multiple times",
341
+ "clock1": {
342
+ "hour": 13,
343
+ "minute": 49
344
+ },
345
+ "clock2": {
346
+ "hour": -83,
347
+ "minute": 49
348
+ },
349
+ "expected": true
350
+ },
351
+ {
352
+ "description": "clocks with minute overflow",
353
+ "clock1": {
354
+ "hour": 0,
355
+ "minute": 1
356
+ },
357
+ "clock2": {
358
+ "hour": 0,
359
+ "minute": 1441
360
+ },
361
+ "expected": true
362
+ },
363
+ {
364
+ "description": "clocks with minute overflow by several days",
365
+ "clock1": {
366
+ "hour": 2,
367
+ "minute": 2
368
+ },
369
+ "clock2": {
370
+ "hour": 2,
371
+ "minute": 4322
372
+ },
373
+ "expected": true
374
+ },
375
+ {
376
+ "description": "clocks with negative minute",
377
+ "clock1": {
378
+ "hour": 2,
379
+ "minute": 40
380
+ },
381
+ "clock2": {
382
+ "hour": 3,
383
+ "minute": -20
384
+ },
385
+ "expected": true
386
+ },
387
+ {
388
+ "description": "clocks with negative minute that wraps",
389
+ "clock1": {
390
+ "hour": 4,
391
+ "minute": 10
392
+ },
393
+ "clock2": {
394
+ "hour": 5,
395
+ "minute": -1490
396
+ },
397
+ "expected": true
398
+ },
399
+ {
400
+ "description": "clocks with negative minute that wraps multiple times",
401
+ "clock1": {
402
+ "hour": 6,
403
+ "minute": 15
404
+ },
405
+ "clock2": {
406
+ "hour": 6,
407
+ "minute": -4305
408
+ },
409
+ "expected": true
410
+ },
411
+ {
412
+ "description": "clocks with negative hours and minutes",
413
+ "clock1": {
414
+ "hour": 7,
415
+ "minute": 32
416
+ },
417
+ "clock2": {
418
+ "hour": -12,
419
+ "minute": -268
420
+ },
421
+ "expected": true
422
+ },
423
+ {
424
+ "description": "clocks with negative hours and minutes that wrap",
425
+ "clock1": {
426
+ "hour": 18,
427
+ "minute": 7
428
+ },
429
+ "clock2": {
430
+ "hour": -54,
431
+ "minute": -11513
432
+ },
433
+ "expected": true
434
+ }
435
+ ]
436
+ }
437
+ }