From 0997540ae2397359381e7f28c254baa7352644bb Mon Sep 17 00:00:00 2001 From: Aliya Hameer Date: Sun, 14 Jul 2019 21:08:03 -0400 Subject: [PATCH 1/7] Mutation testing functionality --- demo-repository/exercises/demo/solution.ml | 7 + demo-repository/exercises/demo/template.ml | 6 + demo-repository/exercises/demo/test.ml | 55 ++++-- src/grader/dune | 6 +- src/grader/mutation_test.ml | 206 +++++++++++++++++++++ src/grader/mutation_test.mli | 62 +++++++ src/ppx-metaquot/ty.ml | 23 +++ src/ppx-metaquot/ty.mli | 4 + 8 files changed, 355 insertions(+), 14 deletions(-) create mode 100644 src/grader/mutation_test.ml create mode 100644 src/grader/mutation_test.mli diff --git a/demo-repository/exercises/demo/solution.ml b/demo-repository/exercises/demo/solution.ml index cb7e63583..096ec194b 100644 --- a/demo-repository/exercises/demo/solution.ml +++ b/demo-repository/exercises/demo/solution.ml @@ -1,4 +1,11 @@ let plus = (+) +let plus_tests = [((1, 1), 2)] + let times = ( * ) +let times_tests = [((2, 2), 4)] + let minus = ( - ) +let minus_tests = [((1, 1), 0)] + let divide = ( / ) +let divide_tests = [((2, 2), 1)] diff --git a/demo-repository/exercises/demo/template.ml b/demo-repository/exercises/demo/template.ml index 8292e5d81..552c1a268 100644 --- a/demo-repository/exercises/demo/template.ml +++ b/demo-repository/exercises/demo/template.ml @@ -1,6 +1,12 @@ let plus x y = x + y ;; +let plus_tests = [ + ((1, 1), 2) +] let minus x y = y - x ;; +let minus_tests = [ + ((1, 2), 1) +] let times x y = x * diff --git a/demo-repository/exercises/demo/test.ml b/demo-repository/exercises/demo/test.ml index 91ca022bd..d488923de 100644 --- a/demo-repository/exercises/demo/test.ml +++ b/demo-repository/exercises/demo/test.ml @@ -1,26 +1,57 @@ open Test_lib open Report +module Mutation_test = Mutation_test.Make (Test_lib) +open Mutation_test + +let test_plus () = + test_function_2_against_solution + [%ty : int -> int -> int ] "plus" + [ (1, 1) ; (2, 2) ; (10, -10) ] + @ + test_unit_tests_2 + [%ty : int -> int -> int ] "plus" + [ fun x y -> x - y ] + +let test_minus () = + test_function_2_against_solution + [%ty : int -> int -> int ] "minus" + [ (1, 1) ; (4, -2) ; (0, 10) ] + @ + test_unit_tests_2 + [%ty : int -> int -> int ] "minus" + [ fun x y -> x + y ] + +let test_times () = + test_function_2_against_solution + [%ty : int -> int -> int ] "times" + [ (1, 3) ; (2, 4) ; (3, 0) ] + @ + test_unit_tests_2 + [%ty: int -> int -> int ] "times" + [ fun x y -> x / y ] + +let test_divide () = + test_function_2_against_solution + [%ty : int -> int -> int ] "divide" + [ (12, 4) ; (12, 5) ; (3, 0) ] + @ + test_unit_tests_2 + [%ty : int -> int -> int ] "divide" + [ fun x y -> x * y ] + let () = set_result @@ ast_sanity_check code_ast @@ fun () -> [ Section ([ Text "Function:" ; Code "plus" ], - test_function_2_against_solution - [%ty : int -> int -> int ] "plus" - [ (1, 1) ; (2, 2) ; (10, -10) ]) ; + test_plus ()) ; Section ([ Text "Function:" ; Code "minus" ], - test_function_2_against_solution - [%ty : int -> int -> int ] "minus" - [ (1, 1) ; (4, -2) ; (0, 10) ]) ; + test_minus ()) ; Section ([ Text "Function:" ; Code "times" ], - test_function_2_against_solution - [%ty : int -> int -> int ] "times" - [ (1, 3) ; (2, 4) ; (3, 0) ]) ; + test_times ()) ; Section ([ Text "Function:" ; Code "divide" ], - test_function_2_against_solution - [%ty : int -> int -> int ] "divide" - [ (12, 4) ; (12, 5) ; (3, 0) ]) ] + test_divide ()) ] diff --git a/src/grader/dune b/src/grader/dune index aa78c19bc..b251d9973 100644 --- a/src/grader/dune +++ b/src/grader/dune @@ -27,7 +27,8 @@ learnocaml_repository) (modules Introspection_intf Introspection - Test_lib) + Test_lib + Mutation_test) (modules_without_implementation Introspection_intf) (preprocess (pps learnocaml_ppx_metaquot)) ) @@ -106,7 +107,8 @@ ../ppx-metaquot/.ty.objs/ty.cmi .testing.objs/introspection_intf.cmi .learnocaml_report.objs/learnocaml_report.cmi - .testing.objs/test_lib.cmi)) + .testing.objs/test_lib.cmi + .testing.objs/mutation_test.cmi)) (action (with-stdout-to %{targets} (run ocp-ocamlres -format ocamlres %{compiler-cmis} %{generated-cmis}))) ) diff --git a/src/grader/mutation_test.ml b/src/grader/mutation_test.ml new file mode 100644 index 000000000..3975fce16 --- /dev/null +++ b/src/grader/mutation_test.ml @@ -0,0 +1,206 @@ +open Learnocaml_report + +type 'a test_result = + | Pass + | Fail of 'a + | Err of exn + +let run_test_against f (input, expected) = + try + let output = f input in + if output = expected then Pass + else Fail output + with exn -> Err exn + +let run_test_against_mutant f (input, expected) = + match run_test_against f (input, expected) with + | Pass -> false + | _ -> true + + +let uncurry2 f = fun (x, y) -> f x y +let uncurry3 f = fun (x, y, z) -> f x y z +let uncurry4 f = fun (x, y, z, w) -> f x y z w + +module type S = sig + val test_unit_tests_1: + ?points: int -> + ('a -> 'b) Ty.ty -> string -> ('a -> 'b) list -> Learnocaml_report.t + val test_unit_tests_2: + ?points: int -> + ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) list -> Learnocaml_report.t + val test_unit_tests_3: + ?points: int -> + ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) list -> Learnocaml_report.t + val test_unit_tests_4: + ?points: int -> + ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) list -> Learnocaml_report.t +end + +module Make (Test_lib: Test_lib.S) : S = struct + open Test_lib + + let typed_printer ty = + let typed_printer ppf v = Introspection.print_value ppf v ty in + Format.asprintf "%a" typed_printer + let string_of_exn = typed_printer [%ty: exn] + + let test_against_mutant ~points mut name tests = + let result = List.exists (run_test_against_mutant mut) tests in + if result then + Message + ([Text "Your tests successfully revealed the bug in implementation"; Text name], + Success points) + else + Message + ([Text "Your tests did not expose the bug in implementation"; Text name], + Failure) + + let test_against_solution soln printer out_printer (input, expected) = + let msg = Message ([Text "Running test"; Code (printer input)], Informative) in + let expected_str = out_printer expected in + let result = run_test_against soln (input, expected) in + let report = + match result with + | Pass -> [Message ([Text "Test passed with output"; + Code expected_str], + Success 0)] + | Fail _ -> + [Message ([Text "Test failed: expected output"; + Code expected_str; + Text "but got something else"], + Failure)] + | Err exn -> + [Message ([Text "Test failed: expected output"; + Code expected_str; + Text "but got an unexpected exception"; + Code (string_of_exn exn)], + Failure)] + in + msg :: report + + + let test_against_mutants ~points muts tests = + let string_of_num x = "#" ^ (string_of_int x) in + let test_against_mutant_i i mut = + test_against_mutant ~points mut (string_of_num (succ i)) tests + in + List.mapi test_against_mutant_i muts + + let test_report soln_report maybe_mut_report = + let soln_section = + Section ([Text "...against the solution"], soln_report) + in + let mut_report = + match maybe_mut_report with + | None -> + [Message ([Text "Some of your tests are incorrect and need to be fixed"], + Failure)] + | Some report -> + [Section ([Text "...against our buggy implementations"], report)] + in + soln_section :: mut_report + + let test ~points test_ty printer out_printer name soln muts = + let test_name = name ^ "_tests" in + let report = + test_variable_property test_ty test_name @@ + fun tests -> + if List.length tests = 0 then + [Message ([Text "You have not yet written any test cases."], Failure)] + else + let tester = test_against_solution soln printer out_printer in + let soln_report = + List.fold_right (fun test acc -> (tester test) @ acc) tests [] + in + let maybe_mut_report = + if snd (Learnocaml_report.result soln_report) then None + else Some (test_against_mutants ~points muts tests) + in + test_report soln_report maybe_mut_report + in + [Section ([Text "Your tests..."], report)] + + + let test_unit_tests_1 ?(points=1) ty name muts = + let (domain, range) = Ty.domains ty in + let test_ty = Ty.lst (Ty.pair2 domain range) in + let in_printer = typed_printer domain in + let printer input = name ^ " " ^ (in_printer input) in + let out_printer = typed_printer range in + let soln = lookup_solution ty name () in + match soln with + | `Unbound (_, report) -> report (* this should never happen *) + | `Found (_, _, soln) -> + test ~points test_ty printer out_printer name soln muts + + let test_unit_tests_2 ?(points=1) ty name muts = + let (dom1, rng) = Ty.domains ty in + let (dom2, range) = Ty.domains rng in + let test_ty = Ty.lst (Ty.pair2 (Ty.pair2 dom1 dom2) range) in + let in1_printer = typed_printer dom1 in + let in2_printer = typed_printer dom2 in + let printer (in1, in2) = + name ^ " " ^ (in1_printer in1) ^ " " ^ (in2_printer in2) + in + let out_printer = typed_printer range in + let muts = List.map uncurry2 muts in + let soln = lookup_solution ty name () in + match soln with + | `Unbound (_, report) -> report (* this should never happen *) + | `Found (_, _, soln) -> + let soln = uncurry2 soln in + test ~points test_ty printer out_printer name soln muts + + let test_unit_tests_3 ?(points=1) ty name muts = + let (dom1, rng1) = Ty.domains ty in + let (dom2, rng2) = Ty.domains rng1 in + let (dom3, range) = Ty.domains rng2 in + let test_ty = + Ty.lst (Ty.pair2 (Ty.pair3 dom1 dom2 dom3) range) + in + let in1_printer = typed_printer dom1 in + let in2_printer = typed_printer dom2 in + let in3_printer = typed_printer dom3 in + let printer (in1, in2, in3) = + name ^ " " ^ (in1_printer in1) + ^ " " ^ (in2_printer in2) + ^ " " ^ (in3_printer in3) + in + let out_printer = typed_printer range in + let muts = List.map uncurry3 muts in + let soln = lookup_solution ty name () in + match soln with + | `Unbound (_, report) -> report (* this should never happen *) + | `Found (_, _, soln) -> + let soln = uncurry3 soln in + test ~points test_ty printer out_printer name soln muts + + let test_unit_tests_4 ?(points=1) ty name muts = + let (dom1, rng1) = Ty.domains ty in + let (dom2, rng2) = Ty.domains rng1 in + let (dom3, rng3) = Ty.domains rng2 in + let (dom4, range) = Ty.domains rng3 in + let test_ty = + Ty.lst (Ty.pair2 (Ty.pair4 dom1 dom2 dom3 dom4) range) + in + let in1_printer = typed_printer dom1 in + let in2_printer = typed_printer dom2 in + let in3_printer = typed_printer dom3 in + let in4_printer = typed_printer dom4 in + let printer (in1, in2, in3, in4) = + name ^ " " ^ (in1_printer in1) + ^ " " ^ (in2_printer in2) + ^ " " ^ (in3_printer in3) + ^ " " ^ (in4_printer in4) + in + let out_printer = typed_printer range in + let muts = List.map uncurry4 muts in + let soln = lookup_solution ty name () in + match soln with + | `Unbound (_, report) -> report (* this should never happen *) + | `Found (_, _, soln) -> + let soln = uncurry4 soln in + test ~points test_ty printer out_printer name soln muts + +end diff --git a/src/grader/mutation_test.mli b/src/grader/mutation_test.mli new file mode 100644 index 000000000..91ce2b699 --- /dev/null +++ b/src/grader/mutation_test.mli @@ -0,0 +1,62 @@ +(* This file is part of Learn-OCaml. + * + * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2016-2018 OCamlPro. + * + * Learn-OCaml is distributed under the terms of the MIT license. See the + * included LICENSE file for details. *) + +type 'a test_result = + | Pass + | Fail of 'a + | Err of exn + +(** Run a test (a pair of input and expected output) on a function. *) +val run_test_against: ('a -> 'b) -> ('a * 'b) -> 'b test_result + +(** Run a test (a pair of input and expected output) on a mutant. + Returns true if the mutant *fails* the test, either by deviating + from the expected output or by raising an error. + Returns false if the mutant *passes* the test. +*) +val run_test_against_mutant: ('a -> 'b) -> ('a * 'b) -> bool + +(** Running mutation tests on a student's test suite. + For testing a function called [foo], the student's tests should + be in a variable called [foo_tests]. + This module needs to be instantiated with an instance of + Test_lib, which is available to the grader code: + + {[ + module M = Mutation_test.Make (Test_lib) + + M.test_unit_tests_1 ... + ]} + + A grading function is defined for each arity function from + one to four: + + [test_unit_tests_ ty name mutants] + grades unit tests for the [args_nb]-arity function named + [name], which are stored in the variable called [name_tests], + against the broken implementations in the list [mutants]. + + The optional argument [~points] specifies how many points + should be given for each mutant exposed by the test suite. +*) +module type S = sig + val test_unit_tests_1: + ?points: int -> + ('a -> 'b) Ty.ty -> string -> ('a -> 'b) list -> Learnocaml_report.t + val test_unit_tests_2: + ?points: int -> + ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) list -> Learnocaml_report.t + val test_unit_tests_3: + ?points: int -> + ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) list -> Learnocaml_report.t + val test_unit_tests_4: + ?points: int -> + ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) list -> Learnocaml_report.t +end + +module Make (Test_lib: Test_lib.S) : S diff --git a/src/ppx-metaquot/ty.ml b/src/ppx-metaquot/ty.ml index dab0d0ffa..d8b85e182 100644 --- a/src/ppx-metaquot/ty.ml +++ b/src/ppx-metaquot/ty.ml @@ -25,3 +25,26 @@ let curry (Ty arg) (Ty ret) = Ty { Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (Asttypes.Nolabel, arg, ret) ; ptyp_loc = Location.none ; ptyp_attributes = [] } + +let pair2 (Ty t1) (Ty t2) = + Ty {Parsetree.ptyp_desc = Parsetree.Ptyp_tuple [t1; t2]; + ptyp_loc = Location.none; + ptyp_attributes = []} + +let pair3 (Ty t1) (Ty t2) (Ty t3) = + Ty {Parsetree.ptyp_desc = Parsetree.Ptyp_tuple [t1; t2; t3]; + ptyp_loc = Location.none; + ptyp_attributes = []} + +let pair4 (Ty t1) (Ty t2) (Ty t3) (Ty t4) = + Ty {Parsetree.ptyp_desc = Parsetree.Ptyp_tuple [t1; t2; t3; t4]; + ptyp_loc = Location.none; + ptyp_attributes = []} + +let lst (Ty ty) = + Ty {Parsetree.ptyp_desc = + Parsetree.Ptyp_constr ({Asttypes.txt = Longident.Lident "list"; + loc = Location.none}, + [ty]); + ptyp_loc = Location.none; + ptyp_attributes = []} diff --git a/src/ppx-metaquot/ty.mli b/src/ppx-metaquot/ty.mli index 19507f409..e000dff50 100644 --- a/src/ppx-metaquot/ty.mli +++ b/src/ppx-metaquot/ty.mli @@ -20,3 +20,7 @@ val repr: Parsetree.core_type -> 'a ty val print: 'a ty -> string val domains: ('a -> 'b) ty -> 'a ty * 'b ty val curry: 'a ty -> 'b ty -> ('a -> 'b) ty +val pair2: 'a ty -> 'b ty -> ('a * 'b) ty +val pair3: 'a ty -> 'b ty -> 'c ty -> ('a * 'b * 'c) ty +val pair4: 'a ty -> 'b ty -> 'c ty -> 'd ty -> ('a * 'b * 'c * 'd) ty +val lst: 'a ty -> ('a list) ty From b1c5bbaccc0d8bd3576c02c8029583cefda883f2 Mon Sep 17 00:00:00 2001 From: Aliya Hameer Date: Thu, 8 Aug 2019 17:45:20 -0400 Subject: [PATCH 2/7] Mutation testing: require descriptive names for mutants --- demo-repository/exercises/demo/descr.html | 34 ++++++++++++++++++++++ demo-repository/exercises/demo/template.ml | 7 +++-- demo-repository/exercises/demo/test.ml | 8 ++--- src/grader/mutation_test.ml | 32 +++++++++++++------- src/grader/mutation_test.mli | 16 +++++++--- 5 files changed, 76 insertions(+), 21 deletions(-) diff --git a/demo-repository/exercises/demo/descr.html b/demo-repository/exercises/demo/descr.html index dcc05ad1f..16fd26914 100644 --- a/demo-repository/exercises/demo/descr.html +++ b/demo-repository/exercises/demo/descr.html @@ -9,18 +9,52 @@

The task

integer-arithmetic functions.

+

+ You are also asked to write test suites for these functions. A test + suite is specified as a list of input/expected output pairs. These + tests will be run against some buggy programs to test their coverage. +

+
  1. Write a function plus of type int -> int -> int. +
      +
    • + Write some test cases for this function in the variable + plus_tests. Your test cases should be pairs of type + (int * int) * int. +
    • +
  2. Write a function minus of type int -> int -> int. +
      +
    • + Write some test cases for this function in the variable + minus_tests. Your test cases should be pairs of type + (int * int) * int. +
    • +
  3. Write a function times of type int -> int -> int. +
      +
    • + Write some test cases for this function in the variable + times_tests. Your test cases should be pairs of type + (int * int) * int. +
    • +
  4. Write a function divide of type int -> int -> int. +
      +
    • + Write some test cases for this function in the variable + divide_tests. Your test cases should be pairs of type + (int * int) * int. +
    • +
diff --git a/demo-repository/exercises/demo/template.ml b/demo-repository/exercises/demo/template.ml index 552c1a268..ad209cb14 100644 --- a/demo-repository/exercises/demo/template.ml +++ b/demo-repository/exercises/demo/template.ml @@ -1,12 +1,13 @@ let plus x y = x + y ;; let plus_tests = [ - ((1, 1), 2) -] + ((1, 1), 2); + ((1, 0), 1) +];; let minus x y = y - x ;; let minus_tests = [ ((1, 2), 1) -] +];; let times x y = x * diff --git a/demo-repository/exercises/demo/test.ml b/demo-repository/exercises/demo/test.ml index d488923de..ad6123f4b 100644 --- a/demo-repository/exercises/demo/test.ml +++ b/demo-repository/exercises/demo/test.ml @@ -11,7 +11,7 @@ let test_plus () = @ test_unit_tests_2 [%ty : int -> int -> int ] "plus" - [ fun x y -> x - y ] + [ ("Subtracts instead of adding", fun x y -> x - y) ] let test_minus () = test_function_2_against_solution @@ -20,7 +20,7 @@ let test_minus () = @ test_unit_tests_2 [%ty : int -> int -> int ] "minus" - [ fun x y -> x + y ] + [ ("Adds instead of subtracting", fun x y -> x + y) ] let test_times () = test_function_2_against_solution @@ -29,7 +29,7 @@ let test_times () = @ test_unit_tests_2 [%ty: int -> int -> int ] "times" - [ fun x y -> x / y ] + [ ("Divides instead of multiplying", fun x y -> x / y) ] let test_divide () = test_function_2_against_solution @@ -38,7 +38,7 @@ let test_divide () = @ test_unit_tests_2 [%ty : int -> int -> int ] "divide" - [ fun x y -> x * y ] + [ ("Multiplies instead of dividing", fun x y -> x * y) ] let () = set_result @@ diff --git a/src/grader/mutation_test.ml b/src/grader/mutation_test.ml index 3975fce16..b3b75fcc1 100644 --- a/src/grader/mutation_test.ml +++ b/src/grader/mutation_test.ml @@ -5,6 +5,8 @@ type 'a test_result = | Fail of 'a | Err of exn +type 'a mutant = string * 'a + let run_test_against f (input, expected) = try let output = f input in @@ -21,20 +23,27 @@ let run_test_against_mutant f (input, expected) = let uncurry2 f = fun (x, y) -> f x y let uncurry3 f = fun (x, y, z) -> f x y z let uncurry4 f = fun (x, y, z, w) -> f x y z w +let map_snd f = fun (x, y) -> (x, f y) module type S = sig val test_unit_tests_1: ?points: int -> - ('a -> 'b) Ty.ty -> string -> ('a -> 'b) list -> Learnocaml_report.t + ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant list -> Learnocaml_report.t val test_unit_tests_2: ?points: int -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) list -> Learnocaml_report.t + ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant list -> Learnocaml_report.t val test_unit_tests_3: ?points: int -> - ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) list -> Learnocaml_report.t + ('a -> 'b -> 'c -> 'd) Ty.ty + -> string + -> ('a -> 'b -> 'c -> 'd) mutant list + -> Learnocaml_report.t val test_unit_tests_4: ?points: int -> - ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) list -> Learnocaml_report.t + ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a -> 'b -> 'c -> 'd -> 'e) mutant list + -> Learnocaml_report.t end module Make (Test_lib: Test_lib.S) : S = struct @@ -45,15 +54,18 @@ module Make (Test_lib: Test_lib.S) : S = struct Format.asprintf "%a" typed_printer let string_of_exn = typed_printer [%ty: exn] - let test_against_mutant ~points mut name tests = + let test_against_mutant ~points (name, mut) num tests = let result = List.exists (run_test_against_mutant mut) tests in if result then Message - ([Text "Your tests successfully revealed the bug in implementation"; Text name], + ([Text "Your tests successfully revealed the bug in implementation"; + Text num; + Text ": "; + Text name], Success points) else Message - ([Text "Your tests did not expose the bug in implementation"; Text name], + ([Text "Your tests did not expose the bug in implementation"; Text num], Failure) let test_against_solution soln printer out_printer (input, expected) = @@ -144,7 +156,7 @@ module Make (Test_lib: Test_lib.S) : S = struct name ^ " " ^ (in1_printer in1) ^ " " ^ (in2_printer in2) in let out_printer = typed_printer range in - let muts = List.map uncurry2 muts in + let muts = List.map (map_snd uncurry2) muts in let soln = lookup_solution ty name () in match soln with | `Unbound (_, report) -> report (* this should never happen *) @@ -168,7 +180,7 @@ module Make (Test_lib: Test_lib.S) : S = struct ^ " " ^ (in3_printer in3) in let out_printer = typed_printer range in - let muts = List.map uncurry3 muts in + let muts = List.map (map_snd uncurry3) muts in let soln = lookup_solution ty name () in match soln with | `Unbound (_, report) -> report (* this should never happen *) @@ -195,7 +207,7 @@ module Make (Test_lib: Test_lib.S) : S = struct ^ " " ^ (in4_printer in4) in let out_printer = typed_printer range in - let muts = List.map uncurry4 muts in + let muts = List.map (map_snd uncurry4) muts in let soln = lookup_solution ty name () in match soln with | `Unbound (_, report) -> report (* this should never happen *) diff --git a/src/grader/mutation_test.mli b/src/grader/mutation_test.mli index 91ce2b699..2730b30db 100644 --- a/src/grader/mutation_test.mli +++ b/src/grader/mutation_test.mli @@ -11,6 +11,8 @@ type 'a test_result = | Fail of 'a | Err of exn +type 'a mutant = string * 'a + (** Run a test (a pair of input and expected output) on a function. *) val run_test_against: ('a -> 'b) -> ('a * 'b) -> 'b test_result @@ -47,16 +49,22 @@ val run_test_against_mutant: ('a -> 'b) -> ('a * 'b) -> bool module type S = sig val test_unit_tests_1: ?points: int -> - ('a -> 'b) Ty.ty -> string -> ('a -> 'b) list -> Learnocaml_report.t + ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant list -> Learnocaml_report.t val test_unit_tests_2: ?points: int -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) list -> Learnocaml_report.t + ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant list -> Learnocaml_report.t val test_unit_tests_3: ?points: int -> - ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) list -> Learnocaml_report.t + ('a -> 'b -> 'c -> 'd) Ty.ty + -> string + -> ('a -> 'b -> 'c -> 'd) mutant list + -> Learnocaml_report.t val test_unit_tests_4: ?points: int -> - ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) list -> Learnocaml_report.t + ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a -> 'b -> 'c -> 'd -> 'e) mutant list + -> Learnocaml_report.t end module Make (Test_lib: Test_lib.S) : S From 448bf4e2cb8c85e03a8fe96972bf4042529629e2 Mon Sep 17 00:00:00 2001 From: Aliya Hameer Date: Thu, 8 Aug 2019 17:47:05 -0400 Subject: [PATCH 3/7] Mutation testing: add option to run tests on own implementation --- src/grader/mutation_test.ml | 173 ++++++++++++++++++++++++++--------- src/grader/mutation_test.mli | 19 ++++ 2 files changed, 147 insertions(+), 45 deletions(-) diff --git a/src/grader/mutation_test.ml b/src/grader/mutation_test.ml index b3b75fcc1..4417bc7c1 100644 --- a/src/grader/mutation_test.ml +++ b/src/grader/mutation_test.ml @@ -28,22 +28,27 @@ let map_snd f = fun (x, y) -> (x, f y) module type S = sig val test_unit_tests_1: ?points: int -> + ?test_student_soln: bool -> ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant list -> Learnocaml_report.t val test_unit_tests_2: ?points: int -> + ?test_student_soln: bool -> ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant list -> Learnocaml_report.t val test_unit_tests_3: ?points: int -> + ?test_student_soln: bool -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) mutant list -> Learnocaml_report.t val test_unit_tests_4: ?points: int -> + ?test_student_soln: bool -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) mutant list -> Learnocaml_report.t + val passed_mutation_testing: Learnocaml_report.t -> bool end module Make (Test_lib: Test_lib.S) : S = struct @@ -68,19 +73,21 @@ module Make (Test_lib: Test_lib.S) : S = struct ([Text "Your tests did not expose the bug in implementation"; Text num], Failure) - let test_against_solution soln printer out_printer (input, expected) = + let test_against_fn ?(show_output=false) f printer out_printer (input, expected) = let msg = Message ([Text "Running test"; Code (printer input)], Informative) in let expected_str = out_printer expected in - let result = run_test_against soln (input, expected) in + let result = run_test_against f (input, expected) in let report = match result with | Pass -> [Message ([Text "Test passed with output"; Code expected_str], Success 0)] - | Fail _ -> + | Fail out -> [Message ([Text "Test failed: expected output"; Code expected_str; - Text "but got something else"], + Text "but got"; + if show_output then Code (out_printer out) + else Text "something else"], Failure)] | Err exn -> [Message ([Text "Test failed: expected output"; @@ -91,6 +98,10 @@ module Make (Test_lib: Test_lib.S) : S = struct in msg :: report + let section_header = "Your tests..." + let soln_header = "...against the solution" + let mutation_header = "...against our buggy implementations" + let stud_header = "...against your implementation" let test_against_mutants ~points muts tests = let string_of_num x = "#" ^ (string_of_int x) in @@ -99,54 +110,117 @@ module Make (Test_lib: Test_lib.S) : S = struct in List.mapi test_against_mutant_i muts - let test_report soln_report maybe_mut_report = + let test_report soln_report stud_section maybe_mut_report = let soln_section = - Section ([Text "...against the solution"], soln_report) + Section ([Text soln_header], soln_report) in let mut_report = match maybe_mut_report with | None -> - [Message ([Text "Some of your tests are incorrect and need to be fixed"], - Failure)] + Message ([Text "Some of your tests are incorrect and need to be fixed"], + Failure) | Some report -> - [Section ([Text "...against our buggy implementations"], report)] + Section ([Text mutation_header], report) in - soln_section :: mut_report + soln_section :: mut_report :: stud_section + + let passed_mutation_testing report = + match report with + | [Section ([Text title], items)] when String.equal title section_header -> + (* Remove the student implementation section, if present *) + let report' = + List.filter + (function + | Section ([Text title], _) -> + not (String.equal title stud_header) + | _ -> true) + items + in + not (snd (Learnocaml_report.result report')) + | _ -> false + + type 'a lookup = + | Unbound of Learnocaml_report.t + | Found of 'a + + let no_test_cases_report = + [Message ([Text "You have not yet written any test cases."], Failure)] + let soln_not_found_msg = + Message ([Text "Reference solution not found."; + Text "This is an error with the grader."; + Text "Please contact your instructor."], + Failure) + + let append_map f l = + List.fold_right (fun x acc -> (f x) @ acc) l [] + + let test_soln_report soln printer out_printer tests = + match soln with + | Unbound report -> soln_not_found_msg :: report + | Found soln -> + let tester = test_against_fn soln printer out_printer in + append_map tester tests + + let test_stud_section stud printer out_printer tests = + match stud with + | None -> [] + | Some lookup -> + let stud_report = + match lookup with + | Unbound report -> report + | Found stud -> + let tester = + test_against_fn ~show_output: true stud printer out_printer + in + append_map tester tests + in + [Section ([Text stud_header], stud_report)] - let test ~points test_ty printer out_printer name soln muts = + let test ~points test_ty printer out_printer name soln stud muts = let test_name = name ^ "_tests" in let report = test_variable_property test_ty test_name @@ fun tests -> if List.length tests = 0 then - [Message ([Text "You have not yet written any test cases."], Failure)] + no_test_cases_report else - let tester = test_against_solution soln printer out_printer in - let soln_report = - List.fold_right (fun test acc -> (tester test) @ acc) tests [] - in + let soln_report = test_soln_report soln printer out_printer tests in + let stud_section = test_stud_section stud printer out_printer tests in let maybe_mut_report = if snd (Learnocaml_report.result soln_report) then None else Some (test_against_mutants ~points muts tests) in - test_report soln_report maybe_mut_report + test_report soln_report stud_section maybe_mut_report in - [Section ([Text "Your tests..."], report)] + [Section ([Text section_header], report)] + + let process_lookup process lookup ty name = + match lookup ty name () with + | `Unbound (_, report) -> Unbound report + | `Found (_, _, data) -> Found (process data) - let test_unit_tests_1 ?(points=1) ty name muts = + let test_unit_tests_1 + ?(points = 1) + ?(test_student_soln = true) + ty name muts = let (domain, range) = Ty.domains ty in let test_ty = Ty.lst (Ty.pair2 domain range) in let in_printer = typed_printer domain in let printer input = name ^ " " ^ (in_printer input) in let out_printer = typed_printer range in - let soln = lookup_solution ty name () in - match soln with - | `Unbound (_, report) -> report (* this should never happen *) - | `Found (_, _, soln) -> - test ~points test_ty printer out_printer name soln muts + let soln = process_lookup (fun x -> x) lookup_solution ty name in + let stud = + if test_student_soln then + Some (process_lookup (fun x -> x) lookup_student ty name) + else None + in + test ~points test_ty printer out_printer name soln stud muts - let test_unit_tests_2 ?(points=1) ty name muts = + let test_unit_tests_2 + ?(points = 1) + ?(test_student_soln = true) + ty name muts = let (dom1, rng) = Ty.domains ty in let (dom2, range) = Ty.domains rng in let test_ty = Ty.lst (Ty.pair2 (Ty.pair2 dom1 dom2) range) in @@ -157,14 +231,18 @@ module Make (Test_lib: Test_lib.S) : S = struct in let out_printer = typed_printer range in let muts = List.map (map_snd uncurry2) muts in - let soln = lookup_solution ty name () in - match soln with - | `Unbound (_, report) -> report (* this should never happen *) - | `Found (_, _, soln) -> - let soln = uncurry2 soln in - test ~points test_ty printer out_printer name soln muts + let soln = process_lookup uncurry2 lookup_solution ty name in + let stud = + if test_student_soln then + Some (process_lookup uncurry2 lookup_student ty name) + else None + in + test ~points test_ty printer out_printer name soln stud muts - let test_unit_tests_3 ?(points=1) ty name muts = + let test_unit_tests_3 + ?(points = 1) + ?(test_student_soln = true) + ty name muts = let (dom1, rng1) = Ty.domains ty in let (dom2, rng2) = Ty.domains rng1 in let (dom3, range) = Ty.domains rng2 in @@ -181,14 +259,18 @@ module Make (Test_lib: Test_lib.S) : S = struct in let out_printer = typed_printer range in let muts = List.map (map_snd uncurry3) muts in - let soln = lookup_solution ty name () in - match soln with - | `Unbound (_, report) -> report (* this should never happen *) - | `Found (_, _, soln) -> - let soln = uncurry3 soln in - test ~points test_ty printer out_printer name soln muts + let soln = process_lookup uncurry3 lookup_solution ty name in + let stud = + if test_student_soln then + Some (process_lookup uncurry3 lookup_student ty name) + else None + in + test ~points test_ty printer out_printer name soln stud muts - let test_unit_tests_4 ?(points=1) ty name muts = + let test_unit_tests_4 + ?(points = 1) + ?(test_student_soln = true) + ty name muts = let (dom1, rng1) = Ty.domains ty in let (dom2, rng2) = Ty.domains rng1 in let (dom3, rng3) = Ty.domains rng2 in @@ -208,11 +290,12 @@ module Make (Test_lib: Test_lib.S) : S = struct in let out_printer = typed_printer range in let muts = List.map (map_snd uncurry4) muts in - let soln = lookup_solution ty name () in - match soln with - | `Unbound (_, report) -> report (* this should never happen *) - | `Found (_, _, soln) -> - let soln = uncurry4 soln in - test ~points test_ty printer out_printer name soln muts + let soln = process_lookup uncurry4 lookup_solution ty name in + let stud = + if test_student_soln then + Some (process_lookup uncurry4 lookup_student ty name) + else None + in + test ~points test_ty printer out_printer name soln stud muts end diff --git a/src/grader/mutation_test.mli b/src/grader/mutation_test.mli index 2730b30db..c30dcf197 100644 --- a/src/grader/mutation_test.mli +++ b/src/grader/mutation_test.mli @@ -26,6 +26,7 @@ val run_test_against_mutant: ('a -> 'b) -> ('a * 'b) -> bool (** Running mutation tests on a student's test suite. For testing a function called [foo], the student's tests should be in a variable called [foo_tests]. + This module needs to be instantiated with an instance of Test_lib, which is available to the grader code: @@ -45,26 +46,44 @@ val run_test_against_mutant: ('a -> 'b) -> ('a * 'b) -> bool The optional argument [~points] specifies how many points should be given for each mutant exposed by the test suite. + If [test_student_soln] is [true] (as it is by default), + also runs the student's test suite against the student's own + implementation and reports the results. *) module type S = sig val test_unit_tests_1: ?points: int -> + ?test_student_soln: bool -> ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant list -> Learnocaml_report.t val test_unit_tests_2: ?points: int -> + ?test_student_soln: bool -> ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant list -> Learnocaml_report.t val test_unit_tests_3: ?points: int -> + ?test_student_soln: bool -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) mutant list -> Learnocaml_report.t val test_unit_tests_4: ?points: int -> + ?test_student_soln: bool -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) mutant list -> Learnocaml_report.t + + (* To be called on a report returned by one of the above 4 functions, + for checking whether the student passed or failed mutation testing. + The [Learnocaml_report.result] function is not sufficient for + checking this since a report will register as a failure if the + student's implementation does not pass all of their own tests, even + if the student did pass mutation testing. + If this function is called on a report that did not result from + one of the above 4 functions, the result is undefined. + *) + val passed_mutation_testing: Learnocaml_report.t -> bool end module Make (Test_lib: Test_lib.S) : S From 807b6023ecb4689c432d51686a4023a6f72b8494 Mon Sep 17 00:00:00 2001 From: Aliya Hameer Date: Sun, 11 Aug 2019 16:23:12 -0400 Subject: [PATCH 4/7] Mutation testing: allow custom output comparison --- src/grader/mutation_test.ml | 74 +++++++++++++++++++++++++----------- src/grader/mutation_test.mli | 31 ++++++++++++--- 2 files changed, 78 insertions(+), 27 deletions(-) diff --git a/src/grader/mutation_test.ml b/src/grader/mutation_test.ml index 4417bc7c1..66445be77 100644 --- a/src/grader/mutation_test.ml +++ b/src/grader/mutation_test.ml @@ -7,15 +7,15 @@ type 'a test_result = type 'a mutant = string * 'a -let run_test_against f (input, expected) = +let run_test_against ?(compare = (=)) f (input, expected) = try let output = f input in - if output = expected then Pass + if compare output expected then Pass else Fail output with exn -> Err exn -let run_test_against_mutant f (input, expected) = - match run_test_against f (input, expected) with +let run_test_against_mutant ?(compare = (=)) f (input, expected) = + match run_test_against ~compare f (input, expected) with | Pass -> false | _ -> true @@ -29,14 +29,17 @@ module type S = sig val test_unit_tests_1: ?points: int -> ?test_student_soln: bool -> + ?test: ('b -> 'b -> bool) -> ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant list -> Learnocaml_report.t val test_unit_tests_2: ?points: int -> ?test_student_soln: bool -> + ?test: ('c -> 'c -> bool) -> ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant list -> Learnocaml_report.t val test_unit_tests_3: ?points: int -> ?test_student_soln: bool -> + ?test: ('d -> 'd -> bool) -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) mutant list @@ -44,6 +47,7 @@ module type S = sig val test_unit_tests_4: ?points: int -> ?test_student_soln: bool -> + ?test: ('e -> 'e -> bool) -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) mutant list @@ -59,8 +63,8 @@ module Make (Test_lib: Test_lib.S) : S = struct Format.asprintf "%a" typed_printer let string_of_exn = typed_printer [%ty: exn] - let test_against_mutant ~points (name, mut) num tests = - let result = List.exists (run_test_against_mutant mut) tests in + let test_against_mutant ~points ~compare (name, mut) num tests = + let result = List.exists (run_test_against_mutant ~compare mut) tests in if result then Message ([Text "Your tests successfully revealed the bug in implementation"; @@ -73,10 +77,13 @@ module Make (Test_lib: Test_lib.S) : S = struct ([Text "Your tests did not expose the bug in implementation"; Text num], Failure) - let test_against_fn ?(show_output=false) f printer out_printer (input, expected) = + let test_against_fn + ~compare + ?(show_output=false) + f printer out_printer (input, expected) = let msg = Message ([Text "Running test"; Code (printer input)], Informative) in let expected_str = out_printer expected in - let result = run_test_against f (input, expected) in + let result = run_test_against ~compare f (input, expected) in let report = match result with | Pass -> [Message ([Text "Test passed with output"; @@ -103,10 +110,13 @@ module Make (Test_lib: Test_lib.S) : S = struct let mutation_header = "...against our buggy implementations" let stud_header = "...against your implementation" - let test_against_mutants ~points muts tests = + let test_against_mutants ~points ~compare muts tests = let string_of_num x = "#" ^ (string_of_int x) in let test_against_mutant_i i mut = - test_against_mutant ~points mut (string_of_num (succ i)) tests + test_against_mutant + ~points + ~compare + mut (string_of_num (succ i)) tests in List.mapi test_against_mutant_i muts @@ -154,14 +164,14 @@ module Make (Test_lib: Test_lib.S) : S = struct let append_map f l = List.fold_right (fun x acc -> (f x) @ acc) l [] - let test_soln_report soln printer out_printer tests = + let test_soln_report ~compare soln printer out_printer tests = match soln with | Unbound report -> soln_not_found_msg :: report | Found soln -> - let tester = test_against_fn soln printer out_printer in + let tester = test_against_fn ~compare soln printer out_printer in append_map tester tests - let test_stud_section stud printer out_printer tests = + let test_stud_section ~compare stud printer out_printer tests = match stud with | None -> [] | Some lookup -> @@ -170,13 +180,16 @@ module Make (Test_lib: Test_lib.S) : S = struct | Unbound report -> report | Found stud -> let tester = - test_against_fn ~show_output: true stud printer out_printer + test_against_fn + ~compare + ~show_output: true + stud printer out_printer in append_map tester tests in [Section ([Text stud_header], stud_report)] - let test ~points test_ty printer out_printer name soln stud muts = + let test ~points ~compare test_ty printer out_printer name soln stud muts = let test_name = name ^ "_tests" in let report = test_variable_property test_ty test_name @@ @@ -184,11 +197,15 @@ module Make (Test_lib: Test_lib.S) : S = struct if List.length tests = 0 then no_test_cases_report else - let soln_report = test_soln_report soln printer out_printer tests in - let stud_section = test_stud_section stud printer out_printer tests in + let soln_report = + test_soln_report ~compare soln printer out_printer tests + in + let stud_section = + test_stud_section ~compare stud printer out_printer tests + in let maybe_mut_report = if snd (Learnocaml_report.result soln_report) then None - else Some (test_against_mutants ~points muts tests) + else Some (test_against_mutants ~points ~compare muts tests) in test_report soln_report stud_section maybe_mut_report in @@ -203,6 +220,7 @@ module Make (Test_lib: Test_lib.S) : S = struct let test_unit_tests_1 ?(points = 1) ?(test_student_soln = true) + ?test:(compare = (=)) ty name muts = let (domain, range) = Ty.domains ty in let test_ty = Ty.lst (Ty.pair2 domain range) in @@ -215,11 +233,12 @@ module Make (Test_lib: Test_lib.S) : S = struct Some (process_lookup (fun x -> x) lookup_student ty name) else None in - test ~points test_ty printer out_printer name soln stud muts + test ~points ~compare test_ty printer out_printer name soln stud muts let test_unit_tests_2 ?(points = 1) ?(test_student_soln = true) + ?test:(compare = (=)) ty name muts = let (dom1, rng) = Ty.domains ty in let (dom2, range) = Ty.domains rng in @@ -237,11 +256,15 @@ module Make (Test_lib: Test_lib.S) : S = struct Some (process_lookup uncurry2 lookup_student ty name) else None in - test ~points test_ty printer out_printer name soln stud muts + test + ~points + ~compare + test_ty printer out_printer name soln stud muts let test_unit_tests_3 ?(points = 1) ?(test_student_soln = true) + ?test:(compare = (=)) ty name muts = let (dom1, rng1) = Ty.domains ty in let (dom2, rng2) = Ty.domains rng1 in @@ -265,11 +288,15 @@ module Make (Test_lib: Test_lib.S) : S = struct Some (process_lookup uncurry3 lookup_student ty name) else None in - test ~points test_ty printer out_printer name soln stud muts + test + ~points + ~compare + test_ty printer out_printer name soln stud muts let test_unit_tests_4 ?(points = 1) ?(test_student_soln = true) + ?test:(compare = (=)) ty name muts = let (dom1, rng1) = Ty.domains ty in let (dom2, rng2) = Ty.domains rng1 in @@ -296,6 +323,9 @@ module Make (Test_lib: Test_lib.S) : S = struct Some (process_lookup uncurry4 lookup_student ty name) else None in - test ~points test_ty printer out_printer name soln stud muts + test + ~points + ~compare + test_ty printer out_printer name soln stud muts end diff --git a/src/grader/mutation_test.mli b/src/grader/mutation_test.mli index c30dcf197..9ad088a84 100644 --- a/src/grader/mutation_test.mli +++ b/src/grader/mutation_test.mli @@ -11,24 +11,38 @@ type 'a test_result = | Fail of 'a | Err of exn +(** A mutant is a pair of a string describing the mutant and the + mutant function itself. +*) type 'a mutant = string * 'a -(** Run a test (a pair of input and expected output) on a function. *) -val run_test_against: ('a -> 'b) -> ('a * 'b) -> 'b test_result +(** Run a test (a pair of input and expected output) on a function. + The [compare] parameter specifies a comparison function for + comparing the expected and actual outputs, and defaults to + structural equality ([(=)]). +*) +val run_test_against: + ?compare: ('b -> 'b -> bool) -> + ('a -> 'b) -> ('a * 'b) -> 'b test_result (** Run a test (a pair of input and expected output) on a mutant. Returns true if the mutant *fails* the test, either by deviating from the expected output or by raising an error. Returns false if the mutant *passes* the test. + The [compare] parameter specifies a comparison function for + comparing the expected and actual outputs, and defaults to + structural equality ([(=)]). *) -val run_test_against_mutant: ('a -> 'b) -> ('a * 'b) -> bool +val run_test_against_mutant: + ?compare: ('b -> 'b -> bool) -> + ('a -> 'b) -> ('a * 'b) -> bool (** Running mutation tests on a student's test suite. - For testing a function called [foo], the student's tests should + For testing a function call [foo], the student's tests should be in a variable called [foo_tests]. This module needs to be instantiated with an instance of - Test_lib, which is available to the grader code: + [Test_lib], which is available to the grader code: {[ module M = Mutation_test.Make (Test_lib) @@ -49,19 +63,25 @@ val run_test_against_mutant: ('a -> 'b) -> ('a * 'b) -> bool If [test_student_soln] is [true] (as it is by default), also runs the student's test suite against the student's own implementation and reports the results. + The [test] parameter specifies a comparison function for the + expected and actual outputs, and defaults to structural + equality ([(=)]). *) module type S = sig val test_unit_tests_1: ?points: int -> ?test_student_soln: bool -> + ?test: ('b -> 'b -> bool) -> ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant list -> Learnocaml_report.t val test_unit_tests_2: ?points: int -> ?test_student_soln: bool -> + ?test: ('c -> 'c -> bool) -> ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant list -> Learnocaml_report.t val test_unit_tests_3: ?points: int -> ?test_student_soln: bool -> + ?test: ('d -> 'd -> bool) -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) mutant list @@ -69,6 +89,7 @@ module type S = sig val test_unit_tests_4: ?points: int -> ?test_student_soln: bool -> + ?test: ('e -> 'e -> bool) -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) mutant list From e90d70ff279588dd273851ce0aed660a037e5d47 Mon Sep 17 00:00:00 2001 From: Aliya Hameer Date: Sun, 11 Aug 2019 18:54:22 -0400 Subject: [PATCH 5/7] Temporary: use Important instead of Success 0 This can be reverted if ocaml-sf/learn-ocaml#300 is fixed. --- src/grader/mutation_test.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/grader/mutation_test.ml b/src/grader/mutation_test.ml index 66445be77..1f103ca88 100644 --- a/src/grader/mutation_test.ml +++ b/src/grader/mutation_test.ml @@ -88,7 +88,7 @@ module Make (Test_lib: Test_lib.S) : S = struct match result with | Pass -> [Message ([Text "Test passed with output"; Code expected_str], - Success 0)] + Important)] | Fail out -> [Message ([Text "Test failed: expected output"; Code expected_str; From f09c5dc2b2d9bc8bda45be300f83784b6ab9fe99 Mon Sep 17 00:00:00 2001 From: Aliya Hameer Date: Fri, 16 Aug 2019 17:18:18 -0400 Subject: [PATCH 6/7] Mutation testing: specify points per mutant --- demo-repository/exercises/demo/test.ml | 8 +++--- src/grader/mutation_test.ml | 40 +++++++++----------------- src/grader/mutation_test.mli | 20 ++++++------- 3 files changed, 27 insertions(+), 41 deletions(-) diff --git a/demo-repository/exercises/demo/test.ml b/demo-repository/exercises/demo/test.ml index ad6123f4b..58ef13e0c 100644 --- a/demo-repository/exercises/demo/test.ml +++ b/demo-repository/exercises/demo/test.ml @@ -11,7 +11,7 @@ let test_plus () = @ test_unit_tests_2 [%ty : int -> int -> int ] "plus" - [ ("Subtracts instead of adding", fun x y -> x - y) ] + [ ("Subtracts instead of adding", 1, fun x y -> x - y) ] let test_minus () = test_function_2_against_solution @@ -20,7 +20,7 @@ let test_minus () = @ test_unit_tests_2 [%ty : int -> int -> int ] "minus" - [ ("Adds instead of subtracting", fun x y -> x + y) ] + [ ("Adds instead of subtracting", 1, fun x y -> x + y) ] let test_times () = test_function_2_against_solution @@ -29,7 +29,7 @@ let test_times () = @ test_unit_tests_2 [%ty: int -> int -> int ] "times" - [ ("Divides instead of multiplying", fun x y -> x / y) ] + [ ("Divides instead of multiplying", 1, fun x y -> x / y) ] let test_divide () = test_function_2_against_solution @@ -38,7 +38,7 @@ let test_divide () = @ test_unit_tests_2 [%ty : int -> int -> int ] "divide" - [ ("Multiplies instead of dividing", fun x y -> x * y) ] + [ ("Multiplies instead of dividing", 1, fun x y -> x * y) ] let () = set_result @@ diff --git a/src/grader/mutation_test.ml b/src/grader/mutation_test.ml index 1f103ca88..ccc22dcbd 100644 --- a/src/grader/mutation_test.ml +++ b/src/grader/mutation_test.ml @@ -5,7 +5,7 @@ type 'a test_result = | Fail of 'a | Err of exn -type 'a mutant = string * 'a +type 'a mutant_info = string * int * 'a let run_test_against ?(compare = (=)) f (input, expected) = try @@ -23,34 +23,30 @@ let run_test_against_mutant ?(compare = (=)) f (input, expected) = let uncurry2 f = fun (x, y) -> f x y let uncurry3 f = fun (x, y, z) -> f x y z let uncurry4 f = fun (x, y, z, w) -> f x y z w -let map_snd f = fun (x, y) -> (x, f y) +let map_third f = fun (x, y, z) -> (x, y, f z) module type S = sig val test_unit_tests_1: - ?points: int -> ?test_student_soln: bool -> ?test: ('b -> 'b -> bool) -> - ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant list -> Learnocaml_report.t + ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant_info list -> Learnocaml_report.t val test_unit_tests_2: - ?points: int -> ?test_student_soln: bool -> ?test: ('c -> 'c -> bool) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant list -> Learnocaml_report.t + ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant_info list -> Learnocaml_report.t val test_unit_tests_3: - ?points: int -> ?test_student_soln: bool -> ?test: ('d -> 'd -> bool) -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string - -> ('a -> 'b -> 'c -> 'd) mutant list + -> ('a -> 'b -> 'c -> 'd) mutant_info list -> Learnocaml_report.t val test_unit_tests_4: - ?points: int -> ?test_student_soln: bool -> ?test: ('e -> 'e -> bool) -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string - -> ('a -> 'b -> 'c -> 'd -> 'e) mutant list + -> ('a -> 'b -> 'c -> 'd -> 'e) mutant_info list -> Learnocaml_report.t val passed_mutation_testing: Learnocaml_report.t -> bool end @@ -63,7 +59,7 @@ module Make (Test_lib: Test_lib.S) : S = struct Format.asprintf "%a" typed_printer let string_of_exn = typed_printer [%ty: exn] - let test_against_mutant ~points ~compare (name, mut) num tests = + let test_against_mutant ~compare (name, points, mut) num tests = let result = List.exists (run_test_against_mutant ~compare mut) tests in if result then Message @@ -110,11 +106,10 @@ module Make (Test_lib: Test_lib.S) : S = struct let mutation_header = "...against our buggy implementations" let stud_header = "...against your implementation" - let test_against_mutants ~points ~compare muts tests = + let test_against_mutants ~compare muts tests = let string_of_num x = "#" ^ (string_of_int x) in let test_against_mutant_i i mut = test_against_mutant - ~points ~compare mut (string_of_num (succ i)) tests in @@ -189,7 +184,7 @@ module Make (Test_lib: Test_lib.S) : S = struct in [Section ([Text stud_header], stud_report)] - let test ~points ~compare test_ty printer out_printer name soln stud muts = + let test ~compare test_ty printer out_printer name soln stud muts = let test_name = name ^ "_tests" in let report = test_variable_property test_ty test_name @@ @@ -205,7 +200,7 @@ module Make (Test_lib: Test_lib.S) : S = struct in let maybe_mut_report = if snd (Learnocaml_report.result soln_report) then None - else Some (test_against_mutants ~points ~compare muts tests) + else Some (test_against_mutants ~compare muts tests) in test_report soln_report stud_section maybe_mut_report in @@ -218,7 +213,6 @@ module Make (Test_lib: Test_lib.S) : S = struct let test_unit_tests_1 - ?(points = 1) ?(test_student_soln = true) ?test:(compare = (=)) ty name muts = @@ -233,10 +227,9 @@ module Make (Test_lib: Test_lib.S) : S = struct Some (process_lookup (fun x -> x) lookup_student ty name) else None in - test ~points ~compare test_ty printer out_printer name soln stud muts + test ~compare test_ty printer out_printer name soln stud muts let test_unit_tests_2 - ?(points = 1) ?(test_student_soln = true) ?test:(compare = (=)) ty name muts = @@ -249,7 +242,7 @@ module Make (Test_lib: Test_lib.S) : S = struct name ^ " " ^ (in1_printer in1) ^ " " ^ (in2_printer in2) in let out_printer = typed_printer range in - let muts = List.map (map_snd uncurry2) muts in + let muts = List.map (map_third uncurry2) muts in let soln = process_lookup uncurry2 lookup_solution ty name in let stud = if test_student_soln then @@ -257,12 +250,10 @@ module Make (Test_lib: Test_lib.S) : S = struct else None in test - ~points ~compare test_ty printer out_printer name soln stud muts let test_unit_tests_3 - ?(points = 1) ?(test_student_soln = true) ?test:(compare = (=)) ty name muts = @@ -281,7 +272,7 @@ module Make (Test_lib: Test_lib.S) : S = struct ^ " " ^ (in3_printer in3) in let out_printer = typed_printer range in - let muts = List.map (map_snd uncurry3) muts in + let muts = List.map (map_third uncurry3) muts in let soln = process_lookup uncurry3 lookup_solution ty name in let stud = if test_student_soln then @@ -289,12 +280,10 @@ module Make (Test_lib: Test_lib.S) : S = struct else None in test - ~points ~compare test_ty printer out_printer name soln stud muts let test_unit_tests_4 - ?(points = 1) ?(test_student_soln = true) ?test:(compare = (=)) ty name muts = @@ -316,7 +305,7 @@ module Make (Test_lib: Test_lib.S) : S = struct ^ " " ^ (in4_printer in4) in let out_printer = typed_printer range in - let muts = List.map (map_snd uncurry4) muts in + let muts = List.map (map_third uncurry4) muts in let soln = process_lookup uncurry4 lookup_solution ty name in let stud = if test_student_soln then @@ -324,7 +313,6 @@ module Make (Test_lib: Test_lib.S) : S = struct else None in test - ~points ~compare test_ty printer out_printer name soln stud muts diff --git a/src/grader/mutation_test.mli b/src/grader/mutation_test.mli index 9ad088a84..7391b283a 100644 --- a/src/grader/mutation_test.mli +++ b/src/grader/mutation_test.mli @@ -11,10 +11,12 @@ type 'a test_result = | Fail of 'a | Err of exn -(** A mutant is a pair of a string describing the mutant and the - mutant function itself. +(** The information about a mutant is made up of: + - A name describing the bug in the mutant function + - The number of points to be awarded for exposing the bug + - The mutant function itself. *) -type 'a mutant = string * 'a +type 'a mutant_info = string * int * 'a (** Run a test (a pair of input and expected output) on a function. The [compare] parameter specifies a comparison function for @@ -69,30 +71,26 @@ val run_test_against_mutant: *) module type S = sig val test_unit_tests_1: - ?points: int -> ?test_student_soln: bool -> ?test: ('b -> 'b -> bool) -> - ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant list -> Learnocaml_report.t + ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant_info list -> Learnocaml_report.t val test_unit_tests_2: - ?points: int -> ?test_student_soln: bool -> ?test: ('c -> 'c -> bool) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant list -> Learnocaml_report.t + ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant_info list -> Learnocaml_report.t val test_unit_tests_3: - ?points: int -> ?test_student_soln: bool -> ?test: ('d -> 'd -> bool) -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string - -> ('a -> 'b -> 'c -> 'd) mutant list + -> ('a -> 'b -> 'c -> 'd) mutant_info list -> Learnocaml_report.t val test_unit_tests_4: - ?points: int -> ?test_student_soln: bool -> ?test: ('e -> 'e -> bool) -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string - -> ('a -> 'b -> 'c -> 'd -> 'e) mutant list + -> ('a -> 'b -> 'c -> 'd -> 'e) mutant_info list -> Learnocaml_report.t (* To be called on a report returned by one of the above 4 functions, From a5a7d3a7d4a29770a34e3006105aac795bf24742 Mon Sep 17 00:00:00 2001 From: Aliya Hameer Date: Wed, 28 Aug 2019 17:56:25 -0400 Subject: [PATCH 7/7] Mutation testing: reorganization and documentation --- src/grader/mutation_test.ml | 29 +++++++------- src/grader/mutation_test.mli | 74 +++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/src/grader/mutation_test.ml b/src/grader/mutation_test.ml index ccc22dcbd..3622b2499 100644 --- a/src/grader/mutation_test.ml +++ b/src/grader/mutation_test.ml @@ -7,25 +7,15 @@ type 'a test_result = type 'a mutant_info = string * int * 'a -let run_test_against ?(compare = (=)) f (input, expected) = - try - let output = f input in - if compare output expected then Pass - else Fail output - with exn -> Err exn - -let run_test_against_mutant ?(compare = (=)) f (input, expected) = - match run_test_against ~compare f (input, expected) with - | Pass -> false - | _ -> true - - let uncurry2 f = fun (x, y) -> f x y let uncurry3 f = fun (x, y, z) -> f x y z let uncurry4 f = fun (x, y, z, w) -> f x y z w let map_third f = fun (x, y, z) -> (x, y, f z) module type S = sig + val run_test_against_mutant: + ?compare: ('b -> 'b -> bool) -> + ('a -> 'b) -> ('a * 'b) -> bool val test_unit_tests_1: ?test_student_soln: bool -> ?test: ('b -> 'b -> bool) -> @@ -54,6 +44,19 @@ end module Make (Test_lib: Test_lib.S) : S = struct open Test_lib + let run_test_against ?(compare = (=)) f (input, expected) = + try + let run_f () = f input in + let output = run_timeout run_f in + if compare output expected then Pass + else Fail output + with exn -> Err exn + + let run_test_against_mutant ?(compare = (=)) f (input, expected) = + match run_test_against ~compare f (input, expected) with + | Pass -> false + | _ -> true + let typed_printer ty = let typed_printer ppf v = Introspection.print_value ppf v ty in Format.asprintf "%a" typed_printer diff --git a/src/grader/mutation_test.mli b/src/grader/mutation_test.mli index 7391b283a..d6e81d044 100644 --- a/src/grader/mutation_test.mli +++ b/src/grader/mutation_test.mli @@ -6,10 +6,20 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -type 'a test_result = - | Pass - | Fail of 'a - | Err of exn +(* This module provides functions for automatically grading + a student's unit tests using *mutation testing*. + + A student's tests for a function [foo] are run against several + *mutants* (buggy implementations of [foo]). The student then + receives a grade based on how many mutants *failed* at least + one of their tests, i.e. how many buggy implementations were + exposed as buggy by their test suite. + + Mutants are written by the instructor, and ideally should + be chosen to emphasize test cases that students should be + testing for. One could think of mutants as "test cases for + the tests". +*) (** The information about a mutant is made up of: - A name describing the bug in the mutant function @@ -18,30 +28,9 @@ type 'a test_result = *) type 'a mutant_info = string * int * 'a -(** Run a test (a pair of input and expected output) on a function. - The [compare] parameter specifies a comparison function for - comparing the expected and actual outputs, and defaults to - structural equality ([(=)]). -*) -val run_test_against: - ?compare: ('b -> 'b -> bool) -> - ('a -> 'b) -> ('a * 'b) -> 'b test_result - -(** Run a test (a pair of input and expected output) on a mutant. - Returns true if the mutant *fails* the test, either by deviating - from the expected output or by raising an error. - Returns false if the mutant *passes* the test. - The [compare] parameter specifies a comparison function for - comparing the expected and actual outputs, and defaults to - structural equality ([(=)]). -*) -val run_test_against_mutant: - ?compare: ('b -> 'b -> bool) -> - ('a -> 'b) -> ('a * 'b) -> bool - (** Running mutation tests on a student's test suite. - For testing a function call [foo], the student's tests should - be in a variable called [foo_tests]. + For testing a function called [foo], the student's tests + should be in a variable called [foo_tests]. This module needs to be instantiated with an instance of [Test_lib], which is available to the grader code: @@ -70,6 +59,20 @@ val run_test_against_mutant: equality ([(=)]). *) module type S = sig + + (** Run a test (a pair of input and expected output) on a mutant + function. + Returns true if the mutant *fails* the test, either by deviating + from the expected output or by raising an error. + Returns false if the mutant *passes* the test. + The [compare] parameter specifies a comparison function for + comparing the expected and actual outputs, and defaults to + structural equality ([(=)]). + *) + val run_test_against_mutant: + ?compare: ('b -> 'b -> bool) -> + ('a -> 'b) -> ('a * 'b) -> bool + val test_unit_tests_1: ?test_student_soln: bool -> ?test: ('b -> 'b -> bool) -> @@ -93,14 +96,15 @@ module type S = sig -> ('a -> 'b -> 'c -> 'd -> 'e) mutant_info list -> Learnocaml_report.t - (* To be called on a report returned by one of the above 4 functions, - for checking whether the student passed or failed mutation testing. - The [Learnocaml_report.result] function is not sufficient for - checking this since a report will register as a failure if the - student's implementation does not pass all of their own tests, even - if the student did pass mutation testing. - If this function is called on a report that did not result from - one of the above 4 functions, the result is undefined. + (** To be called on a report returned by one of the + [test_unit_tests_] functions, + for checking whether the student passed or failed mutation testing. + The [Learnocaml_report.result] function is not sufficient for + checking this since a report will register as a failure if the + student's implementation does not pass all of their own tests, even + if the student did pass mutation testing. + If this function is called on a report that did not result from + one of the above 4 functions, the result is undefined. *) val passed_mutation_testing: Learnocaml_report.t -> bool end