Browse Source

testrun call summary

aguryev 3 years ago
parent
commit
ad25cb1f0c

+ 2 - 2
.gitignore

@@ -14,10 +14,10 @@ browsermob-proxy
 1TestResults
 TestDownloads
 uploads/*.xlsx
-files/app/uploads/*
+files/app/uploads
 ui/data
 ui/populate_db.py
-ui/app/static/files*
+ui/app/static/files
 api/jsons
 parent
 run_docker_mysql.sh

+ 0 - 209
api/DataBaseORM.py

@@ -1,209 +0,0 @@
-from sqlalchemy import Column, String, Integer, DateTime, Boolean, Table, ForeignKey
-#from sqlalchemy.types import Binary(16), TypeDecorator
-from sqlalchemy.orm import relationship
-from sqlalchemy import create_engine
-from sqlalchemy.ext.declarative import declarative_base
-import os
-import re
-import uuid
-from baangt.base.PathManagement import ManagedPaths
-
-
-managedPaths = ManagedPaths()
-DATABASE_URL = os.getenv('DATABASE_URL') or 'sqlite:///'+str(managedPaths.derivePathForOSAndInstallationOption().joinpath('testrun.db'))
-
-# handle sqlalchemy dialects
-dialect = re.match(r'\w+', DATABASE_URL)
-if dialect.group() == 'mysql':
-	from sqlalchemy.dialects.mysql import BINARY as Binary
-else:
-	from sqlalchemy import LargeBinary as Binary
-
-engine = create_engine(DATABASE_URL)
-base = declarative_base()
-
-#
-# UUID as bytes
-#
-def uuidAsBytes():
-	return uuid.uuid4().bytes
-
-#
-# Testrun models
-#
-
-class TestrunLog(base):
-	#
-	# summary on Testrun results
-	#
-	__tablename__ = "testruns"
-	# columns
-	id = Column(Binary(16), primary_key=True, default=uuidAsBytes)
-	testrunName = Column(String(32), nullable=False)
-	logfileName = Column(String(1024), nullable=False)
-	startTime = Column(DateTime, nullable=False)
-	endTime = Column(DateTime, nullable=False)
-	dataFile = Column(String(1024), nullable=True)
-	statusOk = Column(Integer, nullable=False)
-	statusFailed = Column(Integer, nullable=False)
-	statusPaused = Column(Integer, nullable=False)
-	# relationships
-	globalVars = relationship('GlobalAttribute')
-	testcase_sequences = relationship('TestCaseSequenceLog')
-
-	@property
-	def recordCount(self):
-		return self.statusOk + self.statusPaused + self.statusFailed
-
-	@property
-	def duration(self):
-		return (self.endTime - self.startTime).seconds
-	
-
-	def __str__(self):
-		return str(uuid.UUID(bytes=self.id))
-
-	def to_json(self):
-		return {
-			'id': str(self),
-			'Name': self.testrunName,
-			'Summary': {
-				'TestRecords': self.recordCount,
-				'Successful': self.statusOk,
-				'Paused': self.statusPaused,
-				'Error': self.statusFailed,
-				'LogFile': self.logfileName,
-				'StartTime': self.startTime.strftime('%H:%M:%S'),
-				'EndTime': self.endTime.strftime('%H:%M:%S'),
-				'Duration': str(self.endTime - self.startTime),
-			},
-			'GlobalSettings': {gv.name: gv.value for gv in self.globalVars},
-			'TestSequences': [tsq.to_json() for tsq in self.testcase_sequences],
-		}
-
-
-
-class GlobalAttribute(base):
-	#
-	# global vars
-	#
-	__tablename__ = 'globals'
-	# columns
-	id = Column(Integer, primary_key=True)
-	name = Column(String(64), nullable=False)
-	value = Column(String(1024), nullable=True)
-	testrun_id = Column(Binary(16), ForeignKey('testruns.id'), nullable=False)
-	# relationships
-	testrun = relationship('TestrunLog', foreign_keys=[testrun_id])
-
-
-#
-# Test Case Sequence models
-#
-
-class TestCaseSequenceLog(base):
-	#
-	# TestCase Sequence
-	#
-
-	__tablename__ = 'testCaseSequences'
-	# columns
-	id = Column(Binary(16), primary_key=True, default=uuidAsBytes)
-	testrun_id = Column(Binary(16), ForeignKey('testruns.id'), nullable=False)
-	# relationships
-	testrun = relationship('TestrunLog', foreign_keys=[testrun_id])
-	testcases = relationship('TestCaseLog')
-
-	def __str__(self):
-		return str(uuid.UUID(bytes=self.id))
-
-	def to_json(self):
-		return {
-			'id': str(self),
-			'TestCases': [tc.to_json() for tc in self.testcases],
-		}
-
-
-#
-# Test Case models
-#
-
-class TestCaseLog(base):
-	#
-	# TestCase results
-	#
-	__tablename__ = 'testCases'
-	# columns
-	id = Column(Binary(16), primary_key=True, default=uuidAsBytes)
-	testcase_sequence_id = Column(Binary(16), ForeignKey('testCaseSequences.id'), nullable=False)
-	# relationships
-	testcase_sequence = relationship('TestCaseSequenceLog', foreign_keys=[testcase_sequence_id])
-	fields = relationship('TestCaseField')
-	networkInfo = relationship('TestCaseNetworkInfo')
-
-	def __str__(self):
-		return str(uuid.UUID(bytes=self.id))
-
-	def to_json(self):
-		return {
-			'id': str(self),
-			'Parameters': {pr.name: pr.value for pr in self.fields},
-			'NetworkInfo': [nw.to_json() for nw in self.networkInfo],
-		}
-
-
-class TestCaseField(base):
-	#
-	# field for a TestCase results
-	#
-	__tablename__ = 'testCaseFields'
-	# columns
-	id = Column(Integer, primary_key=True)
-	name = Column(String(64), nullable=False)
-	value = Column(String(1024), nullable=True)
-	testcase_id = Column(Binary(16), ForeignKey('testCases.id'), nullable=False)
-	# relationships
-	testcase = relationship('TestCaseLog', foreign_keys=[testcase_id])
-
-class TestCaseNetworkInfo(base):
-	#
-	# network info for a TestCase
-	#
-	__tablename__ = 'networkInfo'
-	# columns
-	id = Column(Integer, primary_key=True)
-	browserName = Column(String(64), nullable=True)
-	status = Column(Integer, nullable=True)
-	method = Column(String(16), nullable=True)
-	url = Column(String(256), nullable=True)
-	contentType = Column(String(64), nullable=True)
-	contentSize = Column(Integer, nullable=True)
-	headers = Column(String(4096), nullable=True)
-	params = Column(String(1024), nullable=True)
-	response = Column(String(4096), nullable=True)
-	startDateTime = Column(DateTime, nullable=True)
-	duration = Column(Integer, nullable=True)
-	testcase_id = Column(Binary(16), ForeignKey('testCases.id'), nullable=True)
-	# relationships
-	testcase = relationship('TestCaseLog', foreign_keys=[testcase_id])
-
-	def to_json(self):
-		return {
-			'BrowserName': self.browserName,
-			'Status': self.status,
-			'Method': self.method,
-			'URL': self.url,
-			'ContentType': self.contentType,
-			'ContentSize': self.contentSize,
-			'Headers': self.headers,
-			'Params': self.params,
-			'Response': '',#self.response,
-			'StartDateTime': str(self.startDateTime),
-			'Duration': self.duration,
-		}
-
-
-
-# create tables
-#if __name__ == '__main__':
-base.metadata.create_all(engine)

+ 1 - 1
api/Dockerfile

@@ -74,7 +74,7 @@ RUN mkdir /baangt/browserDrivers && \
 ADD . /baangt
 
 #RUN pip3 install dataclasses dataclasses_json
-COPY DataBaseORM.py /usr/local/lib/python3.6/dist-packages/baangt/base/DataBaseORM.py
+#COPY DataBaseORM.py /usr/local/lib/python3.6/dist-packages/baangt/base/DataBaseORM.py
 
 RUN chmod +x runservice.sh
 

+ 0 - 795
api/ExportResults.py

@@ -1,795 +0,0 @@
-import xlsxwriter
-import logging
-import json
-import baangt.base.GlobalConstants as GC
-from baangt.base.Timing.Timing import Timing
-from baangt.base.Utils import utils
-from pathlib import Path
-from typing import Optional
-from xlsxwriter.worksheet import (
-    Worksheet, cell_number_tuple, cell_string_tuple)
-from sqlalchemy import create_engine
-from sqlalchemy.orm import sessionmaker
-from baangt.base.DataBaseORM import DATABASE_URL, TestrunLog, TestCaseSequenceLog
-from baangt.base.DataBaseORM import TestCaseLog, TestCaseField, GlobalAttribute, TestCaseNetworkInfo
-from datetime import datetime
-from sqlite3 import IntegrityError
-from baangt import plugin_manager
-import re
-import csv
-from dateutil.parser import parse
-from uuid import uuid4
-from pathlib import Path
-
-logger = logging.getLogger("pyC")
-
-
-class ExportResults:
-    def __init__(self, **kwargs):
-        self.kwargs = kwargs
-        self.testList = []
-        self.testRunInstance = kwargs.get(GC.KWARGS_TESTRUNINSTANCE)
-        self.testCasesEndDateTimes_1D = kwargs.get('testCasesEndDateTimes_1D')
-        self.testCasesEndDateTimes_2D = kwargs.get('testCasesEndDateTimes_2D')
-        self.networkInfo = self._get_network_info(kwargs.get('networkInfo'))
-        self.testRunName = self.testRunInstance.testRunName
-        self.dataRecords = self.testRunInstance.dataRecords
-        self.stage = self.__getStageFromDataRecordsOrGlobalSettings()
-        self.logfile = logger.handlers[1].baseFilename
-
-        try:
-            self.exportFormat = kwargs.get(GC.KWARGS_TESTRUNATTRIBUTES).get(GC.EXPORT_FORMAT)
-            if isinstance(self.exportFormat, dict):
-                self.exportFormat = self.exportFormat.get(GC.EXPORT_FORMAT)
-
-            if not self.exportFormat:
-                self.exportFormat = GC.EXP_XLSX
-        except KeyError:
-            self.exportFormat = GC.EXP_XLSX
-
-        self.fileName = self.__getOutputFileName()
-        logger.info("Export-Sheet for results: " + self.fileName)
-
-        # export results to DB
-        self.testcase_uuids = []
-        self.exportToDataBase()
-
-        if self.exportFormat == GC.EXP_XLSX:
-            self.fieldListExport = kwargs.get(GC.KWARGS_TESTRUNATTRIBUTES).get(GC.EXPORT_FORMAT)["Fieldlist"]
-            self.workbook = xlsxwriter.Workbook(self.fileName)
-            self.summarySheet = self.workbook.add_worksheet("Summary")
-            self.worksheet = self.workbook.add_worksheet("Output")
-            self.jsonSheet = self.workbook.add_worksheet(f"{self.stage}_JSON")
-            self.timingSheet = self.workbook.add_worksheet("Timing")
-            self.cellFormatGreen = self.workbook.add_format()
-            self.cellFormatGreen.set_bg_color('green')
-            self.cellFormatRed = self.workbook.add_format()
-            self.cellFormatRed.set_bg_color('red')
-            self.cellFormatBold = self.workbook.add_format()
-            self.cellFormatBold.set_bold(bold=True)
-            self.summaryRow = 0
-            self.__setHeaderDetailSheetExcel()
-            self.makeSummaryExcel()
-            self.exportResultExcel()
-            self.exportJsonExcel()
-            self.exportAdditionalData()
-            self.exportTiming = ExportTiming(self.dataRecords,
-                                             self.timingSheet)
-            if self.networkInfo:
-                self.networkSheet = self.workbook.add_worksheet("Network")
-                self.exportNetWork = ExportNetWork(self.networkInfo,
-                                                   self.testCasesEndDateTimes_1D,
-                                                   self.testCasesEndDateTimes_2D,
-                                                   self.workbook,
-                                                   self.networkSheet)
-            self.closeExcel()
-        elif self.exportFormat == GC.EXP_CSV:
-            self.export2CSV()
-        #self.exportToDataBase()
-
-    def exportAdditionalData(self):
-        # Runs only, when KWARGS-Parameter is set.
-        if self.kwargs.get(GC.EXPORT_ADDITIONAL_DATA):
-            addExportData = self.kwargs[GC.EXPORT_ADDITIONAL_DATA]
-            # Loop over the items. KEY = Tabname, Value = Data to be exported.
-            # For data KEY = Fieldname, Value = Cell-Value
-
-            for key, value in addExportData.items():
-                lExport = ExportAdditionalDataIntoTab(tabname=key, valueDict=value, outputExcelSheet=self.workbook)
-                lExport.export()
-
-    def __getStageFromDataRecordsOrGlobalSettings(self):
-        """
-        If "STAGE" is not provided in the data fields (should actually not happen, but who knows),
-        we shall take it from GlobalSettings. If also not there, take the default Value GC.EXECUTIN_STAGE_TEST
-        :return:
-        """
-        value = None
-        for key, value in self.dataRecords.items():
-            break
-        if not value.get(GC.EXECUTION_STAGE):
-            stage = self.testRunInstance.globalSettings.get('TC.Stage', GC.EXECUTION_STAGE_TEST)
-        else:
-            stage = value.get(GC.EXECUTION_STAGE)
-
-        return stage
-        
-
-    def export2CSV(self):
-        """
-        Writes CSV-File of datarecords
-
-        """
-        f = open(self.fileName, 'w', encoding='utf-8-sig', newline='')
-        writer = csv.DictWriter(f, self.dataRecords[0].keys())
-        writer.writeheader()
-        for i in range(0, len(self.dataRecords) - 1):
-            writer.writerow(self.dataRecords[i])
-        f.close()
-
-    def exportToDataBase(self):
-        #
-        # writes results to DB
-        #
-        logger.info(f'Export results to database at: {DATABASE_URL}')
-        engine = create_engine(DATABASE_URL)
-
-        # create a Session
-        Session = sessionmaker(bind=engine)
-        session = Session()
-
-        # get timings
-        timing: Timing = self.testRunInstance.timing
-        start, end, duration = timing.returnTimeSegment(GC.TIMING_TESTRUN)
-
-        # get status
-        success = 0
-        error = 0
-        waiting = 0
-        for value in self.dataRecords.values():
-            if value[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_SUCCESS:
-                success += 1
-            elif value[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_ERROR:
-                error += 1
-            if value[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_WAITING:
-                waiting += 1
-
-        # create testrun object
-        tr_log = TestrunLog(
-            id=self.testRunInstance.uuid.bytes,
-            testrunName=self.testRunName,
-            logfileName=self.logfile,
-            startTime=datetime.strptime(start, "%d-%m-%Y %H:%M:%S"),
-            endTime=datetime.strptime(end, "%d-%m-%Y %H:%M:%S"),
-            statusOk=success,
-            statusFailed=error,
-            statusPaused=waiting,
-            dataFile=self.fileName,
-        )
-        # add to DataBase
-        session.add(tr_log)
-
-        # set globals
-        for key, value in self.testRunInstance.globalSettings.items():
-            globalVar = GlobalAttribute(
-                name=key,
-                value=str(value),
-                testrun=tr_log,
-            )
-            session.add(globalVar)
-
-        #self.__save_commit(session)
-
-        # create testcase sequence instance
-        tcs_log = TestCaseSequenceLog(testrun=tr_log)
-
-        # create testcases
-        for tc in self.dataRecords.values():
-            # get uuid
-            uuid = uuid4()
-            # create TestCaseLog instances
-            tc_log = TestCaseLog(
-                id=uuid.bytes,
-                testcase_sequence=tcs_log
-            )
-            # store uuid
-            self.testcase_uuids.append(uuid)
-            session.add(tc_log)
-            # add TestCase fields
-            for key, value in tc.items():
-                field = TestCaseField(name=key, value=str(value), testcase=tc_log)
-                session.add(field)
-
-        #self.__save_commit(session)
-
-        # network info
-        if self.networkInfo:
-            for entry in self.networkInfo:
-                if type(entry.get('testcase')) == type(1):
-                    nw_info = TestCaseNetworkInfo(
-                        testcase=tcs_log.testcases[entry.get('testcase')-1],
-                        browserName=entry.get('browserName'),
-                        status=entry.get('status'),
-                        method=entry.get('method'),
-                        url=entry.get('url'),
-                        contentType=entry.get('contentType'),
-                        contentSize=entry.get('contentSize'),
-                        headers=str(entry.get('headers')),
-                        params=str(entry.get('params')),
-                        response=entry.get('response'),
-                        startDateTime=datetime.strptime(entry.get('startDateTime')[:19], '%Y-%m-%dT%H:%M:%S'),
-                        duration=entry.get('duration'),
-                    )
-                    session.add(nw_info)
-
-        #self.__save_commit(session)
-        try:
-            session.commit()
-        except IntegrityError as e:
-            logger.critical(f"Integrity Error during commit to database: {e}")
-            session.rollback()
-        except Exception as e:
-            logger.critical(f"Unknown error during database commit: {e}")
-            session.rollback()
-
-    '''
-    def __save_commit(self, session):
-        try:
-            session.commit()
-        except IntegrityError as e:
-            logger.critical(f"Integrity Error during commit to database: {e}")
-        except Exception as e:
-            logger.critical(f"Unknown error during database commit: {e}")
-    '''
-
-    def _get_test_case_num(self, start_date_time, browser_name):
-        d_t = parse(start_date_time)
-        d_t = d_t.replace(tzinfo=None)
-        if self.testCasesEndDateTimes_1D:
-            for index, dt_end in enumerate(self.testCasesEndDateTimes_1D):
-                if d_t < dt_end:
-                    return index + 1
-        elif self.testCasesEndDateTimes_2D:
-            browser_num = re.findall(r"\d+\.?\d*", str(browser_name))[-1] \
-                if re.findall(r"\d+\.?\d*", str(browser_name)) else 0
-            dt_list_index = int(browser_num) if int(browser_num) > 0 else 0
-            for i, tcAndDtEnd in enumerate(self.testCasesEndDateTimes_2D[dt_list_index]):
-                if d_t < tcAndDtEnd[1]:
-                    return tcAndDtEnd[0] + 1
-        return 'unknown'
-
-    def _get_network_info(self, networkInfoDict):
-        #
-        # extracts network info data from the given dict 
-        #
-        if networkInfoDict:
-            extractedNetworkInfo = []
-            for info in networkInfoDict:
-                #extractedEntry = {}
-                for entry in info['log']['entries']:
-                    # extract the current entry
-                    extractedNetworkInfo.append({
-                        'testcase': self._get_test_case_num(entry['startedDateTime'], entry['pageref']),
-                        'browserName': entry.get('pageref'),
-                        'status': entry['response'].get('status'),
-                        'method': entry['request'].get('method'),
-                        'url': entry['request'].get('url'),
-                        'contentType': entry['response']['content'].get('mimeType'),
-                        'contentSize': entry['response']['content'].get('size'),
-                        'headers': entry['response']['headers'],
-                        'params': entry['request']['queryString'],
-                        'response': entry['response']['content'].get('text'),
-                        'startDateTime': entry['startedDateTime'],
-                        'duration': entry.get('time'),
-                    })
-            return extractedNetworkInfo
-
-        return None
-
-
-    def exportResultExcel(self, **kwargs):
-        self._exportData()
-
-    def exportJsonExcel(self):
-        # headers
-        headers = [
-            'Stage',
-            'UUID',
-            'Attribute',
-            'Value',
-        ]
-        # header style
-        header_style = self.workbook.add_format()
-        header_style.set_bold()
-        # write header
-        for index in range(len(headers)):
-            self.jsonSheet.write(0, index, headers[index], header_style)
-        # write data
-        row = 0
-        for index, testcase in self.dataRecords.items():
-            # add TestCase fields
-            for key, value in testcase.items():
-                row += 1
-                self.jsonSheet.write(row, 0, self.stage)
-                self.jsonSheet.write(row, 1, str(self.testcase_uuids[index]))
-                self.jsonSheet.write(row, 2, key)
-                self.jsonSheet.write(row, 3, str(value))
-        # Autowidth
-        for n in range(len(headers)):
-            ExcelSheetHelperFunctions.set_column_autowidth(self.jsonSheet, n)
-
-
-    def makeSummaryExcel(self):
-
-        self.summarySheet.write(0, 0, f"Testreport for {self.testRunName}", self.cellFormatBold)
-        self.summarySheet.set_column(0, last_col=0, width=15)
-        # get testrunname my
-        self.testList.append(self.testRunName)
-        # Testrecords
-        self.__writeSummaryCell("Testrecords", len(self.dataRecords), row=2, format=self.cellFormatBold)
-        value = len([x for x in self.dataRecords.values()
-                     if x[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_SUCCESS])
-        self.testList.append(value)  # Ok my
-        if not value:
-            value = ""
-        self.__writeSummaryCell("Successful", value, format=self.cellFormatGreen)
-        self.testList.append(value)  # paused my
-        self.__writeSummaryCell("Paused", len([x for x in self.dataRecords.values()
-                                               if x[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_WAITING]))
-        value = len([x["Screenshots"] for x in self.dataRecords.values()
-                     if x[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_ERROR])
-        self.testList.append(value)  # error my
-        if not value:
-            value = ""
-        self.__writeSummaryCell("Error", value, format=self.cellFormatRed)
-
-        # Logfile
-        self.__writeSummaryCell("Logfile", logger.handlers[1].baseFilename, row=7)
-        # get logfilename for database my
-        self.testList.append(logger.handlers[1].baseFilename)
-        # database id
-        self.__writeSummaryCell("Testrun UUID", str(self.testRunInstance.uuid), row=8)
-        # Timing
-        timing: Timing = self.testRunInstance.timing
-        start, end, duration = timing.returnTimeSegment(GC.TIMING_TESTRUN)
-        self.__writeSummaryCell("Starttime", start, row=10)
-        # get start end during time my
-        self.testList.append(start)
-        self.testList.append(end)
-
-        self.__writeSummaryCell("Endtime", end)
-        self.__writeSummaryCell("Duration", duration, format=self.cellFormatBold)
-        self.__writeSummaryCell("Avg. Dur", "")
-        # Globals:
-        self.__writeSummaryCell("Global settings for this testrun", "", format=self.cellFormatBold, row=15)
-        for key, value in self.testRunInstance.globalSettings.items():
-            self.__writeSummaryCell(key, str(value))
-            # get global data my
-            self.testList.append(str(value))
-        # Testcase and Testsequence setting
-        self.summaryRow += 1
-        self.__writeSummaryCell("TestSequence settings follow:", "", format=self.cellFormatBold)
-        lSequence = self.testRunInstance.testRunUtils.getSequenceByNumber(testRunName=self.testRunName, sequence="1")
-        if lSequence:
-            for key, value in lSequence[1].items():
-                if isinstance(value, list) or isinstance(value, dict):
-                    continue
-                self.__writeSummaryCell(key, str(value))
-
-    def __writeSummaryCell(self, lineHeader, lineText, row=None, format=None, image=False):
-        if not row:
-            self.summaryRow += 1
-        else:
-            self.summaryRow = row
-
-        if not lineText:
-            # If we have no lineText we want to apply format to the Header
-            self.summarySheet.write(self.summaryRow, 0, lineHeader, format)
-        else:
-            self.summarySheet.write(self.summaryRow, 0, lineHeader)
-            self.summarySheet.write(self.summaryRow, 1, lineText, format)
-
-    def __getOutputFileName(self):
-        l_file = Path(self.testRunInstance.managedPaths.getOrSetExportPath())
-
-        if self.exportFormat == GC.EXP_XLSX:
-            lExtension = '.xlsx'
-        elif self.exportFormat == GC.EXP_CSV:
-            lExtension = '.csv'
-        else:
-            logger.critical(f"wrong export file format: {self.exportFormat}, using 'xlsx' instead")
-            lExtension = '.xlsx'
-
-        l_file = l_file.joinpath("baangt_" + self.testRunName + "_" + utils.datetime_return() + lExtension)
-        logger.debug(f"Filename for export: {str(l_file)}")
-        return str(l_file)
-
-    def __setHeaderDetailSheetExcel(self):
-        # the 1st column is DB UUID
-        self.worksheet.write(0, 0, 'UUID')
-        # Add fields with name "RESULT_*" to output fields.
-        i = 1
-        self.__extendFieldList()
-        for column in self.fieldListExport:
-            self.worksheet.write(0, i, column)
-            i += 1
-        # add JSON field
-        self.worksheet.write(0, len(self.fieldListExport)+1, "JSON")
-
-    def __extendFieldList(self):
-        """
-        Fields, that start with "RESULT_" shall always be exported.
-
-        Other fields, that shall always be exported are also added (Testcaseerrorlog, etc.)
-
-        If global Parameter "TC.ExportAllFields" is set to True ALL fields will be exported
-
-        @return:
-        """
-
-        if self.testRunInstance.globalSettings.get("TC.ExportAllFields", False):
-            self.fieldListExport = []  # Make an empty list, so that we don't have duplicates
-            for key in self.dataRecords[0].keys():
-                self.fieldListExport.append(key)
-            return
-
-        try:
-            for key in self.dataRecords[0].keys():
-                if "RESULT_" in key.upper():
-                    if not key in self.fieldListExport:
-                        self.fieldListExport.append(key)
-
-        except Exception as e:
-            logger.critical(
-                f'looks like we have no data in records: {self.dataRecords}, len of dataRecords: {len(self.dataRecords)}')
-
-        # They are added here, because they'll not necessarily appear in the first record of the export data:
-        if not GC.TESTCASEERRORLOG in self.fieldListExport:
-            self.fieldListExport.append(GC.TESTCASEERRORLOG)
-        if not GC.SCREENSHOTS in self.fieldListExport:
-            self.fieldListExport.append(GC.SCREENSHOTS)
-        if not GC.EXECUTION_STAGE in self.fieldListExport:
-            self.fieldListExport.append(GC.EXECUTION_STAGE)
-
-    def _exportData(self):
-        for key, value in self.dataRecords.items():
-            # write DB UUID
-            try:
-                self.worksheet.write(key + 1, 0, str(self.testcase_uuids[key]))
-                # write RESULT fields
-                for (n, column) in enumerate(self.fieldListExport):
-                    self.__writeCell(key + 1, n + 1, value, column)
-                # Also write everything as JSON-String into the last column
-                self.worksheet.write(key + 1, len(self.fieldListExport) + 1, json.dumps(value))
-            except IndexError as e:
-                logger.error(f"List of testcase_uuids didn't have a value for {key}. That shouldn't happen!")
-            except BaseException as e:
-                logger.error(f"Error happened where it shouldn't. Error was {e}")
-
-        # Create autofilter
-        self.worksheet.autofilter(0, 0, len(self.dataRecords.items()), len(self.fieldListExport))
-
-        # Make cells wide enough
-        for n in range(0, len(self.fieldListExport)):
-            ExcelSheetHelperFunctions.set_column_autowidth(self.worksheet, n)
-
-    def __writeCell(self, line, cellNumber, testRecordDict, fieldName, strip=False):
-        if fieldName in testRecordDict.keys() and testRecordDict[fieldName]:
-            # Convert boolean for Output
-            if isinstance(testRecordDict[fieldName], bool):
-                testRecordDict[fieldName] = "True" if testRecordDict[fieldName] else "False"
-
-            # Remove leading New-Line:
-            if '\n' in testRecordDict[fieldName][0:5] or strip:
-                testRecordDict[fieldName] = testRecordDict[fieldName].strip()
-            # Do different stuff for Dicts and Lists:
-            if isinstance(testRecordDict[fieldName], dict):
-                self.worksheet.write(line, cellNumber, testRecordDict[fieldName])
-            elif isinstance(testRecordDict[fieldName], list):
-                if fieldName == GC.SCREENSHOTS:
-                    self.__attachScreenshotsToExcelCells(cellNumber, fieldName, line, testRecordDict)
-                else:
-                    self.worksheet.write(line, cellNumber,
-                                         utils.listToString(testRecordDict[fieldName]))
-            else:
-                if fieldName == GC.TESTCASESTATUS:
-                    if testRecordDict[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_SUCCESS:
-                        self.worksheet.write(line, cellNumber, testRecordDict[fieldName], self.cellFormatGreen)
-                    elif testRecordDict[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_ERROR:
-                        self.worksheet.write(line, cellNumber, testRecordDict[fieldName], self.cellFormatRed)
-                elif fieldName == GC.SCREENSHOTS:
-                    self.__attachScreenshotsToExcelCells(cellNumber, fieldName, line, testRecordDict)
-                else:
-                    self.worksheet.write(line, cellNumber, testRecordDict[fieldName])
-
-    def __attachScreenshotsToExcelCells(self, cellNumber, fieldName, line, testRecordDict):
-        # Place the screenshot images "on" the appropriate cell
-        try:
-            if type(testRecordDict[fieldName]) == list:
-
-                if Path(testRecordDict[fieldName][-1]).is_file():
-                    self.worksheet.insert_image(line, cellNumber, testRecordDict[fieldName][-1], {'x_scale': 0.05,
-                                                                                                  'y_scale': 0.05})
-                else:
-                    logger.error(f"Sceenshot file {testRecordDict[fieldName][-1]} can't be found")
-
-                for nextScreenshotNumber in range(len(testRecordDict[fieldName]) - 1):
-                    if Path(testRecordDict[fieldName][nextScreenshotNumber]).is_file():
-                        self.worksheet.insert_image(line, len(self.fieldListExport) + nextScreenshotNumber + 1,
-                                                    testRecordDict[fieldName][nextScreenshotNumber],
-                                                    {'x_scale': 0.05, 'y_scale': 0.05})
-                    else:
-                        logger.error(f"Screenshot file {testRecordDict[fieldName][nextScreenshotNumber]} can't be found")
-            else:
-                if Path(testRecordDict[fieldName]).is_file():
-                    self.worksheet.insert_image(line, cellNumber, testRecordDict[fieldName], {'x_scale': 0.05,
-                                                                                              'y_scale': 0.05})
-                else:
-                    logger.error(f"Screenshot file {testRecordDict[fieldName]} can't be found")
-
-        except Exception as e:
-            logger.error(f"Problem with screenshots - can't attach them {e}")
-
-        self.worksheet.set_row(line, 35)
-
-    def closeExcel(self):
-        self.workbook.close()
-        # Next line doesn't work on MAC. Returns "not authorized"
-        # subprocess.Popen([self.filename], shell=True)
-
-
-class ExportAdditionalDataIntoTab:
-    def __init__(self, tabname, valueDict, outputExcelSheet: xlsxwriter.Workbook):
-        self.tab = outputExcelSheet.add_worksheet(tabname)
-        self.values = valueDict
-
-    def export(self):
-        self.makeHeader()
-        self.writeLines()
-
-    def makeHeader(self):
-        for cellNumber, entries in self.values.items():
-            for column, (key, value) in enumerate(entries.items()):
-                self.tab.write(0, column, key)
-            break  # Write header only for first line.
-
-    def writeLines(self):
-        currentLine = 1
-        for line, values in self.values.items():
-            for column, (key, value) in enumerate(values.items()):
-                self.tab.write(currentLine, column, value)
-            currentLine += 1
-
-
-class ExcelSheetHelperFunctions:
-    def __init__(self):
-        pass
-
-    @staticmethod
-    def set_column_autowidth(worksheet: Worksheet, column: int):
-        """
-        Set the width automatically on a column in the `Worksheet`.
-        !!! Make sure you run this function AFTER having all cells filled in
-        the worksheet!
-        """
-        maxwidth = ExcelSheetHelperFunctions.get_column_width(worksheet=worksheet, column=column)
-        if maxwidth is None:
-            return
-        elif maxwidth > 45:
-            maxwidth = 45
-        worksheet.set_column(first_col=column, last_col=column, width=maxwidth)
-
-    @staticmethod
-    def get_column_width(worksheet: Worksheet, column: int) -> Optional[int]:
-        """Get the max column width in a `Worksheet` column."""
-        strings = getattr(worksheet, '_ts_all_strings', None)
-        if strings is None:
-            strings = worksheet._ts_all_strings = sorted(
-                worksheet.str_table.string_table,
-                key=worksheet.str_table.string_table.__getitem__)
-        lengths = set()
-        for row_id, colums_dict in worksheet.table.items():  # type: int, dict
-            data = colums_dict.get(column)
-            if not data:
-                continue
-            if type(data) is cell_string_tuple:
-                iter_length = len(strings[data.string])
-                if not iter_length:
-                    continue
-                lengths.add(iter_length)
-                continue
-            if type(data) is cell_number_tuple:
-                iter_length = len(str(data.number))
-                if not iter_length:
-                    continue
-                lengths.add(iter_length)
-        if not lengths:
-            return None
-        return max(lengths)
-
-
-class ExportNetWork:
-    headers = ['BrowserName', 'TestCaseNum', 'Status', 'Method', 'URL', 'ContentType', 'ContentSize', 'Headers',
-               'Params', 'Response', 'startDateTime', 'Duration/ms']
-
-    def __init__(self, networkInfo: dict, testCasesEndDateTimes_1D: list,
-                 testCasesEndDateTimes_2D: list, workbook: xlsxwriter.Workbook, sheet: xlsxwriter.worksheet):
-
-        self.networkInfo = networkInfo
-        #self.testCasesEndDateTimes_1D = testCasesEndDateTimes_1D
-        #self.testCasesEndDateTimes_2D = testCasesEndDateTimes_2D
-        self.workbook = workbook
-        self.sheet = sheet
-        header_style = self.get_header_style()
-        self.write_header(style=header_style)
-        self.set_column_align()
-        self.write_content()
-        self.set_column_width()
-
-    def set_column_align(self):
-        right_align_indexes = list()
-        right_align_indexes.append(ExportNetWork.headers.index('ContentSize'))
-        right_align_indexes.append(ExportNetWork.headers.index('Duration/ms'))
-        right_align_style = self.get_column_style(alignment='right')
-        left_align_style = self.get_column_style(alignment='left')
-        [self.sheet.set_column(i, i, cell_format=right_align_style) if i in right_align_indexes else
-         self.sheet.set_column(i, i, cell_format=left_align_style) for i in range(len(ExportNetWork.headers))]
-
-    def set_column_width(self):
-        [ExcelSheetHelperFunctions.set_column_autowidth(self.sheet, i) for i in range(len(ExportNetWork.headers))]
-
-    def get_header_style(self):
-        header_style = self.workbook.add_format()
-        header_style.set_bg_color("#00CCFF")
-        header_style.set_color("#FFFFFF")
-        header_style.set_bold()
-        header_style.set_border()
-        return header_style
-
-    def get_column_style(self, alignment=None):
-        column_style = self.workbook.add_format()
-        column_style.set_color("black")
-        column_style.set_align('right') if alignment == 'right' \
-            else column_style.set_align('left') if alignment == 'left' else None
-        column_style.set_border()
-        return column_style
-
-    def write_header(self, style=None):
-        for index, value in enumerate(ExportNetWork.headers):
-            self.sheet.write(0, index, value, style)
-
-    def _get_test_case_num(self, start_date_time, browser_name):
-        d_t = parse(start_date_time)
-        d_t = d_t.replace(tzinfo=None)
-        if self.testCasesEndDateTimes_1D:
-            for index, dt_end in enumerate(self.testCasesEndDateTimes_1D):
-                if d_t < dt_end:
-                    return index + 1
-        elif self.testCasesEndDateTimes_2D:
-            browser_num = re.findall(r"\d+\.?\d*", str(browser_name))[-1] \
-                if re.findall(r"\d+\.?\d*", str(browser_name)) else 0
-            dt_list_index = int(browser_num) if int(browser_num) > 0 else 0
-            for i, tcAndDtEnd in enumerate(self.testCasesEndDateTimes_2D[dt_list_index]):
-                if d_t < tcAndDtEnd[1]:
-                    return tcAndDtEnd[0] + 1
-        return 'unknown'
-
-    def write_content(self):
-        if not self.networkInfo:
-            return
-
-        #partition_index = 0
-
-        for index in range(len(self.networkInfo)):
-            data_list = [
-                self.networkInfo[index]['browserName'],
-                self.networkInfo[index]['testcase'],
-                self.networkInfo[index]['status'],
-                self.networkInfo[index]['method'],
-                self.networkInfo[index]['url'],
-                self.networkInfo[index]['contentType'],
-                self.networkInfo[index]['contentSize'],
-                self.networkInfo[index]['headers'],
-                self.networkInfo[index]['params'],
-                self.networkInfo[index]['response'],
-                self.networkInfo[index]['startDateTime'],
-                self.networkInfo[index]['duration'],
-            ]
-
-            for i in range(len(data_list)):
-                self.sheet.write(index + 1, i, str(data_list[i]) or 'null')
-
-
-class ExportTiming:
-    def __init__(self, testdataRecords: dict, sheet: xlsxwriter.worksheet):
-        self.testdataRecords = testdataRecords
-        self.sheet: xlsxwriter.worksheet = sheet
-
-        self.sections = {}
-
-        self.findAllTimingSections()
-        self.writeHeader()
-        self.writeLines()
-
-        # Autowidth
-        for n in range(0, len(self.sections) + 1):
-            ExcelSheetHelperFunctions.set_column_autowidth(self.sheet, n)
-
-    def writeHeader(self):
-        self.wc(0, 0, "Testcase#")
-        for index, key in enumerate(self.sections.keys(), start=1):
-            self.wc(0, index, key)
-
-    def writeLines(self):
-        for tcNumber, (key, line) in enumerate(self.testdataRecords.items(), start=1):
-            self.wc(tcNumber, 0, tcNumber)
-            lSections = self.interpretTimeLog(line[GC.TIMELOG])
-            for section, timingValue in lSections.items():
-                # find, in which column this section should be written:
-                for column, key in enumerate(self.sections.keys(), 1):
-                    if key == section:
-                        self.wc(tcNumber, column,
-                                timingValue[GC.TIMING_DURATION])
-                        continue
-
-    @staticmethod
-    def shortenTimingValue(timingValue):
-        # TimingValue is seconds in Float. 2 decimals is enough:
-        timingValue = int(float(timingValue) * 100)
-        return timingValue / 100
-
-    def writeCell(self, row, col, content, format=None):
-        self.sheet.write(row, col, content, format)
-
-    wc = writeCell
-
-    def findAllTimingSections(self):
-        """
-        We try to have an ordered list of Timing Sequences. As each Testcase might have different sections we'll have
-        to make guesses
-
-        @return:
-        """
-        lSections = {}
-        for key, line in self.testdataRecords.items():
-            lTiming: dict = ExportTiming.interpretTimeLog(line[GC.TIMELOG])
-            for key in lTiming.keys():
-                if lSections.get(key):
-                    continue
-                else:
-                    lSections[key] = None
-
-        self.sections = lSections
-
-    @staticmethod
-    def interpretTimeLog(lTimeLog):
-        """Example Time Log:
-        Complete Testrun: Start: 1579553837.241974 - no end recorded
-        TestCaseSequenceMaster: Start: 1579553837.243414 - no end recorded
-        CustTestCaseMaster: Start: 1579553838.97329 - no end recorded
-        Browser Start: , since last call: 2.3161418437957764
-        Empfehlungen: , since last call: 6.440968036651611, ZIDs:[175aeac023237a73], TS:2020-01-20 21:57:46.525577
-        Annahme_RABAZ: , since last call: 2.002716064453125e-05, ZIDs:[6be7d0a44e59acf6], TS:2020-01-20 21:58:37.203583
-        Antrag drucken: , since last call: 9.075241088867188, ZIDs:[6be7d0a44e59acf6, b27c3875ddcbb4fa], TS:2020-01-20 21:58:38.040137
-        Warten auf Senden an Bestand Button: , since last call: 1.3927149772644043
-        Senden an Bestand: , since last call: 9.60469913482666, ZIDs:[66b12fa4869cf8a0, ad1f3d47c4694e26], TS:2020-01-20 21:58:49.472288
-
-        where the first part before ":" is the section, "since last call:" is the duration, TS: is the timestamp
-
-        Update 29.3.2020: Format changed to "since last call: 00:xx:xx,", rest looks identical.
-        """
-        lExport = {}
-        lLines = lTimeLog.split("\n")
-        for line in lLines:
-            parts = line.split(",")
-            if len(parts) < 2:
-                continue
-            if "Start:" in line:
-                # Format <sequence>: <Start>: <time.loctime>
-                continue
-            else:
-                lSection = parts[0].replace(":", "").strip()
-                lDuration = parts[1].split("since last call: ")[1]
-                lExport[lSection] = {GC.TIMING_DURATION: lDuration}
-        return lExport
-

BIN
api/app/uploads/387271d2-1f6e-4a51-8390-18594425f416


BIN
api/app/uploads/63a1fd96-c509-4822-98b2-9d85e33da5ff


BIN
api/app/uploads/8029f595-1403-4742-97ef-d1b6a0f62ccc


+ 1 - 1
api/app/uploads/custom_globals.json

@@ -1 +1 @@
-{"exportFilesBasePath": "", "TC.Lines": "", "TC.dontCloseBrowser": "", "TC.slowExecution": "", "TC.BrowserAttributes": {"HEADLESS": "True"}}
+{"exportFilesBasePath": "", "TC.Stage": "Debug", "TC.Lines": "", "TC.dontCloseBrowser": "True", "TC.slowExecution": "False", "TC.NetworkInfo": "True", "TX.DEBUG": "True", "TC.BrowserAttributes": {"HEADLESS": "True"}}

+ 2 - 2
api/patch/DataBaseORM.py

@@ -73,8 +73,8 @@ class TestrunLog(base):
 				'Paused': self.statusPaused,
 				'Error': self.statusFailed,
 				'LogFile': self.logfileName,
-				'StartTime': self.startTime.strftime('%H:%M:%S'),
-				'EndTime': self.endTime.strftime('%H:%M:%S'),
+				'StartTime': self.startTime.strftime('%Y-%m-%d %H:%M:%S'),
+				'EndTime': self.endTime.strftime('%Y-%m-%d %H:%M:%S'),
 				'Duration': str(self.endTime - self.startTime),
 			},
 			'GlobalSettings': {gv.name: gv.value for gv in self.globalVars},

+ 1 - 1
api/requirements.txt

@@ -18,6 +18,6 @@ faker>=4.0.3
 
 
 
-baangt>=1.1.2
+baangt>=1.1.4
 pymsteams>=0.1.12
 slack_webhook>=1.0.3

+ 27 - 1
files/app/routes.py

@@ -1,4 +1,4 @@
-from flask import request, render_template, send_from_directory, jsonify
+from flask import request, render_template, send_from_directory, jsonify, send_file, abort
 from app import app, forms
 from uuid import uuid4
 import os
@@ -86,6 +86,32 @@ def upload_files(uuid):
 
 	return jsonify({'uuid': uuid, 'files': upload_response}), 200
 
+@app.route('/get/<string:uuid>/<string:file>')
+def get_logfile(uuid, file):
+	#
+	# retrieve a file by UUID
+	#
+	
+	# log file
+	if file == app.config['TESTRUN_LOGFILE']:
+		app.logger.info(f'Requested logfile from {uuid}')
+		return send_file(
+			'/'.join((app.config['UPLOAD_FOLDER'], uuid, app.config['TESTRUN_LOGFILE'])),
+			mimetype='text/plain',
+			attachment_filename=f'{uuid}.log',
+		)
+
+	# redults file
+	elif file == app.config['TESTRUN_RESULTS']:
+		app.logger.info(f'Requested output file from {uuid}')
+		return send_file(
+			'/'.join((app.config['UPLOAD_FOLDER'], uuid, app.config['TESTRUN_RESULTS'])),
+			attachment_filename=f'{uuid}.xlsx',
+			as_attachment=True,
+		)
+
+	abort(404)
+
 
 @app.route('/get/<string:uuid>')
 def get_file(uuid):

+ 5 - 1
files/config.py

@@ -10,4 +10,8 @@ class Config(object):
 
 	# upload settings
 	UPLOAD_FOLDER = os.getenv('UPLOAD_FOLDER') or 'uploads'
-	ALLOWED_EXTENSIONS = {'XLSX'}
+	ALLOWED_EXTENSIONS = {'XLSX'}
+
+	# Testrun execution files
+	TESTRUN_LOGFILE = 'logfile'
+	TESTRUN_RESULTS = 'outfile'

+ 184 - 6
ui/app/charts.py

@@ -1,8 +1,187 @@
+from flask import current_app
 
+class SummaryCharts:
+	#
+	# prepares jsons for samary charts
+	#
+
+	# status strings from GlobalConstants
+	TESTCASESTATUS_SUCCESS = "OK"
+	TESTCASESTATUS_ERROR = "Failed"
+	TESTCASESTATUS_WAITING = "Paused"
+
+
+	def __init__(self, log, min_length=10):
+		self.log = log
+		self.shortest = min_length
+
+	def get_charts(self):
+		#
+		# summary set of charts
+		#
+		return {
+			'figures': self.figures(),
+			'status': self.status(),
+			'testcases': self.testcases(),
+			'files': self.files(),
+		}
 
-class ChartJS:
+	def files(self):
+		return [
+			{
+				'title': 'Logfile',
+				'url': '/'.join((
+					'http:/',
+					current_app.config.get('BAANGT_DATAFILE_HOST'),
+					current_app.config.get('BAANGT_DATAFILE_GET'),
+					self.log['id'],
+					current_app.config.get('BAANGT_DATAFILE_LOGFILE'),
+				)),
+			},
+			{
+				'title': 'Results',
+				'url': '/'.join((
+					'http:/',
+					current_app.config.get('BAANGT_DATAFILE_HOST'),
+					current_app.config.get('BAANGT_DATAFILE_GET'),
+					self.log['id'],
+					current_app.config.get('BAANGT_DATAFILE_RESULTS'),
+				)),
+			},
+		]
+
+	def figures(self):
+		#
+		# builds json with log statistics
+		#
+
+		return {
+			'records': self.log['Summary'].get('TestRecords'),
+			'successful': self.log['Summary'].get('Successful'),
+			'error': self.log['Summary'].get('Error'),
+			'paused': self.log['Summary'].get('Paused'),
+		}
+
+	def status(self):
+		#
+		# status chart
+		#
+
+		return {
+			'type': 'doughnut',
+			'data': {
+				'datasets': [{
+					'data': [
+						self.log['Summary'].get('Successful'),
+						self.log['Summary'].get('Error'),
+						self.log['Summary'].get('Paused'),
+					],
+					'backgroundColor': [
+						'#52ff52',
+						'#ff5252',
+						'#bdbdbd',
+					],
+				}],
+				'labels': [
+					'Passed',
+					'Failed',
+					'Paused'
+				],
+			},
+			'options': {
+				'legend': {
+					'display': False,
+				},
+			},
+		}
+
+	def testcases(self):
+		#
+		# builds json for Chart.js: duration of testcases
+		#
+
+		testcases = []
+
+		# collect data for every TestCaseSequence
+		for tcs in self.log['TestSequences']:
+			null = max(0, self.shortest - len(tcs['TestCases']))
+			testcases.append({
+				'type': 'bar',
+					'data': {
+						'datasets': [
+							{
+								'data': [self.duration_to_seconds(t['Parameters'].get('Duration')) \
+									if t['Parameters'].get('TestCaseStatus') == self.TESTCASESTATUS_SUCCESS else 0 \
+									for t in tcs['TestCases']] + [None]*null,
+								'backgroundColor': '#52ff52',
+								'label': 'PASSED',
+							},
+							{
+								'data': [self.duration_to_seconds(t['Parameters'].get('Duration')) \
+									if t['Parameters'].get('TestCaseStatus') == self.TESTCASESTATUS_ERROR else 0 \
+									for t in tcs['TestCases']] + [None]*null,
+								'backgroundColor': '#ff5252',
+								'label': 'FAILED',
+							},
+							{
+								'data': [self.duration_to_seconds(t['Parameters'].get('Duration')) \
+									if t['Parameters'].get('TestCaseStatus') == self.TESTCASESTATUS_WAITING else 0 \
+									for t in tcs['TestCases']] + [None]*null,
+								'backgroundColor': '#bdbdbd',
+								'label': 'PAUSED',
+							},
+						],
+						'labels': [t['Parameters'].get('Duration') for t in tcs['TestCases']] + [None]*null,
+					},
+					'options': {
+						'legend': {
+							'display': False,
+						},
+						'tooltips': {
+							'mode': 'index',
+							'intersect': False,
+						},
+						'scales': {
+							'xAxes': [{
+								'gridLines': {
+									'display': False,
+									'drawBorder': False,
+								},
+								'ticks': {
+									'display': False,
+								},
+								'stacked': True,
+							}],
+							'yAxes': [{
+								'gridLines': {
+									'display': False,
+									'drawBorder': False,
+								},
+								'ticks': {
+									'display': False,
+								},
+								'stacked': True,
+							}],
+						},
+					},
+				})
+
+		return testcases
+		
+
+	def duration_to_seconds(self, duration):
+		if duration:
+			without_decimals = duration.split('.')[0]
+			factors = [3600, 60, 1]
+			
+			return sum(t*f for t,f in zip(map(int, without_decimals.split(':')), factors))
+
+		return 0
+
+
+class DashboardCharts:
 	#
-	# prepares jsons ro Charts.js
+	# prepares jsons for dashboard Charts.js
 	#
 
 	def __init__(self, logs, length=10):
@@ -10,7 +189,7 @@ class ChartJS:
 		self.null = length - len(self.logs)
 
 
-	def dashboard(self):
+	def get_charts(self):
 		#
 		# dashboard chart set
 		#
@@ -22,7 +201,6 @@ class ChartJS:
 			'duration': self.duration(),
 		}
 
-
 	def figures(self):
 		#
 		# builds json with log statistics
@@ -94,7 +272,7 @@ class ChartJS:
 						'label': 'Paused',
 					},
 				],
-				'labels': [x.created.strftime('%Y-%m-%d %H:%M') for x in self.logs] + [None]*self.null,
+				'labels': [x.created.strftime('%Y-%m-%d %H:%M:%S') for x in self.logs] + [None]*self.null,
 			},
 			'options': {
 				'legend': {
@@ -148,7 +326,7 @@ class ChartJS:
 					'lineTension': 0,
 					'label': 'Duration',
 				}],
-				'labels': [x.created.strftime('%Y-%m-%d %H:%M') for x in self.logs] + [None]*self.null,
+				'labels': [x.created.strftime('%Y-%m-%d %H:%M:%S') for x in self.logs] + [None]*self.null,
 			},
 			'options': {
 				'legend': {

+ 12 - 1
ui/app/static/css/styles.css

@@ -76,7 +76,17 @@ body {
   z-index: 100;
   width: 100vw;
   height: 150vh;
-  background-color: rgba(192, 192, 192, 0.8);
+  background-color: #37474faa;
+}
+
+#loading-active {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  width: 100vw;
+  height: 200vh;
+  background-color: #37474faa;
 }
 
 .spinner {
@@ -84,6 +94,7 @@ body {
   height: 3rem;
 }
 
+/* additional heights */
 .h-80 {
   height: 80vh;
 }

