Code coverage utility for Common Lisp
Richard C. Waters
;This is the September 15, 1991 version of ;the on-line documentation for Richard Waters' test case coverage checker. The COVER system: Determining the Coverage of a Test Suite Richard C. Waters MIT AI Laboratory 545 Technology Sq. Cambridge MA 02139 Dick@AI.MIT.EDU Mitsubishi Electric Research Laboratories 201 Broadway Cambridge MA 02139 Dick@MERL.COM The value of a suite of test cases depends critically on its coverage. Ideally a suite should test every facet of the specification for a program and every facet of the algorithms used to implement the specification. Unfortunately, there is no practical way to be sure that complete coverage has been achieved. However, something should be done to assess the coverage of a test suite, because a test suite with poor coverage has little value. A traditional approximate method of assessing the coverage of a test suite is to check that every condition tested by the program is exercised. For every predicate in the program, there should be at least one test case that causes the predicate to be true and one that causes it to be false. Consider the function MY* below which uses a convoluted algorithm to compute the product of two numbers. (DEFUN MY* (X Y) (LET ((SIGN 1)) (WHEN (MINUSP X) (SETQ SIGN (- SIGN)) (SETQ X (- X))) (WHEN (MINUSP Y) (SETQ SIGN (- SIGN)) (SETQ Y (- X))) (* SIGN X Y))) The function my* contains two predicates, (MINUSP X) and (MINUSP Y), which lead to four conditions: X is negative, X is not negative, Y is negative, and Y is not negative. To be at all thorough, a test suite must contain tests exercising all four of these conditions. For instance, any test suite that fails to exercise the condition where Y is negative will fail to detect the bug in the next to last line of the function. (As an example of the fact that covering all the conditions in a program does not guarantee that every facet of either the algorithm or the specification will be covered, consider the fact that the two test cases (MY* 2.1 3) and (MY* -1/2 -1/2) cover all four conditions. However, they do not detect the bug on the next to last line and they do not detect the fact that MY* fails to work on complex numbers.) The COVER system determines which conditions tested by a program are exercised by a given test suite. This is no substitute for thinking hard about the coverage of the test suite. However, it provides a useful starting point and can indicate some areas where additional test cases should be devised. USER'S MANUAL FOR COVER The functions, macros, and variables that make up the COVER system are in a package called "COVER". The six exported symbols are documented below. COVER:ANNOTATE t-or-nil Evaluating (COVER:ANNOTATE T) triggers the processing of function and macro definitions by the COVER system. Each subsequent instance of DEFUN or DEFMACRO is altered by adding annotation that maintains information about the various conditions tested in the body. Evaluating (COVER:ANNOTATE NIL) stops the special processing of function and macro definitions. Subsequent definitions are not annotated. However, if a function or macro that is currently annotated is redefined, the new definition is annotated as well. The macro COVER:ANNOTATE should only be used as a top-level form. When annotation is triggered, a warning message is printed, and T is returned. Otherwise, NIL is returned. (COVER:ANNOTATE T) => t ; after printing: ;;; Warning: Coverage annotation applied. COVER:FORGET-ALL This function, which always returns t, has the effect of removing all coverage annotation from every function and macro. It is appropriate to do this before completely recompiling the system being tested or before switching to a different system to be tested. COVER:RESET Each condition tested by an annotated function and macro is associated with a flag that trips when the condition is exercised. The function COVER:RESET resets all these flags, and returns T. It is appropriate to do this before rerunning a test suite to reevaluate its coverage. COVER:REPORT &KEY FN OUT ALL This function displays the information maintained by COVER, returning no values. FN must be the name of an annotated function or macro. If FN is specified, a report is printed showing information about that function or macro only. Otherwise, reports are printed about every annotated function and macro. OUT, which defaults to *STANDARD-OUTPUT*, must either be an output stream or the name of a file. It specifies where the reports should be printed. If ALL, which defaults to NIL, is non-null then the reports printed contain information about every condition. Otherwise, the reports are abbreviated to highlight key conditions that have not been exercised. COVER:*LINE-LIMIT* default value 75 The output produced by COVER:REPORT is truncated to ensure that it is no wider than COVER:*LINE-LIMIT*. AN EXAMPLE Suppose that the function MY* has been annotated and that no other functions or macros have been annotated. The following example illustrates the operation of COVER and the reports printed by COVER:REPORT. (setq cover:*line-limit* 43) => 43 (cover:reset) => T (cover:report) => ; after printing: ;- :REACH (DEFUN MY* (X Y)) <1> (my* 2 2) => 4 (cover:report) => ; after printing: ;+ :REACH (DEFUN MY* (X Y)) <1> ; + :REACH (WHEN (MINUSP X) (SETQ S <2> ; - :NON-NULL (MINUSP X) <4> ; + :REACH (WHEN (MINUSP Y) (SETQ S <6> ; - :NON-NULL (MINUSP Y) <8> (my* -2 2) => -4 (cover:report) => ; after printing: ;+ :REACH (DEFUN MY* (X Y)) <1> ; + :REACH (WHEN (MINUSP Y) (SETQ S <6> ; - :NON-NULL (MINUSP Y) <8> (cover:report :all t) => ; after printing: ;+ :REACH (DEFUN MY* (X Y)) <1> ; + :REACH (WHEN (MINUSP X) (SETQ S <2> ; + :NON-NULL (MINUSP X) <4> ; + :NULL (MINUSP X) <5> ; + :REACH (WHEN (MINUSP Y) (SETQ S <6> ; - :NON-NULL (MINUSP Y) <8> ; + :NULL (MINUSP Y) <9> Each line in a report contains three pieces of information about a point in a definition: +/- specifying that the point either has (+) or has not (-) been exercised, a message indicating the physical and logical placement of the point in the definition, and in angle brackets < >, an integer that is a unique identifier for the point. Indentation is used to indicate that some points are subordinate to others in the sense that the subordinate points cannot be exercised without also exercising their superiors. The order of the lines of the report is the same as the order of the points in the definition. Each message contains a label (e.g., :REACH, :NULL) and a piece of code. There is a point labeled :REACH corresponding to each definition as a whole and each conditional form within each definition. Subordinate points corresponding to the conditions a conditional form tests are grouped under the point corresponding to the form. As discussed in detail in the next subsection, the messages for the subordinate points describe the situations in which the conditions are exercised. Lines that would otherwise be too long to fit on one line have their messages truncated (e.g., points <2> and <6>). The first three reports are abbreviated based on two principles. First, if a point P and all of its subordinates have been exercised, then P and all of its subordinates are omitted from the report. This is done to focus the user's attention on the points that have not been exercised. Second, if a point P has not been exercised, then all of the points subordinate to it are omitted from the report. This reflects the fact that it is not possible for any of these subordinate points to have been exercised and one cannot devise a test case that exercises any of the subordinate points without first figuring out how to exercise P. An additional complicating factor is that COVER operates in an incremental fashion and does not, in general, have full information about the subordinates of points that have not been exercised. As a result, it is not always possible to present a complete report. However, one can have total confidence that if the report says that every point has been exercised, this statement is based on complete information. The first report in shows that none of the points within MY* has been exercised. The second report displays most of the points in MY*, to set the context for the two points that have not been exercised. The third report omits <2> and its subordinates, since they have all been exercised. The fourth report shows a complete report corresponding to the third abbreviated report. COVER:FORGET &rest ids This function gives the user greater control over the reports produced by COVER:REPORT. Each ID must be an integer identifying a point. All information about the specified points (and their subordinates) is forgotten. From the point of view of COVER:REPORT, the effect is as if the points never existed. (A forgotten point can be retrieved by reevaluating or recompiling the function or macro definition containing it.) The example below, which follows on after the end of the example above, shows the action of COVER:FORGET. (cover:forget 6) => T (cover:report :all t) => ; after printing: ;+ :REACH (DEFUN MY* (X Y)) <1> ; + :REACH (WHEN (MINUSP X) (SETQ S <2> ; + :NON-NULL (MINUSP X) <4> ; + :NULL (MINUSP X) <5> (cover:report) => after printing ;All points exercised. The abbreviated report above does not describe any points, because every point in MY* that has not been forgotten has been exercised. It is appropriate to forget a point if there is some reason that no test case can possibly exercise the point. However, it is much better to write your code so that every condition can be tested. (Point numbers are assigned based on the order in which points are entered into COVER's database. In general, whenever a definition is reevaluated or recompiled, the numbers of the points within it change.) THE WAY CONDITIONALS ARE ANNOTATED <If the follwing is written in a file> (in-package "USER") (require "COVER" ...) (defmacro maybe+ (x y) `(if (numberp ,x) (+ ,x ,y))) (cover:annotate t) (defmacro maybe- (x y) `(if (numberp ,x) (- ,x ,y))) (defun g (x y) (cond ((and (null x) y) y) (y (case y (1 (maybe- x y)) (2 (maybe+ x y)))))) (cover:annotate nil) (defun h (x y) ...) (cover:reset) (run-tests) (cover:report :out "report" :all t) <loading the file produces the following output in the file "report"> ;+ :REACH (DEFMACRO MAYBE- (X Y)) <1> ;+ :REACH (DEFUN G (X Y)) <2> ; + :REACH (COND ((AND # Y) Y) (Y ( <3> ; + :REACH (AND (NULL X) Y) <9> ; + :FIRST-NULL (NULL X) <11> ; + :EVAL-ALL Y <12> ; + :FIRST-NON-NULL (AND (NULL X) <5> ; + :FIRST-NON-NULL Y <7> ; + :REACH (CASE Y (1 (MAYBE- X Y <13> ; + :SELECT 1 <15> ; + :REACH (IF (NUMBERP X) (- X <18> ; + :NON-NULL (NUMBERP X) <20> ; + :NULL (NUMBERP X) <21> ; + :SELECT 2 <16> ; - :SELECT-NONE <17> ; + :ALL-NULL <8> The above shows a file that makes use of COVER and the kind of report that might be produced by loading the file. Because, MAYBE- and G are the only definitions that have been annotated, these are the only definitions that are reported on. The order of the reports is the same as the order in which the definitions were compiled. The report on G indicates that the tests performed by RUN-TESTS exercise most of the conditions tested by G. However, they do not exercise the situation in which the CASE statement is reached, but neither of its clauses is selected. There are no points within MAYBE-, because the code for MAYBE- does not contain any conditional forms. It is interesting to consider the precise points that COVER includes for G. When COVER processes a definition, a cluster of points is generated corresponding to each conditional form (i.e., IF, WHEN, UNTIL, COND, CASE, TYPECASE, AND, and OR) that is literally present in the program. In addition, points are generated corresponding to conditional forms that are produced by macros that are annotated (e.g., the IF produced by the MAYBE- in the first CASE clause in G). However, annotation is not applied to conditionals that come from other sources (e.g., from macros that are defined outside of the system being tested). These conditionals are omitted, because there is no reasonable way for the user to know how they relate to the code, and therefore there is no reasonable way for the user to devise a test case that will exercise them. The messages associated with a point's subordinates describe the situations under which the subordinates are exercised. The pattern of messages associated with CASE and TYPECASE is illustrated by the portion (reproduced below) of the report above that describes the CASE in G. ; + :REACH (CASE Y (1 (MAYBE- X Y <13> ; + :SELECT 1 <15> ; + :SELECT 2 <16> ; - :SELECT-NONE <17> There are two subpoints corresponding to the two clauses of the CASE. In addition, since the last clause does not begin with T or OTHERWISE, there is an additional point corresponding to the situation where none of the clauses of the CASE are executed. The pattern of messages associated with a COND is illustrated by the portion of the report that describes the COND in G. ; + :REACH (COND ((AND # Y) Y) (Y ( <3> ; + :REACH (AND (NULL X) Y) <9> ; + :FIRST-NON-NULL (AND (NULL X) <5> ; + :FIRST-NON-NULL Y <7> ; + :ALL-NULL <8> There are subordinate points corresponding to the two clauses and the situation where neither clause is executed. There is also a point <9> corresponding to the AND that is the predicate of the first COND clause. This point is placed directly under <3>, because it is not subordinate to any of the individual COND clauses. The treatment of AND (and OR) is particularly interesting. Sometimes AND is used as a control construct on a par with COND. In that situation, it is clear that AND should be treated analogously to COND. However, at other times, AND is used to compute a value that is tested by another conditional form. In that situation, COVER could choose to treat AND as a simple function. However, it is nevertheless still reasonable to think of an AND as having conditional points that correspond to different reasons why the AND returns a true or false value. It is wise to include tests corresponding to each of these different reasons. The pattern of messages associated with an AND is illustrated by the portion of the report that describes the AND in G. (cover:report :all t) ; + :REACH (AND (NULL X) Y) <9> ; + :FIRST-NULL (NULL X) <11> ; + :EVAL-ALL Y <12> The final subpoint corresponds to the situation where all of the arguments of the AND have been evaluated. The AND then returns whatever the final argument returned. The example in this section illustrates a batch-oriented use of COVER. However, COVER is most effectively used in an interactive way. It is recommended that you first create as comprehensive a test suite as you can and capture it using a tool such as RT. The tests should then be run in conjunction with COVER and repeated reports from COVER generated as additional tests are created until complete coverage of conditions has been achieved. To robustly support this mode of operation, COVER has been carefully designed so that it will work with batch-compiled definitions, incrementally-compiled definitions, and interpreted definitions. OBTAINING COVER COVER is written in portable Common Lisp and has been tested in several different Common Lisp implementations. The source for cover can be obtained over the INTERNET by using FTP. Connection should be made to FTP.AI.MIT.EDU (INTERNET number 18.104.22.168). Login as ``anonymous'' and copy the files shown below. In the directory /pub/lptrs/ cover.lisp ; source code cover-test.lisp ; test suite cover-doc.txt ; brief documentation The code for COVER and the files above are copyright 1991 by the Massachusetts Institute of Technology, Cambridge MA. Permission to use, copy, modify, and distribute this software for any purpose and without fee is hereby granted, provided that this copyright and permission notice appear in all copies and supporting documentation, and that the names of MIT and/or the author are not used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. MIT and the author make no representations about the suitability of this software for any purpose. It is provided ``as is'' without express or implied warranty. MIT and the author disclaim all warranties with regard to this software, including all implied warranties of merchantability and fitness. In no event shall MIT or the author be liable for any special, indirect or consequential damages or any damages whatsoever resulting from loss of use, data or profits, whether in an action of contract, negligence or other tortious action, arising out of or in connection with the use or performance of this software.