This document describes how to write and run tests for the Checker Framework. Writing and running tests is useful for bug submitters, checker writers, and Checker Framework maintainers. Users of the Checker Framework and of the checkers packaged with it should read the manual instead; see file ../../docs/manual/manual.html . How to run all the tests for the Checker Framework ================================================== # To run from the checker-framework directory: ./gradlew allTests Other Gradle tasks also exist to run a subset of the tests; for example, # Run from the checker-framework directory: ./gradlew :checker:NullnessTest # Run from the checker directory: ../gradlew NullnessTest # To see all tasks ./gradlew tasks How to run just one test for the Checker Framework ================================================== To run a subset of the JUnit tests, use Gradle's --tests command-line argument, from a project directory such as checker/, dataflow/, or framework/: cd $CHECKERFRAMEWORK/checker ../gradlew test --tests '*ParseAllJdkTest' To check one source code file, do something like the following, assuming that the source code file to be checked is AssertNonNullTest.java in directory $CHECKERFRAMEWORK/checker/tests/nullness/ and the checker is org.checkerframework.checker.nullness.NullnessChecker. cd $CHECKERFRAMEWORK ./gradlew assembleForJavac && checker/bin/javac -processor org.checkerframework.checker.nullness.NullnessChecker -implicit:class checker/tests/nullness/AssertNonNullTest.java where the specific checker and command-line arguments are often clear from the directory name but can also be determined from a file such as checker-framework/checker/tests/src/tests/MyTypeSystemTest.java which is the source code for the test itself. Writing new tests for an existing checker ========================================= To create a new test case, just place a Java file in the test directory, whose name usually corresponds to the checker name, such as checker-framework/checker/tests/nullness/ . Unless the README file in the test directory specifies otherwise, then the Java file must 1. Not issue any javac errors. 2. Not declare a class with the same (fully qualified) name as any other class in the test directory. 3. Not declare a class with the same (simple) name as any commonly used Java library class such as List. The testing framework for the Checker Framework is built on top of JUnit. However, its tests are more like end-to-end system tests than unit tests. A checker test case has two parts: 1. the Java class to be compiled, and 2. a set of expected errors. Both parts can be expressed in one Java file (see below). Classes in checker-framework/framework/tests/src/tests/lib can be referenced by tests to check behavior related to unchecked bytecode. By convention, when a test is about an issue in the issue tracker, we write a comment at the top of the file, in this format: // Test case for issue 266: // https://github.com/typetools/checker-framework/issues/266 // @skip-test until the issue is fixed Specifying expected warnings and errors ======================================= A test case is a Java file that uses stylized comments to indicate expected error messages. Suppose that you want to test the Nullness Checker's behavior when type-checking the following Java class: public class MyNullnessTest { void method() { Object nullable = null; nullable.toString(); // should emit error } } The Nullness Checker should report an error for the dereference in line 4. The non-localized message key for such an error is 'dereference.of.nullable'. You could learn that by reading the Javadoc (or the source code) for org.checkerframework.checker.nullness.NullnessVisitor, or by creating the test and observing the failure. To indicate the expected failure, insert the line // :: error: () directly preceding the expected error line. If a warning rather than an error is expected, insert the line // :: warning: () If a stub parser warning is expected, insert the line //warning: AnnotationFileParser: If multiple errors are expected on a single line, duplicate everything except the "//" comment characters, as in // :: error: () :: error: () If the expected failures line would be very long, you may break it across multiple comment lines. It is permitted to write a "// ::" comment after "{" that was at the end of a line, to indicate a warning on the line immediately after the "{". So the final test case would be: public class MyNullnessTest { void method() { Object nullable = null; // :: error: (dereference.of.nullable) nullable.toString(); } } The file may appear anywhere in or under checker-framework/checker/tests/nullness/. (You may find it useful to use separate subfolders, such as nullness/tests/nullness/dereference/.) Each checker should have its own folder under checker-framework/checker/tests/, such as checker-framework/checker/tests/interning/, checker-framework/checker/tests/regex/, etc. You can indicate an expected warning (as opposed to error) by using "warning:" instead of "error:", as in // :: warning: (nulltest.redundant) assert val != null; Multiple expected messages can be given on the same line using the "// :: A :: B :: C" syntax. This example expects both an error and a warning from the same line of code: @Regex String s1 = null; // :: error: (assignment) :: warning: (cast.unsafe) @Regex(3) String s2 = (@Regex(2) String) s; As an alternative to writing expected errors in the source file using "// ::" syntax, expected errors can be specified in a separate file using the .out file extension. These files contain lines of the following format: :19: error: (dereference.of.nullable) The number between the colons is the line number of the expected error message. This format is harder to maintain, and we suggest using the in-line comment format. Writing tests for a new checker or with different command-line arguments ======================================================================== To create tests for a new checker, mimic some existing checker's tests: * create a top-level test directory, such as checker-framework/checker/tests/regex for the test cases * create a top-level JUnit test in checker-framework/checker/src/test/java/tests, such as: RegexTest.java It is a subclass of CheckerFrameworkPerDirectoryTest, and its list of checker options must include "-Anomsgtext". (See the API documentation for CheckerFrameworkPerDirectoryTest for more detailed information.) * include "all-systems" as a test directly by adding it to the array created in getTestDirs in the new test class. See checker-framework/framework/tests/all-systems/README for more details about the all-systems tests. Different test cases may need to pass different command-line arguments (flags) to the checker -- for instance, to check an optional command-line argument that should not be enabled for every test. Follow the same instructions as for writing tests for a new checker. A Gradle task is created for each Junit test class. The task is named the same as the Junit test classname, for example, ./gradlew RegexTest runs the Regex Checker tests. Disabling a test case ===================== Write @skip-test anywhere in a test file to disable that test. Write @below-java17-jdk-skip-test anywhere in a test file to disable that test if the executing JDK version is lower than 17. This is useful when the test depends on Java 17 language features that need runtime support or depends on Java 17 APIs. If the test contains a record, then it must also be in a directory named java17 so that it is only formatting when using JDK 17. Write @infer-ajava-skip-test, @infer-jaifs-skip-test, or @infer-stubs-skip-test to skip running inference using a particular output format on a test. If you want to run inference on the test case but not validate the results of inference, then delete the test file in the inference Gradle task that tests it. To disable all tests for a given type system, annotate the JUnit test class with @Ignore. Annotated JDK ============= The tests run with the annotated JDK. Keep this in mind when writing tests. Seeing the javac commands that are run ====================================== To see the exact javac commands that the Checker Framework test framework runs, use -Pemit.test.debug=true For example: ./gradlew NullnessStubfileTest -Pemit.test.debug=true This may be helpful during debugging.