Source file b0_expect.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
open B0_std
open Result.Syntax
let pp_cli_arg fmt = Fmt.st' [`Underline] fmt
let fpath_pp_high_suffix pre ppf p = match Fpath.strip_prefix pre p with
| None -> (Fmt.code' Fpath.pp) ppf p
| Some p ->
Fpath.pp ppf pre;
(if not (Fpath.is_syntactic_dir pre)
then Fmt.char ppf Fpath.natural_dir_sep_char);
(Fmt.code' Fpath.pp) ppf p
exception Abort of string
let abort msg = raise (Abort msg)
let abortf fmt = Format.kasprintf abort fmt
let result_to_abort = function Ok v -> v | Error msg -> abort msg
let abort_to_result f = try Ok (f ()) with Abort e -> Error e
module Outcome = struct
type status = [ `Corrected | `Expected | `New | `Unexpected | `Unknown ]
type test = File of { file : Fpath.t; diff : bool }
type t = { status : status; test : test }
let v status test = { status; test }
let status o = o.status
let test o = o.test
end
type t =
{ base : Fpath.t;
env : B0_env.t;
log_absolute : bool;
log_diffs : bool;
mutable outcomes : Outcome.t list;
mutable seen : Fpath.Set.t;
time : Os.Mtime.counter;
vcs_repo : B0_vcs_repo.t; }
let make ?vcs_repo ?(log_absolute = false) ?(log_diffs = true) env ~base =
let scope_dir = B0_env.scope_dir env in
let base = Fpath.(scope_dir // base) in
let outcomes = [] and seen = Fpath.Set.empty in
let time = Os.Mtime.counter () in
let vcs_repo = match vcs_repo with
| None -> B0_vcs_repo.get ~dir:scope_dir () |> result_to_abort
| Some vcs -> vcs
in
{ base; env; log_absolute; log_diffs; outcomes; seen; time; vcs_repo }
let base ctx = ctx.base
let base_files ?rel ctx ~recurse =
result_to_abort @@
Os.Dir.fold_files ?rel ~recurse Os.Dir.path_list ctx.base []
let dur ctx = Os.Mtime.count ctx.time
let env ctx = ctx.env
let get_unit_exe_file_cmd ctx u =
B0_env.unit_exe_file_cmd ctx.env u |> result_to_abort
let log_absolute ctx = ctx.log_absolute
let log_diffs ctx = ctx.log_diffs
let vcs_repo ctx = ctx.vcs_repo
let outcomes ctx = ctx.outcomes
let cwd_rel_path ctx p = Fpath.relative ~to_dir:(B0_env.cwd ctx.env) p
let path_for_user ctx p = if ctx.log_absolute then p else cwd_rel_path ctx p
let pp_path_for_user ctx ppf p = match ctx.log_absolute with
| true -> fpath_pp_high_suffix (B0_env.scope_dir ctx.env) ppf p
| false -> (Fmt.code' Fpath.pp) ppf (cwd_rel_path ctx p)
let log_diff ctx file = match B0_vcs_repo.kind ctx.vcs_repo with
| Git ->
let git = B0_vcs_repo.repo_cmd ctx.vcs_repo in
let color = match Fmt.styler () with Fmt.Ansi -> true | _ -> false in
let color = Cmd.(if' color (arg "--color=always")) in
let cmd = Cmd.(git % "--no-pager" % "diff" %% color %% path file) in
Log.if_error ~use:() (Os.Cmd.run cmd)
| Hg ->
failwith "Hg support is TODO"
let log_outcome ctx o =
let label ppf st l = Fmt.st st ppf (String.concat " " ["";l;""]) in
let pp_label ppf = function
| `Unexpected -> label ppf [`Bg `Red; `Fg `White] "M"
| `New -> label ppf [`Bg `Yellow; `Fg `Black] "?"
in
match Outcome.test o with
| File { file; diff } ->
match Outcome.status o with
| `Unexpected | `New as st ->
Log.stdout
(fun m -> m "%a %a" pp_label st (pp_path_for_user ctx) file);
if ctx.log_diffs && diff then log_diff ctx file
| _ -> ()
let pp_vcs_cmd vcs ?(file = false) ppf cmd =
let pp_file_arg ppf () = Fmt.(pp_cli_arg string) ppf "file" in
let file = if file then pp_file_arg else Fmt.nop in
Fmt.pf ppf "%a %a" Fmt.code (String.concat " " [vcs; cmd]) file ()
let pp_git = pp_vcs_cmd "git"
let pp_hg = pp_vcs_cmd "hg"
let pp_new_cmd ppf vcs = match B0_vcs_repo.kind vcs with
| Git -> pp_git ~file:true ppf "add"
| Hg -> pp_hg ppf ~file:true "TODO"
let pp_correct_cmd ppf vcs = match B0_vcs_repo.kind vcs with
| Git -> pp_git ~file:true ppf "add -p"
| Hg -> pp_hg ~file:true ppf "TODO"
let pp_unexpected_cmd ppf vcs = match B0_vcs_repo.kind vcs with
| Git -> pp_git ~file:true ppf "diff"
| Hg -> pp_hg ppf ~file:true "TODO"
let pp_status_cmd ppf (vcs, dir) = match B0_vcs_repo.kind vcs with
| Git -> pp_git ppf ("status -s " ^ Fpath.to_string dir)
| Hg -> pp_hg ppf "TODO"
let pp_diff_cmd ppf (vcs, dir) = match B0_vcs_repo.kind vcs with
| Git -> pp_git ppf ("diff " ^ Fpath.to_string dir)
| Hg -> pp_hg ppf "TODO"
let pp_status st status =
Fmt.st' st (fun ppf c -> Fmt.pf ppf "%d %s" c status)
let pp_corrected ppf n = if n = 0 then () else Fmt.pf ppf " (%d corrected)" n
let pp_expected = pp_status [`Fg `Green] "expected"
let pp_unexpected = pp_status [`Fg `Red] "unexpected"
let pp_new = pp_status [`Fg `Yellow] "new"
let pp_unknown = pp_status [`Fg `Red] "unknown"
let pp_expected ppf = function
| (0, _) -> () | (n, c) -> Fmt.pf ppf "@,%a%a" pp_expected n pp_corrected c
let pp_unexpected ppf = function
| (0, _) -> () | (n, vcs) ->
Fmt.pf ppf "@,%a (check with %a, correct with %a)"
pp_unexpected n pp_unexpected_cmd vcs pp_correct_cmd vcs
let pp_new ppf = function
| (0, _) -> () | (n, vcs) ->
Fmt.pf ppf "@,%a (integrate with %a)" pp_new n pp_new_cmd vcs
let pp_unknown ppf = function 0 -> () | n -> Fmt.pf ppf "@,%a" pp_unknown n
let pp_all_pass ppf (count, corr, dur) =
let test = if count > 1 then "tests expected" else "test expected" in
let green = [`Fg `Green] in
Fmt.pf ppf "%a %a%a in %a"
(Fmt.st green) "All" (pp_status green test) count
pp_corrected corr Mtime.Span.pp dur
let pp_total ppf (count, dur) =
let test = if count > 1 then "tests" else "test" in
Fmt.pf ppf "@,%a in %a" (pp_status [`Bold] test) count Mtime.Span.pp dur
let log_summary ctx =
let expected = ref 0 and unexpected = ref 0 and new' = ref 0
and corrected = ref 0 and unknown = ref 0 in
let incr o = match Outcome.status o with
| `Expected -> incr expected | `Unexpected -> incr unexpected
| `New -> incr new' | `Corrected -> incr expected; incr corrected
| `Unknown -> incr unknown
in
let count = List.length ctx.outcomes in
let () = List.iter incr ctx.outcomes in
match !expected = count with
| true ->
Log.stdout (fun m -> m "%a" pp_all_pass (count, !corrected, dur ctx));
Os.Exit.ok
| false ->
Log.stdout (fun m ->
m "@[<v> @[<v>%a%a%a%a%a@]@,@,\
Summary with %a@,Details with %a@]"
pp_expected (!expected, !corrected) pp_new (!new', ctx.vcs_repo)
pp_unknown !unknown pp_unexpected (!unexpected, ctx.vcs_repo)
pp_total (count, dur ctx)
pp_status_cmd (ctx.vcs_repo, path_for_user ctx ctx.base)
pp_diff_cmd (ctx.vcs_repo, path_for_user ctx ctx.base));
Os.Exit.code 1
let outcome_of_file ?(diff = true) ctx file =
let file = Fpath.(ctx.base // file) in
match B0_vcs_repo.kind ctx.vcs_repo with
| Git ->
let git = B0_vcs_repo.repo_cmd ctx.vcs_repo in
let cmd = Cmd.(git % "status" % "--porcelain" %% path file) in
let status = Os.Cmd.run_out ~trim:false cmd |> result_to_abort in
let status = match String.take_first 2 status with
| "" -> `Expected
| "M " | "A " -> `Corrected
| "??" -> `New
| s when s.[1] = 'M' -> `Unexpected
| _ -> `Unknown
in
Outcome.v status (File { file; diff })
| Hg ->
failwith "Hg support is TODO"
let check_add ctx o = match Outcome.test o with
| File { file; _ } ->
if not (Fpath.Set.mem file ctx.seen)
then ctx.seen <- Fpath.Set.add file ctx.seen
else Fmt.invalid_arg "%a: file already checked. \
Are you trying to write it multiple times ?"
Fpath.pp_unquoted file
let add_outcome ctx o =
check_add ctx o; ctx.outcomes <- o :: ctx.outcomes; log_outcome ctx o
let finish ctx = log_summary ctx
let file ?diff ctx file = add_outcome ctx (outcome_of_file ctx file)
let stdout ?diff ctx ?env ?cwd ?stdout cmd =
let out = match stdout with
| Some stdout -> stdout
| None ->
Fpath.v (Fpath.basename (Cmd.find_tool cmd |> Option.get) ^ ".stdout")
in
let out = Fpath.(ctx.base // out) in
let stdout = Os.Cmd.out_file ~force:true ~make_path:true out in
let () = Os.Cmd.run ?env ?cwd ~stdout cmd |> result_to_abort in
file ?diff ctx out
let stderr ?diff ctx ?env ?cwd ?stderr cmd =
let out = match stderr with
| Some stderr -> stderr
| None ->
Fpath.v (Fpath.basename (Cmd.find_tool cmd |> Option.get) ^ ".stderr")
in
let out = Fpath.(ctx.base // out) in
let stderr = Os.Cmd.out_file ~force:true ~make_path:true out in
let _status = Os.Cmd.run_status ?env ?cwd ~stderr cmd |> result_to_abort in
file ?diff ctx out
let short =
let doc = "Short output, do not output diffs on unexpected or new tests." in
Cmdliner.Arg.(value & flag & info ["s"; "short"] ~doc)
let log_absolute_arg =
let doc =
"Output absolute paths instead of having them relative to the \
current working directory."
in
Cmdliner.Arg.(value & flag & info ["a"; "absolute-paths"] ~doc)
let exits =
Cmdliner.Cmd.Exit.info 1 ~doc:"on unexpected expectations." ::
Cmdliner.Cmd.Exit.defaults
let run f env base short log_absolute =
Os.Exit.of_result' @@
abort_to_result @@ fun () ->
let ctx = make env ~log_absolute ~log_diffs:(not short) ~base in
f ctx; finish ctx
let action_func ~base f =
B0_unit.Action.of_cmdliner_term ~exits @@ fun env u ->
let run = run f env base in
Cmdliner.Term.(const run $ short $ log_absolute_arg)