123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155(** Convert a parser event list to the yaml-test-suite tree notation. The tree
notation is a human-readable, line-by-line representation of the event
stream, used by the test suite to specify expected output. *)(*
Notation summary
~~~~~~~~~~~~~~~~
+STR stream start
-STR stream end
+DOC [---] document start (with or without explicit marker)
-DOC [...] document end (with or without explicit marker)
+MAP [{}] mapping start (flow=true; optionally &anchor and tag URI)
-MAP mapping end
+SEQ [[]] sequence start (flow=true; optionally &anchor and tag URI)
-SEQ sequence end
=VAL [&anchor] [<tag>] STYLE VALUE
where STYLE is one of:
: plain
' single-quoted
'"' double-quoted
| literal block
> folded block
=ALI *name alias
The VALUE in =VAL lines has special characters escaped:
actual newline -> \n
actual tab -> \t
actual backslash -> \\
The tree lines in the test suite have leading spaces for visual nesting;
we strip those when comparing (see [normalize_tree]).
*)openTypes(* ------------------------------------------------------------------ *)(* Value escaping *)(* ------------------------------------------------------------------ *)(** Escape a scalar value string for display in the tree format. Newlines, tabs,
backslashes, and carriage returns are made visible. Trailing spaces are
replaced with U+2423 OPEN BOX (␣) per the yaml-test-suite convention, so
they are not invisible in the output. *)letescape_value(s:string):string=letbuf=Buffer.create(String.lengths)inString.iter(func->matchcwith|'\n'->Buffer.add_stringbuf"\\n"|'\t'->Buffer.add_stringbuf"\\t"|'\r'->Buffer.add_stringbuf"\\r"|'\\'->Buffer.add_stringbuf"\\\\"|'\x08'->Buffer.add_stringbuf"\\b"|_->Buffer.add_charbufc)s;(* Replace trailing ASCII spaces with U+2423 (OPEN BOX = ␣) *)letescaped=Buffer.contentsbufinletn=String.lengthescapedinleti=refninwhile!i>0&&escaped.[!i-1]=' 'dodecridone;if!i=nthenescapedelsebeginletbuf2=Buffer.create(n+((!i-n)*2))inBuffer.add_stringbuf2(String.subescaped0!i);for_=!i+1tondoBuffer.add_stringbuf2"\xE2\x90\xA3"(* U+2423 ␣ *)done;Buffer.contentsbuf2end(* ------------------------------------------------------------------ *)(* Style prefix *)(* ------------------------------------------------------------------ *)letstyle_prefix=function|Plain->":"|Single_quoted->"'"|Double_quoted->"\""|Literal->"|"|Folded->">"(* ------------------------------------------------------------------ *)(* Anchor and tag formatting *)(* ------------------------------------------------------------------ *)letformat_anchor=function|None->""|Somea->" &"^aletformat_tag=function|None->""|Somet->" <"^t^">"(* ------------------------------------------------------------------ *)(* Event to tree line(s) *)(* ------------------------------------------------------------------ *)(** Render a single event as a tree-format line. Returns one string per event.
*)letevent_to_line(ev:event):string=matchev.kindwith|Stream_start->"+STR"|Stream_end->"-STR"|Document_start{explicit;_}->ifexplicitthen"+DOC ---"else"+DOC"|Document_end{explicit}->ifexplicitthen"-DOC ..."else"-DOC"|Mapping_start{anchor;tag;flow;_}->letflow_marker=ifflowthen" {}"else""in"+MAP"^flow_marker^format_anchoranchor^format_tagtag|Mapping_end->"-MAP"|Sequence_start{anchor;tag;flow;_}->letflow_marker=ifflowthen" []"else""in"+SEQ"^flow_marker^format_anchoranchor^format_tagtag|Sequence_end->"-SEQ"|Scalar{anchor;tag;value;style}->"=VAL"^format_anchoranchor^format_tagtag^" "^style_prefixstyle^escape_valuevalue|Aliasname->"=ALI *"^name(** Convert an event list to the multi-line tree string. Each event becomes one
line; lines are separated by newlines. The result ends with a final newline.
*)letto_tree(events:eventlist):string=letlines=List_ext.mapevent_to_lineeventsinString.concat"\n"lines^"\n"(* ------------------------------------------------------------------ *)(* Comparison helpers *)(* ------------------------------------------------------------------ *)(** Split [s] into trimmed, non-empty lines for comparison. Leading whitespace
(the visual indentation in the test suite's tree format) is stripped since
it carries no semantic meaning. *)letnormalize_tree(s:string):stringlist=String.split_on_char'\n's|>List_ext.mapString.trim|>List_ext.filter(funl->l<>"")(** Return a human-readable diff between [expected] and [actual] tree strings.
Only lists the first differing line. *)letdiff_trees~expected~actual:stringoption=letexp_lines=normalize_treeexpectedinletact_lines=normalize_treeactualinletrecgoi=function|[],[]->None|e::es,a::ax->ife=athengo(i+1)(es,ax)elseSome(Printf.sprintf"line %d: expected %S, got %S"(i+1)ea)|e::_,[]->Some(Printf.sprintf"line %d: expected %S, got <end>"(i+1)e)|[],a::_->Some(Printf.sprintf"line %d: expected <end>, got %S"(i+1)a)ingo1(exp_lines,act_lines)