diff --git a/frappe/commands/testing.py b/frappe/commands/testing.py index 9e3f8ef29e..481f259d76 100644 --- a/frappe/commands/testing.py +++ b/frappe/commands/testing.py @@ -1,7 +1,11 @@ +import importlib import os import subprocess import sys import time +import unittest +from collections import deque +from pathlib import Path from typing import TYPE_CHECKING import click @@ -37,6 +41,7 @@ def main( """Main function to run tests""" if light: from frappe.testing import TestConfig + from frappe.testing.config import TestParameters from frappe.testing.environment import _disable_scheduler_if_needed test_config = TestConfig( @@ -48,6 +53,23 @@ def main( selected_categories=selected_categories or [], skip_before_tests=skip_before_tests, ) + + test_params = TestParameters( + site=site, + app=app, + module=module, + doctype=doctype, + module_def=module_def, + verbose=verbose, + tests=tests, + force=force, + profile=profile, + junit_xml_output=junit_xml_output, + doctype_list_path=doctype_list_path, + failfast=failfast, + case=case, + ) + # init environment frappe.init(site) if not frappe.db: @@ -57,6 +79,57 @@ def main( _disable_scheduler_if_needed() frappe.clear_cache() + # class LightTestRunner(unittest.TextTestRunner): + # def run(test): + # print(f"calling super no {test}") + # super().run(test) + # lightrunner = LightTestRunner() + + # Methods of this loader should be frappe specific + # it should respect parameters passed from command line + class FrappeTestLoader(unittest.TestLoader): + def __init__(self, params: TestParameters): + super(unittest.TestLoader, self).__init__() + self.params = params + + def discover( + self, + start_dir: str | None = None, + pattern: str | None = None, + top_level_dir: str | None = None, + ) -> unittest.TestSuite: + testsuite = unittest.TestSuite() + self.files = [] + apps = self.params.app or frappe.get_installed_apps() + for app in apps: + app_path = Path(frappe.get_app_path(app)) + for test_file in app_path.glob("**/test_*.py"): + module_name = f"{'.'.join(test_file.relative_to(app_path.parent).parent.parts)}.{test_file.stem}" + module = importlib.import_module(module_name) + self.files.append((test_file, module)) + + for file, module in self.files: + _s = unittest.defaultTestLoader.loadTestsFromModule(module) + suites_to_process = deque([_s]) + while suites_to_process: + suite = suites_to_process.popleft() + for elem in suite: + if elem.countTestCases(): + if isinstance(elem, unittest.TestSuite): + suites_to_process.append(elem) + elif isinstance(elem, unittest.TestCase): + if self.params.tests: + if elem._testMethodName in self.params.tests: + testsuite.addTest(elem) + else: + testsuite.addTest(elem) + + return testsuite + + suite = FrappeTestLoader(test_params).discover() + for x in suite: + print(x) + else: import logging diff --git a/frappe/testing/config.py b/frappe/testing/config.py index b44d165f12..eb6d47450a 100644 --- a/frappe/testing/config.py +++ b/frappe/testing/config.py @@ -12,3 +12,22 @@ class TestConfig: pdb_on_exceptions: tuple | None = None selected_categories: list[str] = field(default_factory=list) skip_before_tests: bool = False + + +@dataclass +class TestParameters: + """Configuration class for test runner""" + + site: str | None = None + app: str | None = None + module: str | None = None + doctype: str | None = None + module_def: str | None = None + verbose: bool = False + tests: tuple = () + force: bool = False + profile: bool = False + junit_xml_output: str | None = None + doctype_list_path: str | None = None + failfast: bool = False + case: str | None = None