13 ============================================================================== 14 Copyright (C) 2012 Hewlett-Packard Development Company, L.P. 16 This program is free software; you can redistribute it and/or 17 modify it under the terms of the GNU General Public License 18 version 2 as published by the Free Software Foundation. 20 This program is distributed in the hope that it will be useful, 21 but WITHOUT ANY WARRANTY; without even the implied warranty of 22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 GNU General Public License for more details. 25 You should have received a copy of the GNU General Public License along 26 with this program; if not, write to the Free Software Foundation, Inc., 27 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 28 ============================================================================== 31 from xml.dom.minidom
import getDOMImplementation
32 from xml.dom.minidom
import parseString
33 from xml.dom
import Node
34 from optparse
import OptionParser
47 defsReplace = re.compile(
'{([^{}]*)}')
48 defsSplit = re.compile(
'([^\s]+):([^\s]+)')
51 """ Error class used for missing definitions in the xml file """ 57 return repr(self.
value)
60 """ Error class used when a test suite takes too long to run """ 63 def timeout(func, maxRuntime):
72 def timeout_handler(signum, frame):
75 signal.signal(signal.SIGALRM, timeout_handler)
76 signal.alarm(maxRuntime)
90 The testsuite class is used to deserialize a test suite from the xml file, 91 run the tests and report the results to another xml document. 93 name the name of the test suite 94 defs a map of strings to values used to do variables replacement 95 setup list of Actions that will be taken before running the tests 96 cleanup list of Actions that will be taken after running the tests 97 tests list of Actions that are the actual tests 98 subpro list of processes that are running concurrently with the tests 103 Constructor for the testsuite class. This will deserialize the testsuite 104 from the xml file that describes all the tests. For each element in the 105 setup, and cleanup and action will be created. For each element under each 106 <test></test> tag an action will be created. 108 This will also grab the definitions of variables for the self.defines map. The 109 variable substitution will be performed when the definition is loaded from 115 defNode = node.getElementsByTagName(
'definitions')[0]
116 definitions = defNode.attributes
118 self.
name = node.getAttribute(
'name')
124 for i
in xrange(definitions.length):
125 if definitions.item(i).name
not in self.
defines:
126 self.
defines[definitions.item(i).name] = self.
substitute(definitions.item(i).value, defNode)
135 if len(node.getElementsByTagName(
'setup')) != 0:
136 setup = node.getElementsByTagName(
'setup')[0]
137 for action
in [curr
for curr
in setup.childNodes
if curr.nodeType == Node.ELEMENT_NODE]:
141 if len(node.getElementsByTagName(
'cleanup')) != 0:
142 cleanup = node.getElementsByTagName(
'cleanup')[0]
143 for action
in [curr
for curr
in cleanup.childNodes
if curr.nodeType == Node.ELEMENT_NODE]:
147 for test
in node.getElementsByTagName(
'test'):
148 newTest = (test.getAttribute(
'name'), [])
149 for action
in [curr
for curr
in test.childNodes
if curr.nodeType == Node.ELEMENT_NODE]:
151 self.tests.append(newTest)
155 Simple function to make calling processVariable a lot cleaner 157 Returns the string with the variables correctly substituted 159 while defsReplace.search(string):
160 string = defsReplace.sub(functools.partial(self.
processVariable, node), string)
165 Function passed to the regular expression library to replace variables in a 166 string from the xml file. 169 The regular expression used is "{([^\s]*?)}". This will match anything that 170 doesn't contain any whitespace and falls between two curly braces. For 171 example "{hello}" will match, but "{hello goodbye}" and "hello" will not. 173 Any variable name that starts with a "$" has a special meaning. The text 174 following the "$" will be used as a shell command and executed. The 175 "{$text}" will be replaced with the output of the shell command. For example 176 "{$pwd}" will be replaced with the output of the shell command "pwd". 178 If a variable has a ":" in it, anything that follows the ":" will be used 179 to index into the associative array in the definitions map. For example 180 "{pids:0}" will access the element that is mapped to the string "0" in the 181 associative array that is as the mapped to the string "pids" in the defs 184 Returns the replacement string 186 name = match.group(1)
190 process = os.popen(name[1:],
'r') 196 arrayMatch = defsSplit.match(name)
198 name = arrayMatch.group(1)
199 index = self.
substitute(arrayMatch.group(2), node)
201 if not isinstance(self.
defines[name], dict):
202 raise DefineError(
'"{0}" is not a dictionary in testsuite "{1}"'.format(name, self.
name))
204 if node
and node.hasAttribute(name):
207 raise DefineError(
'"{0}" not defined in testsuite "{1}"'.format(name, self.
name))
208 if index
not in self.
defines[name]:
209 raise DefineError(
'"{0}" is out of bounds for "{1}.{2}"'.format(index, self.
name, name))
210 return self.
defines[name][arrayMatch.group(2)]
214 if node
and node.hasAttribute(name):
217 raise DefineError(
'"{0}" not defined in testsuite "{1}"'.format(name, self.
name))
222 Puts a failure node into an the results document 227 fail = doc.createElement(
'failure')
228 fail.setAttribute(
'type', type)
230 text = doc.createTextNode(value)
231 fail.appendChild(text)
233 dest.appendChild(fail)
241 Creates all the child actions for a particular node in the xml file. 243 return [self.
createAction(child)
for child
in node.childNodes
if child.nodeType == Node.ELEMENT_NODE]
247 Creates an action given a particular test suite and xml node. This uses 248 simple python reflection to get the method of the testsuite class that has 249 the same name as the xml node tag. The action is a functor that can be 250 called later be another part of the test harness. 252 To write a new type of action write a function with the signature: 253 actionName(self, source_node, xml_document, destination_node) 255 * The source_node is the xml node that described the action, this node 256 should describe everything that is necessary for the action to be 257 performed. This is passed to the action when the action is created. 258 * The xml_document is the document that the test results are being written 259 to. This is passed to the action when it is called, not during creation. 260 * The destination_node is the node in the results xml document that this 261 particular action should be writing its results to. This is passed in when 262 the action is called, not during creation. 264 The action should return the number of tests that it ran and the number of 265 failures that it experienced. A failing action has different meanings 266 during different parts of the code. During setup, a failing action 267 indicates that the setup is not ready to proceed. Failing actions during 268 setup will be called repeatedly once every five seconds until they no 269 longer register a failure. Failing actions during testing indicate a 270 failing test. The failure will be reported to results document, but the 271 action should still call the failure method to indicate in the results 272 document why the failure happened. During cleanup what an action returns is 275 Returns the new action 277 def action_wrapper(action, node, doc, dest):
279 return action(node, doc, dest)
281 if not hasattr(self, node.nodeName):
282 raise DefineError(
'testsuite "{0}" does not have an "{1}" action'.format(self.
name, node.nodeName))
283 attr = getattr(self, node.nodeName)
284 return functools.partial(action_wrapper, attr, node)
288 Get a required attribute for a particular action. If the attribute is not 289 defined in the xml file, this will throw a DefineError. This will perform 290 the necessary substitution for the value of the attribute. 292 retval = self.
substitute(node.getAttribute(name))
295 raise DefineError(
'attribute({0}) required for action({1})'.format(name, node.nodeName))
300 Gets an options attribute for a particular action. This will perform the 301 necessary substitution for the value of the attribute. 303 return self.
substitute(node.getAttribute(name))
310 command [required]: the name of the process that will be executed 311 params [required]: the command line parameters passed to the command 313 This executes a shell command concurrently with the testing harness. This 314 starts the process, sleeps for a second and then checks the pid of the 315 process. The pid will be appended to the list of pid's in the definitions 316 map. This action cannot fail as it does not check any of the results of the 317 process that was created. 321 command = self.
required(node,
'command')
322 params = self.
required(node,
'params')
324 cmd = shlex.split(
"{0} {1}".format(command, params))
325 proc = subprocess.Popen(cmd, 0)
327 self.subpro.append(proc)
328 self.
defines[
'pids'][str(len(self.
defines[
'pids']))] = str(proc.pid)
337 command [required]: the name of the process that will be executed 338 params [required]: the command line parameters passed to the command 339 result [optional]: what the process should print to stdout 340 retval [optional]: what the exit value of the process should be 342 This executes a shell command synchronously with the testing harness. This 343 starts the process, grabs anything written to stdout by the process and the 344 return value of the process. If the results and retval attributes are 345 provided, these are compared with what the process printed/returned. If 346 the results or return value do not match, this will return False. 348 Returns True if the results and return value match those provided 350 command = self.
required(node,
'command')
351 params = self.
required(node,
'params')
352 expected = self.
optional(node,
'result')
353 retval = self.
optional(node,
'retval')
355 cmd =
"{0} {1}".format(command, params)
356 proc = subprocess.Popen(cmd, 0, shell =
True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
358 result = proc.stdout.readlines()
359 if len(result) != 0
and len(expected) != 0
and result[0].strip() != expected:
360 self.
failure(doc, dest,
"ResultMismatch",
361 "expected: '{0}' != result: '{1}'".format(expected, result[0].strip()))
366 if len(retval) != 0
and proc.returncode != int(retval):
367 self.
failure(doc, dest,
"IncorrectReturn",
"expected: {0} != result: {1}".format(retval, proc.returncode))
376 duration [require]: how long the test harness should sleep for 378 This action simply pauses execution of the test harness for duration 379 seconds. This action cannot fail and will always return True. 383 duration = node.getAttribute(
'duration')
384 time.sleep(int(duration))
392 directory [required]: the directory location of the fossology.conf file 394 This loads the configuration and VERSION data from the fossology.conf file 395 and the VERSION file. It puts the information in the definitions map. 399 dir = self.
required(node,
'directory')
401 config = ConfigParser.ConfigParser()
402 config.readfp(open(dir +
"/fossology.conf"))
407 self.
defines[
"FOSSOLOGY"][
"port"] = config.get(
"FOSSOLOGY",
"port")
408 self.
defines[
"FOSSOLOGY"][
"path"] = config.get(
"FOSSOLOGY",
"path")
409 self.
defines[
"FOSSOLOGY"][
"depth"] = config.get(
"FOSSOLOGY",
"depth")
411 config.readfp(open(dir +
"/VERSION"))
413 self.
defines[
"BUILD"][
"VERSION"] = config.get(
"BUILD",
"VERSION")
414 self.
defines[
"BUILD"][
"COMMIT_HASH"] = config.get(
"BUILD",
"COMMIT_HASH")
415 self.
defines[
"BUILD"][
"BUILD_DATE"] = config.get(
"BUILD",
"BUILD_DATE")
419 def loop(self, node, doc, dest):
424 varname [required]: the name of the variable storing the current iteration 425 values [optional]: the values that the variable should take 426 iterations [optional]: the number of iterations the loop with take 428 This action actually executes the actions contained within it. This will loop 429 over the values specified or loop for the number of iterations specified. The 430 current value of the variable will be stored in the definitions mapping with 431 the value varname. While both "values" and "iterations" are optional 432 parameters, one of them is required to be provided. 434 varname = self.
required(node,
'varname')
435 values = self.
optional(node,
'values')
436 iterations = self.
optional(node,
'iterations')
444 for value
in values.split(
','):
445 self.
defines[varname] = value.strip()
446 for action
in actions:
447 ret = action(doc, dest)
451 for i
in xrange(int(iterations)):
453 for action
in actions:
454 ret = action(doc, dest)
459 return (tests, failed)
466 file [required]: the file that will be uploaded to the fossology database 468 This action uploads a new file into the fossology test(hopefully) database 469 so that an agent can work with it. This will place the upload_pk for the 470 file in the self.sefs map under the name ['upload_pk'][index] where the 471 index is the current number of elements in the ['upload_pk'] mapping. So the 472 upload_pk's for the files should showup in the order they were uploaded. 474 Returns True if and only if cp2foss succeeded 478 cmd = self.
substitute(
'{pwd}/cli/cp2foss -c {config} --user {user} --password {pass} ' + file)
479 proc = subprocess.Popen(cmd, 0, shell =
True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
482 if proc.returncode != 0:
485 result = proc.stdout.readlines()
486 if 'upload_pk' not in self.
defines:
488 self.
defines[
'upload_pk'][str(len(self.
defines[
'upload_pk']))] = re.search(
r'\d+', result[-1]).group(0)
497 upload [required]: the index of the upload in the ['upload_pk'] mapping 498 agents [optional]: comma seperated list of agent to schedule. If this is 499 not specified, all agents will be scheduled 501 This action will schedule agents to run on a particular upload. 503 Returns True if and only if fossjobs succeeded 505 upload = self.
required(node,
'upload')
506 agents = self.
optional(node,
'agents')
511 cmd = self.
substitute(
'{pwd}/cli/fossjobs -c {config} --user {user} --password {pass} -U ' + upload +
' -A ' + agents)
512 proc = subprocess.Popen(cmd, 0, shell =
True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
515 if proc.returncode != 0:
524 sql [required]: the sql that will be exectued 529 This action will execute an sql statement on the relevant database. It can 530 check that the results of the sql were correct. 532 Returns True if results aren't expected or the results were correct 536 cmd =
'psql --username={0} --host=localhost --dbname={1} --command="{2}" -tA'.format(
537 self.
defines[
"dbuser"], self.
defines[
'config'].split(
'/')[2], sql)
538 proc = subprocess.Popen(cmd, 0, shell =
True, stdout = subprocess.PIPE)
544 self.
dbresult = [str.split()
for str
in proc.stdout.readlines()]
546 ret = action(doc, dest)
552 return (total, failed)
559 row [required]: the row of the database results 560 col [required]: the column of the database results 561 val [required]: the expected value found at that row and column 563 checks if a particular row and column in the results of a database call are 564 an expected value. This fails if the correct value is not reported by the 567 returns True if the expected is the same as the result 569 row = int(self.
required(node,
'row'))
570 col = int(self.
required(node,
'col'))
574 raise DefineError(
"dbresult action must be within a database action")
576 if len(result) <= row:
577 self.
failure(doc, dest,
"DatabaseMismatch",
"Index out of bounds: {0} > {1}".format(row, len(result)))
579 if len(result[row]) <= col:
580 self.
failure(doc, dest,
"DatabaseMismatch",
"Index out of bounds: {0} > {1}".format(col, len(result[row])))
582 if val != result[row][col]:
583 self.
failure(doc, dest,
"DatabaseMismatch",
"[{2}, {3}]: expected: {0} != result: {1}".format(val, result[row][col], row, col))
593 Runs the tests and writes the output to the results document. 602 for action
in self.
setup:
603 while action(
None,
None)[1] != 0:
607 for test
in self.
tests:
609 testNode = document.createElement(
"testcase")
611 testNode.setAttribute(
"class", test[0])
612 testNode.setAttribute(
"name", test[0])
614 starttime = time.time()
615 for action
in test[1]:
616 res = action(document, testNode)
619 runtime = (time.time() - starttime)
621 testNode.setAttribute(
"assertions", str(assertions))
622 testNode.setAttribute(
"time", str(runtime))
626 totalasserts += assertions
628 suiteNode.appendChild(testNode)
634 for process
in self.
subpro:
637 suiteNode.setAttribute(
"failures", str(failures))
638 suiteNode.setAttribute(
"tests", str(tests))
639 suiteNode.setAttribute(
"assertions", str(totalasserts))
648 Main entry point for the Functional tests 650 usage =
"usage: %prog [options]" 651 parser = OptionParser(usage = usage)
652 parser.add_option(
"-t",
"--tests", dest =
"testfile", help =
"The xml file to pull the tests from")
653 parser.add_option(
"-r",
"--results", dest =
"resultfile", help =
"The file to output the junit xml to" )
654 parser.add_option(
"-s",
"--specific", dest =
"specific", help =
"Only run the test with this particular name")
655 parser.add_option(
"-l",
"--longest",dest =
"skipLongTests",help =
"Skip test suites if there are expected to run longer than x time units")
657 (options, args) = parser.parse_args()
659 testFile = open(options.testfile)
660 dom = parseString(testFile.read())
668 for child
in dom.childNodes:
669 if child.nodeType == Node.COMMENT_NODE:
670 comment_list.append(child)
672 for node
in comment_list:
673 node.parentNode.removeChild(node)
675 setupNode = dom.firstChild.getElementsByTagName(
'setup')[0]
676 cleanupNode = dom.firstChild.getElementsByTagName(
'cleanup')[0]
678 resultsDoc = getDOMImplementation().createDocument(
None,
"testsuites",
None)
679 top_output = resultsDoc.documentElement
681 maxRuntime = int(dom.firstChild.getAttribute(
"timeout"))
683 for suite
in dom.firstChild.getElementsByTagName(
'testsuite'):
684 if options.specific
and suite.getAttribute(
"name") != options.specific:
686 if suite.hasAttribute(
"disable"):
687 print suite.getAttribute(
"name"),
'::',
'disabled' 689 if options.skipLongTests
and suite.hasAttribute(
"longest")
and int(suite.getAttribute(
"longest"))>int(options.skipLongTests):
690 print suite.getAttribute(
"name"),
'::',
'expected to run',suite.getAttribute(
"longest"),
'time units' 692 suiteNode = resultsDoc.createElement(
"testsuite")
695 suiteNode.setAttribute(
"name", suite.getAttribute(
"name"))
696 suiteNode.setAttribute(
"errors",
"0")
697 suiteNode.setAttribute(
"time",
"0")
702 setup = curr.createAllActions(setupNode)
703 cleanup = curr.createAllActions(cleanupNode)
705 curr.setup = setup + curr.setup
706 curr.cleanup = cleanup + curr.cleanup
708 starttime = time.time()
709 print "{0: >15} ::".format(suite.getAttribute(
"name")),
710 if not timeout(functools.partial(curr.performTests, suiteNode, resultsDoc, testFile.name), maxRuntime):
712 errorNode = resultsDoc.createElement(
"error")
713 errorNode.setAttribute(
"type",
"TimeOut")
714 errorNode.appendChild(resultsDoc.createTextNode(
"Test suite took too long to run."))
715 suiteNode.appendChild(errorNode)
716 runtime = (time.time() - starttime)
718 suiteNode.setAttribute(
"time", str(runtime))
720 except DefineError
as detail:
722 errorNode = resultsDoc.createElement(
"error")
723 errorNode.setAttribute(
"type",
"DefinitionError")
724 errorNode.appendChild(resultsDoc.createTextNode(
"DefineError: {0}".format(detail.value)))
725 suiteNode.appendChild(errorNode)
728 suiteNode.setAttribute(
"errors", str(errors))
729 top_output.appendChild(suiteNode)
733 output = open(options.resultfile,
'w')
734 resultsDoc.writexml(output,
"",
" ",
"\n")
739 if __name__ ==
"__main__":
def sleep(self, node, doc, dest)
def optional(self, node, name)
def processVariable(self, node, match)
def schedule(self, node, doc, dest)
def createAllActions(self, node)
actions that tests can take #
def concurrently(self, node, doc, dest)
def substitute(self, string, node=None)
def sequential(self, node, doc, dest)
def upload(self, node, doc, dest)
def performTests(self, suiteNode, document, fname)
run tests and produce output #
def required(self, node, name)
def createAction(self, node)
class that handles running a test suite ####################################
def loadConf(self, node, doc, dest)
def __init__(self, value)
def failure(self, doc, dest, type, value)
def dbequal(self, node, doc, dest)
def database(self, node, doc, dest)
def loop(self, node, doc, dest)