ResultsBrowser.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. from sqlalchemy import create_engine, desc, and_
  2. from sqlalchemy.orm import sessionmaker
  3. from baangt.base.DataBaseORM import DATABASE_URL, TestrunLog, GlobalAttribute, TestCaseLog, TestCaseSequenceLog, TestCaseField
  4. from baangt.base.ExportResults.ExportResults import ExcelSheetHelperFunctions
  5. from baangt.base.PathManagement import ManagedPaths
  6. import baangt.base.GlobalConstants as GC
  7. import uuid
  8. from datetime import datetime
  9. from xlsxwriter import Workbook
  10. import logging
  11. import os
  12. import json
  13. import re
  14. import uuid
  15. import time # time tracker
  16. logger = logging.getLogger("pyC")
  17. class QuerySet:
  18. # flags
  19. SIZE_TCS = 0
  20. SIZE_TC = 1
  21. def __init__(self):
  22. self.data = None
  23. def __del__(self):
  24. del self.data
  25. @property
  26. def length(self):
  27. return len(self.data)
  28. def set(self, array):
  29. self.data = array
  30. def names(self):
  31. return {tr.testrunName for tr in self.data}
  32. def all(self):
  33. return (tr for tr in self.data)
  34. def filter(self, tr_name, tcs_index=None, tc_index=None):
  35. #
  36. # returns generator of filtered query_set
  37. #
  38. if tcs_index is None:
  39. return filter(lambda tr: tr.testrunName == tr_name, self.data)
  40. if tc_index is None:
  41. return filter(lambda tr: tr.testrunName == tr_name and len(tr.testcase_sequences) > tcs_index, self.data)
  42. return filter(
  43. lambda tr: tr.testrunName == tr_name and len(tr.testcase_sequences) > tcs_index \
  44. and len(tr.testcase_sequences[tcs_index].testcases) > tc_index,
  45. self.data,
  46. )
  47. def max_size(self, tr_name, tcs_index=None):
  48. #
  49. # returns the max number of
  50. # tcs_index is None: testcase sequences within the testruns in query_set
  51. # tcs_index is a number: testcases within specified testcase sequence of all testruns in db
  52. #
  53. # test case sequences
  54. if tcs_index is None:
  55. return max(map(lambda tr: len(tr.testcase_sequences), self.filter(tr_name=tr_name)))
  56. # test cases
  57. return max(map(lambda tr: len(tr.testcase_sequences[tcs_index].testcases), self.filter(tr_name=tr_name, tcs_index=tcs_index)))
  58. def tr_avg_duration(self, tr_name):
  59. duration = 0
  60. quantity = 0
  61. for d in (tr.duration for tr in self.filter(tr_name) if tr.duration):
  62. duration += d
  63. quantity += 1
  64. if quantity > 0:
  65. return round(duration/quantity, 2)
  66. return None
  67. def tc_avg_duration(self, tr_name, indexes):
  68. durations = map(
  69. lambda tr: tr.testcase_sequences[indexes[0]].testcases[indexes[1]].duration,
  70. self.filter(tr_name, tcs_index=indexes[0], tc_index=indexes[1]),
  71. )
  72. duration = 0
  73. quantity = 0
  74. for d in durations:
  75. if d:
  76. duration += d
  77. quantity += 1
  78. if quantity > 0:
  79. return round(duration/quantity, 2)
  80. return None
  81. class ExportSheet:
  82. def __init__(self, sheet, header_format):
  83. self.sheet = sheet
  84. self.line = 1 # current line
  85. self.column = 0
  86. self.header_map = {}
  87. self.header_format = header_format
  88. def set_header_format(self, header_format):
  89. self.header_format = header_format
  90. def hr(self):
  91. self.line += 1
  92. def header(self, headers):
  93. for header in headers:
  94. self.sheet.write(0, self.column, header, self.header_format)
  95. self.header_map[header] = self.column
  96. self.column += 1
  97. def add_header(self, header):
  98. if not header in self.header_map:
  99. self.sheet.write(0, self.column, header, self.header_format)
  100. self.header_map[header] = self.column
  101. self.column += 1
  102. def row(self, row_data):
  103. for col, data in enumerate(row_data):
  104. self.sheet.write(self.line, col, data.get('value'), data.get('format'))
  105. self.line += 1
  106. def new_row(self, row_data):
  107. self.line += 1
  108. for col, data in enumerate(row_data):
  109. self.sheet.write(self.line, col, data.get('value'), data.get('format'))
  110. def by_header(self, header, value):
  111. self.add_header(header)
  112. self.sheet.write(self.line, self.header_map[header], value)
  113. class ResultsBrowser:
  114. def __init__(self, db_url=None):
  115. # setup db engine
  116. if db_url:
  117. engine = create_engine(db_url)
  118. else:
  119. engine = create_engine(DATABASE_URL)
  120. self.db = sessionmaker(bind=engine)()
  121. # result query set
  122. self.query_set = QuerySet()
  123. # tag of the current query set
  124. self.tags = None
  125. # set of stages
  126. self.stages = None
  127. # path management
  128. self.managedPaths = ManagedPaths()
  129. logger.info(f'Initiated with DATABASE_URL: {db_url if db_url else DATABASE_URL}')
  130. def __del__(self):
  131. self.db.close()
  132. def name_list(self):
  133. names = self.db.query(TestrunLog.testrunName).group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
  134. return [x[0] for x in names]
  135. def stage_list(self):
  136. stages = self.db.query(GlobalAttribute.value).filter_by(name=GC.EXECUTION_STAGE)\
  137. .group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
  138. return [x[0] for x in stages]
  139. def query(self, name=None, stage=None, start_date=None, end_date=None):
  140. #
  141. # get TestrunLogs by name, stage and dates
  142. #
  143. # format date
  144. format_date = lambda date: date.strftime('%Y-%m-%d') if date else None
  145. # get records
  146. records = []
  147. logger.info(f'Quering: name={name}, stage={stage}, dates=[{format_date(start_date)} - {format_date(end_date)}]')
  148. # filter by name and stage
  149. if name and stage:
  150. self.stages = {stage}
  151. records = self.db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=name)\
  152. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage))).all()
  153. # filter by name
  154. elif name:
  155. # get Testrun stages
  156. stages = self.db.query(GlobalAttribute.value).filter(GlobalAttribute.testrun.has(TestrunLog.testrunName==name))\
  157. .filter_by(name=GC.EXECUTION_STAGE).group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
  158. self.stages = {x[0] for x in stages}
  159. for s in self.stages:
  160. logs = self.db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=name)\
  161. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==s))).all()
  162. records.extend(logs)
  163. # filter by stage
  164. elif stage:
  165. self.stages = {stage}
  166. # get Testrun names
  167. names = self.db.query(TestrunLog.testrunName)\
  168. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage)))\
  169. .group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
  170. names = [x[0] for x in names]
  171. for n in names:
  172. logs = self.db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=n)\
  173. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage))).all()
  174. records.extend(logs)
  175. # get all testruns ordered by name and stage
  176. else:
  177. # get Testrun names
  178. names = self.db.query(TestrunLog.testrunName).group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
  179. names = [x[0] for x in names]
  180. self.stages = set()
  181. for n in names:
  182. # get Testrun stages
  183. stages = self.db.query(GlobalAttribute.value).filter(GlobalAttribute.testrun.has(TestrunLog.testrunName==n))\
  184. .filter_by(name=GC.EXECUTION_STAGE).group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
  185. stages = [x[0] for x in stages]
  186. self.stages.update(stages)
  187. for s in stages:
  188. logs = self.db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=n)\
  189. .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==s))).all()
  190. records.extend(logs)
  191. # filter by dates
  192. if start_date and end_date:
  193. self.query_set.set([log for log in records if log.startTime > start_date and log.startTime < end_date])
  194. elif start_date:
  195. self.query_set.set([log for log in records if log.startTime >= start_date])
  196. elif end_date:
  197. self.query_set.set([log for log in records if log.startTime <= end_date])
  198. else:
  199. self.query_set.set(records)
  200. # set the tags
  201. self.tags = {
  202. 'Name': name or 'All',
  203. 'Stage': stage or 'All',
  204. 'Date from': format_date(start_date) or 'None',
  205. 'Date to': format_date(end_date or datetime.now()),
  206. }
  207. logger.info(f'Number of found records: {self.query_set.length}')
  208. def export(self):
  209. #
  210. # exports the query set to xlsx
  211. #
  212. # initialize workbook
  213. path_to_file = self.managedPaths.getOrSetDBExportPath().joinpath(f'TestrunLogs_{"_".join(list(map(str, self.tags.values())))}.xlsx')
  214. workbook = Workbook(str(path_to_file))
  215. # set labels
  216. labels = {
  217. 'testrun': 'TestRun',
  218. 'testcase_sequence': 'Test Case Sequence',
  219. 'testcase': 'Test Case',
  220. 'stage': 'Stage',
  221. 'duration': 'Avg. Duration',
  222. }
  223. # set output headers
  224. base_headers = [
  225. 'Testrun ID',
  226. 'TestCase ID',
  227. 'TestCase Number',
  228. ]
  229. base_fields = [
  230. GC.EXECUTION_STAGE,
  231. GC.TESTCASESTATUS,
  232. GC.TIMING_DURATION,
  233. ]
  234. # define cell formats
  235. cformats = {
  236. 'bg_red': workbook.add_format({'bg_color': 'red'}),
  237. 'bg_green': workbook.add_format({'bg_color': 'green'}),
  238. 'bg_yellow': workbook.add_format({'bg_color': 'yellow'}),
  239. 'font_bold': workbook.add_format({'bold': True}),
  240. 'font_bold_italic': workbook.add_format({'bold': True, 'italic': True}),
  241. }
  242. # map status styles
  243. status_style = {
  244. GC.TESTCASESTATUS_SUCCESS: cformats.get('bg_green'),
  245. GC.TESTCASESTATUS_ERROR: cformats.get('bg_red'),
  246. GC.TESTCASESTATUS_WAITING: None,
  247. }
  248. # create sheets
  249. sheets = {
  250. 'summary': ExportSheet(workbook.add_worksheet('Summary'), cformats.get('font_bold')),
  251. 'output': ExportSheet(workbook.add_worksheet('Output'), cformats.get('font_bold')),
  252. }
  253. # write summary titles
  254. time_start = time.time() # -------------------------> time tracker
  255. # title
  256. sheets['summary'].sheet.set_column(first_col=0, last_col=0, width=18)
  257. sheets['summary'].header([f'{labels.get("testrun")}s Summary'])
  258. # parameters
  259. for key, value in self.tags.items():
  260. sheets['summary'].row([
  261. {
  262. 'value': key,
  263. 'format': cformats.get('font_bold'),
  264. },
  265. {
  266. 'value': value,
  267. }
  268. ])
  269. # write output titles
  270. sheets['output'].header(base_headers + base_fields)
  271. # write items
  272. # testruns
  273. for tr_name in self.query_set.names():
  274. logger.info(f'Exporting Tetsrun "{tr_name}": {len(list(self.query_set.filter(tr_name)))} records')
  275. # testrun name
  276. sheets['summary'].hr()
  277. sheets['summary'].row([
  278. {
  279. 'value': labels.get('testrun'),
  280. 'format': cformats.get('font_bold'),
  281. },
  282. {
  283. 'value': tr_name,
  284. }
  285. ])
  286. # average duration
  287. sheets['summary'].row([
  288. {
  289. 'value': labels.get('duration'),
  290. 'format': cformats.get('font_bold'),
  291. },
  292. {
  293. 'value': self.query_set.tr_avg_duration(tr_name),
  294. }
  295. ])
  296. #
  297. # testcase sequences
  298. for tcs_index in range(self.query_set.max_size(tr_name)):
  299. tcs_number = tcs_index+1
  300. # testcase sequence
  301. sheets['summary'].hr()
  302. sheets['summary'].row([
  303. {
  304. 'value': labels.get('testcase_sequence'),
  305. 'format': cformats.get('font_bold'),
  306. },
  307. {
  308. 'value': tcs_number,
  309. }
  310. ])
  311. # test cases
  312. # header
  313. tc_num_max = self.query_set.max_size(tr_name, tcs_index=tcs_index)
  314. sheets['summary'].hr()
  315. sheets['summary'].row(
  316. [
  317. {
  318. 'value': 'Run Date',
  319. 'format': cformats.get('font_bold_italic'),
  320. },
  321. {
  322. 'value': labels.get('testcase'),
  323. 'format': cformats.get('font_bold_italic'),
  324. },
  325. ] + [{} for i in range(1, tc_num_max)] +[
  326. {
  327. 'value': labels.get('stage'),
  328. 'format': cformats.get('font_bold_italic'),
  329. },
  330. {
  331. 'value': f'{labels.get("testcase")} ID',
  332. 'format': cformats.get('font_bold_italic'),
  333. },
  334. ]
  335. )
  336. sheets['summary'].row(
  337. [{}] + [{'value': i} for i in range(tc_num_max)]
  338. )
  339. durations = [[.0,0] for i in range(tc_num_max)]
  340. for tr_index, tr in enumerate(self.query_set.filter(tr_name, tcs_index)):
  341. tc_num = len(tr.testcase_sequences[tcs_index].testcases)
  342. status_row = [{'value': tr.startTime.strftime('%Y-%m-%d %H:%M:%S')}]
  343. # query fields
  344. data = self.db.query(
  345. TestCaseField.name,
  346. TestCaseField.value,
  347. TestCaseLog.number,
  348. TestCaseLog.id,
  349. ).join(TestCaseField.testcase).join(TestCaseLog.testcase_sequence).join(TestCaseSequenceLog.testrun)\
  350. .filter(and_(TestrunLog.id == tr.id, TestCaseSequenceLog.number == tcs_number)).order_by(TestCaseLog.number)
  351. tc_id_cur = None
  352. for name, value, tc_index, tc_id in data.yield_per(500):
  353. # summary data
  354. if name == GC.TESTCASESTATUS:
  355. status_row.append({
  356. 'value': value,
  357. 'format': status_style.get(value),
  358. })
  359. elif name == GC.EXECUTION_STAGE:
  360. tr_stage = value
  361. elif name == GC.TIMING_DURATION:
  362. m = re.search(r'(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)', value)
  363. if m:
  364. factors = {
  365. 'hours': 3600,
  366. 'minutes': 60,
  367. 'seconds': 1,
  368. }
  369. durations[tc_index-1][0] += sum([factors[key]*float(value) for key, value in m.groupdict().items()])
  370. durations[tc_index-1][1] += 1
  371. # write to output
  372. # write testcase
  373. if tc_id != tc_id_cur:
  374. tc_id_cur = tc_id
  375. sheets['output'].new_row([
  376. {
  377. 'value': str(tr),
  378. },
  379. {
  380. 'value': str(uuid.UUID(bytes=tc_id)),
  381. },
  382. {
  383. 'value': tc_index,
  384. },
  385. ])
  386. #field
  387. sheets['output'].by_header(name, value)
  388. # write state row to summary sheet
  389. sheets['summary'].row(
  390. status_row + [{} for i in range(tc_num, tc_num_max)] + [
  391. {
  392. 'value': tr_stage,
  393. },
  394. {
  395. 'value': str(tr),
  396. },
  397. ]
  398. )
  399. # avg durations
  400. sheets['summary'].row(
  401. [
  402. {
  403. 'value': labels.get('duration'),
  404. 'format': cformats.get('font_bold_italic'),
  405. },
  406. ] + [{'value': d} for d in map(lambda d: round(d[0]/d[1], 2) if d[1] > 0 else .0, durations)]
  407. )
  408. # autowidth output columns
  409. for col in range(len(base_headers)+len(base_fields)):
  410. ExcelSheetHelperFunctions.set_column_autowidth(sheets['output'].sheet, col)
  411. workbook.close()
  412. logger.info(f'Query successfully exported to {path_to_file} in {time.time()-time_start} seconds')
  413. return path_to_file
  414. def export_txt(self):
  415. #
  416. # export to txt
  417. #
  418. path_to_file = self.managedPaths.getOrSetDBExportPath().joinpath(f'TestrunLogs_{"_".join(list(map(str, self.tags.values())))}.txt')
  419. # set labels
  420. labels = {
  421. 'testrun': 'TestRun',
  422. 'testcase_sequence': 'Test Case Sequence',
  423. 'testcase': 'Test Case',
  424. 'stage': 'Stage',
  425. 'duration': 'Avg. Duration',
  426. }
  427. # write data
  428. with open(path_to_file, 'w') as f:
  429. # title
  430. f.write(f'{labels.get("testrun")}s Summary\n\n')
  431. # parameters
  432. for key, value in self.tags.items():
  433. f.write(f'{key}\t{value}\n')
  434. # testruns
  435. for tr_name in self.query_set.names():
  436. print(f'*** Tetsrun "{tr_name}"')
  437. # testrun name
  438. f.write(f'\n{labels.get("testrun")}: {tr_name}\n')
  439. # average duration
  440. f.write(f'{labels.get("duration")}: {self.query_set.tr_avg_duration(tr_name)}\n')
  441. # testcase sequences
  442. for tcs_index in range(self.query_set.max_size(tr_name)):
  443. print(f'**** TestCaseSequence-{tcs_index}')
  444. # testcase sequence
  445. f.write(f'\n{labels.get("testcase_sequence")}: {tcs_index}\n\n')
  446. # test cases
  447. # header
  448. tc_num = self.query_set.max_size(tr_name, tcs_index=tcs_index)
  449. f.write(f'{"Run Date":20}{labels.get("testcase"):8}')
  450. f.write(' '*7)
  451. f.write((' '*8)*(tc_num-2))
  452. f.write(f'{labels.get("stage"):8}{labels.get("testcase")} ID\n')
  453. f.write(' '*20)
  454. for tc_index in range(tc_num):
  455. f.write(f'{tc_index:<8}')
  456. f.write('\n')
  457. # testcase status
  458. tr_counter = 1
  459. for tr in self.query_set.filter(tr_name, tcs_index):
  460. print(f'***** TestRun {tr_counter}: {len(tr.testcase_sequences[tcs_index].testcases)} testcases')
  461. tr_counter += 1
  462. f.write(f'{tr.startTime.strftime("%Y-%m-%d %H:%M:%S"):20}')
  463. for tc in tr.testcase_sequences[tcs_index].testcases:
  464. f.write(f'{tc.status:8}')
  465. # tail
  466. print(f'{tc_num} - {len(tr.testcase_sequences[tcs_index].testcases)} = {(tc_num-len(tr.testcase_sequences[tcs_index].testcases))}')
  467. f.write((' '*8)*(tc_num-len(tr.testcase_sequences[tcs_index].testcases)))
  468. f.write(f'{tr.stage:8}{tr}\n')
  469. # average durations
  470. f.write(f'{labels.get("duration"):20}')
  471. for tc_index in range(tc_num):
  472. f.write(f'{self.query_set.tc_avg_duration(tr_name, (tcs_index, tc_index)):8}')
  473. logger.info(f'Query successfully exported to {path_to_file}')
  474. return path_to_file