TestRun.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. from baangt.base.BrowserHandling.BrowserHandling import BrowserDriver
  2. from baangt.base.ApiHandling import ApiHandling
  3. from baangt.base.ExportResults.ExportResults import ExportResults
  4. from baangt.base.Utils import utils
  5. from baangt.base import GlobalConstants as GC
  6. from baangt.base.TestRunExcelImporter import TestRunExcelImporter
  7. from baangt.base.BrowserFactory import BrowserFactory
  8. # needed - they'll be used dynamically later
  9. from baangt.TestSteps.TestStepMaster import TestStepMaster
  10. from baangt.TestCase.TestCaseMaster import TestCaseMaster
  11. from baangt.TestCaseSequence.TestCaseSequenceMaster import TestCaseSequenceMaster
  12. from baangt.base.ProxyRotate import ProxyRotate
  13. from baangt.base.FilesOpen import FilesOpen
  14. import xlsxwriter
  15. import os
  16. import json
  17. import logging
  18. from pathlib import Path
  19. import sys
  20. from baangt.base.Timing.Timing import Timing
  21. from baangt.base.TestRunUtils import TestRunUtils
  22. from baangt.base.TestRun.ClassesForObjects import ClassesForObjects
  23. import time
  24. from baangt.base.PathManagement import ManagedPaths
  25. from uuid import uuid4
  26. from baangt.base.RuntimeStatistics import Statistic
  27. from baangt.base.SendReports import Sender
  28. import signal
  29. from baangt.TestDataGenerator.TestDataGenerator import TestDataGenerator
  30. logger = logging.getLogger("pyC")
  31. class TestRun:
  32. """
  33. This is the main Class of Testexecution in the baangt Framework. It is usually started
  34. from baangtIA.py
  35. """
  36. def __init__(self, testRunName, globalSettingsFileNameAndPath=None,
  37. testRunDict=None, uuid=uuid4(), executeDirect=True, noCloneXls=False): # -- API support: testRunDict --
  38. """
  39. @param testRunName: The name of the TestRun to be executed.
  40. @param globalSettingsFileNameAndPath: from where to read the <globals>.json
  41. """
  42. # Take over importing parameters:
  43. self.uuid = uuid
  44. logger.info(f'Init Testrun, uuid is {self.uuid}')
  45. self.testRunDict = testRunDict
  46. self.globalSettingsFileNameAndPath = globalSettingsFileNameAndPath
  47. self.testRunName, self.testRunFileName = \
  48. self._sanitizeTestRunNameAndFileName(testRunName, executeDirect)
  49. # Initialize everything else
  50. self.apiInstance = None
  51. self.testType = None
  52. self.networkInfo = None
  53. self.results = None
  54. self.browserFactory = None
  55. self.kwargs = {}
  56. self.dataRecords = {}
  57. self.globalSettings = {}
  58. self.json_dict = {} # Used to maintain records of RLP_ data which will be used will exporting results
  59. self.managedPaths = ManagedPaths()
  60. self.classesForObjects = ClassesForObjects() # Dynamically loaded classes
  61. self.timing = Timing()
  62. self.testRunUtils = TestRunUtils()
  63. self.testCasesEndDateTimes_1D = [] # refer to single execution
  64. self.testCasesEndDateTimes_2D = [[]] # refer to parallel execution
  65. # New way to export additional Tabs to Excel
  66. # If you want to export additional data, place a Dict with Tabname + Datafields in additionalExportTabs
  67. # from anywhere within your custom code base.
  68. self.additionalExportTabs = {}
  69. self.statistics = Statistic()
  70. self.noCloneXls = noCloneXls
  71. signal.signal(signal.SIGINT, self.exit_signal_handler)
  72. signal.signal(signal.SIGTERM, self.exit_signal_handler)
  73. # Initialize other values
  74. self.timing.takeTime(GC.TIMING_TESTRUN) # Initialize Testrun Duration
  75. # Usually the Testrun is called without the parameter executeDirect, meaning it default to "Execute"
  76. # during Unit-Tests we don't want this behaviour:
  77. if executeDirect:
  78. self.executeTestRun()
  79. def exit_signal_handler(self, signal, frame):
  80. self.browserFactory.teardown()
  81. def executeTestRun(self):
  82. self._initTestRunSettingsFromFile() # Loads the globals*.json file
  83. self._loadJSONTestRunDefinitions()
  84. self._loadExcelTestRunDefinitions()
  85. self.browserFactory = BrowserFactory(self)
  86. self.executeTestSequence()
  87. self.tearDown()
  88. try:
  89. Sender.send_all(self.results, self.globalSettings)
  90. except Exception as ex:
  91. logger.error(f"Error from SendAll: {ex}")
  92. def append1DTestCaseEndDateTimes(self, dt):
  93. self.testCasesEndDateTimes_1D.append(dt)
  94. def append2DTestCaseEndDateTimes(self, index, tcAndDt):
  95. tc = tcAndDt[0]
  96. dt = tcAndDt[1]
  97. [self.testCasesEndDateTimes_2D.append([]) for i in range(
  98. index + 1 - len(self.testCasesEndDateTimes_2D))] if index + 1 > len(
  99. self.testCasesEndDateTimes_2D) else None
  100. self.testCasesEndDateTimes_2D[index].append([tc, dt])
  101. def tearDown(self):
  102. """
  103. Close browser (unless stated in the Globals to not do so) and API-Instances
  104. Take overall Time spent for the complete TestRun
  105. Write results of TestRun to output channel(s)
  106. """
  107. self.timing.takeTime(GC.TIMING_TESTRUN)
  108. self.timing.takeTimeSumOutput()
  109. if self.apiInstance:
  110. self.apiInstance.tearDown()
  111. network_info = self.browserFactory.teardown()
  112. self.kwargs['networkInfo'] = network_info
  113. if self.testCasesEndDateTimes_1D:
  114. self.kwargs['testCasesEndDateTimes_1D'] = self.testCasesEndDateTimes_1D
  115. if self.testCasesEndDateTimes_2D and self.testCasesEndDateTimes_2D[0]:
  116. self.kwargs['testCasesEndDateTimes_2D'] = self.testCasesEndDateTimes_2D
  117. if len(self.additionalExportTabs) > 0:
  118. self.kwargs[GC.EXPORT_ADDITIONAL_DATA] = self.additionalExportTabs
  119. self.results = ExportResults(**self.kwargs) # -- API support: self.results --
  120. successful, error = self.getSuccessAndError()
  121. waiting = self.getWaiting()
  122. self.statistics.update_all(successful, error, waiting)
  123. logger.info(f"Finished execution of Testrun {self.testRunName}. "
  124. f"{successful} Testcases successfully executed, {error} errors")
  125. print(f"Finished execution of Testrun {self.testRunName}. "
  126. f"{successful} Testcases successfully executed, {error} errors")
  127. def getSuccessAndError(self):
  128. """
  129. Returns number of successful and number of error test cases of the current test run
  130. @rtype: object
  131. """
  132. lError = 0
  133. lSuccess = 0
  134. for value in self.dataRecords.values():
  135. if value[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_ERROR:
  136. lError += 1
  137. elif value[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_SUCCESS:
  138. lSuccess += 1
  139. return lSuccess, lError
  140. def getWaiting(self):
  141. lWaiting =0
  142. for value in self.dataRecords.values():
  143. if value[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_WAITING:
  144. lWaiting += 1
  145. return lWaiting
  146. def getAllTestRunAttributes(self):
  147. return self.testRunUtils.getCompleteTestRunAttributes(self.testRunName)
  148. def getBrowser(self, browserInstance=0, browserName=None, browserAttributes=None, mobileType=None, mobileApp=None,
  149. desired_app=None, mobile_app_setting=None, browserWindowSize=None):
  150. return self.browserFactory.getBrowser(browserInstance=browserInstance,
  151. browserName=browserName,
  152. browserAttributes=browserAttributes,
  153. mobileType=mobileType,
  154. mobileApp=mobileApp,
  155. desired_app=desired_app,
  156. mobile_app_setting=mobile_app_setting,
  157. browserWindowSize=browserWindowSize)
  158. def getAPI(self):
  159. if not self.apiInstance:
  160. self.apiInstance = ApiHandling()
  161. return self.apiInstance
  162. def setResult(self, recordNumber, dataRecordResult):
  163. logger.debug(f"Received new result for Testrecord {recordNumber}")
  164. self.dataRecords[recordNumber] = dataRecordResult
  165. def executeTestSequence(self):
  166. """
  167. Start TestcaseSequence
  168. TestCaseSequence is a sequence of Testcases. In the TestcaseSequence there's a sequential List of
  169. Testcases to be executed.
  170. Before the loop (executeDictSequenceOfClasses) variables inside the the testrun-definition are replaced
  171. by values from the globals-file (e.g. if you want to generally run with FF, but in a certain case you want to
  172. run with Chrome, you'd have FF in the Testrundefinition, but set parameter in globals_chrome.json accordingly
  173. (in this case {"TC.Browser": "CHROME"}. TC.-Prefix signals the logic to look for this variable ("Browser")
  174. inside the testcase definitions and replace it with value "CHROME".
  175. """
  176. self.testRunUtils.replaceGlobals(self.globalSettings)
  177. self.testRunUtils.replaceClasses(self.testRunName, self.classesForObjects)
  178. kwargs = {GC.KWARGS_TESTRUNATTRIBUTES: self.getAllTestRunAttributes(),
  179. GC.KWARGS_TESTRUNINSTANCE: self,
  180. GC.KWARGS_TIMING: self.timing}
  181. self.executeDictSequenceOfClasses(
  182. kwargs[GC.KWARGS_TESTRUNATTRIBUTES][GC.STRUCTURE_TESTCASESEQUENCE],
  183. counterName=GC.STRUCTURE_TESTCASESEQUENCE, **kwargs)
  184. def executeDictSequenceOfClasses(self, dictSequenceOfClasses, counterName, **kwargs):
  185. """
  186. This is the main loop of the TestCaseSequence, TestCases, TestStepSequences and TestSteps.
  187. The Sequence of which class instance to create is defined by the TestRunAttributes.
  188. Before instancgetBrowsering the class it is checked, whether the class was loaded already and if not, will be loaded
  189. (only if the classname is fully qualified (e.g baangt<projectname>.TestSteps.myTestStep).
  190. If the testcase-Status is already "error" (GC.TESTCASESTATUS_ERROR) we'll stop the loop.
  191. @param dictSequenceOfClasses: The list of classes to be instanced. Must be a dict of {Enum, Classname},
  192. can be also {enum: [classname, <whatEverElse>]}
  193. @param counterName: Which Structure element we're currently looping, e.g. "TestStep" (GC.STRUCTURE_TESTSTEP)
  194. @param kwargs: TestrunAttributes, this TestRun, the Timings-Instance, the datarecord
  195. """
  196. if not kwargs.get(GC.KWARGS_TESTRUNATTRIBUTES):
  197. kwargs[GC.KWARGS_TESTRUNATTRIBUTES] = self.getAllTestRunAttributes()
  198. if not kwargs.get(GC.KWARGS_TESTRUNINSTANCE):
  199. kwargs[GC.KWARGS_TESTRUNINSTANCE] = self
  200. logger.info('get into not kwargs.getGC.KWARGS_TESTRUNINSTANCE, id is {}'.format(id(self)))
  201. if not kwargs.get(GC.KWARGS_TIMING):
  202. kwargs[GC.KWARGS_TIMING] = self.timing
  203. for key, value in dictSequenceOfClasses.items():
  204. # If any of the previous steps set the testcase to "Error" - exit here.
  205. if kwargs.get(GC.KWARGS_DATA):
  206. if kwargs[GC.KWARGS_DATA][GC.TESTCASESTATUS] == GC.TESTCASESTATUS_ERROR:
  207. logger.info(f"TC is already in status Error - not processing steps {counterName}: {key}, {value}"
  208. f"and everything behind this step")
  209. return
  210. if kwargs[GC.KWARGS_DATA].get(GC.TESTCASESTATUS_STOP):
  211. logger.info(f"TC wanted to stop. Not processing steps {counterName}: {key}, {value}"
  212. f"and everything behind this step. TC-Status is "
  213. f"{kwargs[GC.KWARGS_DATA][GC.TESTCASESTATUS]}")
  214. return
  215. if kwargs[GC.KWARGS_DATA].get(GC.TESTCASESTATUS_STOPERROR):
  216. kwargs[GC.KWARGS_DATA][GC.TESTCASESTATUS] = GC.TESTCASESTATUS_ERROR
  217. if not kwargs[GC.KWARGS_DATA].get(GC.TESTCASEERRORLOG):
  218. kwargs[GC.KWARGS_DATA][GC.TESTCASEERRORLOG] = "Aborted by command 'TCStopTestCaseError'"
  219. else:
  220. kwargs[GC.KWARGS_DATA][GC.TESTCASEERRORLOG] = kwargs[GC.KWARGS_DATA][GC.TESTCASEERRORLOG] \
  221. + "\nAborted by command 'TCStopTestCaseError'"
  222. return
  223. logger.info(f"Starting {counterName}: {key}")
  224. kwargs[counterName] = key
  225. # Get the class reference:
  226. if isinstance(value, list):
  227. lFullQualified = value[0] # First List-Entry must hold the ClassName
  228. else:
  229. lFullQualified = value
  230. l_class = TestRun.__dynamicImportClasses(lFullQualified)
  231. try:
  232. l_class(**kwargs) # Executes the class´es __init__ method
  233. except TypeError as e:
  234. # Damn! The class is wrong.
  235. l_class = TestRun.__dynamicImportClasses(lFullQualified)
  236. l_class(**kwargs)
  237. self.kwargs = kwargs
  238. def _initTestRunSettingsFromFile(self):
  239. self.__loadJSONGlobals()
  240. self.__setPathsIfNotPredefined()
  241. self.__sanitizeGlobalsValues()
  242. def __setPathsIfNotPredefined(self):
  243. if not self.globalSettings.get(GC.PATH_SCREENSHOTS, None):
  244. self.globalSettings[GC.PATH_SCREENSHOTS] = str(
  245. Path(self.managedPaths.getOrSetScreenshotsPath()).expanduser())
  246. else:
  247. self.managedPaths.getOrSetScreenshotsPath(path=self.globalSettings.get(GC.PATH_SCREENSHOTS))
  248. if not self.globalSettings.get(GC.PATH_EXPORT, None):
  249. self.globalSettings[GC.PATH_EXPORT] = str(Path(self.managedPaths.getOrSetExportPath()).expanduser())
  250. else:
  251. self.managedPaths.getOrSetExportPath(path=self.globalSettings.get(GC.PATH_EXPORT))
  252. if not self.globalSettings.get(GC.PATH_IMPORT, None):
  253. self.globalSettings[GC.PATH_IMPORT] = str(Path(self.managedPaths.getOrSetImportPath()).expanduser())
  254. else:
  255. self.managedPaths.getOrSetImportPath(path=self.globalSettings.get(GC.PATH_IMPORT))
  256. if not self.globalSettings.get(GC.PATH_ROOT, None):
  257. self.globalSettings[GC.PATH_ROOT] = str(Path(self.managedPaths.getOrSetRootPath()).parent.expanduser())
  258. else:
  259. self.managedPaths.getOrSetRootPath(path=self.globalSettings.get(GC.PATH_ROOT))
  260. def __loadJSONGlobals(self):
  261. if self.globalSettingsFileNameAndPath:
  262. self.globalSettings = utils.openJson(self.globalSettingsFileNameAndPath)
  263. # Set default execution STAGE
  264. if not self.globalSettings.get(GC.EXECUTION_STAGE, None):
  265. logger.debug(f"Execution Stage was not set. Setting to default value {GC.EXECUTION_STAGE_TEST}")
  266. self.globalSettings[GC.EXECUTION_STAGE] = GC.EXECUTION_STAGE_TEST
  267. def __sanitizeGlobalsValues(self):
  268. # Support for new dataClass to load different Classes
  269. for key, value in self.globalSettings.items():
  270. if "CL." in key:
  271. self.classesForObjects.__setattr__(key.strip("CL."), value)
  272. # Change boolean strings into boolean values.
  273. if isinstance(value, str):
  274. if value.lower() in ("false", "true", "no", "x"):
  275. self.globalSettings[key] = utils.anything2Boolean(value)
  276. elif "renv_" in value.lower():
  277. self.globalSettings[key] = TestDataGenerator.get_env_variable(value[5:])
  278. if isinstance(value, dict):
  279. if "default" in value:
  280. # This happens in the new UI, if a value was added manually,
  281. # but is not part of the globalSetting.json. In this case there's the whole shebang in a dict. We
  282. # are only interested in the actual value, which is stored in "default":
  283. self.globalSettings[key] = value["default"]
  284. if isinstance(self.globalSettings[key], str):
  285. if "renv_" in self.globalSettings[key].lower():
  286. self.globalSettings[key] = TestDataGenerator.get_env_variable(self.globalSettings[key][5:])
  287. continue
  288. else:
  289. # This could be the "old" way of the globals-file (with {"HEADLESS":"True"})
  290. self.globalSettings[key] = value
  291. continue
  292. if isinstance(value, str) and len(value) > 0:
  293. if value[0] == "{" and value[-1] == "}":
  294. # Dict, that is not seen as dict
  295. value = value.replace("\'", '"')
  296. self.globalSettings[key] = json.loads(value)
  297. if self.globalSettings.get("TC." + GC.EXECUTION_LOGLEVEL):
  298. utils.setLogLevel(self.globalSettings.get("TC." + GC.EXECUTION_LOGLEVEL))
  299. def _loadJSONTestRunDefinitions(self):
  300. if not self.testRunFileName and not self.testRunDict: # -- API support: testRunDict --
  301. return
  302. if self.testRunFileName and ".JSON" in self.testRunFileName.upper(): # -- API support: self.testRunFileName --
  303. data = utils.replaceAllGlobalConstantsInDict(utils.openJson(self.testRunFileName))
  304. self.testRunUtils.setCompleteTestRunAttributes(testRunName=self.testRunName,
  305. testRunAttributes=data)
  306. # -- API support --
  307. # load TestRun from dict
  308. if self.testRunDict:
  309. data = utils.replaceAllGlobalConstantsInDict(self.testRunDict)
  310. self.testRunUtils.setCompleteTestRunAttributes(testRunName=self.testRunName,
  311. testRunAttributes=data)
  312. # -- END of API support --
  313. def _loadExcelTestRunDefinitions(self):
  314. if not self.testRunFileName:
  315. return
  316. if ".XLSX" in self.testRunFileName.upper():
  317. logger.info(f"Reading Definition from {self.testRunFileName}")
  318. lExcelImport = TestRunExcelImporter(FileNameAndPath=self.testRunFileName, testRunUtils=self.testRunUtils)
  319. lExcelImport.importConfig(global_settings=self.globalSettings)
  320. @staticmethod
  321. def __dynamicImportClasses(fullQualifiedImportName):
  322. return utils.dynamicImportOfClasses(fullQualifiedImportName=fullQualifiedImportName)
  323. @staticmethod
  324. def _sanitizeTestRunNameAndFileName(TestRunNameInput, direct):
  325. """
  326. @param TestRunNameInput: The complete File and Path of the TestRun definition (JSON or XLSX).
  327. @return: TestRunName and FileName (if definition of testrun comes from a file (JSON or XLSX)
  328. """
  329. if ".XLSX" in TestRunNameInput.upper() or ".JSON" in TestRunNameInput.upper():
  330. lRunName = utils.extractFileNameFromFullPath(TestRunNameInput)
  331. lFileName = TestRunNameInput
  332. else:
  333. lRunName = TestRunNameInput
  334. lFileName = None
  335. return lRunName, lFileName