File: structure.md

package info (click to toggle)
ocaml-dune 3.20.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 33,564 kB
  • sloc: ml: 175,178; asm: 28,570; ansic: 5,251; sh: 1,096; lisp: 625; makefile: 148; python: 125; cpp: 48; javascript: 10
file content (201 lines) | stat: -rw-r--r-- 5,493 bytes parent folder | download | duplicates (2)
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
Improving Structure
===================

Our calculator is fairly monolithic at this stage.
Instead of a single executable, we're going to extract a library and create
some tests.
For now, this is just a cram test that will call the executable, but this
structure will later allow adding unit tests for the library.

## Extract a Library

Create folders `bin`, `lib` and `test`, for binary, library, and tests,
respectively.

Run the following command to install [cmdliner](https://ocaml.org/p/cmdliner/1.3.0/doc/index.html):

```sh
opam install cmdliner.1.3.0
```

Let's create a library. Create `lib/dune` with the `(ocamllex)` and `(menhir)` stanzas from the original `dune` file and a new `(library)` stanza:

:::{literalinclude} structure/lib/dune
:language: dune
:emphasize-lines: 1-3
:::

:::{note}
This is the whole contents of the file. The `(library)` part is highlighted to
show that it's the part that we've just added.
:::

:::{note}

We're defining a {doc}`library </reference/dune/library>` that depends on the
`cmdliner` library.

Libraries can either be defined in your project, or provided by an opam
package. In the case of `cmdliner`, this is the latter, since we've installed
it just before.

{doc}`The OCaml Ecosystem </explanation/ocaml-ecosystem>` covers the difference
between packages, libraries, and modules.
:::

Move `ast.ml`, `lexer.mll`, and `parser.mly` to the `lib` directory.

Now we're going to move `calc.ml` to `lib/cli.ml` and replace it by the following:

:::{literalinclude} structure/lib/cli.ml
:language: ocaml
:::

:::{note}
Two things are happening here.

We are adding a second code path to evaluate a `string` directly, so we extract
an `eval_lb` function that operates on a `lexbuf` (the "source" a lexer can
read from).

We are also moving to `cmdliner` for command-line parsing. This consists in:
- an `info` value (of type `Cmdliner.Cmd.info`) which contains metadata for the program (used in help, etc)
- a `term` value (of type `unit Cmdliner.Term.t`) which sets up arguments and calls `eval_lb` with the right `lexbuf`
- a `cmd` value (of type `unit Cmdliner.Cmd.t`) grouping `info` and `term` together 
- a `main` function of type `unit -> 'a` to run `cmd`
:::

## Extract an Executable

Let's create an executable in `bin`. To do so, create a `bin/dune` file with the following contents:

:::{literalinclude} structure/bin/dune
:language: dune
:::

And `bin/calc.ml` with a single function call:

:::{literalinclude} structure/bin/calc.ml
:language: ocaml
:::

Delete the `dune` at the root.

## Create a Test

Create `test/calc.t` with the following contents.

:::{important}
In {doc}`cram tests </reference/cram>`, commands start with two spaces, a
dollar sign, and a space.

Make sure to include **two spaces** at the beginning of the line.
:::

:::{literalinclude} structure/test/calc.t
:language: cram
:lines: 1
:::

Now create `test/dune` to inform Dune that cram tests will use our `calc`
executable and need to be executed again when it changes:

:::{literalinclude} structure/test/dune
:language: dune
:::

At this stage, we're ready to run our test.

Let's do this with `dune runtest`.

It's displaying a diff:

```diff
   $ calc -e '1+2'
+  3
```

Now, run `dune promote`. The contents of `test/calc.t` have changed. Most
editors will pick this up automatically, but it might be necessary to reload
the file to see the change.

Finally, run `dune runtest`. Nothing happens.

Now, run the calculator by running `dune exec calc` to confirm that the
interactive mode still works.

:::{note}
What happened here? This Dune feature, where some tests can edit the source
file, is called {doc}`promotion </concepts/promotion>`.

{doc}`Cram tests </reference/cram>` contain both commands and their expected input.
We did not include any output in the initial cram test. When running `dune
runtest` for the first time, Dune executes the commands, and calls `diff`
between the *expected output* (in `test/calc.t`: no output at all) and the *actual
output* (from running the command: the line "3"), which will display added
lines with a `+` sign and deleted lines with a `-` sign.

Running `dune promote` replaces the input file (`test/calc.t`) with the last
*actual output*. So this includes the line with "3".

Running `dune runtest` again will execute the test again and compare the
*expected output* (`test/calc.t` with the "3" line in it) with the *actual
output* and finds no difference. This means that the test passes.
:::

::::{dropdown} Checkpoint
:icon: location

This is how the project looks like at the end of this chapter.

:::{literalinclude} introduction/dune-project
:caption: dune-project (unchanged)
:language: dune
:::

:::{literalinclude} structure/bin/dune
:caption: bin/dune
:language: dune
:::

:::{literalinclude} structure/bin/calc.ml
:caption: bin/calc.ml
:language: ocaml
:::

:::{literalinclude} structure/lib/dune
:caption: lib/dune
:language: dune
:::

:::{literalinclude} introduction/ast.ml
:caption: lib/ast.ml (unchanged)
:language: ocaml
:::

:::{literalinclude} structure/lib/cli.ml
:caption: lib/cli.ml
:language: ocaml
:::

:::{literalinclude} introduction/lexer.mll
:caption: lib/lexer.mll (unchanged)
:language: ocaml
:::

:::{literalinclude} introduction/parser.mly
:caption: lib/parser.mly (unchanged)
:language: ocaml
:::

:::{literalinclude} structure/test/dune
:caption: test/dune
:language: dune
:::

:::{literalinclude} structure/test/calc.t
:caption: test/calc.t
:language: cram
:::

::::