reports.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. from datetime import datetime
  2. from sqlalchemy.orm import sessionmaker
  3. from sqlalchemy import desc, and_
  4. from baangt.base.DataBaseORM import engine, TestrunLog, GlobalAttribute, TestCaseLog, TestCaseSequenceLog, TestCaseField
  5. import baangt.base.GlobalConstants as GC
  6. from baangt.base.PathManagement import ManagedPaths
  7. from jinja2 import Environment, FileSystemLoader
  8. import json
  9. import os
  10. import webbrowser
  11. import uuid
  12. # number of items on history charts
  13. history_items = 10
  14. template_dir = 'templates'
  15. class Report:
  16. #
  17. # Parent reports class
  18. # defines constants and construcor
  19. #
  20. def __init__(self):
  21. self.created = datetime.now()
  22. self.managedPaths = ManagedPaths()
  23. self.generate();
  24. @property
  25. def path(self):
  26. return ''
  27. def generate(self):
  28. #
  29. # report generator
  30. #
  31. pass
  32. def show(self):
  33. #
  34. # shows html report
  35. #
  36. webbrowser.open(f'file://{self.path}', new=2)
  37. def template(self, template_name):
  38. #
  39. # returns jinja2 template
  40. #
  41. file_loader = FileSystemLoader(os.path.join(os.path.dirname(__file__), template_dir))
  42. env = Environment(loader=file_loader)
  43. return env.get_template(template_name)
  44. def chart_figures(self, log):
  45. #
  46. # builds json with log statistics
  47. #
  48. return {
  49. 'records': log.recordCount,
  50. 'successful': log.statusOk,
  51. 'error': log.statusFailed,
  52. 'paused': log.statusPaused,
  53. }
  54. def time_in_seconds(self, time):
  55. time_int = time.split('.')[0]
  56. factors = [3600, 60, 1]
  57. return sum(t*f for t,f in zip(map(int, time_int.split(':')), factors))
  58. def chart_status(self, log):
  59. #
  60. # builds json for Chart.js: log statistics
  61. #
  62. return json.dumps({
  63. 'type': 'doughnut',
  64. 'data': {
  65. 'datasets': [{
  66. 'data': [
  67. log.statusOk,
  68. log.statusFailed,
  69. log.statusPaused,
  70. ],
  71. 'backgroundColor': [
  72. '#52ff52',
  73. '#ff5252',
  74. '#bdbdbd',
  75. ],
  76. }],
  77. 'labels': [
  78. 'Passed',
  79. 'Failed',
  80. 'Paused'
  81. ],
  82. },
  83. 'options': {
  84. 'legend': {
  85. 'display': False,
  86. },
  87. },
  88. })
  89. def chart_results(self, logs):
  90. #
  91. # builds json for Chart.js: history of log results
  92. #
  93. logs = logs[-history_items:]
  94. empty = history_items - len(logs)
  95. return json.dumps({
  96. 'type': 'bar',
  97. 'data': {
  98. 'datasets': [
  99. {
  100. 'data': [x.statusOk for x in logs] + [None]*empty,
  101. 'backgroundColor': '#52ff52',
  102. 'label': 'Passed',
  103. },
  104. {
  105. 'data': [x.statusFailed for x in logs] + [None]*empty,
  106. 'backgroundColor': '#ff5252',
  107. 'label': 'Failed',
  108. },
  109. {
  110. 'data': [x.statusPaused for x in logs] + [None]*empty,
  111. 'backgroundColor': '#bdbdbd',
  112. 'label': 'Paused',
  113. },
  114. ],
  115. 'labels': [x.startTime.strftime('%Y-%m-%d %H:%M') for x in logs] + [None]*empty,
  116. },
  117. 'options': {
  118. 'legend': {
  119. 'display': False,
  120. },
  121. 'tooltips': {
  122. 'mode': 'index',
  123. 'intersect': False,
  124. },
  125. 'scales': {
  126. 'xAxes': [{
  127. 'gridLines': {
  128. 'display': False,
  129. 'drawBorder': False,
  130. },
  131. 'ticks': {
  132. 'display': False,
  133. },
  134. 'stacked': True,
  135. }],
  136. 'yAxes': [{
  137. 'gridLines': {
  138. 'display': False,
  139. 'drawBorder': False,
  140. },
  141. 'ticks': {
  142. 'display': False,
  143. },
  144. 'stacked': True,
  145. }],
  146. },
  147. },
  148. })
  149. def chart_duration(self, logs):
  150. #
  151. # builds json for Chart.js: history of log durations
  152. #
  153. logs = logs[-history_items:]
  154. empty = history_items - len(logs)
  155. return json.dumps({
  156. 'type': 'line',
  157. 'data': {
  158. 'datasets': [{
  159. 'data': [str(x.duration) for x in logs] + [None]*empty,
  160. 'fill': False,
  161. 'borderColor': '#cfd8dc',
  162. 'borderWidth': 1,
  163. 'pointBackgroundColor': 'transparent',
  164. 'pointBorderColor': '#0277bd',
  165. 'pointRadius': 5,
  166. 'lineTension': 0,
  167. 'label': 'Duration',
  168. }],
  169. 'labels': [x.startTime.strftime('%Y-%m-%d %H:%M') for x in logs] + [None]*empty,
  170. },
  171. 'options': {
  172. 'legend': {
  173. 'display': False,
  174. },
  175. 'tooltips': {
  176. 'mode': 'index',
  177. 'intersect': False,
  178. },
  179. 'layout': {
  180. 'padding': {
  181. 'top': 10,
  182. 'right': 10,
  183. },
  184. },
  185. 'scales': {
  186. 'xAxes': [{
  187. 'gridLines': {
  188. 'display': False,
  189. 'drawBorder': False,
  190. },
  191. 'ticks': {
  192. 'display': False,
  193. },
  194. }],
  195. 'yAxes': [{
  196. 'gridLines': {
  197. 'display': False,
  198. 'drawBorder': False,
  199. },
  200. 'ticks': {
  201. 'display': False,
  202. },
  203. }],
  204. },
  205. },
  206. })
  207. def chart_testcases(self, logs):
  208. #
  209. # builds json for Chart.js: history of log results
  210. #
  211. tc = logs[-history_items:]
  212. empty = history_items - len(tc)
  213. return json.dumps({
  214. 'type': 'bar',
  215. 'data': {
  216. 'datasets': [
  217. {
  218. 'data': [self.time_in_seconds(t['duration']) if t['status'] == GC.TESTCASESTATUS_SUCCESS else 0 for t in tc] + [None]*empty,
  219. 'backgroundColor': '#52ff52',
  220. 'label': 'PASSED',
  221. },
  222. {
  223. 'data': [self.time_in_seconds(t['duration']) if t['status'] == GC.TESTCASESTATUS_ERROR else 0 for t in tc] + [None]*empty,
  224. 'backgroundColor': '#ff5252',
  225. 'label': 'FAILED',
  226. },
  227. {
  228. 'data': [self.time_in_seconds(t['duration']) if t['status'] == GC.TESTCASESTATUS_WAITING else 0 for t in tc] + [None]*empty,
  229. 'backgroundColor': '#bdbdbd',
  230. 'label': 'PAUSED',
  231. },
  232. ],
  233. 'labels': [t['duration'] for t in tc] + [None]*empty,
  234. },
  235. 'options': {
  236. 'legend': {
  237. 'display': False,
  238. },
  239. 'tooltips': {
  240. 'mode': 'index',
  241. 'intersect': False,
  242. },
  243. 'scales': {
  244. 'xAxes': [{
  245. 'gridLines': {
  246. 'display': False,
  247. 'drawBorder': False,
  248. },
  249. 'ticks': {
  250. 'display': False,
  251. },
  252. 'stacked': True,
  253. }],
  254. 'yAxes': [{
  255. 'gridLines': {
  256. 'display': False,
  257. 'drawBorder': False,
  258. },
  259. 'ticks': {
  260. 'display': False,
  261. },
  262. 'stacked': True,
  263. }],
  264. },
  265. },
  266. })
  267. class Dashboard(Report):
  268. def __init__(self, name=None, stage=None):
  269. self.name = name
  270. self.stage = stage
  271. super().__init__()
  272. @property
  273. def path(self):
  274. #
  275. # path to report
  276. #
  277. return self.managedPaths.getOrSetReportPath().joinpath(self.created.strftime('dashboard-%Y%m%d-%H%M%S.html'))
  278. def generate(self):
  279. #
  280. # generates HTML report
  281. #
  282. # get jinja2 template
  283. template = self.template('dashboard.html')
  284. data = self.get_data() or None
  285. if data is None:
  286. error_msg = 'No TestrunLog maches:'
  287. if self.name:
  288. error_msg = f'{error_msg} Name "{self.name}"'
  289. if self.stage:
  290. error_msg = f'{error_msg},'
  291. if self.stage:
  292. error_msg = f'{error_msg} Stage "{self.stage}"'
  293. raise ValueError(error_msg)
  294. # generate report
  295. with open(self.path, 'w') as f:
  296. f.write(template.render(
  297. type='Dashboard',
  298. data=data,
  299. created=self.created.strftime('%Y-%m-%d %H:%M:%S'),
  300. name=self.name,
  301. stage=self.stage,
  302. ))
  303. def get_data(self):
  304. #
  305. # get data from db for the report
  306. #
  307. db = sessionmaker(bind=engine)()
  308. records = []
  309. if self.name and self.stage:
  310. logs = db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=self.name)\
  311. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==self.stage))).all()
  312. return {'records': [self.build_charts(logs)]}
  313. elif self.name:
  314. # get Testrun stages
  315. stages = db.query(GlobalAttribute.value).filter(GlobalAttribute.testrun.has(TestrunLog.testrunName==self.name))\
  316. .filter_by(name=GC.EXECUTION_STAGE).group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
  317. stages = [x[0] for x in stages]
  318. for stage in stages:
  319. logs = db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=self.name)\
  320. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage))).all()
  321. records.append(self.build_charts(logs, stage=stage))
  322. return {'records': records}
  323. elif self.stage:
  324. # get Testrun names
  325. names = db.query(TestrunLog.testrunName)\
  326. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==self.stage)))\
  327. .group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
  328. names = [x[0] for x in names]
  329. for name in names:
  330. logs = db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=name)\
  331. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==self.stage))).all()
  332. records.append(self.build_charts(logs, name=name))
  333. return {'records': records}
  334. else:
  335. # get Testrun names
  336. names = db.query(TestrunLog.testrunName)\
  337. .group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
  338. names = [x[0] for x in names]
  339. stage_set = set()
  340. for name in names:
  341. # get Testrun stages
  342. stages = db.query(GlobalAttribute.value).filter(GlobalAttribute.testrun.has(TestrunLog.testrunName==name))\
  343. .filter_by(name=GC.EXECUTION_STAGE).group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
  344. stages = [x[0] for x in stages]
  345. stage_set.update(stages)
  346. for stage in stages:
  347. logs = db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=name)\
  348. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage))).all()
  349. records.append(self.build_charts(logs, name=name, stage=stage))
  350. return {
  351. 'names': names,
  352. 'stages': list(stage_set),
  353. 'records': records,
  354. }
  355. def build_charts(self, logs, name=None, stage=None):
  356. #
  357. # builds Chart.js data collections
  358. #
  359. name = self.name or name
  360. stage = self.stage or stage
  361. if logs:
  362. return {
  363. 'id': f'record-{uuid.uuid4()}',
  364. 'name': name,
  365. 'stage': stage,
  366. 'figures': self.chart_figures(logs[-1]),
  367. 'status': self.chart_status(logs[-1]),
  368. 'results': self.chart_results(logs),
  369. 'duration': self.chart_duration(logs),
  370. }
  371. else:
  372. error_msg = 'No TestrunLog maches:'
  373. if self.name:
  374. error_msg = f'{error_msg} Name "{self.name}"'
  375. if self.stage:
  376. error_msg = f'{error_msg},'
  377. if self.stage:
  378. error_msg = f'{error_msg} Stage "{self.stage}"'
  379. raise ValueError(error_msg)
  380. class Summary(Report):
  381. def __init__(self, id):
  382. self.id = id
  383. super().__init__()
  384. @property
  385. def path(self):
  386. #
  387. # path to report
  388. #
  389. return self.managedPaths.getOrSetReportPath().joinpath(self.created.strftime('summary-%Y%m%d-%H%M%S.html'))
  390. def generate(self):
  391. #
  392. # generates HTML report
  393. #
  394. # get jinja2 template
  395. template = self.template('summary.html')
  396. # generate report
  397. with open(self.path, 'w') as f:
  398. f.write(template.render(
  399. type='Report',
  400. data=self.get_data(),
  401. created=self.created.strftime('%Y-%m-%d %H:%M:%S'),
  402. ))
  403. def get_data(self):
  404. #
  405. # get data from db for the report
  406. #
  407. db = sessionmaker(bind=engine)()
  408. log = db.query(TestrunLog).get(uuid.UUID(self.id).bytes)
  409. if log is None:
  410. raise ValueError(f'TestrunLog {self.id} does not exist')
  411. # collect TestCases and screenshots
  412. testcases = []
  413. screenshots = []
  414. for index, tc in enumerate(db.query(TestCaseLog).filter(TestCaseLog.testcase_sequence.has(TestCaseSequenceLog.testrun_id == log.id))):
  415. testcases.append({
  416. 'status': db.query(TestCaseField.value).filter_by(name=GC.TESTCASESTATUS).filter(TestCaseField.testcase_id == tc.id).first()[0],
  417. 'duration': db.query(TestCaseField.value).filter_by(name=GC.TIMING_DURATION).filter(TestCaseField.testcase_id == tc.id).first()[0],
  418. })
  419. tc_screenshots = db.query(TestCaseField.value).filter_by(name=GC.SCREENSHOTS).filter(TestCaseField.testcase_id == tc.id).first()[0]
  420. if tc_screenshots:
  421. for shot in json.loads(tc_screenshots.replace("'", '"')):
  422. screenshots.append({
  423. 'index': index + 1,
  424. 'path': shot,
  425. })
  426. # collect files
  427. files = [
  428. {
  429. 'name': 'Log',
  430. 'path': log.logfileName,
  431. },
  432. {
  433. 'name': 'Results',
  434. 'path': log.dataFile,
  435. }
  436. ]
  437. data = {
  438. 'id': self.id,
  439. 'time': log.startTime.strftime('%Y-%m-%d %H:%M:%S'),
  440. 'name': log.testrunName,
  441. 'figures': self.chart_figures(log),
  442. 'status': self.chart_status(log),
  443. 'testcases': self.chart_testcases(testcases),
  444. 'screenshots': screenshots,
  445. 'files': files,
  446. }
  447. return data