From 16e5818f82eae289b2897f47b7dd1448f0806830 Mon Sep 17 00:00:00 2001 From: Tyler Desjardins <117662482+ty-desj@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:11:25 -0400 Subject: [PATCH 1/7] Added preliminary template for execution file. --- templates/execution-template.json | 146 ++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 templates/execution-template.json diff --git a/templates/execution-template.json b/templates/execution-template.json new file mode 100644 index 0000000..c2ae8b1 --- /dev/null +++ b/templates/execution-template.json @@ -0,0 +1,146 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "TASS Execution File", + "description": "A TASS job file, to be executed using the TASS framework.", + "type": "object", + "properties": { + "schema-version": { + "type": "string", + "pattern": "^(\\d+\\.)?(\\d+\\.)?(\\*|\\d+)$" + }, + "Test_cases": { + "description": "List of Test cases that may be executed.", + "type": "array", + "items": {"$ref": "#/$defs/test-case"} + }, + "Browsers": { + "description": "Browser configurations to be used in this test run.", + "type": "array", + "items": {"$ref": "#/$defs/browser"} + }, + "Meta": { + "description": "Additional information about this test run that does not affect the test.", + "type": "object", + "properties": { + "results-path": { + "type": "string", + "description": "File path to write results. Overwrites default." + }, + "pages-path": { + "type": "string", + "description": "File path to read POM files from." + }, + "parent": { + "description": "Description of the parent job if generated from converter.", + "type": "object", + "properties": { + "uuid": {"$ref": "#/$defs/uuid"}, + "title": { + "description": "Title of the parent document.", + "type": "string" + }, + "path": { + "description": "File path of the parent document at the time of conversion.", + "type": "string" + } + } + } + } + } + }, + "$defs": { + "uuid": { + "type": "string", + "description": "Universally Unique Identifier", + "$comment": "Created as a definition for possible future functionality. Validating uuids." + }, + "uuid-list": { + "type": "array", + "items": { + "$ref": "#/$defs/uuid" + }, + "description": "List of uuids" + }, + "test-case": { + "type": "object", + "description": "Describes a singular test case. Test cases should be independant of one another.", + "properties": { + "uuid": {"$ref": "#/$defs/uuid"}, + "title": { + "type": "string", + "description": "Human readable descriptor for the test case." + }, + "build": { + "type": "string", + "description": "Version of the application being tested." + }, + "steps": {"$ref": "#/$defs/uuid-list"}, + "browser_uuid": {"$ref": "#/$defs/uuid"}, + "job_uuid": {"$ref": "#/$defs/uuid"} + }, + "required": ["uuid", "title", "steps"] + }, + "step": { + "type": "object", + "description": "Describes a singular step, a single action to be takenby the automation.", + "properties": { + "uuid": {"$ref": "#/$defs/uuid"}, + "title": { + "type": "string", + "description": "Human readable descriptor for the step." + }, + "action": { + "$comment": "TODO: Create external step schema to further validate steps for each action/parameters.", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items":{ + "type": "string" + }, + "description": "An array with a length of 2, the first string states the module, the second the action." + }, + "parameters": { + "type": "object", + "description": "Parameters content is dependant on the action used." + } + }, + "required": ["uuid", "title", "action", "parameters"] + }, + "browser": { + "type": "object", + "description": "Configuration for a browser.", + "properties": { + "uuid": {"$ref": "#/$defs/uuid"}, + "browser_name": { + "enum": ["chrome", "firefox", "edge"], + "description": "The name of the browser to be used." + }, + "configs": { + "type": "object", + "properties": { + "driver": { + "type": "object", + "description": "Configuration values for the automated driver." + }, + "browser": { + "type": "object", + "description": "Configurations for browser preferences.", + "properties": { + "arguments": { + "type": "array", + "items": { "type": "string" }, + "description": "Flags to be passed to the browser instance" + }, + "preferences": { + "type": "object", + "description": "Key value pairs to set preferences of a browser." + } + } + } + } + } + }, + "required": ["uuid", "browser_name", "configs"] + } + } +} \ No newline at end of file From 95022c7fa1a4dc438c45547cc13f1fa005974515 Mon Sep 17 00:00:00 2001 From: Tyler Desjardins <117662482+ty-desj@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:19:46 -0400 Subject: [PATCH 2/7] Updated template parent uuid --- templates/execution-template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/execution-template.json b/templates/execution-template.json index c2ae8b1..2fadb89 100644 --- a/templates/execution-template.json +++ b/templates/execution-template.json @@ -76,7 +76,7 @@ }, "steps": {"$ref": "#/$defs/uuid-list"}, "browser_uuid": {"$ref": "#/$defs/uuid"}, - "job_uuid": {"$ref": "#/$defs/uuid"} + "parent_uuid": {"$ref": "#/$defs/uuid"} }, "required": ["uuid", "title", "steps"] }, From f9b5d86a88d757482aaea4fd2c06fa7bba1f98b9 Mon Sep 17 00:00:00 2001 From: Tyler Desjardins <117662482+ty-desj@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:11:10 -0400 Subject: [PATCH 3/7] Added missing Steps section. --- templates/execution-template.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/templates/execution-template.json b/templates/execution-template.json index 2fadb89..74cfe49 100644 --- a/templates/execution-template.json +++ b/templates/execution-template.json @@ -13,6 +13,11 @@ "type": "array", "items": {"$ref": "#/$defs/test-case"} }, + "Steps": { + "description": "List of steps, singular actions, that can be executed in a test case.", + "type": "array", + "items": {"$ref": "#/$defs/step"} + }, "Browsers": { "description": "Browser configurations to be used in this test run.", "type": "array", From 01e0da103a6902e38fb695e3011558235eb04948 Mon Sep 17 00:00:00 2001 From: Tyler Desjardins <117662482+ty-desj@users.noreply.github.com> Date: Tue, 29 Oct 2024 07:41:47 -0400 Subject: [PATCH 4/7] Updated parser for new format. In progress. --- tass-base/src/tass/base/__main__.py | 2 - tass-base/src/tass/base/core/tass_files.py | 35 ++++--- .../tass/base/exceptions/assertion_errors.py | 5 +- .../src/tass/base/exceptions/tass_errors.py | 18 ++++ tass-base/src/tass/base/schema/parser.py | 94 ++++++++----------- templates/execution-template.json | 45 ++++++--- 6 files changed, 110 insertions(+), 89 deletions(-) create mode 100644 tass-base/src/tass/base/exceptions/tass_errors.py diff --git a/tass-base/src/tass/base/__main__.py b/tass-base/src/tass/base/__main__.py index 6cae7fa..6e7dc90 100644 --- a/tass-base/src/tass/base/__main__.py +++ b/tass-base/src/tass/base/__main__.py @@ -33,8 +33,6 @@ def _make_report(registrar, func_name, *args, **kwargs): log.debug("Reporter: %s executing function", reporter.uuid) getattr(reporter, func_name)(*args, **kwargs) - # TODO: add logging messages. - def main(args): """ diff --git a/tass-base/src/tass/base/core/tass_files.py b/tass-base/src/tass/base/core/tass_files.py index 023cc87..f49761c 100644 --- a/tass-base/src/tass/base/core/tass_files.py +++ b/tass-base/src/tass/base/core/tass_files.py @@ -4,27 +4,27 @@ from ..log.logging import getLogger -class TassSuite(TassFile): - - def collect(self): - # TODO: Collect all test cases as TassItems and yield - pass - - -class TassRun(TassFile): +class TassJob(TassFile): logger = getLogger(__name__) - def __init__(self, path, test_cases, - test_suites, action_managers, + def __init__(self, path, + _meta=None, **kwargs): super().__init__(path, **kwargs) - self._managers = action_managers - self._raw_test_cases = test_cases - self._raw_test_suites = test_suites self._start_time = 'not started' self._completed_cases = [] + self._test_cases = [] self._has_error = False + if _meta: + _meta.setdefault("results-path", "./results") + _meta.setdefault("pages-path", "./pages") + else: + _meta = { + "results-path": "./results", + "pages-path": "./pages" + } + self._meta = _meta def __str__(self): str_ = f""" @@ -33,6 +33,14 @@ def __str__(self): """ return str_ + def add_test_case(self, case): + if isinstance(dict, case): + _ = TassCase(**case) + elif isinstance(TassCase, case): + _ = case + if _: + self._test_cases.append(_) + @property def start_time(self): return self._start_time @@ -59,7 +67,6 @@ def toJson(self): def collect(self): # TODO: Collect all test cases as TassItems, - # then collect all TestSuites and yield TassItems self._start_time = datetime.now().strftime("%d-%m-%Y--%H_%M_%S") self.logger.debug("Start time (%s): %s", self.uuid, self._start_time) diff --git a/tass-base/src/tass/base/exceptions/assertion_errors.py b/tass-base/src/tass/base/exceptions/assertion_errors.py index 67d0270..f67f5b2 100644 --- a/tass-base/src/tass/base/exceptions/assertion_errors.py +++ b/tass-base/src/tass/base/exceptions/assertion_errors.py @@ -1,4 +1,7 @@ -class TassAssertionError(Exception): +from .tass_errors import TassError + + +class TassAssertionError(TassError): def __init__(self, message, reason, *args): super().__init__() self._message = message diff --git a/tass-base/src/tass/base/exceptions/tass_errors.py b/tass-base/src/tass/base/exceptions/tass_errors.py new file mode 100644 index 0000000..598d115 --- /dev/null +++ b/tass-base/src/tass/base/exceptions/tass_errors.py @@ -0,0 +1,18 @@ +class TassError(Exception): + def __init__(self, message, reason, *args): + super().__init__() + self._message = message + self._reason = reason + self._args = args + + @property + def message(self): + return self._message + + @property + def reason(self): + return self._reason + + @property + def args(self): + return self._args \ No newline at end of file diff --git a/tass-base/src/tass/base/schema/parser.py b/tass-base/src/tass/base/schema/parser.py index dd92231..9e2aed0 100644 --- a/tass-base/src/tass/base/schema/parser.py +++ b/tass-base/src/tass/base/schema/parser.py @@ -1,7 +1,7 @@ from tass.base.log.logging import getLogger from tass.report.registrar import ReporterRegistrar from ..actions.action_manager import get_manager -from ..core.tass_files import TassRun +from ..core.tass_files import TassJob class Parser(): @@ -18,7 +18,7 @@ def __init__(self): def parse(self, path, job): - runs = self._parse_runs(path, job) + runs = self._parse_job(path, job) registrar = self._parse_reporters(job) # TODO: parse should only return runs. @@ -26,57 +26,32 @@ def parse(self, path, job): # See https://github.com/StatCan/tass-ssat/issues/152 return runs, registrar - def _parse_runs(self, path, job): - self.log.info("Parsing runs from job file") - all_runs = job.get('Test_runs') - ready_runs = [] - - for run in all_runs: - self.log.info("Reading run: %s - %s", run['uuid'], run['title']) - run['test_cases'], managers = self._parse_cases(job, run) - self.log.info("Run includes actions for: %s", managers) - for browser in self._parse_browsers(job, run['browsers']): - _managers = {} - self.log.info("Creating run using browser: %s", browser) - for _manager in managers: - if _manager not in _managers: - _managers.update(get_manager(_manager, config=browser)) - _run = TassRun(path, action_managers=_managers, **run) - self.log.info("Run: %s ready to execute.", _run.uuid) - - ready_runs.append(_run) - - return ready_runs - - def _parse_suites(job): - # TODO: Parse Suites. - # https://github.com/StatCan/tass-ssat/issues/151 - pass - - def _parse_cases(self, job, run): - cases = [] - test_cases = job.get('Test_cases', []) - all_steps = job.get('Steps', []) - for case_id in run.get('test_cases', []): - steps = [] - case = next( - filter(lambda _c: _c['uuid'] == case_id, test_cases) - ).copy() - - for step in case.get('steps', []): - _ = next( - filter(lambda _c: _c['uuid'] == step, all_steps) - ).copy() - self.log.debug(">>>>> Reading case: %s", _) - steps.append(_) - - case['steps'] = steps - - managers = set([_m['action'][0].lower() for _m in steps]) - - cases.append(case) - - return cases, managers + def _parse_job(self, path, job): + + meta = job.get("Meta", None) + job_raw = job['Job'] + tassjob = TassJob(**job_raw, _meta=meta) + + for case in self._parse_cases(job["Test_cases"], job): + tassjob.add_test_case(case) + + def _parse_cases(self, cases, job): + for case in cases: + try: + browser = self._parse_browser(case['browser_uuid'], job) + steps = self._parse_steps(case['steps'], job) + except + + def _parse_steps(self, steps, job): + all_steps = job['Steps'] + step_config = [] + for uuid in steps: + found = list(filter(lambda x: uuid == x['uuid'], all_steps)) + if len(found)>1: + self.log.warning("Ambigous step configuration selected. Choosing first match.") + elif len(found) == 0: + self.log.warning("No matching step configuration found. Skipping this case.") + continue def _parse_reporters(self, job): reporters = job['Reporters'] @@ -91,7 +66,12 @@ def _parse_reporters(self, job): return registrar - def _parse_browsers(self, job, browser_list): - browsers = job['Browsers'] - self.log.debug("Using browsers: %s", browser_list) - return filter(lambda b: b['uuid'] in browser_list, browsers) + def _parse_browser(self, browser, job): + self.log.debug("Using browsers: %s", browser) + found = list(filter(lambda b: b['uuid'] in browser, job["browsers"])) + if len(found)>1: + self.log.warning("Ambigous browser configuration selected. Choosing first match.") + elif len(found) == 0: + self.log.warning("No matching browser configuration found. Skipping this browser.") + return None + return found[0] diff --git a/templates/execution-template.json b/templates/execution-template.json index 74cfe49..3cd292e 100644 --- a/templates/execution-template.json +++ b/templates/execution-template.json @@ -8,6 +8,36 @@ "type": "string", "pattern": "^(\\d+\\.)?(\\d+\\.)?(\\*|\\d+)$" }, + "Job": { + "description": "Description of the job. Sets default values for undefined test case values.", + "type": "object", + "properties": { + "title": { + "description": "Name of the job. Defaults to the file name.", + "type": "string" + }, + "uuid": {"$ref": "#/$defs/uuid"}, + "build": { + "description": "Build of the application being tested. Inherited by test cases if not specified.", + "type": "string" + }, + "parent": { + "description": "Description of the parent job if generated from converter.", + "type": "object", + "properties": { + "uuid": {"$ref": "#/$defs/uuid"}, + "title": { + "description": "Title of the parent document.", + "type": "string" + }, + "path": { + "description": "File path of the parent document at the time of conversion.", + "type": "string" + } + } + } + } + }, "Test_cases": { "description": "List of Test cases that may be executed.", "type": "array", @@ -34,21 +64,6 @@ "pages-path": { "type": "string", "description": "File path to read POM files from." - }, - "parent": { - "description": "Description of the parent job if generated from converter.", - "type": "object", - "properties": { - "uuid": {"$ref": "#/$defs/uuid"}, - "title": { - "description": "Title of the parent document.", - "type": "string" - }, - "path": { - "description": "File path of the parent document at the time of conversion.", - "type": "string" - } - } } } } From 3eef7ba4ece0156cf33e05dccd6d1c6e54f3b788 Mon Sep 17 00:00:00 2001 From: Desjardins Date: Tue, 29 Oct 2024 11:51:51 -0400 Subject: [PATCH 5/7] Added some error handling. --- .../tass/base/exceptions/assertion_errors.py | 16 ++------- .../src/tass/base/exceptions/tass_errors.py | 36 ++++++++++++------- tass-base/src/tass/base/schema/parser.py | 30 +++++++++++----- tass-base/src/tass/base/schema/validator.py | 4 +++ 4 files changed, 51 insertions(+), 35 deletions(-) diff --git a/tass-base/src/tass/base/exceptions/assertion_errors.py b/tass-base/src/tass/base/exceptions/assertion_errors.py index f67f5b2..59d3c8d 100644 --- a/tass-base/src/tass/base/exceptions/assertion_errors.py +++ b/tass-base/src/tass/base/exceptions/assertion_errors.py @@ -1,25 +1,15 @@ from .tass_errors import TassError -class TassAssertionError(TassError): +class TassAssertionError(TassException): def __init__(self, message, reason, *args): - super().__init__() - self._message = message - self._reason = reason - self._args = args - - @property - def message(self): - return self._message + super().__init__(message, *args) + self._reason = reason # The error or exception that caused the failure if applicable. @property def reason(self): return self._reason - @property - def args(self): - return self._args - class TassSoftAssertionError(TassAssertionError): def __init__(self, message, reason=None, *args): diff --git a/tass-base/src/tass/base/exceptions/tass_errors.py b/tass-base/src/tass/base/exceptions/tass_errors.py index 598d115..6e36770 100644 --- a/tass-base/src/tass/base/exceptions/tass_errors.py +++ b/tass-base/src/tass/base/exceptions/tass_errors.py @@ -1,18 +1,28 @@ -class TassError(Exception): - def __init__(self, message, reason, *args): - super().__init__() - self._message = message - self._reason = reason - self._args = args +class TassException(Exception): + def __init__(self, message, *args): + super().__init__(message, *args) @property def message(self): - return self._message + return self.args[0].format(*self.args[1:]) + - @property - def reason(self): - return self._reason - @property - def args(self): - return self._args \ No newline at end of file +class TassUUIDEexception(TassError): + def __init__(self, message, *args): + super().__init__(message, *args) + + +class TassUUIDNotFound(TassUUIDEexception): + def __init__(self, uuid): + message = "Given UUID: {} was not found." + super().__init__(message, uuid) + + +class TassAmbiguousUUID(TassUUIDEexception): + def __init__(self, uuid): + message = "Given UUID: {} is ambiguous. Associated value is not unique." + super().__init__(message, uuid) + + + \ No newline at end of file diff --git a/tass-base/src/tass/base/schema/parser.py b/tass-base/src/tass/base/schema/parser.py index 9e2aed0..67a6825 100644 --- a/tass-base/src/tass/base/schema/parser.py +++ b/tass-base/src/tass/base/schema/parser.py @@ -1,3 +1,6 @@ +from copy import deepcopy + +from tass.base.exceptions.tass_errors import TassUUIDException, TassUUIDNotFound, TassAmbiguousUUID from tass.base.log.logging import getLogger from tass.report.registrar import ReporterRegistrar from ..actions.action_manager import get_manager @@ -40,18 +43,23 @@ def _parse_cases(self, cases, job): try: browser = self._parse_browser(case['browser_uuid'], job) steps = self._parse_steps(case['steps'], job) - except + except Tass def _parse_steps(self, steps, job): all_steps = job['Steps'] step_config = [] for uuid in steps: - found = list(filter(lambda x: uuid == x['uuid'], all_steps)) + found = list(filter(lambda step: uuid == step['uuid'], all_steps)) if len(found)>1: - self.log.warning("Ambigous step configuration selected. Choosing first match.") + self.log.warning("Ambiguous step configuration selected. Checking compatibility") + if not all(step == found[0] for step in found[1:]): + self.log.warning("Unable to resolve ambiguous uuids.") + raise TassAmbiguousUUID(uuid) elif len(found) == 0: - self.log.warning("No matching step configuration found. Skipping this case.") - continue + self.log.warning("No matching step configuration found.") + raise TassUUIDNotFound(uuid) + + return deepcopy(found[0]) def _parse_reporters(self, job): reporters = job['Reporters'] @@ -70,8 +78,12 @@ def _parse_browser(self, browser, job): self.log.debug("Using browsers: %s", browser) found = list(filter(lambda b: b['uuid'] in browser, job["browsers"])) if len(found)>1: - self.log.warning("Ambigous browser configuration selected. Choosing first match.") + self.log.warning("Ambiguous browser configuration selected. Attempting to resolve.") + if not all(step == found[0] for step in found[1:]): + self.log.warning("Unable to resolve ambiguous uuids.") + raise TassAmbiguousUUID(uuid) elif len(found) == 0: - self.log.warning("No matching browser configuration found. Skipping this browser.") - return None - return found[0] + self.log.warning("No matching browser configuration found.") + raise TassUUIDNotFound(uuid) + + return deepcopy(found[0]) diff --git a/tass-base/src/tass/base/schema/validator.py b/tass-base/src/tass/base/schema/validator.py index 13e1a1d..240f120 100644 --- a/tass-base/src/tass/base/schema/validator.py +++ b/tass-base/src/tass/base/schema/validator.py @@ -21,5 +21,9 @@ class Tass1Validator(Validator): def __init__(self): super().__init__(schemas.SCHEMA_1_0_0) + def validate(self, job): + super().validate(job) + # TODO: validate using uniqueness rules. + def parser(self): return parser.Tass1Parser() From b193a9b7ce402543a5cec39f56f4eb397d98e889 Mon Sep 17 00:00:00 2001 From: Tyler Desjardins <117662482+ty-desj@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:16:53 -0500 Subject: [PATCH 6/7] Modified input format and parser. Updated core functions to accomodate changes. --- examples/demo/simple_demo_v2.json | 286 ++++++++++++++++++ tass-base/src/tass/base/__main__.py | 53 ++-- .../src/tass/base/actions/action_manager.py | 6 +- .../tass/base/actions/core_action_manager.py | 2 +- .../base/actions/selenium_action_manager.py | 9 +- tass-base/src/tass/base/core/tass_case.py | 3 +- tass-base/src/tass/base/core/tass_files.py | 22 +- .../tass/base/exceptions/assertion_errors.py | 2 +- .../src/tass/base/exceptions/tass_errors.py | 11 +- tass-base/src/tass/base/schema/parser.py | 53 ++-- templates/execution-template.json | 5 +- 11 files changed, 365 insertions(+), 87 deletions(-) create mode 100644 examples/demo/simple_demo_v2.json diff --git a/examples/demo/simple_demo_v2.json b/examples/demo/simple_demo_v2.json new file mode 100644 index 0000000..0156a31 --- /dev/null +++ b/examples/demo/simple_demo_v2.json @@ -0,0 +1,286 @@ +{ + "schema-version": "1.0.0", + "Job": + { + "uuid": "tr100101", + "build": "V2.4", + "title": "Simple Demo v.1", + "parent": "None" + }, + + "Test_cases": + [ + { + "uuid": "uuid-tc-01", + "title": "Tass Sample Case A", + "browser": "chrome001", + "steps": [ + "q1", + "q2", + "q3", + "q4", + "q5", + "q6", + "q7", + "q8" + ] + }, + { + "uuid": "Uuid-tc-02", + "title": "Tass Sample Case B", + "browser": "ffox001", + "steps": [ + "q1", + "q2", + "q3", + "q4", + "q5", + "q1", + "q2", + "q3", + "q4", + "q5", + "q6a" + ] + } + ], + "Steps": [ + { + "uuid": "q1", + "title": "Launch test page", + "action": [ + "selenium", + "load_file" + ], + "parameters": { + "relative_path": "examples/demo-html/page1.html" + } + }, + { + "uuid": "q2", + "title": "Assert page is open", + "action": [ + "selenium", + "assert_page_is_open" + ], + "parameters": { + "page": [ + "sample", + "page1" + ] + } + }, + { + "uuid": "q3", + "title": "Wait for element then type", + "action": [ + "selwait", + "wait_element_visible" + ], + "parameters": { + "locator": { + "by": "id", + "value": "nameField" + }, + "action": [ + "selenium", + "write" + ], + "text": "do a barrel roll" + } + }, + { + "uuid": "q4", + "title": "Click a button", + "action": [ + "selenium", + "click" + ], + "parameters": { + "locator": "btnColor", + "page": [ + "sample", + "page1" + ] + } + }, + { + "uuid": "q5", + "title": "Read CSS", + "action": [ + "selenium", + "read_css" + ], + "parameters": { + "locator": "btnColor", + "page": [ + "sample", + "page1" + ], + "attribute": "background-color" + } + }, + { + "uuid": "q6", + "title": "Assert element is displayed", + "action": [ + "selenium", + "assert_displayed" + ], + "parameters": { + "locator": "btnX", + "page": [ + "sample", + "page1" + ], + "soft": "true" + } + }, + { + "uuid": "q7", + "title": "Assert element is displayed", + "action": [ + "selenium", + "assert_displayed" + ], + "parameters": { + "locator": "btnX", + "page": [ + "sample", + "page1" + ] + } + }, + { + "uuid": "q8", + "title": "Assert element is displayed", + "action": [ + "selenium", + "assert_displayed" + ], + "parameters": { + "locator": "btnX", + "page": [ + "sample", + "page1" + ], + "soft": "true" + } + }, + { + "uuid": "q6a", + "title": "Click a button", + "action": [ + "selenium", + "click" + ], + "parameters": { + "locator": "btnFormat", + "page": [ + "sample", + "page1" + ], + "locator_args": [ + "Color" + ] + } + } + ], + "Browsers": [ + { + "browser_name": "chrome", + "uuid": "chrome001", + "configs": { + "driver": { + "implicit_wait": "5", + "explicit_wait": "10" + }, + "browser": { + "arguments": [ + "--start-maximized" + ], + "preferences": {} + } + } + }, + { + "browser_name": "chrome", + "uuid": "chrome001A", + "configs": { + "driver": { + "implicit_wait": "5", + "explicit_wait": "10" + }, + "browser": { + "arguments": [ + "--window-size=1920,1080" + ], + "preferences": {} + } + } + }, + { + "browser_name": "chrome", + "uuid": "chrome001B", + "configs": { + "driver": { + "implicit_wait": "5", + "explicit_wait": "10" + }, + "browser": { + "arguments": [ + "--window-size=900,600" + ], + "preferences": {} + } + } + }, + { + "browser_name": "chrome", + "uuid": "chrome002", + "configs": { + "driver": { + "implicit_wait": "5", + "explicit_wait": "10" + }, + "browser": { + "arguments": [ + "--headless", + "--start-maximized" + ], + "preferences": {} + } + } + }, + { + "browser_name": "firefox", + "uuid": "ffox001", + "configs": { + "driver": { + "implicit_wait": "15", + "explicit_wait": "30" + }, + "browser": { + "arguments": [ + "--start-maximized" + ], + "preferences": {} + } + } + }, + { + "browser_name": "firefox", + "uuid": "ffox002", + "configs": { + "driver": { + "implicit_wait": "15", + "explicit_wait": "30" + }, + "browser": { + "arguments": ["--headless"], + "preferences": {} + } + } + } + ] +} \ No newline at end of file diff --git a/tass-base/src/tass/base/__main__.py b/tass-base/src/tass/base/__main__.py index 6e7dc90..f5d1189 100644 --- a/tass-base/src/tass/base/__main__.py +++ b/tass-base/src/tass/base/__main__.py @@ -26,14 +26,6 @@ def default(self, obj): ) -def _make_report(registrar, func_name, *args, **kwargs): - if registrar: - log.debug("Running report function: %s", func_name) - for reporter in registrar.iter_reporters(): - log.debug("Reporter: %s executing function", reporter.uuid) - getattr(reporter, func_name)(*args, **kwargs) - - def main(args): """ Starting point for execution of tests. @@ -42,36 +34,31 @@ def main(args): path = Path(args.file).resolve() - runs, registrar = parse(path, args.no_validate) + test = parse(path, args.no_validate) - for test in runs: - log.info("<<<<< Starting Run: %s >>>>>", test.uuid) - _make_report(registrar, "start_report", test) - for case in test.collect(): - log.info("") - log.info("< < < Starting Case: %s > > >", case.uuid) - log.info("") + log.info("<<<<< Starting Run: %s >>>>>", test.uuid) + for case in test.collect(): + log.info("") + log.info("< < < Starting Case: %s > > >", case.uuid) + log.info("") - case.execute_tass() + case.execute_tass() - log.info("") - log.info("> > > Finished Case: %s < < <", case.uuid) - log.info("") - - _make_report(registrar, "report", test) - _make_report(registrar, 'end_report', test) + log.info("") + log.info("> > > Finished Case: %s < < <", case.uuid) + log.info("") Path('results').mkdir(exist_ok=True) - for test in runs: - file_name = test.uuid + '---' + test.start_time + '.json' - result_path = Path().resolve() / "results" / file_name - try: - f = open(result_path, 'w+', encoding='utf-8') - except IOError as e: - log.error("An IOError occured: %s" % e) - return - with f: - json.dump(test, f, indent=4, cls=TassEncoder) + + file_name = test.uuid + '---' + test.start_time + '.json' + result_path = Path().resolve() / "results" / file_name + try: + f = open(result_path, 'w+', encoding='utf-8') + except IOError as e: + log.error("An IOError occured: %s" % e) + return + with f: + json.dump(test, f, indent=4, cls=TassEncoder) if __name__ == '__main__': diff --git a/tass-base/src/tass/base/actions/action_manager.py b/tass-base/src/tass/base/actions/action_manager.py index a9edc05..e78c862 100644 --- a/tass-base/src/tass/base/actions/action_manager.py +++ b/tass-base/src/tass/base/actions/action_manager.py @@ -14,13 +14,13 @@ } -def get_manager(module_name, **kwargs): +def get_manager(module_name, *args, **kwargs): # Try to import the required module log.info("Trying to import %s", module_name) module = _import_module(module_name) - log.debug("Using manager args: %s", kwargs) - manager = module.get_manager(**kwargs) + log.debug("Getting manager: %s", module) + manager = module.get_manager(*args, **kwargs) log.info("Created action manager of type: %s", manager.__class__.__name__) return manager diff --git a/tass-base/src/tass/base/actions/core_action_manager.py b/tass-base/src/tass/base/actions/core_action_manager.py index 18fa877..15198c9 100644 --- a/tass-base/src/tass/base/actions/core_action_manager.py +++ b/tass-base/src/tass/base/actions/core_action_manager.py @@ -2,7 +2,7 @@ from . import core as core -def get_manager(*args, **kwargs): +def get_manager(case, *args, **kwargs): return {'core': CoreActionManager()} diff --git a/tass-base/src/tass/base/actions/selenium_action_manager.py b/tass-base/src/tass/base/actions/selenium_action_manager.py index 44933ab..034f4bc 100644 --- a/tass-base/src/tass/base/actions/selenium_action_manager.py +++ b/tass-base/src/tass/base/actions/selenium_action_manager.py @@ -3,11 +3,14 @@ from . import selenium as sel from . import selenium_wait as selwait +all_managers = {} -def get_manager(config, *args, **kwargs): +def get_manager(browser_config, *args, **kwargs): + if browser_config['uuid'] in all_managers: + return all_managers[browser_config['uuid']] managers = {} manager = { - 'config': config, + 'config': browser_config, 'driver': None } selenium = SeleniumActionManager(manager) @@ -16,6 +19,8 @@ def get_manager(config, *args, **kwargs): managers['selenium'] = selenium managers['selwait'] = waiter + all_managers[browser_config['uuid']] = managers + return managers diff --git a/tass-base/src/tass/base/core/tass_case.py b/tass-base/src/tass/base/core/tass_case.py index 3cce263..7d7ab52 100644 --- a/tass-base/src/tass/base/core/tass_case.py +++ b/tass-base/src/tass/base/core/tass_case.py @@ -96,7 +96,8 @@ def toJson(self): "start_time": self._start_time, "status": self._status, "errors": self._errors, - "steps": self._steps + "steps": self._steps, + "managers": self._managers } def _execute_step(self, step): diff --git a/tass-base/src/tass/base/core/tass_files.py b/tass-base/src/tass/base/core/tass_files.py index f49761c..7d817f4 100644 --- a/tass-base/src/tass/base/core/tass_files.py +++ b/tass-base/src/tass/base/core/tass_files.py @@ -34,9 +34,9 @@ def __str__(self): return str_ def add_test_case(self, case): - if isinstance(dict, case): - _ = TassCase(**case) - elif isinstance(TassCase, case): + if isinstance(case, dict): + _ = TassCase.from_parent(parent=self, **case) + elif isinstance(case, TassCase): _ = case if _: self._test_cases.append(_) @@ -61,21 +61,17 @@ def toJson(self): "name": self.title, "uuid": self.uuid, "test_start": self._start_time, - "test_cases": self._completed_cases, - "action_managers": [v.toJson() for v in self._managers.values()] + "test_cases": [c.toJson() for c in self._completed_cases] } def collect(self): - # TODO: Collect all test cases as TassItems, self._start_time = datetime.now().strftime("%d-%m-%Y--%H_%M_%S") self.logger.debug("Start time (%s): %s", self.uuid, self._start_time) - for case in self._raw_test_cases: - tasscase = TassCase.from_parent(parent=self, - managers=self._managers, **case) + for case in self._test_cases: - self.logger.debug("Collected: %r", tasscase) - yield tasscase + self.logger.debug("Collected: %r", case) + yield case - self._completed_cases.append(tasscase) - self.logger.debug("Added to completed cases: %s", tasscase.uuid) + self._completed_cases.append(case) + self.logger.debug("Added to completed cases: %s", case.uuid) diff --git a/tass-base/src/tass/base/exceptions/assertion_errors.py b/tass-base/src/tass/base/exceptions/assertion_errors.py index 59d3c8d..93efc57 100644 --- a/tass-base/src/tass/base/exceptions/assertion_errors.py +++ b/tass-base/src/tass/base/exceptions/assertion_errors.py @@ -1,4 +1,4 @@ -from .tass_errors import TassError +from .tass_errors import TassException class TassAssertionError(TassException): diff --git a/tass-base/src/tass/base/exceptions/tass_errors.py b/tass-base/src/tass/base/exceptions/tass_errors.py index 6e36770..54bddee 100644 --- a/tass-base/src/tass/base/exceptions/tass_errors.py +++ b/tass-base/src/tass/base/exceptions/tass_errors.py @@ -5,24 +5,23 @@ def __init__(self, message, *args): @property def message(self): return self.args[0].format(*self.args[1:]) - -class TassUUIDEexception(TassError): + +class TassUUIDException(TassException): def __init__(self, message, *args): super().__init__(message, *args) -class TassUUIDNotFound(TassUUIDEexception): +class TassUUIDNotFound(TassUUIDException): def __init__(self, uuid): message = "Given UUID: {} was not found." super().__init__(message, uuid) -class TassAmbiguousUUID(TassUUIDEexception): +class TassAmbiguousUUID(TassUUIDException): def __init__(self, uuid): message = "Given UUID: {} is ambiguous. Associated value is not unique." super().__init__(message, uuid) - - \ No newline at end of file + diff --git a/tass-base/src/tass/base/schema/parser.py b/tass-base/src/tass/base/schema/parser.py index 67a6825..c94e61a 100644 --- a/tass-base/src/tass/base/schema/parser.py +++ b/tass-base/src/tass/base/schema/parser.py @@ -5,6 +5,7 @@ from tass.report.registrar import ReporterRegistrar from ..actions.action_manager import get_manager from ..core.tass_files import TassJob +from ..core.tass_case import TassCase class Parser(): @@ -22,28 +23,44 @@ def __init__(self): def parse(self, path, job): runs = self._parse_job(path, job) - registrar = self._parse_reporters(job) # TODO: parse should only return runs. # Requires refactor of reporter implementation. # See https://github.com/StatCan/tass-ssat/issues/152 - return runs, registrar + return runs def _parse_job(self, path, job): meta = job.get("Meta", None) job_raw = job['Job'] - tassjob = TassJob(**job_raw, _meta=meta) + tassjob = TassJob(path, _meta=meta, **job_raw) for case in self._parse_cases(job["Test_cases"], job): tassjob.add_test_case(case) - def _parse_cases(self, cases, job): + return tassjob + + def _parse_cases(self, cases, jobfile, ): for case in cases: try: - browser = self._parse_browser(case['browser_uuid'], job) - steps = self._parse_steps(case['steps'], job) - except Tass + if 'browser' in case: + self.log.debug("Trying to load browser configurations.") + _browser = self._parse_browser(case['browser'], jobfile) + + steps = self._parse_steps(case['steps'], jobfile) + m = set([x['action'][0] for x in steps]) + + case['steps'] = steps + case['managers'] = {} + for manager in m: + case['managers'].update(get_manager(manager, browser_config=_browser)) + + + except TassUUIDException as e: + self.log.warning(e) + continue + + yield case def _parse_steps(self, steps, job): all_steps = job['Steps'] @@ -59,31 +76,19 @@ def _parse_steps(self, steps, job): self.log.warning("No matching step configuration found.") raise TassUUIDNotFound(uuid) - return deepcopy(found[0]) - - def _parse_reporters(self, job): - reporters = job['Reporters'] - if not reporters: - return None - registrar = ReporterRegistrar() - - for reporter in reporters: - self.log.debug("Registering reporter: %s -- type: %s", - reporter['uuid'], reporter['type']) - registrar.register_reporter(**reporter) - - return registrar + step_config.append(deepcopy(found[0])) + return step_config def _parse_browser(self, browser, job): self.log.debug("Using browsers: %s", browser) - found = list(filter(lambda b: b['uuid'] in browser, job["browsers"])) + found = list(filter(lambda b: b['uuid'] in browser, job["Browsers"])) if len(found)>1: self.log.warning("Ambiguous browser configuration selected. Attempting to resolve.") if not all(step == found[0] for step in found[1:]): self.log.warning("Unable to resolve ambiguous uuids.") - raise TassAmbiguousUUID(uuid) + raise TassAmbiguousUUID(browser) elif len(found) == 0: self.log.warning("No matching browser configuration found.") - raise TassUUIDNotFound(uuid) + raise TassUUIDNotFound(browser) return deepcopy(found[0]) diff --git a/templates/execution-template.json b/templates/execution-template.json index 3cd292e..9b62972 100644 --- a/templates/execution-template.json +++ b/templates/execution-template.json @@ -23,7 +23,7 @@ }, "parent": { "description": "Description of the parent job if generated from converter.", - "type": "object", + "type": ["object", "string"], "properties": { "uuid": {"$ref": "#/$defs/uuid"}, "title": { @@ -95,8 +95,7 @@ "description": "Version of the application being tested." }, "steps": {"$ref": "#/$defs/uuid-list"}, - "browser_uuid": {"$ref": "#/$defs/uuid"}, - "parent_uuid": {"$ref": "#/$defs/uuid"} + "browser": {"$ref": "#/$defs/uuid"} }, "required": ["uuid", "title", "steps"] }, From 88855449f5602b95c1e9ff660e87f6a76f75fc78 Mon Sep 17 00:00:00 2001 From: Desjardins Date: Mon, 18 Nov 2024 13:19:35 -0500 Subject: [PATCH 7/7] Added to the templates and parsing tools. --- examples/demo/simple_demo_v2.json | 27 ++++++- tass-base/src/tass/base/schema/parser.py | 64 +++++++++++++--- templates/execution-template.json | 36 ++++++++- templates/testrail-report-template.json | 93 ++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 17 deletions(-) create mode 100644 templates/testrail-report-template.json diff --git a/examples/demo/simple_demo_v2.json b/examples/demo/simple_demo_v2.json index 0156a31..9983fb1 100644 --- a/examples/demo/simple_demo_v2.json +++ b/examples/demo/simple_demo_v2.json @@ -7,13 +7,33 @@ "title": "Simple Demo v.1", "parent": "None" }, - - "Test_cases": + "Tests": [ + { + "uuid": "tr100101--uuid-tc-01--chrome001", + "case": "uuid-tc-01", + "configurations": [ + { + "type": "browser", + "uuid": "chrome001" + } + ] + }, + { + "uuid": "tr100101--uuid-tc-02--ffox002", + "case": "uuid-tc-02", + "configurations": [ + { + "type": "browser", + "uuid": "ffox002" + } + ] + } + ], + "Cases": [ { "uuid": "uuid-tc-01", "title": "Tass Sample Case A", - "browser": "chrome001", "steps": [ "q1", "q2", @@ -28,7 +48,6 @@ { "uuid": "Uuid-tc-02", "title": "Tass Sample Case B", - "browser": "ffox001", "steps": [ "q1", "q2", diff --git a/tass-base/src/tass/base/schema/parser.py b/tass-base/src/tass/base/schema/parser.py index c94e61a..ee82802 100644 --- a/tass-base/src/tass/base/schema/parser.py +++ b/tass-base/src/tass/base/schema/parser.py @@ -1,11 +1,8 @@ from copy import deepcopy - from tass.base.exceptions.tass_errors import TassUUIDException, TassUUIDNotFound, TassAmbiguousUUID from tass.base.log.logging import getLogger -from tass.report.registrar import ReporterRegistrar from ..actions.action_manager import get_manager from ..core.tass_files import TassJob -from ..core.tass_case import TassCase class Parser(): @@ -35,19 +32,68 @@ def _parse_job(self, path, job): job_raw = job['Job'] tassjob = TassJob(path, _meta=meta, **job_raw) - for case in self._parse_cases(job["Test_cases"], job): - tassjob.add_test_case(case) + for test in self._parse_tests(job["Test"], job): + tassjob.add_test_case(test) return tassjob - - def _parse_cases(self, cases, jobfile, ): + + def _parse_tests(self, tests, job): + for test in tests: + _out = {} + _out['uuid'] = test['uuid'] + _out.update(self._parse_case(test['case'], job)) + _out.update(self._parse_configurations(test['configurations'])) + pass + + def _parse_case(self, uuid, job): + found = list(filter(lambda c: uuid == c['uuid'], job['Cases'])) + if len(found)>1: + self.log.warning("Ambiguous case selected. Checking compatibility") + if not all(c == found[0] for c in found[1:]): + self.log.warning("Unable to resolve ambiguous uuids.") + raise TassAmbiguousUUID(uuid) + else: + self.log.warning("Ambigous case conflict resolved.") + elif len(found) == 0: + self.log.warning("No matching case found.") + raise TassUUIDNotFound(uuid) + + _case = deepcopy(found[0]) + _case['steps'] = self._parse_steps(found[0]['steps'], job) + + _case.update(self._parse_configurations()) + + m = set([step['action'][0] for step in _case['steps']]) + + + + def _parse_configurations(self, config, job): + configuration = {} + def browser(uuid): + _browser = self._parse_browser(uuid, job) + configuration['browser'] = _browser + + registered_configuration_parsers = { + "browser": browser + } + + for conf in config: + k, v = conf['type'], conf['uuid'] + if k in registered_configuration_parsers: + parser = registered_configuration_parsers[k] + parser(v) + + return configuration + + + def _parse_cases(self, cases, job): for case in cases: try: if 'browser' in case: self.log.debug("Trying to load browser configurations.") - _browser = self._parse_browser(case['browser'], jobfile) + _browser = self._parse_browser(case['browser'], job) - steps = self._parse_steps(case['steps'], jobfile) + steps = self._parse_steps(case['steps'], job) m = set([x['action'][0] for x in steps]) case['steps'] = steps diff --git a/templates/execution-template.json b/templates/execution-template.json index 9b62972..f973fd4 100644 --- a/templates/execution-template.json +++ b/templates/execution-template.json @@ -38,10 +38,15 @@ } } }, - "Test_cases": { + "Tests": { + "description": "List of Test cases with configurations that may be executed.", + "type": "array", + "items": {"$ref": "#/$defs/test"} + }, + "Cases": { "description": "List of Test cases that may be executed.", "type": "array", - "items": {"$ref": "#/$defs/test-case"} + "items": {"$ref": "#/$defs/case"} }, "Steps": { "description": "List of steps, singular actions, that can be executed in a test case.", @@ -81,7 +86,31 @@ }, "description": "List of uuids" }, - "test-case": { + "test": { + "type": "object", + "description": "Test to be executed. Includes which case and any other configuration settings.", + "properties": { + "uuid": { + "$ref": "#/$defs/uuid", + "description": "The uuid of this test. Typically a compounded uuid of the job, case, and configuration uuids." + }, + "case": { + "$ref": "#/$defs/uuid", + "description": "The uuid of the case that will be executed." + }, + "configuration": { + "type": "object", + "description": "Contains the various configurations for running a case.", + "properties": { + "browser": { + "ref": "#/$defs/uuid", + "description": "The uuid of the browser used in this configuration." + } + } + } + } + }, + "case": { "type": "object", "description": "Describes a singular test case. Test cases should be independant of one another.", "properties": { @@ -95,7 +124,6 @@ "description": "Version of the application being tested." }, "steps": {"$ref": "#/$defs/uuid-list"}, - "browser": {"$ref": "#/$defs/uuid"} }, "required": ["uuid", "title", "steps"] }, diff --git a/templates/testrail-report-template.json b/templates/testrail-report-template.json new file mode 100644 index 0000000..40e38a7 --- /dev/null +++ b/templates/testrail-report-template.json @@ -0,0 +1,93 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Testrail Report Template", + "description": "Describes how to submit a testrail report.", + "type": "object", + "properties": { + "project_id": { + "type": "string", + "pattern": "\\A\\d+$\\Z", + "description": "The TestRail project id to push reports to." + }, + "Cases": { + "type": "array", + "items": {"$ref": "#/$defs/test-case"} + }, + "Runs": { + "type": "array", + "items": {"$ref": "#/$defs/test-run"} + }, + "Plans": { + "type": "array", + "items": {"$ref": "#/$defs/test-plan"} + } + + }, + "$defs": { + "test-case":{ + "description": "Mappings for a TASS case to convert to a Testrail case", + "type": "object", + "properties": { + "tuuid": { + "description": "TASS uuid of the specific case.", + "type": "string" + }, + "id": { + "description": "The case ID of the case in Testrail.", + "type": "string", + "pattern": "\\A\\d+$\\Z" + } + } + }, + "test-run": { + "description": "Mappings and description to prepare a TestRail run.", + "type": "object", + "properties": { + "cases": { + "description": "Cases should be either 'true' if all cases should be included, or an array containing a list of cases to include using the TASS uuid. Default is to include all cases.", + "type": ["array", "boolean"], + "items": { + "type": "string", + "pattern": "\\A\\d+$\\Z" + }, + "uniqueItems": true + }, + "name": { + "type": "string", + "description": "The name for the run created in Testrail. Defaults to the name of the Tass Execution file." + }, + "description": { + "type": "string", + "description": "Description for the Testrail run. Defaults to use TASS details." + }, + "suite_id": { + "type": "string", + "description": "The suite ID of the Testrail case if applicable", + "pattern": "\\A\\d+$\\Z" + } + } + }, + "test-plan": { + "description": "Mappings and description to prepare a TestRail plan.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name for the run created in Testrail. Defaults to the name of the Tass Execution file." + }, + "description": { + "type": "string", + "description": "Description for the Testrail run. Defaults to use TASS details." + }, + "entries": { + "description": "List of runs as decribed above to include in the plan.", + "type": "array", + "items": { + "type": "string", + "pattern": "\\A\\d+$\\Z" + } + } + } + } + } +} \ No newline at end of file