Skip to content

Commit 3ca7c75

Browse files
Add documentation for Qnit, split implementation files
1 parent 5c5a628 commit 3ca7c75

6 files changed

Lines changed: 473 additions & 137 deletions

File tree

README.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,50 @@ This module takes a set of starting points, ending points, and edges in a graph,
397397

398398
For displaying the discovered paths to users, see the `CustomPathProblem` module above.
399399

400-
### Testing
400+
### Testing with Qnit
401+
402+
While codeql's `test run` subcommand is a great way to test queries, it can be better in some cases
403+
to write a more traditional unit test for CodeQL libraries. Rather than selecting a set of outputs
404+
in a query and then inspecting that the query result (in the `.expectations` file) makes sense, qtil
405+
provides a library called "Qnit" for writing direct test cases with expectations, so that there's
406+
better cohesion between a test case and its expected output.
407+
408+
To use Qnit, import the `qtil.testing.Qnit` module, and create a test class that extends
409+
`Test, Case`. Inside the class override the `run(Qnit test)` member predicate, and conditionally
410+
call `test.pass(name)` or `test.fail(description)` as appropriate.
411+
412+
```ql
413+
import qtil.testing.Qnit
414+
415+
class MyTest extends Test, Case {
416+
override predicate run(Qnit test) {
417+
if 1 = 1
418+
then test.pass("1 equals 1")
419+
else test.fail("1 does not equal 1")
420+
}
421+
}
422+
```
423+
424+
You may define as many test classes as you like, and they will all be run when you run the command
425+
`codeql test run $TESTDIR`. If all tests pass, the test will output "{n} tests passed." If any test
426+
fails, the result of each test will be selected (including failing and passing tests).
427+
428+
For correct use, ensure that each test class passes with a unique name, and that tests always hold
429+
for some result, whether its a pass or a fail.
430+
431+
```
432+
override predicate run(Qnit test) {
433+
if 1 = 1
434+
then test.pass("1 equals 1") // Ensure this is unique to the test
435+
else none() // This would be valid CodeQL, but it would not fail.
436+
}
437+
```
438+
439+
It is particularly risky, albeit useful, to write `test.fail("..." + somePredicate().toString())`,
440+
as this test will **not** fail if `somePredicate()` does not hold. This is a risky pattern, and so
441+
should only be applied with some caution.
442+
443+
See the README in the `qtil.testing` directory for more information on how to use Qnit.
401444

402445
### Parameterization
403446

src/qtil/testing/Qnit.qll

