Theory SpecCheck_Examples
section ‹Examples›
theory SpecCheck_Examples
imports SpecCheck_Dynamic
begin
ML ‹
open SpecCheck
open SpecCheck_Dynamic
structure Gen = SpecCheck_Generator
structure Prop = SpecCheck_Property
structure Show = SpecCheck_Show
structure Shrink = SpecCheck_Shrink
structure Random = SpecCheck_Random
›
subsection ‹Unit Tests and Exceptions›
ML ‹
fun faulty_abs n =
if n < ~10000 then error "out of bounds"
else if n < 0 then ~n
else n
›
ML_command ‹
let
val check_unit_int_pair = check_unit_tests (Show.zip Show.int Show.int)
fun correctness_tests ctxt s = Lecker.test_group ctxt s [
check_unit_int_pair [(~10, 10), (0, 0), (10, 10)] "correctness small values"
(Prop.prop (fn (n, exp) => faulty_abs n = exp)),
check_unit_int_pair [(~999999999, 999999999), (999999999, 999999999)]
"correctness large values" (Prop.prop (fn (n, exp) => faulty_abs n = exp))
]
fun exception_tests ctxt s =
let val exn_prop = Prop.expect_failure (ERROR "out of bounds") faulty_abs
in
Lecker.test_group ctxt s [
check_unit_tests Show.int [~10, 0, 10] "expect exception for small values" exn_prop,
check_unit_tests Show.int [~999999999, ~99999999999999] "expect exception for large values"
exn_prop
]
end
in
Lecker.test_group @{context} () [
check_unit_tests Show.int [~10, 0, 10] "is idempotent"
(Prop.prop (fn n => faulty_abs (faulty_abs n) = faulty_abs n)),
correctness_tests,
exception_tests
]
end
›
subsection ‹Randomised Tests›
ML_command ‹
let
val int_gen = Gen.range_int (~10000000, 10000000)
val size_gen = Gen.nonneg 10
val check_list = check_shrink (Show.list Show.int) (Shrink.list Shrink.int)
(Gen.list size_gen int_gen)
fun list_test (k, f, xs) =
AList.lookup (op=) (AList.map_entry (op=) k f xs) k = Option.map f (AList.lookup (op=) xs k)
val list_test_show = Show.zip3 Show.int Show.none (Show.list (Show.zip Show.int Show.int))
val list_test_gen = Gen.zip3 int_gen (Gen.function' int_gen)
(Gen.list size_gen (Gen.zip int_gen int_gen))
in
Lecker.test_group @{context} (Random.new ()) [
Prop.prop (fn xs => rev xs = xs) |> check_list "rev = I",
Prop.prop (fn xs => rev (rev xs) = xs) |> check_list "rev o rev = I",
Prop.prop list_test |> check list_test_show list_test_gen "lookup map equiv map lookup"
]
end
›
text ‹The next three examples roughly correspond to the above test group (except that there's no
shrinking). Compared to the string-based method, the method above is more flexible - you can change
your generators, shrinking methods, and show instances - and robust - you are not reflecting strings
(which might contain typos) but entering type-checked code. In exchange, it is a bit more work to
set up the generators. However, in practice, one shouldn't rely on default generators in most cases
anyway.›
ML_command ‹
check_dynamic @{context} "ALL xs. rev xs = xs";
›
ML_command ‹
check_dynamic @{context} "ALL xs. rev (rev xs) = xs";
›
subsection ‹AList Specification›
ML_command ‹
check_dynamic @{context} (implode
["ALL k f xs. AList.lookup (op =) (AList.map_entry (op =) k f xs) k = ",
"Option.map f (AList.lookup (op =) xs k)"])
›
ML_command ‹
check_dynamic @{context} "ALL k v xs. AList.defined (op =) (AList.update (op =) (k, v) xs) k";
›
ML_command ‹
check_dynamic @{context}
"ALL k v xs. AList.lookup (op =) (AList.update (op =) (k, v) xs) k = SOME v";
›
ML_command ‹
check_dynamic @{context} "ALL k v xs. AList.defined (op =) (AList.default (op =) (k, v) xs) k";
›
ML_command ‹
check_dynamic @{context} "ALL k xs. not (AList.defined (op =) (AList.delete (op =) k xs) k)";
›
ML_command ‹
check_dynamic @{context} (implode
["ALL k v xs. (AList.lookup (op =) (AList.default (op =) (k, v) xs) k = ",
"(if AList.defined (op =) xs k then AList.lookup (op =) xs k else SOME v))"])
›
subsection ‹Examples on Types and Terms›
ML_command ‹
check_dynamic @{context} "ALL f g t. map_types (g o f) t = (map_types f o map_types g) t";
›
ML_command ‹
check_dynamic @{context} "ALL f g t. map_types (f o g) t = (map_types f o map_types g) t";
›
text ‹One would think this holds:›
ML_command ‹
check_dynamic @{context} "ALL t ts. strip_comb (list_comb (t, ts)) = (t, ts)"
›
text ‹But it only holds with this precondition:›
ML_command ‹
check_dynamic @{context}
"ALL t ts. case t of _ $ _ => true | _ => strip_comb (list_comb (t, ts)) = (t, ts)"
›
subsection ‹Some surprises›
ML_command ‹
check_dynamic @{context} "ALL Ts t. type_of1 (Ts, t) = fastype_of1 (Ts, t)"
›
ML_command ‹
val thy = \<^theory>;
check_dynamic (Context.the_local_context ())
"ALL t u. if Pattern.matches thy (t, u) then Term.could_unify (t, u) else true"
›
end