Source file ppx_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
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
open! Base
open Ppxlib
open Ast_builder.Default
open Ppx_expect_runtime_types [@@alert "-ppx_expect_runtime_types"]
let strict_indent = ref false
let allow_skipping_reachability_check = ref false
let allow_skipping_reachability_flag =
"-expect-test-allow-output-block-to-suppress-reachability-check"
;;
module Expr = struct
let option ~loc expression_of_a = function
| Some x -> [%expr Some [%e expression_of_a ~loc x]]
| None -> [%expr None]
;;
let pair ~loc expression_of_a expression_of_b (a, b) =
[%expr [%e expression_of_a ~loc a], [%e expression_of_b ~loc b]]
;;
let delimiter ~loc (delimiter : String_node_format.Delimiter.t) =
[%expr
([%e
match delimiter with
| T Quote -> [%expr T Quote]
| T (Tag tag) -> [%expr T (Tag [%e estring ~loc tag])]]
: (Ppx_expect_runtime.Delimiter.t[@alert "-ppx_expect_runtime"]))]
;;
let id ~loc id =
[%expr
(Ppx_expect_runtime.Expectation_id.of_int_exn [@alert "-ppx_expect_runtime"])
[%e eint ~loc (Expectation_id.to_int_exn id)]]
;;
let compact_loc ~loc ({ start_bol; start_pos; end_pos } : Compact_loc.t) =
[%expr
{ start_bol = [%e eint ~loc start_bol]
; start_pos = [%e eint ~loc start_pos]
; end_pos = [%e eint ~loc end_pos]
}]
;;
let payload ~loc ({ contents; tag } : Payload.t) =
[%expr { contents = [%e estring ~loc contents]; tag = [%e delimiter ~loc tag] }]
;;
let id_expr_alist ~loc alist =
List.map alist ~f:(fun (expect_id, expr) -> [%expr [%e id ~loc expect_id], [%e expr]])
|> elist ~loc
;;
let flexibility_of_strictness ~loc =
if !strict_indent
then
[%expr
Ppx_expect_runtime.Expect_node_formatting.Flexibility.Exactly_formatted
[@alert "-ppx_expect_runtime"]]
else
[%expr
Ppx_expect_runtime.Expect_node_formatting.Flexibility.Flexible_modulo
Ppx_expect_runtime.Expect_node_formatting.default
[@alert "-ppx_expect_runtime"]]
;;
end
let compact_loc_of_ppxlib_location { loc_start; loc_end; loc_ghost = _ } : Compact_loc.t =
{ start_bol = loc_start.pos_bol
; start_pos = loc_start.pos_cnum
; end_pos = loc_end.pos_cnum
}
;;
module Expectation_node = struct
type expect_node_info =
{ located_payload : (Payload.t * Compact_loc.t) option
; node_loc : Compact_loc.t
}
type t =
| Expect of expect_node_info
| Expect_exact of expect_node_info
| Expect_if_reached of expect_node_info
| Expect_unreachable of Compact_loc.t
| Expectation of expect_node_info
| Expectation_never_committed of Compact_loc.t
let id t ~loc =
match t with
| Expect _ ->
[%expr Ppx_expect_runtime.Test_node.Create.expect [@alert "-ppx_expect_runtime"]]
| Expect_exact _ ->
[%expr
Ppx_expect_runtime.Test_node.Create.expect_exact [@alert "-ppx_expect_runtime"]]
| Expect_if_reached _ ->
[%expr
Ppx_expect_runtime.Test_node.Create.expect_if_reached
[@alert "-ppx_expect_runtime"]]
| Expect_unreachable _ ->
[%expr
Ppx_expect_runtime.Test_node.Create.expect_unreachable
[@alert "-ppx_expect_runtime"]]
| Expectation _ ->
[%expr
Ppx_expect_runtime.Test_node.Create.expectation [@alert "-ppx_expect_runtime"]]
| Expectation_never_committed _ ->
[%expr
Ppx_expect_runtime.Test_node.Create.expectation_never_committed
[@alert "-ppx_expect_runtime"]]
;;
let to_expr ~loc t =
let make_expect_node node_expr { located_payload; node_loc } =
[%expr
[%e node_expr]
~formatting_flexibility:[%e Expr.flexibility_of_strictness ~loc]
~located_payload:
[%e Expr.(option ~loc (pair payload compact_loc)) located_payload]
~node_loc:[%e Expr.compact_loc ~loc node_loc]]
in
match t with
| Expect expect_node_info
| Expect_exact expect_node_info
| Expectation expect_node_info -> make_expect_node (id t ~loc) expect_node_info
| Expect_if_reached expect_node_info ->
if !allow_skipping_reachability_check
then make_expect_node (id t ~loc) expect_node_info
else (
let error_text =
Printf.sprintf
"ppx driver is not configured to accept [[%%expect.if_reached]] (can be \
enabled by adding [%s] to ppx driver flags)"
allow_skipping_reachability_flag
in
[%expr [%ocaml.error [%e estring ~loc error_text]]])
| Expect_unreachable node_loc ->
[%expr [%e id t ~loc] ~node_loc:[%e Expr.compact_loc ~loc node_loc]]
| Expectation_never_committed node_loc ->
[%expr [%e id t ~loc] ~node_loc:[%e Expr.compact_loc ~loc node_loc]]
;;
end
module Pattern = struct
open Ast_pattern
let string () =
map
(single_expr_payload (as__ (pexp_constant (pconst_string __ __ __))))
~f:(fun f payload_expr contents _loc tag ->
let (tag : String_node_format.Delimiter.t) =
match tag with
| None -> T Quote
| Some tag -> T (Tag tag)
in
let payload_loc = compact_loc_of_ppxlib_location payload_expr.pexp_loc in
let located_payload = Some (({ contents; tag } : Payload.t), payload_loc) in
f ~located_payload)
;;
let empty () = pstr nil
let maybe_string () = string () ||| map (empty ()) ~f:(fun f -> f ~located_payload:None)
end
let maybe_string_payload = Pattern.maybe_string
module Parsed_node = struct
type t =
| Expectation_node of Expectation_id.t * Expectation_node.t
| Expectation_result_node of Expectation_id.t * Expectation_node.t
| Output
open struct
let declare_expect name constructor =
Extension.Expert.declare
name
Expression
(Pattern.maybe_string ())
(fun ~located_payload node_loc ->
Expectation_node
( Expectation_id.lookup_or_mint Parsed node_loc
, constructor located_payload node_loc ))
;;
end
let expect =
declare_expect "expect" (fun located_payload node_loc ->
Expect { located_payload; node_loc })
;;
let expect_exact =
declare_expect "expect_exact" (fun located_payload node_loc ->
Expect_exact { located_payload; node_loc })
;;
let expect_if_reached =
declare_expect "expect.if_reached" (fun located_payload node_loc ->
Expect_if_reached { located_payload; node_loc })
;;
let expectation =
Extension.Expert.declare
"expectation"
Expression
(Pattern.maybe_string ())
(fun ~located_payload node_loc ->
Expectation_result_node
( Expectation_id.lookup_or_mint Parsed node_loc
, Expectation { located_payload; node_loc } ))
;;
let expect_output =
Extension.Expert.declare "@expect.output" Expression (Pattern.empty ()) (fun _ ->
Output)
;;
let expect_unreachable =
Extension.Expert.declare
"@expect.unreachable"
Expression
(Pattern.empty ())
(fun compact_loc ->
Expectation_node
( Expectation_id.lookup_or_mint Parsed compact_loc
, Expect_unreachable compact_loc ))
;;
let expectation_never_committed =
Extension.Expert.declare
"@expectation.never_committed"
Expression
(Pattern.empty ())
(fun compact_loc ->
Expectation_result_node
( Expectation_id.lookup_or_mint Parsed compact_loc
, Expectation_never_committed compact_loc ))
;;
let match_expectation =
let expectations =
[ expect
; expect_exact
; expect_if_reached
; expect_output
; expect_unreachable
; expectation
; expectation_never_committed
]
in
function
| { pexp_desc = Pexp_extension extension; pexp_loc; _ } ->
Extension.Expert.convert expectations ~loc:pexp_loc extension
| _ -> None
;;
end
let is_a_ppx_expect_ext_node e = Option.is_some (Parsed_node.match_expectation e)
let replace_and_collect_expects =
object
inherit [(Expectation_id.t, expression) List.Assoc.t] Ast_traverse.fold_map as super
method! expression ({ pexp_attributes; pexp_loc = loc; _ } as expr) acc =
match Parsed_node.match_expectation expr with
| None -> super#expression expr acc
| Some expect_node ->
let expr, acc =
match expect_node (compact_loc_of_ppxlib_location loc) with
| Expectation_node (id, expect_expr) ->
( [%expr Ppx_expect_test_block.run_test ~test_id:[%e Expr.id ~loc id]]
, (id, Expectation_node.to_expr expect_expr ~loc) :: acc )
| Expectation_result_node (id, expect_expr) ->
( [%expr
Ppx_expect_test_block.run_test_without_commiting
~test_id:[%e Expr.id ~loc id]]
, (id, Expectation_node.to_expr expect_expr ~loc) :: acc )
| Output ->
[%expr Ppx_expect_test_block.read_test_output_no_backtrace_check ()], acc
in
Merlin_helpers.hide_expression { expr with pexp_attributes }, acc
end
;;
let transform_let_expect
~trailing_location
~tags
~expected_exn
~description
~loc
~attrs
body
=
let body, expectations = replace_and_collect_expects#expression body [] in
let body_thunk =
{ ([%expr fun () -> [%e body]]) with pexp_attributes = attrs }
in
let filename_rel_to_project_root =
Ppx_here_expander.expand_filename loc.loc_start.pos_fname
in
let trailing_location = compact_loc_of_ppxlib_location trailing_location in
let body_loc =
compact_loc_of_ppxlib_location
{ loc_start = loc.loc_start; loc_end = body.pexp_loc.loc_end; loc_ghost = true }
in
let trailing_test_id = Expectation_id.lookup_or_mint Trailing body_loc in
let exn_test_id = Expectation_id.lookup_or_mint Exception body_loc in
[%expr
match Ppx_inline_test_lib.testing () with
| `Not_testing -> ()
| `Testing _ ->
if Ppx_inline_test_lib.force_drop
then ()
else
let module Ppx_expect_test_block =
Ppx_expect_runtime.Make_test_block [@alert "-ppx_expect_runtime"]
(Expect_test_config)
in
Ppx_expect_test_block.run_suite
~filename_rel_to_project_root:[%e estring ~loc filename_rel_to_project_root]
~line_number:[%e eint ~loc loc.loc_start.pos_lnum]
~location:[%e Expr.compact_loc ~loc (compact_loc_of_ppxlib_location loc)]
~trailing_loc:[%e Expr.compact_loc ~loc trailing_location]
~body_loc:[%e Expr.compact_loc ~loc body_loc]
~formatting_flexibility:[%e Expr.flexibility_of_strictness ~loc]
~expected_exn:[%e Expr.(option ~loc (pair payload compact_loc)) expected_exn]
~trailing_test_id:[%e Expr.id ~loc trailing_test_id]
~exn_test_id:[%e Expr.id ~loc exn_test_id]
~description:[%e Expr.option estring ~loc description]
~tags:[%e tags |> List.map ~f:(estring ~loc) |> elist ~loc]
~inline_test_config:(module Inline_test_config)
~expectations:
[%e Merlin_helpers.hide_expression (Expr.id_expr_alist ~loc expectations)]
[%e body_thunk]]
;;
let let_expect_pat =
let open Ast_pattern in
let uncaught_exn =
Attribute.declare_with_attr_loc
"@expect.uncaught_exn"
Attribute.Context.value_binding
(Pattern.string ())
(fun ~attr_loc ~located_payload -> attr_loc, located_payload)
in
let opt_name =
map (pstring __) ~f:(fun f x -> f ~name:(Some x))
||| map ppat_any ~f:(fun f -> f ~name:None)
in
pstr
(pstr_value
nonrecursive
(Attribute.pattern
uncaught_exn
(value_binding_attributes
__
(value_binding
~pat:
(map
(Attribute.pattern Ppx_inline_test.tags opt_name)
~f:(fun f attributes ->
f ~tags:(Option.value ~default:[] attributes)))
~expr:__))
^:: nil)
^:: nil)
;;
let expect_test =
Extension.V3.declare_inline
"expect_test"
Structure_item
let_expect_pat
(fun ~ctxt trailing attrs ~tags ~name code ->
let loc = Ppxlib.Expansion_context.Extension.extension_point_loc ctxt in
let loc = { loc with loc_ghost = true } in
let trailing_location, expected_exn =
match trailing with
| Some (attr_loc, expected_exn) -> attr_loc, expected_exn
| None -> { loc with loc_start = loc.loc_end }, None
in
Ppx_inline_test.validate_extension_point_exn
~name_of_ppx_rewriter:"ppx_expect"
~loc
~tags;
transform_let_expect
~trailing_location
~tags
~expected_exn
~description:name
~loc
~attrs
code
|> Ppx_inline_test.maybe_drop loc)
;;
let () =
Driver.add_arg
"-expect-test-strict-indentation"
(Bool (( := ) strict_indent))
~doc:
(Printf.sprintf
"BOOL Require standardized indentation in [[%%expect]] (default: %b)"
!strict_indent);
Driver.add_arg
allow_skipping_reachability_flag
(Bool (( := ) allow_skipping_reachability_check))
~doc:
(Printf.sprintf
"BOOL permit the [[%%expect.if_reached]] extension (default: %b)"
!allow_skipping_reachability_check)
;;
let () =
Driver.register_transformation
"expect_test"
~rules:[ Context_free.Rule.extension expect_test ]
~enclose_impl:(fun source_file_loc ->
match source_file_loc, Ppx_inline_test_libname.get () with
| Some loc, Some _ ->
let loc = { loc with loc_ghost = true } in
let filename_rel_to_project_root =
Ppx_here_expander.expand_filename loc.loc_start.pos_fname
in
let =
let loc = { loc with loc_end = loc.loc_start } in
Ppx_inline_test.guard_toplevel_test_effects
loc
[%expr
(Ppx_expect_runtime.Current_file.set [@alert "-ppx_expect_runtime"])
~filename_rel_to_project_root:
[%e estring ~loc filename_rel_to_project_root]]
and =
let loc = { loc with loc_start = loc.loc_end } in
Ppx_inline_test.guard_toplevel_test_effects
loc
[%expr
(Ppx_expect_runtime.Current_file.unset [@alert "-ppx_expect_runtime"]) ()]
in
header, footer
| _ -> [], [])
;;