check
Tests: unit tests, properties and models, run by heff test.
A test is a top-level definition of type Test: heff test FILE
runs each one of the file’s own, after its watches, but one another
of them uses (directly or through the file’s other definitions),
which runs under that one, by its groups’ names. It is found by
its type, not by its name; a watch stays an example in a definition’s
documentation, a test is what is checked of it, and both are counted
by the same runner.
import check import co import generator as Generator
arithmetic : Test arithmetic = group “arithmetic” [ unit “one and one” (assertEqual (1 + 1) 2) , property “reversed twice” (do xs <- forAll (Generator.list 0 20 (Generator.int 0 99)) label (if null xs then “empty” else “not empty”) assertEqual (reverse (reverse xs)) xs) ]
A test is a computation with effects, and its type says
which: a unit test only checks (check), a property also draws random
values (random). Neither can do anything else. The runner handles
both, and gives the randomness from a seed, so that a run is replayed
by its seed. It handles them in a generator’s monad, so
a property is itself a generator, of runs: a run that fails shrinks
as all of its draws do, each made again from the shrinks of those
before it, with no shrinking written for it.
An integration test (integration) also has the world outside: the
process (commands run, std.process) and the filesystem. heff test
cannot run it in itself; it builds the file’s tests into a program
through C (integrationMain), and runs each integration test in it,
in a new directory of its own, killed at its time limit (withinTime).
A unit test or an integration test may keep golden texts (golden),
held to the files kept beside the test’s, or written there by heff test --update.
What a value is shown as, for a counterexample or for what differs, is
its type’s Debug (the prelude’s: as the source writes it, a string
quoted, not its Show text), an instance argument: a type of the
test’s own is given one by an instance (instance instanceDebugColor : Debug Color). A generator is given by hand. Two values are equal
for assertEqual when they are shown the same, as a watch holds when
its value is shown as its expectation is.
A model-based test (agree) runs a program of commands on a system
and on a model of it, and holds when they observe the same after each
command; a property draws the programs, which shrink to a short one
that tells the two apart (std.ordmap against a
list).
RandomF
data RandomF (m : Type -> Type) x = Draw (Generator.Generator x)Randomness: a value drawn from a generator.
random
random : HFunctorFor random values drawn: what a property is given, and a unit test is not.
Failure
data Failure = Because String | Differ String StringWhy a test failed: said in words, or two values (shown) that are not the same, what was expected first.
CheckF
data CheckF (m : Type -> Type) x = Fail Failure | Discard | Skip String | Label String x | Note (Unit -> String) x | Golden String String x | Sub String (m Unit) xChecking: a failure, a run given up on (its assumptions do not hold),
a label of the run (counted over the runs), a note (shown if the
run fails; made only then), a golden text (a name, and the text,
held by heff test to the one kept under that name), and a subtest
(its name, and what it does), and a test skipped (why: what it
needs is not there).
check
check : HFunctorFor what a test says of itself: a failure, a case discarded or skipped, labels and notes, a golden text compared, and its subtests.
forAll
forAll : {a : Type} -> {e : HFunctor} -> {{rnd : random <: e}} -> {{chk : check <: e}} -> {{shows : Debug a}} -> Generator.Generator a -> Co e aA value of g, shown (debug) if the run fails.
failure
failure : {a : Type} -> {e : HFunctor} -> {{chk : check <: e}} -> String -> Co e aThe test fails, for this reason.
assert
assert : {e : HFunctor} -> {{chk : check <: e}} -> String -> Bool -> Co e UnitThe test fails unless b holds; what says what should have.
assertEqual
assertEqual : {a : Type} -> {e : HFunctor} -> {{chk : check <: e}} -> {{shows : Debug a}} -> a -> a -> Co e UnitThe test fails unless what was got (first) and what was expected are
the same when shown (debug); then it says what differs.
assume
assume : {e : HFunctor} -> {{chk : check <: e}} -> Bool -> Co e UnitThe run is given up on unless b holds: a property’s assumption.
Runs given up on are not counted as runs, and too many fail it.
skip
skip : {a : Type} -> {e : HFunctor} -> {{chk : check <: e}} -> String -> Co e aThe test is skipped, for this reason: what it needs is not there (a
tool not installed, a system it is not of). heff test says it is,
and why, and counts it apart, neither held nor failed.
label
label : {e : HFunctor} -> {{chk : check <: e}} -> String -> Co e UnitThe run is counted under l: the runner says what part of the runs
each label had, so that a property that held only of trivial values
shows.
note
note : {e : HFunctor} -> {{chk : check <: e}} -> String -> Co e UnitShown with the counterexample, if the run fails.
golden
golden : {e : HFunctor} -> {{chk : check <: e}} -> String -> String -> Co e UnitThe text, held to the golden one kept under name: heff test
keeps it in a file beside the test’s own, in the directory named as
the file without its .hf and with .golden (NAME in
parser.golden/, of parser.hf), and fails the test where the two
differ; heff test --update writes the text there instead. The
name is a file’s: letters, digits, -, _ and ., not first. A
unit test or an integration test keeps goldens, not a property.
subtest
subtest : {e : HFunctor} -> {{chk : check <: e}} -> String -> Co e Unit -> Co e UnitA test of its own within an integration test, as Go’s t.Run: it
is said on a line of its own, test / name, holds or fails by
itself and keeps its own golden texts, and --run selects it at its
level. heff test runs it in a program of its own, as many at once
as its jobs: the test’s program, asked for the subtest by its names,
does the test again, doing only the subtests on the way to it. So a
test does the same each time up to its subtests, and names each once
(no / in a name). What the test does after a subtest is done as
if the subtest held.
each : List String -> Co e Unit each Nil = pure MkUnit each (Cons n rest) = do subtest n (do …) each rest
Trace
type Trace = { notes : List (Unit -> String) , labels : List String , goldens : List (String, String) , subtests : List String }What a run has said so far, backwards: its notes, labels, golden texts and the subtests it named.
noTrace
noTrace : TraceandThen
andThen : Trace -> Trace -> TraceRun
data Run a = Going Trace a | Failed Trace Failure | Discarded | Skipped Trace String | Reached (Run Unit)A run of a test, so far: going on, failed, given up on, or come to the subtest it was run for, and what came of that.
after
after : {a : Type} -> Trace -> Run a -> Run aproceed
proceed : {a b : Type} -> (a -> Generator.Generator (Run b)) -> Run a -> Generator.Generator (Run b)drawAlg
drawAlg : {m : Type -> Type} -> {a : Type} -> RandomF m a -> Generator.Generator (Run a)checkRun
checkRun : {m : Type -> Type} -> {a : Type} -> CheckF m a -> Run acheckAlg
checkAlg : {m : Type -> Type} -> {a : Type} -> CheckF m a -> Generator.Generator (Run a)runner
runner : Handler (random <+> check)The runner’s handler: a test is a generator of runs, so a draw is a generator’s value, and a run’s draws shrink together.
Config
type Config = { runs : Int, discards : Int, shrinks : Int }How a property is run: how many runs, how many may be given up on, and how many shrinks are tried at most.
defaults
defaults : Configoutside
outside : HFunctorThe world outside the program: the process (commands run), the
filesystem, and the releases the program’s withs keep there.
integrated
integrated : HFunctorThe effects an integration test is run in: checking, and the world outside.
defaultLimit
defaultLimit : IntHow long an integration test is given, unless withinTime says: a
minute.
Test
data Test = UnitTest String (Co (random <+> check) Unit) | PropTest String Config (Co (random <+> check) Unit) | IntegrationTest String Int (Co integrated Unit) | Group String (List Test)A test: a unit test, a property, an integration test (its time limit, in milliseconds), or tests under a name. (Each is kept in the runner’s world, its effects given.)
unit
unit : String -> ({e : HFunctor} -> {{chk : check <: e}} -> Co e Unit) -> TestA unit test: it only checks.
property
property : String -> ({e : HFunctor} -> {{rnd : random <: e}} -> {{chk : check <: e}} -> Co e Unit) -> TestA property: it draws random values and checks them, run 100 times
(runs says otherwise).
thrownOutside
thrownOutside : Either Process.ProcessError (Either Filesystem.FileError Unit) -> Co integrated Unitintegration
integration : String -> ({e : HFunctor} -> {{chk : check <: e}} -> {{sys : Process.process <: e}} -> {{disk : Filesystem.filesystem <: e}} -> {{ex : exception Process.ProcessError <: e}} -> {{fx : exception Filesystem.FileError <: e}} -> {{cu : custodian <: e}} -> Co e Unit) -> TestAn integration test: it checks, runs commands and reads and writes
files. heff test runs it in a program of its own, built through
C, in a new directory of its own as its working directory (removed
after it), and kills it at its time limit (a minute; withinTime says
otherwise). A command held by withProcess that could not be run,
a file held by a with that did not open, is thrown; a throw the
test does not hear (try) fails it.
group
group : String -> List Test -> TestTests under a name.
runs
runs : Int -> Test -> TestThe property run n times (each property of the group).
withinTime
withinTime : Int -> Test -> TestThe integration test given ms milliseconds, not a minute (each
integration test of the group).
Machine
type Machine c s m o = { system : s , model : m , step : c -> s -> (s, o) , stepModel : c -> m -> (m, o) , valid : s -> Bool }A system and a model of it, each run a command at a time, each
saying what it observes of the command: they agree when they observe
the same. valid is what the system must always be.
agree
agree : {c s m o : Type} -> {e : HFunctor} -> {{chk : check <: e}} -> {{showsC : Debug c}} -> {{showsO : Debug o}} -> Machine c s m o -> List c -> Co e UnitThe commands run on the system and on the model, in order: after
each, both observe the same, and the system is valid. (Drawn by a
property, the commands shrink to a short program that tells them
apart.) Each is shown, a command and what is observed, as its type’s
Debug writes it.
Outcome
type Outcome = { name : String , ok : Bool , skipped : Maybe String , property : Bool , runs : Int , discarded : Int , shrinks : Int , labels : List (String, Int) , notes : List String , failure : Maybe Failure , seed : Int , goldens : List (String, String) , subtests : List String }What came of a test: its name (under its groups’), whether it held,
how many runs held and how many were given up on, how many shrinks
made the counterexample, how many runs had each label, the
counterexample’s notes (what was drawn, in order), why it failed,
the seed it was run from, the golden texts it kept (each a name
and a text, in order), for heff test to hold to theirs, the
subtests it named, in order, for heff test to run, and why it was
skipped, if it was (it holds, then, having checked nothing).
hashName
hashName : String -> IntnubLabels
nubLabels : List String -> List StringcountLabels
countLabels : OrdMap.OrdMap String Int -> List String -> OrdMap.OrdMap String IntTally
type Tally = { runs : Int, discarded : Int, labels : OrdMap.OrdMap String Int }noTally
noTally : Tallypassed
passed : String -> Bool -> Int -> Tally -> List (String, String) -> OutcomefailedWith
failedWith : String -> Bool -> Int -> Tally -> Int -> Trace -> Failure -> OutcomeskippedWith
skippedWith : String -> Bool -> Int -> Trace -> String -> OutcomeisFailed
isFailed : {a : Type} -> Run a -> BoolfirstFailing
firstFailing : List (Unit -> Generator.Tree (Run Unit)) -> Maybe (Generator.Tree (Run Unit))Shrunk
type Shrunk = { tree : Generator.Tree (Run Unit), shrinks : Int }shrink
shrink : Int -> Int -> Generator.Tree (Run Unit) -> ShrunkA failing run made smaller: its first shrink that fails too, then
that one’s, as long as there is one (and limit is not reached).
failedRun
failedRun : String -> Bool -> Int -> Tally -> Shrunk -> OutcomeNext
data Next = Again Tally | Done OutcomeAfter a run: the tally with it, or the outcome it made.
judge
judge : String -> Config -> Int -> Tally -> Generator.Tree (Run Unit) -> Nextloop
loop : String -> Config -> Int -> Generator.Generator (Run Unit) -> Generator.Seed -> Tally -> OutcomeThe property’s runs: each from a seed of its own, at a size growing to 100.
runUnit
runUnit : String -> Int -> Co (random <+> check) Unit -> OutcomenameOf
nameOf : List String -> StringselectedBy
selectedBy : List Regex.Regex -> List String -> BoolWhether a test of these names, its groups’ and then its own, is
selected by the levels of a pattern (heff test --run): each name
matched by the level of its depth, those deeper than the pattern by
any, as Go’s -run A/B selects.
runUnder
runUnder : Int -> List Regex.Regex -> List String -> Test -> List OutcomerunTest
runTest : Int -> Test -> List OutcomeEach test, run from the seed: its unit tests and properties (--seed N gives the seed; the same seed runs the same).
runSelected
runSelected : String -> Int -> Test -> List OutcomeEach test the pattern selects (selectedBy), run from the seed: what
heff test runs in itself of each test it finds. A pattern that
is not read selects none (heff test reads it first).
integrationsUnder
integrationsUnder : List Regex.Regex -> List String -> Test -> List (String, Int)integrationsSelected
integrationsSelected : String -> Test -> List (String, Int)The integration tests the pattern selects, each name and time limit:
what heff test runs outside itself, a program each.
integrationsIn
integrationsIn : List String -> Test -> List (String, Co integrated Unit)mapRun
mapRun : {a b : Type} -> (a -> b) -> Run a -> Run bOutside
type Outside a = List String -> Co outside (Run a)A run outside: given the names of the subtest it is run for, those not yet come to (none: the test itself).
runOutside
runOutside : RawFunctorvalueOfRun
valueOfRun : {a : Type} -> Run a -> Co outside acontinueOutside
continueOutside : {a b : Type} -> (a -> Co outside (Run b)) -> Run a -> Co outside (Run b)cameTo
cameTo : {a : Type} -> List String -> a -> Run Unit -> Run asubtestRun
subtestRun : {a : Type} -> String -> Outside Unit -> a -> List String -> Co outside (Run a)A subtest come to, run for these names: the test itself names it (none left), and does no more of it; on the way to the one run for, it is done, the names after its own given it; else passed over.
checkOutside
checkOutside : {a : Type} -> CheckF Outside a -> Outside aoutsideRunner
outsideRunner : Handler integratedThe runner of an integration test: checking handled, the world outside’s operations done as they are.
integrationOutcome
integrationOutcome : String -> Run Unit -> Outcometwice
twice : List String -> Maybe StringbadName
badName : String -> BoolnamesHeld
namesHeld : Outcome -> Outcomewhat came of a test, its subtests’ names held to being names: each
once, and none with a /, which --run takes for a level’s end
subtestOutcome
subtestOutcome : String -> List String -> Run Unit -> OutcomeintegrationRun
integrationRun : String -> List String -> Maybe (String, Co integrated Unit) -> Co outside OutcomefailureJson
failureJson : Maybe Failure -> JSON.JsonoutcomeJson
outcomeJson : Outcome -> JSON.JsonAn outcome of an integration test as its program writes it, JSON: whether it held, its notes, why it failed, its golden texts, its subtests’ names, why it was skipped.
outcomeMarker
outcomeMarker : StringWhat an integration test’s program writes before its outcome, on a
line of its own, where it is run by hand (not by heff test, which
names a file for it, outcomeVariable).
outcomeVariable
outcomeVariable : StringThe environment variable by which heff test names the file an
integration test’s program writes its outcome to: a file of its
own, not its output, which what the test runs may write to too.
outcomeWritten
outcomeWritten : Maybe String -> Outcome -> Co outside UnitIntegrations
type Integrations = Co outside UnitWhat a file’s integration tests are run by: integrationMain’s.
integrationMain
integrationMain : List Test -> IntegrationsThe program heff test builds of a file’s tests: it runs the
integration test its first argument names (its subtest, the
arguments after it naming it a level each), where it is run (in the
directory heff test gives it), and writes what came of it to the
file outcomeVariable names (else to its output, after
outcomeMarker).
instanceDebugFailure
instance instanceDebugFailure : Debug FailureinstanceDebugOutcome
instance instanceDebugOutcome : Debug OutcomeAn outcome, as the record it is (for a test of tests).