+ 3 - 0
ui/app/static/js/charts.js

@@ -67,6 +67,9 @@ document.addEventListener('DOMContentLoaded', () => {
             var statusChart = new Chart(item.querySelector('.statusChart'), request.response['status']);
             var resultsChart = new Chart(item.querySelector('.resultsChart'), request.response['results']);
             var durationChart = new Chart(item.querySelector('.durationChart'), duration);
+
+            // hide loading screen
+            document.querySelector('#loading-active').style.display = 'none';
         };
         request.send();
     });

+ 82 - 0
ui/app/static/js/summary.js

@@ -0,0 +1,82 @@
+document.addEventListener('DOMContentLoaded', () => {
+    // get chart data
+    const call_id = document.querySelector('main').id;
+    const request = new XMLHttpRequest();
+    request.responseType = 'json';
+    request.open('POST', `/summary/${call_id}`);
+    request.onload = () => {
+        // set figures
+        const figures = request.response['figures'];
+        // records
+        document.querySelector('#records').querySelector('div').innerHTML = figures['records'];
+        // successfull
+        if (figures['successful'] > 0) {
+            document.querySelector('#passed').querySelector('div').innerHTML = figures['successful'];
+        } else {
+            document.querySelector('#passed').setAttribute('class', 'hide');
+        }
+        // errors
+        if (figures['error'] > 0) {
+            document.querySelector('#failed').querySelector('div').innerHTML = figures['error'];
+        } else {
+            document.querySelector('#failed').setAttribute('class', 'hide');
+        }
+        // paused
+        if (figures['paused'] > 0) {
+            document.querySelector('#paused').querySelector('div').innerHTML = figures['paused'];
+        } else {
+            document.querySelector('#paused').setAttribute('class', 'hide');
+        }
+        // customize legend
+        /*
+        var duration = request.response['duration'];
+        duration['options']['tooltips']['callbacks'] = {
+            label: function(item, data) {
+                var label = data.datasets[item.datasetIndex].label || '';
+                if (label) {
+                    label += ': ';
+                }
+                label += item.value;
+                label += ' sec';
+
+                return label;
+            }
+        };
+        */
+        // draw status chart 
+        var statusChart = new Chart(document.querySelector('#statusChart'), request.response['status']);
+        // draw testcase charts
+        const testcases = document.querySelector('#testcaseCharts');
+        request.response['testcases'].forEach(testcase => {
+            var canvas = document.createElement('canvas');
+            canvas.setAttribute('class', 'rounded border w-100 mb-4 p-2 shadow');
+            var chart = new Chart(canvas, testcase);
+            testcases.appendChild(canvas);
+        });
+
+        // add files
+        console.log('Files');
+        const files = document.querySelector('#fileList');
+        request.response['files'].forEach(file => {
+            var file_item = document.createElement('li');
+            file_item.setAttribute('class', 'list-group-item');
+            file_item.innerHTML = `
+                <div class="row">
+                    <div class="col-2 h5">
+                        ${file['title']}
+                    </div>
+                    <div class="col-10 h6">
+                        <a href="${file['url']}">${file['url']}</a>
+                    </div>
+                </div>
+            `;
+            files.appendChild(file_item);
+        });
+
+        // hide loading screen
+        document.querySelector('#loading-active').style.display = 'none';
+
+    };
+    request.send();
+
+});

