Browse Source

Merge branch 'master' into refactor

bernhardbuhl 3 years ago
parent
commit
66b3d75fd5

+ 1 - 1
.bumpversion.cfg

@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 1.2.4
+current_version = 1.2.5
 commit = True
 tag = True
 

+ 97 - 0
baangt/TestSteps/RandomValues.py

@@ -0,0 +1,97 @@
+from baangt.base.Faker import Faker
+import random
+import datetime
+import logging
+
+logger = logging.getLogger("pyC")
+
+class RandomValues:
+    def __init__(self):
+        self.fake = Faker().faker
+
+    def retrieveRandomValue(self, RandomizationType="string", mini=None, maxi=None, format=None):
+        '''
+        updates attribute then calls another function to generate random value and return it to the caller.
+        :param RandomizationType:
+        :param mini:
+        :param maxi:
+        :param format:
+        :return:
+        '''
+        self.RandomizationType = RandomizationType
+        self.min = mini
+        self.max = maxi
+        self.format = format
+        return self.generateValue()
+
+    def generateValue(self):
+        '''
+        Generates random value as per input type and other parameters
+        :return:
+        '''
+        if self.RandomizationType.lower() == "string":
+            self.min = self.min or 3  # If value is none than change it to 3. We can do this in parameter but as we have
+            self.max = self.max or 10 # multiple types and we need different default value for each of them, this is used.
+            return self.fake.pystr(self.min, self.max)  # used faker module of baangt to generate string name
+
+        elif self.RandomizationType.lower() == "name":
+            return self.fake.name()  # used faker module of baangt to generate fake name
+
+        elif self.RandomizationType.lower() == "int":
+            self.min = self.min or 0
+            self.max = self.max or 1000000
+            return random.randint(self.min, self.max)
+
+        elif self.RandomizationType.lower() == "float":
+            self.min = self.min or 0
+            self.max = self.max or 1000
+            flt = random.uniform(self.min, self.max)
+            return flt
+
+        elif self.RandomizationType.lower() == "date":
+            self.format = self.format or "%d/%m/%Y"
+            if self.min:
+                try:
+                    self.min = datetime.datetime.strptime(self.min, self.format).timestamp()
+                except Exception as ex:
+                    logger.info(f"Minimum date's structure or format is incorrect - {str(ex)}")
+                    self.min = 86400
+            else:
+                self.min = 86400
+            if self.max:
+                try:
+                    self.max = datetime.datetime.strptime(self.max, self.format).timestamp()
+                except Exception as ex:
+                    logger.info(f"Maximum date's structure or format is incorrect - {str(ex)}")
+                    self.max = datetime.datetime.now().timestamp()
+            else:
+                self.max = datetime.datetime.now().timestamp()
+            return datetime.datetime.fromtimestamp(random.randint(self.min, self.max)).strftime(self.format)
+
+        elif self.RandomizationType.lower() == "time":
+            self.format = self.format or "%H:%M:%S"
+            base = datetime.datetime(2000, 1, 1)
+            if self.min:
+                try:
+                    time = datetime.datetime.strptime(self.min, self.format)
+                    mini = base.replace(hour=time.hour, minute=time.minute, second=time.second).timestamp()
+                except Exception as ex:
+                    logger.info(f"Minimum time's structure or format is incorrect - {str(ex)}")
+                    mini = base.replace(hour=0, minute=0, second=0).timestamp()
+            else:
+                mini = base.replace(hour=0, minute=0, second=0).timestamp()
+            if self.max:
+                try:
+                    time = datetime.datetime.strptime(self.max, self.format)
+                    maxi = base.replace(hour=time.hour, minute=time.minute, second=time.second).timestamp()
+                except Exception as ex:
+                    logger.info(f"Maximum time's structure or format is incorrect - {str(ex)}")
+                    maxi = base.replace(hour=11, minute=59, second=59).timestamp()
+            else:
+                maxi = base.replace(hour=11, minute=59, second=59).timestamp()
+            return datetime.datetime.fromtimestamp(random.uniform(mini, maxi)).strftime(self.format)
+
+        else:  # if type is not valid this statement will be executed
+            raise BaseException(
+                f"Incorrect type {self.RandomizationType}. Please use string, name, int, float, date, time.")
+

+ 18 - 2
baangt/TestSteps/TestStepMaster.py

@@ -13,6 +13,8 @@ from baangt.base.Utils import utils
 from baangt.base.RuntimeStatistics import Statistic
 import random
 import itertools
+import json
+from baangt.TestSteps.RandomValues import RandomValues
 
 logger = logging.getLogger("pyC")
 
