Skip to content

Commit 6fee40c

Browse files
committed
Add flow-through test case generator
1 parent d4bb8a7 commit 6fee40c

File tree

4 files changed

+491
-0
lines changed

4 files changed

+491
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import errno
2+
import json
3+
import os
4+
import os.path
5+
import re
6+
import shlex
7+
import shutil
8+
import subprocess
9+
import sys
10+
import tempfile
11+
12+
if len(sys.argv) != 4:
13+
print("Usage: GenerateFlowTestCase.py specsToTest.ssv projectPom.xml outdir", file=sys.stderr)
14+
print("specsToTest.ssv should contain SSV rows describing method taint-propagation specifications to test", file=sys.stderr)
15+
print("projectPom.xml should import dependencies sufficient to resolve the types used in specsToTest.ssv", file=sys.stderr)
16+
sys.exit(1)
17+
18+
try:
19+
os.makedirs(sys.argv[3])
20+
except Exception as e:
21+
if e.errno != errno.EEXIST:
22+
print("Failed to create output directory %s: %s" % (sys.argv[3], e))
23+
sys.exit(1)
24+
25+
resultJava = os.path.join(sys.argv[3], "Test.java")
26+
resultQl = os.path.join(sys.argv[3], "test.ql")
27+
28+
if os.path.exists(resultJava) or os.path.exists(resultQl):
29+
print("Won't overwrite existing files '%s' or '%s'" % (resultJava, resultQl), file = sys.stderr)
30+
sys.exit(1)
31+
32+
workDir = tempfile.mkdtemp()
33+
34+
# Step 1: make a database that touches all types whose methods we want to test:
35+
print("Creating Maven project")
36+
projectDir = os.path.join(workDir, "mavenProject")
37+
os.makedirs(projectDir)
38+
39+
try:
40+
shutil.copyfile(sys.argv[2], os.path.join(projectDir, "pom.xml"))
41+
except Exception as e:
42+
print("Failed to read project POM %s: %s" % (sys.argv[2], e), file = sys.stderr)
43+
sys.exit(1)
44+
45+
commentRegex = re.compile("^\s*(//|#)")
46+
def isComment(s):
47+
return commentRegex.match(s) is not None
48+
49+
try:
50+
with open(sys.argv[1], "r") as f:
51+
specs = [l for l in f if not isComment(l)]
52+
except Exception as e:
53+
print("Failed to open %s: %s\n" % (sys.argv[1], e))
54+
sys.exit(1)
55+
56+
projectTestPkgDir = os.path.join(projectDir, "src", "main", "java", "test")
57+
projectTestFile = os.path.join(projectTestPkgDir, "Test.java")
58+
59+
os.makedirs(projectTestPkgDir)
60+
61+
def qualifiedOuterNameFromSsvRow(row):
62+
cells = row.split(";")
63+
if len(cells) < 2:
64+
return None
65+
return cells[0] + "." + cells[1].replace("$", ".")
66+
67+
with open(projectTestFile, "w") as testJava:
68+
testJava.write("package test;\n\npublic class Test {\n\n")
69+
70+
for i, spec in enumerate(specs):
71+
outerName = qualifiedOuterNameFromSsvRow(spec)
72+
if outerName is None:
73+
print("A taint specification has the wrong format: should be 'package;classname;methodname....'", file = sys.stderr)
74+
print("Mis-formatted row: " + spec, file = sys.stderr)
75+
sys.exit(1)
76+
testJava.write("\t%s obj%d = null;\n" % (outerName, i))
77+
78+
testJava.write("}")
79+
80+
print("Creating project database")
81+
cmd = ["codeql", "database", "create", "--language=java", "db"]
82+
ret = subprocess.call(cmd, cwd = projectDir)
83+
if ret != 0:
84+
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)
85+
print("Failed command was: %s (cwd: %s)" % (shlex.join(cmd), projectDir), file = sys.stderr)
86+
sys.exit(1)
87+
88+
print("Creating test-generation query")
89+
queryDir = os.path.join(workDir, "query")
90+
os.makedirs(queryDir)
91+
qlFile = os.path.join(queryDir, "gen.ql")
92+
with open(os.path.join(queryDir, "qlpack.yml"), "w") as f:
93+
f.write("name: test-generation-query\nversion: 0.0.0\nlibraryPathDependencies: codeql-java")
94+
with open(qlFile, "w") as f:
95+
f.write("import java\nimport utils.GenerateFlowTestCase\n\nclass GenRow extends CsvRow {\n\n\tGenRow() {\n\t\tthis = [\n")
96+
f.write(",\n".join('\t\t\t"%s"' % spec.strip() for spec in specs))
97+
f.write("\n\t\t]\n\t}\n}\n")
98+
99+
print("Generating tests")
100+
generatedBqrs = os.path.join(queryDir, "out.bqrs")
101+
cmd = ['codeql', 'query', 'run', qlFile, '--database', os.path.join(projectDir, "db"), '--output', generatedBqrs]
102+
ret = subprocess.call(cmd)
103+
if ret != 0:
104+
print("Failed to generate tests. Failed command was: " + shlex.join(cmd))
105+
sys.exit(1)
106+
107+
generatedJson = os.path.join(queryDir, "out.json")
108+
cmd = ['codeql', 'bqrs', 'decode', generatedBqrs, '--format=json', '--output', generatedJson]
109+
ret = subprocess.call(cmd)
110+
if ret != 0:
111+
print("Failed to decode BQRS. Failed command was: " + shlex.join(cmd))
112+
sys.exit(1)
113+
114+
def getTuples(queryName, jsonResult, fname):
115+
if queryName not in jsonResult or "tuples" not in jsonResult[queryName]:
116+
print("Failed to read generated tests: expected key '%s' with a 'tuples' subkey in file '%s'" % (queryName, fname), file = sys.stderr)
117+
sys.exit(1)
118+
return jsonResult[queryName]["tuples"]
119+
120+
with open(generatedJson, "r") as f:
121+
generateOutput = json.load(f)
122+
testCaseRows = getTuples("getTestCase", generateOutput, generatedJson)
123+
supportModelRows = getTuples("getASupportMethodModel", generateOutput, generatedJson)
124+
if len(testCaseRows) != 1 or len(testCaseRows[0]) != 1:
125+
print("Expected exactly one getTestCase result with one column (got: %s)" % json.dumps(testCaseRows), file = sys.stderr)
126+
if any(len(row) != 1 for row in supportModelRows):
127+
print("Expected exactly one column in getASupportMethodModel relation (got: %s)" % json.dumps(supportModelRows), file = sys.stderr)
128+
129+
with open(resultJava, "w") as f:
130+
f.write(generateOutput["getTestCase"]["tuples"][0][0])
131+
132+
scriptPath = os.path.dirname(sys.argv[0])
133+
134+
with open(resultQl, "w") as f:
135+
with open(os.path.join(scriptPath, "testHeader.qlfrag"), "r") as header:
136+
shutil.copyfileobj(header, f)
137+
f.write(", ".join('"%s"' % modelSpecRow[0].strip() for modelSpecRow in supportModelRows))
138+
with open(os.path.join(scriptPath, "testFooter.qlfrag"), "r") as header:
139+
shutil.copyfileobj(header, f)
140+
141+
cmd = ['codeql', 'query', 'format', '-qq', '-i', resultQl]
142+
subprocess.call(cmd)
143+
144+
shutil.rmtree(workDir)

0 commit comments

Comments
 (0)