Package: exactly
Tests a command line program by executing it in a temporary sandbox directory and inspecting its result.Or tests properties of existing files, directories etc.Assertions are expressed in a declarative style, using a specialized type system.Syntax is inspired by shell script, but thoroughly specialized.A test may have setup and cleanup actions.Supports individual test cases and test suites.Supports easy referencing of predefined files and files created in the temporary sandbox.Supports execution of arbitrary programs, as well as checking their result.Exactly has a  built in help system,
which can, among other things,
generate thisReference Manual.ContentsTEST CASESTesting stdin, stdout, stderr, exit codeTesting side effects on files and directoriesTesting and transforming the contents of filesUsing external programsTesting existing OS environment - tests without[act]Testing a git commit hookTesting source code filesTesting source codePrint output from the tested programReferencing filesORGANIZING TESTSFile inclusionTest suitesMORE EXAMPLESINSTALLINGDEVELOPMENT STATUSFuture developmentAUTHORTHANKSDEDICATIONTEST CASESA test case is written as a plain text file.Testing stdin, stdout, stderr, exit codeThe following checks that your newmy-contacts-programreads a contact list from stdin,
and is able to find the email of a person:[setup]

stdin = -contents-of some-test-contacts.txt

[act]

my-contacts-program get-email --name 'Pablo Gauss'

[assert]

exit-code == 0

stdout equals <<EOF
pablo@gauss.org
EOF

stderr is-emptyIf the file ‘contacts.case’ contains this test case, then Exactly can execute it:> exactly contacts.casePASS“PASS” means that all assertions were satisfied.This test assumes thatthe system under test -my-contacts-program- is is found in the same directory as the test case filethe file “some-test-contacts.txt” (that is referenced from the test case) is found in the same directory as the test case fileNoteThe directories where Exactly looks for files can be changed using thehomeandact-homeinstructions.If the actual email address of “Pablo Gauss” is not the expected one,
then Exactly willreport failure. For example:> exactly contacts.caseFAILIn [assert]
 contacts.case, line 13

   stdout equals <<EOF
   pablo@gauss.org
   EOF


 Unexpected contents of stdout from the "action to check"

   @[EXACTLY_RESULT]@/stdout(F) equalsExpectedSTRING
           'pablo@gauss.org\n'Diff--- Expected
 +++ Actual
 @@ -1 +1 @@
 -pablo@gauss.org
 +pablo.gauss@masters.orgTesting side effects on files and directoriesWhen the execution of a test case starts,
the current directory is set to a temporary directory.
This gives the test case a sandbox where it can create and manipulate files.The sandbox - and all files within it - are removed when the execution ends.The following tests a program that classifies
files as either good or bad, by moving them to the
appropriate directory:[setup]

dir output/good
dir output/bad

dir input =
{
    file a.txt = 'GOOD contents'
    file b.txt = 'bad contents'
    dir  sub   = { file c.txt = 'more bad contents' }
}

[act]

classify-files-by-moving-to-appropriate-dir GOOD input/ output/

[assert]

dir-contents input       : is-empty

dir-contents output/good : matches -full { a.txt : type file }

dir-contents output/bad  : matches -full
    {
        b.txt : type file
        sub   : type dir &&
                dir-contents matches -full
                {
                    c.txt : type file
                }
    }dircreates a directory in the current directory (by default).Testing and transforming the contents of filesUsecontentsto test the contents of a file,
or a transformed version of it,
by applying a “text transformer”.Such a “text transformer” may be given a name
using thedefinstruction
to make the test easier to read.The following case
tests that “timing lines” are output as part of a log file “log.txt”.The challenge is that the (fictive) log file contains
non-timing lines that the test is not interested in,
and that timing lines contains a time stamp of the form
“NN:NN”, who’s exact value also is not interesting.A “text transformer” is used to extract all timing lines
and to replace “NN:NN” time stamps with the constant stringTIMESTAMP:[setup]

def line-matcher     IS_TIMING_LINE     = contents matches ^timing

def text-transformer REPLACE_TIMESTAMPS = replace [0-9]{2}:[0-9]{2} TIMESTAMP

def text-transformer GET_TIMING_LINES   = filter IS_TIMING_LINE | REPLACE_TIMESTAMPS

[act]

program-that-writes-log-file

[assert]

contents log.txt :
         -transformed-by GET_TIMING_LINES
         equals <<EOF
timing TIMESTAMP begin
timing TIMESTAMP preprocessing
timing TIMESTAMP validation
timing TIMESTAMP execution
timing TIMESTAMP end
EOFThe-transformed-byoption does not modify the tested file,
it just applies the assertion to a transformed version of it.Using external programsExternal programs can help with setup, assertions etc.Exactly can run executable files, shell commands and programs in the OS PATH,
usingrun,$,%.The following case shows some examples, butdoesn’t make senseas a realistic test case, tough:[setup]

