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 \n public 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\n version: 0.0.0\n libraryPathDependencies: codeql-java" )
114
+ with open (qlFile , "w" ) as f :
115
+ f .write ("import java\n import utils.GenerateFlowTestCase\n \n class GenRow extends TargetSummaryModelCsv {\n \n \t override predicate row(string r) {\n \t \t r = [\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