@@ -55,6 +57,7 @@ class TestStepMaster:
                                                          sequence=kwargs.get(GC.STRUCTURE_TESTCASESEQUENCE))
         self.testCase = self.testRunUtil.getTestCaseByNumber(lSequence, kwargs.get(GC.STRUCTURE_TESTCASE))
         self.testStep = self.testRunUtil.getTestStepByNumber(self.testCase, self.testStepNumber)
+        self.randomValues = RandomValues()
 
         try:
             if self.testStep and len(self.testStep) > 1:
@@ -652,8 +655,19 @@ class TestStepMaster:
                     for key in center.split('.')[-1:]:
                         dic = self.iterate_json(dic, key)
                     centerValue = dic
-
-            if centerValue: # if we got value from the passed json then bypass this if else conditions
+            if "random{" in center.lower() and "}" in center:  # random keyword is processed here
+                args = json.loads(center[6:])
+                # dictionary used in converting data in to real parameters which are used by the method
+                change_args = {"type": "RandomizationType", "min": "mini", "max": "maxi", "format": "format"}
+                data = {}  # dictionary containing arguments for method and will be used as **args
+                for keys in args:
+                    if keys not in change_args:  # if key is not a valid argument for method
+                        logger.info(f'"{keys}" is not a valid argument for Random data generator. So it is not used')
+                    else:
+                        data[change_args[keys]] = args[keys]
+                centerValue = self.randomValues.retrieveRandomValue(**data)
+
+            if centerValue:  # if we got value from the passed json then bypass this if else conditions
                 pass
             elif "." not in center:
                 # Replace the variable with the value from data structure
@@ -686,6 +700,8 @@ class TestStepMaster:
                 expression = "".join([left_part, str(centerValue), right_part])
             else:
                 expression = centerValue
+                if type(expression) == float or type(expression) == int:
+                    expression = str(int(expression))
         return expression
 
     def iterate_json(self, data, key):

+ 117 - 96
baangt/base/ResultsBrowser.py

@@ -32,8 +32,8 @@ class QuerySet:
     def length(self):
         return len(self.data)
 
-    def set(self, array):
-        self.data = array
+    def set(self, data):
+        self.data = list(data)
 
     def names(self):
         return {tr.testrunName for tr in self.data}
@@ -158,10 +158,8 @@ class ResultsBrowser:
         self.db = sessionmaker(bind=engine)()
         # result query set
         self.query_set = QuerySet()
-        # tag of the current query set
-        self.tags = None
-        # set of stages
-        self.stages = None
+        # query filters
+        self.filters = {}
         # path management
         self.managedPaths = ManagedPaths()
         logger.info(f'Initiated with DATABASE_URL: {db_url if db_url else DATABASE_URL}')
@@ -170,106 +168,106 @@ class ResultsBrowser:
         self.db.close()
 
     def name_list(self):
-        names = self.db.query(TestrunLog.testrunName).group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
+        names = self.db.query(TestrunLog.testrunName).group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName)
         return [x[0] for x in names]
 
     def stage_list(self):
         stages = self.db.query(GlobalAttribute.value).filter_by(name=GC.EXECUTION_STAGE)\
-            .group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
+            .group_by(GlobalAttribute.value).order_by(GlobalAttribute.value)
         return [x[0] for x in stages]
 
-    def query(self, name=None, stage=None, start_date=None, end_date=None):
+    def globals_names(self):
+        names = self.db.query(GlobalAttribute.name).group_by(GlobalAttribute.name).order_by(GlobalAttribute.name)
+        return [x[0] for x in names]
+
+    def globals_values(self, name):
+        values = self.db.query(GlobalAttribute.value).filter_by(name=name).group_by(GlobalAttribute.value).order_by(GlobalAttribute.value)
+        return [x[0] for x in values]
+
+    def query(self, name=None, stage=None, start_date=None, end_date=None, global_settings={}):
         #
-        # get TestrunLogs by name, stage and dates
+        # get TestrunLogs from db
         #
 
-        # format date
-        format_date = lambda date: date.strftime('%Y-%m-%d') if date else None
-
+        # store filters
+        self.filters = {
+            'name': name,
+            'stage': stage,
+            'start_date': start_date,
+            'end_date': end_date,
+            'globals': global_settings,
+        }
 
         # get records
-        records = []
-        logger.info(f'Quering: name={name}, stage={stage}, dates=[{format_date(start_date)} - {format_date(end_date)}]')
-
-        # filter by name and stage
-        if name and stage:
-            self.stages = {stage}
-            records = self.db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=name)\
-                .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage))).all()
-        
+        logger.info(f'Quering: name={name}, stage={stage}, dates=[{start_date} - {end_date}], globals={global_settings}')
+        records = self.db.query(TestrunLog).order_by(TestrunLog.startTime)
+
         # filter by name
