123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720(*
Command-line interface generated for a test program.
*)openPrintfopenTesto_utilopenFpath_.OperatorsopenCmdliner(* Record that groups configuration options known before parsing the command
line. *)typeocaml_conf={argv:stringarray;project_name:string;expectation_workspace_root:Fpath.toption;status_workspace_root:Fpath.toption;}(*
Configuration object type that is used for all subcommands although
not all of them use all the fields.
This configuration is obtained after evaluating the command line.
*)typeconf={(* All subcommands *)chdir:Fpath.toption;debug:bool;filter_by_substring:stringlistoption;filter_by_tag:Tag_query.toption;env:(string*string)list;ocaml_conf:ocaml_conf;(* Run and Status *)intro:string;show_output:bool;autoclean:bool;max_inline_log_bytes:intoption;(* Status *)status_output_style:Run.status_output_style;(* Run *)lazy_:bool;slice:Slice.tlist;is_worker:bool;jobs:intoption;strict:bool;test_list_checksum:stringoption;}letdefault_max_inline_log_bytes=1_000_000letdefault_ocaml_conf={argv=Sys.argv;project_name="";expectation_workspace_root=None;status_workspace_root=None;}letdefault_conf={ocaml_conf=default_ocaml_conf;chdir=None;debug=false;filter_by_substring=None;filter_by_tag=None;env=[];show_output=false;autoclean=false;max_inline_log_bytes=Somedefault_max_inline_log_bytes;status_output_style=Compact_important;intro=Run.introduction_text;lazy_=false;slice=[];is_worker=false;jobs=None;strict=false;test_list_checksum=None;}(*
Subcommands:
- run
- status
- approve
*)typecmd_conf=|Run_testsofconf|Statusofconf|Approveofconf|Show_tagsofocaml_conftypesubcommand_result=|Run_resultofTypes.test_with_statuslist|Status_resultofTypes.test_with_statuslist|Approve_resulttype'continuation_resulttest_spec=((string*string)list->Types.testlist)*(int->subcommand_result->'continuation_result)(****************************************************************************)(* Dispatch subcommands to do real work *)(****************************************************************************)letshow_tags()=Tag.list()|>List.iter(funtag->printf"%s\n"(Tag.to_stringtag))letinit_cwd_sensitive~chdir:cwdocaml_conf=letorig_cwd=Option.map(fun_->Fpath.v(Sys.getcwd()))cwdinOption.iterHelpers.chdircwd;Store.init_settings?expectation_workspace_root:ocaml_conf.expectation_workspace_root?status_workspace_root:ocaml_conf.status_workspace_root~project_name:ocaml_conf.project_name();orig_cwdletrun_with_conf((get_tests,handle_subcommand_result):_test_spec)(cmd_conf:cmd_conf):unit=(*
The creation of tests can take a while so it's delayed until we
really need the tests. This makes '--help' fast.
*)matchcmd_confwith|Run_testsconf->letorig_cwd=init_cwd_sensitive~chdir:conf.chdirconf.ocaml_confinDebug.debug:=conf.debug;(* Make sure to communicate over the pipe in binary mode to avoid
CRLF<->LF conversions.
We do this before running the user function 'get_tests' in case
it prints messages to stdout that ends up as junk in the pipe. *)set_binary_mode_instdintrue;set_binary_mode_outstdouttrue;lettests=get_testsconf.envinRun.cmd_run~always_show_unchecked_output:(conf.show_output,conf.max_inline_log_bytes)~argv:conf.ocaml_conf.argv~autoclean:conf.autoclean~filter_by_substring:conf.filter_by_substring~filter_by_tag:conf.filter_by_tag~intro:conf.intro~is_worker:conf.is_worker~jobs:conf.jobs~lazy_:conf.lazy_~orig_cwd~slice:conf.slice~strict:conf.strict~test_list_checksum:conf.test_list_checksumtests(funexit_codetests_with_status->handle_subcommand_resultexit_code(Run_resulttests_with_status))|>(* TODO: ignoring this promise doesn't make sense.
The whole Lwt support needs testing and probably doesn't
work as is. If someone really needs it, please provide a test
environment where it's justified i.e. where we can't
call 'Lwt_main.run' so we can make this work. *)ignore|Statusconf->let_orig_cwd=init_cwd_sensitive~chdir:conf.chdirconf.ocaml_confinDebug.debug:=conf.debug;letexit_code,tests_with_status=Run.cmd_status~always_show_unchecked_output:(conf.show_output,conf.max_inline_log_bytes)~autoclean:conf.autoclean~filter_by_substring:conf.filter_by_substring~filter_by_tag:conf.filter_by_tag~intro:conf.intro~output_style:conf.status_output_style~strict:conf.strict(get_testsconf.env)inhandle_subcommand_resultexit_code(Status_resulttests_with_status)|Approveconf->let_orig_cwd=init_cwd_sensitive~chdir:conf.chdirconf.ocaml_confinDebug.debug:=conf.debug;letexit_code=Run.cmd_approve~filter_by_substring:conf.filter_by_substring~filter_by_tag:conf.filter_by_tag(get_testsconf.env)inhandle_subcommand_resultexit_codeApprove_result|Show_tagsocaml_conf->let_orig_cwd=init_cwd_sensitive~chdir:Noneocaml_confinshow_tags()(****************************************************************************)(* Command-line options *)(****************************************************************************)(*
Some of the command-line options are shared among subcommands.
*)letautoclean_term:boolTerm.t=letinfo=Arg.info["autoclean"]~doc:"Remove test snapshots that don't match any test."inArg.value(Arg.flaginfo)letdebug_term:boolTerm.t=letinfo=Arg.info["debug"]~doc:"Log information that can be useful for debugging the Testo library."inArg.value(Arg.flaginfo)letchdir_term:stringoptionTerm.t=letinfo=Arg.info["C";"chdir"]~docv:"DIR"~doc:"Change the current working directory to $(docv) before doing any work."inArg.value(Arg.opt(Arg.someArg.string)Noneinfo)letexpert_term:boolTerm.t=letinfo=Arg.info["expert"]~doc:"Assume the user is familiar with Testo and don't show non-essential\n\
messages or tips targeted at new users."inArg.value(Arg.flaginfo)letfilter_by_substring_term:stringlistTerm.t=letinfo=Arg.info["s";"filter-substring"]~docv:"SUBSTRING"~doc:{|Select tests whose description
contains $(docv).
Multiple '-s' search queries can be specified in which case only one
of them needs to match.|}inArg.value(Arg.opt_allArg.string[]info)lettag_query_conv=letparsestr=matchTag_query.parsestrwith|Okx->Okx|Errormsg->Error(`Msgmsg)inCmdliner.Arg.conv~docv:"QUERY"(parse,Tag_query.pp)(* This option currently supports only one tag. In the future, we might
want to support boolean queries e.g. '-t "lang.python and not todo"' *)letfilter_by_tag_term:Tag_query.toptionTerm.t=letinfo=Arg.info["t";"filter-tag"]~docv:"QUERY"~doc:(sprintf"Select tests whose tags match $(docv). Filtering by tag is \
generally more robust than selecting tests by text contained in \
their name with '-s'. $(docv) is a boolean query combining tags \
with 'and', 'or', 'not', and parentheses using the usual \
precedence rules. Tag-like selectors 'all' and 'none' are also \
supported. For example, '(foo or bar) and not e2e' will select any \
test with the tag 'foo' or the tag 'bar' but not if it has the tag \
'e2e'. Run '%s show-tags' to see the list of tags defined for the \
current test suite."Sys.argv.(0))inArg.value(Arg.opt(Arg.sometag_query_conv)Noneinfo)letshow_output_term:boolTerm.t=letinfo=Arg.info["w";"show-output"]~doc:"Show the output of all tests rather than only showing the output of \
the failed tests. This excludes the output (stdout, stderr, or both) \
that may be captured explicitly to be compared against expectations."inArg.value(Arg.flaginfo)letnonnegative_int_limit=letparse_nonnegative_int_limitstr=matchstrwith|"unlimited"->OkNone|_->(matchint_of_string_optstrwith|None->Error(`Msg(Printf.sprintf"not a nonnegative int or 'unlimited': %s"str))|Somen->ifn<0thenError(`Msg(Printf.sprintf"A negative integer is not a valid upper limit for a \
nonnegative int: %s. To suppress the default limit, use \
'unlimited'"str))elseOk(Somen))inletprint_nonnegative_int_limitppfopt_int=matchopt_intwith|None->Format.fprintfppf"unlimited"|Somen->Format.fprintfppf"%d"ninCmdliner.Arg.conv~docv:"LIMIT"(parse_nonnegative_int_limit,print_nonnegative_int_limit)letmax_inline_log_bytes_term:intoptionTerm.t=letinfo=Arg.info["max-inline-log-bytes"]~docv:"LIMIT"~doc:(sprintf"When displaying logs (unchecked stdout or stderr output), show at \
most that many bytes for each test.\n\
\ The default limit is %d bytes. To remove the \
default limit, specify 'unlimited'."default_max_inline_log_bytes)inArg.value(Arg.optnonnegative_int_limit(Somedefault_max_inline_log_bytes)info)letstrict_term:boolTerm.t=letinfo=Arg.info["strict"]~doc:"Treat flaky tests as ordinary tests. This disables the default \
behavior of ignoring failing tests that were marked as flaky by the \
programmer when determining the overall success of the test run."inArg.value(Arg.flaginfo)letverbose_run_term:boolTerm.t=letinfo=Arg.info["v";"verbose"]~doc:"Print more details than by default. Currently, this is equivalent to \
'--show-output' but it may be extended in the future to bundle up \
more options."inArg.value(Arg.flaginfo)(* Converter for arguments of the form KEY=VALUE *)letenv_conv=letkey_re=Re.Pcre.regexp{|\A[A-Za-z_][A-Za-z_0-9]*\z|}inleterrorstr=Error(sprintf"Malformed KEY=VALUE pair: %s"str)inletparsestr=matchString.index_optstr'='with|None->errorstr|Somepos->letkey=String.substr0posinifnot(Re.Pcre.pmatch~rex:key_rekey)thenerrorstrelseletvalue=String.substr(pos+1)(String.lengthstr-pos-1)inOk(key,value)inletprintfmt(key,value)=Format.fprintffmt"%s=%s"keyvalueinArg.conv'~docv:"KEY=VALUE"(parse,print)letenv_term:(string*string)listTerm.t=letinfo=Arg.info["e";"env"]~docv:"KEY=VALUE"~doc:"Pass a key/value pair to the function that creates the test suite. \
KEY must be an alphanumeric identifier of the form \
[A-Za-z_][A-Za-z_0-9]*. VALUE can be any string. This mechanism for \
passing arbitrary runtime settings to the test suite is offered as a \
safer alternative to environment variables. Multiple -e/--env options \
are supported in the same command, each defining one key/value pair."inArg.value(Arg.opt_allenv_conv[]info)(****************************************************************************)(* Subcommand: run (replaces alcotest's 'test') *)(****************************************************************************)letjobs_term~default_workers:intoptionTerm.t=letdefault_str=matchdefault_workerswith|None->"set to the number of CPUs detected on the machine"|Somen->string_of_intninletinfo=Arg.info["j";"jobs"]~docv:"NUM"~doc:(sprintf"Specify the number of jobs to run in parallel. By default, this \
value is %s. Both '-j0' and '-j1' ensure sequential, \
non-overlapping execution of the tests. Unlike '-j1', '-j0' will \
not create a separate worker process to run the tests. The default \
can be changed by passing '~default_workers' to the OCaml function \
'Testo.interpret_argv'. NOTE: Parallel executation of tests is not \
stable on Windows."default_str)inArg.value(Arg.opt(Arg.someArg.int)Noneinfo)letlazy_term:boolTerm.t=letinfo=Arg.info["lazy"]~doc:"Run only the tests that were not previously successful."inArg.value(Arg.flaginfo)(* Converter for arguments of the form NUM/NUM_SLICES *)letslice_conv=letparsestr=matchSlice.of_stringstrwith|None->Error(sprintf"Malformed slice: %s"str)|Somex->Okxinletprintfmtslice=Format.pp_print_stringfmt(Slice.to_stringslice)inArg.conv'~docv:"NUM/NUM_SLICES"(parse,print)letslice_term:Slice.tlistTerm.t=letinfo=Arg.info["slice"]~docv:"NUM/NUM_SLICES"~doc:"Divide the test suite into NUM_SLICES and work on slice NUM only \
(1-based). For example, '1/4' is the first of four slices and '4/4' \
is the last one. If multiple '--slice' options are specified, they \
are applied sequentially from left to right. If the original suite \
defines 23 tests, '--slice 2/4' selects tests [7,8,9,10,11,12] \
(1-based). If additionally '--slice 1/3' is specified after it on the \
same command line, the remaining tests will be [7,8]. This filter is \
meant for running tests in parallel, possibly across multiple hosts. \
It is applied after any other filters to as to divide the work more \
evenly."inArg.value(Arg.opt_allslice_conv[]info)lettest_list_checksum_term:stringoptionTerm.t=letinfo=Arg.info["test-list-checksum"]~docv:"STR"~doc:"Internal use only. This is a checksum used to check that the list of \
tests generated in a worker is the same as the list of tests \
generated by the master."inArg.value(Arg.opt(Arg.someArg.string)Noneinfo)letworker_term:boolTerm.t=letinfo=Arg.info["worker"]~doc:"Internal option used to launch a parallel worker."inArg.value(Arg.flaginfo)letrun_doc="run the tests"letrun_man:Manpage.blocklist=[`SManpage.s_description;`P{|Run all or only some of the tests. By default, the status
of each test is reported as they are executed. Here's the legend for test
statuses:|};`Pre"• [PASS]: a successful test that was expected to succeed (good);\n\
• [FAIL]: a failing test that was expected to succeed (needs fixing);\n\
• [XFAIL]: a failing test that was expected to fail (tolerated failure);\n\
• [XPASS]: a successful test that was expected to fail (progress?).\n\
• [MISS]: a test that never ran;\n\
• [SKIP]: a test that is always skipped but kept around for some reason;\n\
• [xxxx*]: a new test for which there's no expected output yet.\n\
\ In this case, you should review the test output and run the 'approve'\n\
\ subcommand once you're satisfied with the output.\n";`P{|To review the status of the tests without rerunning them,
use the 'status' subcommand.|};]letoptional_nonempty_listxs=matchxswith|[]->None|or_terms->Someor_termsletsubcmd_run_term~default_workersocaml_conf(test_spec:_test_spec):unitTerm.t=letcombineautocleanchdirdebugenvexpertfilter_by_substringfilter_by_tagjobslazy_max_inline_log_bytesshow_outputslicestricttest_list_checksumverboseworker=letfilter_by_substring=optional_nonempty_listfilter_by_substringinletintro=ifexpertthen""elsedefault_conf.introinletshow_output=show_output||verboseinletjobs=matchjobswith|None->default_workers|Some_->jobsinRun_tests{default_confwithautoclean;chdir=Option.mapFpath.vchdir;debug;env;filter_by_substring;filter_by_tag;intro;is_worker=worker;jobs;lazy_;max_inline_log_bytes;ocaml_conf;show_output;slice;strict;test_list_checksum;}|>run_with_conftest_specinTerm.(constcombine$autoclean_term$chdir_term$debug_term$env_term$expert_term$filter_by_substring_term$filter_by_tag_term$jobs_term~default_workers$lazy_term$max_inline_log_bytes_term$show_output_term$slice_term$strict_term$test_list_checksum_term$verbose_run_term$worker_term)letsubcmd_run~default_workersocaml_conftest_spec=letinfo=Cmd.info"run"~doc:run_doc~man:run_maninCmd.vinfo(subcmd_run_term~default_workersocaml_conftest_spec)(****************************************************************************)(* Subcommand: status (replaces alcotest's 'list') *)(****************************************************************************)(*
Design: the options '-l' and '-a' were chosen for two reasons:
- make the status output compact by default;
- adopt a similar behavior as the '-l' and '-a' options of 'ls'.
*)letlong_term:boolTerm.t=letinfo=Arg.info["l";"long"]~doc:"Print details instead of just a one-line summary for each test."inArg.value(Arg.flaginfo)letall_term:boolTerm.t=letinfo=Arg.info["a";"all"]~doc:"Report tests in all statuses instead of only the tests that\n\
\ need attention."inArg.value(Arg.flaginfo)letverbose_status_term:boolTerm.t=letinfo=Arg.info["v";"verbose"]~doc:"Report the status of the tests with maximum verbosity.\n\
\ This is currently equivalent to '-alw'."inArg.value(Arg.flaginfo)letstatus_doc="show test status"letsubcmd_status_termocaml_conftests:unitTerm.t=letcombineallautocleanchdirdebugenvexpertfilter_by_substringfilter_by_taglongmax_inline_log_bytesshow_outputstrictverbose=letfilter_by_substring=optional_nonempty_listfilter_by_substringinletintro=ifexpertthen""elsedefault_conf.introinletstatus_output_style:Run.status_output_style=ifverbosethenLong_allelsematch(long,all)with|true,true->Long_all|false,true->Compact_all|true,false->Long_important|false,false->Compact_importantinletshow_output=show_output||verboseinStatus{default_confwithautoclean;chdir=Option.mapFpath.vchdir;debug;env;filter_by_substring;filter_by_tag;intro;max_inline_log_bytes;ocaml_conf;show_output;status_output_style;strict;}|>run_with_conftestsinTerm.(constcombine$all_term$autoclean_term$chdir_term$debug_term$env_term$expert_term$filter_by_substring_term$filter_by_tag_term$long_term$max_inline_log_bytes_term$show_output_term$strict_term$verbose_status_term)letsubcmd_statusocaml_conftests=letinfo=Cmd.info"status"~doc:status_docinCmd.vinfo(subcmd_status_termocaml_conftests)(****************************************************************************)(* Subcommand: approve *)(****************************************************************************)letapprove_doc="approve new test output"letsubcmd_approve_termocaml_conftests:unitTerm.t=letcombinechdirdebugenvfilter_by_substringfilter_by_tag=letfilter_by_substring=optional_nonempty_listfilter_by_substringinApprove{default_confwithchdir=Option.mapFpath.vchdir;debug;env;filter_by_substring;filter_by_tag;ocaml_conf;}|>run_with_conftestsinTerm.(constcombine$chdir_term$debug_term$env_term$filter_by_substring_term$filter_by_tag_term)letsubcmd_approveocaml_conftests=letinfo=Cmd.info"approve"~doc:approve_docinCmd.vinfo(subcmd_approve_termocaml_conftests)(****************************************************************************)(* Subcommand: show-tags *)(****************************************************************************)letshow_tags_doc="show the list of valid tags for this test suite"letsubcmd_show_tags_termocaml_conftests:unitTerm.t=letcombine()=run_with_conftests(Show_tagsocaml_conf)inTerm.(constcombine$const())letsubcmd_show_tagsocaml_conftests=letinfo=Cmd.info"show-tags"~doc:show_tags_docinCmd.vinfo(subcmd_show_tags_termocaml_conftests)(****************************************************************************)(* Main command *)(****************************************************************************)letroot_dococaml_conf=sprintf"run tests for %s"ocaml_conf.project_nameletroot_manocaml_conf:Manpage.blocklist=[`SManpage.s_description;`P(sprintf{|This is the program built for running and managing the tests for this project,
%s. It revolves around 3 main subcommands: 'run', 'status', and 'approve'.
Use the 'status' subcommand to check the status of each test without having
to re-run them. 'approve' must be used on tests whose output is captured
so as to make their latest output the new reference.
|}ocaml_conf.project_name);`P(sprintf(* NOTE: We use quoted string paths via %S to avoid conflicts with
Cmdliner's markup (this occurs, e.g., with `\` in Windows paths). *){|This test program was configured to store the temporary results in
%S and the expected test output in the persistent folder %S.
The latter should be kept under version control (git or similar).
|}!!(Option.valueocaml_conf.status_workspace_root~default:Store.default_status_workspace_root)!!(Option.valueocaml_conf.status_workspace_root~default:Store.default_expectation_workspace_root));`P{|Visit https://testocaml.net/ to learn how to
create and manage test suites with Testo.|};]letroot_infoocaml_conf=letname=Filename.basenameSys.argv.(0)inCmd.infoname~doc:(root_dococaml_conf)~man:(root_manocaml_conf)letroot_term~default_workersocaml_conftest_spec=(*
Term.ret (Term.const (`Help (`Pager, None)))
*)subcmd_run_term~default_workersocaml_conftest_specletsubcommands~default_workers~ocaml_conftest_spec=[subcmd_run~default_workersocaml_conftest_spec;subcmd_statusocaml_conftest_spec;subcmd_approveocaml_conftest_spec;subcmd_show_tagsocaml_conftest_spec;]letwith_record_backtracefunc=letoriginal_state=Printexc.backtrace_status()inPrintexc.record_backtracetrue;Fun.protect~finally:(fun()->Printexc.record_backtraceoriginal_state)func(*
$ cmdliner-demo-subcmd -> parsed as root subcommand
$ cmdliner-demo-subcmd --help -> also parsed as root subcommand
$ cmdliner-demo-subcmd subcmd1 -> parsed as 'subcmd1' subcommand
If there is a request to display the help page, it displayed at this point,
returning '`Help'.
Otherwise, 'conf' is returned to the application.
*)letinterpret_argv?(argv=Sys.argv)?(default_workers=None)?expectation_workspace_root?(handle_subcommand_result=funexit_code_->exitexit_code)?status_workspace_root~project_name(get_tests:(string*string)list->Types.testlist)=letocaml_conf={argv;project_name;expectation_workspace_root;status_workspace_root}inlettest_spec=(get_tests,handle_subcommand_result)in(* TODO: is there any reason why we shouldn't always record a stack
backtrace when running tests? *)with_record_backtrace(fun()->Cmd.group~default:(root_term~default_workersocaml_conftest_spec)(root_infoocaml_conf)(subcommands~ocaml_conf~default_workerstest_spec)|>Cmd.eval~argv|>(* does not reach this point by default *)exit)