TestRun.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. from baangt.base.BrowserHandling import BrowserDriver
  2. from baangt.base.ApiHandling import ApiHandling
  3. from baangt.base.ExportResults import ExportResults
  4. from baangt.base.Timing import Timing
  5. from baangt.base.utils import utils
  6. from baangt.base import GlobalConstants as GC
  7. from baangt.base.TestRunExcelImporter import TestRunExcelImporter
  8. from baangt.base.TestRunUtils import TestRunUtils
  9. # needed - they'll be used dynamically later
  10. from baangt.TestSteps.TestStepMaster import TestStepMaster
  11. from baangt.TestCase.TestCaseMaster import TestCaseMaster
  12. from baangt.TestCaseSequence.TestCaseSequenceMaster import TestCaseSequenceMaster
  13. import logging
  14. from pathlib import Path
  15. import sys
  16. logger = logging.getLogger("pyC")
  17. class TestRun:
  18. """
  19. This is the main Class of Testexecution in the baangt Framework. It is usually started
  20. from baangt.py
  21. """
  22. def __init__(self, testRunName, globalSettingsFileNameAndPath=None):
  23. """
  24. @param testRunName: The name of the TestRun to be executed.
  25. @param globalSettingsFileNameAndPath: from where to read the <globals>.json
  26. """
  27. self.browser = {}
  28. self.apiInstance = None
  29. self.testType = None
  30. self.kwargs = {}
  31. self.dataRecords = {}
  32. self.globalSettingsFileNameAndPath = globalSettingsFileNameAndPath
  33. self.globalSettings = {}
  34. self.testRunName, self.testRunFileName = TestRun._sanitizeTestRunNameAndFileName(testRunName)
  35. self.timing = Timing()
  36. self.timing.takeTime(GC.TIMING_TESTRUN) # Initialize Testrun Duration
  37. self.testRunUtils = TestRunUtils()
  38. self._initTestRun()
  39. self._loadJSONTestRunDefinitions()
  40. self._loadExcelTestRunDefinitions()
  41. self.executeTestRun()
  42. self.tearDown()
  43. def tearDown(self):
  44. """
  45. Close browser (unless stated in the Globals to not do so) and API-Instances
  46. Take overall Time spent for the complete TestRun
  47. Write results of TestRun to output channel(s)
  48. """
  49. if not self.globalSettings.get(GC.EXECUTION_DONTCLOSEBROWSER):
  50. for browserInstance in self.browser.keys():
  51. self.browser[browserInstance].closeBrowser()
  52. self.timing.takeTime(GC.TIMING_TESTRUN)
  53. self.timing.takeTimeSumOutput()
  54. if self.apiInstance:
  55. self.apiInstance.tearDown()
  56. ExportResults(**self.kwargs)
  57. successful, error = self.getSuccessAndError()
  58. logger.info(f"Finished execution of Testrun {self.testRunName}. "
  59. f"{successful} Testcases successfully executed, {error} errors")
  60. print(f"Finished execution of Testrun {self.testRunName}. "
  61. f"{successful} Testcases successfully executed, {error} errors")
  62. def getSuccessAndError(self):
  63. """
  64. Returns number of successful and number of error test cases of the current test run
  65. @rtype: object
  66. """
  67. lError = 0
  68. lSuccess = 0
  69. for value in self.dataRecords.values():
  70. if value[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_ERROR:
  71. lError += 1
  72. elif value[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_SUCCESS:
  73. lSuccess += 1
  74. return lSuccess, lError
  75. def getAllTestRunAttributes(self):
  76. return self.testRunUtils.getCompleteTestRunAttributes(self.testRunName)
  77. def getBrowser(self, browserInstance=1, browserName=None, browserAttributes=None):
  78. """
  79. This method is called whenever a browser instance (existing or new) is needed. If called without
  80. parameters it will create one instance of Firefox (geckodriver).
  81. if global setting TC.EXECUTION_SLOW is set, inform the browser instance about it.
  82. @param browserInstance: Number of the requested browser instance. If none is provided, always the default
  83. browser instance will be returned
  84. @param browserName: one of the browser names (e.g. FF, Chrome) from GC.BROWSER*
  85. @param browserAttributes: optional Browser Attributes
  86. @return: the browser instance of base class BrowserDriver
  87. """
  88. if browserInstance not in self.browser.keys():
  89. logger.info(f"opening new instance {browserInstance} of browser {browserName}")
  90. self._getBrowserInstance(browserInstance=browserInstance)
  91. self.browser[browserInstance].createNewBrowser(browserName=browserName,
  92. desiredCapabilities=browserAttributes)
  93. if self.globalSettings.get("TC." + GC.EXECUTION_SLOW):
  94. self.browser[browserInstance].slowExecutionToggle()
  95. else:
  96. logger.debug(f"Using existing instance of browser {browserInstance}")
  97. return self.browser[browserInstance]
  98. def _getBrowserInstance(self, browserInstance):
  99. self.browser[browserInstance] = BrowserDriver(timing=self.timing,
  100. screenshotPath=self.globalSettings[GC.PATH_SCREENSHOTS])
  101. def getAPI(self):
  102. if not self.apiInstance:
  103. self.apiInstance = ApiHandling()
  104. return self.apiInstance
  105. def setResult(self, recordNumber, dataRecordResult):
  106. logger.debug(f"Received new result for Testrecord {recordNumber}")
  107. self.dataRecords[recordNumber] = dataRecordResult
  108. def executeTestRun(self):
  109. """
  110. Start TestcaseSequence
  111. TestCaseSequence is a sequence of Testcases. In the TestcaseSequence there's a sequential List of
  112. Testcases to be executed.
  113. Before the loop (executeDictSequenceOfClasses) variables inside the the testrun-definition are replaced
  114. by values from the globals-file (e.g. if you want to generally run with FF, but in a certain case you want to
  115. run with Chrome, you'd have FF in the Testrundefinition, but set parameter in globals_chrome.json accordingly
  116. (in this case {"TC.Browser": "CHROME"}. TC.-Prefix signals the logic to look for this variable ("Browser")
  117. inside the testcase definitions and replace it with value "CHROME".
  118. """
  119. self.testRunUtils.replaceGlobals(self.globalSettings)
  120. self.executeDictSequenceOfClasses(
  121. self.testRunUtils.getCompleteTestRunAttributes(self.testRunName)[GC.STRUCTURE_TESTCASESEQUENCE],
  122. counterName=GC.STRUCTURE_TESTCASESEQUENCE)
  123. def executeDictSequenceOfClasses(self, dictSequenceOfClasses, counterName, **kwargs):
  124. """
  125. This is the main loop of the TestCaseSequence, TestCases, TestStepSequences and TestSteps.
  126. The Sequence of which class instance to create is defined by the TestRunAttributes.
  127. Before instancing the class it is checked, whether the class was loaded already and if not, will be loaded
  128. (only if the classname is fully qualified (e.g baangt<projectname>.TestSteps.myTestStep).
  129. If the testcase-Status is already "error" (GC.TESTCASESTATUS_ERROR) we'll stop the loop.
  130. @param dictSequenceOfClasses: The list of classes to be instanced. Must be a dict of {Enum, Classname},
  131. can be also {enum: [classname, <whatEverElse>]}
  132. @param counterName: Which Structure element we're currently looping, e.g. "TestStep" (GC.STRUCTURE_TESTSTEP)
  133. @param kwargs: TestrunAttributes, this TestRun, the Timings-Instance, the datarecord
  134. """
  135. if not kwargs.get(GC.KWARGS_TESTRUNATTRIBUTES):
  136. kwargs[GC.KWARGS_TESTRUNATTRIBUTES] = self.getAllTestRunAttributes()
  137. if not kwargs.get(GC.KWARGS_TESTRUNINSTANCE):
  138. kwargs[GC.KWARGS_TESTRUNINSTANCE] = self
  139. if not kwargs.get(GC.KWARGS_TIMING):
  140. kwargs[GC.KWARGS_TIMING] = self.timing
  141. for key, value in dictSequenceOfClasses.items():
  142. # If any of the previous steps set the testcase to "Error" - exit here.
  143. if kwargs.get(GC.KWARGS_DATA):
  144. if kwargs[GC.KWARGS_DATA][GC.TESTCASESTATUS] == GC.TESTCASESTATUS_ERROR:
  145. logger.info(f"TC is already in status Error - not processing steps {counterName}: {key}, {value}"
  146. f"and everything behind this step")
  147. return
  148. logger.info(f"Starting {counterName}: {key}, {value} ")
  149. kwargs[counterName] = key
  150. if isinstance(value, list):
  151. lFullQualified = value[0] # First List-Entry must hold the ClassName
  152. else:
  153. lFullQualified = value
  154. if "." in lFullQualified:
  155. l_class = TestRun.__dynamicImportClasses(lFullQualified)
  156. else:
  157. l_class = globals()[lFullQualified]
  158. l_class(**kwargs) # Executes the class __init__
  159. self.kwargs = kwargs
  160. def _initTestRun(self):
  161. self.loadJSONGlobals()
  162. if not self.globalSettings.get(GC.PATH_SCREENSHOTS,None):
  163. self.globalSettings[GC.PATH_SCREENSHOTS] = str(Path(self.globalSettingsFileNameAndPath
  164. ).parent.joinpath("Screenshots").expanduser())
  165. self.globalSettings[GC.PATH_EXPORT] = str(Path(self.globalSettingsFileNameAndPath
  166. ).parent.joinpath("1testoutput").expanduser())
  167. self.globalSettings[GC.PATH_IMPORT] = str(Path(self.globalSettingsFileNameAndPath
  168. ).parent.joinpath("0testdateninput").expanduser())
  169. self.globalSettings[GC.PATH_ROOT] = str(Path(self.globalSettingsFileNameAndPath
  170. ).parent.expanduser())
  171. def loadJSONGlobals(self):
  172. if self.globalSettingsFileNameAndPath:
  173. self.globalSettings = utils.openJson(self.globalSettingsFileNameAndPath)
  174. def _loadJSONTestRunDefinitions(self):
  175. if not self.testRunFileName:
  176. return
  177. if ".JSON" in self.testRunFileName.upper():
  178. data = utils.replaceAllGlobalConstantsInDict(utils.openJson(self.testRunFileName))
  179. self.testRunUtils.setCompleteTestRunAttributes(testRunName=self.testRunName,
  180. testRunAttributes=data)
  181. def _loadExcelTestRunDefinitions(self):
  182. if not self.testRunFileName:
  183. return
  184. if ".XLSX" in self.testRunFileName.upper():
  185. logger.info(f"Reading Definition from {self.testRunFileName}")
  186. lExcelImport = TestRunExcelImporter(FileNameAndPath=self.testRunFileName, testRunUtils=self.testRunUtils)
  187. lExcelImport.importConfig()
  188. @staticmethod
  189. def __dynamicImportClasses(fullQualifiedImportName):
  190. """
  191. Requires fully qualified Name of Import-Class and module,
  192. e.g. TestCaseSequence.TCS_VIGO.TCS_VIGO if the class TCS_VIGO
  193. is inside the Python-File TCS_VIGO
  194. which is inside the Module TestCaseSequence
  195. if name is not fully qualified, the ClassName must be identical with the Python-File-Name,
  196. e.g. TestSteps.Franzi will try to import TestSteps.Franzi.Franzi
  197. @param fullQualifiedImportName:
  198. @return: The class instance. If no class instance can be found the TestRun aborts hard with sys.exit
  199. """
  200. importClass = fullQualifiedImportName.split(".")[-1]
  201. if globals().get(importClass):
  202. return globals()[importClass] # Class already imported
  203. if fullQualifiedImportName.split(".")[-2:-1][0] == fullQualifiedImportName.split(".")[-1]:
  204. moduleToImport = ".".join(fullQualifiedImportName.split(".")[0:-1])
  205. else:
  206. moduleToImport = fullQualifiedImportName
  207. mod = __import__(moduleToImport, fromlist=importClass)
  208. logger.debug(f"Imported module {fullQualifiedImportName}, result was {str(mod)}")
  209. retClass = getattr(mod, importClass)
  210. if not retClass:
  211. logger.critical(f"Can't import module: {fullQualifiedImportName}")
  212. sys.exit("Critical Error in Class import - can't continue. "
  213. "Please maintain proper classnames in Testrundefinition.")
  214. return retClass
  215. @staticmethod
  216. def _sanitizeTestRunNameAndFileName(TestRunNameInput):
  217. """
  218. @param TestRunNameInput: The complete File and Path of the TestRun definition (JSON or XLSX).
  219. @return: TestRunName and FileName (if definition of testrun comes from a file (JSON or XLSX)
  220. """
  221. if ".XLSX" in TestRunNameInput.upper() or ".JSON" in TestRunNameInput.upper():
  222. lRunName = utils.extractFileNameFromFullPath(TestRunNameInput)
  223. lFileName = TestRunNameInput
  224. else:
  225. lRunName = TestRunNameInput
  226. lFileName = None
  227. return lRunName, lFileName