Cram Tests
==========

Synopsis
--------

Cram tests are integrations tests that describe a shell session. These tests
contain commands and expected outputs. When executed, the commands are executed
and the actual output is compared to the expected output.

Here is an example showing how ``echo``, ``cat``, and ``rm`` interact.

.. code:: cram

   Create a file:

     $ echo contents > data.txt

   Display it:

     $ cat data.txt
     contents

   Remove it:

     $ rm data.txt

   Try to remove it again:

     $ rm data.txt
     rm: cannot remove 'data.txt': No such file or directory
     [1]

The syntax mimics a shell session: there are comments and shell commands with
their output.

Examples
--------

Simple Commands
^^^^^^^^^^^^^^^

This is the simplest test case: it executes the command ``touch
this-file.txt`` and expects that the command has no output.

.. code:: console

   $ touch this-file.txt

Output
^^^^^^

This executes ``ls`` and expects it to display ``this-file.txt``:

.. code:: console

   $ ls
   this-file.txt

There can be several output lines if the command is expected to print several
lines.
Also, note that if a command has no output, the next one can come in the next
line.

.. code:: console

   $ touch other-file.txt
   $ ls
   other-file.txt
   this-file.txt

Comments
^^^^^^^^

Lines that are not indented are ignored. These act as comments.

.. code:: cram

   "touch" will create an empty file:

     $ touch data.txt

   Printing it will do nothing:

     $ cat data.txt

Continuation Lines
^^^^^^^^^^^^^^^^^^

Continuation lines are used when a command fits on several lines. This can
happen in all the cases where pressing Enter would not run the command. For
example, when passing a backslash character to escape the line ending. In that
case, all the continuation lines are grouped together as a single command.

This syntax mimics the PS2 prompt in shells - the ">" character is not passed
to the command.

.. code:: console

   $ echo \
   >   a \
   >   b \
   >   d \
   >   c
   a b c d

This is often used with shell "heredocs" to create files:

.. code:: console

   $ cat > file.txt << EOF
   > Everything
   > here will
   > written to
   > the file
   > EOF

   $ cat file.txt
   Everything
   here will
   written to
   the file

Exit Codes
^^^^^^^^^^

When a command exits with a nonzero exit code, it is displayed between square
brackets after its output:

.. code:: console

   $ false
   [1]

   $ echo hello; false
   hello
   [1]

Syntax Details
--------------

Cram tests are parsed line by line, depending on the first characters of
each line:

- If a line starts with ``␣␣$␣`` (``␣`` denoting a space character), the rest
  is a command.
- If it starts with ``␣␣>␣``, the rest is the continuation of a command
  (continuation lines must immediately follow a command).
- If it start with ``␣␣`` and something else, the rest is the expected output
  or exit code of the previous command.
- Everything else is a comment.

File and Directory Tests
------------------------

There are two types of Cram tests: file tests and directory tests. File tests
are files with a ``.t`` extension. Directory tests are files named ``run.t``
within a directory with a name that ends with ``.t``.

A Cram test begins its execution in a temporary directory where its
dependencies (as listed in the corresponding :doc:`cram stanzas <dune/cram>`,
if any) are available. In the case of a directory test, the contents of the
directory are also available.

File tests have the nice property that they are self-contained: everything
happens in a single file. This is handy because it does not make a deep file
hierarchy in a project. But if the test requires some files, these need to be
created using ``cat`` and heredocs. Directory tests, on the other hand, allow
creating these test fixtures as normal files. This can be more comfortable
because it makes the usual tooling (syntax highlighting, completion, etc.)
available.

Executing Cram Tests
--------------------

Every Cram test has a name. For file tests, the name of ``something.t`` is
``something``, and for directory tests, the name of ``something.t/run.t`` is
``something``.

There are several ways to execute Cram tests:

- Running ``dune runtest something.t`` will run the cram test with filename
  ``something.t``.
- All Cram tests are attached to the :doc:`/reference/aliases/runtest` alias.
  So ``dune runtest`` will run all Cram tests.
- Every Cram test creates an alias after its name. So, ``dune build
  @something`` will run tests named ``something``.


When a Cram test is executed, the commands it contains are executed, and a
corrected file is created where the command outputs are inserted after
each command. This corrected file is then offered for :doc:`promotion
<../concepts/promotion>` by Dune.

Concretely, this means that Dune will display the difference between the
Cram test's current contents and the latest run's output. This diff
can be applied by running ``dune promote``, as usual.

.. code:: diff

   $ touch changed-name.txt
   $ ls
  -other-file.txt
  +changed-name.txt
   this-file.txt

Early Shell Exits
^^^^^^^^^^^^^^^^^

If a command causes the shell to exit early, all subsequent commands become
unreachable. Dune marks these with ``***** UNREACHABLE *****``:

.. code:: cram

   Commands after an early exit are unreachable:

     $ exit 1
     ***** UNREACHABLE *****
     $ echo "this won't run"
     ***** UNREACHABLE *****

Output from commands that did execute can still be promoted normally.

A common cause of unexpected early exits is the POSIX special builtin behavior.
Cram tests run with ``sh`` by default, which operates in POSIX mode. In POSIX,
``.`` (source) is a *special builtin*, and when a special builtin fails, a
non-interactive shell exits immediately.

This means ``source foo.sh`` (without ``./``) can cause the shell to exit if
``foo.sh`` is not found in ``PATH``:

.. code:: cram

   This will exit the shell because foo.sh is not in PATH:

     $ source foo.sh
     ***** UNREACHABLE *****

   Use an explicit path to avoid this:

     $ source ./foo.sh

