trackler 2.0.3.8 → 2.0.3.9

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