diff --git a/framework/pytest_plugins/collection_plugin.py b/framework/pytest_plugins/collection_plugin.py index 547b3918..1a807b33 100644 --- a/framework/pytest_plugins/collection_plugin.py +++ b/framework/pytest_plugins/collection_plugin.py @@ -1,54 +1,84 @@ import os +from pathlib import Path +from typing import Any from framework.database.objects.testcase import TestCase class CollectionPlugin: """ - Plugin that allows us to get all tests after running a collect only in pytest + Plugin that allows us to get all tests after running a collect only in pytest. """ - PRIORITIES = ['p0', 'p1', 'p2', 'p3'] + PRIORITIES = ["p0", "p1", "p2", "p3"] - def __init__(self): + def __init__(self, repo_root: str): + """ + Constructor. + + Args: + repo_root (str): The Absolute path to the root of the repo. + """ + self.repo_root: str = repo_root self.tests: [TestCase] = [] - def pytest_report_collectionfinish(self, items): + def _get_full_nodeid(self, test: Any) -> str: """ - Run after collection is finished + Ensures the nodeid is relative to the repository root. + Args: - items (): list of test items + test (Any): The pytest test item. Returns: - + str: Full nodeid relative to repository root. """ + # Get the absolute path of the test file + abs_path = Path(test.path).absolute() + repo_root_path = Path(self.repo_root) + # Make it relative to repo root + rel_path = abs_path.relative_to(repo_root_path).as_posix() + + # Replace the file path portion of nodeid with the repository-relative path + parts = test.nodeid.split("::") + return "::".join([rel_path] + parts[1:]) + + def pytest_report_collectionfinish(self, items: Any): + """ + Run after collection is finished. + + Args: + items (Any): list of Pytest test items. + """ for test in items: markers = list(map(lambda marker: marker.name, test.own_markers)) priority = self.get_testcase_priority(markers) if priority: markers.remove(priority) - testcase = TestCase(test.name, os.path.basename(test.location[0]), priority, test.location[0], test.nodeid) + full_node_id = self._get_full_nodeid(test) + testcase = TestCase(test.name, os.path.basename(test.location[0]), priority, test.location[0], full_node_id) testcase.set_markers(markers) self.tests.append(testcase) - def get_tests(self) -> [TestCase]: + def get_tests(self) -> list[TestCase]: """ - Returns the tests - Returns: + Returns the tests. + Returns: + list[TestCase]: List of test cases collected during pytest collection. """ return self.tests - def get_testcase_priority(self, markers): + def get_testcase_priority(self, markers: Any) -> str: """ - Gets the testcase priority from the list of markers + Gets the testcase priority from the list of markers. + Args: - markers: the markers to find the priority from - - Returns: testcase priority + markers (Any): the pytest markers to find the priority from. + Returns: + str: Testcase priority. """ for mark in markers: if mark in self.PRIORITIES: diff --git a/framework/resources/resource_finder.py b/framework/resources/resource_finder.py index 945a058b..597c7574 100644 --- a/framework/resources/resource_finder.py +++ b/framework/resources/resource_finder.py @@ -4,28 +4,46 @@ import os.path from pathlib import Path +def get_stx_repo_root() -> str: + """ + Find the full path to the repository root. + + Uses the position of the current file relative to the root of the repo. + + Returns: + str: The absolute path to the repository root. + + Example: + >>> get_repo_root() + will return /home/user/repo/starlingx + """ + path_of_this_file = Path(__file__) + + # This file is in framework/resources/resource_finder.py, so go up 3 levels + root_folder = path_of_this_file.parent.parent.parent + return root_folder.as_posix() + + def get_stx_resource_path(relative_path: str) -> str: """ This function will get the full path to the resource from the relative_path provided. + This will allow projects that use StarlingX as a submodule to still find resource files using the relative path. - + Args: - relative_path: The relative path to the resource. - - Returns: The full path to the resource - + relative_path (str): The relative path to the resource. + + Returns: + str: The full path to the resource. + Example: >>> get_resource_path("framework/resources/resource_finder.py") will return /home/user/repo/starlingx/framework/resources/resource_finder.py """ - # check to see if the path really is relative, if not just return the path if os.path.isabs(relative_path): return relative_path - path_of_this_file = Path(__file__) - root_folder_of_stx = path_of_this_file.parent.parent.parent + root_folder_of_stx = get_stx_repo_root() path_to_resource = Path(root_folder_of_stx, relative_path).as_posix() return path_to_resource - - diff --git a/framework/runner/objects/test_capability_matcher.py b/framework/runner/objects/test_capability_matcher.py index fac25716..d09ecbf9 100644 --- a/framework/runner/objects/test_capability_matcher.py +++ b/framework/runner/objects/test_capability_matcher.py @@ -1,34 +1,56 @@ import pytest + from config.lab.objects.lab_config import LabConfig from framework.database.objects.testcase import TestCase from framework.database.operations.lab_capability_operation import LabCapabilityOperation from framework.database.operations.lab_operation import LabOperation from framework.database.operations.run_content_operation import RunContentOperation from framework.pytest_plugins.collection_plugin import CollectionPlugin +from framework.resources.resource_finder import get_stx_repo_root class TestCapabilityMatcher: """ - Class to hold matches for a set of tests given a lab config + Class to hold matches for a set of tests given a lab config. """ - priority_marker_list = ['p0', 'p1', 'p2', 'p3'] + priority_marker_list = ["p0", "p1", "p2", "p3"] def __init__(self, lab_config: LabConfig): + """ + Constructor + + Args: + lab_config (LabConfig): Config for the lab + """ self.lab_config = lab_config - def get_list_of_tests(self, test_case_folder: str) -> []: + def get_list_of_tests(self, test_case_folder: str) -> list[TestCase]: """ - Getter for the list of tests that this lab can run - Returns: the list of tests + Getter for the list of tests that this lab can run. + Args: + test_case_folder (str): Path to the folder containing test cases. + + Returns: + list[TestCase]: List of tests that can be run. """ tests = self._get_all_tests_in_folder(test_case_folder) capabilities = self.lab_config.get_lab_capabilities() return self._filter_tests(tests, capabilities) - def get_list_of_tests_from_db(self, run_id: int) -> []: + def get_list_of_tests_from_db(self, run_id: int) -> list[TestCase]: + """ + This function will return the list of test cases matching the run_id from the database. + + Args: + run_id (int): Run Id + + Returns: + list[TestCase]: + """ + run_content_operation = RunContentOperation() tests = run_content_operation.get_tests_from_run_content(run_id) @@ -40,15 +62,16 @@ class TestCapabilityMatcher: return self._filter_tests(tests, capabilities) - def _filter_tests(self, tests: [TestCase], capabilities: [str]) -> [TestCase]: + def _filter_tests(self, tests: list[TestCase], capabilities: list[str]) -> list[TestCase]: """ - Filters out the tests that can run on the given lab + Filters out the tests that can run on the given lab. + Args: - tests (): the list of tests - capabilities (): the capabilities + tests (list[TestCase]): The list of tests. + capabilities (list[str]): The capabilities. Returns: - + list[TestCase]: List of tests that can run on the lab based on capabilities. """ tests_to_run = [] for test in tests: @@ -57,24 +80,30 @@ class TestCapabilityMatcher: tests_to_run.append(test) return tests_to_run - def _get_all_tests_in_folder(self, test_case_folder: str) -> [TestCase]: + def _get_all_tests_in_folder(self, test_case_folder: str) -> list[TestCase]: """ - Gerts all tests in the testcase folder - Returns: + Gets all tests in the testcase folder. + Args: + test_case_folder (str): Path to the folder containing test cases. + + Returns: + list[TestCase]: List of test cases found in the folder. """ - collection_plugin = CollectionPlugin() + repo_root = get_stx_repo_root() + collection_plugin = CollectionPlugin(repo_root) pytest.main(["--collect-only", test_case_folder], plugins=[collection_plugin]) return collection_plugin.get_tests() - def _get_markers(self, test: TestCase): + def _get_markers(self, test: TestCase) -> list[str]: """ - Gets the markers for the given test + Gets the markers for the given test. + Args: - test (): the test + test (TestCase): The test case to get markers from. Returns: - + list[str]: List of markers associated with the test. """ markers = [] for marker in test.get_markers(): diff --git a/framework/runner/scripts/test_executor.py b/framework/runner/scripts/test_executor.py index b26084f7..8ecc0cb2 100644 --- a/framework/runner/scripts/test_executor.py +++ b/framework/runner/scripts/test_executor.py @@ -1,12 +1,9 @@ -import os from optparse import OptionParser from typing import Optional import pytest -from config.configuration_file_locations_manager import ( - ConfigurationFileLocationsManager, -) +from config.configuration_file_locations_manager import ConfigurationFileLocationsManager from config.configuration_manager import ConfigurationManager from framework.database.objects.testcase import TestCase from framework.database.operations.run_content_operation import RunContentOperation @@ -14,7 +11,6 @@ from framework.database.operations.run_operation import RunOperation from framework.database.operations.test_plan_operation import TestPlanOperation from framework.logging.automation_logger import get_logger from framework.pytest_plugins.result_collector import ResultCollector -from framework.resources.resource_finder import get_stx_resource_path from framework.runner.objects.test_capability_matcher import TestCapabilityMatcher from framework.runner.objects.test_executor_summary import TestExecutorSummary from testcases.conftest import log_configuration @@ -32,16 +28,7 @@ def execute_test(test: TestCase, test_executor_summary: TestExecutorSummary, tes """ result_collector = ResultCollector(test_executor_summary, test, test_case_result_id) pytest_args = ConfigurationManager.get_config_pytest_args() - - node_id = test.get_pytest_node_id().lstrip("/") # Normalize node_id - - # Ensure we do not prepend "testcases/" for unit tests - if node_id.startswith("unit_tests/"): - resolved_path = get_stx_resource_path(node_id) - else: - resolved_path = get_stx_resource_path(os.path.join("testcases", node_id)) - - pytest_args.append(resolved_path) + pytest_args.append(test.get_pytest_node_id()) pytest.main(pytest_args, plugins=[result_collector]) diff --git a/framework/scanning/objects/test_scanner_uploader.py b/framework/scanning/objects/test_scanner_uploader.py index 6d2f7711..e0db6a61 100644 --- a/framework/scanning/objects/test_scanner_uploader.py +++ b/framework/scanning/objects/test_scanner_uploader.py @@ -1,6 +1,7 @@ from typing import List import pytest + from framework.database.objects.testcase import TestCase from framework.database.operations.capability_operation import CapabilityOperation from framework.database.operations.test_capability_operation import TestCapabilityOperation @@ -16,15 +17,17 @@ class TestScannerUploader: def __init__(self, test_folders: List[str]): self.test_folders = test_folders - def scan_and_upload_tests(self): + def scan_and_upload_tests(self, repo_root: str): """ - Scan code base and upload/update tests - Returns: + Scans the repo and uploads the new tests to the database. + + Args: + repo_root (str): The full path to the root of the repo. """ test_info_operation = TestInfoOperation() - scanned_tests = self.scan_for_tests() + scanned_tests = self.scan_for_tests(repo_root) # Filter to find only the test cases in the desired folders. filtered_test_cases = [] @@ -47,22 +50,28 @@ class TestScannerUploader: self.update_pytest_node_id(test, database_testcase) self.update_capability(test, database_testcase.get_test_info_id()) - def scan_for_tests(self) -> [TestCase]: + def scan_for_tests(self, repo_root: str) -> [TestCase]: """ Scan for tests - Returns: list of Testcases + + Args: + repo_root (str): The full path to the root of the repo. + + Returns: + [TestCase]: list of Testcases """ - collection_plugin = CollectionPlugin() + collection_plugin = CollectionPlugin(repo_root) pytest.main(["--collect-only"], plugins=[collection_plugin]) return collection_plugin.get_tests() def update_priority(self, test: TestCase, database_testcase: TestCase): """ Checks the current priority of the test, if it's changed, update it + Args: - test: the Test in the Repo Scan - database_testcase: the Test in the Database + test (TestCase): the Test in the Repo Scan + database_testcase (TestCase): the Test in the Database """ database_priority = database_testcase.get_priority() @@ -74,9 +83,10 @@ class TestScannerUploader: def update_test_path(self, test: TestCase, database_testcase: TestCase): """ Checks the current test_path of the test, if it's changed, update it + Args: - test: the Test in the Repo Scan - database_testcase: the Test in the Database + test (TestCase): the Test in the Repo Scan + database_testcase (TestCase): the Test in the Database """ database_test_path = database_testcase.get_test_path() actual_test_path = test.get_test_path().replace("\\", "/") @@ -88,9 +98,10 @@ class TestScannerUploader: def update_pytest_node_id(self, test: TestCase, database_testcase: TestCase): """ Checks the current pytest_node_id of the test, if it's changed, update it + Args: - test: the Test in the Repo Scan - database_testcase: the Test in the Database + test (TestCase): the Test in the Repo Scan + database_testcase (TestCase): the Test in the Database """ current_pytest_node_id = database_testcase.get_pytest_node_id() if not current_pytest_node_id or current_pytest_node_id is not test.get_pytest_node_id(): @@ -100,9 +111,10 @@ class TestScannerUploader: def update_capability(self, test: TestCase, test_info_id: int): """ Updates the test in the db with any capabilities it has + Args: - test: the test - test_info_id: the id of the test to check. + test (TestCase): the test + test_info_id (int): the id of the test to check. """ capability_operation = CapabilityOperation() capability_test_operation = TestCapabilityOperation() @@ -126,13 +138,13 @@ class TestScannerUploader: self.check_for_capabilities_to_remove(test_info_id, capabilities) - def check_for_capabilities_to_remove(self, test_info_id, capabilities): + def check_for_capabilities_to_remove(self, test_info_id: int, capabilities: [str]): """ - Checks for capabilities in the db that no longer exist on the test - Args: - test_info_id: the test_info_id - capabilities: the capabilities on the test - + Checks for capabilities in the db that no longer exist on the test + Args: + test_info_id (int): the test_info_id + capabilities ([str]): the capabilities on the test + v """ capability_test_operation = TestCapabilityOperation() # next we need to remove capabilities that are in the database but no longer on the test diff --git a/scripts/test_case_scanner.py b/scripts/test_case_scanner.py index 4cecd82c..fefdf90f 100644 --- a/scripts/test_case_scanner.py +++ b/scripts/test_case_scanner.py @@ -2,9 +2,10 @@ from optparse import OptionParser from config.configuration_file_locations_manager import ConfigurationFileLocationsManager from config.configuration_manager import ConfigurationManager +from framework.resources.resource_finder import get_stx_repo_root from framework.scanning.objects.test_scanner_uploader import TestScannerUploader -if __name__ == '__main__': +if __name__ == "__main__": """ This Function will scan the repository for all test cases and update the database. @@ -18,6 +19,7 @@ if __name__ == '__main__': configuration_locations_manager.set_configs_from_options_parser(parser) ConfigurationManager.load_configs(configuration_locations_manager) + repo_root = get_stx_repo_root() folders_to_scan = ["testcases"] test_scanner_uploader = TestScannerUploader(folders_to_scan) - test_scanner_uploader.scan_and_upload_tests() + test_scanner_uploader.scan_and_upload_tests(repo_root)