Source file cli.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
open Base
type log_level = Quiet | Error | Warn | Report | Info | Debug
type temporary_file_mode = Delete | Delete_if_successful | Keep
type loop_mode = Infinite | Count of int
type on_unknown_regression_files_mode = Warn | Ignore | Fail | Delete
type options = {
mutable color : bool;
mutable log_level : log_level;
mutable log_file : out_channel option;
mutable log_filename : string option;
mutable log_buffer_size : int;
mutable log_worker_id : bool;
mutable commands : bool;
mutable temporary_file_mode : temporary_file_mode;
mutable keep_going : bool;
mutable files_to_run : string list;
mutable files_not_to_run : string list;
mutable tests_to_run : string list;
mutable tests_not_to_run : string list;
mutable patterns_to_run : rex list;
mutable patterns_not_to_run : rex list;
mutable tags_to_run : string list;
mutable tags_not_to_run : string list;
mutable list : [`Ascii_art | `Tsv] option;
mutable global_timeout : float option;
mutable test_timeout : float option;
mutable retry : int;
mutable reset_regressions : bool;
mutable on_unknown_regression_files_mode : on_unknown_regression_files_mode;
mutable loop_mode : loop_mode;
mutable time : bool;
mutable record : string option;
mutable from_records : string list;
mutable resume_file : string option;
mutable resume : bool;
mutable job : (int * int) option;
mutable job_count : int;
mutable suggest_jobs : bool;
mutable junit : string option;
mutable skip : int;
mutable only : int option;
mutable test_args : string String_map.t;
mutable seed : int option;
}
let options =
{
color = Unix.isatty Unix.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 = Count 1;
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.iter close_out options.log_file
let set_log_file filename =
Option.iter close_out options.log_file ;
options.log_filename <- Some filename ;
options.log_file <- Some (open_out filename)
let init ?args () =
let set_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))
in
let set_job_count value =
if value < 1 then raise (Arg.Bad "--job-count must be positive") ;
options.job_count <- value
in
let set_loop_count value =
if value < 0 then raise (Arg.Bad "--loop-count must be positive or null") ;
options.loop_mode <- Count value
in
let set_job value =
match value =~** 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) ->
let int s =
match int_of_string_opt s with
| None -> raise (Arg.Bad ("value too large: " ^ s))
| Some i -> i
in
let index = int index in
let count = int count in
if index < 1 then raise (Arg.Bad "--job index must be at least 1")
else if count < 1 then raise (Arg.Bad "--job count must be at least 1")
else if index > count then
raise (Arg.Bad "--job index cannot be greater than job count")
else options.job <- Some (index, count)
in
let add_from_record path =
if not (Sys.is_directory path) then
options.from_records <- path :: options.from_records
else
let records =
Sys.readdir path |> Array.to_list
|> List.filter (fun name -> Filename.extension name = ".json")
|> List.map (fun name -> path // name)
in
options.from_records <- records @ options.from_records
in
let set_skip value =
if value < 0 then raise (Arg.Bad "--skip must be non-negative") ;
options.skip <- value
in
let set_only value =
if value <= 0 then raise (Arg.Bad "--only must be at least one") ;
options.only <- Some value
in
let set_on_unknown_regression_files_mode value =
options.on_unknown_regression_files_mode <-
(match value with
| "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)))
in
let add_test_arg value =
let len = String.length value in
let rec find_equal i =
if i >= len then None
else if value.[i] = '=' then Some i
else find_equal (i + 1)
in
let parameter, value =
match find_equal 0 with
| None -> (value, "true")
| Some i -> (String.sub value 0 i, String.sub value (i + 1) (len - i - 1))
in
options.test_args <- String_map.add parameter value options.test_args
in
let spec =
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.String set_log_level,
"<LEVEL> Set log level to LEVEL. Possible LEVELs are: quiet, error, \
warn, report, info, debug. Default is report." );
( "--log-file",
Arg.String set_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 (fun x -> 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
(fun file -> 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
(fun file -> options.files_to_run <- file :: options.files_to_run),
"<FILE> Same as --file." );
( "--not-file",
Arg.String
(fun file ->
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
(fun pattern ->
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
(fun pattern ->
options.patterns_to_run <-
Base.rex ~opts:[`Caseless] pattern :: options.patterns_to_run),
"<PERL_REGEXP> Same as --match." );
( "--not-match",
Arg.String
(fun pattern ->
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
(fun title -> options.tests_to_run <- title :: options.tests_to_run),
"<TITLE> Only run tests which are exactly entitled TITLE (see \
SELECTING TESTS)." );
( "--test",
Arg.String
(fun title -> options.tests_to_run <- title :: options.tests_to_run),
"<TITLE> Same as --title." );
( "-t",
Arg.String
(fun title -> options.tests_to_run <- title :: options.tests_to_run),
"<TITLE> Same as --title." );
( "--not-title",
Arg.String
(fun title ->
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
(fun title ->
options.tests_not_to_run <- title :: options.tests_not_to_run),
"<TITLE> Same as --not-title." );
( "--global-timeout",
Arg.Float (fun delay -> options.global_timeout <- Some delay),
"<SECONDS> Fail if the set of tests takes more than SECONDS to run."
);
( "--test-timeout",
Arg.Float (fun delay -> options.test_timeout <- Some delay),
"<SECONDS> Fail if a test takes, on its own, more than SECONDS to \
run." );
( "--retry",
Arg.Int (fun retry -> 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.String set_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.Int set_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 (fun file -> options.record <- Some file),
"<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.String add_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 (fun filename -> options.resume_file <- Some filename),
"<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.String set_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.Int set_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.Int set_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 (fun path -> options.junit <- Some path),
"<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.Int set_skip,
"<COUNT> Skip the first COUNT tests. This filter is applied after \
--job and before --only." );
( "--only",
Arg.Int set_only,
"<COUNT> Only run the first COUNT tests. This filter is applied \
after --job and --skip." );
( "--test-arg",
Arg.String add_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.String add_test_arg,
"<PARAMETER>=<VALUE> Same as --test-arg." );
( "--seed",
Arg.Int (fun seed -> options.seed <- Some seed),
"<SEED> Force tests declared with ~seed: Random to initialize the \
pseudo-random number generator with this seed." );
]
in
let usage =
"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"
in
let add_tag tag =
if tag = "" || tag.[0] <> '/' then
options.tags_to_run <- tag :: options.tags_to_run
else
options.tags_not_to_run <-
String.sub tag 1 (String.length tag - 1) :: options.tags_not_to_run
in
let argv =
let executable_name =
if Array.length Sys.argv > 0 then Sys.argv.(0) else Sys.executable_name
in
match args with
| None -> Sys.argv
| Some x -> Array.of_list (executable_name :: x)
in
try Arg.parse_argv argv spec add_tag usage with
| Arg.Bad msg ->
Printf.eprintf "%s" msg ;
exit 2
| Arg.Help msg ->
Printf.printf "%s" msg ;
exit 0
let () = init ()
let get_opt parse parameter =
match String_map.find_opt parameter options.test_args with
| Some value -> (
match parse value with
| None -> failwith (sf "invalid value for -a %s: %s" parameter value)
| Some value -> Some value)
| None -> None
let get ?default parse parameter =
match get_opt parse parameter with
| Some v -> v
| None -> (
match default with
| None ->
failwith
(sf
"missing test argument %s, please specify it with: -a %s=<VALUE>"
parameter
parameter)
| Some default -> default)
let get_bool ?default parameter = get ?default bool_of_string_opt parameter
let get_int ?default parameter = get ?default int_of_string_opt parameter
let get_float ?default parameter = get ?default float_of_string_opt parameter
let get_string ?default parameter = get ?default Option.some parameter
let get_bool_opt parameter = get_opt bool_of_string_opt parameter
let get_int_opt parameter = get_opt int_of_string_opt parameter
let get_float_opt parameter = get_opt float_of_string_opt parameter
let get_string_opt parameter = get_opt Option.some parameter