-        elif name:
-            # get Testrun stages
-            stages = self.db.query(GlobalAttribute.value).filter(GlobalAttribute.testrun.has(TestrunLog.testrunName==name))\
-            .filter_by(name=GC.EXECUTION_STAGE).group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
-            self.stages = {x[0] for x in stages}
-
-            for s in self.stages:
-                logs = self.db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=name)\
-                    .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==s))).all()
-                records.extend(logs)
-
-        # filter by stage
-        elif stage:
-            self.stages = {stage}
-            # get Testrun names
-            names = self.db.query(TestrunLog.testrunName)\
-            .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage)))\
-            .group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
-            names = [x[0] for x in names]
-
-            for n in names:
-                logs = self.db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=n)\
-                    .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==stage))).all()
-                records.extend(logs)
-
-        # get all testruns ordered by name and stage
-        else:
-            # get Testrun names
-            names = self.db.query(TestrunLog.testrunName).group_by(TestrunLog.testrunName).order_by(TestrunLog.testrunName).all()
-            names = [x[0] for x in names]
-            
-            self.stages = set()
-            for n in names:
-                # get Testrun stages
-                stages = self.db.query(GlobalAttribute.value).filter(GlobalAttribute.testrun.has(TestrunLog.testrunName==n))\
-                .filter_by(name=GC.EXECUTION_STAGE).group_by(GlobalAttribute.value).order_by(GlobalAttribute.value).all()
-                stages = [x[0] for x in stages]
-                self.stages.update(stages)
-
-                for s in stages:
-                    logs = self.db.query(TestrunLog).order_by(TestrunLog.startTime).filter_by(testrunName=n)\
-                        .filter(TestrunLog.globalVars.any(and_(GlobalAttribute.name==GC.EXECUTION_STAGE, GlobalAttribute.value==s))).all()
-                    records.extend(logs)
-            
-        # filter by dates
-        if start_date and end_date:
-            self.query_set.set([log for log in records if log.startTime > start_date and log.startTime < end_date])
-        elif start_date:
-            self.query_set.set([log for log in records if log.startTime >= start_date])
-        elif end_date:
-            self.query_set.set([log for log in records if log.startTime <= end_date])
-        else:
-            self.query_set.set(records)
-
-        # set the tags
-        self.tags = {
-            'Name': name or 'All',
-            'Stage': stage or 'All',
-            'Date from': format_date(start_date) or 'None',
-            'Date to': format_date(end_date or datetime.now()),
-        }
+        if name:
+            records = records.filter_by(testrunName=name)
+
+        # filter bay dates
+        if start_date:
+            records = records.filter(TestrunLog.startTime >= start_date)
+
+        if end_date:
+            records = records.filter(TestrunLog.endTime <= end_date)
+
+        # filter by globals
+        if stage:
+            records = records.filter(TestrunLog.globalVars.any(and_(
+                GlobalAttribute.name == GC.EXECUTION_STAGE,
+                GlobalAttribute.value == stage,
+            )))
+
+        for key, value in global_settings.items():
+            records = records.filter(TestrunLog.globalVars.any(and_(
+                GlobalAttribute.name == key,
+                GlobalAttribute.value == value,
+            )))
+
+        self.query_set.set(records)
 
         logger.info(f'Number of found records: {self.query_set.length}')
 
-        
+    def filter_to_str(self, key):
+        #
+        # get filter value as str
+        #
+
+        value = self.filters.get(key)
+        if value:
+            return str(value)
+
+        # undefined values
+        if key == 'end_date':
+            return datetime.now().strftime('%Y-%m-%d')
+        if key == 'start_date':
+            return 'None'
+        return 'All'
+
+    def filename(self, extention):
+        #
+        # generate filename for exports
+        #
+
+        filename = '_'.join([
+            'TestrunLogs',
+            self.filter_to_str('name'),
+            self.filter_to_str('stage'),
+            self.filter_to_str('start_date'),
+            self.filter_to_str('end_date'),
+            'globals' if self.filters.get('globals') else 'None',
+        ])
+        return f'{filename}.{extention}'
+            
     def export(self):
         #
         # exports the query set to xlsx
         #
 
         # initialize workbook
-        path_to_file = self.managedPaths.getOrSetDBExportPath().joinpath(f'TestrunLogs_{"_".join(list(map(str, self.tags.values())))}.xlsx')
+        path_to_file = self.managedPaths.getOrSetDBExportPath().joinpath(self.filename('xlsx'))
         workbook = Workbook(str(path_to_file))
 
         # set labels