run my-setup-helper-program first "second arg"

run my-setup-helper-program arg
    -stdin 'the stdin of the program'

run -ignore-exit-code my-setup-helper-program


def list DB_ARGS = -uu -pp -hlocalhost -Dd

run % mysql @[DB_ARGS]@ --batch --execute "create table my_table(id int)"

run % mysql @[DB_ARGS]@ --batch --execute :> create table my_table(id int)

def list MYSQL_BATCH = @[DB_ARGS]@ --batch --execute

file interesting-records.txt =
     -stdout-from
       % mysql @[MYSQL_BATCH]@ :> select * from a_table where name = "interesting"


% touch file

$ ls *.txt

file root-files.txt =
     -stdout-from % ls /
       -transformed-by
         run my-text-transformer-program

file interesting-pgm-output.txt =
     -stdout-from
       -python @[EXACTLY_HOME]@/my-text-generating-program.py
       -transformed-by
         strip -trailing-new-lines

[act]

$ echo ${PATH} > output.txt

[assert]

run my-assert-helper-program

% stat root-files.txt

$ test -f root-files.txt

stdout -from $ echo 'Interesting output'
       equals "Interesting output@[NEW_LINE]@"

exit-code -from my-assert-helper-program
          ( <= 2 || > 10 )

exists output.txt : (
       type file
       &&
       run -python @[EXACTLY_HOME]@/my-file-matcher.py arg1
       &&
       contents run -python @[EXACTLY_HOME]@/my-text-matcher.py arg1 "arg 2"
       )

[cleanup]

% mysql @[MYSQL_BATCH]@ :> drop table my_tableA program executed in[assert]becomes an assertion that depends on the exit code.Program values can be defined for reuse usingdef, and referenced using@:[setup]

def program RUN_MYSQL   = % mysql -uu -pp -hlocalhost -Dd
def program EXECUTE_SQL = @ RUN_MYSQL --skip-column-names --batch --execute


run @ EXECUTE_SQL "create table my_table(id int)"

[act]

@ EXECUTE_SQL :> CALL MyStoredProcedure()

[assert]

stdout -from
       @ EXECUTE_SQL "select * from my_table"
       ! is-empty

[cleanup]

run @ EXECUTE_SQL :> drop table my_table:>treats the rest of the line as a single string.Thus:> a b cbecomes the stringa b c.Testing existing OS environment - tests without[act]A test case does not need to have an[act]phase.
This way, Exactly can be used to check existing files and directories, for example.The following case checks your hierarchy of software projects.The projects are rooted at the directory ‘my-projects’.
Each ‘project’ sub directory contains a project,
and must contain a ‘Makefile’ with a target ‘all’:[assert]

exists @[MY_PROJECTS_ROOT_DIR]@ : type dir && ALL_PROJECT_DIRS_ARE_VALID

[setup]

def path   MY_PROJECTS_ROOT_DIR = -rel-act-home my-projects
def string MY_PROJECT_DIR_NAME  = project

def file-matcher IS_VALID_MAKEFILE =

    type file &&
    contents
      -transformed-by
        filter contents matches '^all:'
        num-lines == 1


def file-matcher IS_VALID_PROJECT_DIR =

    type dir &&
    dir-contents
       matches { Makefile : IS_VALID_MAKEFILE }


def file-matcher ALL_PROJECT_DIRS_ARE_VALID =

    dir-contents -recursive
      -selection name MY_PROJECT_DIR_NAME
        every file : IS_VALID_PROJECT_DIRThe@[symbol_name]@syntax is a reference to the “symbol”symbol_name.
This syntax must be used wherever a string value is accepted,
as in@[MY_PROJECTS_ROOT_DIR]@, where a file name string is accepted.Justsymbol_namewill do in all other contexts.The@[symbol_name]@syntax can be used in any context.Testing a git commit hookThe following tests a git commit hook (prepare-commit-msg).The hook should add the issue id in the branch name,
to commit messages:[setup]


def string ISSUE_ID            = ABC-123
def string MESSAGE_WO_ISSUE_ID = "commit message without issue id"

def program GET_LOG_MESSAGE_OF_LAST_COMMIT = % git log -1 --format=%s


#### Setup a git repo with the commit hook to test

% git init

copy prepare-commit-msg .git/hooks


#### Setup a branch, with issue number in its name,
# and a file to commit

% git checkout -b @[ISSUE_ID]@-branch-with-issue-id

file file-on-branch.txt

% git add file-on-branch.txt


[act]


% git commit -m @[MESSAGE_WO_ISSUE_ID]@


[assert]


exit-code == 0