+ 3 - 3
ui/app/static/js/testrun.js

@@ -313,18 +313,18 @@
         /*
         run the testrun via API web-service
         */
+        
         const request = new XMLHttpRequest();
         request.open('GET', `/testrun/${item_id}/run`);
         request.onload = () => {
             // get response
             const response = request.responseText;
-            //document.querySelector('#loading').style.display = 'none';
-            //alert(response);
             document.documentElement.innerHTML = response;
 
         }
+        // show loading screen
         scroll(0, 0);
-        document.querySelector('#titleLoading').innerHTML = `Running Testrun ID #${item_id}`;
+        document.querySelector('#idLoading').innerHTML = `${item_id}`;
         document.querySelector('#loading').style.display = 'block';
         
         request.send();

+ 27 - 14
ui/app/templates/testrun/dashboard.html

@@ -8,19 +8,30 @@
 
 <main>
 
-<!-- bread crumb -->
-<nav aria-label="breadcrumb">
-    <ol class="breadcrumb">
-        <li class="breadcrumb-item"><a href="/">Home</a></li>
-        <li class="breadcrumb-item active" aria-current="page">Dashboard</li>
-    </ol>
-</nav>
+    <!-- bread crumb -->
+    <nav aria-label="breadcrumb">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a href="/">Home</a></li>
+            <li class="breadcrumb-item active" aria-current="page">Dashboard</li>
+        </ol>
+    </nav>
 