@@ -279,6 +277,7 @@ class ResultsBrowser:
             'testcase': 'Test Case',
             'stage': 'Stage',
             'duration': 'Avg. Duration',
+            'globals': 'Global Settings',
         }
 
         # set output headers
@@ -320,19 +319,39 @@ class ResultsBrowser:
         time_start = time.time() # -------------------------> time tracker
         # title
         sheets['summary'].sheet.set_column(first_col=0, last_col=0, width=18)
+        sheets['summary'].sheet.set_column(first_col=1, last_col=1, width=10)
         sheets['summary'].header([f'{labels.get("testrun")}s Summary'])
         
         # parameters
-        for key, value in self.tags.items():
+        for key in self.filters:
+            if key != 'globals':
+                sheets['summary'].row([
+                    {
+                        'value': key,
+                        'format': cformats.get('font_bold'),
+                    },
+                    {
+                        'value': self.filter_to_str(key),
+                    },
+                ])
+        # global settings
+        if self.filters.get('globals'):
+            sheets['summary'].hr()
             sheets['summary'].row([
                 {
-                    'value': key,
+                    'value': f"{labels.get('globals')}:",
                     'format': cformats.get('font_bold'),
                 },
-                {
-                    'value': value,
-                }
             ])
+            for name, value in self.filters.get('globals').items():
+                sheets['summary'].row([
+                    {
+                        'value': name,
+                    },
+                    {
+                        'value': value,
+                    },
+                ])
 
         # write output titles
         sheets['output'].header(base_headers + base_fields)
@@ -401,7 +420,7 @@ class ResultsBrowser:
                             'format': cformats.get('font_bold_italic'),
                         },
                         {
-                            'value': f'{labels.get("testcase")} ID',
+                            'value': f'{labels.get("testrun")} ID',
                             'format': cformats.get('font_bold_italic'),
                         },
                     ]
@@ -424,8 +443,9 @@ class ResultsBrowser:
                     ).join(TestCaseField.testcase).join(TestCaseLog.testcase_sequence).join(TestCaseSequenceLog.testrun)\
                     .filter(and_(TestrunLog.id == tr.id, TestCaseSequenceLog.number == tcs_number)).order_by(TestCaseLog.number)
                     tc_id_cur = None
+                    tr_stage = None
                     for name, value, tc_index, tc_id in data.yield_per(500):
