13 SPDX-FileCopyrightText: © 2012 Hewlett-Packard Development Company, L.P.
15 SPDX-License-Identifier: GPL-2.0-only
18 from xml.dom.minidom
import getDOMImplementation
19 from xml.dom.minidom
import parseString
20 from xml.dom
import Node
21 from optparse
import OptionParser
34 defsReplace = re.compile(
'{([^{}]*)}')
35 defsSplit = re.compile(
'([^\s]+):([^\s]+)')
38 """ Error class used for missing definitions in the xml file """
41 self.
valuevalue = value
44 return repr(self.
valuevalue)
47 """ Error class used when a test suite takes too long to run """
50 def timeout(func, maxRuntime):
59 def timeout_handler(signum, frame):
62 signal.signal(signal.SIGALRM, timeout_handler)
63 signal.alarm(maxRuntime)
77 The testsuite class is used to deserialize a test suite from the xml file,
78 run the tests and report the results to another xml document.
80 name the name of the test suite
81 defs a map of strings to values used to do variables replacement
82 setup list of Actions that will be taken before running the tests
83 cleanup list of Actions that will be taken after running the tests
84 tests list of Actions that are the actual tests
85 subpro list of processes that are running concurrently with the tests
90 Constructor for the testsuite class. This will deserialize the testsuite
91 from the xml file that describes all the tests. For each element in the
92 setup, and cleanup and action will be created. For each element under each
93 <test></test> tag an action will be created.
95 This will also grab the definitions of variables for the self.defines map. The
96 variable substitution will be performed when the definition is loaded from
102 defNode = node.getElementsByTagName(
'definitions')[0]
103 definitions = defNode.attributes
105 self.
namename = node.getAttribute(
'name')
108 self.
definesdefines[
'pids'] = {}
111 for i
in range(definitions.length):
112 if definitions.item(i).name
not in self.
definesdefines:
113 self.
definesdefines[definitions.item(i).name] = self.
substitutesubstitute(definitions.item(i).value, defNode)
122 if len(node.getElementsByTagName(
'setup')) != 0:
123 setup = node.getElementsByTagName(
'setup')[0]
124 for action
in [curr
for curr
in setup.childNodes
if curr.nodeType == Node.ELEMENT_NODE]:
128 if len(node.getElementsByTagName(
'cleanup')) != 0:
129 cleanup = node.getElementsByTagName(
'cleanup')[0]
130 for action
in [curr
for curr
in cleanup.childNodes
if curr.nodeType == Node.ELEMENT_NODE]:
134 for test
in node.getElementsByTagName(
'test'):
135 newTest = (test.getAttribute(
'name'), [])
136 for action
in [curr
for curr
in test.childNodes
if curr.nodeType == Node.ELEMENT_NODE]:
137 newTest[1].append(self.
createActioncreateAction(action))
138 self.
teststests.append(newTest)
142 Simple function to make calling processVariable a lot cleaner
144 Returns the string with the variables correctly substituted
146 while defsReplace.search(string):
147 string = defsReplace.sub(functools.partial(self.
processVariableprocessVariable, node), string)
152 Function passed to the regular expression library to replace variables in a
153 string from the xml file.
156 The regular expression used is "{([^\s]*?)}". This will match anything that
157 doesn't contain any whitespace and falls between two curly braces. For
158 example "{hello}" will match, but "{hello goodbye}" and "hello" will not.
160 Any variable name that starts with a "$" has a special meaning. The text
161 following the "$" will be used as a shell command and executed. The
162 "{$text}" will be replaced with the output of the shell command. For example
163 "{$pwd}" will be replaced with the output of the shell command "pwd".
165 If a variable has a ":" in it, anything that follows the ":" will be used
166 to index into the associative array in the definitions map. For example
167 "{pids:0}" will access the element that is mapped to the string "0" in the
168 associative array that is as the mapped to the string "pids" in the defs
171 Returns the replacement string
173 name = match.group(1)
177 process = os.popen(name[1:],
'r')
183 arrayMatch = defsSplit.match(name)
185 name = arrayMatch.group(1)
186 index = self.
substitutesubstitute(arrayMatch.group(2), node)
188 if not isinstance(self.
definesdefines[name], dict):
189 raise DefineError(
'"{0}" is not a dictionary in testsuite "{1}"'.format(name, self.
namename))
190 if name
not in self.
definesdefines:
191 if node
and node.hasAttribute(name):
194 raise DefineError(
'"{0}" not defined in testsuite "{1}"'.format(name, self.
namename))
195 if index
not in self.
definesdefines[name]:
196 raise DefineError(
'"{0}" is out of bounds for "{1}.{2}"'.format(index, self.
namename, name))
197 return self.
definesdefines[name][arrayMatch.group(2)]
200 if name
not in self.
definesdefines:
201 if node
and node.hasAttribute(name):
202 self.
definesdefines[name] = self.
substitutesubstitute(node.getAttribute(name), node)
204 raise DefineError(
'"{0}" not defined in testsuite "{1}"'.format(name, self.
namename))
205 return self.
definesdefines[name]
209 Puts a failure node into an the results document
214 fail = doc.createElement(
'failure')
215 fail.setAttribute(
'type', type)
217 text = doc.createTextNode(value)
218 fail.appendChild(text)
220 dest.appendChild(fail)
228 Creates all the child actions for a particular node in the xml file.
230 return [self.
createActioncreateAction(child)
for child
in node.childNodes
if child.nodeType == Node.ELEMENT_NODE]
234 Creates an action given a particular test suite and xml node. This uses
235 simple python reflection to get the method of the testsuite class that has
236 the same name as the xml node tag. The action is a functor that can be
237 called later be another part of the test harness.
239 To write a new type of action write a function with the signature:
240 actionName(self, source_node, xml_document, destination_node)
242 * The source_node is the xml node that described the action, this node
243 should describe everything that is necessary for the action to be
244 performed. This is passed to the action when the action is created.
245 * The xml_document is the document that the test results are being written
246 to. This is passed to the action when it is called, not during creation.
247 * The destination_node is the node in the results xml document that this
248 particular action should be writing its results to. This is passed in when
249 the action is called, not during creation.
251 The action should return the number of tests that it ran and the number of
252 failures that it experienced. A failing action has different meanings
253 during different parts of the code. During setup, a failing action
254 indicates that the setup is not ready to proceed. Failing actions during
255 setup will be called repeatedly once every five seconds until they no
256 longer register a failure. Failing actions during testing indicate a
257 failing test. The failure will be reported to results document, but the
258 action should still call the failure method to indicate in the results
259 document why the failure happened. During cleanup what an action returns is
262 Returns the new action
264 def action_wrapper(action, node, doc, dest):
266 return action(node, doc, dest)
268 if not hasattr(self, node.nodeName):
269 raise DefineError(
'testsuite "{0}" does not have an "{1}" action'.format(self.
namename, node.nodeName))
270 attr = getattr(self, node.nodeName)
271 return functools.partial(action_wrapper, attr, node)
275 Get a required attribute for a particular action. If the attribute is not
276 defined in the xml file, this will throw a DefineError. This will perform
277 the necessary substitution for the value of the attribute.
279 retval = self.
substitutesubstitute(node.getAttribute(name))
282 raise DefineError(
'attribute({0}) required for action({1})'.format(name, node.nodeName))
287 Gets an options attribute for a particular action. This will perform the
288 necessary substitution for the value of the attribute.
290 return self.
substitutesubstitute(node.getAttribute(name))
297 command [required]: the name of the process that will be executed
298 params [required]: the command line parameters passed to the command
300 This executes a shell command concurrently with the testing harness. This
301 starts the process, sleeps for a second and then checks the pid of the
302 process. The pid will be appended to the list of pid's in the definitions
303 map. This action cannot fail as it does not check any of the results of the
304 process that was created.
308 command = self.
requiredrequired(node,
'command')
309 params = self.
requiredrequired(node,
'params')
311 cmd = shlex.split(
"{0} {1}".format(command, params))
312 proc = subprocess.Popen(cmd, 0)
314 self.
subprosubpro.append(proc)
315 self.
definesdefines[
'pids'][str(len(self.
definesdefines[
'pids']))] = str(proc.pid)
324 command [required]: the name of the process that will be executed
325 params [required]: the command line parameters passed to the command
326 result [optional]: what the process should print to stdout
327 retval [optional]: what the exit value of the process should be
329 This executes a shell command synchronously with the testing harness. This
330 starts the process, grabs anything written to stdout by the process and the
331 return value of the process. If the results and retval attributes are
332 provided, these are compared with what the process printed/returned. If
333 the results or return value do not match, this will return False.
335 Returns True if the results and return value match those provided
337 command = self.
requiredrequired(node,
'command')
338 params = self.
requiredrequired(node,
'params')
339 expected = self.
optionaloptional(node,
'result')
340 retval = self.
optionaloptional(node,
'retval')
342 cmd =
"{0} {1}".format(command, params)
343 proc = subprocess.Popen(cmd, 0, shell =
True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
345 result = proc.stdout.readlines()
346 if len(result) != 0
and len(expected) != 0
and result[0].strip() != expected:
347 self.
failurefailure(doc, dest,
"ResultMismatch",
348 "expected: '{0}' != result: '{1}'".format(expected, result[0].strip()))
353 if len(retval) != 0
and proc.returncode != int(retval):
354 self.
failurefailure(doc, dest,
"IncorrectReturn",
"expected: {0} != result: {1}".format(retval, proc.returncode))
363 duration [require]: how long the test harness should sleep for
365 This action simply pauses execution of the test harness for duration
366 seconds. This action cannot fail and will always return True.
370 duration = node.getAttribute(
'duration')
371 time.sleep(int(duration))
379 directory [required]: the directory location of the fossology.conf file
381 This loads the configuration and VERSION data from the fossology.conf file
382 and the VERSION file. It puts the information in the definitions map.
386 dir = self.
requiredrequired(node,
'directory')
388 config = configparser.ConfigParser()
389 config.read_file(open(dir +
"/fossology.conf"))
391 self.
definesdefines[
"FOSSOLOGY"] = {}
392 self.
definesdefines[
"BUILD"] = {}
394 self.
definesdefines[
"FOSSOLOGY"][
"port"] = config.get(
"FOSSOLOGY",
"port")
395 self.
definesdefines[
"FOSSOLOGY"][
"path"] = config.get(
"FOSSOLOGY",
"path")
396 self.
definesdefines[
"FOSSOLOGY"][
"depth"] = config.get(
"FOSSOLOGY",
"depth")
398 config.read_file(open(dir +
"/VERSION"))
400 self.
definesdefines[
"BUILD"][
"VERSION"] = config.get(
"BUILD",
"VERSION")
401 self.
definesdefines[
"BUILD"][
"COMMIT_HASH"] = config.get(
"BUILD",
"COMMIT_HASH")
402 self.
definesdefines[
"BUILD"][
"BUILD_DATE"] = config.get(
"BUILD",
"BUILD_DATE")
406 def loop(self, node, doc, dest):
411 varname [required]: the name of the variable storing the current iteration
412 values [optional]: the values that the variable should take
413 iterations [optional]: the number of iterations the loop with take
415 This action actually executes the actions contained within it. This will loop
416 over the values specified or loop for the number of iterations specified. The
417 current value of the variable will be stored in the definitions mapping with
418 the value varname. While both "values" and "iterations" are optional
419 parameters, one of them is required to be provided.
421 varname = self.
requiredrequired(node,
'varname')
422 values = self.
optionaloptional(node,
'values')
423 iterations = self.
optionaloptional(node,
'iterations')
431 for value
in values.split(
','):
432 self.
definesdefines[varname] = value.strip()
433 for action
in actions:
434 ret = action(doc, dest)
438 for i
in range(int(iterations)):
439 self.
definesdefines[varname] = str(i)
440 for action
in actions:
441 ret = action(doc, dest)
445 del self.
definesdefines[varname]
446 return (tests, failed)
453 file [required]: the file that will be uploaded to the fossology database
455 This action uploads a new file into the fossology test(hopefully) database
456 so that an agent can work with it. This will place the upload_pk for the
457 file in the self.sefs map under the name ['upload_pk'][index] where the
458 index is the current number of elements in the ['upload_pk'] mapping. So the
459 upload_pk's for the files should showup in the order they were uploaded.
461 Returns True if and only if cp2foss succeeded
463 file = self.
requiredrequired(node,
'file')
465 cmd = self.
substitutesubstitute(
'{pwd}/cli/cp2foss -c {config} --user {user} --password {pass} ' + file)
466 proc = subprocess.Popen(cmd, 0, shell =
True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
469 if proc.returncode != 0:
472 result = proc.stdout.readlines()
473 if 'upload_pk' not in self.
definesdefines:
474 self.
definesdefines[
'upload_pk'] = {}
475 self.
definesdefines[
'upload_pk'][str(len(self.
definesdefines[
'upload_pk']))] = re.search(
r'\d+', result[-1]).group(0)
484 upload [required]: the index of the upload in the ['upload_pk'] mapping
485 agents [optional]: comma seperated list of agent to schedule. If this is
486 not specified, all agents will be scheduled
488 This action will schedule agents to run on a particular upload.
490 Returns True if and only if fossjobs succeeded
492 upload = self.
requiredrequired(node,
'upload')
493 agents = self.
optionaloptional(node,
'agents')
498 cmd = self.
substitutesubstitute(
'{pwd}/cli/fossjobs -c {config} --user {user} --password {pass} -U ' + upload +
' -A ' + agents)
499 proc = subprocess.Popen(cmd, 0, shell =
True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
502 if proc.returncode != 0:
511 sql [required]: the sql that will be exectued
516 This action will execute an sql statement on the relevant database. It can
517 check that the results of the sql were correct.
519 Returns True if results aren't expected or the results were correct
521 sql = self.
requiredrequired(node,
'sql')
523 cmd =
'psql --username={0} --host=localhost --dbname={1} --command="{2}" -tA'.format(
524 self.
definesdefines[
"dbuser"], self.
definesdefines[
'config'].split(
'/')[2], sql)
525 proc = subprocess.Popen(cmd, 0, shell =
True, stdout = subprocess.PIPE)
531 self.
dbresultdbresult = [str.split()
for str
in proc.stdout.readlines()]
533 ret = action(doc, dest)
539 return (total, failed)
546 row [required]: the row of the database results
547 col [required]: the column of the database results
548 val [required]: the expected value found at that row and column
550 checks if a particular row and column in the results of a database call are
551 an expected value. This fails if the correct value is not reported by the
554 returns True if the expected is the same as the result
556 row = int(self.
requiredrequired(node,
'row'))
557 col = int(self.
requiredrequired(node,
'col'))
558 val = self.
requiredrequired(node,
'val')
561 raise DefineError(
"dbresult action must be within a database action")
563 if len(result) <= row:
564 self.
failurefailure(doc, dest,
"DatabaseMismatch",
"Index out of bounds: {0} > {1}".format(row, len(result)))
566 if len(result[row]) <= col:
567 self.
failurefailure(doc, dest,
"DatabaseMismatch",
"Index out of bounds: {0} > {1}".format(col, len(result[row])))
569 if val != result[row][col]:
570 self.
failurefailure(doc, dest,
"DatabaseMismatch",
"[{2}, {3}]: expected: {0} != result: {1}".format(val, result[row][col], row, col))
580 Runs the tests and writes the output to the results document.
588 print(
"start up", end=
' ')
589 for action
in self.
setupsetup:
590 while action(
None,
None)[1] != 0:
593 print(
"tests", end=
' ')
594 for test
in self.
teststests:
596 testNode = document.createElement(
"testcase")
598 testNode.setAttribute(
"class", test[0])
599 testNode.setAttribute(
"name", test[0])
601 starttime = time.time()
602 for action
in test[1]:
603 res = action(document, testNode)
606 runtime = (time.time() - starttime)
608 testNode.setAttribute(
"assertions", str(assertions))
609 testNode.setAttribute(
"time", str(runtime))
613 totalasserts += assertions
615 suiteNode.appendChild(testNode)
617 print(
" clean up", end=
' ')
618 for action
in self.
cleanupcleanup:
621 for process
in self.
subprosubpro:
624 suiteNode.setAttribute(
"failures", str(failures))
625 suiteNode.setAttribute(
"tests", str(tests))
626 suiteNode.setAttribute(
"assertions", str(totalasserts))
635 Main entry point for the Functional tests
637 usage =
"usage: %prog [options]"
638 parser = OptionParser(usage = usage)
639 parser.add_option(
"-t",
"--tests", dest =
"testfile", help =
"The xml file to pull the tests from")
640 parser.add_option(
"-r",
"--results", dest =
"resultfile", help =
"The file to output the junit xml to" )
641 parser.add_option(
"-s",
"--specific", dest =
"specific", help =
"Only run the test with this particular name")
642 parser.add_option(
"-l",
"--longest",dest =
"skipLongTests",help =
"Skip test suites if there are expected to run longer than x time units")
644 (options, args) = parser.parse_args()
646 testFile = open(options.testfile)
647 dom = parseString(testFile.read())
655 for child
in dom.childNodes:
656 if child.nodeType == Node.COMMENT_NODE:
657 comment_list.append(child)
659 for node
in comment_list:
660 node.parentNode.removeChild(node)
662 setupNode = dom.firstChild.getElementsByTagName(
'setup')[0]
663 cleanupNode = dom.firstChild.getElementsByTagName(
'cleanup')[0]
665 resultsDoc = getDOMImplementation().createDocument(
None,
"testsuites",
None)
666 top_output = resultsDoc.documentElement
668 maxRuntime = int(dom.firstChild.getAttribute(
"timeout"))
670 for suite
in dom.firstChild.getElementsByTagName(
'testsuite'):
671 if options.specific
and suite.getAttribute(
"name") != options.specific:
673 if suite.hasAttribute(
"disable"):
674 print(suite.getAttribute(
"name"),
'::',
'disabled')
676 if options.skipLongTests
and suite.hasAttribute(
"longest")
and int(suite.getAttribute(
"longest"))>int(options.skipLongTests):
677 print(suite.getAttribute(
"name"),
'::',
'expected to run',suite.getAttribute(
"longest"),
'time units')
679 suiteNode = resultsDoc.createElement(
"testsuite")
682 suiteNode.setAttribute(
"name", suite.getAttribute(
"name"))
683 suiteNode.setAttribute(
"errors",
"0")
684 suiteNode.setAttribute(
"time",
"0")
689 setup = curr.createAllActions(setupNode)
690 cleanup = curr.createAllActions(cleanupNode)
692 curr.setup = setup + curr.setup
693 curr.cleanup = cleanup + curr.cleanup
695 starttime = time.time()
696 print(
"{0: >15} ::".format(suite.getAttribute(
"name")), end=
' ')
697 if not timeout(functools.partial(curr.performTests, suiteNode, resultsDoc, testFile.name), maxRuntime):
699 errorNode = resultsDoc.createElement(
"error")
700 errorNode.setAttribute(
"type",
"TimeOut")
701 errorNode.appendChild(resultsDoc.createTextNode(
"Test suite took too long to run."))
702 suiteNode.appendChild(errorNode)
703 runtime = (time.time() - starttime)
705 suiteNode.setAttribute(
"time", str(runtime))
707 except DefineError
as detail:
709 errorNode = resultsDoc.createElement(
"error")
710 errorNode.setAttribute(
"type",
"DefinitionError")
711 errorNode.appendChild(resultsDoc.createTextNode(
"DefineError: {0}".format(detail.value)))
712 suiteNode.appendChild(errorNode)
715 suiteNode.setAttribute(
"errors", str(errors))
716 top_output.appendChild(suiteNode)
720 output = open(options.resultfile,
'w')
721 resultsDoc.writexml(output,
"",
" ",
"\n")
726 if __name__ ==
"__main__":
def __init__(self, value)
class that handles running a test suite ####################################
def dbequal(self, node, doc, dest)
def schedule(self, node, doc, dest)
def performTests(self, suiteNode, document, fname)
run tests and produce output #
def failure(self, doc, dest, type, value)
def processVariable(self, node, match)
def concurrently(self, node, doc, dest)
def createAllActions(self, node)
actions that tests can take #
def loop(self, node, doc, dest)
def sequential(self, node, doc, dest)
def substitute(self, string, node=None)
def optional(self, node, name)
def upload(self, node, doc, dest)
def required(self, node, name)
def loadConf(self, node, doc, dest)
def database(self, node, doc, dest)
def createAction(self, node)
def sleep(self, node, doc, dest)