-<!-- flash messages -->
-{% include "includes/flash.html" %}
+    <!-- flash messages -->
+    {% include "includes/flash.html" %}
 
-<!-- testrun list -->
-<div class="container-fluid mt-2">
+    <!-- loading screen -->
+    <div id="loading-active">
+        <div class="d-flex justify-content-center align-items-center flex-column h-80">
+            <div class="display-3 text-white mb-5">Please wait</div>
+            <div class="display-4 text-white" id="titleLoading">Updating Testrun Results</div>
+            <div class="spinner-border text-white spinner mt-5" role="status">
+                <span class="sr-only">Loading...</span>
+            </div>
+        </div>
+    </div>
+
+    <!-- testrun list -->
+    <div class="container-fluid mt-2">
 
     <!-- filters -->
     <div class="row mx-2 pt-3 rounded shadow">
@@ -30,7 +41,7 @@
                 <label class="input-group-text px-4" for="selectName">Name</label>
             </div>
             <select class="custom-select" id="selectName">
-                <option value="0" selected>None</option>
+                <option value="0" selected>All</option>
                 {% for tr in testruns %}
                     <option value="{{ tr.uuid }}">{{ tr.name }}</option>
                 {% endfor %}
@@ -41,7 +52,7 @@
                 <label class="input-group-text px-4" for="selectStage">Stage</label>
             </div>
             <select class="custom-select" id="selectStage">
