FOSSology  3.2.0rc1
Open Source License Compliance by Open Source Software
Functional.py
Go to the documentation of this file.
1 #!/usr/bin/python -u
2 
11 
12 """
13  ==============================================================================
14  Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
15 
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.
19 
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.
24 
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  ==============================================================================
29 """
30 
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
35 import ConfigParser
36 import subprocess
37 import functools
38 import signal
39 import shlex
40 import time
41 import re
42 import os
43 
44 ################################################################################
45 ### utility ####################################################################
46 ################################################################################
47 defsReplace = re.compile('{([^{}]*)}')
48 defsSplit = re.compile('([^\s]+):([^\s]+)')
49 
50 class DefineError(Exception):
51  """ Error class used for missing definitions in the xml file """
52  def __init__(self, value):
53  """ Constructor """
54  self.value = value
55  def __str__(self):
56  """ To string """
57  return repr(self.value)
58 
59 class TimeoutError(Exception):
60  """ Error class used when a test suite takes too long to run """
61  pass
62 
63 def timeout(func, maxRuntime):
64 
71 
72  def timeout_handler(signum, frame):
73  raise TimeoutError()
74 
75  signal.signal(signal.SIGALRM, timeout_handler)
76  signal.alarm(maxRuntime)
77 
78  try:
79  func()
80  except TimeoutError:
81  return False
82  return True
83 
84 ################################################################################
85 ### class that handles running a test suite ####################################
86 ################################################################################
87 
88 class testsuite:
89  """
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.
92 
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
99  """
100 
101  def __init__(self, node):
102  """
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.
107 
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
110  the file.
111 
112  Returns nothing
113  """
114 
115  defNode = node.getElementsByTagName('definitions')[0]
116  definitions = defNode.attributes
117 
118  self.name = node.getAttribute('name')
119 
120  self.defines = {}
121  self.defines['pids'] = {}
122 
123  # get variable definitions
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)
127 
128  self.setup = []
129  self.cleanup = []
130  self.tests = []
131  self.subpro = []
132  self.dbresult = None
133 
134  # parse all actions that will be taken during the setup phase
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]:
138  self.setup.append(self.createAction(action))
139 
140  # parse all actions that will be taken during the cleanup phase
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]:
144  self.cleanup.append(self.createAction(action))
145 
146  # parse all actions that will be taken during the testing phase
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]:
150  newTest[1].append(self.createAction(action))
151  self.tests.append(newTest)
152 
153  def substitute(self, string, node = None):
154  """
155  Simple function to make calling processVariable a lot cleaner
156 
157  Returns the string with the variables correctly substituted
158  """
159  while defsReplace.search(string):
160  string = defsReplace.sub(functools.partial(self.processVariable, node), string)
161  return string
162 
163  def processVariable(self, node, match):
164  """
165  Function passed to the regular expression library to replace variables in a
166  string from the xml file.
167  doc, dest, "")
168  return (1, 0)
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.
172 
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".
177 
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
182  map.
183 
184  Returns the replacement string
185  """
186  name = match.group(1)
187 
188  # variable begins with $, replace with output of shell command
189  if name[0] == '$':
190  process = os.popen(name[1:], 'r')
191  ret = process.read()
192  process.close()
193  return ret[:-1]
194 
195  # variable contains a ":", access defs[name] as an associative array or dictionary
196  arrayMatch = defsSplit.match(name)
197  if arrayMatch:
198  name = arrayMatch.group(1)
199  index = self.substitute(arrayMatch.group(2), node)
200 
201  if not isinstance(self.defines[name], dict):
202  raise DefineError('"{0}" is not a dictionary in testsuite "{1}"'.format(name, self.name))
203  if name not in self.defines:
204  if node and node.hasAttribute(name):
205  self.defines[name] = self.substitute(node.getAttribute(name))
206  else:
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)]
211 
212  # this is a simply definition access, check validity and return the result
213  if name not in self.defines:
214  if node and node.hasAttribute(name):
215  self.defines[name] = self.substitute(node.getAttribute(name), node)
216  else:
217  raise DefineError('"{0}" not defined in testsuite "{1}"'.format(name, self.name))
218  return self.defines[name]
219 
220  def failure(self, doc, dest, type, value):
221  """
222  Puts a failure node into an the results document
223 
224  Return nothing
225  """
226  if doc and dest:
227  fail = doc.createElement('failure')
228  fail.setAttribute('type', type)
229 
230  text = doc.createTextNode(value)
231  fail.appendChild(text)
232 
233  dest.appendChild(fail)
234 
235  ###############################
236  # actions that tests can take #
237  ###############################
238 
239  def createAllActions(self, node):
240  """
241  Creates all the child actions for a particular node in the xml file.
242  """
243  return [self.createAction(child) for child in node.childNodes if child.nodeType == Node.ELEMENT_NODE]
244 
245  def createAction(self, node):
246  """
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.
251 
252  To write a new type of action write a function with the signature:
253  actionName(self, source_node, xml_document, destination_node)
254 
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.
263 
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
273  ignored.
274 
275  Returns the new action
276  """
277  def action_wrapper(action, node, doc, dest):
278  print '.',# node.nodeName,
279  return action(node, doc, dest)
280 
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)
285 
286  def required(self, node, name):
287  """
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.
291  """
292  retval = self.substitute(node.getAttribute(name))
293 
294  if not retval:
295  raise DefineError('attribute({0}) required for action({1})'.format(name, node.nodeName))
296  return retval
297 
298  def optional(self, node, name):
299  """
300  Gets an options attribute for a particular action. This will perform the
301  necessary substitution for the value of the attribute.
302  """
303  return self.substitute(node.getAttribute(name))
304 
305  def concurrently(self, node, doc, dest):
306  """
307  Action
308 
309  Attributes:
310  command [required]: the name of the process that will be executed
311  params [required]: the command line parameters passed to the command
312 
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.
318 
319  Returns True
320  """
321  command = self.required(node, 'command')
322  params = self.required(node, 'params')
323 
324  cmd = shlex.split("{0} {1}".format(command, params))
325  proc = subprocess.Popen(cmd, 0)
326  time.sleep(1)
327  self.subpro.append(proc)
328  self.defines['pids'][str(len(self.defines['pids']))] = str(proc.pid)
329 
330  return (1, 0)
331 
332  def sequential(self, node, doc, dest):
333  """
334  Action
335 
336  Attributes:
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
341 
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.
347 
348  Returns True if the results and return value match those provided
349  """
350  command = self.required(node, 'command')
351  params = self.required(node, 'params')
352  expected = self.optional(node, 'result')
353  retval = self.optional(node, 'retval')
354 
355  cmd = "{0} {1}".format(command, params)
356  proc = subprocess.Popen(cmd, 0, shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
357 
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()))
362  return (1, 1)
363 
364  proc.wait()
365 
366  if len(retval) != 0 and proc.returncode != int(retval):
367  self.failure(doc, dest, "IncorrectReturn", "expected: {0} != result: {1}".format(retval, proc.returncode))
368  return (1, 1)
369  return (1, 0)
370 
371  def sleep(self, node, doc, dest):
372  """
373  Action
374 
375  Attributes:
376  duration [require]: how long the test harness should sleep for
377 
378  This action simply pauses execution of the test harness for duration
379  seconds. This action cannot fail and will always return True.
380 
381  Returns True
382  """
383  duration = node.getAttribute('duration')
384  time.sleep(int(duration))
385  return (1, 0)
386 
387  def loadConf(self, node, doc, dest):
388  """
389  Action
390 
391  Attributes:
392  directory [required]: the directory location of the fossology.conf file
393 
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.
396 
397  Returns True
398  """
399  dir = self.required(node, 'directory')
400 
401  config = ConfigParser.ConfigParser()
402  config.readfp(open(dir + "/fossology.conf"))
403 
404  self.defines["FOSSOLOGY"] = {}
405  self.defines["BUILD"] = {}
406 
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")
410 
411  config.readfp(open(dir + "/VERSION"))
412 
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")
416 
417  return (1, 0)
418 
419  def loop(self, node, doc, dest):
420  """
421  Action
422 
423  Attributes:
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
427 
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.
433  """
434  varname = self.required(node, 'varname')
435  values = self.optional(node, 'values')
436  iterations = self.optional(node, 'iterations')
437 
438  actions = self.createAllActions(node)
439 
440  tests = 0
441  failed = 0
442 
443  if values:
444  for value in values.split(','):
445  self.defines[varname] = value.strip()
446  for action in actions:
447  ret = action(doc, dest)
448  tests += ret[0]
449  failed += ret[1]
450  else:
451  for i in xrange(int(iterations)):
452  self.defines[varname] = str(i)
453  for action in actions:
454  ret = action(doc, dest)
455  tests += ret[0]
456  failed += ret[1]
457 
458  del self.defines[varname]
459  return (tests, failed)
460 
461  def upload(self, node, doc, dest):
462  """
463  Action
464 
465  Attributes:
466  file [required]: the file that will be uploaded to the fossology database
467 
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.
473 
474  Returns True if and only if cp2foss succeeded
475  """
476  file = self.required(node, 'file')
477 
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)
480  proc.wait()
481 
482  if proc.returncode != 0:
483  return (1, 1)
484 
485  result = proc.stdout.readlines()
486  if 'upload_pk' not in self.defines:
487  self.defines['upload_pk'] = {}
488  self.defines['upload_pk'][str(len(self.defines['upload_pk']))] = re.search(r'\d+', result[-1]).group(0)
489 
490  return (1, 0)
491 
492  def schedule(self, node ,doc, dest):
493  """
494  Action
495 
496  Attributes:
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
500 
501  This action will schedule agents to run on a particular upload.
502 
503  Returns True if and only if fossjobs succeeded
504  """
505  upload = self.required(node, 'upload')
506  agents = self.optional(node, 'agents')
507 
508  if not agents:
509  agents = ""
510 
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)
513  proc.wait()
514 
515  if proc.returncode != 0:
516  return (1, 1)
517  return (1, 0)
518 
519  def database(self, node, doc, dest):
520  """
521  Action
522 
523  Attributes:
524  sql [required]: the sql that will be exectued
525 
526  Sub nodes:
527 
528 
529  This action will execute an sql statement on the relevant database. It can
530  check that the results of the sql were correct.
531 
532  Returns True if results aren't expected or the results were correct
533  """
534  sql = self.required(node, 'sql')
535 
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)
539  proc.wait()
540 
541  total = 0
542  failed = 0
543 
544  self.dbresult = [str.split() for str in proc.stdout.readlines()]
545  for action in self.createAllActions(node):
546  ret = action(doc, dest)
547  total += ret[0]
548  failed += ret[1]
549 
550  del self.dbresult
551  self.dbresult = None
552  return (total, failed)
553 
554  def dbequal(self, node, doc, dest):
555  """
556  Action
557 
558  Attributes:
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
562 
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
565  database.
566 
567  returns True if the expected is the same as the result
568  """
569  row = int(self.required(node, 'row'))
570  col = int(self.required(node, 'col'))
571  val = self.required(node, 'val')
572 
573  if not self.dbresult:
574  raise DefineError("dbresult action must be within a database action")
575  result = self.dbresult
576  if len(result) <= row:
577  self.failure(doc, dest, "DatabaseMismatch", "Index out of bounds: {0} > {1}".format(row, len(result)))
578  return (1, 1)
579  if len(result[row]) <= col:
580  self.failure(doc, dest, "DatabaseMismatch", "Index out of bounds: {0} > {1}".format(col, len(result[row])))
581  return (1, 1)
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))
584  return (1, 1)
585  return (1, 0)
586 
587  ################################
588  # run tests and produce output #
589  ################################
590 
591  def performTests(self, suiteNode, document, fname):
592  """
593  Runs the tests and writes the output to the results document.
594 
595  Returns nothing
596  """
597  failures = 0
598  tests = 0
599  totalasserts = 0
600 
601  print "start up",
602  for action in self.setup:
603  while action(None, None)[1] != 0:
604  time.sleep(5)
605 
606  print "tests",
607  for test in self.tests:
608  assertions = 0
609  testNode = document.createElement("testcase")
610 
611  testNode.setAttribute("class", test[0])
612  testNode.setAttribute("name", test[0])
613 
614  starttime = time.time()
615  for action in test[1]:
616  res = action(document, testNode)
617  assertions += res[0]
618  failures += res[1]
619  runtime = (time.time() - starttime)
620 
621  testNode.setAttribute("assertions", str(assertions))
622  testNode.setAttribute("time", str(runtime))
623 
624  tests += 1
625 
626  totalasserts += assertions
627 
628  suiteNode.appendChild(testNode)
629 
630  print " clean up",
631  for action in self.cleanup:
632  action(None, None)
633 
634  for process in self.subpro:
635  process.wait()
636 
637  suiteNode.setAttribute("failures", str(failures))
638  suiteNode.setAttribute("tests", str(tests))
639  suiteNode.setAttribute("assertions", str(totalasserts))
640  print
641 
642 ################################################################################
643 ### MAIN #######################################################################
644 ################################################################################
645 
646 def main():
647  """
648  Main entry point for the Functional tests
649  """
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")
656 
657  (options, args) = parser.parse_args()
658 
659  testFile = open(options.testfile)
660  dom = parseString(testFile.read())
661  dir = os.getcwd()
662 
663  os.chdir('../../..')
664 
665  # remove all of the comment nodes, since the code assumes
666  # that there are no comment nodes within the XML
667  comment_list = []
668  for child in dom.childNodes:
669  if child.nodeType == Node.COMMENT_NODE:
670  comment_list.append(child)
671 
672  for node in comment_list:
673  node.parentNode.removeChild(node)
674 
675  setupNode = dom.firstChild.getElementsByTagName('setup')[0]
676  cleanupNode = dom.firstChild.getElementsByTagName('cleanup')[0]
677 
678  resultsDoc = getDOMImplementation().createDocument(None, "testsuites", None)
679  top_output = resultsDoc.documentElement
680 
681  maxRuntime = int(dom.firstChild.getAttribute("timeout"))
682 
683  for suite in dom.firstChild.getElementsByTagName('testsuite'):
684  if options.specific and suite.getAttribute("name") != options.specific:
685  continue
686  if suite.hasAttribute("disable"):
687  print suite.getAttribute("name"),'::','disabled'
688  continue
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'
691  continue
692  suiteNode = resultsDoc.createElement("testsuite")
693  errors = 0
694 
695  suiteNode.setAttribute("name", suite.getAttribute("name"))
696  suiteNode.setAttribute("errors", "0")
697  suiteNode.setAttribute("time", "0")
698 
699  try:
700  curr = testsuite(suite)
701 
702  setup = curr.createAllActions(setupNode)
703  cleanup = curr.createAllActions(cleanupNode)
704 
705  curr.setup = setup + curr.setup
706  curr.cleanup = cleanup + curr.cleanup
707 
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):
711  errors += 1
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)
717 
718  suiteNode.setAttribute("time", str(runtime))
719 
720  except DefineError as detail:
721  errors += 1
722  errorNode = resultsDoc.createElement("error")
723  errorNode.setAttribute("type", "DefinitionError")
724  errorNode.appendChild(resultsDoc.createTextNode("DefineError: {0}".format(detail.value)))
725  suiteNode.appendChild(errorNode)
726 
727  finally:
728  suiteNode.setAttribute("errors", str(errors))
729  top_output.appendChild(suiteNode)
730 
731  os.chdir(dir);
732 
733  output = open(options.resultfile, 'w')
734  resultsDoc.writexml(output, "", " ", "\n")
735  output.close()
736 
737  os.chdir(dir)
738 
739 if __name__ == "__main__":
740  main()
def sleep(self, node, doc, dest)
Definition: Functional.py:371
def optional(self, node, name)
Definition: Functional.py:298
def processVariable(self, node, match)
Definition: Functional.py:163
def schedule(self, node, doc, dest)
Definition: Functional.py:492
def createAllActions(self, node)
actions that tests can take #
Definition: Functional.py:239
def concurrently(self, node, doc, dest)
Definition: Functional.py:305
def substitute(self, string, node=None)
Definition: Functional.py:153
def sequential(self, node, doc, dest)
Definition: Functional.py:332
def upload(self, node, doc, dest)
Definition: Functional.py:461
def performTests(self, suiteNode, document, fname)
run tests and produce output #
Definition: Functional.py:591
def required(self, node, name)
Definition: Functional.py:286
def createAction(self, node)
Definition: Functional.py:245
class that handles running a test suite ####################################
Definition: Functional.py:88
def loadConf(self, node, doc, dest)
Definition: Functional.py:387
def __init__(self, value)
Definition: Functional.py:52
def failure(self, doc, dest, type, value)
Definition: Functional.py:220
def __init__(self, node)
Definition: Functional.py:101
def dbequal(self, node, doc, dest)
Definition: Functional.py:554
def database(self, node, doc, dest)
Definition: Functional.py:519
def loop(self, node, doc, dest)
Definition: Functional.py:419