-                        # summary data  
+                        # summary data
                         if name == GC.TESTCASESTATUS:
                             status_row.append({
                                 'value': value,
@@ -491,7 +511,8 @@ class ResultsBrowser:
 
         workbook.close()
 
-        logger.info(f'Query successfully exported to {path_to_file} in {time.time()-time_start} seconds')
+        execution_time = round(time.time()-time_start, 2)
+        logger.info(f'Query successfully exported to {path_to_file} in {execution_time} seconds')
 
         return path_to_file
     
@@ -501,7 +522,7 @@ class ResultsBrowser:
         # export to txt
         #
 
-        path_to_file = self.managedPaths.getOrSetDBExportPath().joinpath(f'TestrunLogs_{"_".join(list(map(str, self.tags.values())))}.txt')
+        path_to_file = self.managedPaths.getOrSetDBExportPath().joinpath(self.filename('txt'))
 
         # set labels
         labels = {
@@ -518,8 +539,8 @@ class ResultsBrowser:
             f.write(f'{labels.get("testrun")}s Summary\n\n')
             
             # parameters
-            for key, value in self.tags.items():
-                f.write(f'{key}\t{value}\n')
+            for key in self.filters:
+                f.write(f'{key}\t{self.filter_to_str(key)}\n')
 
             # testruns
             for tr_name in self.query_set.names():

+ 35 - 2
baangt/ui/pyqt/uiDesign.py

@@ -9,6 +9,8 @@
 from PyQt5 import QtCore, QtGui, QtWidgets
 import logging
 
+GLOBALS_FILTER_NUMBER = 2
+
 
 class Ui_MainWindow(QtCore.QObject):
     def setupUi(self, MainWindow):
@@ -455,7 +457,7 @@ class Ui_MainWindow(QtCore.QObject):
         self.queryTitleLabel.setFont(titleFont)
         self.queryTitleLabel.setMinimumSize(QtCore.QSize(0, 0))
         self.queryTitleLabel.setMaximumSize(QtCore.QSize(200, 15))
-        self.queryTitleLabel.setObjectName("stageComboBoxLabel")
+        self.queryTitleLabel.setObjectName("queryTitleLabel")
         self.queryMainLayout.addWidget(self.queryTitleLabel)
 
         # action layout
@@ -491,12 +493,19 @@ class Ui_MainWindow(QtCore.QObject):
         self.queryInputLayout.setColumnStretch(2, 2)
         self.queryInputLayout.setColumnStretch(3, 5)
 
+        self.queryGlobalsLayout = QtWidgets.QGridLayout()
+        self.queryGlobalsLayout.setObjectName("queryGlobalsLayout")
+        self.queryGlobalsLayout.setSpacing(5)
+        self.queryGlobalsLayout.setColumnStretch(0, 8)
+        self.queryGlobalsLayout.setColumnStretch(1, 8)
+
         self.queryButtonLayout = QtWidgets.QHBoxLayout()
         self.queryButtonLayout.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize)
         self.queryButtonLayout.setContentsMargins(5, 5, 5, 10)
         self.queryButtonLayout.setSpacing(15)
 
         self.queryFormLayout.addLayout(self.queryInputLayout)
+        self.queryFormLayout.addLayout(self.queryGlobalsLayout)
         self.queryFormLayout.addLayout(self.queryButtonLayout)
 
         # name
@@ -548,6 +557,29 @@ class Ui_MainWindow(QtCore.QObject):
         self.dateToInput.setObjectName("dateToInput")
         self.queryInputLayout.addWidget(self.dateToInput)
 
+        # Globals
+        # title
+        self.globalsTile = QtWidgets.QLabel(self.queryGroupBox)
+        self.globalsTile.setFont(labelFont)
+        self.globalsTile.setStyleSheet("color: rgb(32, 74, 135);")
+        self.globalsTile.setObjectName("globalsTile")
+        self.queryGlobalsLayout.addWidget(self.globalsTile, 0, 0)
+        # options
+        self.globalsOptions = []
+        for index in range(GLOBALS_FILTER_NUMBER):
+            # name
+            globalsNameComboBox = QtWidgets.QComboBox(self.queryGroupBox)
+            globalsNameComboBox.setStyleSheet("color: rgb(46, 52, 54); background-color: rgb(255, 255, 255);")
+            globalsNameComboBox.setObjectName(f"globalsNameComboBox{index}")
+            self.queryGlobalsLayout.addWidget(globalsNameComboBox, index+1, 0)
+            # value
+            globalsValueComboBox = QtWidgets.QComboBox(self.queryGroupBox)
+            globalsValueComboBox.setStyleSheet("color: rgb(46, 52, 54); background-color: rgb(255, 255, 255);")
+            globalsValueComboBox.setObjectName(f"globalsValueComboBox{index}")
+            self.queryGlobalsLayout.addWidget(globalsValueComboBox, index+1, 1)
+            # store
+            self.globalsOptions.append((globalsNameComboBox, globalsValueComboBox))
+
         # query buttons
         sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
         sizePolicy.setHorizontalStretch(0)
@@ -601,7 +633,7 @@ class Ui_MainWindow(QtCore.QObject):
         # status message
         self.queryStatusLabel = QtWidgets.QLabel(self.queryGroupBox)
         self.queryStatusLabel.setAlignment(QtCore.Qt.AlignTop)
-        self.queryStatusLabel.setObjectName("stageComboBoxLabel")
+        self.queryStatusLabel.setObjectName("queryStatusLabel")
         self.queryMainLayout.addWidget(self.queryStatusLabel)
 
         # logo
@@ -973,6 +1005,7 @@ class Ui_MainWindow(QtCore.QObject):
         self.stageComboBoxLabel.setText(_translate("MainWindow", "Stage"))
         self.dateFromInputLabel.setText(_translate("MainWindow", "Date from"))
         self.dateToInputLabel.setText(_translate("MainWindow", "Date to"))
+        self.globalsTile.setText(_translate("MainWindow", "Global Settings"))
         self.queryMakePushButton.setText(_translate("MainWindow", "Query"))
         self.queryExportPushButton.setText(_translate("MainWindow", "Export"))
         self.openExportPushButton.setText(_translate("MainWindow", "Open Recent"))

+ 43 - 3
baangt/ui/pyqt/uimain.py

@@ -41,6 +41,9 @@ from datetime import datetime
 
 logger = logging.getLogger("pyC")
 
+NAME_PLACEHOLDER = '< Name >'
+VALUE_PLACEHOLDER = '< Value >'
+
 
 class PyqtKatalonUI(ImportKatalonRecorder):
     """ Subclass of ImportKatalonRecorder :
@@ -1365,12 +1368,32 @@ class MainWindow(Ui_MainWindow):
         self.nameComboBox.addItems([''] + self.queryResults.name_list())
         self.stageComboBox.clear()
         self.stageComboBox.addItems([''] + self.queryResults.stage_list())
+        # globals
+        for nameComboBox, valueComboBox in self.globalsOptions:
+            nameComboBox.clear()
+            nameComboBox.addItems([NAME_PLACEHOLDER] + self.queryResults.globals_names())
+            nameComboBox.currentIndexChanged.connect(self.onGlobalsNameChange)
+            valueComboBox.clear()
+            valueComboBox.addItems([VALUE_PLACEHOLDER])
 
         # show the page
         self.stackedWidget.setCurrentIndex(4)
         self.statusMessage("Query Page is triggered", 1000)
 
 
+    @pyqtSlot(int)
+    def onGlobalsNameChange(self, index):
+        #
+        # loads globals values on name changed
+        #
+
+        combo = self.sender()
+        for nameComboBox, valueComboBox in self.globalsOptions:
+            if combo == nameComboBox:
+                valueComboBox.clear()
+                valueComboBox.addItems([VALUE_PLACEHOLDER] + self.queryResults.globals_values(combo.currentText()))
+
+
     @pyqtSlot()
     def makeResultQuery(self):
         #
@@ -1380,11 +1403,28 @@ class MainWindow(Ui_MainWindow):
         # get field data
         name = self.nameComboBox.currentText() or None
         stage = self.stageComboBox.currentText() or None
-        date_from = datetime.strptime(self.dateFromInput.date().toString("yyyyMMdd"), "%Y%m%d")
-        date_to = datetime.strptime(self.dateToInput.date().toString("yyyyMMdd"), "%Y%m%d")
+        #date_from = datetime.strptime(self.dateFromInput.date().toString("yyyyMMdd"), "%Y%m%d")
+        #date_to = datetime.strptime(self.dateToInput.date().toString("yyyyMMdd"), "%Y%m%d")
+        date_from = self.dateFromInput.date().toString("yyyy-MM-dd")
+        date_to = self.dateToInput.date().toString("yyyy-MM-dd")
+        # get globals
+        globals_value = lambda v: v if v != VALUE_PLACEHOLDER else None
+
+        global_settings = { 
+            nameComboBox.currentText(): globals_value(valueComboBox.currentText()) for nameComboBox, valueComboBox in filter(
+                lambda g: g[0].currentText() != NAME_PLACEHOLDER,
+                self.globalsOptions,
+            )
+        }
 
         # make query
-        self.queryResults.query(name=name, stage=stage, start_date=date_from, end_date=date_to)
+        self.queryResults.query(
+            name=name,
+            stage=stage,
+            start_date=date_from,
+            end_date=date_to,
+            global_settings=global_settings,
+        )
 
         # display status
         if self.queryResults.query_set:

+ 48 - 0
docs/SimpleAPI.rst

@@ -116,3 +116,51 @@ manually declaring them:
     * - ANSWER_CONTENT
       - Last content of an API-Call (Post, Get, etc.). Again you can access/extract/replace parts of this content using
         the "." like described in the line above (e.g. ``$(ANSWER_CONTENT.FRANZI)`` to refer to a content part ``FRANZI``.
+
+Random
+------
+Sometimes we need random values like string, name, integer, float, date, time now in such case we have ``random``
+functionality. It is used inside value column of and its structure is
+``$(random{"type":<Type>},"min":<Minimum>,"max"<Maximum>,"format":<Format>)``. Only ``type`` field is compulsory and
+every other fields are optional, also each fields are not useful in every type, e.g.- ``name`` type doesn't need any
+other optional fields as they are use less for it. You can see fields and types supporting them.
+
+
+.. list-table:: Fields supporting types
+   :widths: 25 75
+   :header-rows: 1
+
+   * - Field
+     - Type
+
+   * - type
+     - This field is compulsory and base of ``random`` funtionality.
+       string, name, int, float, date, time are the types currently supported
+
+   * - min
+     - string, int, float, date, time are the types supporting this field. Value of min will be with respect to its
+       type like value for string will be an integer containing minimum number of characters in string and for all other
+       it will be lower limit, for int it will be an integer & float for float, for date value will be a date e.g. -
+       "31/01/2020" and for time it would look like "20:30:59"
+
+   * - max
+     - string, int, float, date, time are the types supporting this field. Value of max will be same like in min,
+       value for string will be an integer containing maximum number of characters in string and for all other it
+       will be upper limit, for int it will be an integer & float for float, for date value will be a date e.g. -
+       "01/06/2020" and for time it would look like "13:10:30"
+
+   * - format
+     - date, time are the only types supporting format field. In above date examples date is in %d/%m/%Y format and
+       time is in %H:%M:%S format. Here "%d" stands for the day, "%m" stands for month, "%Y" stands for year including
+       century e.g.- 2020, if you want only year you can use "%y" e.g. 20. If you use min and max fields in date, time
+       then you must input its written format in format field, default will be ""%d/%m/%Y" for date. Now if you want
+       date with "-" as seperator you can write format as "%d-%m-%Y" so the output would be like "31-01-2020".
+
+       `examples`
+        $(random{"type":"name"})
+        $(random{"type":"string", "min":10, "max":100})
+        $(random{"type":"int", "min":10, "max":100})
+        $(random{"type":"float"})
+        $(random{"type":"date", "min":"20/1/2020", "max":"30/6/2020", "format":"%d/%m/%Y"})
+        $(random{"type":"time"})
+        $(random{"type":"time", "min":"10.30.00", "max":"15.00.00"}, "format": "%H.%M.%S")

+ 20 - 0
docs/changelog.rst

@@ -1,6 +1,26 @@
 Change log
 ==========
 
+1.2.x
+^^^^^
+
+New features:
++++++++++++++
+* Completely new concept for file handling. Original Excel-Files are not overwritten. Including TestDataGenerator.
+* ``Usecount`` columns will show, how often qualitifed test data was selected in all test runs.
+* ``TestResult``: if this column is in a test data sheet, we'll write the test result of each test run (condensed) into this cell
+* Change Log: In all copied files there's a sheet ``Change Log`` which shows per each timestamp, which changes were made to the original sheet. Incredibly helpful feature!
+* Teststeps: New comparison operators ``CO``ntains and ``IP`` (Is Part of) to compare strings
+
+Refactoring:
+++++++++++++
+* Great speed improvements with large files (read and write, also in remote read)
+* Teststeps: ``GoToURL`` parameter now also possible in ``Locator`` (makes more sense). ``Value`` still works fine.
+
+Bugfixing:
+++++++++++
+
+
 1.1.15
 ^^^^^^^
 

+ 1 - 1
docs/conf.py

@@ -24,7 +24,7 @@ copyright = '2020, Bernhard Buhl'
 author = 'Bernhard Buhl'
 
 # The full version, including alpha/beta/rc tags
-release = '1.2.4'
+release = '1.2.5'
 
 
 # -- General configuration ---------------------------------------------------

+ 51 - 1
docs/simpleExample.rst

@@ -188,6 +188,8 @@ More details on Activities
      - Write the text given in column ``value`` to the element specified by ``locator``. Only rarely will you have fixed
        values. Usually you'll assign columns of the test data using variable replacement (e.g. ``$(POSTCODE)`` to set the
        text from column ``POSTCODE`` from the datafile into the destination element.
+       In some cases we need to write any random value in the field or we need random value like name, string, interger,
+       date, etc. for some other purpose for that we ``Random`` funtion. You will learn about it further in this document.
    * - setTextIF
      - Same as SetText, but will only do something in cases where there is a value in the datafile. Similarly to clickIF
        this little helper functionality can help you save hours and hours in creation and maintenance of rocksolid and
@@ -275,7 +277,8 @@ More details on Activities
          Default field-value: {'HouseNumber': '6', 'AdditionalData1': 'Near MahavirChowk', 'AdditionalData2': 'Opposite St. Marish Church', 'StreetName': 'GoreGaon', 'CityName': 'Ambala', 'PostalCode': '160055', 'CountryCode': 'India'} 
 
        These fields can be used as filter criteria in field value.
-       Example value= `{CountryCode:CY, PostlCode: 7}`. 
+       Example value= `{CountryCode:CY, PostlCode: 7}`.
+
 
 
        Resulted field-value :{'HouseNumber': '6', 'AdditionalData1': 'Near MahavirChowk', 'AdditionalData2': 'Opposite St. Marish Church', 'StreetName': 'GoreGaon', 'CityName': 'Ambala', 'PostalCode': '7', 'CountryCode': 'CY'}
@@ -284,3 +287,50 @@ More details on Activities
        If a prefix was povided in field Value2, the fieldnames will be concatenated with this prefix,
        e.g.if value2=`PremiumPayer_`, then the resulting field for CountryCode in testDataDict would become PremiumPayer_CountryCode.
 
+Random
+------
+Sometimes we need random values like string, name, integer, float, date, time now in such case we have ``random``
+functionality. It is used inside value column of and its structure is
+``$(random{"type":<Type>},"min":<Minimum>,"max"<Maximum>,"format":<Format>)``. Only ``type`` field is compulsory and
+every other fields are optional, also each fields are not useful in every type, e.g.- ``name`` type doesn't need any
+other optional fields as they are use less for it. You can see fields and types supporting them.
+
+
+.. list-table:: Fields supporting types
+   :widths: 25 75
+   :header-rows: 1
+
+   * - Field
+     - Type
+
+   * - type
+     - This field is compulsory and base of ``random`` funtionality.
+       string, name, int, float, date, time are the types currently supported
+
+   * - min
+     - string, int, float, date, time are the types supporting this field. Value of min will be with respect to its
+       type like value for string will be an integer containing minimum number of characters in string and for all other
+       it will be lower limit, for int it will be an integer & float for float, for date value will be a date e.g. -
+       "31/01/2020" and for time it would look like "20:30:59"
+
+   * - max
+     - string, int, float, date, time are the types supporting this field. Value of max will be same like in min,
+       value for string will be an integer containing maximum number of characters in string and for all other it
+       will be upper limit, for int it will be an integer & float for float, for date value will be a date e.g. -
+       "01/06/2020" and for time it would look like "13:10:30"
+
+   * - format
+     - date, time are the only types supporting format field. In above date examples date is in %d/%m/%Y format and
+       time is in %H:%M:%S format. Here "%d" stands for the day, "%m" stands for month, "%Y" stands for year including
+       century e.g.- 2020, if you want only year you can use "%y" e.g. 20. If you use min and max fields in date, time
+       then you must input its written format in format field, default will be ""%d/%m/%Y" for date. Now if you want
+       date with "-" as seperator you can write format as "%d-%m-%Y" so the output would be like "31-01-2020".
+
+       `examples`
+        $(random{"type":"name"})
+        $(random{"type":"string", "min":10, "max":100})
+        $(random{"type":"int", "min":10, "max":100})
+        $(random{"type":"float"})
+        $(random{"type":"date", "min":"20/1/2020", "max":"30/6/2020", "format":"%d/%m/%Y"})
+        $(random{"type":"time"})
+        $(random{"type":"time", "min":"10.30.00", "max":"15.00.00"}, "format": "%H.%M.%S")

BIN
examples/CompleteBaangtWebdemo_random.xlsx


+ 1 - 1
requirements.txt

@@ -29,4 +29,4 @@ slack-webhook>=1.0.3
 psutil>=5.7.2
 atlassian-python-api>=1.17.3
 icopy2xls>=0.0.4
-xlsclone>=0.0.5
+xlsclone>=0.0.7

+ 1 - 1
setup.py

@@ -6,7 +6,7 @@ if __name__ == '__main__':
 
     setuptools.setup(
         name="baangt",
-        version="1.2.4",
+        version="1.2.5",
         author="Bernhard Buhl",
         author_email="info@baangt.org",
         description="Open source Test Automation Suite for MacOS, Windows, Linux",

+ 42 - 0
tests/test_Random.py

@@ -0,0 +1,42 @@
+from baangt.TestSteps.TestStepMaster import TestStepMaster
+from baangt.TestSteps.RandomValues import RandomValues
+import datetime
+
+TestStepMaster.testcaseDataDict = {}
+TestStepMaster.randomValues = RandomValues()
+
+
+def test_name():
+    name = TestStepMaster.replaceVariables(TestStepMaster, '$(random{"type":"name"})')
+    assert len(name) > 0 and len(name.split()) > 1
+
+
+def test_string():
+    string1 = TestStepMaster.replaceVariables(TestStepMaster, '$(random{"type":"string"})')
+    assert len(string1) > 0
+    string2 = TestStepMaster.replaceVariables(TestStepMaster, '$(random{"type":"string", "min":10, "max":100})')
+    assert len(string2) > 0 and len(string2) < 101
+
+
+def test_int():
+    integer = TestStepMaster.replaceVariables(TestStepMaster, '$(random{"type":"int", "min":100, "max":1000})')
+    assert int(integer) > 99 and int(integer) < 1001
+
+
+def test_float():
+    flt = TestStepMaster.replaceVariables(TestStepMaster, '$(random{"type":"float", "min":100, "max":1000})')
+    assert float(flt) > 99 and float(flt) < 1001
+
+
+def test_date():
+    date = TestStepMaster.replaceVariables(
+        TestStepMaster, '$(random{"type":"date", "min":"20/1/2020", "max":"30/6/2020", "format":"%d/%m/%Y"})')
+    date_obj = datetime.datetime.strptime(date, "%d/%m/%Y")
+    assert type(date_obj) is datetime.datetime and date_obj.year == 2020
+
+
+def test_time():
+    time = TestStepMaster.replaceVariables(
+        TestStepMaster, '$(random{"type":"time", "min":"10.30.00", "max":"15.00.00", "format": "%H.%M.%S"}')
+    time_obj = datetime.datetime.strptime(time, "%H.%M.%S")
+    assert type(time_obj) is datetime.datetime and 9 < time_obj.hour < 15