···11-(* Load yaml-test-suite test cases from YAML format *)
22-open Yamle
11+(* Load yaml-test-suite test cases from data branch format *)
3243type test_case = {
54 id : string;
65 name : string;
76 yaml : string;
87 tree : string option;
99- json : string option; (* If present, indicates test should parse successfully *)
88+ json : string option;
109 fail : bool;
1110}
12111312let read_file path =
1414- let ic = open_in path in
1515- let n = in_channel_length ic in
1616- let s = really_input_string ic n in
1717- close_in ic;
1818- s
1313+ try
1414+ let ic = open_in path in
1515+ let n = in_channel_length ic in
1616+ let s = really_input_string ic n in
1717+ close_in ic;
1818+ Some s
1919+ with _ -> None
19202020-(* Convert YAML test suite visual representations to actual characters *)
2121-let convert_test_yaml yaml =
2222- let result = Buffer.create (String.length yaml) in
2323- let len = String.length yaml in
2424- let rec process i =
2525- if i >= len then ()
2626- else
2727- (* Check for multi-character sequences - must check longest first *)
2828- (* ————» = em-dash em-dash em-dash em-dash guillemet (4 spaces = tab expanded) *)
2929- if i + 14 <= len && String.sub yaml i 14 = "\xe2\x80\x94\xe2\x80\x94\xe2\x80\x94\xe2\x80\x94\xc2\xbb" then begin
3030- Buffer.add_char result '\t';
3131- process (i + 14)
3232- end
3333- (* ———» = em-dash em-dash em-dash guillemet *)
3434- else if i + 11 <= len && String.sub yaml i 11 = "\xe2\x80\x94\xe2\x80\x94\xe2\x80\x94\xc2\xbb" then begin
3535- Buffer.add_char result '\t';
3636- process (i + 11)
3737- end
3838- (* ——» = em-dash em-dash guillemet *)
3939- else if i + 8 <= len && String.sub yaml i 8 = "\xe2\x80\x94\xe2\x80\x94\xc2\xbb" then begin
4040- Buffer.add_char result '\t';
4141- process (i + 8)
4242- end
4343- (* —» = em-dash guillemet *)
4444- else if i + 5 <= len && String.sub yaml i 5 = "\xe2\x80\x94\xc2\xbb" then begin
4545- Buffer.add_char result '\t';
4646- process (i + 5)
4747- end
4848- (* » = guillemet alone *)
4949- else if i + 2 <= len && String.sub yaml i 2 = "\xc2\xbb" then begin
5050- Buffer.add_char result '\t';
5151- process (i + 2)
5252- end
5353- (* ␣ = open box for trailing space *)
5454- else if i + 3 <= len && String.sub yaml i 3 = "\xe2\x90\xa3" then begin
5555- Buffer.add_char result ' ';
5656- process (i + 3)
5757- end
5858- (* ← = leftwards arrow for carriage return *)
5959- else if i + 3 <= len && String.sub yaml i 3 = "\xe2\x86\x90" then begin
6060- Buffer.add_char result '\r';
6161- process (i + 3)
6262- end
6363- (* ⇔ = left-right double arrow for BOM *)
6464- else if i + 3 <= len && String.sub yaml i 3 = "\xe2\x87\x94" then begin
6565- Buffer.add_string result "\xEF\xBB\xBF";
6666- process (i + 3)
6767- end
6868- (* ↵ = up-down arrow for explicit newline.
6969- This represents a newline in the output AND replaces the following actual newline
7070- (since each ↵ is on its own line in the test file's yaml field). *)
7171- else if i + 3 <= len && String.sub yaml i 3 = "\xe2\x86\xb5" then begin
7272- Buffer.add_char result '\n';
7373- (* Skip the following newline if present (it's part of the test file structure, not content) *)
7474- let next_i = i + 3 in
7575- if next_i < len && yaml.[next_i] = '\n' then
7676- process (next_i + 1)
7777- else
7878- process next_i
7979- end
8080- (* ∎ = end-of-proof symbol for empty stream *)
8181- else if i + 3 <= len && String.sub yaml i 3 = "\xe2\x88\x8e" then begin
8282- (* Skip this - it represents an empty file, so we add nothing *)
8383- process (i + 3)
8484- end
8585- else begin
8686- Buffer.add_char result yaml.[i];
8787- process (i + 1)
8888- end
8989- in
9090- process 0;
9191- Buffer.contents result
2121+let read_file_required path =
2222+ match read_file path with
2323+ | Some s -> s
2424+ | None -> ""
92259393-(* Extract a field value from parsed YAML events *)
9494-let extract_mapping_value events key =
9595- let rec find_key = function
9696- | [] -> None
9797- | { Event.event = Event.Scalar { value; _ }; _ } :: rest when value = key ->
9898- (* Found the key, now get the value *)
9999- (match rest with
100100- | { Event.event = Event.Scalar { value; _ }; _ } :: _ -> Some value
101101- | _ -> None)
102102- | _ :: rest -> find_key rest
103103- in
104104- find_key events
2626+let file_exists path =
2727+ Sys.file_exists path
10528106106-(* Parse a single test case from a mapping *)
107107-let parse_test_case id events =
108108- let name = match extract_mapping_value events "name" with
109109- | Some n -> n
110110- | None -> id
111111- in
112112- let yaml = match extract_mapping_value events "yaml" with
113113- | Some y -> convert_test_yaml y
114114- | None -> ""
115115- in
116116- let tree = extract_mapping_value events "tree" in
117117- let json = extract_mapping_value events "json" in
118118- let fail = match extract_mapping_value events "fail" with
119119- | Some "true" -> true
120120- | _ -> Option.is_some (extract_mapping_value events "error")
121121- in
122122- { id; name; yaml; tree; json; fail }
2929+let is_directory path =
3030+ Sys.file_exists path && Sys.is_directory path
12331124124-(* Load tests from a single YAML file *)
125125-let load_file path =
126126- let id = Filename.chop_extension (Filename.basename path) in
127127- try
128128- let content = read_file path in
129129- let parser = Parser.of_string content in
130130- let events = Parser.to_list parser in
3232+(* Load a single test from a directory *)
3333+let load_test_dir base_id dir_path =
3434+ let name_file = Filename.concat dir_path "===" in
3535+ let yaml_file = Filename.concat dir_path "in.yaml" in
3636+ let tree_file = Filename.concat dir_path "test.event" in
3737+ let json_file = Filename.concat dir_path "in.json" in
3838+ let error_file = Filename.concat dir_path "error" in
13139132132- (* File contains a sequence of test cases *)
133133- let tests = ref [] in
134134- let current_events = ref [] in
135135- let in_mapping = ref false in
136136- let depth = ref 0 in
137137- let test_index = ref 0 in
4040+ (* Must have in.yaml to be a valid test *)
4141+ if not (file_exists yaml_file) then None
4242+ else
4343+ let name = match read_file name_file with
4444+ | Some s -> String.trim s
4545+ | None -> base_id
4646+ in
4747+ let yaml = read_file_required yaml_file in
4848+ let tree = read_file tree_file in
4949+ let json = read_file json_file in
5050+ let fail = file_exists error_file in
5151+ Some { id = base_id; name; yaml; tree; json; fail }
13852139139- List.iter (fun (e : Event.spanned) ->
140140- match e.event with
141141- | Event.Mapping_start _ when !depth = 1 ->
142142- in_mapping := true;
143143- current_events := [e];
144144- incr depth
145145- | Event.Mapping_end when !depth = 2 ->
146146- current_events := e :: !current_events;
147147- let test_id = if !test_index = 0 then id else Printf.sprintf "%s/%02d" id !test_index in
148148- let test = parse_test_case test_id (List.rev !current_events) in
149149- if test.yaml <> "" then tests := test :: !tests;
150150- in_mapping := false;
151151- current_events := [];
152152- incr test_index;
153153- decr depth
154154- | _ when !in_mapping ->
155155- current_events := e :: !current_events;
156156- (match e.event with
157157- | Event.Mapping_start _ | Event.Sequence_start _ -> incr depth
158158- | Event.Mapping_end | Event.Sequence_end -> decr depth
159159- | _ -> ())
160160- | Event.Sequence_start _ when !depth = 0 -> depth := 1
161161- | Event.Sequence_end when !depth = 1 -> depth := 0
162162- | _ -> ()
163163- ) events;
5353+(* Load tests from a test ID directory (may have subdirectories for variants) *)
5454+let load_test_id test_suite_path test_id =
5555+ let dir_path = Filename.concat test_suite_path test_id in
5656+ if not (is_directory dir_path) then []
5757+ else
5858+ (* Check if this directory has variant subdirectories (00, 01, etc.) *)
5959+ let entries = Sys.readdir dir_path in
6060+ let has_variants = Array.exists (fun e ->
6161+ let subdir = Filename.concat dir_path e in
6262+ is_directory subdir &&
6363+ String.length e >= 2 &&
6464+ e.[0] >= '0' && e.[0] <= '9'
6565+ ) entries in
16466165165- List.rev !tests
166166- with _ -> []
6767+ if has_variants then
6868+ (* Load each variant subdirectory *)
6969+ let variants = Array.to_list entries
7070+ |> List.filter (fun e ->
7171+ let subdir = Filename.concat dir_path e in
7272+ is_directory subdir && String.length e >= 2 && e.[0] >= '0' && e.[0] <= '9')
7373+ |> List.sort String.compare
7474+ in
7575+ List.filter_map (fun variant ->
7676+ let variant_path = Filename.concat dir_path variant in
7777+ let variant_id = Printf.sprintf "%s:%s" test_id variant in
7878+ load_test_dir variant_id variant_path
7979+ ) variants
8080+ else
8181+ (* Single test in this directory *)
8282+ match load_test_dir test_id dir_path with
8383+ | Some t -> [t]
8484+ | None -> []
16785168168-let load_directory src_path =
169169- let entries = Sys.readdir src_path in
170170- let tests = ref [] in
171171- Array.iter (fun entry ->
172172- if Filename.check_suffix entry ".yaml" then begin
173173- let path = Filename.concat src_path entry in
174174- let file_tests = load_file path in
175175- tests := file_tests @ !tests
176176- end
177177- ) entries;
178178- List.sort (fun a b -> String.compare a.id b.id) !tests
8686+let load_directory test_suite_path =
8787+ if not (is_directory test_suite_path) then []
8888+ else
8989+ let entries = Sys.readdir test_suite_path in
9090+ let test_ids = Array.to_list entries
9191+ |> List.filter (fun e ->
9292+ is_directory (Filename.concat test_suite_path e) &&
9393+ String.length e >= 4 && (* Test IDs are 4 chars *)
9494+ e.[0] >= '0' && e.[0] <= 'Z') (* Start with alphanumeric *)
9595+ |> List.sort String.compare
9696+ in
9797+ List.concat_map (load_test_id test_suite_path) test_ids