-                <option value="0" selected>None</option>
+                <option value="0" selected>All</option>
                 {% for stage in stage_set %}
                     <option value="{{ loop.index }}">{{ stage }}</option>
                 {% endfor %}
@@ -62,7 +73,9 @@
     {% for stage in stages[loop.index0] %}
         <div class="row hovered-item border-top py-3 mx-2" data-id="{{ tr.uuid }}" data-stage="{{ stage }}">
             <div class="col-md-3 text-break">
-                <p class="text-primary">{{ tr.name }}</p>
+                <a href="/testrun?show={{ tr.uuid }}#item{{ tr.uuid }}" class="text-primary">
+                    {{ tr.name }}
+                </a>
                 <p class="text-secondary">{{ stage }}</p> 
             </div>
             <div class="col-md-3">

+ 4 - 3
ui/app/templates/testrun/item_list.html

@@ -23,9 +23,10 @@
 {% if type == 'testrun' %}
 <div id="loading">
     <div class="d-flex justify-content-center align-items-center flex-column h-80">
-        <h1>Please wait</h1>
-        <h2 id="titleLoading">Loading...</h2>
-        <div class="spinner-border spinner mt-5" role="status">
+        <div class="display-3 text-white-50 mb-5">Please wait</div>
+        <div class="display-4 text-white" id="titleLoading">Launching Testrun</div>
+        <div class="h1 text-white" id="idLoading">Loading...</div>
+        <div class="spinner-border text-white spinner mt-5" role="status">
             <span class="sr-only">Loading...</span>
         </div>
     </div>

