123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602(*****************************************************************************)(* *)(* Open Source License *)(* Copyright (c) 2020-2022 Nomadic Labs <contact@nomadic-labs.com> *)(* Copyright (c) 2020 Metastate AG <hello@metastate.dev> *)(* *)(* Permission is hereby granted, free of charge, to any person obtaining a *)(* copy of this software and associated documentation files (the "Software"),*)(* to deal in the Software without restriction, including without limitation *)(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)(* and/or sell copies of the Software, and to permit persons to whom the *)(* Software is furnished to do so, subject to the following conditions: *)(* *)(* The above copyright notice and this permission notice shall be included *)(* in all copies or substantial portions of the Software. *)(* *)(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)(* DEALINGS IN THE SOFTWARE. *)(* *)(*****************************************************************************)openBasetypelog_level=Quiet|Error|Warn|Report|Info|Debugtypetemporary_file_mode=Delete|Delete_if_successful|Keeptypeloop_mode=Infinite|Countofinttypeon_unknown_regression_files_mode=Warn|Ignore|Fail|Deletetypeoptions={mutablecolor:bool;mutablelog_level:log_level;mutablelog_file:out_channeloption;mutablelog_filename:stringoption;mutablelog_buffer_size:int;mutablelog_worker_id:bool;mutablecommands:bool;mutabletemporary_file_mode:temporary_file_mode;mutablekeep_going:bool;mutablefiles_to_run:stringlist;mutablefiles_not_to_run:stringlist;mutabletests_to_run:stringlist;mutabletests_not_to_run:stringlist;mutablepatterns_to_run:rexlist;mutablepatterns_not_to_run:rexlist;mutabletags_to_run:stringlist;mutabletags_not_to_run:stringlist;mutablelist:[`Ascii_art|`Tsv]option;mutableglobal_timeout:floatoption;mutabletest_timeout:floatoption;mutableretry:int;mutablereset_regressions:bool;mutableon_unknown_regression_files_mode:on_unknown_regression_files_mode;mutableloop_mode:loop_mode;mutabletime:bool;mutablerecord:stringoption;mutablefrom_records:stringlist;mutableresume_file:stringoption;mutableresume:bool;mutablejob:(int*int)option;mutablejob_count:int;mutablesuggest_jobs:bool;mutablejunit:stringoption;mutableskip:int;mutableonly:intoption;mutabletest_args:stringString_map.t;mutableseed:intoption;}letoptions={color=Unix.isattyUnix.stdout&&Sys.getenv_opt"TERM"<>Some"dumb";log_level=Report;log_file=None;log_filename=None;log_buffer_size=50;log_worker_id=false;commands=false;temporary_file_mode=Delete;keep_going=false;files_to_run=[];files_not_to_run=[];tests_to_run=[];tests_not_to_run=[];patterns_to_run=[];patterns_not_to_run=[];tags_to_run=[];tags_not_to_run=[];list=None;global_timeout=None;test_timeout=None;retry=0;reset_regressions=false;on_unknown_regression_files_mode=Warn;loop_mode=Count1;time=false;record=None;from_records=[];resume_file=None;resume=false;job=None;job_count=1;suggest_jobs=false;junit=None;skip=0;only=None;test_args=String_map.empty;seed=None;}let()=at_exit@@fun()->Option.iterclose_outoptions.log_fileletset_log_filefilename=Option.iterclose_outoptions.log_file;options.log_filename<-Somefilename;options.log_file<-Some(open_outfilename)letinit?args()=letset_log_level=function|"quiet"->options.log_level<-Quiet|"error"->options.log_level<-Error|"warn"->options.log_level<-Warn|"report"->options.log_level<-Report|"info"->options.log_level<-Info|"debug"->options.log_level<-Debug|level->raise(Arg.Bad(Printf.sprintf"invalid log level: %S"level))inletset_job_countvalue=ifvalue<1thenraise(Arg.Bad"--job-count must be positive");options.job_count<-valueinletset_loop_countvalue=ifvalue<0thenraise(Arg.Bad"--loop-count must be positive or null");options.loop_mode<-Countvalueinletset_jobvalue=matchvalue=~**rex"^([0-9]+)/([0-9]+)$"with|None->raise(Arg.Bad"--job must be of the form: X/Y where X and Y are positive \
integers")|Some(index,count)->letints=matchint_of_string_optswith|None->raise(Arg.Bad("value too large: "^s))|Somei->iinletindex=intindexinletcount=intcountinifindex<1thenraise(Arg.Bad"--job index must be at least 1")elseifcount<1thenraise(Arg.Bad"--job count must be at least 1")elseifindex>countthenraise(Arg.Bad"--job index cannot be greater than job count")elseoptions.job<-Some(index,count)inletadd_from_recordpath=ifnot(Sys.is_directorypath)thenoptions.from_records<-path::options.from_recordselseletrecords=Sys.readdirpath|>Array.to_list|>List.filter(funname->Filename.extensionname=".json")|>List.map(funname->path//name)inoptions.from_records<-records@options.from_recordsinletset_skipvalue=ifvalue<0thenraise(Arg.Bad"--skip must be non-negative");options.skip<-valueinletset_onlyvalue=ifvalue<=0thenraise(Arg.Bad"--only must be at least one");options.only<-Somevalueinletset_on_unknown_regression_files_modevalue=options.on_unknown_regression_files_mode<-(matchvaluewith|"warn"->Warn|"ignore"->Ignore|"delete"->Delete|"fail"->Fail|_->raise(Arg.Bad(Format.asprintf"--on-unknown-regression-files must be either `warn`, \
`ignore`, `delete` or `fail` (was `%s`)."value)))inletadd_test_argvalue=letlen=String.lengthvalueinletrecfind_equali=ifi>=lenthenNoneelseifvalue.[i]='='thenSomeielsefind_equal(i+1)inletparameter,value=matchfind_equal0with|None->(value,"true")|Somei->(String.subvalue0i,String.subvalue(i+1)(len-i-1))inoptions.test_args<-String_map.addparametervalueoptions.test_argsinletspec=Arg.align[("--color",Arg.Unit(fun()->options.color<-true)," Use colors in output.");("--no-color",Arg.Unit(fun()->options.color<-false)," Do not use colors in output.");("--log-level",Arg.Stringset_log_level,"<LEVEL> Set log level to LEVEL. Possible LEVELs are: quiet, error, \
warn, report, info, debug. Default is report.");("--log-file",Arg.Stringset_log_file,"<FILE> Also log to FILE (in verbose mode: --log-level only applies \
to stdout). In the presence of --job-count, the main process will \
log test results to FILE while each worker writes test logs to a \
separate file BASENAME-WORKER_ID[.EXT]. BASENAME is the basename of \
FILE, WORKER_ID is the zero-indexed id of the worker and .EXT is \
the extension of FILE if present.");("--log-buffer-size",Arg.Int(funx->options.log_buffer_size<-x),"<COUNT> Before logging an error on stdout, also log the last COUNT \
messages that have been ignored because of the log level since the \
last message that was not ignored. Default is 50.");("--log-worker-id",Arg.Unit(fun()->options.log_worker_id<-true)," Decorate logs with worker IDs when --job-count is more than 1.");("--verbose",Arg.Unit(fun()->options.log_level<-Debug)," Same as --log-level debug.");("-v",Arg.Unit(fun()->options.log_level<-Debug)," Same as --verbose.");("--quiet",Arg.Unit(fun()->options.log_level<-Quiet)," Same as --log-level quiet.");("-q",Arg.Unit(fun()->options.log_level<-Quiet)," Same as --quiet.");("--info",Arg.Unit(fun()->options.log_level<-Info)," Same as --log-level info.");("-i",Arg.Unit(fun()->options.log_level<-Info)," Same as --info.");("--commands",Arg.Unit(fun()->options.commands<-true)," Output commands which are run, in a way that is easily copy-pasted \
for manual reproductibility.");("-c",Arg.Unit(fun()->options.commands<-true)," Same as --commands.");("--delete-temp",Arg.Unit(fun()->options.temporary_file_mode<-Delete)," Delete temporary files and directories that were created (this is \
the default).");("--delete-temp-if-success",Arg.Unit(fun()->options.temporary_file_mode<-Delete_if_successful)," Delete temporary files and directories, except if the test failed.");("--keep-temp",Arg.Unit(fun()->options.temporary_file_mode<-Keep)," Do not delete temporary files and directories that were created.");("--keep-going",Arg.Unit(fun()->options.keep_going<-true)," If a test fails, continue with the remaining tests instead of \
stopping. Aborting manually with Ctrl+C still stops everything.");("-k",Arg.Unit(fun()->options.keep_going<-true)," Same as --keep-going.");("--list",Arg.Unit(fun()->options.list<-Some`Ascii_art)," List tests instead of running them. Pass --time to also display \
results and timings (in seconds) from a previous execution given \
through --record, in the format TIME (COUNT). TIME is the average \
time of successful executions. COUNT is SCOUNT/(SCOUNT+FCOUNT) \
where SCOUNT (resp. FCOUNT) is the number of successful (resp. \
failed) tests in the record. If there is only one successful test, \
then (COUNT) is omitted. Tests lacking a past record of successful \
executions are noted '-'. A final row is added containing the total \
of the averages of successful test executions, and the total number \
of selected tests.");("-l",Arg.Unit(fun()->options.list<-Some`Ascii_art)," Same as --list.");("--list-tsv",Arg.Unit(fun()->options.list<-Some`Tsv)," List tests instead of running them but one-per-line, as \
tab-separated values in the format FILE TITLE TAGS. Pass --time to \
also display results and timings (in nanoseconds) from a previous \
execution given through --record. Then each line is appended with \
STIME SCOUNT FTIME FCOUNT. STIME (resp. FTIME) is the total running \
time in nanoseconds of successful (resp. failed) previous runs. \
SCOUNT (resp. FCOUNT) is the count of successful (resp. failed) \
previous runs.");("--file",Arg.String(funfile->options.files_to_run<-file::options.files_to_run),"<FILE> Only run tests implemented in source files ending with FILE \
(see SELECTING TESTS).");("-f",Arg.String(funfile->options.files_to_run<-file::options.files_to_run),"<FILE> Same as --file.");("--not-file",Arg.String(funfile->options.files_not_to_run<-file::options.files_not_to_run),"<FILE> Only run tests not implemented in source files ending with \
FILE (see SELECTING TESTS).");("--match",Arg.String(funpattern->options.patterns_to_run<-Base.rex~opts:[`Caseless]pattern::options.patterns_to_run),"<PERL_REGEXP> Only run tests for which 'FILE: TITLE' matches \
PERL_REGEXP (case insensitive), where FILE is the source file of \
the test and TITLE its title (see SELECTING TESTS).");("-m",Arg.String(funpattern->options.patterns_to_run<-Base.rex~opts:[`Caseless]pattern::options.patterns_to_run),"<PERL_REGEXP> Same as --match.");("--not-match",Arg.String(funpattern->options.patterns_not_to_run<-Base.rex~opts:[`Caseless]pattern::options.patterns_not_to_run),"<PERL_REGEXP> Only run tests for which 'FILE: TITLE' does not match \
PERL_REGEXP (case insensitive), where FILE is the source file of \
the test and TITLE its title (see SELECTING TESTS).");("--title",Arg.String(funtitle->options.tests_to_run<-title::options.tests_to_run),"<TITLE> Only run tests which are exactly entitled TITLE (see \
SELECTING TESTS).");("--test",Arg.String(funtitle->options.tests_to_run<-title::options.tests_to_run),"<TITLE> Same as --title.");("-t",Arg.String(funtitle->options.tests_to_run<-title::options.tests_to_run),"<TITLE> Same as --title.");("--not-title",Arg.String(funtitle->options.tests_not_to_run<-title::options.tests_not_to_run),"<TITLE> Only run tests which are not exactly entitled TITLE (see \
SELECTING TESTS).");("--not-test",Arg.String(funtitle->options.tests_not_to_run<-title::options.tests_not_to_run),"<TITLE> Same as --not-title.");("--global-timeout",Arg.Float(fundelay->options.global_timeout<-Somedelay),"<SECONDS> Fail if the set of tests takes more than SECONDS to run.");("--test-timeout",Arg.Float(fundelay->options.test_timeout<-Somedelay),"<SECONDS> Fail if a test takes, on its own, more than SECONDS to \
run.");("--retry",Arg.Int(funretry->options.retry<-retry),"<COUNT> Retry each failing test up to COUNT times. If one retry is \
successful, the test is considered successful.");("--reset-regressions",Arg.Unit(fun()->options.reset_regressions<-true)," Remove regression test outputs if they exist, and regenerate them.");("--on-unknown-regression-files",Arg.Stringset_on_unknown_regression_files_mode,"<MODE> How to handle regression test outputs that are not declared \
by any test. MODE should be either 'warn', 'ignore', 'fail' or \
'delete'. If set to 'warn', emit a warning for unknown output \
files. If set to 'ignore', ignore unknown output files. If set to \
'fail', terminate execution with exit code 1 and without running \
any further action when unknown output files are found. If set to \
'delete', delete unknown output files. To check which files would \
be deleted, run with this option set to 'warn', which is the \
default.");("--loop",Arg.Unit(fun()->options.loop_mode<-Infinite)," Restart from the beginning once all tests are done. All tests are \
repeated until one of them fails or if you interrupt with Ctrl+C. \
This is useful to reproduce non-deterministic failures. When used \
in conjunction with --keep-going, tests are repeated even if they \
fail, until you interrupt them with Ctrl+C.");("--loop-count",Arg.Intset_loop_count,"<COUNT> Same as --loop, but stop after all tests have been run \
COUNT times. A value of 0 means tests are not run. The default \
behavior corresponds to --loop-count 1. If you specify both --loop \
and --loop-count, only the last one is taken into account.");("--time",Arg.Unit(fun()->options.time<-true)," Print a summary of the total time taken by each test. Ignored if a \
test failed. Includes the time read from records: to display a \
record, you can use --time --loop-count 0 --from-record <FILE>.");("--record",Arg.String(funfile->options.record<-Somefile),"<FILE> Record test results to FILE. This file can then be used with \
--from-record. If you use --loop or --loop-count, times are \
averaged for each test.");("--from-record",Arg.Stringadd_from_record,"<FILE> Start from a file recorded with --record. Can be specified \
multiple times. If <FILE> is a directory, this is equivalent to \
specifying --from-record for all files in this directory that have \
the .json extension. When using --time, test durations include \
tests found in record files. When using --record, the new record \
which is output does NOT include the input records. When using \
--junit, reports do NOT include input records.");("--resume-file",Arg.String(funfilename->options.resume_file<-Somefilename),"<FILE> Record test results to FILE for use with --resume. When \
using --resume, test results that existed in FILE are kept, \
contrary to --record.");("--resume",Arg.Unit(fun()->options.resume<-true)," Resume from a previous run. This reads the resume file located at \
--resume-file to resume from it. If --resume-file is not specified, \
--resume implies --resume-file tezt-resume.json. If the resume file \
does not exist, act as if it was empty. Before running a test, it \
is checked whether this test was already successfully ran according \
to the resume file. If it was, the test is skipped. When using \
--loop or --loop-count, the test is skipped as many times as it was \
successful according to the resume file.");("-r",Arg.Unit(fun()->options.resume<-true)," Same as --resume.");("--job",Arg.Stringset_job,"<INDEX>/<COUNT> COUNT must be at least 1 and INDEX must be between \
1 and COUNT. Use --from-record to feed duration data from past \
runs. Split the set of selected tests (see SELECTING TESTS) into \
COUNT subsets of roughly the same total duration. Execute only one \
of these subsets, specified by INDEX. Tests for which no time data \
is available are given a default duration of 1 second. You can use \
--list to see what tests are in a subset without actually running \
the tests. A typical use is to run tests in parallel on different \
machines. For instance, have one machine run with --job 1/3, one \
with --job 2/3 and one with --job 3/3. Be sure to provide exactly \
the same records with --from-record, in the same order, and to \
select exactly the same set of tests (same tags, same --file and \
same --test) for all machines, otherwise some tests may not be run \
at all.");("--job-count",Arg.Intset_job_count,"<COUNT> Run COUNT tests in parallel, in separate processes. With \
--suggest-jobs, set the number of target jobs for --suggest-jobs \
instead (default is 1).");("-j",Arg.Intset_job_count,"<COUNT> Same as --job-count.");("--suggest-jobs",Arg.Unit(fun()->options.suggest_jobs<-true)," Read test results records specified with --from-records and \
suggest a partition of the tests that would result in --job-count \
sets of roughly the same total duration. Output each job as a list \
of flags that can be passed to Tezt, followed by a shell comment \
that denotes the expected duration of the job. A similar result can \
be obtained with --list --job, except that the last job suggested \
by --suggest-jobs uses --not-test to express \"all tests that are \
not already in other jobs\", meaning that the last job acts as a \
catch-all for unknown tests.");("--junit",Arg.String(funpath->options.junit<-Somepath),"<FILE> Store test results in FILE using JUnit XML format. Time \
information for each test is the sum of all runs of this test for \
the current session. Test result (success or failure) is the result \
for the last run of the test.");("--skip",Arg.Intset_skip,"<COUNT> Skip the first COUNT tests. This filter is applied after \
--job and before --only.");("--only",Arg.Intset_only,"<COUNT> Only run the first COUNT tests. This filter is applied \
after --job and --skip.");("--test-arg",Arg.Stringadd_test_arg,"<PARAMETER>=<VALUE> Pass a generic argument to tests. Tests can get \
this argument with Cli.get. --test-arg <PARAMETER> is a short-hand \
for: --test-arg <PARAMETER>=true");("-a",Arg.Stringadd_test_arg,"<PARAMETER>=<VALUE> Same as --test-arg.");("--seed",Arg.Int(funseed->options.seed<-Someseed),"<SEED> Force tests declared with ~seed: Random to initialize the \
pseudo-random number generator with this seed.");]inletusage=(* This was formatted by ocamlformat. Sorry for all the slashes. *)"Usage: "^Sys.argv.(0)^" [OPTION..] [TAG..]\n\n\
SELECTING TESTS\n\n\
\ You can specify multiple tags, negated tags, titles, title patterns \
and filenames on the command line. Only tests which match all the \
following conditions will be run:\n\
\ - the test must have all tags and none of the negated tags;\n\
\ - the test must have one of the specified titles;\n\
\ - the test must have a title matching one of the specified patterns;\n\
\ - the test must be implemented in one of the specified files.\n\n\
\ The tags of a test are given by the ~tags argument of Test.register. \
To negate a tag, prefix it with a slash: /\n\n\
\ The title of a test is given by the ~title argument of Test.register. \
It is what is printed after [SUCCESS] (or [FAILURE] or [ABORTED]) in \
the reports. Use --title (respectively --not-title) to select \
(respectively unselect) a test by its title on the command-line. You \
can also select (respectively unselect) tests for which 'filename: \
title' matches one or several Perl regular expressions using --match \
(respectively --not-match).\n\n\
\ The file in which a test is implemented is specified by the ~__FILE__ \
argument of Test.register. In other words, it is the path of the file \
in which the test is defined. Use --file (respectively --not-file) to \
select (respectively unselect) a test by its path (or a suffix thereof) \
on the command-line.\n\n\
\ For instance:\n\n\
\ "^Sys.argv.(0)^" node bake /rpc --file bootstrap.ml --file sync.ml\n\n\
\ will run all tests defined in either bootstrap.ml or sync.ml, which \
have at least tags 'node' and 'bake', but which do not have the 'rpc' \
tag.\n\n\
OPTIONS\n"inletadd_tagtag=iftag=""||tag.[0]<>'/'thenoptions.tags_to_run<-tag::options.tags_to_runelseoptions.tags_not_to_run<-String.subtag1(String.lengthtag-1)::options.tags_not_to_runinletargv=letexecutable_name=ifArray.lengthSys.argv>0thenSys.argv.(0)elseSys.executable_nameinmatchargswith|None->Sys.argv|Somex->Array.of_list(executable_name::x)intryArg.parse_argvargvspecadd_tagusagewith|Arg.Badmsg->Printf.eprintf"%s"msg;exit2|Arg.Helpmsg->Printf.printf"%s"msg;exit0let()=init()letget_optparseparameter=matchString_map.find_optparameteroptions.test_argswith|Somevalue->(matchparsevaluewith|None->failwith(sf"invalid value for -a %s: %s"parametervalue)|Somevalue->Somevalue)|None->Noneletget?defaultparseparameter=matchget_optparseparameterwith|Somev->v|None->(matchdefaultwith|None->failwith(sf"missing test argument %s, please specify it with: -a %s=<VALUE>"parameterparameter)|Somedefault->default)letget_bool?defaultparameter=get?defaultbool_of_string_optparameterletget_int?defaultparameter=get?defaultint_of_string_optparameterletget_float?defaultparameter=get?defaultfloat_of_string_optparameterletget_string?defaultparameter=get?defaultOption.someparameterletget_bool_optparameter=get_optbool_of_string_optparameterletget_int_optparameter=get_optint_of_string_optparameterletget_float_optparameter=get_optfloat_of_string_optparameterletget_string_optparameter=get_optOption.someparameter