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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. import fstools
  5. from unittest import jsonlog
  6. from unittest import output
  7. import report
  8. import reqif
  9. import json
  10. import os
  11. import sys
  12. import platform
  13. try:
  14. from platform import dist as dist
  15. except ImportError:
  16. from distro import linux_distribution as dist
  17. import getpass
  18. import subprocess
  19. import imp
  20. import xml.dom.minidom
  21. try:
  22. import jinja2
  23. except ImportError:
  24. jinja2 = None
  25. import shutil
  26. FN_DATA_COLLECTION = 'unittest.json'
  27. FN_TEX_REPORT = 'unittest.tex'
  28. FN_PDF_REPORT = 'unittest.pdf'
  29. FN_COVERAGE = 'coverage.xml'
  30. REPORT_FILES = [FN_DATA_COLLECTION, FN_COVERAGE, FN_PDF_REPORT]
  31. def testresults_filename(ut_folder, filename):
  32. return os.path.join(jsonlog.get_ut_testresult_folder(ut_folder), filename)
  33. def remove_file(filename):
  34. if os.path.exists(filename) and not filename.endswith('.gitkeep'):
  35. try:
  36. output.print_info('Removing %s' % filename)
  37. os.remove(filename)
  38. except OSError:
  39. pass
  40. class coverage_info(list):
  41. KEY_FRAGMENTS = 'fragments'
  42. KEY_START_LINE = 'start'
  43. KEY_END_LINE = 'end'
  44. KEY_COVERAGE_STATE = 'coverage_state'
  45. COVERED = 'covered'
  46. UNCOVERED = 'uncovered'
  47. CLEAN = 'clean'
  48. PARTIALLY_COVERED = 'partially-covered'
  49. def __init__(self, xml_filename, module_basepath):
  50. list.__init__(self)
  51. xmldoc = xml.dom.minidom.parse(xml_filename)
  52. itemlist = xmldoc.getElementsByTagName('package')
  53. for p in itemlist:
  54. module = {}
  55. module[jsonlog.COVI_KEY_NAME] = p.attributes['name'].value[len(module_basepath) + 1:]
  56. module[jsonlog.COVI_KEY_FILEPATH] = p.attributes['name'].value.replace('.', os.path.sep)
  57. module[jsonlog.COVI_KEY_LINE_COVERAGE] = float(p.attributes['line-rate'].value) * 100.
  58. try:
  59. module[jsonlog.COVI_KEY_BRANCH_COVERAGE] = float(p.attributes['branch-rate'].value) * 100.
  60. except AttributeError:
  61. module[jsonlog.COVI_KEY_BRANCH_COVERAGE] = None
  62. module[jsonlog.COVI_KEY_FILES] = []
  63. for c in p.getElementsByTagName('class'):
  64. f = {}
  65. f[jsonlog.COVI_KEY_NAME] = c.attributes['filename'].value[len(module_basepath) + 1:].replace(os.path.sep, '.')
  66. f[jsonlog.COVI_KEY_FILEPATH] = c.attributes['filename'].value
  67. f[jsonlog.COVI_KEY_LINE_COVERAGE] = float(c.attributes['line-rate'].value) * 100.
  68. try:
  69. f[jsonlog.COVI_KEY_BRANCH_COVERAGE] = float(p.attributes['branch-rate'].value) * 100.
  70. except Exception:
  71. f[jsonlog.COVI_KEY_BRANCH_COVERAGE] = None
  72. f[self.KEY_FRAGMENTS] = []
  73. last_hit = None
  74. start_line = 1
  75. end_line = 1
  76. for line in c.getElementsByTagName('line'):
  77. line_no = int(line.attributes['number'].value)
  78. hit = bool(int(line.attributes['hits'].value))
  79. if hit:
  80. cc = line.attributes.get('condition-coverage')
  81. if cc is not None and not cc.value.startswith('100%'):
  82. hit = self.PARTIALLY_COVERED
  83. else:
  84. hit = self.COVERED
  85. else:
  86. hit = self.UNCOVERED
  87. if line_no == 1:
  88. last_hit = hit
  89. elif last_hit != hit or line_no > end_line + 1:
  90. if last_hit is not None:
  91. line = {}
  92. line[self.KEY_START_LINE] = start_line
  93. line[self.KEY_END_LINE] = end_line
  94. line[self.KEY_COVERAGE_STATE] = last_hit
  95. f[self.KEY_FRAGMENTS].append(line)
  96. if line_no > end_line + 1:
  97. line = {}
  98. if last_hit is not None:
  99. line[self.KEY_START_LINE] = end_line + 1
  100. else:
  101. line[self.KEY_START_LINE] = start_line
  102. line[self.KEY_END_LINE] = line_no - 1
  103. line[self.KEY_COVERAGE_STATE] = self.CLEAN
  104. f[self.KEY_FRAGMENTS].append(line)
  105. start_line = line_no
  106. end_line = line_no
  107. last_hit = hit
  108. elif line_no == end_line + 1:
  109. end_line = line_no
  110. if last_hit is not None:
  111. line = {}
  112. line[self.KEY_START_LINE] = start_line
  113. line[self.KEY_END_LINE] = end_line
  114. line[self.KEY_COVERAGE_STATE] = last_hit
  115. f[self.KEY_FRAGMENTS].append(line)
  116. line = {}
  117. if last_hit is not None:
  118. line[self.KEY_START_LINE] = end_line + 1
  119. else:
  120. line[self.KEY_START_LINE] = start_line
  121. line[self.KEY_END_LINE] = None
  122. line[self.KEY_COVERAGE_STATE] = self.CLEAN
  123. f[self.KEY_FRAGMENTS].append(line)
  124. module[jsonlog.COVI_KEY_FILES].append(f)
  125. self.append(module)
  126. def __str__(self):
  127. rv = ''
  128. for module in self:
  129. rv += '%s (%.1f%% - %s)\n' % (module.get(jsonlog.COVI_KEY_NAME), module.get(jsonlog.COVI_KEY_LINE_COVERAGE), module.get(jsonlog.COVI_KEY_FILEPATH))
  130. for py_file in module.get(jsonlog.COVI_KEY_FILES):
  131. rv += ' %s (%.1f%% - %s)\n' % (py_file.get(jsonlog.COVI_KEY_NAME), py_file.get(jsonlog.COVI_KEY_LINE_COVERAGE), py_file.get(jsonlog.COVI_KEY_FILEPATH))
  132. for fragment in py_file.get(self.KEY_FRAGMENTS):
  133. if fragment.get(self.KEY_END_LINE) is not None:
  134. rv += ' %d - %d: %s\n' % (fragment.get(self.KEY_START_LINE), fragment.get(self.KEY_END_LINE), repr(fragment.get(self.KEY_COVERAGE_STATE)))
  135. else:
  136. rv += ' %d - : %s\n' % (fragment.get(self.KEY_START_LINE), repr(fragment.get(self.COVERAGE_STATE)))
  137. return rv
  138. def unittest(options, args, unittest_folder):
  139. if 'release_testcases' in args:
  140. unittest_release_testcases(unittest_folder)
  141. elif 'prepare' in args:
  142. unittest_prepare(unittest_folder)
  143. elif 'testrun' in args:
  144. unittest_testrun(unittest_folder, options)
  145. elif 'finalise' in args:
  146. unittest_finalise(unittest_folder)
  147. elif 'status' in args:
  148. unittest_status(unittest_folder)
  149. elif 'publish' in args:
  150. unittest_publish(unittest_folder)
  151. def unittest_release_testcases(ut_folder):
  152. unittest_uid = jsonlog.module_uid(jsonlog.get_ut_testcase_folder(ut_folder))
  153. output.print_header('Releasing unittest')
  154. config_file = jsonlog.get_ut_config(ut_folder)
  155. with open(config_file, 'r') as fh:
  156. conf_file = fh.read()
  157. output.print_action('Setting release_unittest_version = %s in %s' % (unittest_uid, config_file))
  158. with open(config_file, 'w') as fh:
  159. for line in conf_file.splitlines():
  160. if line.startswith('release_unittest_version'):
  161. fh.write("release_unittest_version = '%s'\n" % unittest_uid)
  162. else:
  163. fh.write(line + '\n')
  164. def unittest_prepare(ut_folder):
  165. config = imp.load_source('', jsonlog.get_ut_config(ut_folder))
  166. #
  167. output.print_header("Initiating unittest for first testrun...")
  168. if not os.path.exists(testresults_filename(ut_folder, '')):
  169. output.print_action('Creating outpout folder %s' % testresults_filename(ut_folder, ''))
  170. fstools.mkdir(testresults_filename(ut_folder, ''))
  171. #
  172. output.print_action('Creating unittest data-collection: %s' % testresults_filename(ut_folder, FN_DATA_COLLECTION))
  173. #
  174. system_info = {}
  175. system_info[jsonlog.SYSI_ARCHITECTURE] = platform.architecture()[0]
  176. system_info[jsonlog.SYSI_MACHINE] = platform.machine()
  177. system_info[jsonlog.SYSI_HOSTNAME] = platform.node()
  178. system_info[jsonlog.SYSI_DISTRIBUTION] = ' '.join(dist())
  179. system_info[jsonlog.SYSI_SYSTEM] = platform.system()
  180. system_info[jsonlog.SYSI_KERNEL] = platform.release() + ' (%s)' % platform.version()
  181. system_info[jsonlog.SYSI_USERNAME] = getpass.getuser()
  182. system_info[jsonlog.SYSI_PATH] = ut_folder
  183. #
  184. unittest_info = {}
  185. unittest_info[jsonlog.UTEI_VERSION] = jsonlog.module_uid(jsonlog.get_ut_testcase_folder(ut_folder))
  186. #
  187. testobject_info = {}
  188. testobject_info[jsonlog.TOBI_NAME] = config.lib.__name__
  189. testobject_info[jsonlog.TOBI_VERSION] = jsonlog.module_uid(config.lib.__path__[0])
  190. testobject_info[jsonlog.TOBI_DESCRIPTION] = config.lib.__DESCRIPTION__
  191. testobject_info[jsonlog.TOBI_SUPP_INTERP] = ', '.join(['python%d' % vers for vers in config.lib.__INTERPRETER__])
  192. testobject_info[jsonlog.TOBI_STATE] = jsonlog.TOBI_STATE_RELEASED if config.release_unittest_version == unittest_info[jsonlog.UTEI_VERSION] else jsonlog.TOBI_STATE_IN_DEVELOPMENT
  193. testobject_info[jsonlog.TOBI_DEPENDENCIES] = []
  194. for dependency in config.lib.__DEPENDENCIES__:
  195. testobject_info[jsonlog.TOBI_DEPENDENCIES].append((dependency, jsonlog.module_uid(os.path.join(jsonlog.get_ut_src_folder(ut_folder), dependency))))
  196. #
  197. spec_filename = os.path.join(ut_folder, 'requirements', 'specification.reqif')
  198. output.print_action("Adding Requirement Specification from %s" % spec_filename)
  199. try:
  200. spec = reqif.reqif_dict(spec_filename, 'Heading', 'Software Specification')
  201. except FileNotFoundError:
  202. output.print_info(output.STATUS_FAILED)
  203. spec = {}
  204. else:
  205. output.print_info(output.STATUS_SUCCESS)
  206. #
  207. data_collection = {
  208. jsonlog.MAIN_KEY_SYSTEM_INFO: system_info,
  209. jsonlog.MAIN_KEY_UNITTEST_INFO: unittest_info,
  210. jsonlog.MAIN_KEY_TESTOBJECT_INFO: testobject_info,
  211. jsonlog.MAIN_KEY_SPECIFICATION: spec,
  212. jsonlog.MAIN_KEY_TESTRUNS: [],
  213. }
  214. with open(testresults_filename(ut_folder, FN_DATA_COLLECTION), 'w') as fh:
  215. fh.write(json.dumps(data_collection, indent=4, sort_keys=True))
  216. def unittest_testrun(ut_folder, options):
  217. tests = imp.load_source('', os.path.join(jsonlog.get_ut_testcase_folder(ut_folder), '__init__.py'))
  218. config = imp.load_source('', jsonlog.get_ut_config(ut_folder))
  219. #
  220. interpreter_version = 'python ' + '.'.join(['%d' % n for n in sys.version_info[:3]]) + ' (%s)' % sys.version_info[3]
  221. #
  222. execution_level = report.TCEL_REVERSE_NAMED.get(options.execution_level, report.TCEL_FULL)
  223. #
  224. if sys.version_info.major in config.lib.__INTERPRETER__:
  225. output.print_header("Running \"%s\" Unittest with %s" % (options.execution_level, interpreter_version))
  226. with open(testresults_filename(ut_folder, FN_DATA_COLLECTION), 'r') as fh:
  227. data_collection = json.loads(fh.read())
  228. output.print_action('Executing Testcases')
  229. heading_dict = {}
  230. for key in data_collection[jsonlog.MAIN_KEY_SPECIFICATION].get(jsonlog.SPEC_ITEM_DICT, {}):
  231. heading_dict[key] = data_collection[jsonlog.MAIN_KEY_SPECIFICATION][jsonlog.SPEC_ITEM_DICT][key]['Heading']
  232. test_session = report.testSession(
  233. ['__unittest__', 'root', config.lib.__name__],
  234. interpreter=interpreter_version,
  235. testcase_execution_level=execution_level,
  236. testrun_id='p%d' % sys.version_info[0],
  237. heading_dict=heading_dict
  238. )
  239. tests.testrun(test_session)
  240. #
  241. output.print_action('Adding Testrun data to %s' % testresults_filename(ut_folder, FN_DATA_COLLECTION))
  242. data_collection[jsonlog.MAIN_KEY_TESTRUNS].append(test_session)
  243. with open(testresults_filename(ut_folder, FN_DATA_COLLECTION), 'w') as fh:
  244. fh.write(json.dumps(data_collection, indent=4, sort_keys=True))
  245. else:
  246. output.print_header("Library does not support %s." % interpreter_version)
  247. def unittest_finalise(ut_folder):
  248. config = imp.load_source('', jsonlog.get_ut_config(ut_folder))
  249. #
  250. output.print_header("Adding Requirement information")
  251. #
  252. with open(testresults_filename(ut_folder, FN_DATA_COLLECTION), 'r') as fh:
  253. data_collection = json.loads(fh.read())
  254. #
  255. data_collection[jsonlog.MAIN_KEY_LOST_SOULS] = {}
  256. #
  257. output.print_action("Adding Lost Requirement Soul")
  258. data_collection[jsonlog.MAIN_KEY_LOST_SOULS][jsonlog.LOST_ITEMLIST] = []
  259. for req_id in data_collection[jsonlog.MAIN_KEY_SPECIFICATION].get(jsonlog.SPEC_ITEM_DICT, {}):
  260. item = data_collection[jsonlog.MAIN_KEY_SPECIFICATION][jsonlog.SPEC_ITEM_DICT][req_id]
  261. if item['system_type_uid'] == '_MR7eNHYYEem_kd-7nxt1sg':
  262. testcase_available = False
  263. for testrun in data_collection[jsonlog.MAIN_KEY_TESTRUNS]:
  264. if req_id in testrun[jsonlog.TRUN_TESTCASES]:
  265. testcase_available = True
  266. break
  267. if not testcase_available:
  268. data_collection[jsonlog.MAIN_KEY_LOST_SOULS][jsonlog.LOST_ITEMLIST].append(req_id)
  269. output.print_info('%s - "%s" has no corresponding testcase' % (item['system_uid'], item['Heading']), output.termcolors.FAIL)
  270. #
  271. output.print_action("Adding Lost Testcase Soul")
  272. data_collection[jsonlog.MAIN_KEY_LOST_SOULS][jsonlog.LOST_TESTCASELIST] = []
  273. for testrun in data_collection[jsonlog.MAIN_KEY_TESTRUNS]:
  274. for tc_id in testrun.get(jsonlog.TRUN_TESTCASES, {}):
  275. if tc_id not in data_collection[jsonlog.MAIN_KEY_SPECIFICATION].get(jsonlog.SPEC_ITEM_DICT, {}) and tc_id not in data_collection[jsonlog.MAIN_KEY_LOST_SOULS][jsonlog.LOST_TESTCASELIST]:
  276. data_collection[jsonlog.MAIN_KEY_LOST_SOULS][jsonlog.LOST_TESTCASELIST].append(tc_id)
  277. output.print_info('"%s" has no corresponding testcase' % tc_id, output.termcolors.FAIL)
  278. #
  279. output.print_header("Adding Coverage information")
  280. output.print_action('Adding Coverage Information to %s' % testresults_filename(ut_folder, FN_DATA_COLLECTION))
  281. data_collection[jsonlog.MAIN_KEY_COVERAGE_INFO] = coverage_info(testresults_filename(ut_folder, FN_COVERAGE), os.path.dirname(config.lib_path))
  282. with open(testresults_filename(ut_folder, FN_DATA_COLLECTION), 'w') as fh:
  283. fh.write(json.dumps(data_collection, indent=4, sort_keys=True))
  284. #
  285. output.print_header("Creating LaTeX-Report of Unittest")
  286. with open(testresults_filename(ut_folder, FN_DATA_COLLECTION), 'r') as fh:
  287. data_collection = json.loads(fh.read())
  288. if jinja2 is None:
  289. output.print_action('You need to install jinja2 to create a LaTeX-Report!', output.termcolors.FAIL)
  290. else:
  291. fn = testresults_filename(ut_folder, FN_TEX_REPORT)
  292. output.print_action('Creating LaTeX-File %s' % fn)
  293. with open(fn, 'w') as fh:
  294. #
  295. template_path = os.path.join(os.path.dirname(__file__), 'templates')
  296. template_filename = 'unittest.tex'
  297. jenv = jinja2.Environment(loader=jinja2.FileSystemLoader(template_path))
  298. template = jenv.get_template(template_filename)
  299. fh.write(template.render(data=data_collection))
  300. def unittest_publish(ut_folder):
  301. config = imp.load_source('', jsonlog.get_ut_config(ut_folder))
  302. output.print_header('Checking testrun state')
  303. output.print_action('Release State...')
  304. rs = jsonlog.get_ut_release_state(ut_folder)
  305. output.print_info(rs)
  306. output.print_action('Testcase Integrity...')
  307. tci = jsonlog.get_ut_testcase_integrity(ut_folder)
  308. output.print_info(tci)
  309. output.print_action('Source Integrity...')
  310. sri = jsonlog.get_ut_src_integrity(ut_folder)
  311. output.print_info(sri)
  312. if rs == jsonlog.STATUS_RELEASED and tci == jsonlog.STATUS_CLEAN and sri == jsonlog.STATUS_CLEAN:
  313. output.print_header('Copy unittest files to library')
  314. target_folder = os.path.join(config.lib_path, '_testresults_')
  315. output.print_action('Copying Unittest Files to %s' % target_folder)
  316. if not os.path.exists(target_folder):
  317. output.print_info('Creating folder %s' % target_folder)
  318. fstools.mkdir(target_folder)
  319. else:
  320. for fn in os.listdir(target_folder):
  321. remove_file(os.path.join(target_folder, fn))
  322. for fn in REPORT_FILES:
  323. src = testresults_filename(ut_folder, fn)
  324. dst = os.path.join(target_folder, fn)
  325. output.print_info('copying %s -> %s' % (src, dst))
  326. shutil.copyfile(src, dst)
  327. else:
  328. output.print_action('Proceed Conditions...')
  329. output.print_info(output.STATUS_FAILED)
  330. def unittest_status(ut_folder):
  331. #
  332. # GIT STATUS
  333. #
  334. output.print_header('Checking GIT repository status')
  335. # GIT FETCH
  336. output.print_action('Fetching repository from server...')
  337. process = subprocess.Popen("LANGUAGE='en_US.UTF-8 git' git submodule foreach git fetch", cwd=ut_folder, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  338. stderroutput = process.communicate()[1]
  339. if stderroutput == b'':
  340. output.print_info(output.STATUS_SUCCESS)
  341. else:
  342. output.print_info(output.STATUS_FAILED)
  343. # GIT_REPO
  344. output.print_action('Analysing repository status...')
  345. output.print_info(jsonlog.status_git(ut_folder))
  346. # SUBMODULES
  347. output.print_action('Analysing submodule status...')
  348. process = subprocess.Popen("LANGUAGE='en_US.UTF-8 git' git submodule foreach git status", cwd=ut_folder, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  349. stdoutput, stderroutput = process.communicate()
  350. if stderroutput == b'':
  351. module = None
  352. data = {}
  353. for line in stdoutput.splitlines():
  354. line = str(line)
  355. if 'Entering' in line:
  356. m = line[line.index("'") + 1:]
  357. m = str(m[:m.index("'")])
  358. if m != module:
  359. data[m] = ''
  360. module = m
  361. else:
  362. data[m] += line
  363. for key in data:
  364. if "working tree clean" not in data[key] and "working directory clean" not in data[key]:
  365. data[key] = ("LOCAL CHANGES", output.termcolors.WARNING)
  366. elif "Your branch is behind" in data[key]:
  367. data[key] = ("OUTDATED (try git pull)", output.termcolors.WARNING)
  368. elif "HEAD detached at" in data[key]:
  369. data[key] = ("OUTDATED (try git checkout master)", output.termcolors.WARNING)
  370. elif "Your branch is ahead of" in data[key]:
  371. data[key] = ("CHANGED (try git push)", output.termcolors.WARNING)
  372. elif "nothing to commit" in data[key]:
  373. data[key] = ("CLEAN", output.termcolors.OKGREEN)
  374. else:
  375. data[key] = ("UNKNOWN", output.termcolors.FAIL)
  376. output.print_info('Submodule %s... %s' % (key, data[key][1] + data[key][0]))
  377. else:
  378. output.print_info(output.STATUS_FAILED)
  379. #
  380. # TESTRUN STATUS
  381. #
  382. output.print_header('Checking status of unittest in the library')
  383. for txt, fcn in (
  384. ('Checking release state... ', jsonlog.get_lib_release_state),
  385. ('Checking testcase integrity... ', jsonlog.get_lib_testcase_integrity),
  386. ('Checking source integrity... ', jsonlog.get_lib_src_integrity)
  387. ):
  388. output.print_action(txt)
  389. output.print_info(fcn(ut_folder))
  390. output.print_action('Checking code coverage... ')
  391. output.print_coverage(*jsonlog.lib_coverage(ut_folder))
  392. #
  393. output.print_header('Checking status of unittest for this testrun')
  394. for txt, fcn in (
  395. ('Checking release state... ', jsonlog.get_ut_release_state),
  396. ('Checking testcase integrity... ', jsonlog.get_ut_testcase_integrity),
  397. ('Checking source integrity... ', jsonlog.get_ut_src_integrity)
  398. ):
  399. output.print_action(txt)
  400. output.print_info(fcn(ut_folder))
  401. output.print_action('Checking code coverage... ')
  402. output.print_coverage(*jsonlog.ut_coverage(ut_folder))