Skip to content

Commit 42d8127

Browse files
authored
Merge pull request #320 from lsylvestre/master
support for test code reuse
2 parents 43a1d03 + 7b5d29b commit 42d8127

File tree

6 files changed

+344
-23
lines changed

6 files changed

+344
-23
lines changed

docs/howto-write-exercises.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,37 +32,42 @@ to get the files for step 1, and replace `step-1` by `step-2` to
3232
get the files for the second step, and so on and so forth.
3333

3434
## The tutorials
35-
[Step 0 : Preliminaries](../tutorials/step-0)
35+
[Step 0 : Preliminaries](tutorials/step-0.md)
3636

3737
- Structure of an exercise
3838

3939
- Purpose of each file
4040

41-
[Step 1: Create a trivial exercise](../tutorials/step-1)
41+
[Step 1: Create a trivial exercise](tutorials/step-1.md)
4242

43-
[Step 2: Basic grading by comparison with your solution](../tutorials/step-2)
43+
[Step 2: Basic grading by comparison with your solution](tutorials/step-2.md)
4444

4545
- Simple example to grade by comparison with a solution
4646

4747
- With polymorphic functions
4848

4949
- With multiple arguments functions
5050

51-
[Step 3: Grading with generators for Ocaml built-in types](../tutorials/step-3)
51+
[Step 3: Grading with generators for Ocaml built-in types](tutorials/step-3.md)
5252

5353
- Generate tests by using the pre-construct samplers
5454

5555
- Generate tests by defining its own sampler
5656

57-
[Step 4: Grading with generators for user-defined types](../tutorials/step-4)
57+
[Step 4: Grading with generators for user-defined types](tutorials/step-4.md)
5858

5959
- Generate tests for non-parametric user-defined types
6060

6161
- Generate tests for parametric user-defined types
6262

63-
[Step 5 : More test functions](../tutorials/step-5)
63+
[Step 5 : More test functions](tutorials/step-5.md)
6464

65-
[Step 6 : Grading functions for variables](../tutorials/step-6)
66-
67-
[Step 7 : Introspection of students code](../tutorials/step-7)
65+
[Step 6 : Grading functions for variables](tutorials/step-6.md)
6866

67+
[Step 7 : Modifying the comparison functions (testers) with the optional arguments [~test], [~test_stdout], [~test_stderr]](tutorials/step-7.md)
68+
69+
[Step 8 : Reusing the grader code](tutorials/step-8.md)
70+
71+
- Separating the grader code
72+
73+
[Step 9 : Introspection of students code](tutorials/step-9.md)

