Skip to content

Commit 11fc23b

Browse files
authored
Merge pull request github#6030 from smowton/smowton/admin/test-generator
Add test-generator script + add generated models for Spring summary steps
2 parents 3a33985 + e9390cb commit 11fc23b

File tree

10 files changed

+819
-17
lines changed

10 files changed

+819
-17
lines changed

csharp/ql/src/semmle/code/csharp/dataflow/internal/FlowSummaryImpl.qll

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,13 @@ module Private {
642642
)
643643
}
644644

645+
/**
646+
* Holds if `spec` specifies summary component stack `stack`.
647+
*/
648+
predicate interpretSpec(string spec, SummaryComponentStack stack) {
649+
interpretSpec(spec, 0, stack)
650+
}
651+
645652
private predicate interpretSpec(string spec, int idx, SummaryComponentStack stack) {
646653
exists(string c |
647654
relevantSpec(spec) and
@@ -680,8 +687,8 @@ module Private {
680687
) {
681688
exists(string inSpec, string outSpec, string kind |
682689
summaryElement(this, inSpec, outSpec, kind) and
683-
interpretSpec(inSpec, 0, input) and
684-
interpretSpec(outSpec, 0, output)
690+
interpretSpec(inSpec, input) and
691+
interpretSpec(outSpec, output)
685692
|
686693
kind = "value" and preservesValue = true
687694
or

java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -436,19 +436,25 @@ predicate summaryModel(
436436
string namespace, string type, boolean subtypes, string name, string signature, string ext,
437437
string input, string output, string kind
438438
) {
439-
exists(string row |
440-
summaryModel(row) and
441-
row.splitAt(";", 0) = namespace and
442-
row.splitAt(";", 1) = type and
443-
row.splitAt(";", 2) = subtypes.toString() and
444-
subtypes = [true, false] and
445-
row.splitAt(";", 3) = name and
446-
row.splitAt(";", 4) = signature and
447-
row.splitAt(";", 5) = ext and
448-
row.splitAt(";", 6) = input and
449-
row.splitAt(";", 7) = output and
450-
row.splitAt(";", 8) = kind
451-
)
439+
summaryModel(namespace, type, subtypes, name, signature, ext, input, output, kind, _)
440+
}
441+
442+
/** Holds if a summary model `row` exists for the given parameters. */
443+
predicate summaryModel(
444+
string namespace, string type, boolean subtypes, string name, string signature, string ext,
445+
string input, string output, string kind, string row
446+
) {
447+
summaryModel(row) and
448+
row.splitAt(";", 0) = namespace and
449+
row.splitAt(";", 1) = type and
450+
row.splitAt(";", 2) = subtypes.toString() and
451+
subtypes = [true, false] and
452+
row.splitAt(";", 3) = name and
453+
row.splitAt(";", 4) = signature and
454+
row.splitAt(";", 5) = ext and
455+
row.splitAt(";", 6) = input and
456+
row.splitAt(";", 7) = output and
457+
row.splitAt(";", 8) = kind
452458
}
453459

454460
private predicate relevantPackage(string package) {

java/ql/src/semmle/code/java/dataflow/internal/DataFlowUtil.qll

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,13 @@ class Content extends TContent {
173173
/** Gets a textual representation of this element. */
174174
abstract string toString();
175175

176+
/**
177+
* Holds if this element is at the specified location.
178+
* The location spans column `startcolumn` of line `startline` to
179+
* column `endcolumn` of line `endline` in file `filepath`.
180+
* For more information, see
181+
* [Locations](https://help.semmle.com/QL/learn-ql/ql/locations.html).
182+
*/
176183
predicate hasLocationInfo(string path, int sl, int sc, int el, int ec) {
177184
path = "" and sl = 0 and sc = 0 and el = 0 and ec = 0
178185
}

java/ql/src/semmle/code/java/dataflow/internal/FlowSummaryImpl.qll

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,13 @@ module Private {
642642
)
643643
}
644644

645+
/**
646+
* Holds if `spec` specifies summary component stack `stack`.
647+
*/
648+
predicate interpretSpec(string spec, SummaryComponentStack stack) {
649+
interpretSpec(spec, 0, stack)
650+
}
651+
645652
private predicate interpretSpec(string spec, int idx, SummaryComponentStack stack) {
646653
exists(string c |
647654
relevantSpec(spec) and
@@ -680,8 +687,8 @@ module Private {
680687
) {
681688
exists(string inSpec, string outSpec, string kind |
682689
summaryElement(this, inSpec, outSpec, kind) and
683-
interpretSpec(inSpec, 0, input) and
684-
interpretSpec(outSpec, 0, output)
690+
interpretSpec(inSpec, input) and
691+
interpretSpec(outSpec, output)
685692
|
686693
kind = "value" and preservesValue = true
687694
or
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/python3
2+
3+
import errno
4+
import json
5+
import os
6+
import os.path
7+
import re
8+
import shlex
9+
import shutil
10+
import subprocess
11+
import sys
12+
import tempfile
13+
14+
if any(s == "--help" for s in sys.argv):
15+
print("""Usage:
16+
GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir
17+
18+
This generates test cases exercising function model specifications found in specsToTest.csv
19+
producing files Test.java, test.ql and test.expected in outdir.
20+
21+
projectPom.xml should be a Maven pom sufficient to resolve the classes named in specsToTest.csv.
22+
Typically this means supplying a skeleton POM <dependencies> section that retrieves whatever jars
23+
contain the needed classes.
24+
25+
Requirements: `mvn` and `codeql` should both appear on your path.
26+
27+
After test generation completes, any lines in specsToTest.csv that didn't produce tests are output.
28+
If this happens, check the spelling of class and method names, and the syntax of input and output specifications.
29+
""")
30+
sys.exit(0)
31+
32+
if len(sys.argv) != 4:
33+
print("Usage: GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir", file=sys.stderr)
34+
print("specsToTest.csv should contain CSV rows describing method taint-propagation specifications to test", file=sys.stderr)
35+
print("projectPom.xml should import dependencies sufficient to resolve the types used in specsToTest.csv", file=sys.stderr)
36+
sys.exit(1)
37+
38+
try:
39+
os.makedirs(sys.argv[3])
40+
except Exception as e:
41+
if e.errno != errno.EEXIST:
42+
print("Failed to create output directory %s: %s" % (sys.argv[3], e))
43+
sys.exit(1)
44+
45+
resultJava = os.path.join(sys.argv[3], "Test.java")
46+
resultQl = os.path.join(sys.argv[3], "test.ql")
47+
48+
if os.path.exists(resultJava) or os.path.exists(resultQl):
49+
print("Won't overwrite existing files '%s' or '%s'" % (resultJava, resultQl), file = sys.stderr)
50+
sys.exit(1)
51+
52+
workDir = tempfile.mkdtemp()
53+
54+
# Make a database that touches all types whose methods we want to test:
55+
print("Creating Maven project")
56+
projectDir = os.path.join(workDir, "mavenProject")
57+
os.makedirs(projectDir)
58+
59+
try:
60+
shutil.copyfile(sys.argv[2], os.path.join(projectDir, "pom.xml"))
61+
except Exception as e:
62+
print("Failed to read project POM %s: %s" % (sys.argv[2], e), file = sys.stderr)
63+
sys.exit(1)
64+
65+
commentRegex = re.compile("^\s*(//|#)")
66+
def isComment(s):
67+
return commentRegex.match(s) is not None
68+
69+
try:
70+
with open(sys.argv[1], "r") as f:
71+
specs = [l for l in f if not isComment(l)]
72+
except Exception as e:
73+
print("Failed to open %s: %s\n" % (sys.argv[1], e))
74+
sys.exit(1)
75+
76+
projectTestPkgDir = os.path.join(projectDir, "src", "main", "java", "test")
77+
projectTestFile = os.path.join(projectTestPkgDir, "Test.java")
78+
79+
os.makedirs(projectTestPkgDir)
80+
81+
def qualifiedOuterNameFromCsvRow(row):
82+
cells = row.split(";")
83+
if len(cells) < 2:
84+
return None
85+
return cells[0] + "." + cells[1].replace("$", ".")
86+
87+
with open(projectTestFile, "w") as testJava:
88+
testJava.write("package test;\n\npublic class Test {\n\n")
89+
90+
for i, spec in enumerate(specs):
91+
outerName = qualifiedOuterNameFromCsvRow(spec)
92+
if outerName is None:
93+
print("A taint specification has the wrong format: should be 'package;classname;methodname....'", file = sys.stderr)
94+
print("Mis-formatted row: " + spec, file = sys.stderr)
95+
sys.exit(1)
96+
testJava.write("\t%s obj%d = null;\n" % (outerName, i))
97+
98+
testJava.write("}")
99+
100+
print("Creating project database")
101+
cmd = ["codeql", "database", "create", "--language=java", "db"]
102+
ret = subprocess.call(cmd, cwd = projectDir)
103+
if ret != 0:
104+
print("Failed to create project database. Check that '%s' is a valid POM that pulls in all necessary dependencies, and '%s' specifies valid classes and methods." % (sys.argv[2], sys.argv[1]), file = sys.stderr)
105+
print("Failed command was: %s (cwd: %s)" % (shlex.join(cmd), projectDir), file = sys.stderr)
106+
sys.exit(1)
107+
108+
print("Creating test-generation query")
109+
queryDir = os.path.join(workDir, "query")
110+
os.makedirs(queryDir)
111+
qlFile = os.path.join(queryDir, "gen.ql")
112+
with open(os.path.join(queryDir, "qlpack.yml"), "w") as f:
113+
f.write("name: test-generation-query\nversion: 0.0.0\nlibraryPathDependencies: codeql-java")
114+
with open(qlFile, "w") as f:
115+
f.write("import java\nimport utils.GenerateFlowTestCase\n\nclass GenRow extends TargetSummaryModelCsv {\n\n\toverride predicate row(string r) {\n\t\tr = [\n")
116+
f.write(",\n".join('\t\t\t"%s"' % spec.strip() for spec in specs))
117+
f.write("\n\t\t]\n\t}\n}\n")
118+
119+
print("Generating tests")
120+
generatedBqrs = os.path.join(queryDir, "out.bqrs")
121+
cmd = ['codeql', 'query', 'run', qlFile, '--database', os.path.join(projectDir, "db"), '--output', generatedBqrs]
122+
ret = subprocess.call(cmd)
123+
if ret != 0:
124+
print("Failed to generate tests. Failed command was: " + shlex.join(cmd))
125+
sys.exit(1)
126+
127+
generatedJson = os.path.join(queryDir, "out.json")
128+
cmd = ['codeql', 'bqrs', 'decode', generatedBqrs, '--format=json', '--output', generatedJson]
129+
ret = subprocess.call(cmd)
130+
if ret != 0:
131+
print("Failed to decode BQRS. Failed command was: " + shlex.join(cmd))
132+
sys.exit(1)
133+
134+
def getTuples(queryName, jsonResult, fname):
135+
if queryName not in jsonResult or "tuples" not in jsonResult[queryName]:
136+
print("Failed to read generated tests: expected key '%s' with a 'tuples' subkey in file '%s'" % (queryName, fname), file = sys.stderr)
137+
sys.exit(1)
138+
return jsonResult[queryName]["tuples"]
139+
140+
with open(generatedJson, "r") as f:
141+
generateOutput = json.load(f)
142+
expectedTables = ("getTestCase", "getASupportMethodModel", "missingSummaryModelCsv", "getAParseFailure")
143+
144+
testCaseRows, supportModelRows, missingSummaryModelCsvRows, parseFailureRows = \
145+
tuple([getTuples(k, generateOutput, generatedJson) for k in expectedTables])
146+
147+
if len(testCaseRows) != 1 or len(testCaseRows[0]) != 1:
148+
print("Expected exactly one getTestCase result with one column (got: %s)" % json.dumps(testCaseRows), file = sys.stderr)
149+
if any(len(row) != 1 for row in supportModelRows):
150+
print("Expected exactly one column in getASupportMethodModel relation (got: %s)" % json.dumps(supportModelRows), file = sys.stderr)
151+
if any(len(row) != 2 for row in parseFailureRows):
152+
print("Expected exactly two columns in parseFailureRows relation (got: %s)" % json.dumps(parseFailureRows), file = sys.stderr)
153+
154+
if len(missingSummaryModelCsvRows) != 0:
155+
print("Tests for some CSV rows were requested that were not in scope (SummaryModelCsv.row does not hold):\n" + "\n".join(r[0] for r in missingSummaryModelCsvRows))
156+
sys.exit(1)
157+
if len(parseFailureRows) != 0:
158+
print("The following rows failed to generate any test case. Check package, class and method name spelling, and argument and result specifications:\n%s" % "\n".join(r[0] + ": " + r[1] for r in parseFailureRows), file = sys.stderr)
159+
sys.exit(1)
160+
161+
with open(resultJava, "w") as f:
162+
f.write(generateOutput["getTestCase"]["tuples"][0][0])
163+
164+
scriptPath = os.path.dirname(sys.argv[0])
165+
166+
def copyfile(fromName, toFileHandle):
167+
with open(os.path.join(scriptPath, fromName), "r") as fromFileHandle:
168+
shutil.copyfileobj(fromFileHandle, toFileHandle)
169+
170+
with open(resultQl, "w") as f:
171+
copyfile("testHeader.qlfrag", f)
172+
if len(supportModelRows) != 0:
173+
copyfile("testModelsHeader.qlfrag", f)
174+
f.write(", ".join('"%s"' % modelSpecRow[0].strip() for modelSpecRow in supportModelRows))
175+
copyfile("testModelsFooter.qlfrag", f)
176+
copyfile("testFooter.qlfrag", f)
177+
178+
# Make an empty .expected file, since this is an inline-exectations test
179+
with open(os.path.join(sys.argv[3], "test.expected"), "w"):
180+
pass
181+
182+
cmd = ['codeql', 'query', 'format', '-qq', '-i', resultQl]
183+
subprocess.call(cmd)
184+
185+
shutil.rmtree(workDir)

0 commit comments

Comments
 (0)