TestRun.py 18 KB

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