docs/tutorials/step-8.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Step 8: Reusing the grader code
2+
3+
This step explains how to separate the grader code, and eventually reuse it in
4+
other exercises.
5+
6+
During the grading, the file **test.ml** is evaluated in an environment that
7+
contains notably:
8+
- **prelude.ml** and **prepare.ml** ;
9+
- the student code isolated in a module `Code` ;
10+
- **solution.ml** in a module `Solution` ;
11+
- the grading modules **Introspection**, **Report** and **Test_lib**.
12+
13+
### Separating the grader code
14+
15+
It is possible to extend this environment by declaring some other user-defined
16+
modules in an optional file **depend.txt**, located in the exercise directory.
17+
18+
Each declaration in **depend.txt** is a single line containing the relative path
19+
of an *.ml* or *.mli* file. The order of the *.ml* declarations specifies the
20+
order in which each module is loaded in the grading environment.
21+
22+
By default each dependency *foo.ml* is isolated in a module *Foo*, which can be
23+
constrained by the content of an optional signature file *foo.mli*. Furthermore,
24+
an annotation `[@@@included]` can be used at the beginning of a file *foo.ml* to
25+
denote that all the bindings of *foo.ml* are evaluated in the toplevel
26+
environment (and not in a module *Foo*).
27+
28+
Dependencies that are not defined at the root of the exercise repository are
29+
ignored by the build system: therefore, if you modify them, do not forget to
30+
refresh the timestamp of `test.ml` (using `touch` for instance).
31+
32+
### A complete example
33+
34+
Let's write an exercise dedicated to *Peano numbers*. Here is the structure of
35+
the exercise:
36+
37+
```
38+
.
39+
├── exercises
40+
│ ├── index.json
41+
│ └── lib
42+
│ │ ├── check.ml
43+
│ │ └── check.mli
44+
│ ├── peano
45+
│ │ ├── depend.txt
46+
│ │ ├── descr.md
47+
│ │ ├── meta.json
48+
│ │ ├── prelude.ml
49+
│ │ ├── prepare.ml
50+
│ │ ├── solution.ml
51+
│ │ ├── template.ml
52+
│ │ ├── test.ml
53+
│ │ └── tests
54+
│ │ ├── samples.ml
55+
│ │ ├── add.ml
56+
│ │ └── odd_even.ml
57+
│ ├── an-other-exercise
58+
│ │ ├── depend.txt
59+
│ │ │ ...
60+
```
61+
The exercise **peano** follows the classical format : **prelude.ml**,
62+
**prepare.ml**, **solution.ml**, **template.ml** and **test.ml**.
63+
It also includes several dependencies (**check.ml**, **samples.ml**, **add.ml**
64+
and **odd_even.ml**) which are declared as follows in **depend.txt**:
65+
66+
```txt
67+
../lib/check.mli
68+
../lib/check.ml # a comment
69+
70+
tests/samples.ml
71+
tests/add.ml
72+
tests/odd_even.ml
73+
```
74+
75+
Here is in details the source code of the exercise :
76+
77+
- **descr.md**
78+
79+
> * implement the function `add : peano -> peano -> peano` ;
80+
> * implement the functions `odd : peano -> bool` and `even : peano -> bool`.
81+
82+
- **prelude.ml**
83+
```ocaml
84+
type peano = Z | S of peano
85+
```
86+
87+
- **solution.ml**
88+
```ocaml
89+
let rec add n = function
90+
| Z -> n
91+
| S m -> S (add n m)
92+
93+
let rec odd = function
94+
| Z -> false
95+
| S n -> even n
96+
and even = function
97+
| Z -> true
98+
| S n -> odd n
99+
```
100+
101+
- **test.ml**
102+
```ocaml
103+
let () =
104+
Check.safe_set_result [ Add.test ; Odd_even.test ]
105+
```
106+
107+
Note that **test.ml** is very compact because it simply combines functions
108+
defined in separated files.
109+
110+
- **../lib/check.ml**:
111+
```ocaml
112+
open Test_lib
113+
open Report
114+
115+
let safe_set_result tests =
116+
set_result @@
117+
ast_sanity_check code_ast @@ fun () ->
118+
List.mapi (fun i test ->
119+
Section ([ Text ("Question " ^ string_of_int i ^ ":") ],
120+
test ())) tests
121+
```
122+
- **../lib/check.mli**:
123+
```ocaml
124+
val safe_set_result : (unit -> Report.t) list -> unit
125+
```
126+
127+
- **tests/add.ml**:
128+
```ocaml
129+
let test () =
130+
Test_lib.test_function_2_against_solution
131+
[%ty : peano -> peano -> peano ] "add"
132+
[ (Z, Z) ; (S(Z), S(S(Z))) ]
133+
```
134+
- **tests/odd_even.ml** :
135+
```ocaml
136+
let test () =
137+
Test_lib.test_function_1_against_solution
138+
[%ty : peano -> bool ] "odd"
139+
[ Z ; S(Z) ; S(S(Z)) ]
140+
@
141+
Test_lib.test_function_1_against_solution
142+
[%ty : peano -> bool ] "even"
143+
[ Z ; S(Z) ; S(S(Z)) ]
144+
```
145+
Remember that **Test_lib** internally requires a user-defined sampler
146+
`sample_peano : unit -> peano` to generate value of type `peano`. This sampler
147+
has to be present in the toplevel environment -- and not in a module -- in order
148+
to be found by the introspection primitives during grading. Therefore,
149+
we define this sampler in a file starting with the annotation `[@@@included]`.
150+
- **tests/samples.ml**:
151+
```ocaml
152+
[@@@included]
153+
154+
let sample_peano () =
155+
let rec aux = function
156+
| 0 -> Z
157+
| n -> S (aux (n-1))
158+
in aux (Random.int 42)
159+
```
160+
161+
Finally, the content of **test.ml** will be evaluated in the following
162+
environment:
163+
164+
```ocaml
165+
val print_html : 'a -> 'b
166+
type peano = Z | S of peano
167+
module Code :
168+
sig
169+
val add : peano -> peano -> peano
170+
val odd : peano -> bool
171+
val even : peano -> bool
172+
end
173+
module Solution :
174+
sig
175+
val add : peano -> peano -> peano
176+
val odd : peano -> bool
177+
val even : peano -> bool
178+
end
179+
module Test_lib : Test_lib.S
180+
module Report = Learnocaml_report
181+
module Check : sig val check_all : (unit -> Report.t) list -> unit end
182+
val sample_peano : unit -> peano
183+
module Add : sig val test : unit -> Report.t end
184+
module Odd_even : sig val test : unit -> Report.t end
185+
```
186+
187+
In the end, this feature can provide an increased comfort for writing large a
188+
utomated graders and for reusing them in other exercises.
189+
190+