+ 93 - 0
ui/app/templates/testrun/summary.html

@@ -0,0 +1,93 @@
+{% extends "base.html" %}
+
+{% block title %}
+    Testrun Execution Summary
+{% endblock %}
+
+{% block content %}
+
+<main id="{{ call }}">
+
+    <!-- bread crumb -->
+    <nav aria-label="breadcrumb">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a href="/">Home</a></li>
+            <li class="breadcrumb-item"><a href="/testrun">{{ 'testrun'|name_by_type }}</a></li>
+            <li class="breadcrumb-item active" aria-current="page">{{ call }}</li>
+        </ol>
+    </nav>
+
+    <!-- flash messages -->
+    {% include "includes/flash.html" %}
+
+    <!-- loading screen -->
+    <div id="loading-active">
+        <div class="d-flex justify-content-center align-items-center flex-column h-80">
+            <div class="display-3 text-white mb-5">Please wait</div>
+            <div class="display-4 text-white" id="titleLoading">Retrieving Testrun Results</div>
+            <div class="h1 text-white" id="idLoading">{{ call }}</div>
+            <div class="spinner-border text-white spinner mt-5" role="status">
+                <span class="sr-only">Loading...</span>
+            </div>
+        </div>
+    </div>
+
+    <!-- Report -->
+    <div class="container">
+
+        <div class="d-flex align-items-center p-3 my-4 rounded shadow">
+            <div class="lh-100">
+                <a href="/testrun?show={{ call.testrun.uuid }}#item{{ call.testrun.uuid }}" class="display-4 text-primary">
+                    {{ call.testrun.name }}
+                </a>
+                <div class="mt-1 text-dark">Execution ID: {{ call }}</div>
+                <div class="text-secondary mb-2">Execution time: {{ call.created }}</div>
+            </div>
+        </div>
+    
+        <!-- Summary -->
+        <div class="display-4 p-2">Summary</div>
+        <div class="card mb-4 shadow">
+            <div class="row no-gutters">
+                <div class="col-md-5">
+                    <canvas class="card-img my-3" id="statusChart"></canvas>
+                </div>
+                <div class="col-md-7">
+                    <div class="card-body row h-100 align-items-center">
+                        <div class="d-flex flex-column col-3 border-right px-0" id="records">
+                            <div class="display-3 text-center"></div>
+                            <h4 class="text-center">TESTS</h4>
+                        </div>
+                        <div class="d-flex flex-column col-3 text-success px-0" id="passed">
+                            <div class="display-3 text-center"></div>
+                            <h4 class="text-center">PASSED</h4>
+                        </div>
+                        <div class="d-flex flex-column col-3 text-danger px-0" id="failed">
+                            <div class="display-3 text-center"></div>
+                            <h4 class="text-center">FAILED</h4>
+                        </div>
+                        <div class="d-flex flex-column col-3 text-secondary px-0" id="paused">
+                            <div class="display-3 text-center"></div>
+                            <h4 class="text-center">PAUSED</h4>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- TestCases -->
+        <div class="display-4 p-2">Test Cases</div>
+        <div id="testcaseCharts"></div>
+
+        <!-- Files -->
+        <div class="display-4 p-2">Files</div>
+        <ul class="list-group w-100 mb-5 shadow" id="fileList"></ul>
+
+    </div>
+
+</main>
+
+<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
+<script src="{{ url_for('static', filename='js/summary.js') }}"></script>
+
+{% endblock %}

