Skip to content

Commit 095c73f

Browse files
committed
redo the ApiGraph testing framework
1 parent 66fd43f commit 095c73f

33 files changed

+136
-103
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* A test query that verifies assertions about the API graph embedded in source-code comments.
3+
*
4+
* An assertion is a comment of the form `def=<path>` or `use=<path>`, and asserts that
5+
* there is a def/use feature reachable from the root along the given path, and its
6+
* associated data-flow node must start on the same line as the comment.
7+
*
8+
* The query only produces output for failed assertions, meaning that it should have no output
9+
* under normal circumstances.
10+
*
11+
* The syntax is made to look exactly like inline expectation tests, so that the tests
12+
* can remain consistent with other Python tests.
13+
*/
14+
15+
import python
16+
import semmle.python.dataflow.new.DataFlow
17+
import semmle.python.ApiGraphs
18+
19+
private DataFlow::Node getNode(API::Node nd, string kind) {
20+
kind = "def" and
21+
result = nd.getARhs()
22+
or
23+
kind = "use" and
24+
result = nd.getAUse()
25+
}
26+
27+
private string getLocStr(Location loc) {
28+
exists(string filepath, int startline |
29+
loc.hasLocationInfo(filepath, startline, _, _, _) and
30+
result = filepath + ":" + startline
31+
)
32+
}
33+
34+
/**
35+
* An assertion matching a data-flow node against an API-graph feature.
36+
*/
37+
class Assertion extends Comment {
38+
string expectedKind;
39+
string expectedLoc;
40+
string path;
41+
string polarity;
42+
43+
Assertion() {
44+
exists(string txt, string rex |
45+
txt = this.getText().trim() and
46+
rex = "#\\$.*?((?:MISSING: )?)(def|use)=([\\w\\(\\)\"\\.]*).*"
47+
|
48+
polarity = txt.regexpCapture(rex, 1) and
49+
expectedKind = txt.regexpCapture(rex, 2) and
50+
path = txt.regexpCapture(rex, 3) and
51+
expectedLoc = getLocStr(this.getLocation())
52+
)
53+
}
54+
55+
string getEdgeLabel(int i) {
56+
// matches a single edge. E.g. `getParameter(1)` or `getMember("foo")`.
57+
// The lookbehind/lookahead ensure that the boundary is correct, that is
58+
// either the edge is next to a ".", or it's the end of the string.
59+
result = path.regexpFind("(?<=\\.|^)([\\w\\(\\)\"]+)(?=\\.|$)", i, _).trim()
60+
}
61+
62+
int getPathLength() { result = max(int i | exists(this.getEdgeLabel(i))) + 1 }
63+
64+
predicate isNegative() { polarity = "MISSING: " }
65+
66+
API::Node lookup(int i) {
67+
i = 0 and
68+
result = API::root()
69+
or
70+
result =
71+
this.lookup(i - 1)
72+
.getASuccessor(any(API::Label::ApiLabel label |
73+
label.toString() = this.getEdgeLabel(i - 1)
74+
))
75+
}
76+
77+
API::Node lookup() { result = this.lookup(this.getPathLength()) }
78+
79+
predicate holds() { getLocStr(getNode(this.lookup(), expectedKind).getLocation()) = expectedLoc }
80+
81+
string tryExplainFailure() {
82+
exists(int i, API::Node nd, string prefix, string suffix |
83+
nd = this.lookup(i) and
84+
i < getPathLength() and
85+
not exists(this.lookup([i + 1 .. getPathLength()])) and
86+
prefix = nd + " has no outgoing edge labelled " + this.getEdgeLabel(i) + ";" and
87+
if exists(nd.getASuccessor())
88+
then
89+
suffix =
90+
"it does have outgoing edges labelled " +
91+
concat(string lbl |
92+
exists(nd.getASuccessor(any(API::Label::ApiLabel label | label.toString() = lbl)))
93+
|
94+
lbl, ", "
95+
) + "."
96+
else suffix = "it has no outgoing edges at all."
97+
|
98+
result = prefix + " " + suffix
99+
)
100+
or
101+
exists(API::Node nd, string kind | nd = this.lookup() |
102+
exists(getNode(nd, kind)) and
103+
not exists(getNode(nd, expectedKind)) and
104+
result = "Expected " + expectedKind + " node, but found " + kind + " node."
105+
)
106+
or
107+
exists(DataFlow::Node nd | nd = getNode(this.lookup(), expectedKind) |
108+
not getLocStr(nd.getLocation()) = expectedLoc and
109+
result =
110+
"Node not found on this line (but there is one on line " + min(getLocStr(nd.getLocation())) +
111+
")."
112+
)
113+
}
114+
115+
string explainFailure() {
116+
if this.isNegative()
117+
then (
118+
this.holds() and
119+
result = "Negative assertion failed."
120+
) else (
121+
not this.holds() and
122+
(
123+
result = this.tryExplainFailure()
124+
or
125+
not exists(this.tryExplainFailure()) and
126+
result = "Positive assertion failed for unknown reasons."
127+
)
128+
)
129+
}
130+
}
131+
132+
query predicate failed(Assertion a, string explanation) { explanation = a.explainFailure() }

python/ql/test/library-tests/ApiGraphs/awaited.ql

Lines changed: 0 additions & 26 deletions
This file was deleted.

python/ql/test/library-tests/ApiGraphs/def.ql

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import TestUtilities.VerifyApiGraphs

0 commit comments

Comments
 (0)