2. GNATtest User’s Guide
gnattest tool is a utility that creates unit-test skeletons
as well as a test driver infrastructure (harness). gnattest creates
a skeleton for each visible subprogram in the packages under consideration when
they do not exist already.
gnattest is a project-aware tool. A project file is mandatory for test
driver generation.
The project file package that can specify
gnattest switches is named gnattest.
The user can choose to generate a single test driver that will run all individual tests, or separate test drivers for each test. The second option allows much greater flexibility in test execution environment, allows to benefit from parallel tests execution to increase performance, and provides stubbing support.
gnattest also has a mode of operation where it acts as the test
aggregator when multiple test executables must be run, in particular when
the separate test drivers were generated. In this mode it handles individual
tests execution and upon completion reports the summary results of the test
run.
In order to process source files from a project, gnattest has to
semantically analyze the sources. Therefore, test skeletons can only be
generated for legal Ada units. If a unit is dependent on other units,
those units should be among the source files of the project or of other projects
imported by this one. Note that it is no longer necessary to specify the Ada
language version; gnattest can process Ada source code written in any
version from Ada 83 onward without specifying any language version switch.
Generated skeletons and harnesses are based on the AUnit testing framework.
AUnit is an Ada adaptation of the xxxUnit testing frameworks, similar to JUnit
for Java or CppUnit for C++. While it is advised that gnattest users read the
AUnit manual, deep knowledge of AUnit is not necessary for using gnattest.
2.1. Setting up the runtimes required by gnattest
gnattest depends on one or more runtime libraries in order to execute the
test drivers.
As mentioned earlier, the test harness generated by gnattest is based on Aunit, as such AUnit should be installed and aunit.gpr must be on the project path. If no test input generation is used, and the version of GNATDAS is kept identical to that of GNAT Pro, then the Aunit library distributed with GNAT Pro will work out of the box and no further steps are needed.
Automatic test input generation and testcase serialization for integration with GNATfuzz also depend on a test generation runtime library, which should be compiled and made available on the GPR_PROJECT_PATH environment variable.
In order to build and install both Aunit and the test generation runtime library, the following command should be run once per toolchain installation.
$ gnattest setup [options] [-gargs gprbuild-options]
where the options are:
--prefix=dirspecifies the directory in which the libraries should be installed. It defaults to the install prefix of
gnattestitself.
--compiler-prefixinstructs gnattest to install the runtime libraries in the toolchain installation directory. This removes the need for adding the installation directory to the GPR_PROJECT_PATH environment variable. It overrides
--prefix.
--target=targetSpecifies the target for which the runtime libraries should be compiled
--RTS=runtimeSpecifies the Ada runtime library name or profile to use for compiling the test runtime libraries.
--rts-profile=profileSpecifies the AUnit runtime profile to build against.
profilemust be one ofauto,full,zfp,zfp-cross,ravenscar,ravenscar-certorcert. It defaults toauto, in which case the profile is inferred from--RTSand--target.
--config=filePasses
filetogprbuildas its configuration project.
--tgen,--no-tgenForce or skip building the test generation runtime. See below for the default.
-q,-vQuiet mode, and verbose mode, which echoes the commands being run.
-gargs gprbuild-optionsPasses all the remaining arguments to
gprbuild.
The AUnit library is always built and installed. The test generation runtime is
additionally built when the Ada runtime profile in use is full,
embedded or ravenscar and the compiler supports Ada 2022, which
gnattest setup probes for; otherwise it is skipped. Pass --tgen
to build it without probing the compiler, or --no-tgen to skip it
altogether. Since test input generation needs that runtime, skipping it means
the automatic test case generation
features will not be usable.
If --compiler-prefix is not passed to the gnattest setup invocation, it is
then necessary to add <installation_dir>/share/gpr to the
GPR_PROJECT_PATH environment variable to make sure the generated test
harness can be compiled.
This step needs only needs to be executed once for a given configuration. If
any of the target,, the runtime profile, the version of GNAT Pro or the version
of GNAT DAS change, then it is necessary to re-run gnattest setup to make
sure the installed runtime libraries are up to date.
2.2. Running gnattest
There are two ways of running gnattest.
2.2.1. Framework Generation Mode
In this mode gnattest has the following command-line interface:
$ gnattest -Pprojname [ switches ] [ filename ]
where
-Pprojnamespecifies the project defining the location of source files. This switch is required. See Selecting the sources to process for the rules that determine which of the project’s sources are processed when no file name is given on the command line.
For the semantics of aggregate project processing by gnattest, see the Aggregate project handling section.
filenameis the name of the source file containing the library unit package declaration (the package “spec”) for which a test package will be created. The file name may be given with a path.
switchesis an optional sequence of switches as described below.
gnattest results can be found in two different places.
- automatic harness:
This is the harness code, which is located by default in “gnattest/harness” directory created in the object directory of the main project file. All of this code is generated completely automatically and can be destroyed and regenerated at will, with the exception of the file gnattest_common.gpr, which is created if absent, but never overwritten. It is not recommended to modify other files manually, since these modifications will be lost if
gnattestis re-run. The entry point in the harness code is the project file named test_driver.gpr. Tests can be compiled and run using a command such as:$ gprbuild -P<harness-dir>/test_driver
Note that if you need to adjust any options used to compile the harness, you can do so by editing the file gnattest_common.gpr.
- actual unit test skeletons:
A test skeleton for each visible subprogram is created in a separate file, if it doesn’t exist already. By default, those separate test files are located in a “gnattest/tests” directory that is created in the object directory of corresponding project file. For example, if a source file my_unit.ads contains a visible subprogram Proc, then the corresponding unit test will be found in file <object-dir>/gnattest/tests/my_unit-test_data-tests.adb and will be called Test_Proc_<code>. <code> is a signature encoding used to differentiate test names in case of overloading. The
--tests-dir,--subdirsand--tests-rootswitches select other locations.Note that if the project already has both my_unit.ads and my_unit-test_data.ads, this will cause a name conflict with the generated test package.
2.2.2. Test Execution Mode
In this mode gnattest has a the following command-line interface:
$ gnattest test_drivers.list [ switches ]
where
test_drivers.listis the name of the text file containing the list of executables to treat as test drivers. This file is automatically generated by gnattest, but can be hand-edited to add or remove tests. This switch is required.
switchesis an optional sequence of switches as described below.
2.3. Switches for gnattest in framework generation mode
--strictReturn a failure exit status if
gnattestfailed to process one of the argument sources, either because the source has diagnostics when it is analyzed, or because an error occurred while generating its test package, its stub or its instrumented version. Without this switch,gnattestreports such failures and skips the offending sources.--strictalso makesgnattestprint the exception name and a symbolic traceback for each of those failures, which is useful when reporting a bug.-q, --quietQuiet mode: suppresses noncritical output messages.
-v, --verboseVerbose mode: produces additional output about the execution of the tool, starting with the tool version banner. Note that this does not stop
gnattestfrom running; use--versionto print the version and exit.--versionPrint the tool version and exit.
--helpPrint a usage summary and exit.
-r, -UProcess all the sources visible from the root project, that is to say its own sources plus those of the projects it imports. This overrides the default selection based on the project’s mains; see Selecting the sources to process.
-U source_fileProcess only those source files for units in the closure of the Ada source contained in
source_file. Note that this option expects the source file name but not the Ada unit name as its parameter.--no-subprojectsProcess only source files from the root project.
-Xname=valIndicates that the external variable
namein the project has the valueval.-files=filenameTake as arguments the files listed in text file
file. Text filefilemay contain empty lines that are ignored. Each nonempty line should contain the name of an existing file. Several such switches may be specified simultaneously.--ignore=filenameDo not process the sources listed in a specified file.
--RTS=rts-nameSpecifies the name of the runtime library the harness should use. For restricted profiles,
gnattesttakes into account the run-time limitations when generating the harness.--additional-tests=projnameSources described in
projnameare considered potential additional manual tests to be added to the test suite.--harness-onlyWhen this option is given,
gnattestcreates a harness for all sources, treating them as test packages. This option is not compatible with closure computation done by -U main.--separate-drivers[=val]Generates a separate test driver for each test or unit under test, rather than a single executable incorporating all tests.
valcan be “unit” or “test”, or may be omitted, which defaults to “unit”.--separate-drivers=testis not supported on light runtime profiles.--stubGenerates the testing framework that uses subsystem stubbing to isolate the code under test.
--recursive-stubUsed along –stub, indicates gnattest to generate stubs for all the packages that are withed by the stubbed units, recursively.
--harness-dir=dirnameSpecifies the directory that will hold the harness packages and project file for the test driver. If the
dirnameis a relative path, it is considered relative to the object directory of the project file.--tests-dir=dirnameAll test packages are placed in the
dirnamedirectory. If thedirnameis a relative path, it is considered relative to the object directory of the project file. When all sources from all projects are taken recursively from all projects,dirnamedirectories are created for each project in their object directories and test packages are placed accordingly.--subdirs=dirnameTest packages are placed in a subdirectory of the corresponding source directory, with the name
dirname. Thus, each set of unit tests is located in a subdirectory of the code under test. If the sources are in separate directories, each source directory has a test subdirectory nameddirname.--tests-root=dirnameThe hierarchy of source directories, if any, is recreated in the
dirnamedirectory, with test packages placed in directories corresponding to those of the sources. If thedirnameis a relative path, it is considered relative to the object directory of the project file. When projects are considered recursively, directory hierarchies of tested sources are recreated for each project in their object directories and test packages are placed accordingly.--stubs-dir=dirnameThe hierarchy of directories containing stubbed units is recreated in the
dirnamedirectory, with stubs placed in directories corresponding to projects they are derived from. If thedirnameis a relative path, it is considered relative to the object directory of the project file. When projects are considered recursively, directory hierarchies of stubs are recreated for each project in their object directories and test packages are placed accordingly.--exclude-from-stubbing=filenameDisables stubbing of units listed in
filename. The file should contain corresponding spec files, one per line.--exclude-from-stubbing=spec:filenameSame as above, but corresponding units will not be stubbed only when testing unit whose specification is declared in specified
specfile.--include-for-stubbing=filenameEnables stubbing of units listed in
filename. The file should contain corresponding spec files, one per line.--include-for-stubbing=spec:filenameSame as above, but corresponding units will be stubbed only when testing unit whose specification is declared in specified
specfile.
Note: in case of using both include-for-stubbing and exclude-from-stubbing, local configuration will override the global configuration, e.g. if one unit is excluded by default from stubbing, and then included for a specific unit by using the {spec}:{filename} variant, then it shall be included for this specific unit.
gnattest rejects cases of using both options with the same granularity level.
--validate-type-extensionsEnables substitution check: run all tests from all parents in order to check substitutability in accordance with the Liskov substitution principle (LSP).
--inheritance-checkEnables inheritance check: run inherited tests against descendants.
--no-inheritance-checkDisables inheritance check.
--test-case-onlyGenerates test skeletons only for subprograms that have at least one associated pragma or aspect Test_Case.
--skeleton-default=valSpecifies the default behavior of generated skeletons.
valcan be either “fail” or “pass”, “fail” being the default.--passed-tests=valSpecifies whether or not passed tests should be shown.
valcan be either “show” or “hide”, “show” being the default.--exit-status=valSpecifies whether or not generated test driver should return failure exit status if at least one test fails or crashes.
valcan be either “on” or “off”, “off” being the default. If--exit-status=onis used to generate the test harness, it should also be used if running the test drivers via thegnattest test_drivers.listcommand.--omit-slocSuppresses comment line containing file name and line number of corresponding subprograms in test skeletons.
--no-command-lineDon’t add command line support to test driver. Note that regardless of this switch,
gnattestwill automatically refrain from adding command line support if it detects that the selected run-time doesn’t provide this capability.--test-durationAdds time measurements for each test in generated test driver.
--reporter=valUse the specified reporter to output the test results.
valmust be one ofgnattest(the default),text,xmlorjunit; any other value is rejected. The deprecated valuexml_deprecatedselects the legacy XML output and emits a warning; usexmlinstead.The
xmlandjunitreporters need the name of the tested subprogram in their output, so they imply--include-subp-name.This switch has no effect when combined with
--stubor--separate-drivers, andgnattestemits a warning saying so. Since the test execution mode only applies to the individual test drivers produced by those two switches, the test drivers it runs always use the default reporter.
--tests-root, --subdirs and --tests-dir switches are mutually exclusive.
2.4. Selecting the sources to process
The set of sources gnattest processes is determined as follows.
If one or more file names are given on the command line, or with one or more
-files=filename switches, only those sources are processed.
Otherwise, the sources are taken from the project, and the selection depends on whether the root project defines mains:
if the root project has at least one
Mainand all of its mains are Ada sources, only the units in the closure of those mains are processed;otherwise, all the sources visible from the root project, that is to say its own sources plus those of the projects it imports, are processed. Sources belonging to externally built projects are never processed.
Note in particular that, for a project that defines a Main, the default is
not to process every source of the project: units that the main does not
depend on are left out. Use -U to process them as well.
The following switches change this selection:
-Uprocesses all the sources visible from the root project, regardless of anyMainthe project may define.-U source_fileprocesses the closure ofsource_file. Note that this switch expects a source file name, not an Ada unit name.--no-subprojectsprocesses only the sources of the root project, leaving out those of the imported projects.--ignore=filenameremoves from the selection the sources listed infilename, whichever way the selection was made.
2.5. Switches for gnattest in test execution mode
--passed-tests=valSpecifies whether or not passed tests should be shown.
valcan be either “show” or “hide”, “show” being the default.--exit-status=valSpecifies whether or not generated test driver should return failure exit status if at least one test fails or crashes.
valcan be either “on” or “off”, “off” being the default. The switch--exit-status=onshould be used both when generating the test harness and when running the test drivers via thegnattest test_drivers.listcommand.--queues=n,-jnRuns
ntests in parallel (default is 1).--copy-environment=dirContents of
dirdirectory will be copied to temporary directories created by gnattest in which individual test drivers are spawned.--subdirs=dirnameTest driver executables from
test_drivers.listare searched indirnamesubdirectories of specified locations.
2.6. Project Attributes for gnattest
Most of the command-line options can also be passed to the tool by adding
special attributes to the project file. Those attributes should be put in
package Gnattest. Here is the list of attributes:
Tests_Rootis used to select the same output mode as with the
--tests-rootoption. This attribute cannot be used together withSubdirorTests_Dir.
Subdiris used to select the same output mode as with the
--subdirsoption. This attribute cannot be used together withTests_RootorTests_Dir.
Tests_Diris used to select the same output mode as with the
--tests-diroption. This attribute cannot be used together withSubdirorTests_Root.
Stubs_Diris used to select the same output mode as with the
--stubs-diroption.
Harness_Diris used to specify the directory in which to place harness packages and project file for the test driver, otherwise specified by
--harness-dir.
Additional_Testsis used to specify the project file, otherwise given by
--additional-testsswitch.
Skeletons_Defaultis used to specify the default behaviour of test skeletons, otherwise specified by
--skeleton-defaultoption. The value of this attribute should be eitherpassorfail.
Default_Stub_Exclusion_Listis used to specify the file with list of units whose bodies should not be stubbed, otherwise specified by
--exclude-from-stubbing=filename.
Stub_Exclusion_List ("spec")is used to specify the file with list of units whose bodies should not be stubbed when testing “spec”, otherwise specified by
--exclude-from-stubbing=spec:filename.
Each of those attributes can be overridden from the command line if needed.
Other gnattest switches can be passed via the project file using the two
following attributes, both of which take a list of switches:
Default_Switchesswitches to pass to every
gnattestinvocation on this project.
Switches ("source_file")switches to pass to
gnattestwhen it is invoked onsource_file. This attribute is only taken into account when exactly one file name is given on the command line, and, when it applies, it replacesDefault_Switchesrather than adding to it.
For instance:
project My_Project is package Gnattest is for Default_Switches use ("--passed-tests=hide", "--exit-status=on"); for Switches ("tricky_unit.ads") use ("--exit-status=on", "--omit-sloc"); end Gnattest; end My_Project;
2.7. Simple Example
Let’s take a very simple example using the first gnattest example
located in:
<install_prefix>/share/examples/gnattest/simple
This project contains a simple package containing one subprogram. By running gnattest:
$ gnattest --harness-dir=driver -Psimple.gpr
a test driver is created in directory driver. It can be compiled and run:
$ cd obj/driver $ gprbuild -Ptest_driver $ test_runner
One failed test with the diagnosis “test not implemented” is reported.
Since no special output option was specified, the test package Simple.Tests
is located in:
<install_prefix>/share/examples/gnattest/simple/obj/gnattest/tests
For each package containing visible subprograms, a child test package is
generated. It contains one test routine per tested subprogram. Each
declaration of a test subprogram has a comment specifying which tested
subprogram it corresponds to. Bodies of test routines are placed in test package
bodies and are surrounded by special comment sections. Those comment sections
should not be removed or modified in order for gnattest to be able to regenerate
test packages and keep already written tests in place.
The test routine Test_Inc_4f8b9f located at simple-test_data-tests.adb contains
a single statement: a call to procedure Assert. It has two arguments:
the Boolean expression we want to check and the diagnosis message to display if
the condition is false.
That is where actual testing code should be written after a proper setup.
An actual check can be performed by replacing the Assert call with:
Assert (Inc (1) = 2, "wrong incrementation");
After recompiling and running the test driver, one successfully passed test is reported.
2.8. Setting Up and Tearing Down the Testing Environment
Besides test routines themselves, each test package has a parent package
Test_Data that has two procedures: Set_Up and Tear_Down. This package is never
overwritten by the tool. Set_Up is called before each test routine of the
package, and Tear_Down is called after each test routine. Those two procedures
can be used to perform necessary initialization and finalization,
memory allocation, etc. Test type declared in Test_Data package is parent type
for the test type of test package and can have user-defined components whose
values can be set by Set_Up routine and used in test routines afterwards.
2.9. Regenerating Tests
Bodies of test routines and Test_Data packages are never overridden after they
have been created once. As long as the name of the subprogram, full expanded Ada
names and order of its parameters are the same, and comment sections are
intact, the old test routine will fit in its place and no test skeleton will be
generated for the subprogram.
This can be demonstrated with the previous example. By uncommenting declaration
and body of function Dec in simple.ads and simple.adb, running
gnattest on the project, and then running the test driver:
$ gnattest --harness-dir=driver -Psimple.gpr $ cd obj/driver $ gprbuild -Ptest_driver $ test_runner
The old test is not replaced with a stub, nor is it lost, but a new test
skeleton is created for function Dec.
The only way of regenerating tests skeletons is to remove the previously created tests together with corresponding comment sections.
2.10. Default Test Behavior
The generated test driver can treat unimplemented tests in two ways: either count them all as failed (this is useful to see which tests are still left to implement) or as passed (to sort out unimplemented ones from those actually failing).
The test driver accepts a switch to specify this behavior:
--skeleton-default=val, where val is either pass or fail (exactly as for
gnattest).
The default behavior of the test driver is set with the same switch
as passed to gnattest when generating the test driver.
Passing it to the driver generated on the first example:
$ test_runner --skeleton-default=pass
makes both tests pass, even the unimplemented one.
2.11. Selecting Tests to Run
By default the generated test driver runs every test it contains. It also
accepts a --routines switch to run only the tests associated with
selected subprograms, which is convenient to re-run a single test during
development (this switch is, for instance, what the GNAT Studio and VS Code
extensions use to run an individual test).
A subprogram is designated by the source location of its declaration, given as the simple name of its spec file and the line number, separated by a colon:
$ test_runner --routines=my_unit.ads:7
This runs all the tests associated with the subprogram declared at line 7 of
my_unit.ads. The selection follows the test type hierarchy, so any
inherited, overridden, or per-instance tests derived from that subprogram are
run as well.
The switch may be repeated to select several subprograms:
$ test_runner --routines=my_unit.ads:7 --routines=other.ads:10
The list of source locations may also be read from a file, one file:line
entry per line, by prefixing its name with @:
$ test_runner --routines=@selected.txt
If no subprogram corresponds to a given source location, the driver reports an
error such as no subprogram corresponds to sloc my_unit.ads:7 and aborts.
2.12. Testing Primitive Operations of Tagged Types
Creation of test skeletons for primitive operations of tagged types entails
a number of features. Test routines for all primitives of a given tagged type
are placed in a separate child package named according to the tagged type. For
example, if you have tagged type T in package P, all tests for primitives
of T will be in P.T_Test_Data.T_Tests.
Consider running gnattest on the second example (note: actual tests for this
example already exist, so there’s no need to worry if the tool reports that
no new stubs were generated):
$ cd <install_prefix>/share/examples/gnattest/tagged_rec $ gnattest --harness-dir=driver -Ptagged_rec.gpr
Taking a closer look at the test type declared in the test package Speed1.Controller_Test_Data is necessary. It is declared in:
<install_prefix>/share/examples/gnattest/tagged_rec/obj/gnattest/tests
Test types are direct or indirect descendants of AUnit.Test_Fixtures.Test_Fixture type. In the case of non-primitive tested subprograms, the user doesn’t need to be concerned with them. However, when generating test packages for primitive operations, there are some things the user needs to know.
Type Test_Controller has components that allow assignment of various
derivations of type Controller. And if you look at the specification of
package Speed2.Auto_Controller, you will see that Test_Auto_Controller
actually derives from Test_Controller rather than AUnit type Test_Fixture.
Thus, test types mirror the hierarchy of tested types.
The Set_Up procedure of Test_Data package corresponding to a test package
of primitive operations of type T assigns to Fixture a reference to an
object of that exact type T. Note, however, that if the tagged type has
discriminants, the Set_Up only has a commented template for setting
up the fixture, since filling the discriminant with actual value is up
to the user.
The knowledge of the structure of test types allows additional testing without additional effort. Those possibilities are described below.
2.13. Testing Inheritance
Since the test type hierarchy mimics the hierarchy of tested types, the inheritance of tests takes place. An example of such inheritance can be seen by running the test driver generated for the second example. As previously mentioned, actual tests are already written for this example.
$ cd obj/driver $ gprbuild -Ptest_driver $ test_runner
There are 6 passed tests while there are only 5 testable subprograms. The test routine for function Speed has been inherited and run against objects of the derived type.
2.14. Tagged Type Substitutability Testing
Tagged Type Substitutability Testing is a way of verifying the global type
consistency by testing. Global type consistency is a principle stating that if
S is a subtype of T (in Ada, S is a derived type of tagged type T),
then objects of type T may be replaced with objects of type S (that is,
objects of type S may be substituted for objects of type T), without
altering any of the desirable properties of the program. When the properties
of the program are expressed in the form of subprogram preconditions and
postconditions (let’s call them pre and post), the principle is formulated as
relations between the pre and post of primitive operations and the pre and post
of their derived operations. The pre of a derived operation should not be
stronger than the original pre, and the post of the derived operation should
not be weaker than the original post. Those relations ensure that verifying if
a dispatching call is safe can be done just by using the pre and post of the
root operation.
Verifying global type consistency by testing consists of running all the unit tests associated with the primitives of a given tagged type with objects of its derived types.
In the example used in the previous section, there was clearly a violation of
type consistency. The overriding primitive Adjust_Speed in package Speed2
removes the functionality of the overridden primitive and thus doesn’t respect
the consistency principle.
gnattest has a special option to run overridden parent tests against objects
of the type which have overriding primitives:
$ gnattest --harness-dir=driver --validate-type-extensions -Ptagged_rec.gpr $ cd obj/driver $ gprbuild -Ptest_driver $ test_runner
While all the tests pass by themselves, the parent test for Adjust_Speed fails
against objects of the derived type.
Non-overridden tests are already inherited for derived test types, so the
--validate-type-extensions enables the application of overridden tests
to objects of derived types.
2.15. Testing with Contracts
gnattest supports pragmas Pre, Post, and Test_Case,
as well as the corresponding Ada 2012 aspects.
Test routines are generated, one per each Test_Case associated with a tested
subprogram. Those test routines have special wrappers for tested functions
that have composition of pre- and postcondition of the subprogram with
“requires” and “ensures” of the Test_Case (depending on the mode, pre and post
either count for Nominal mode or do not count for Robustness mode).
The third example demonstrates how this works:
$ cd <install_prefix>/share/examples/gnattest/contracts $ gnattest --harness-dir=driver -Pcontracts.gpr
Putting actual checks within the range of the contract does not cause any error reports. For example, for the test routine which corresponds to test case 1:
Assert (Sqrt (9.0) = 3.0, "wrong sqrt");
and for the test routine corresponding to test case 2:
Assert (Sqrt (-5.0) = -1.0, "wrong error indication");
are acceptable:
$ cd obj/driver $ gprbuild -Ptest_driver $ test_runner
However, by changing 9.0 to 25.0 and 3.0 to 5.0, for example, you can get a precondition violation for test case one. Also, by using any otherwise correct but positive pair of numbers in the second test routine, you can also get a precondition violation. Postconditions are checked and reported the same way.
2.16. Additional Tests
gnattest can add user-written tests to the main suite of the test
driver. gnattest traverses the given packages and searches for test
routines. All procedures with a single in out parameter of a type which is
derived from AUnit.Test_Fixtures.Test_Fixture and that are declared in package
specifications are added to the suites and are then executed by the test driver.
(Set_Up and Tear_Down are filtered out.)
An example illustrates two ways of creating test harnesses for user-written
tests. Directory additional_tests contains an AUnit-based test driver written
by hand.
<install_prefix>/share/examples/gnattest/additional_tests/
To create a test driver for already-written tests, use the --harness-only
option:
gnattest -Padditional/harness/harness.gpr --harness-dir=harness_only \\ --harness-only gprbuild -Pharness_only/test_driver.gpr harness_only/test_runner
Additional tests can also be executed together with generated tests:
gnattest -Psimple.gpr --additional-tests=additional/harness/harness.gpr \\ --harness-dir=mixing gprbuild -Pmixing/test_driver.gpr mixing/test_runner
2.17. Individual Test Drivers
By default, gnattest generates a monolithic test driver that
aggregates the individual tests into a single executable. It is also possible
to generate separate executables for each test or each unit under test, by
passing the switch --separate-drivers[={val}]. The parameter val
selects the granularity: unit (the default, also used when val is
omitted) generates one test driver per unit under test, while test
generates one test driver per individual test. This
approach scales better for large testing campaigns, especially involving target
architectures with limited resources typical for embedded development. It can
also provide a major performance benefit on multi-core systems by allowing
simultaneous execution of multiple tests.
gnattest can take charge of executing the individual tests; for this,
instead of passing a project file, a text file containing the list of
executables can be passed. Such a file is automatically generated by gnattest
under the name test_drivers.list, but it can be
hand-edited to add or remove tests, or replaced. The individual tests can
also be executed standalone, or from any user-defined scripted framework.
2.18. Stubbing
Depending on the testing campaign, it is sometimes necessary to isolate the part of the algorithm under test from its dependencies. This is accomplished via stubbing, i.e. replacing the subprograms that are called from the subprogram under test by stand-in subprograms that match the profiles of the original ones, but simply return predetermined values required by the test scenario.
This mode of test harness generation is activated by the switch --stub.
The implementation approach chosen by gnattest is as follows.
For each package under consideration all the packages it is directly depending
on are stubbed, excluding the generic packages and package instantiations.
The stubs are shared for each package under test. The specs of packages to stub
remain intact, while their bodies are replaced, and hide the original bodies by
means of extending projects. Also, for each stubbed
package, a child package with setter routines for each subprogram declaration
is created. These setters are meant to be used to set the behavior of
stubbed subprograms from within test cases.
Note that subprograms belonging to the same package as the subprogram under test are not stubbed. This guarantees that the sources being tested are exactly the sources used for production, which is an important property for establishing the traceability between the testing campaign and production code.
Due to the nature of stubbing process, this mode implies the switch
--separate-drivers, i.e. an individual test driver (with the
corresponding hierarchy of extending projects) is generated for each unit under
test.
Note
Developing a stubs-based testing campaign requires
good understanding of the infrastructure created by gnattest for
this purpose. We recommend following the two stubbing tutorials
simple_stubbing and advanced_stubbing provided
under <install_prefix>/share/examples/gnattest before
attempting to use this powerful feature.
2.19. Integration with GNATcoverage
In addition to the harness, gnattest generates a Makefile. This Makefile
provides targets for building the test drivers and also the targets for
computing the coverage information using GNATcoverage framework when this
coverage analysis tool is available.
The target coverage fully automates the process using source traces:
it instruments the test driver projects with gnatcov instrument, builds
them against the instrumented sources, runs them, turns each resulting source
trace into a checkpoint and finally consolidates the checkpoints into a
report:
make coverage
The Makefile also provides the following targets:
allbuilds all the test drivers, without any coverage instrumentation.
instrument-allruns
gnatcov instrumenton all the test driver projects, without building them.
instr-build-allsame, and builds the instrumented drivers.
cleanruns
gprcleanon every test driver project and removes the trace files.
The switches passed to the various gnatcov commands are held in
coverage_settings.mk, which is generated once and then owned by the user;
see Structure of the Generated Harness.
For more details about using GNATtest with GNATcoverage see Using GNATtest with GNATcoverage.
2.20. Structure of the Generated Harness
gnattest generates a number of artifacts. Understanding what each one is
for, and in particular which ones are regenerated on every run versus
created once and then owned by the user, is essential both for customizing the
harness and for deciding what to put under version control.
The artifacts fall into two broad families: the harness (the test driver
infrastructure) and the test code (the skeletons you fill in). With the
default object-directory locations and a single test driver, the layout looks
like this, where <u> stands for the name of a unit under test, <U> for
its Ada unit name and <prj> for the name of the project:
<object-dir>/gnattest/
harness/ <- test driver infrastructure
test_driver.gpr regenerated project to build/run the driver
test_<prj>.gpr regenerated project compiling the test skeletons
gnattest_common.gpr created once shared build options (user-owned)
test_runner.adb regenerated driver main
gnattest_main_suite.ad[bs] regenerated top-level AUnit suite
<u>-test_data-tests-suite.ad[bs] regenerated per-unit AUnit suite
common/
gnattest_generated.ads regenerated support unit shared by the drivers
gnattest_generated-persistent.ad[bs]
created once setup/teardown shared by the drivers (user owned)
gnattest.xml regenerated test/source mapping file
suppress.adc, created once global configuration pragmas (user owned)
suppress_no_ghost.adc
preprocessor.def regenerated preprocessor symbol definitions
.gnattest-config.json regenerated internal harness configuration
Makefile regenerated GNATcoverage integration driver
coverage_settings.mk created once gnatcov switches (user-owned)
tests/ <- test code
<u>-test_data.ad[bs] owned Set_Up / Tear_Down, fixture type
<u>-test_data-tests.ads regenerated test package spec
<u>-test_data-tests.adb owned your test routine bodies
With --separate-drivers (and hence with --stub), the per-driver files
move into one subdirectory per unit, or per test, under test, and two more
files appear at the top of the harness directory:
<object-dir>/gnattest/
harness/
<U>.Test_Data.Tests/ <- one such directory per driver
test_driver.gpr regenerated project to build/run this driver
<u>-test_data-tests-suite-test_runner.adb
regenerated this driver's main
<u>-test_data-tests-suite.ad[bs]
regenerated this driver's AUnit suite
units.list regenerated unit under test, for gnatcov
test_drivers.gpr regenerated aggregate project building them all
test_drivers.list regenerated list of driver executables
... the files listed above, except test_driver.gpr,
test_runner.adb and the suite units
Only the files described in the next section have a stable interest for the user; the others are internal to the harness, regenerated on every run, and should not be edited.
2.20.1. Responsibilities of the harness components
- test_driver.gpr
The entry point of the harness: the project file used to build and run the test driver (
gprbuild -P<harness-dir>/test_driver). It is regenerated on every run. With--separate-drivers, one such project is generated per unit (or per test) under test, each in its own subdirectory.
- gnattest_common.gpr
A project file, imported by every generated test driver project, that holds build options shared across the harness (compiler and linker switches, extra source directories, global configuration pragmas, etc.). It is created once if absent and never overwritten, so it is the intended place for the user to customize how the harness is compiled. See for instance Instrumenting test harnesses for a SPARK project for using it to pass a configuration pragma file for SPARK code.
- test_runner and the suite units
The generated main (
test_runner.adb, or<u>-test_data-tests-suite-test_runner.adbwith separate drivers) and the AUnit suite-aggregation packages (gnattest_main_suite.ad[bs]and<u>-test_data-tests-suite.ad[bs]). They are fully automatic, regenerated on every run, and should not be edited.
- test_drivers.gpr
An aggregate project, generated with
--separate-drivers, that builds all the individual test driver projects at once. Regenerated on every run.
- gnattest.xml
The mapping between the sources under test and the generated test artifacts, used by IDEs to navigate between a subprogram and its tests. Regenerated on every run.
- Makefile
Automates the production of a coverage report with GNATcoverage (see Integration with GNATcoverage). It is regenerated on every invocation of
gnattestand should not be edited.
- coverage_settings.mk
A secondary makefile, included by
Makefile, holding the switches passed to the variousgnatcovcommands. Its values are extracted from the root project when first generated, but unlikeMakefileit is created once and not regenerated, so it is the intended place to adjust GNATcoverage settings that are not expressed in the project file. See Using the Makefile generated by GNATtest for details.
- units.list
Generated alongside each
test_driver.gpr, and therefore only with separate drivers; lists the unit under test so thatgnatcovcan be told which unit is of interest and avoid incidental coverage. Regenerated on every run.
- test_drivers.list
The list of test driver executables consumed by the test execution mode. Like
units.listit is only generated with separate drivers, since that mode is what the test execution mode runs. It is generated automatically but may be hand-edited to add or remove tests; it is also safe to letgnattestregenerate it.
2.20.2. Files the user is expected to modify
Most of the harness is disposable, but two harness files are deliberately created once and never regenerated so that the user can adapt the harness to the project’s needs:
gnattest_common.gpr— to customize how the harness is built.coverage_settings.mk— to customize thegnatcovswitches used by the generatedMakefile(only relevant when using the GNATcoverage integration).
Because they are preserved across regenerations and hold project-specific settings, both of these files are good candidates for version control (see Putting Tests under Version Control).
The following table summarizes, for each generated artifact, whether it is regenerated, whether it is meant to be edited, and whether it should be put under version control:
File |
Regenerated? |
User-editable? |
Version control? |
|---|---|---|---|
|
Yes |
No |
No |
|
No (created once) |
Yes |
Yes |
|
No (created once) |
Yes |
Yes |
|
Yes |
No |
No |
|
Yes |
No |
No |
|
Yes |
No |
No |
|
Yes |
No |
No |
|
No (created once) |
Yes |
Yes (with GNATcov integration) |
|
Yes |
No |
No |
|
Yes |
Yes (optional) |
Optional |
|
No (preserved) |
Yes |
Yes |
|
No (preserved) |
Yes |
Yes |
|
No (preserved) |
Yes |
Yes |
|
Yes |
No |
No |
stub / stub-data bodies ( |
No (preserved) |
Yes |
Yes |
2.21. Putting Tests under Version Control
As has been stated earlier, gnattest generates two different types
of code, test skeletons and harness. With the exception of the two user-owned
files noted below, the harness is generated completely automatically each time,
does not require manual changes and therefore should not be put under version
control.
It makes sense to put under version control the test data packages, both their
specs and their bodies, and the bodies of the test packages. Note that the test
package specs (*-test_data-tests.ads) are the only part of the test code
that is regenerated on every run, and should not be put under version control.
Test data package specs (*-test_data.ads) are created once and never
overwritten afterwards, except for the sections surrounded by read only
markers. They are meant to be edited, since this is where the components of the
test fixture type are declared (see
Setting Up and Tearing Down the Testing Environment), so they belong
under version control together with their bodies.
Additionally, if stubbing is enabled with --stub, it also makes sense to
put the stubbed bodies, as well as the stub-data bodies under source control,
as gnattest will preserve modifications made to these files.
Two files located in the harness directory are an exception to the rule above: they are created once and never overwritten, are meant to be adapted by the user, and should therefore be put under version control:
gnattest_common.gpr, which holds the build options shared by the harness projects;coverage_settings.mk, which holds thegnatcovswitches used by the generatedMakefile, when the GNATcoverage integration is used.
See Structure of the Generated Harness for a description of all the generated artifacts and a summary of which ones should be put under version control.
Option --omit-sloc may be useful when putting test packages under version control.
2.22. Aggregate project handling
If the project passed to gnattest with the -P switch is an aggregate
project, the aggregated projects will be processed sequentially and
independently. This will result in one harness directory being generated by
default, in the object directories of each of the aggregated projects.
gnattest will not generate any project file or makefile to automate the
build of the harnesses of each of the aggregated project.
2.22.1. Artifact directories and aggregate projects
By default, all artifacts generated by gnattest are located in
subdirectories of the object directory of each of the aggregated projects. This
in particular means that tests or stubs for a common dependency of two
aggregated projects will be duplicated. In order to avoid this, options such as
--stubs-dir., --tests-dir or --subdirs can be used, with relative
paths so that the artifacts for the common dependencies are generated in the
same location, and re-used across each test harness.
On the contrary, using --harness-dir or --tests-dir with an absolute
path will result in the harness and/or files of a first aggregated project being
overwritten by the generation of the test harness for subsequent aggregated
projects, and should thus be avoided.
2.23. Current Limitations
The tool currently has the following limitations:
generic tests for nested generic packages and their instantiations are not supported;
tests for protected subprograms and entries are not supported;
pragma
No_Run_Timeis not supported;pragma
No_Secondary_Stackis not supported;if pragmas for interfacing with foreign languages are used, manual adjustments might be necessary to make the test harness compilable;
use of some constructs, such as elaboration-control pragmas, Type_Invariant aspects, and complex variable initializations that use Subprogram’Access, may result in elaboration circularities in the generated harness;
heavy usage of preprocessor that affects constructs like subprogram profiles or tagged type hierarchies may result in improper test driver generation.
2.24. Automatically generating test cases (experimental)
Please note that all the features described bellow are experimental, and the interface is subject to change.
GNATtest has the capability to generate test inputs for subprograms under test. This test generation feature is also usable in conjunction with GNATfuzz, in order to use GNATtest harnesses (generated or manually written) as a starting corpus for a fuzzing session, and to integrate inputs of interest found by GNATfuzz back into the test harness. For more details, see section Using GNATtest with GNATfuzz (experimental).
Test input generation and execution is supported on native platforms, as well as cross bareboard targets provided that the Ada runtime is of an embedded profile. Test input generation for target with only light or light tasking profiles is not supported.
2.24.1. Setting up the test generation runtime
Generation of values for Ada cannot be fully done statically, as the bounds of
some types may only be defined at runtime. As such, the test generation feature
requires the compilation and installation of a runtime project. This can be
done using the gnattest setup command, see Setting up the runtimes required by gnattest for more
information.
2.24.2. Generating test inputs
Note
Test generation is only supported for projects built as static libraries or
as standalone executables. It is not supported when the project under
test is a shared library, i.e. when its Library_Kind attribute is set to
relocatable. The test generation runtime and the support library
generated for the project are only ever provided as static libraries, so a
relocatable library cannot be linked against them, and building the harness
will fail at link time. To use test generation on such a project, build it as
a static library (Library_Kind set to static, or static-pic if
position-independent code is required).
gnattest provides a --gen-test-vectors switch that can be used to
automatically generate test cases for all of the supported subprogram profiles.
The number of generated test cases can be configured through the
--gen-test-num switch.
As mentioned in section Setting up the test generation runtime, test input generation requires executing code to determine some of the characteristics of the types at hand. This means that for both native and cross targets, a GNAT Pro toolchain for the corresponding target must be available in the environment.
For cross targets, the test input generation harness will be executed through
GNATemulator. GNATtest may thus compile it against a different runtime than the
one specified through the --RTS switch, to use one corresponding to a board
supported by GNATemulator for the given target. This should have no impact on
the definition of the various types, as only the target CPU helps define the
size of predefined types. The mapping between targets and the runtime used for
test input generation is defined under
<gnatdas_install_dir>/share/tgen/tgen_target_runtimes.json, users may
copy and modify it, and feed the modified mapping to GNATtest using the
--tgen-target-config=FILE command line option.
2.24.2.1. Supported types
gnattest can natively generate test cases for a subprogram when it can
generate a value for every one of its parameters, and for the global inputs
listed in its Global aspect, if it has one. Note that this applies to
parameters of every mode, out parameters included: gnattest needs a
representation of the type in order to marshall the value back, so an
unsupported type disables generation for the subprogram whichever mode it
appears in.
A type is not natively supported when it is, or has a subcomponent that is, one of the following:
an access type, including a subprogram access type;
a class-wide type, or a type derived from an abstract type;
an anonymous array or anonymous access type;
a private type declared in a nested package;
a type derived from a private type that is unconstrained;
a generic formal type;
a type declared in a generic package instantiation that is a library item;
a task type or a protected type;
System.Address;an array type whose number of elements exceeds the configured array size limit, which defaults to 1000 and can be changed with the
TGEN_ARRAY_LIMITenvironment variable.
Note that concrete tagged types and limited types are supported.
gnattest reports the reason why it cannot generate values for a given
parameter, so the diagnostics it prints are the authoritative answer for a
given subprogram. For instance:
pkg.print_acc.A: pkg.int_acc is not supported (Access types are not supported)
For types not respecting the above restrictions, it is possible to generate test inputs using a proxy generation function.
2.24.2.2. Test input generation strategies
Input value generation currently follows a simple strategy for each input
parameter of the subprogram under test. Parameters of scalar types, and scalar
components of composite types have their values uniformly generated. For
unconstrained array types, a total number of elements is randomly chosen
between 0 and 10 for each dimension, with thus an upper limit of 10 ** n
elements, where n is the number of dimensions of the array (so between 0
and 10 elements for the common case of a one-dimensional array), then, for each
dimension, the low bound is randomly chosen and the high bound computed
accordingly to those two first points.
Independently of this, the marshallers refuse to read back an array with more
than 1000 elements per dimension, to avoid allocating overly large arrays on
the stack. This limit can be changed through the TGEN_ARRAY_LIMIT
environment variable; array types whose number of elements is statically known
to exceed this value (all dimensions combined) are reported as unsupported.
For record discriminants, different strategies are chosen depending on the use of the discriminant within the record: If the discriminant constraints a array component, then the array strategy described above is used. If the discriminant is used in a variant part, generation will be biased in order to generated all possible shapes of the record (i.e. explore all variants). Otherwise, these are generated as any other scalar component.
The generated test cases are then stored in a ad-hoc (and yet to be specified)
JSON format, in files under the <obj_dir>/gnattest/tests/JSON_Tests directory.
The generated JSON files are preserved through a gnattest rerun. The user is
thus free to modify them, to e.g. fill in expected return values, though
backward compatibility of the format is not guaranteed at this stage.
gnattest also generates Ada files to actually execute the test cases. Each test vector
has its own AUnit test case, and all test cases for a specific subprogram are all
stored in a dedicated file, namely
<unit_name>-test_data-test_<subp_name>_<subp_hash>.ad[bs], where
<unit_name> is the name of the unit in which the subprogram is declared,
<subp_name> is the name of the subprogram, and <subp_hash> is a hash based
on the profile of the subprogram, in order to differentiate overloads.
The content of these files are re-generated each time gnattest is invoked,
independently of the presence of the --gen-test-vectors switch on the
command line. It is thus not necessary to re-invoke gnattest with that
switch more than once, unless the goal is to generate additional test inputs.
2.24.3. Test input generation through a proxy subprogram
It is possible to assign to each type, even those not natively supported, a
proxy function, which GNATtest will use every time it need to generate a
value for the return type of the proxy function. For a given type T, a
subprogram is eligible to be the proxy of T if it meets the following
conditions:
The proxy must be a function whose return type is
T;The proxy must have no out parameters;
The proxy must have at least one
inorin outparameters, and all the input parameters should either be natively supported or have a proxy subprogram themselves;The proxy subprogram must be visible from the package in which
Tis declared.
A proxy subprogram can be designated explicitly, by using the TGen_Proxy =>
<Proxy Name> aspect on the type definition, or gnattest can automatically
identify one for types that are not natively supported.
The --detect-tgen-proxies={policy} switch controls how far GNATtest looks
when automatically identifying a proxy for a type that is not natively
supported. policy must be one of:
noneno automatic search is performed at all. Only the proxies designated explicitly with the
TGen_Proxyaspect are used.
unit(the default)only the unit in which the target type is declared is searched.
all_refsthe unit in which the target type is declared is searched first; if that yields nothing, GNATtest then inspects the references to the target type across the units of the project to find a suitable proxy function.
Note that this switch has no effect on proxies designated explicitly with the
TGen_Proxy aspect, which are always honored.
A proxy cannot be used for every unsupported type. Types for which no type-compatible helper subprogram can be declared, namely anonymous array and access types, generic formal types, private types declared in a nested package, and types declared in a library-level generic package instantiation, are rejected regardless of any proxy.
Once a proxy is defined for a given type, GNATtest will generate test inputs for the proxy. These test inputs are written to the serialized test files. When executing the tests or loading the test inputs in GNATfuzz, the input values for the proxy are de-serialized, then the proxy subprogram called to obtain a value for its return type. In particular, this means that it is not possible to serialize Ada values for a type with a proxy, and thus test input dumping from pre-existing tests is not supported for proxy annotated types.
For example, given the following Ada package:
package Pkg is
type Int_Acc is access all Integer with
TGen_Proxy => Make_Access;
function Make_Access (Val : Integer) return Int_Acc;
procedure Print_Int_Acc (Acc : Int_Acc);
end Pkg;
When generating test inputs for Print_Int_Acc, GNATtest will first generate
integer values, and invoke Make_Access with those values as parameters when
executing the test.