FOSSology  4.4.0
Open Source License Compliance by Open Source Software
Functional.py
Go to the documentation of this file.
1 #!/usr/bin/python3 -u
2 
11 
12 """
13  SPDX-FileCopyrightText: © 2012 Hewlett-Packard Development Company, L.P.
14 
15  SPDX-License-Identifier: GPL-2.0-only
16 """
17 
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
22 import configparser
23 import subprocess
24 import functools
25 import signal
26 import shlex
27 import time
28 import re
29 import os
30 
31 
34 defsReplace = re.compile('{([^{}]*)}')
35 defsSplit = re.compile('([^\s]+):([^\s]+)')
36 
38  """ Error class used for missing definitions in the xml file """
39  def __init__(self, value):
40  """ Constructor """
41  self.valuevalue = value
42  def __str__(self):
43  """ To string """
44  return repr(self.valuevalue)
45 
47  """ Error class used when a test suite takes too long to run """
48  pass
49 
50 def timeout(func, maxRuntime):
51 
58 
59  def timeout_handler(signum, frame):
60  raise TimeoutError()
61 
62  signal.signal(signal.SIGALRM, timeout_handler)
63  signal.alarm(maxRuntime)
64 
65  try:
66  func()
67  except TimeoutError:
68  return False
69  return True
70 
71 
74 
75 class testsuite:
76  """
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.
79 
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
86  """
87 
88  def __init__(self, node):
89  """
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.
94 
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
97  the file.
98 
99  Returns nothing
100  """
101 
102  defNode = node.getElementsByTagName('definitions')[0]
103  definitions = defNode.attributes
104 
105  self.namename = node.getAttribute('name')
106 
107  self.definesdefines = {}
108  self.definesdefines['pids'] = {}
109 
110  # get variable definitions
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)
114 
115  self.setupsetup = []
116  self.cleanupcleanup = []
117  self.teststests = []
118  self.subprosubpro = []
119  self.dbresultdbresult = None
120 
121  # parse all actions that will be taken during the setup phase
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]:
125  self.setupsetup.append(self.createActioncreateAction(action))
126 
127  # parse all actions that will be taken during the cleanup phase
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]:
131  self.cleanupcleanup.append(self.createActioncreateAction(action))
132 
133  # parse all actions that will be taken during the testing phase
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)
139 
140  def substitute(self, string, node = None):
141  """
142  Simple function to make calling processVariable a lot cleaner
143 
144  Returns the string with the variables correctly substituted
145  """
146  while defsReplace.search(string):
147  string = defsReplace.sub(functools.partial(self.processVariableprocessVariable, node), string)
148  return string
149 
150  def processVariable(self, node, match):
151  """
152  Function passed to the regular expression library to replace variables in a
153  string from the xml file.
154  doc, dest, "")
155  return (1, 0)
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.
159 
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".
164 
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
169  map.
170 
171  Returns the replacement string
172  """
173  name = match.group(1)
174 
175  # variable begins with $, replace with output of shell command
176  if name[0] == '$':
177  process = os.popen(name[1:], 'r')
178  ret = process.read()
179  process.close()
180  return ret[:-1]
181 
182  # variable contains a ":", access defs[name] as an associative array or dictionary
183  arrayMatch = defsSplit.match(name)
184  if arrayMatch:
185  name = arrayMatch.group(1)
186  index = self.substitutesubstitute(arrayMatch.group(2), node)
187 
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):
192  self.definesdefines[name] = self.substitutesubstitute(node.getAttribute(name))
193  else:
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)]
198 
199  # this is a simply definition access, check validity and return the result
200  if name not in self.definesdefines:
201  if node and node.hasAttribute(name):
202  self.definesdefines[name] = self.substitutesubstitute(node.getAttribute(name), node)
203  else:
204  raise DefineError('"{0}" not defined in testsuite "{1}"'.format(name, self.namename))
205  return self.definesdefines[name]
206 
207  def failure(self, doc, dest, type, value):
208  """
209  Puts a failure node into an the results document
210 
211  Return nothing
212  """
213  if doc and dest:
214  fail = doc.createElement('failure')
215  fail.setAttribute('type', type)
216 
217  text = doc.createTextNode(value)
218  fail.appendChild(text)
219 
220  dest.appendChild(fail)
221 
222 
225 
226  def createAllActions(self, node):
227  """
228  Creates all the child actions for a particular node in the xml file.
229  """
230  return [self.createActioncreateAction(child) for child in node.childNodes if child.nodeType == Node.ELEMENT_NODE]
231 
232  def createAction(self, node):
233  """
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.
238 
239  To write a new type of action write a function with the signature:
240  actionName(self, source_node, xml_document, destination_node)
241 
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.
250 
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
260  ignored.
261 
262  Returns the new action
263  """
264  def action_wrapper(action, node, doc, dest):
265  print('.', end=' ')# node.nodeName,
266  return action(node, doc, dest)
267 
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)
272 
273  def required(self, node, name):
274  """
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.
278  """
279  retval = self.substitutesubstitute(node.getAttribute(name))
280 
281  if not retval:
282  raise DefineError('attribute({0}) required for action({1})'.format(name, node.nodeName))
283  return retval
284 
285  def optional(self, node, name):
286  """
287  Gets an options attribute for a particular action. This will perform the
288  necessary substitution for the value of the attribute.
289  """
290  return self.substitutesubstitute(node.getAttribute(name))
291 
292  def concurrently(self, node, doc, dest):
293  """
294  Action
295 
296  Attributes:
297  command [required]: the name of the process that will be executed
298  params [required]: the command line parameters passed to the command
299 
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.
305 
306  Returns True
307  """
308  command = self.requiredrequired(node, 'command')
309  params = self.requiredrequired(node, 'params')
310 
311  cmd = shlex.split("{0} {1}".format(command, params))
312  proc = subprocess.Popen(cmd, 0)
313  time.sleep(1)
314  self.subprosubpro.append(proc)
315  self.definesdefines['pids'][str(len(self.definesdefines['pids']))] = str(proc.pid)
316 
317  return (1, 0)
318 
319  def sequential(self, node, doc, dest):
320  """
321  Action
322 
323  Attributes:
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
328 
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.
334 
335  Returns True if the results and return value match those provided
336  """
337  command = self.requiredrequired(node, 'command')
338  params = self.requiredrequired(node, 'params')
339  expected = self.optionaloptional(node, 'result')
340  retval = self.optionaloptional(node, 'retval')
341 
342  cmd = "{0} {1}".format(command, params)
343  proc = subprocess.Popen(cmd, 0, shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
344 
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()))
349  return (1, 1)
350 
351  proc.wait()
352 
353  if len(retval) != 0 and proc.returncode != int(retval):
354  self.failurefailure(doc, dest, "IncorrectReturn", "expected: {0} != result: {1}".format(retval, proc.returncode))
355  return (1, 1)
356  return (1, 0)
357 
358  def sleep(self, node, doc, dest):
359  """
360  Action
361 
362  Attributes:
363  duration [require]: how long the test harness should sleep for
364 
365  This action simply pauses execution of the test harness for duration
366  seconds. This action cannot fail and will always return True.
367 
368  Returns True
369  """
370  duration = node.getAttribute('duration')
371  time.sleep(int(duration))
372  return (1, 0)
373 
374  def loadConf(self, node, doc, dest):
375  """
376  Action
377 
378  Attributes:
379  directory [required]: the directory location of the fossology.conf file
380 
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.
383 
384  Returns True
385  """
386  dir = self.requiredrequired(node, 'directory')
387 
388  config = configparser.ConfigParser()
389  config.read_file(open(dir + "/fossology.conf"))
390 
391  self.definesdefines["FOSSOLOGY"] = {}
392  self.definesdefines["BUILD"] = {}
393 
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")
397 
398  config.read_file(open(dir + "/VERSION"))
399 
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")
403 
404  return (1, 0)
405 
406  def loop(self, node, doc, dest):
407  """
408  Action
409 
410  Attributes:
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
414 
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.
420  """
421  varname = self.requiredrequired(node, 'varname')
422  values = self.optionaloptional(node, 'values')
423  iterations = self.optionaloptional(node, 'iterations')
424 
425  actions = self.createAllActionscreateAllActions(node)
426 
427  tests = 0
428  failed = 0
429 
430  if values:
431  for value in values.split(','):
432  self.definesdefines[varname] = value.strip()
433  for action in actions:
434  ret = action(doc, dest)
435  tests += ret[0]
436  failed += ret[1]
437  else:
438  for i in range(int(iterations)):
439  self.definesdefines[varname] = str(i)
440  for action in actions:
441  ret = action(doc, dest)
442  tests += ret[0]
443  failed += ret[1]
444 
445  del self.definesdefines[varname]
446  return (tests, failed)
447 
448  def upload(self, node, doc, dest):
449  """
450  Action
451 
452  Attributes:
453  file [required]: the file that will be uploaded to the fossology database
454 
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.
460 
461  Returns True if and only if cp2foss succeeded
462  """
463  file = self.requiredrequired(node, 'file')
464 
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)
467  proc.wait()
468 
469  if proc.returncode != 0:
470  return (1, 1)
471 
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)
476 
477  return (1, 0)
478 
479  def schedule(self, node ,doc, dest):
480  """
481  Action
482 
483  Attributes:
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
487 
488  This action will schedule agents to run on a particular upload.
489 
490  Returns True if and only if fossjobs succeeded
491  """
492  upload = self.requiredrequired(node, 'upload')
493  agents = self.optionaloptional(node, 'agents')
494 
495  if not agents:
496  agents = ""
497 
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)
500  proc.wait()
501 
502  if proc.returncode != 0:
503  return (1, 1)
504  return (1, 0)
505 
506  def database(self, node, doc, dest):
507  """
508  Action
509 
510  Attributes:
511  sql [required]: the sql that will be exectued
512 
513  Sub nodes:
514 
515 
516  This action will execute an sql statement on the relevant database. It can
517  check that the results of the sql were correct.
518 
519  Returns True if results aren't expected or the results were correct
520  """
521  sql = self.requiredrequired(node, 'sql')
522 
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)
526  proc.wait()
527 
528  total = 0
529  failed = 0
530 
531  self.dbresultdbresult = [str.split() for str in proc.stdout.readlines()]
532  for action in self.createAllActionscreateAllActions(node):
533  ret = action(doc, dest)
534  total += ret[0]
535  failed += ret[1]
536 
537  del self.dbresultdbresult
538  self.dbresultdbresult = None
539  return (total, failed)
540 
541  def dbequal(self, node, doc, dest):
542  """
543  Action
544 
545  Attributes:
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
549 
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
552  database.
553 
554  returns True if the expected is the same as the result
555  """
556  row = int(self.requiredrequired(node, 'row'))
557  col = int(self.requiredrequired(node, 'col'))
558  val = self.requiredrequired(node, 'val')
559 
560  if not self.dbresultdbresult:
561  raise DefineError("dbresult action must be within a database action")
562  result = self.dbresultdbresult
563  if len(result) <= row:
564  self.failurefailure(doc, dest, "DatabaseMismatch", "Index out of bounds: {0} > {1}".format(row, len(result)))
565  return (1, 1)
566  if len(result[row]) <= col:
567  self.failurefailure(doc, dest, "DatabaseMismatch", "Index out of bounds: {0} > {1}".format(col, len(result[row])))
568  return (1, 1)
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))
571  return (1, 1)
572  return (1, 0)
573 
574 
577 
578  def performTests(self, suiteNode, document, fname):
579  """
580  Runs the tests and writes the output to the results document.
581 
582  Returns nothing
583  """
584  failures = 0
585  tests = 0
586  totalasserts = 0
587 
588  print("start up", end=' ')
589  for action in self.setupsetup:
590  while action(None, None)[1] != 0:
591  time.sleep(5)
592 
593  print("tests", end=' ')
594  for test in self.teststests:
595  assertions = 0
596  testNode = document.createElement("testcase")
597 
598  testNode.setAttribute("class", test[0])
599  testNode.setAttribute("name", test[0])
600 
601  starttime = time.time()
602  for action in test[1]:
603  res = action(document, testNode)
604  assertions += res[0]
605  failures += res[1]
606  runtime = (time.time() - starttime)
607 
608  testNode.setAttribute("assertions", str(assertions))
609  testNode.setAttribute("time", str(runtime))
610 
611  tests += 1
612 
613  totalasserts += assertions
614 
615  suiteNode.appendChild(testNode)
616 
617  print(" clean up", end=' ')
618  for action in self.cleanupcleanup:
619  action(None, None)
620 
621  for process in self.subprosubpro:
622  process.wait()
623 
624  suiteNode.setAttribute("failures", str(failures))
625  suiteNode.setAttribute("tests", str(tests))
626  suiteNode.setAttribute("assertions", str(totalasserts))
627  print()
628 
629 
632 
633 def main():
634  """
635  Main entry point for the Functional tests
636  """
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")
643 
644  (options, args) = parser.parse_args()
645 
646  testFile = open(options.testfile)
647  dom = parseString(testFile.read())
648  dir = os.getcwd()
649 
650  os.chdir('../../..')
651 
652  # remove all of the comment nodes, since the code assumes
653  # that there are no comment nodes within the XML
654  comment_list = []
655  for child in dom.childNodes:
656  if child.nodeType == Node.COMMENT_NODE:
657  comment_list.append(child)
658 
659  for node in comment_list:
660  node.parentNode.removeChild(node)
661 
662  setupNode = dom.firstChild.getElementsByTagName('setup')[0]
663  cleanupNode = dom.firstChild.getElementsByTagName('cleanup')[0]
664 
665  resultsDoc = getDOMImplementation().createDocument(None, "testsuites", None)
666  top_output = resultsDoc.documentElement
667 
668  maxRuntime = int(dom.firstChild.getAttribute("timeout"))
669 
670  for suite in dom.firstChild.getElementsByTagName('testsuite'):
671  if options.specific and suite.getAttribute("name") != options.specific:
672  continue
673  if suite.hasAttribute("disable"):
674  print(suite.getAttribute("name"),'::','disabled')
675  continue
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')
678  continue
679  suiteNode = resultsDoc.createElement("testsuite")
680  errors = 0
681 
682  suiteNode.setAttribute("name", suite.getAttribute("name"))
683  suiteNode.setAttribute("errors", "0")
684  suiteNode.setAttribute("time", "0")
685 
686  try:
687  curr = testsuite(suite)
688 
689  setup = curr.createAllActions(setupNode)
690  cleanup = curr.createAllActions(cleanupNode)
691 
692  curr.setup = setup + curr.setup
693  curr.cleanup = cleanup + curr.cleanup
694 
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):
698  errors += 1
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)
704 
705  suiteNode.setAttribute("time", str(runtime))
706 
707  except DefineError as detail:
708  errors += 1
709  errorNode = resultsDoc.createElement("error")
710  errorNode.setAttribute("type", "DefinitionError")
711  errorNode.appendChild(resultsDoc.createTextNode("DefineError: {0}".format(detail.value)))
712  suiteNode.appendChild(errorNode)
713 
714  finally:
715  suiteNode.setAttribute("errors", str(errors))
716  top_output.appendChild(suiteNode)
717 
718  os.chdir(dir)
719 
720  output = open(options.resultfile, 'w')
721  resultsDoc.writexml(output, "", " ", "\n")
722  output.close()
723 
724  os.chdir(dir)
725 
726 if __name__ == "__main__":
727  main()
def __init__(self, value)
Definition: Functional.py:39
class that handles running a test suite ####################################
Definition: Functional.py:75
def dbequal(self, node, doc, dest)
Definition: Functional.py:541
def schedule(self, node, doc, dest)
Definition: Functional.py:479
def performTests(self, suiteNode, document, fname)
run tests and produce output #
Definition: Functional.py:578
def failure(self, doc, dest, type, value)
Definition: Functional.py:207
def __init__(self, node)
Definition: Functional.py:88
def processVariable(self, node, match)
Definition: Functional.py:150
def concurrently(self, node, doc, dest)
Definition: Functional.py:292
def createAllActions(self, node)
actions that tests can take #
Definition: Functional.py:226
def loop(self, node, doc, dest)
Definition: Functional.py:406
def sequential(self, node, doc, dest)
Definition: Functional.py:319
def substitute(self, string, node=None)
Definition: Functional.py:140
def optional(self, node, name)
Definition: Functional.py:285
def upload(self, node, doc, dest)
Definition: Functional.py:448
def required(self, node, name)
Definition: Functional.py:273
def loadConf(self, node, doc, dest)
Definition: Functional.py:374
def database(self, node, doc, dest)
Definition: Functional.py:506
def createAction(self, node)
Definition: Functional.py:232
def sleep(self, node, doc, dest)
Definition: Functional.py:358