Lines changed: 3 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -27,139 +27,6 @@
2727
* Be careful in deviating from this pattern, but do so at your own risk, so long as `pass()` and
2828
* `fail()` hold as in the example above.
2929
*/
30-
31-
private import codeql.util.Unit
32-
private import qtil.strings.Plural
33-
private import qtil.inheritance.UnderlyingString
34-
import QnitImpl::Public
35-
36-
private module QnitImpl {
37-
module Public {
38-
/**
39-
* A string that is re-typedef'd to Qnit for the sake of a pretty API.
40-
*
41-
* This class is used to define the predicate `run(Qnit test)`, which is prettier than
42-
* `run(string test)`. See also the `pass()` and `fail()` methods.
43-
*/
44-
bindingset[this]
45-
class Qnit extends UnderlyingString {
46-
bindingset[this]
47-
predicate isFailing() { str().matches("FAILURE: %") }
48-
49-
bindingset[this]
50-
predicate isPassing() { str().matches("PASS: %") }
51-
52-
/**
53-
* Call this method inside of `Test.run(Qnit test)` to report a failing test case.
54-
*
55-
* It is recommended to use unique strings for each test case, as this will allow you to
56-
* uniquely identify which tests failed, due to the way QL works.
57-
*/
58-
bindingset[description]
59-
predicate fail(string description) { this = "FAILURE: " + description }
60-
61-
/**
62-
* Call this method inside of `Test.run(Qnit test)` to report a passing test case.
63-
*
64-
* It is recommended to use unique strings for each test case, as this will allow Qnit to
65-
* properly count the number of tests that passed, due to the way QL works.
66-
*/
67-
bindingset[name]
68-
predicate pass(string name) { this = "PASS: " + name }
69-
}
70-
71-
/**
72-
* A test case that can be run by Qnit.
73-
*
74-
* This class is used to define the predicate `run(Qnit test)`, which is used to run the test
75-
* case. The `pass()` and `fail()` methods are used to report the result of the test case, as
76-
* follows:
77-
*
78-
* ```ql
79-
* class MyTest extends Test, Case {
80-
* override predicate run(Qnit test) {
81-
* if (someCondition)
82-
* then test.pass("some condition passed")
83-
* else test.fail("some condition failed")
84-
* }
85-
* }
86-
* ```
87-
*
88-
* This is simply an abstract class extending the CodeQL's empty `Unit` type, as its subclasses
89-
* are singletons with no underlying data.
90-
*/
91-
abstract class Case extends Unit {
92-
/**
93-
* Overridable method to define the behavior of the test case, which should generally follow
94-
* the pattern of:
95-
*
96-
* ```ql
97-
* if (someCondition)
98-
* then test.pass("some condition passed")
99-
* else test.fail("some condition failed")
100-
* ```
101-
*
102-
* It is best to use `pass()` and `fail()` with unique strings, as this will allow Qnit to
103-
* properly count the number of tests that passed, and uniquely identify which tests failed,
104-
* due to the way QL works.
105-
*
106-
* This is designed this way because we cannot execute an abstract method in QL while knowing
107-
* which concrete class it belongs to. Rather, `Case.run(x)` holds for all `x` defined in all
108-
* test cases. Making `x` a field does not solve this problem. We also must work around the
109-
* limitation of not having anonymous functions. Etc.
110-
*/
111-
abstract predicate run(Qnit test);
112-
}
113-
114-
/**
115-
* Extend this class to suppress a warning that is generated by QL when you override an
116-
* abstract class without implementing a characteristic predicate.
117-
*
118-
* ```ql
119-
* class MyTest extends Test, Case {
120-
* ...
121-
* }
122-
* ```
123-
*
124-
* Extends CodeQL's `Unit` type to match the `Test` class and support multiple inheritance.
125-
*/
126-
class Test extends Unit { }
127-
128-
query predicate test(string report) {
129-
if count(Qnit test | isFailing(test)) = 0
130-
then
131-
exists(int passed |
132-
passed = count(Qnit test | isPassing(test)) and
133-
report = plural("1 test", "All " + passed + " tests", passed) + " passed."
134-
)
135-
else
136-
exists(Qnit test |
137-
(isFailing(test) or isPassing(test)) and
138-
report = test
139-
)
140-
}
141-
}
142-
143-
/**
144-
* A base class that defines `toString()` in order to enable the API `extends Test, Case`.
145-
*
146-
* Extends CodeQL's `Unit` type becaue it has no underlying data.
147-
*
148-
* Ordinarily, the QL compiler will issue a warning if you override an abstract class without
149-
* implementing a characteristic predicate, as in `class MyTest extends Case {...}`. In order to
150-
* suppress this warning, we can extend an additional non-abstract class, and therefore our
151-
* recommended pattern is to extend `Test, Case` in order to define a test case.
152-
*
153-
* In QL, `Test` and `Case` must both extend the same base type. If that base type were `Unit`,
154-
* then both `Test` and `Case` would need to implement `toString()`. At this point, QL would
155-
* issue a warning for `class MyTest extends Test, Case {...}` because QL cannot disambiguate
156-
* which `toString()` method to inherit. Therefore, we define `CaseBase` as a subclass of `TCase`
157-
* which defines an unambiguous `toString()` method.
158-
*
159-
* With this workaround, we can define `class MyTest extends Test, Case {...}` without any
160-
* warnings from the QL compiler, and without having to implement `toString()` in every test.
161-
*/
162-
private predicate isFailing(Qnit test) { exists(Case c | c.run(test) and test.isFailing()) }
163-
164-
private predicate isPassing(Qnit test) { exists(Case c | c.run(test) and test.isPassing()) }
165-
}
30+
import qtil.testing.impl.Qnit
31+
import qtil.testing.impl.Test
32+
import qtil.testing.impl.Runner

0 commit comments

Comments
 (0)