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
:::
::::
|