Python Library Unittest
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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