Source file quick_test_attributes.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
open! Core
open Ppxlib
module String_constant = struct
type t =
{ string_value : string
; location : location
; delimiter : string option
}
end
module Sexp_examples = struct
type t =
| Provided of
{ string_constants : String_constant.t list
; expression_placement_cnum : int
}
| NotProvided
end
module Shrinker = struct
type t =
| Custom of expression
| Default of core_type
end
module Generator = struct
type t =
| Custom of expression
| Default of core_type
end
module Attribute_name = struct
type t =
| Config
| Cr
| Examples
| Hide_positions
| Generator
| Shrinker
| Remember_failures
| Remember_failures_ignore
let to_string t =
let prefix = "quick_test" in
let body =
match t with
| Config -> "config"
| Cr -> "cr"
| Examples -> "examples"
| Hide_positions -> "hide_positions"
| Generator -> "generator"
| Shrinker -> "shrinker"
| Remember_failures -> "remember_failures"
| Remember_failures_ignore -> "remember_failures.ignore"
in
[%string "%{prefix}.%{body}"]
;;
let pass_throughs =
[ Config, Quick_test_parameter.Config
; Cr, Quick_test_parameter.Cr
; Hide_positions, Quick_test_parameter.Hide_positions
; Examples, Quick_test_parameter.Examples
]
;;
let test_scoped =
[ Config; Cr; Examples; Remember_failures; Remember_failures_ignore; Hide_positions ]
;;
let type_scoped = [ Generator; Shrinker ]
end
type t =
{ pass_through_attrs : (Quick_test_parameter.t * expression) list
; sexp_examples : Sexp_examples.t
; generators : Generator.t list
; shrinkers : Shrinker.t list
}
module Parse_result = struct
type nonrec t =
{ new_pattern : pattern
; new_parameters : (pattern * core_type) list
; attributes : t
}
end
let declare_single_expr_attribute (name : Attribute_name.t) ~context =
Attribute.declare
(Attribute_name.to_string name)
context
Ast_pattern.(single_expr_payload __)
Fn.id
;;
let pass_through_attributes =
Attribute_name.pass_throughs
|> List.map ~f:(fun (attribute_name, parameter_name) ->
( parameter_name
, declare_single_expr_attribute attribute_name ~context:Attribute.Context.Pattern ))
;;
let sexp_examples_attribute =
Attribute.declare_with_attr_loc
(Attribute_name.to_string Remember_failures)
Attribute.Context.Pattern
Ast_pattern.(alt_option (single_expr_payload __) (pstr nil))
(fun ~attr_loc e -> attr_loc, e)
;;
let generator_attribute =
declare_single_expr_attribute
Attribute_name.Generator
~context:Attribute.Context.Core_type
;;
let shrinker_attribute =
declare_single_expr_attribute
Attribute_name.Shrinker
~context:Attribute.Context.Core_type
;;
let assert_no_unused_attributes pattern =
let unused_attributes = Attribute.collect_unused_attributes_errors#pattern pattern [] in
match unused_attributes with
| [] -> ()
| error :: _ ->
let format_names names =
names
|> List.map ~f:Attribute_name.to_string
|> List.map ~f:(Format.sprintf "* [@%s]")
|> String.concat ~sep:"\n"
in
let message =
[%string
{|%{Location.Error.message error}
"ppx_quick_test" found an unexpected attribute.
Supported test-scoped attributes:
%{format_names Attribute_name.test_scoped}
Supported type-scoped attributes:
%{format_names Attribute_name.type_scoped}
For example:
let%quick_test _ [@quick_test.hide_positions true]
= fun (x : int [@quick_test.generator Base_quickcheck.Generator.int_uniform]) -> assert ( * x >= 0)
|}]
in
let error = Location.Error.set_message error message in
Location.Error.raise error
;;
let parse_attribute_from_context ~context ~attribute =
Attribute.consume_res attribute context
|> Result.map ~f:(function
| Some (new_context, expr) -> new_context, Some expr
| None -> context, None)
|> function
| Ok res -> res
| Error error_list ->
let name = Attribute.name attribute in
let error = Stdppx.NonEmptyList.hd error_list in
let message =
[%string
{|"ppx_quick_test" found incorrect use of attribute `%{name}': %{Location.Error.message error}. Example of correct usage:
{[
let%%quick_test "my test" [@quick_test.cr CR.CR_someday] = fun (x : int) -> assert (x * x >= 0)
]} |}]
in
let error = Location.Error.set_message error message in
Location.Error.raise error
;;
let parse_pass_through_attributes_from_pattern pattern =
let pattern, attributes =
List.fold_map
pass_through_attributes
~init:pattern
~f:(fun pattern (param_name, attribute) ->
let pattern, expr = parse_attribute_from_context ~context:pattern ~attribute in
pattern, Option.map expr ~f:(fun expr -> param_name, expr))
in
let attributes = List.filter_opt attributes in
pattern, attributes
;;
let parse_string_constant_from_expression expression =
match expression.pexp_desc with
| Pexp_constant (Pconst_string (contents, loc, delimiter)) ->
{ String_constant.string_value = contents; location = loc; delimiter }
| _ ->
Location.raise_errorf
~loc:expression.pexp_loc
{|"ppx_quick_test" expected this expression to be a string literal constant|}
;;
let parse_expression_from_unlabeled_argument argument =
match fst argument with
| Nolabel -> snd argument
| _ ->
Location.raise_errorf
~loc:(snd argument).pexp_loc
{|"ppx_quick_test" expected this argument to unlabeled|}
;;
let parse_string_constant_list_from_expression expr =
Option.value_map ~default:[] expr ~f:(fun expr ->
match expr.pexp_desc with
| Pexp_apply (first, rest) ->
let exprs = first :: List.map rest ~f:parse_expression_from_unlabeled_argument in
List.map exprs ~f:parse_string_constant_from_expression
| _ -> [ parse_string_constant_from_expression expr ])
;;
let parse_sexp_examples_attribute pattern =
let pattern, sexp_examples_payload =
parse_attribute_from_context ~context:pattern ~attribute:sexp_examples_attribute
in
let sexp_examples =
match sexp_examples_payload with
| None -> Sexp_examples.NotProvided
| Some (attr_location, expr) ->
Sexp_examples.Provided
{ string_constants = parse_string_constant_list_from_expression expr
; expression_placement_cnum =
(match expr with
| Some expr -> expr.pexp_loc.loc_end.pos_cnum
| None -> attr_location.loc_end.pos_cnum - 1)
}
in
pattern, sexp_examples
;;
let parse_generators_and_shrinkers parameters =
let resolve_generator ~default ~generator_payload =
match generator_payload with
| None -> Generator.Default default
| Some expr -> Generator.Custom expr
in
let resolve_shrinker ~default ~shrinker_payload =
match shrinker_payload with
| None -> Shrinker.Default default
| Some expr -> Shrinker.Custom expr
in
let parse_generator_and_shrinker (pattern, type_) =
let type_, generator_payload =
parse_attribute_from_context ~context:type_ ~attribute:generator_attribute
in
let type_, shrinker_payload =
parse_attribute_from_context ~context:type_ ~attribute:shrinker_attribute
in
let generator = resolve_generator ~default:type_ ~generator_payload in
let shrinker = resolve_shrinker ~default:type_ ~shrinker_payload in
(pattern, type_), generator, shrinker
in
parameters |> List.map ~f:parse_generator_and_shrinker |> List.unzip3
;;
let parse ~pattern ~parameters =
let pattern, pass_through_attrs = parse_pass_through_attributes_from_pattern pattern in
let pattern, sexp_examples = parse_sexp_examples_attribute pattern in
let parameters, generators, shrinkers = parse_generators_and_shrinkers parameters in
assert_no_unused_attributes pattern;
let attributes = { pass_through_attrs; sexp_examples; generators; shrinkers } in
{ Parse_result.new_pattern = pattern; new_parameters = parameters; attributes }
;;
let expand_string_constant_to_expression
{ String_constant.string_value; location = loc; delimiter }
=
let open (val Ast_builder.make loc) in
pexp_constant (Pconst_string (string_value, loc, delimiter))
;;
let create_list_expression expr_list ~loc =
let open (val Ast_builder.make loc) in
let list_expr =
List.fold_right expr_list ~init:[%expr []] ~f:(fun list_elem acc ->
[%expr [%e list_elem] :: [%e acc]])
in
list_expr
;;
let create_int_expression n ~loc =
let open (val Ast_builder.make loc) in
Pconst_integer (Int.to_string n, None) |> pexp_constant
;;
let expand_not_provided_sexp_examples_expression ~loc =
let open (val Ast_builder.make loc) in
[%expr Ppx_quick_test_runtime_lib.Sexp_examples.NotProvided]
;;
let expand_provided_sexp_examples_expression
~loc
~input_type
~string_constants
~expression_placement_cnum
=
let open (val Ast_builder.make loc) in
let string_constant_exprs =
List.map string_constants ~f:expand_string_constant_to_expression
in
[%expr
Ppx_quick_test_runtime_lib.Sexp_examples.Provided
{ sexp_strings = [%e create_list_expression ~loc string_constant_exprs]
; of_sexp = [%of_sexp: [%t input_type]]
; expression_placement_cnum =
[%e create_int_expression ~loc expression_placement_cnum]
}]
;;
let expand_sexp_examples_argument sexp_examples ~loc ~input_type =
let open (val Ast_builder.make loc) in
let open Merlin_helpers in
let expr =
match sexp_examples with
| Sexp_examples.NotProvided -> expand_not_provided_sexp_examples_expression ~loc
| Sexp_examples.Provided
{ string_constants : String_constant.t list; expression_placement_cnum : int } ->
expand_provided_sexp_examples_expression
~loc
~input_type
~string_constants
~expression_placement_cnum
in
Quick_test_parameter.Sexp_examples, hide_expression expr
;;
let expand_generator_argument (generators : Generator.t list) ~loc =
let open (val Ast_builder.make loc) in
let open Merlin_helpers in
let generator_type =
generators
|> List.map ~f:(function
| Default type_ -> type_
| Custom expr -> [%type: [%custom [%e expr]]])
|> ptyp_tuple
in
let expr = hide_expression [%expr [%quickcheck.generator: [%t generator_type]]] in
Quick_test_parameter.Generator, expr
;;
let expand_shrinker_argument (shrinkers : Shrinker.t list) ~loc =
let open (val Ast_builder.make loc) in
let open Merlin_helpers in
let shrinkers_type =
shrinkers
|> List.map ~f:(function
| Default type_ -> type_
| Custom expr -> [%type: [%custom [%e expr]]])
|> ptyp_tuple
in
let expr = hide_expression [%expr [%quickcheck.shrinker: [%t shrinkers_type]]] in
Quick_test_parameter.Shrinker, expr
;;
let expand_to_args t ~loc ~input_type =
let sexp_examples_arg =
expand_sexp_examples_argument t.sexp_examples ~loc ~input_type
in
let generator_arg = expand_generator_argument t.generators ~loc in
let shrinker_arg = expand_shrinker_argument t.shrinkers ~loc in
let args = generator_arg :: shrinker_arg :: sexp_examples_arg :: t.pass_through_attrs in
args
;;