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
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.
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
Nonewhen 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.
Nonewhen 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.
Nonewhen 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 answersSystem.IO.FileNotFoundException, orSystem.IO.DirectoryNotFoundExceptionwhen 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
WriteModeto a stream to write the file through.Appendstarts the stream with what the path held.CreateNewon a file that exists answersSystem.IO.IOException. A directory answersSystem.UnauthorizedAccessException, and a missing directory above the path answersSystem.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, orNonewhen 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.GetFileSystemEntriesgives them:"dir/a.txt", not"a.txt". A path that is not a directory answersSystem.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.CreateDirectorydoes. A directory that exists is not an error. A file at the path answersSystem.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 answersSystem.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 answersSystem.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.Movedoes: a missing source answersSystem.IO.FileNotFoundException. A destination that exists answersSystem.IO.IOException, unless the mode isMoveReplaceand a file replaces a file. A missing directory above the destination answersSystem.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