123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196(*
* Copyright (c) 2018 Thomas Gazagnaire <thomas@gazagnaire.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*)letsrc=Logs.Src.create"ocaml-mdx"moduleLog=(valLogs.src_logsrc:Logs.LOG)open Astringtype t={command:stringlist;output:Output.tlist;exit_code:int}typecram_tests={start_pad:int;hpad:int;tests:tlist;end_pad:string option;}letdump_line ppf =function|#Output.taso->Output.dumpppfo|`Exiti->Fmt.pfppf"`Exit %d"i|`Commandc->Fmt.pfppf"`Command %S"c|`Command_firstc->Fmt.pfppf"`Command_first %S"c|`Command_contc->Fmt.pfppf"`Command_cont %S"c|`Command_lastc->Fmt.pfppf"`Command_last %S"cletdumpppf{command;output;exit_code}=Fmt.pfppf"{@[command: %a;@ output: %a;@ exit_code: %d]}"Fmt.Dump.(liststring)command(Fmt.Dump.listOutput.dump)outputexit_codeletrecpp_vertical_pad ppf=function|0->()|n->Fmt.pfppf"\n";pp_vertical_pad ppf(Int.predn)letpp_command?(pad=0)ppf(t:t)=matcht.commandwith|[]->()|l->letsepppf()=Fmt.pfppf"\\\n%a> "Pp.pp_padpadinFmt.pfppf"%a$ %a"Pp.pp_padpadFmt.(list~sepstring)lletpp_exit_code?(pad=0)ppf=function|0->()|n->Fmt.pfppf"\n%a[%d]"Pp.pp_padpadnletpp?padppf(t:t)=pp_command ?padppft;Fmt.stringppf"\n";Pp.pp_lines(Output.pp?pad)ppft.output;pp_exit_code?padppft.exit_codelethpad_of_lines=function|[]->0|h::_->leti=ref0inwhile!i<String.lengthh&&h.[!i]=' 'doincridone;!iletunpad_line~hpadline=matchUtil.String.all_blanklinewith|true->String.with_index_rangeline~first:hpad|false ->(matchString.lengthline<hpadwith|true->Fmt.failwith"invalid padding: %S"line|false->String.with_index_rangeline~first:hpad)letunpadhpad=List.map(unpad_line~hpad)letdump_cram_testsppf{start_pad;hpad;tests;end_pad}=Fmt.pfppf"{@[start_pad: %d;@ hpad: %d;@ tests: %a;@ end_pad: %a]}"start_padhpadFmt.Dump.(list dump)testsFmt.Dump.(option string)end_pad(* determine the amount of empty lines before the first non-empty line *)letstart_padlines=letpad_lines,code_lines=Util.List.partition_until(String.equal"")linesin(* make sure there *are* non-empty lines in the first place *)matchList.lengthcode_lineswith|0->(0,lines)|_->(List.lengthpad_lines,code_lines)letrecend_pad=function|[]->(None,[])|[x;last]whenUtil.String.all_blanklast->(Somelast,[x])|x::xs->letpad,xs=end_padxsin(pad,x::xs)typecram_input={start_pad:int;tests:stringlist;end_pad:stringoption;}letdetermine_padding lines=matchList.length lineswith|0->failwith"unable to determine padding, no lines in block"(* one line, it doesn't have any paddings *)|1->{start_pad=0;tests=lines;end_pad=None}|_->letstart_pad,lines=start_pad linesinletend_pad,lines=end_padlinesinletlines =matchList.for_allUtil.String.all_blank lineswith|true ->[]|false->linesin{start_pad;tests=lines;end_pad }letof_lines t=let{start_pad;tests;end_pad}=determine_padding tinlethpad=hpad_of_linestestsinletlines=unpadhpadtests inletlexer_input=lines|>List.map((Fun.flipString.append)"\n")|>String.concatinletlines=Lexer_cram.token (Lexing.from_stringlexer_input)inLog.debug(funl->l"Cram.of_lines (pad=%d) %a"hpadFmt.(Dump.listdump_line)lines);letmkcommandoutput~exit:exit_code={command;output=List.rev output;exit_code}inletreccommand_contacc=function|`Command_contc::t->command_cont(c::acc)t|`Command_lastc::t->(List.rev(c::acc),t)|_->Fmt.failwith"invalid multi-line command"inletrecauxcommandoutputacc=function|[]whencommand=[]->List.revacc|[]->List.rev(mkcommandoutput~exit:0::acc)|`Exitexit::t->aux[][](mkcommandoutput~exit ::acc)t|(`Ellipsisaso)::t->auxcommand(o::output)acct|`Command cmd::t->if command=[]thenaux[cmd][]acctelseaux[cmd][](mkcommand output~exit:0::acc)t|`Command_firstcmd::t->let cmd,t=command_cont[cmd]tinaux cmd[](mkcommandoutput ~exit:0::acc)t|(`Output_aso)::t->auxcommand(o::output)acct|(`Command_last s|`Command_conts)::t->auxcommandoutputacc(`Output s::t)inlethpad,tests=matchlineswith|`Command_firstcmd::t->let cmd,t=command_cont[cmd]tin(hpad,auxcmd[][]t)|`Command cmd::t->(hpad,aux [cmd][][]t)|[]->(0,[])|`Outputline::_->if String.lengthline>0&&line.[0]='$'thenfailwith"Blocks must start with a command or similar, not with an output \
line. To indicate a line as a command, start it with a dollar \
followed by a space."elsefailwith"Blocks must start with a command or similar, not with an output \
line. Please, make sure that there's no spare empty line, \
particularly between the output and its input."|_->Fmt.failwith"invalid cram block: %a"Fmt.(Dump.listdump_line)linesin{start_pad;hpad;tests;end_pad}letexit_code t=t.exit_code(* http://tldp.org/LDP/abs/html/here-docs.html *)letuse_heredoc(t:t)=String.cut(List.hdt.command)~sep:"<<" <>Noneletcommand_linet=ifnot(use_heredoc t)then String.concat~sep:" "t.commandelseString.concat~sep:"\n"t.command