(std simpletest)

Reference

Overview

Assertions that print a line each, with-test for a body under a scope, and a fake filesystem.

(import (std prelude))
(import (std simpletest))

(: main (-> (Vec string) int))
(defun (main args)
  (expect "adds" (+ 1 2) 3)              ; prints: ok: adds
  (with-test "reads the fixture"
    (with-fake-fs (("a.txt" "hello"))
      (expect "read it" (call-with-input-file "a.txt" read-all) (Ok "hello"))))
  0)

Assertions. expect, expect-true and fail print one line for each assertion. A passed assertion prints ok: label. A failed assertion prints a line that starts with FAILURE:, and sets the exit code of the program to 1, even when main answers 0. Thus a test file is a command that a shell or CI can check, and the compiler’s own test runner looks for the FAILURE: lines. A failed assertion does not stop the program.

(std simpletest) and (std test). This module has no registry and no runner: a test file is an ordinary program that calls the assertions. (std test) is the newer harness. Its tests are silent unless they fail, they are registered with deftest, and bjo test runs them. Each deftest runs in a scope as with-test does. The fake filesystem of this module works in both.

with-test runs a body under a deadline, and fails if the body leaves a resource open. The scope gives four things. A test gets a timeout, so a leaked fiber is a failure that names the test, and the suite does not hang. The scope waits for the fibers that the body spawned, so an assertion inside one is not missed. The resources that the body opened are released. And several failures in one body are all reported, because an assertion does not stop the body.

The fake filesystem. with-fake-fs installs a handler for each operation of the FS effect of the prelude. Everything that goes to the disk through it answers from a map instead: the text and byte openers, file-read-text and its neighbours, file-exists?, file-delete, file-move and the directory functions. See FakeFs for its rules.

See also: expect, with-test, with-fake-fs

Reference

Types

Functions

Macros

Types

FakeFs

record ↑ Types

A filesystem in memory, for with-fake-fs-of.

(def fs (fake-fs (list (Tuple "in.txt" "a b c"))))
(with-fake-fs-of fs
  (ignore (file-write-text "out.txt" "done")))
(fake-fs-written fs "out.txt")   ; (Some "done")

It is mutable, so that a later read sees a write.

Paths. A seeded path is a file, and every directory above a file exists. A path written in the fake can be read after. Paths are compared as text after one normalization: a backslash becomes a slash, and a leading ./ and a trailing / are removed. "" and "." are the root. Nothing resolves .., and a relative path is not made absolute, so "a.txt" and "/tmp/a.txt" are different files.

Contents. A write to a path replaces what the path held. A write in Append mode continues what the path held. Bytes are kept as they were written, also when they are not UTF-8. A read sees what has reached the stream of a writer, also when the writer is still open.

Errors. An operation that fails answers the exception type that the .NET call would raise, so a test that matches on the error matches either way.

Time. The fake has a clock of its own, so that a test about modified times does not depend on how fast it runs. The clock starts at 2000-01-01T00:00:00Z, which is the time of every seed. Each write that is opened and each directory that is created moves the clock one second forward and stamps the path with it. Thus a file written later is always newer. A move keeps the times of what it moves, as a rename on a disk does. fake-fs-set-modified! sets a time.

Ports. The openers answer a MemoryStream, and the ports on it are made by the same code that makes ports on a file. Thus a fake port is registered on the scope that opened it, and a leak test on a fake filesystem is the same as a leak test on a real one.

See also: fake-fs, with-fake-fs, with-fake-fs-of, fake-fs-written, fake-fs-contents

Functions

expect

function ↑ Functions

(: expect (-> string %a %a void))
(expect label actual wanted)

Asserts that two values are equal, and prints the result.

label
What the assertion is about. The printed line shows it.
actual
The value to test.
wanted
The value it must be equal to.
(expect "parse" (parse "12") (Ok 12))
;; prints:  ok: parse
;; or:      FAILURE: parse gave (Ok 13) but wanted (Ok 12)

A failure also sets the exit code of the program to 1.

See also: expect-true, fail

expect-true

function ↑ Functions

(: expect-true (-> string bool void))
(expect-true label ok)

Asserts that a condition is #t, and prints the result.

label
What the assertion is about. The printed line shows it.
ok
The condition.

Prints ok: label, or FAILURE: label and sets the exit code of the program to 1.

See also: expect, fail

fail

function ↑ Functions

(: fail (-> string void))
(fail label)

Fails at once: for a branch that a test must not reach.

label
Why the branch is wrong.

Prints FAILURE: label and sets the exit code of the program to 1. It does not stop the program.

See also: expect, expect-true

default-test-deadline

function ↑ Functions

(: default-test-deadline (-> Duration))
(default-test-deadline)

The deadline of a with-test that gives none: 5 seconds.

returns
(seconds 5).

Exported because the template of with-test names it.

See also: with-test