+ 17 - 12
ui/app/templates/testrun/testrun_status.html

@@ -13,7 +13,7 @@
     <ol class="breadcrumb">
         <li class="breadcrumb-item"><a href="/">Home</a></li>
         <li class="breadcrumb-item"><a href="/testrun">{{ 'testrun'|name_by_type }}</a></li>
-        <li class="breadcrumb-item active" aria-current="page">{{ results.id }}</li>
+        <li class="breadcrumb-item active" aria-current="page">{{ call }}</li>
     </ol>
 </nav>
 
@@ -25,23 +25,28 @@
         <h1>Testrun Execution Status</h1>
     </div>
 
-    {% if status == 202 %}
-    <div class="card bg-light mt-3">
-        <div class="card-header">In progress</div>
+    {% if call.is_failed %}
+    <div class="card text-white bg-danger mt-3">
+        <div class="card-header">Failed</div>
         <div class="card-body">
-            <h5 class="card-title">ID: {{ results.id }}</h5>
-            <p class="card-text">Testrun is executing. After the testrun is completed the results will be availabale at:</p>
-            <a href={{ url_for("get_results", call_id=results.id) }}>{{ request.host }}{{ url_for("get_results", call_id=results.id) }}</a>
+            <h5 class="card-title">ID: {{ call }}</h5>
+            <h5 class="card-title">ERROR:</h5>
+            <p class="card-text">{{ call.error_message }}</p>
+            <a href="/testrun?show={{ call.testrun.uuid }}#item{{ call.testrun.uuid }}" class="btn btn-dark">Back to Testrun</a>
         </div>
     </div>
     {% else %}
-    <div class="card text-white bg-danger mt-3">
-        <div class="card-header">Error</div>
+    <div class="card bg-light mt-3">
+        <div class="card-header">In progress</div>
         <div class="card-body">
-            <h5 class="card-title">ID: {{ results.id }}</h5>
-            <p class="card-text">{{ results.error }}</p>
+            <h5 class="card-title">ID: {{ call }}</h5>
+            <p class="card-text">Testrun is executing. After the Testrun finish, the results will be available at:</p>
+            <a href={{ url_for("get_results", call_id=call) }}>{{ request.host }}{{ url_for("get_results", call_id=call) }}</a>
         </div>