src/grader/grading.ml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,50 @@ let get_grade
142142
Toploop_ext.use_string ~print_outcome ~ppf_answer
143143
"module Report = Learnocaml_report" ;
144144
set_progress [%i"Launching the test bench."] ;
145+
146+
let () =
147+
let open Learnocaml_exercise in
148+
let files = File.dependencies (access File.depend exo) in
149+
let rec load_dependencies signatures = function
150+
| [] -> () (* signatures without implementation are ignored *)
151+
| file::fs ->
152+
let path = File.key file
153+
and content = decipher file exo in
154+
let modname = String.capitalize_ascii @@
155+
Filename.remove_extension @@ Filename.basename path in
156+
match Filename.extension path with
157+
| ".mli" -> load_dependencies ((modname,content) :: signatures) fs
158+
| ".ml" ->
159+
let included,content =
160+
(* the first line of an .ml file can contain an annotation *)
161+
(* [@@@included] which denotes that this file has to be included *)
162+
(* directly in the toplevel environment, and not in an module. *)
163+
match String.index_opt content '\n' with
164+
| None -> (false,content)
165+
| Some i ->
166+
(match String.trim (String.sub content 0 i) with
167+
| "[@@@included]" ->
168+
let content' = String.sub content i @@
169+
(String.length content - i)
170+
in (true,content')
171+
| _ -> (false,content))
172+
in
173+
(handle_error (internal_error [%i"while loading user dependencies"]) @@
174+
match included with
175+
| true -> Toploop_ext.use_string ~print_outcome ~ppf_answer
176+
~filename:(Filename.basename path) content
177+
| false ->
178+
let use_mod =
179+
Toploop_ext.use_mod_string ~print_outcome ~ppf_answer ~modname in
180+
match List.assoc_opt modname signatures with
181+
| Some sig_code -> use_mod ~sig_code content
182+
| None -> use_mod content);
183+
load_dependencies signatures fs
184+
| _ -> failwith ("uninterpreted dependency \"" ^ path ^
185+
"\", file extension expected : .ml or .mli") in
186+
load_dependencies [] files
187+
in
188+
145189
handle_error (internal_error [%i"while testing your solution"]) @@
146190
Toploop_ext.use_string ~print_outcome ~ppf_answer ~filename:(file "test.ml")
147191
(Learnocaml_exercise.(decipher File.test exo)) ;

0 commit comments

Comments
 (0)