overran?

function ↑ Functions

(: overran? (-> (Box (Option Scope)) bool))
(overran? holder)

Whether a deadline ended a scope.

holder
A box that holds the scope, or None when the scope did not start.
returns
#t when the scope was cancelled because of its deadline, else #f.

A fiber that a body leaked is cancelled by the deadline, and a cancelled child is not a failure, so the scope closes cleanly. The token of the scope keeps the reason for the cancellation, so with-test keeps the scope in a box and reads the reason after the scope has closed. Exported because the template of with-test names it.

See also: with-test

fake-fs

function ↑ Functions

(: fake-fs (-> (List (Tuple string string)) FakeFs))
(fake-fs seeds)

A new fake filesystem, seeded with files.

seeds
Pairs of a path and the text of the file at that path.
returns
The filesystem. Every directory above a seeded file exists.
(fake-fs (list (Tuple "notes.txt" "a b c")
               (Tuple "dir/empty.txt" "")))

See also: with-fake-fs-of, with-fake-fs

fake-fs-written

function ↑ Functions

(: fake-fs-written (-> FakeFs string (Option string)))
(fake-fs-written fs path)

What was written to a path of a fake filesystem.

fs
The filesystem.
path
The path.
returns
The text written to the path, decoded as UTF-8. None when nothing was written to it, also when it is a seeded file.

It works after the port is closed, because it reads the MemoryStream with ToArray.

See also: fake-fs-contents, with-fake-fs-of

fake-fs-contents

function ↑ Functions

(: fake-fs-contents (-> FakeFs string (Option string)))
(fake-fs-contents fs path)

What a file of a fake filesystem holds, whether it was seeded or written.

fs
The filesystem.
path
The path.
returns
The text of the file, decoded as UTF-8. None when there is no file at the path.

See also: fake-fs-written

fake-fs-set-modified!

function ↑ Functions

(: fake-fs-set-modified! (-> FakeFs string long void))
(fake-fs-set-modified! fs path ms)

Sets the modified time of a path of a fake filesystem.

fs
The filesystem.
path
The path.
ms
The time, in Unix milliseconds.
;; for a test such as "skip what was written in the last five seconds"
(fake-fs-set-modified! fs "old.txt" 946684800000L)

See also: FakeFs

fake-fs-reader

function ↑ Functions

(: fake-fs-reader (-> FakeFs (-> string (Result Exception Stream))))
(fake-fs-reader fs)

The handler of open-read-stream for a fake filesystem.

fs
The filesystem.
returns
A function from a path to a stream on what the file holds. A directory answers System.UnauthorizedAccessException. A missing file answers System.IO.FileNotFoundException, or System.IO.DirectoryNotFoundException when its directory is missing too.

The handlers are exported because the templates of with-fake-fs-of name them. Use with-fake-fs-of to install them.

See also: with-fake-fs-of

fake-fs-writer

function ↑ Functions

(: fake-fs-writer (-> FakeFs (-> string WriteMode (Result Exception Stream))))
(fake-fs-writer fs)

The handler of open-write-stream for a fake filesystem.

fs
The filesystem.
returns
A function from a path and a WriteMode to a stream to write the file through. Append starts the stream with what the path held. CreateNew on a file that exists answers System.IO.IOException. A directory answers System.UnauthorizedAccessException, and a missing directory above the path answers System.IO.DirectoryNotFoundException. Each write that opens moves the clock one second forward and stamps the path.

See also: with-fake-fs-of

fake-fs-info

function ↑ Functions

(: fake-fs-info (-> FakeFs (-> string (Option FsInfo))))
(fake-fs-info fs)

The handler of fs-info for a fake filesystem.

fs
The filesystem.
returns
A function from a path to its FsInfo, or None when nothing is at the path. The size is in bytes, and is 0 for a directory. The modified time is the stamp of the path, or the start of the clock. Nothing is a link.

See also: with-fake-fs-of

fake-fs-list

function ↑ Functions

(: fake-fs-list (-> FakeFs (-> string (Result Exception (Vec string)))))
(fake-fs-list fs)

The handler of fs-list for a fake filesystem.

fs
The filesystem.
returns
A function from a directory to the paths of what is directly in it, sorted. The paths are whole, as Directory.GetFileSystemEntries gives them: "dir/a.txt", not "a.txt". A path that is not a directory answers System.IO.DirectoryNotFoundException.

See also: with-fake-fs-of

fake-fs-create-directory

function ↑ Functions

(: fake-fs-create-directory (-> FakeFs (-> string (Result Exception Unit))))
(fake-fs-create-directory fs)

The handler of fs-create-directory for a fake filesystem.

fs
The filesystem.
returns
A function that creates a directory and each missing directory above it, as Directory.CreateDirectory does. A directory that exists is not an error. A file at the path answers System.IO.IOException. A new directory moves the clock one second forward and is stamped with it.