-    </div>
+        <div class="card-body">
+            <a href="/testrun?show={{ call.testrun.uuid }}#item{{ call.testrun.uuid }}" class="btn btn-primary">Back to Testrun</a>
+        </div>
+    </div>    
     {% endif %}
 
 </div>

+ 40 - 37
ui/app/utils.py

@@ -1063,45 +1063,48 @@ def log_import(item, item_id, updated):
 
 
 #
-# Updating Testrun results
+# Updating Testrun calls
 #
-def update_results():
+def update_all_calls():
 	for call in models.TestRunCall.query.filter_by(is_finished=False).filter_by(is_failed=False).all():
-		# call API
-		url = '/'.join((
-			'http:/',
-			app.config.get('BAANGT_API_HOST'),
-			app.config.get('BAANGT_API_STATUS'),
-			str(call),
-		))
-		app.logger.info(f'Call results by {current_user}. API URL: {url}')
-		r = requests.get(url)
-		app.logger.info(f'Response: {r.status_code}')
-
-		if r.status_code == 500:
-			raise Exception('API Server interanal error')
+		update_call(call)
+
+def update_call(call):
+	# call API
+	url = '/'.join((
+		'http:/',
+		app.config.get('BAANGT_API_HOST'),
+		app.config.get('BAANGT_API_STATUS'),
+		str(call),
+	))
+	app.logger.info(f'Call results by {current_user}. API URL: {url}')
+	r = requests.get(url)
+	app.logger.info(f'Response: {r.status_code}')
+
+	if r.status_code == 500:
+		raise Exception('API Server interanal error')
 		
-		elif r.status_code == 200:
-			results = json.loads(r.text)
-			call.is_finished = True
-			call.testrecords = results['Summary'].get('TestRecords')
-			call.successful = results['Summary'].get('Successful')
-			call.error = results['Summary'].get('Error')
-			call.paused = results['Summary'].get('Paused')
-			call.stage = results['GlobalSettings'].get('Stage')
-			# calculate duration in ms
-			starttime = datetime.strptime(results['Summary'].get('StartTime'), '%H:%M:%S')
-			endtime = datetime.strptime(results['Summary'].get('EndTime'), '%H:%M:%S')
-			duration = endtime - starttime
-			call.duration = duration.seconds + round(duration.microseconds/1000000)
-			# commit update
-			db.session.commit()
-			app.logger.info(f'Testrun call {call} updated by {current_user}')
+	elif r.status_code == 200:
+		results = json.loads(r.text)
+		call.is_finished = True
+		call.testrecords = results['Summary'].get('TestRecords')
+		call.successful = results['Summary'].get('Successful')
+		call.error = results['Summary'].get('Error')
+		call.paused = results['Summary'].get('Paused')
+		call.stage = results['GlobalSettings'].get('Stage')
+		# calculate duration in ms
+		starttime = datetime.strptime(results['Summary'].get('StartTime'), '%H:%M:%S')
+		endtime = datetime.strptime(results['Summary'].get('EndTime'), '%H:%M:%S')
+		duration = endtime - starttime
+		call.duration = duration.seconds + round(duration.microseconds/1000000)
+		# commit update
+		db.session.commit()
+		app.logger.info(f'Testrun call {call} updated by {current_user}')
 		
-		elif r.status_code//100 == 4:
-			call.is_failed = True
-			call.error_message = r.text[-512:]
-			# commit update
-			db.session.commit()
-			app.logger.warning(f'Testrun call {call} updated by {current_user} as failed:\n{r.text}')
+	elif r.status_code//100 == 4:
+		call.is_failed = True
+		call.error_message = r.text[-512:]
+		# commit update
+		db.session.commit()
+		app.logger.warning(f'Testrun call {call} updated by {current_user} as failed:\n{r.text}')
 

+ 65 - 11
ui/app/views.py

@@ -3,7 +3,7 @@ from flask import render_template, redirect, flash, request, url_for, send_from_
 from flask_login import login_required, current_user, login_user, logout_user
 from flask.logging import default_handler
 from app import app, db, models, forms, utils
-from app.charts import ChartJS
+from app.charts import DashboardCharts, SummaryCharts
 from datetime import datetime
 from sqlalchemy import func
 import requests
@@ -411,7 +411,7 @@ def run_testrun(item_id):
 		db.session.add(tr_call)
 		db.session.commit()
 		app.logger.info(f'Created Testrun Call {jsonResult["id"]} by {current_user}.')
-		return render_template('testrun/testrun_status.html', results=jsonResult, status=r.status_code) 
+		return render_template('testrun/testrun_status.html', call=tr_call) 
 	else:
 		flash(f'ERROR: {r.text}', 'danger')
 		return redirect(url_for('item_list', item_type='testrun'))
@@ -534,7 +534,7 @@ def update_testsun(item_id):
 
 @app.route('/results/<string:call_id>')
 @login_required
-def get_results(call_id):
+def get_summary(call_id):
 	#
 	# shows Testrun results
 	#
@@ -559,11 +559,70 @@ def get_results(call_id):
 	app.logger.info(f'API response: status code {r.status_code} JSON {jsonResult}')
 
 	if r.status_code == 200:
+		print(json.dumps(jsonResult, indent=2))
+		#call = models.TestRunCall.query.get(uuid.UUID(call_id).bytes)
+		#charts = ChartJS([models.TestRunCall.query.get(uuid.UUID(call_id).bytes)])
+		jsonResult['chart'] = utils
 		return render_template('testrun/testrun_results.html', results=jsonResult)
 
 	return render_template('testrun/testrun_status.html', results=jsonResult, status=r.status_code) 
 
 
+@app.route('/summary/<string:call_id>', methods=['GET', 'POST'])
+@login_required
+def get_results(call_id):
+	#
+	# show Testrun call results
+	#
+
+	# fetch call data
+	if request.method == 'POST':
+
+		# call API
+		url = '/'.join(('http:/', app.config.get('BAANGT_API_HOST'), app.config.get('BAANGT_API_STATUS'), call_id))
+		app.logger.info(f'Call results by {current_user}. API URL: {url}')
+		try:
+			r = requests.get(url)
+		except Exception as error:
+			app.logger.error(f'Failed to connect to {app.config.get("TESTRUN_SERVICE_HOST")} by {current_user}. {error}')
+			return jsonify({'error': f'Cannot establish connection: {app.config.get("TESTRUN_SERVICE_HOST")}'}), 400
+
+		app.logger.info(f'API response: status code {r.status_code} -  JSON {r.text}')
+
+		# API server error handler
+		if r.status_code == 500:
+			app.logger.error('ERROR: API Server interanal error')
+			return jsonify({'error': 'API Server interanal error'}), 400
+
+		if r.status_code == 200:
+			# build charts
+			try:
+				charts = SummaryCharts(json.loads(r.text))
+				print('******* CHARTS:')
+				print(json.dumps(charts.get_charts(), indent=2))
+				return jsonify(charts.get_charts()), 200
+			except Exception as e:
+				print(f'ERROR: {e}')
+				return jsonify({'error': f'{e}'}), 400
+
+		return jsonify({'error': f'Response: {r.status_code}'}), 400
+
+	# show results
+	call = models.TestRunCall.query.get(uuid.UUID(call_id).bytes)
+
+	if not call.is_finished and not call.is_failed:
+		# update Testrun calls
+		try:
+			utils.update_call(call)
+		except Exception as e:
+			flash(f'{e}', 'warning')
+
+	if not call.is_finished or call.is_failed:
+		return render_template('testrun/testrun_status.html', call=call)
+
+	return render_template('testrun/summary.html', call=call)
+
+
 @app.route('/chart/<string:testrun_id>/<string:stage>', methods=['POST'])
 @login_required
 def get_charts(testrun_id, stage):
@@ -573,15 +632,10 @@ def get_charts(testrun_id, stage):
 
 	testrun = models.Testrun.query.get(uuid.UUID(testrun_id).bytes)
 
-	#print('*********** CHARTS')
-	#print(testrun_id)
-	#print(stage)
-	#print(len(testrun.finished_calls(stage=stage)))
-
 	# build charts
 	try:
-		charts = ChartJS(testrun.finished_calls(stage=stage))
-		return jsonify(charts.dashboard()), 200
+		charts = DashboardCharts(testrun.finished_calls(stage=stage))
+		return jsonify(charts.get_charts()), 200
 	except Exception as e:
 		print(f'ERROR: {e}')
 		return jsonify({'error': 'error'}), 400
@@ -596,7 +650,7 @@ def dashboard():
 
 	# update Testrun calls
 	try:
-		utils.update_results()
+		utils.update_all_calls()
 	except Exception as e:
 		flash(f'{e}', 'warning')
 

+ 3 - 0
ui/config.py

@@ -31,6 +31,9 @@ class Config(object):
 	BAANGT_DATAFILE_HOST = os.getenv('BAANGT_DATAFILE_HOST') or '127.0.0.1:5050'
 	BAANGT_DATAFILE_SAVE = 'save'
 	BAANGT_DATAFILE_UPDATE = 'update'
+	BAANGT_DATAFILE_GET = 'get'
+	BAANGT_DATAFILE_LOGFILE = 'logfile'
+	BAANGT_DATAFILE_RESULTS = 'outfile'
 
 
 	

BIN
ui/testrun.db