Python Library Unittest
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

run.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. import fstools
  5. import report
  6. import reqif
  7. import json
  8. import os
  9. import sys
  10. import platform
  11. try:
  12. from platform import dist as dist
  13. except ImportError:
  14. from distro import linux_distribution as dist
  15. import getpass
  16. import subprocess
  17. import imp
  18. import xml.dom.minidom
  19. try:
  20. import jinja2
  21. except ImportError:
  22. jinja2 = None
  23. import shutil
  24. HEADER = '\033[95m'
  25. OKBLUE = '\033[94m'
  26. OKGREEN = '\033[92m'
  27. WARNING = '\033[93m'
  28. FAIL = '\033[91m'
  29. ENDC = '\033[0m'
  30. BOLD = '\033[1m'
  31. UNDERLINE = '\033[4m'
  32. ARG_CLEAN = 'clean'
  33. ARG_RUN = 'run'
  34. ARG_FINALISE = 'finalise'
  35. ARG_PDF = 'pdf'
  36. ARG_STATUS = 'status'
  37. ARG_COPY = 'copy'
  38. ARG_RELEASE = 'release'
  39. UNITTEST_KEY_SYSTEM_INFO = 'system_information'
  40. UNITTEST_KEY_UNITTEST_INFO = 'unittest_information'
  41. UNITTEST_KEY_TESTOBJECT_INFO = 'testobject_information'
  42. UNITTEST_KEY_TESTRUNS = 'testrun_list'
  43. UNITTEST_KEY_COVERAGE_INFO = 'coverage_information'
  44. UNITTEST_KEY_SPECIFICATION = 'specification'
  45. FILES = {
  46. 'data-collection': 'unittest.json',
  47. 'tex-report': 'unittest.tex',
  48. 'coverage-xml': 'coverage.xml'
  49. }
  50. REPORT_FILES = [FILES['data-collection'], FILES['coverage-xml'], 'unittest.pdf']
  51. class coverage_info(list):
  52. KEY_NAME = 'name'
  53. KEY_FILEPATH = 'filepath'
  54. KEY_LINE_COVERAGE = 'line_coverage'
  55. KEY_BRANCH_COVERAGE = 'branch_coverage'
  56. KEY_FILES = 'files'
  57. KEY_FRAGMENTS = 'fragments'
  58. KEY_START_LINE = 'start'
  59. KEY_END_LINE = 'end'
  60. KEY_COVERAGE_STATE = 'coverage_state'
  61. COVERED = 'covered'
  62. UNCOVERED = 'uncovered'
  63. CLEAN = 'clean'
  64. PARTIALLY_COVERED = 'partially-covered'
  65. def __init__(self, xml_filename, module_basepath):
  66. list.__init__(self)
  67. xmldoc = xml.dom.minidom.parse(xml_filename)
  68. itemlist = xmldoc.getElementsByTagName('package')
  69. for p in itemlist:
  70. module = {}
  71. module[self.KEY_NAME] = p.attributes['name'].value[len(module_basepath) + 1:]
  72. module[self.KEY_FILEPATH] = p.attributes['name'].value.replace('.', os.path.sep)
  73. module[self.KEY_LINE_COVERAGE] = float(p.attributes['line-rate'].value) * 100.
  74. try:
  75. module[self.KEY_BRANCH_COVERAGE] = float(p.attributes['branch-rate'].value) * 100.
  76. except AttributeError:
  77. module[self.KEY_BRANCH_COVERAGE] = None
  78. module[self.KEY_FILES] = []
  79. for c in p.getElementsByTagName('class'):
  80. f = {}
  81. f[self.KEY_NAME] = c.attributes['filename'].value[len(module_basepath) + 1:].replace(os.path.sep, '.')
  82. f[self.KEY_FILEPATH] = c.attributes['filename'].value
  83. f[self.KEY_LINE_COVERAGE] = float(c.attributes['line-rate'].value) * 100.
  84. try:
  85. f[self.KEY_BRANCH_COVERAGE] = float(p.attributes['branch-rate'].value) * 100.
  86. except:
  87. f[self.KEY_BRANCH_COVERAGE] = None
  88. f[self.KEY_FRAGMENTS] = []
  89. last_hit = None
  90. start_line = 1
  91. end_line = 1
  92. for line in c.getElementsByTagName('line'):
  93. line_no = int(line.attributes['number'].value)
  94. hit = bool(int(line.attributes['hits'].value))
  95. if hit:
  96. cc = line.attributes.get('condition-coverage')
  97. if cc is not None and not cc.value.startswith('100%'):
  98. hit = self.PARTIALLY_COVERED
  99. else:
  100. hit = self.COVERED
  101. else:
  102. hit = self.UNCOVERED
  103. if line_no == 1:
  104. last_hit = hit
  105. elif last_hit != hit or line_no > end_line + 1:
  106. if last_hit is not None:
  107. line = {}
  108. line[self.KEY_START_LINE] = start_line
  109. line[self.KEY_END_LINE] = end_line
  110. line[self.KEY_COVERAGE_STATE] = last_hit
  111. f[self.KEY_FRAGMENTS].append(line)
  112. if line_no > end_line + 1:
  113. line = {}
  114. if last_hit is not None:
  115. line[self.KEY_START_LINE] = end_line + 1
  116. else:
  117. line[self.KEY_START_LINE] = start_line
  118. line[self.KEY_END_LINE] = line_no - 1
  119. line[self.KEY_COVERAGE_STATE] = self.CLEAN
  120. f[self.KEY_FRAGMENTS].append(line)
  121. start_line = line_no
  122. end_line = line_no
  123. last_hit = hit
  124. elif line_no == end_line + 1:
  125. end_line = line_no
  126. if last_hit is not None:
  127. line = {}
  128. line[self.KEY_START_LINE] = start_line
  129. line[self.KEY_END_LINE] = end_line
  130. line[self.KEY_COVERAGE_STATE] = last_hit
  131. f[self.KEY_FRAGMENTS].append(line)
  132. line = {}
  133. if last_hit is not None:
  134. line[self.KEY_START_LINE] = end_line + 1
  135. else:
  136. line[self.KEY_START_LINE] = start_line
  137. line[self.KEY_END_LINE] = None
  138. line[self.KEY_COVERAGE_STATE] = self.CLEAN
  139. f[self.KEY_FRAGMENTS].append(line)
  140. module[self.KEY_FILES].append(f)
  141. self.append(module)
  142. def __str__(self):
  143. rv = ''
  144. for module in self:
  145. rv += '%s (%.1f%% - %s)\n' % (module.get(self.KEY_NAME), module.get(self.KEY_LINE_COVERAGE), module.get(self.KEY_FILEPATH))
  146. for py_file in module.get(self.KEY_FILES):
  147. rv += ' %s (%.1f%% - %s)\n' % (py_file.get(self.KEY_NAME), py_file.get(self.KEY_LINE_COVERAGE), py_file.get(self.KEY_FILEPATH))
  148. for fragment in py_file.get(self.KEY_FRAGMENTS):
  149. if fragment.get(self.KEY_END_LINE) is not None:
  150. rv += ' %d - %d: %s\n' % (fragment.get(self.KEY_START_LINE), fragment.get(self.KEY_END_LINE), repr(fragment.get(self.KEY_COVERAGE_STATE)))
  151. else:
  152. rv += ' %d - : %s\n' % (fragment.get(self.KEY_START_LINE), repr(fragment.get(self.KEY_COVERAGE_STATE)))
  153. return rv
  154. def unittest_filename(base_folder, filename):
  155. return os.path.join(base_folder, 'testresults', filename)
  156. def print_header(txt, color=BOLD + WARNING):
  157. print(color + txt + ENDC)
  158. def print_action(txt, color=BOLD):
  159. print(color + ' * ' + txt + ENDC)
  160. def print_info(txt, color=ENDC):
  161. print(' ' + color + txt + ENDC)
  162. def remove_file(filename):
  163. if os.path.exists(filename) and not filename.endswith('.gitkeep'):
  164. try:
  165. print_info('Removing %s' % filename)
  166. os.remove(filename)
  167. except OSError:
  168. pass
  169. def module_uid(path):
  170. return fstools.uid_filelist(path, '*.py', rekursive=True)
  171. def unittest(options, args, unittest_folder):
  172. if ARG_CLEAN in args:
  173. unittest_init(unittest_folder)
  174. elif ARG_RUN in args:
  175. unittest_run(unittest_folder, options)
  176. elif ARG_FINALISE in args:
  177. unittest_finalise(unittest_folder)
  178. elif ARG_PDF in args:
  179. unittest_pdf(unittest_folder)
  180. elif ARG_STATUS in args:
  181. unittest_status(unittest_folder)
  182. elif ARG_COPY in args:
  183. unittest_copy(unittest_folder)
  184. elif ARG_RELEASE in args:
  185. unittest_release(unittest_folder)
  186. def unittest_init(unittest_folder):
  187. config = imp.load_source('', os.path.join(unittest_folder, 'src', 'config.py'))
  188. #
  189. print_header("Initiating unittest for first testrun...")
  190. if not os.path.exists(unittest_filename(unittest_folder, '')):
  191. print_action('Creating outpout folder %s' % unittest_filename(unittest_folder, ''))
  192. fstools.mkdir(unittest_filename(unittest_folder, ''))
  193. #
  194. print_action('Cleaning up data from last testrun')
  195. for fn in os.listdir(unittest_filename(unittest_folder, '')):
  196. remove_file(unittest_filename(unittest_folder, fn))
  197. remove_file(unittest_filename(unittest_folder, FILES['coverage-xml']))
  198. #
  199. print_action('Creating unittest data-collection: %s' % unittest_filename(unittest_folder, FILES['data-collection']))
  200. #
  201. system_info = {}
  202. system_info['Architecture'] = platform.architecture()[0]
  203. system_info['Machine'] = platform.machine()
  204. system_info['Hostname'] = platform.node()
  205. system_info['Distribution'] = ' '.join(dist())
  206. system_info['System'] = platform.system()
  207. system_info['Kernel'] = platform.release() + ' (%s)' % platform.version()
  208. system_info['Username'] = getpass.getuser()
  209. system_info['Path'] = unittest_folder
  210. #
  211. unittest_info = {}
  212. unittest_info['Version'] = module_uid(os.path.join(unittest_folder, 'src', 'tests'))
  213. #
  214. testobject_info = {}
  215. testobject_info['Name'] = config.lib.__name__
  216. testobject_info['Version'] = module_uid(config.lib.__path__[0])
  217. testobject_info['Description'] = config.lib.__DESCRIPTION__
  218. testobject_info['Supported Interpreters'] = ', '.join(['python%d' % vers for vers in config.lib.__INTERPRETER__])
  219. testobject_info['State'] = 'Released' if config.release_unittest_version == module_uid(os.path.join(unittest_folder, 'src', 'tests')) else 'In development'
  220. testobject_info['Dependencies'] = []
  221. for dependency in config.lib.__DEPENDENCIES__:
  222. testobject_info['Dependencies'].append((dependency, module_uid(os.path.join(unittest_folder, 'src', dependency))))
  223. #
  224. spec_filename = os.path.join(unittest_folder, '..', 'requirements', 'specification.reqif')
  225. print_action("Adding Requirement Specification from %s" % spec_filename)
  226. try:
  227. spec = reqif.reqif_dict(spec_filename, 'Heading', 'Software Specification')
  228. except FileNotFoundError:
  229. print_info('FAILED', FAIL)
  230. spec = {}
  231. else:
  232. print_info('SUCCESS', OKGREEN)
  233. #
  234. data_collection = {
  235. UNITTEST_KEY_SYSTEM_INFO: system_info,
  236. UNITTEST_KEY_UNITTEST_INFO: unittest_info,
  237. UNITTEST_KEY_TESTOBJECT_INFO: testobject_info,
  238. UNITTEST_KEY_SPECIFICATION: spec,
  239. UNITTEST_KEY_TESTRUNS: [],
  240. }
  241. with open(unittest_filename(unittest_folder, FILES['data-collection']), 'w') as fh:
  242. fh.write(json.dumps(data_collection, indent=4, sort_keys=True))
  243. def unittest_run(unittest_folder, options):
  244. tests = imp.load_source('', os.path.join(unittest_folder, 'src', 'tests', '__init__.py'))
  245. config = imp.load_source('', os.path.join(unittest_folder, 'src', 'config.py'))
  246. #
  247. interpreter_version = 'python ' + '.'.join(['%d' % n for n in sys.version_info[:3]]) + ' (%s)' % sys.version_info[3]
  248. #
  249. execution_level = report.TCEL_REVERSE_NAMED.get(options.execution_level, report.TCEL_FULL)
  250. #
  251. if sys.version_info.major in config.lib.__INTERPRETER__:
  252. print_header("Running \"%s\" Unittest with %s" % (options.execution_level, interpreter_version))
  253. print_action('Loading Testrun data from %s' % unittest_filename(unittest_folder, FILES['data-collection']))
  254. with open(unittest_filename(unittest_folder, FILES['data-collection']), 'r') as fh:
  255. data_collection = json.loads(fh.read())
  256. print_action('Executing Testcases')
  257. heading_dict = {}
  258. for key in data_collection[UNITTEST_KEY_SPECIFICATION].get('item_dict', {}):
  259. heading_dict[key] = data_collection[UNITTEST_KEY_SPECIFICATION]['item_dict'][key]['Heading']
  260. test_session = report.testSession(
  261. ['__unittest__', 'root'],
  262. interpreter=interpreter_version,
  263. testcase_execution_level=execution_level,
  264. testrun_id='p%d' % sys.version_info[0],
  265. heading_dict=heading_dict
  266. )
  267. tests.testrun(test_session)
  268. #
  269. print_action('Adding Testrun data to %s' % unittest_filename(unittest_folder, FILES['data-collection']))
  270. data_collection[UNITTEST_KEY_TESTRUNS].append(test_session)
  271. with open(unittest_filename(unittest_folder, FILES['data-collection']), 'w') as fh:
  272. fh.write(json.dumps(data_collection, indent=4, sort_keys=True))
  273. else:
  274. print_header("Library does not support %s." % interpreter_version)
  275. def unittest_finalise(unittest_folder):
  276. config = imp.load_source('', os.path.join(unittest_folder, 'src', 'config.py'))
  277. #
  278. print_action('Adding Testrun data to %s' % unittest_filename(unittest_folder, FILES['data-collection']))
  279. with open(unittest_filename(unittest_folder, FILES['data-collection']), 'r') as fh:
  280. data_collection = json.loads(fh.read())
  281. #
  282. print_header("Adding Requirement information")
  283. #
  284. data_collection['lost_souls'] = {}
  285. #
  286. print_action("Adding Lost Requirement Soul")
  287. data_collection['lost_souls']['item_list'] = []
  288. for req_id in data_collection['specification'].get('item_dict', {}):
  289. item = data_collection['specification']['item_dict'][req_id]
  290. if item['system_type_uid'] == '_MR7eNHYYEem_kd-7nxt1sg':
  291. testcase_available = False
  292. for testrun in data_collection['testrun_list']:
  293. if req_id in testrun['testcases']:
  294. testcase_available = True
  295. break
  296. if not testcase_available:
  297. data_collection['lost_souls']['item_list'].append(req_id)
  298. print_info('%s - "%s" has no corresponding testcase' % (item['system_uid'], item['Heading']), FAIL)
  299. #
  300. print_action("Adding Lost Testcase Soul")
  301. data_collection['lost_souls']['testcase_list'] = []
  302. for testrun in data_collection['testrun_list']:
  303. for tc_id in testrun.get('testcases', {}):
  304. if tc_id not in data_collection['specification'].get('item_dict', {}) and tc_id not in data_collection['lost_souls']['testcase_list']:
  305. data_collection['lost_souls']['testcase_list'].append(tc_id)
  306. print_info('"%s" has no corresponding testcase' % tc_id, FAIL)
  307. #
  308. print_header("Adding Coverage information")
  309. print_action('Adding Coverage Information to %s' % unittest_filename(unittest_folder, FILES['data-collection']))
  310. data_collection[UNITTEST_KEY_COVERAGE_INFO] = coverage_info(unittest_filename(unittest_folder, 'coverage.xml'), os.path.dirname(config.lib_path))
  311. with open(unittest_filename(unittest_folder, FILES['data-collection']), 'w') as fh:
  312. fh.write(json.dumps(data_collection, indent=4, sort_keys=True))
  313. def unittest_pdf(unittest_folder):
  314. print_header("Creating PDF-Report of Unittest")
  315. print_action('Loading Testrun data from %s' % unittest_filename(unittest_folder, FILES['data-collection']))
  316. with open(unittest_filename(unittest_folder, FILES['data-collection']), 'r') as fh:
  317. data_collection = json.loads(fh.read())
  318. if jinja2 is None:
  319. print_action('You need to install jinja2 to create a PDF-Report!', FAIL)
  320. else:
  321. fn = unittest_filename(unittest_folder, FILES['tex-report'])
  322. print_action('Creating LaTeX-File %s' % fn)
  323. with open(fn, 'w') as fh:
  324. #
  325. template_path = os.path.join(os.path.dirname(__file__), 'templates')
  326. template_filename = 'unittest.tex'
  327. jenv = jinja2.Environment(loader=jinja2.FileSystemLoader(template_path))
  328. template = jenv.get_template(template_filename)
  329. fh.write(template.render(data=data_collection))
  330. print_action('Creating PDF %s' % unittest_filename(unittest_folder, 'unittest.pdf'))
  331. for i in range(3):
  332. sys.stdout.write(' Starting run %d/3 of pdflatex... ' % (i + 1))
  333. sys.stdout.flush()
  334. exit_value = os.system("pdflatex -interaction nonstopmode --output-directory %(path)s %(path)s/unittest.tex 1> /dev/null" % {'path': unittest_filename(unittest_folder, '')})
  335. if exit_value != 0:
  336. print(FAIL + 'FAILED' + ENDC)
  337. break
  338. else:
  339. print(OKGREEN + 'SUCCESS' + ENDC)
  340. def unittest_status(unittest_folder):
  341. config = imp.load_source('', os.path.join(unittest_folder, 'src', 'config.py'))
  342. #
  343. print_header('Checking status of all submodules')
  344. print_action('Updating all submodules (fetch)')
  345. process = subprocess.Popen("LANGUAGE='en_US.UTF-8 git' git submodule foreach git fetch", cwd=os.path.dirname(unittest_folder), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  346. stderroutput = process.communicate()[1]
  347. if stderroutput == b'':
  348. print_info('SUCCESS', color=OKGREEN)
  349. else:
  350. print_info('FAILED', color=FAIL)
  351. print_action('Checking status...')
  352. process = subprocess.Popen("LANGUAGE='en_US.UTF-8 git' git submodule foreach git status", cwd=os.path.dirname(unittest_folder), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  353. stdoutput, stderroutput = process.communicate()
  354. if stderroutput == b'':
  355. module = None
  356. data = {}
  357. for line in stdoutput.splitlines():
  358. line = str(line)
  359. if 'Entering' in line:
  360. m = line[line.index("'") + 1:]
  361. m = str(m[:m.index("'")])
  362. if m != module:
  363. data[m] = ''
  364. module = m
  365. else:
  366. data[m] += line
  367. for key in data:
  368. if "working tree clean" not in data[key] and "working directory clean" not in data[key]:
  369. data[key] = ("local changes", WARNING)
  370. elif "Your branch is behind" in data[key]:
  371. data[key] = ("no up to date (try git pull)", FAIL)
  372. elif "HEAD detached at" in data[key]:
  373. data[key] = ("no up to date (try git checkout master)", FAIL)
  374. elif "Your branch is ahead of" in data[key]:
  375. data[key] = ("push required", WARNING)
  376. elif "nothing to commit" in data[key]:
  377. data[key] = ("clean", OKGREEN)
  378. else:
  379. data[key] = ("unknown", FAIL)
  380. print_info('Submodule %s... %s' % (key, data[key][1] + data[key][0]))
  381. else:
  382. print_info('FAILED', color=FAIL)
  383. #
  384. print_header('Checking status of unittest and testresults in the library')
  385. print_action('Loading Testrun data from %s' % unittest_filename(unittest_folder, FILES['data-collection']))
  386. with open(unittest_filename(unittest_folder, FILES['data-collection']), 'r') as fh:
  387. data_collection = json.loads(fh.read())
  388. print_action('Checking release state of this testrun... ')
  389. if data_collection['testobject_information']['State'] != 'Released':
  390. print_info("FAILED", FAIL)
  391. else:
  392. print_info("SUCCESS", OKGREEN)
  393. #
  394. print_action('Checking up to dateness of testrults in library...')
  395. try:
  396. with open(os.path.join(unittest_folder, '..', 'pylibs', config.lib.__name__, '_testresults_', FILES['data-collection']), 'r') as fh:
  397. lib_result = json.loads(fh.read())
  398. except FileNotFoundError:
  399. print_info("FAILED: Testresults not in library", FAIL)
  400. else:
  401. if data_collection['testobject_information'] != lib_result['testobject_information'] or data_collection['unittest_information'] != lib_result['unittest_information']:
  402. print_info("FAILED", FAIL)
  403. else:
  404. print_info("SUCCESS", OKGREEN)
  405. def unittest_copy(unittest_folder):
  406. config = imp.load_source('', os.path.join(unittest_folder, 'src', 'config.py'))
  407. #
  408. print_header('Copy unittest files to library')
  409. target_folder = os.path.join(config.lib_path, '_testresults_')
  410. print_action('Copying Unittest Files to %s' % target_folder)
  411. if not os.path.exists(target_folder):
  412. print_info('Creating folder %s' % target_folder)
  413. fstools.mkdir(target_folder)
  414. else:
  415. for fn in os.listdir(target_folder):
  416. remove_file(os.path.join(target_folder, fn))
  417. for fn in REPORT_FILES:
  418. src = unittest_filename(unittest_folder, fn)
  419. dst = os.path.join(target_folder, fn)
  420. print_info('copying %s -> %s' % (src, dst))
  421. shutil.copyfile(src, dst)
  422. def unittest_release(unittest_folder):
  423. unittest_uid = module_uid(os.path.join(unittest_folder, 'src', 'tests'))
  424. config_file = os.path.join(unittest_folder, 'src', 'config.py')
  425. print_header('Releasing unittest')
  426. with open(config_file, 'r') as fh:
  427. conf_file = fh.read()
  428. print_action('Setting release_unittest_version = %s in %s' % (unittest_uid, config_file))
  429. with open(config_file, 'w') as fh:
  430. for line in conf_file.splitlines():
  431. if line.startswith('release_unittest_version'):
  432. fh.write("release_unittest_version = '%s'\n" % unittest_uid)
  433. else:
  434. fh.write(line + '\n')