Jpp  18.1.0
the software that should make you happy
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
run_tests.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 A test runner for Jpp.
5 This script traverses the tests directory and reports a summary.
6 
7 """
8 from __future__ import print_function
9 from collections import defaultdict
10 from glob import glob, iglob
11 import os
12 from os.path import join, basename
13 from subprocess import Popen, PIPE
14 import sys
15 from time import time
16 import re
17 import xml.etree.ElementTree as ET
18 import xml.dom.minidom
19 
20 if not len(sys.argv) == 2:
21  print("Usage: run_tests.py PATH_TO_TESTS")
22  raise SystemExit
23 
24 os.environ["TEST_DEBUG"] = "1"
25 
26 PY2 = sys.version_info < (3, 0, 0)
27 
28 try:
29  unichr
30 except NameError:
31  unichr = chr
32 
33 __author__ = "Tamas Gal"
34 __credits__ = "Brian Beyer"
35 __license__ = "MIT"
36 __email__ = "tgal@km3net.de"
37 __status__ = "Development"
38 
39 TESTS_DIR = sys.argv[1]
40 JUNIT_XML = 'out/junit_{}.xml'.format(os.path.basename(TESTS_DIR))
41 
42 if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty():
43  INFO = '\033[94m' # blue
44  OK = '\033[92m' # green
45  FAIL = '\033[91m' # red
46  RST = '\033[0m' # colour reset
47  BOLD = '\033[1m'
48 else:
49  INFO, OK, FAIL, RST, BOLD = ('', ) * 5
50 
51 
52 def main():
53  test_results = run_tests(TESTS_DIR)
54  n_tests = len(test_results)
55  n_failed_tests = sum(1 for r in test_results.values() if r[0] > 0)
56  total_time = sum(r[1] for r in test_results.values())
57 
58  print("\n{}"
59  "Test summary\n"
60  "============\n{}"
61  "Total number of tests: {}\n{}"
62  "Failed tests: {}{}\n"
63  "Elapsed time: {:.1f}s\n".format(
64  INFO, RST, n_tests, BOLD + FAIL if n_failed_tests > 0 else OK,
65  n_failed_tests, RST, total_time))
66 
67  write_junit_xml(test_results)
68 
69  if n_failed_tests > 0:
70  print_captured_output(test_results)
71  exit(1)
72  else:
73  exit(0)
74 
75 
76 def write_junit_xml(test_results):
77  """Generate XML file according to JUnit specs"""
78  test_cases = []
79  for test_script, (exit_code, t, stdout, stderr) in test_results.items():
80  test_case = TestCase(test_script,
81  elapsed_sec=t,
82  stdout=stdout,
83  stderr=stderr)
84  if exit_code > 0:
85  test_case.add_error_info('non-zero exit-code: %d' % exit_code)
86  test_cases.append(test_case)
87  test_suite = TestSuite("Jpp Test Suite", test_cases)
88  with open(JUNIT_XML, 'w') as f:
89  TestSuite.to_file(f, [test_suite])
90 
91 
92 def print_captured_output(test_results):
93  """Prints the STDOUT and STDERR of failing test scripts"""
94  print("{}"
95  "Captured output of failing tests\n"
96  "================================\n{}".format(INFO, RST))
97  for test_script, (exit_code, t, stdout, stderr) in test_results.items():
98  if exit_code > 0:
99  print("{}\n{}\n".format(test_script, len(test_script) * '-'))
100  print('{}stdout:{}\n{}\n{}stderr:{}\n{}'.format(
101  OK + BOLD, RST, stdout.decode('utf-8'), FAIL + BOLD, RST,
102  stderr.decode('utf-8')))
103 
104 
105 def run_tests(tests_dir):
106  """Runs each script in the tests directory and returns the results.
107 
108  Parameters
109  ----------
110  tests_dir: str
111  The path to the test dir, containing the test scripts (`*.sh`).
112 
113  Returns
114  -------
115  dict: key = script path, value = (exit_code, elapsed_time, stdout, stderr)
116 
117  """
118  test_results = {}
119 
120  for subdir in sorted(glob(join(tests_dir, '*'))):
121  component_group = basename(subdir)
122  print("\n{}{}\n{}{}".format(INFO, component_group,
123  len(component_group) * '=', RST))
124  for test_script in sorted(glob(join(subdir, '*.sh'))):
125  print("+ {}".format(test_script), end=' => ')
126  sys.stdout.flush()
127  start_time = time()
128  proc = Popen(test_script, stdout=PIPE, stderr=PIPE)
129  out, err = proc.communicate()
130  exit_code = proc.wait()
131  delta_t = time() - start_time
132  test_results[test_script] = (exit_code, delta_t, out, err)
133  print(" ({:.2f} s) ".format(delta_t), end='')
134  sys.stdout.flush()
135  if exit_code > 0:
136  print("{}FAILED (exit code {}){}".format(FAIL, exit_code, RST))
137  sys.stdout.flush()
138  else:
139  print("{}OK{}".format(OK, RST))
140  sys.stdout.flush()
141 
142  return test_results
143 
144 
145 def decode(var, encoding):
146  """
147  If not already unicode, decode it.
148  """
149  if PY2:
150  if isinstance(var, unicode):
151  ret = var
152  elif isinstance(var, str):
153  if encoding:
154  ret = var.decode(encoding)
155  else:
156  ret = unicode(var)
157  else:
158  ret = unicode(var)
159  else:
160  ret = str(var)
161  return ret
162 
163 
164 class TestSuite(object):
165  """
166  Suite of test cases.
167  Can handle unicode strings or binary strings if their encoding is provided.
168  """
169  def __init__(self,
170  name,
171  test_cases=None,
172  hostname=None,
173  id=None,
174  package=None,
175  timestamp=None,
176  properties=None,
177  file=None,
178  log=None,
179  url=None,
180  stdout=None,
181  stderr=None):
182  self.name = name
183  if not test_cases:
184  test_cases = []
185  try:
186  iter(test_cases)
187  except TypeError:
188  raise Exception('test_cases must be a list of test cases')
189  self.test_cases = test_cases
190  self.timestamp = timestamp
191  self.hostname = hostname
192  self.id = id
193  self.package = package
194  self.file = file
195  self.log = log
196  self.url = url
197  self.stdout = stdout
198  self.stderr = stderr
199  self.properties = properties
200 
201  def build_xml_doc(self, encoding=None):
202  """
203  Builds the XML document for the JUnit test suite.
204  Produces clean unicode strings and decodes non-unicode with the help of encoding.
205  @param encoding: Used to decode encoded strings.
206  @return: XML document with unicode string elements
207  """
208 
209  # build the test suite element
210  test_suite_attributes = dict()
211  test_suite_attributes['name'] = decode(self.name, encoding)
212  if any(c.assertions for c in self.test_cases):
213  test_suite_attributes['assertions'] = \
214  str(sum([int(c.assertions) for c in self.test_cases if c.assertions]))
215  test_suite_attributes['disabled'] = \
216  str(len([c for c in self.test_cases if not c.is_enabled]))
217  test_suite_attributes['failures'] = \
218  str(len([c for c in self.test_cases if c.is_failure()]))
219  test_suite_attributes['errors'] = \
220  str(len([c for c in self.test_cases if c.is_error()]))
221  test_suite_attributes['skipped'] = \
222  str(len([c for c in self.test_cases if c.is_skipped()]))
223  test_suite_attributes['time'] = \
224  str(sum(c.elapsed_sec for c in self.test_cases if c.elapsed_sec))
225  test_suite_attributes['tests'] = str(len(self.test_cases))
226 
227  if self.hostname:
228  test_suite_attributes['hostname'] = decode(self.hostname, encoding)
229  if self.id:
230  test_suite_attributes['id'] = decode(self.id, encoding)
231  if self.package:
232  test_suite_attributes['package'] = decode(self.package, encoding)
233  if self.timestamp:
234  test_suite_attributes['timestamp'] = decode(
235  self.timestamp, encoding)
236  if self.file:
237  test_suite_attributes['file'] = decode(self.file, encoding)
238  if self.log:
239  test_suite_attributes['log'] = decode(self.log, encoding)
240  if self.url:
241  test_suite_attributes['url'] = decode(self.url, encoding)
242 
243  xml_element = ET.Element("testsuite", test_suite_attributes)
244 
245  # add any properties
246  if self.properties:
247  props_element = ET.SubElement(xml_element, "properties")
248  for k, v in self.properties.items():
249  attrs = {
250  'name': decode(k, encoding),
251  'value': decode(v, encoding)
252  }
253  ET.SubElement(props_element, "property", attrs)
254 
255  # add test suite stdout
256  if self.stdout:
257  stdout_element = ET.SubElement(xml_element, "system-out")
258  stdout_element.text = decode(self.stdout, encoding)
259 
260  # add test suite stderr
261  if self.stderr:
262  stderr_element = ET.SubElement(xml_element, "system-err")
263  stderr_element.text = decode(self.stderr, encoding)
264 
265  # test cases
266  for case in self.test_cases:
267  test_case_attributes = dict()
268  test_case_attributes['name'] = decode(case.name, encoding)
269  if case.assertions:
270  # Number of assertions in the test case
271  test_case_attributes['assertions'] = "%d" % case.assertions
272  if case.elapsed_sec:
273  test_case_attributes['time'] = "%f" % case.elapsed_sec
274  if case.timestamp:
275  test_case_attributes['timestamp'] = decode(
276  case.timestamp, encoding)
277  if case.classname:
278  test_case_attributes['classname'] = decode(
279  case.classname, encoding)
280  if case.status:
281  test_case_attributes['status'] = decode(case.status, encoding)
282  if case.category:
283  test_case_attributes['class'] = decode(case.category, encoding)
284  if case.file:
285  test_case_attributes['file'] = decode(case.file, encoding)
286  if case.line:
287  test_case_attributes['line'] = decode(case.line, encoding)
288  if case.log:
289  test_case_attributes['log'] = decode(case.log, encoding)
290  if case.url:
291  test_case_attributes['url'] = decode(case.url, encoding)
292 
293  test_case_element = ET.SubElement(xml_element, "testcase",
294  test_case_attributes)
295 
296  # failures
297  if case.is_failure():
298  attrs = {'type': 'failure'}
299  if case.failure_message:
300  attrs['message'] = decode(case.failure_message, encoding)
301  if case.failure_type:
302  attrs['type'] = decode(case.failure_type, encoding)
303  failure_element = ET.Element("failure", attrs)
304  if case.failure_output:
305  failure_element.text = decode(case.failure_output,
306  encoding)
307  test_case_element.append(failure_element)
308 
309  # errors
310  if case.is_error():
311  attrs = {'type': 'error'}
312  if case.error_message:
313  attrs['message'] = decode(case.error_message, encoding)
314  if case.error_type:
315  attrs['type'] = decode(case.error_type, encoding)
316  error_element = ET.Element("error", attrs)
317  if case.error_output:
318  error_element.text = decode(case.error_output, encoding)
319  test_case_element.append(error_element)
320 
321  # skippeds
322  if case.is_skipped():
323  attrs = {'type': 'skipped'}
324  if case.skipped_message:
325  attrs['message'] = decode(case.skipped_message, encoding)
326  skipped_element = ET.Element("skipped", attrs)
327  if case.skipped_output:
328  skipped_element.text = decode(case.skipped_output,
329  encoding)
330  test_case_element.append(skipped_element)
331 
332  # test stdout
333  if case.stdout:
334  stdout_element = ET.Element("system-out")
335  stdout_element.text = decode(case.stdout, encoding)
336  test_case_element.append(stdout_element)
337 
338  # test stderr
339  if case.stderr:
340  stderr_element = ET.Element("system-err")
341  stderr_element.text = decode(case.stderr, encoding)
342  test_case_element.append(stderr_element)
343 
344  return xml_element
345 
346  @staticmethod
347  def to_xml_string(test_suites, prettyprint=True, encoding=None):
348  """
349  Returns the string representation of the JUnit XML document.
350  @param encoding: The encoding of the input.
351  @return: unicode string
352  """
353 
354  try:
355  iter(test_suites)
356  except TypeError:
357  raise Exception('test_suites must be a list of test suites')
358 
359  xml_element = ET.Element("testsuites")
360  attributes = defaultdict(int)
361  for ts in test_suites:
362  ts_xml = ts.build_xml_doc(encoding=encoding)
363  for key in ['failures', 'errors', 'tests', 'disabled']:
364  attributes[key] += int(ts_xml.get(key, 0))
365  for key in ['time']:
366  attributes[key] += float(ts_xml.get(key, 0))
367  xml_element.append(ts_xml)
368  for key, value in attributes.items():
369  xml_element.set(key, str(value))
370 
371  xml_string = ET.tostring(xml_element, encoding=encoding)
372  # is encoded now
373  xml_string = TestSuite._clean_illegal_xml_chars(
374  xml_string.decode(encoding or 'utf-8'))
375  # is unicode now
376 
377  if prettyprint:
378  # minidom.parseString() works just on correctly encoded binary strings
379  xml_string = xml_string.encode(encoding or 'utf-8')
380  xml_string = xml.dom.minidom.parseString(xml_string)
381  # toprettyxml() produces unicode if no encoding is being passed or binary string with an encoding
382  xml_string = xml_string.toprettyxml(encoding=encoding)
383  if encoding:
384  xml_string = xml_string.decode(encoding)
385  # is unicode now
386  return xml_string
387 
388  @staticmethod
389  def to_file(file_descriptor, test_suites, prettyprint=True, encoding=None):
390  """
391  Writes the JUnit XML document to a file.
392  """
393  xml_string = TestSuite.to_xml_string(test_suites,
394  prettyprint=prettyprint,
395  encoding=encoding)
396  # has problems with encoded str with non-ASCII (non-default-encoding) characters!
397  file_descriptor.write(xml_string)
398 
399  @staticmethod
400  def _clean_illegal_xml_chars(string_to_clean):
401  """
402  Removes any illegal unicode characters from the given XML string.
403  @see: http://stackoverflow.com/questions/1707890/fast-way-to-filter-illegal-xml-unicode-chars-in-python
404  """
405 
406  illegal_unichrs = [(0x00, 0x08), (0x0B, 0x1F), (0x7F, 0x84),
407  (0x86, 0x9F), (0xD800, 0xDFFF), (0xFDD0, 0xFDDF),
408  (0xFFFE, 0xFFFF), (0x1FFFE, 0x1FFFF),
409  (0x2FFFE, 0x2FFFF), (0x3FFFE, 0x3FFFF),
410  (0x4FFFE, 0x4FFFF), (0x5FFFE, 0x5FFFF),
411  (0x6FFFE, 0x6FFFF), (0x7FFFE, 0x7FFFF),
412  (0x8FFFE, 0x8FFFF), (0x9FFFE, 0x9FFFF),
413  (0xAFFFE, 0xAFFFF), (0xBFFFE, 0xBFFFF),
414  (0xCFFFE, 0xCFFFF), (0xDFFFE, 0xDFFFF),
415  (0xEFFFE, 0xEFFFF), (0xFFFFE, 0xFFFFF),
416  (0x10FFFE, 0x10FFFF)]
417 
418  illegal_ranges = [
419  "%s-%s" % (unichr(low), unichr(high))
420  for (low, high) in illegal_unichrs if low < sys.maxunicode
421  ]
422 
423  illegal_xml_re = re.compile('[%s]' % ''.join(illegal_ranges))
424  return illegal_xml_re.sub('', string_to_clean)
425 
426 
427 class TestCase(object):
428  """A JUnit test case with a result and possibly some stdout or stderr"""
429  def __init__(self,
430  name,
431  classname=None,
432  elapsed_sec=None,
433  stdout=None,
434  stderr=None,
435  assertions=None,
436  timestamp=None,
437  status=None,
438  category=None,
439  file=None,
440  line=None,
441  log=None,
442  group=None,
443  url=None):
444  self.name = name
445  self.assertions = assertions
446  self.elapsed_sec = elapsed_sec
447  self.timestamp = timestamp
448  self.classname = classname
449  self.status = status
450  self.category = category
451  self.file = file
452  self.line = line
453  self.log = log
454  self.url = url
455  self.stdout = stdout
456  self.stderr = stderr
457 
458  self.is_enabled = True
459  self.error_message = None
460  self.error_output = None
461  self.error_type = None
462  self.failure_message = None
463  self.failure_output = None
464  self.failure_type = None
465  self.skipped_message = None
466  self.skipped_output = None
467 
468  def add_error_info(self, message=None, output=None, error_type=None):
469  """Adds an error message, output, or both to the test case"""
470  if message:
471  self.error_message = message
472  if output:
473  self.error_output = output
474  if error_type:
475  self.error_type = error_type
476 
477  def add_failure_info(self, message=None, output=None, failure_type=None):
478  """Adds a failure message, output, or both to the test case"""
479  if message:
480  self.failure_message = message
481  if output:
482  self.failure_output = output
483  if failure_type:
484  self.failure_type = failure_type
485 
486  def add_skipped_info(self, message=None, output=None):
487  """Adds a skipped message, output, or both to the test case"""
488  if message:
489  self.skipped_message = message
490  if output:
491  self.skipped_output = output
492 
493  def is_failure(self):
494  """returns true if this test case is a failure"""
495  return self.failure_output or self.failure_message
496 
497  def is_error(self):
498  """returns true if this test case is an error"""
499  return self.error_output or self.error_message
500 
501  def is_skipped(self):
502  """returns true if this test case has been skipped"""
503  return self.skipped_output or self.skipped_message
504 
505 
506 if __name__ == '__main__':
507  main()
JRange< T, JComparator_t > join(const JRange< T, JComparator_t > &first, const JRange< T, JComparator_t > &second)
Join ranges.
Definition: JRange.hh:659
exit
Definition: JPizza.sh:36
T * open(const std::string &file_name)
Open file.
Definition: JeepToolkit.hh:346
def print_captured_output
Definition: run_tests.py:92
print
Definition: JConvertDusj.sh:44
def _clean_illegal_xml_chars
Definition: run_tests.py:400
def main
Definition: run_tests.py:52
General exception.
Definition: Exception.hh:13
def write_junit_xml
Definition: run_tests.py:76
def decode
Definition: run_tests.py:145
def run_tests
Definition: run_tests.py:105