stdout -from
       @ GET_LOG_MESSAGE_OF_LAST_COMMIT
       equals
<<-
@[ISSUE_ID]@ : @[MESSAGE_WO_ISSUE_ID]@
-% ...runs a program in the OS PATH.Testing source code filesTheactorinstruction can specify an interpreter to test a source code file:[conf]

actor = file % python3

[act]

my-python-program.py 'an argument' second third

[assert]

stdout equals
<<EOF
Argument: an argument
Argument: second
Argument: third
EOFTesting source codeTheactorinstruction can specify an interpreter to test source code in[act]:[conf]

actor = source % python3

[act]

import sys
sys.stdout.write('Hello\n')
sys.stdout.write('world!\n')

[assert]

stdout equals
<<-
Hello
world!
-Print output from the tested programIf--actis used, the output of the “act” phase (the “action to check”)
will become the output ofexactly-
stdout, stderr and exit code:[setup]

dir  a-dir
file a-file

[act]

$ ls

[assert]

stdout num-lines == 314> exactly --act my-test.case
 a-dir
 a-fileThe test case is executed in a temporary sandbox, as usual,
but assertions are ignored.Referencing filesThehome directory structureis directories containing
predefined files involved in a test case:act-homeLocation of the program file being testedhomeLocation of arbitrary test resourcesBoth of them defaults to the directory
that contains the test case file,
but can be changed via[conf].Exactly does its best to prevent files in these directories from being modified.Thesandbox directory structureis temporary directories for
files involved in a single execution of a test case:actThe current directory, when execution beginsresultStores the output from the tested programtmpA directory for arbitrary temporary filesThere are options for making paths relative to all of these.-rel-homerefers to thehomedirectory,
and-rel-actto theactdirectory, for example:[conf]

act-home = ../bin/

home     = data/

[setup]

copy  -rel-home input.txt  -rel-act actual.txt

[act]

my-grep-tool "text to find" actual.txt

[assert]

contents -rel-act actual.txt :
         equals
         -contents-of -rel-home expected.txt-rel-homeinput.txtbecomes a single path argument.These “relativity” options have defaults designed to minimize the
need for them.
The following case does the same thing as the one above:[conf]

act-home = ../bin/

home     = data/

[setup]

copy input.txt actual.txt

[act]

my-grep-tool "text to find" actual.txt

[assert]

contents actual.txt :
         equals
         -contents-of expected.txtORGANIZING TESTSFile inclusionTest case contents can be included from external files:[setup]

including my-dir-symbols.def

including my-common-setup-and-cleanup.xlyTest suitesTests can be grouped in suites:first.case
second.caseor:[cases]

helloworld.case
*.case
**/*.case


[suites]

sub-suite.suite
*.suite
pkg/suite.suite
**/*.suiteIf the filemy-suite.suitecontains this text, then Exactly can run it:> exactly suite my-suite.suite
 ...OKThe result of a suite can be reported as
simple progress information,
or JUnit XML.Suites can contain test case functionality that is common
to all cases in the suite. For example:[cases]

*.case

[conf]

act-home = ../bin/

[setup]

def string CONF_FILE = my.conf

file @[CONF_FILE]@ =
<<EOF
common = configuration
EOFThe common functionality is included in each test case.MORE EXAMPLESTheexamples/directory of the source distribution contains more examples.INSTALLINGExactly is written in Python and does not require any external libraries.Exactly requires Python >= 3.6.Usepiporpip3to install:> pip3 install exactlyThe program can also be run from a source distribution:> python3 src/default-main-program-runner.pyDEVELOPMENT STATUSCurrent version is fully functional, but some syntax and semantics is inconsistent:Some instructions allow arguments to span multiple lines, some do not.Support for character escaping in strings is missing.Support for comments inside instructions is missing.Incompatible changes to syntax and semantics may occur in every 0.x release.Comments are welcome!Future developmentMore functionality is needed, smaller and larger.
Including (but by no means limited to):Improved string character escapingImproved syntax using parenthesesConcurrent execution of processesSupport for non-terminating processesWindows port (most features work, but have not been thoroughly tested)Symbol substitution in filesMore matchers, text transformers, etcLong term goalsDynamic symbol values (contents of dir, current date, e.g.)Macros and functionsEmbedding of Python code in test casesPython library for running cases and suites from within Python as a DSELAUTHOREmil Karlénemil@member.fsf.orgTHANKSThe Python IDEPyCharmfromJetBrainshas greatly helped the development of this software.Thanks for the greatPython languageGNU/LinuxGNU EmacsgitDockerRembrandt Harmenszoon van Rijn’s “De Staalmeesters”DEDICATIONAron KarlénTommy KarlssonGötabergsgatan 10, lägenhet 4