See also: with-fake-fs-of

fake-fs-delete-file

function ↑ Functions

(: fake-fs-delete-file (-> FakeFs (-> string (Result Exception Unit))))
(fake-fs-delete-file fs)

The handler of fs-delete-file for a fake filesystem.

fs
The filesystem.
returns
A function that deletes a file. A missing file is not an error, as with File.Delete. A directory answers System.UnauthorizedAccessException.

See also: with-fake-fs-of

fake-fs-delete-directory

function ↑ Functions

(: fake-fs-delete-directory (-> FakeFs (-> string (Result Exception Unit))))
(fake-fs-delete-directory fs)

The handler of fs-delete-directory for a fake filesystem.

fs
The filesystem.
returns
A function that deletes an empty directory. A directory that is not empty, the root, and a file at the path answer System.IO.IOException. A missing directory answers System.IO.DirectoryNotFoundException.

See also: with-fake-fs-of

fake-fs-move

function ↑ Functions

(: fake-fs-move (-> FakeFs (-> string string MoveMode (Result Exception Unit))))
(fake-fs-move fs)

The handler of fs-move for a fake filesystem.

fs
The filesystem.
returns
A function that moves a file, or a directory and everything below it, with the modified times. The source is checked first, as File.Move does: a missing source answers System.IO.FileNotFoundException. A destination that exists answers System.IO.IOException, unless the mode is MoveReplace and a file replaces a file. A missing directory above the destination answers System.IO.DirectoryNotFoundException.

See also: with-fake-fs-of

Macros

with-test

macro ↑ Macros

(with-test name body ...)
(with-test name #:within d body ...)
(with-test name #:open-ok body ...)
(with-test name #:within d #:open-ok body ...)

Runs a body under a deadline, and fails if it left a resource open.

name
A string: the name of the test. The failure lines show it.
d
The deadline, a Duration. (default-test-deadline), 5 seconds, when it is not given.
body ...
The forms of the test. At least one form. Their values are dropped.
(with-test "reads the fixture"
  (with-fake-fs (("a.txt" "hello"))
    (expect "read it" (call-with-input-file "a.txt" read-all) (Ok "hello"))))

The body runs under with-deadline, so it is a scope. The scope waits for the fibers that the body spawned, and releases the resources that the body opened. When both options are given, #:within comes first.

The test fails, with a FAILURE: line that names it:

- when the body leaves a resource open. The check is done before the scope closes, because the scope releases the resources when it closes, and a test must fail if it does not close its files. #:open-ok turns off the check. Use it in a test of what the closing of a scope does. - when the deadline ends the scope. A fiber that the body leaked is cancelled by the deadline, and the scope then closes cleanly, so without this check the test would pass, slowly. - when the body raises an exception, a cancellation included. The failure shows the message.

It catches every System.Exception, a cancellation included. Usually, a cancellation must not be caught. But this is the top of a test, and a failure that names the test is better than a stack trace.

See also: default-test-deadline, with-fake-fs, expect

with-fake-fs-of

macro ↑ Macros

(with-fake-fs-of fs body ...)

Runs a body with an existing fake filesystem installed for the FS effect.

fs
A FakeFs. It is evaluated once.
body ...
What to run. At least one form. Its last value is the value of the form.
(def fs (fake-fs (list)))
(with-fake-fs-of fs
  (ignore (file-write-text "out.txt" "done")))
(expect "wrote it" (fake-fs-written fs "out.txt") (Some "done"))

Use it, not with-fake-fs, to examine the filesystem after the body.

It is a handler frame, not a scope: it does not wait for a fiber spawned in the body. The fiber keeps the fake, because a spawn takes the environment as it was at the spawn. But the assertions of the body, and the leak check of with-test, run before the fiber is done. So put a fiber that reads or writes the fake in a with-scope of its own, inside this form. This form opens no scope, because a scope would hide from with-test what the body left open.

A macro that writes a with-handler must give the operation names through inject, as this one does. with-handler derives the cell it binds from the name it is given, and a renamed name derives a cell that nothing declares.

See also: with-fake-fs, fake-fs, fake-fs-written

with-fake-fs

macro ↑ Macros

(with-fake-fs ((path contents) ...) body ...)

Runs a body with a new fake filesystem, seeded with files, installed for the FS effect.

path
The path of a seeded file.
contents
The text of that file.
body ...
What to run. At least one form. Its last value is the value of the form.
(with-fake-fs (("notes.txt" "a b c")
               ("empty.txt" ""))
  (expect "read it back" (call-with-input-file "notes.txt" read-all) (Ok "a b c")))

(with-fake-fs-of (fake-fs seeds) body ...). Use with-fake-fs-of to examine the filesystem after the body. A fiber spawned in the body is not waited for: see with-fake-fs-of.

See also: with-fake-fs-of, FakeFs, with-test