Source file regression.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
open Base
let capture_output : (string -> unit) option ref = ref None
let capture line =
match !capture_output with
| None -> ()
| Some output ->
output line ;
output "\n"
let hooks : Process_hooks.t =
{
on_spawn =
(fun command arguments ->
let message = Log.quote_shell_command command arguments in
capture "" ;
capture message);
on_log = capture;
}
let run_and_capture_output ~capture (f : unit -> 'a Lwt.t) =
capture_output := Some capture ;
Lwt.finalize f @@ fun () ->
capture_output := None ;
unit
let run_and_capture_output_to_file ~output_file (f : unit -> 'a Lwt.t) =
let rec create_parent filename =
let parent = Filename.dirname filename in
if String.length parent < String.length filename then (
create_parent parent ;
if not (Sys.file_exists parent) then
try Unix.mkdir parent 0o755
with Unix.Unix_error (EEXIST, _, _) ->
())
in
create_parent output_file ;
let channel = open_out output_file in
capture_output := Some (output_string channel) ;
Lwt.finalize f @@ fun () ->
capture_output := None ;
close_out channel ;
unit
let output_dirs_and_files : String_set.t String_map.t ref = ref String_map.empty
let register ~__FILE__ ~title ~tags ?file f =
let tags = "regression" :: tags in
let output_dir = project_root // Filename.dirname __FILE__ // "expected" in
let relative_output_file =
let file =
match file with
| Some file -> file
| None ->
let sanitize_char = function
| ( 'a' .. 'z'
| 'A' .. 'Z'
| '0' .. '9'
| '_' | '-' | '.' | ' ' | '(' | ')' ) as x ->
x
| _ -> '-'
in
let full = String.map sanitize_char title in
let max_length = 80 in
if String.length full > max_length then String.sub full 0 max_length
else full
in
Filename.basename __FILE__ // (file ^ ".out")
in
let old_relative_output_files =
String_map.find_opt output_dir !output_dirs_and_files
|> Option.value ~default:String_set.empty
in
let stored_full_output_file = output_dir // relative_output_file in
if String_set.mem relative_output_file old_relative_output_files then
invalid_arg
(sf
"the output of test %S would be stored in %S, which is already used \
by another test"
title
stored_full_output_file) ;
output_dirs_and_files :=
String_map.add
output_dir
(String_set.add relative_output_file old_relative_output_files)
!output_dirs_and_files ;
Test.register ~__FILE__ ~title ~tags @@ fun () ->
if
not
(Sys.file_exists stored_full_output_file || Cli.options.reset_regressions)
then
Test.fail
"Regression output file not found: %s. To generate it, use: \
--reset-regressions --title %s"
(Log.quote_shell stored_full_output_file)
(Log.quote_shell title) ;
if Cli.options.reset_regressions then
run_and_capture_output_to_file ~output_file:stored_full_output_file f
else
let* after =
let buffer = Buffer.create 512 in
let* () = run_and_capture_output ~capture:(Buffer.add_string buffer) f in
Buffer.contents buffer |> String.split_on_char '\n' |> Array.of_list
|> return
in
let before =
read_file stored_full_output_file
|> String.split_on_char '\n' |> Array.of_list
in
let diff =
Diff.arrays
~equal:String.equal
~before:stored_full_output_file
~after:"captured"
before
after
in
if diff.different then (
Diff.log (Diff.reduce_context diff) ;
Test.fail
"Regression output file contains differences: %s. To accept the \
differences, use: --reset-regressions --title %s"
(Log.quote_shell stored_full_output_file)
(Log.quote_shell title)) ;
unit
let check_unknown_output_files output_dir relative_output_files =
let full_output_files =
String_set.map
(fun relative_output_file -> output_dir // relative_output_file)
relative_output_files
in
let found_unknown = ref false in
let mode = Cli.options.on_unknown_regression_files_mode in
let log_unused = match mode with Fail -> Log.error | _ -> Log.warn in
let rec browse path =
let handle_file filename =
let full = path // filename in
match Sys.is_directory full with
| exception Sys_error _ ->
()
| true -> browse full
| false ->
if not (String_set.mem full full_output_files) then
if mode = Delete then
try
Sys.remove full ;
Log.report "Deleted file: %s" full
with Sys_error message ->
Log.warn "Failed to delete file: %s" message
else (
log_unused "%s is not used by any test and can be deleted." full ;
found_unknown := true)
in
let try_to_read_dir () =
try Sys.readdir path
with Sys_error _ ->
[||]
in
Array.iter handle_file (try_to_read_dir ()) ;
match Sys.readdir path with
| exception Sys_error _ -> ()
| [||] ->
if mode = Delete then
try
Sys.rmdir path ;
Log.report "Deleted directory: %s" path
with Sys_error message ->
Log.warn "Failed to delete directory: %s" message
else (
log_unused "%s is empty and can be deleted." path ;
found_unknown := true)
| _ -> ()
in
browse output_dir ;
!found_unknown
let () =
Test.before_test_run @@ fun () ->
let check_all_unknown_output_files () =
String_map.fold
(fun output_dir relative_output_files found_unknown ->
check_unknown_output_files output_dir relative_output_files
|| found_unknown)
!output_dirs_and_files
false
in
let warn_unknown_output_files () =
Log.warn
"Use --on-unknown-regression-files delete to delete those files and/or \
directories."
in
match Cli.options.on_unknown_regression_files_mode with
| Ignore -> ()
| Warn ->
let found_unknown = check_all_unknown_output_files () in
if found_unknown then warn_unknown_output_files ()
| Fail ->
let found_unknown = check_all_unknown_output_files () in
if found_unknown then (
warn_unknown_output_files () ;
exit 1)
| Delete ->
let _ = check_all_unknown_output_files () in
exit 0