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