From 3e26cd1e43030206bf532b3f867526bfe4b673d9 Mon Sep 17 00:00:00 2001 From: Mahdi Firouzjah <44068054+mh-firouzjah@users.noreply.github.com> Date: Thu, 22 Jun 2023 05:27:48 +0000 Subject: [PATCH 1/2] mh-firouzjah-patch: Fixed Django tests discovery By invoking newly added function `setup_django_test_env` it will try to set up django environment by setting `DJANGO_SETTINGS_MODULE` environment variable and invoking `django.setup` method, if it succeeds django tests will be discovered as other tests currently are, otherwise it fails silently. --- .../testing_tools/unittest_discovery.py | 16 ++++++++ pythonFiles/unittestadapter/discovery.py | 10 ++++- pythonFiles/unittestadapter/execution.py | 5 ++- pythonFiles/unittestadapter/utils.py | 39 +++++++++++++++++++ pythonFiles/visualstudio_py_testlauncher.py | 8 ++++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/pythonFiles/testing_tools/unittest_discovery.py b/pythonFiles/testing_tools/unittest_discovery.py index 2988092c387c..55f6e3b7692c 100644 --- a/pythonFiles/testing_tools/unittest_discovery.py +++ b/pythonFiles/testing_tools/unittest_discovery.py @@ -4,11 +4,27 @@ import traceback import unittest +import os.path + +sys.path.insert( + 1, + os.path.dirname( # pythonFiles + os.path.dirname( # pythonFiles/testing_tools + os.path.abspath(__file__) # this file + ) + ), +) + +from unittestadapter.utils import setup_django_test_env + start_dir = sys.argv[1] pattern = sys.argv[2] top_level_dir = sys.argv[3] if len(sys.argv) >= 4 else None sys.path.insert(0, os.getcwd()) +# Setup django env to prevent missing django tests +setup_django_test_env(start_dir) + def get_sourceline(obj): try: diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index bcc2fd967f78..00657d664934 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -23,7 +23,12 @@ from testing_tools import socket_manager # If I use from utils then there will be an import error in test_discovery.py. -from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args +from unittestadapter.utils import ( + TestNode, + build_test_tree, + parse_unittest_args, + setup_django_test_env, +) # Add the lib path to sys.path to find the typing_extensions module. sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) @@ -121,6 +126,9 @@ def discover_tests( start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) + # Setup django env to prevent missing django tests + setup_django_test_env(start_dir) + # Perform test discovery. port, uuid = parse_discovery_cli_args(argv[:index]) payload = discover_tests(start_dir, pattern, top_level_dir, uuid) diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 17c125e5843a..e10499a7e540 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -25,7 +25,7 @@ sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) from testing_tools import socket_manager from typing_extensions import NotRequired, TypeAlias, TypedDict -from unittestadapter.utils import parse_unittest_args +from unittestadapter.utils import parse_unittest_args, setup_django_test_env DEFAULT_PORT = "45454" @@ -232,6 +232,9 @@ def run_tests( start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) + # Setup django env to prevent missing django tests + setup_django_test_env(start_dir) + run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT") run_test_ids_port_int = ( int(run_test_ids_port) if run_test_ids_port is not None else 0 diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index 9c8b896a8d6e..7e263888b892 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -233,3 +233,42 @@ def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]: parsed_args.pattern, parsed_args.top_level_directory, ) + + +def setup_django_test_env(root): + """Configure Django environment for running Django tests. + + It checks if Django is installed by attempting to import the `django` module. + Looks for `manage.py` file to extract the value of `DJANGO_SETTINGS_MODULE`. + Sets `DJANGO_SETTINGS_MODULE` environment variable and initializes Django setup. + If couldn't find the file or import django during this process, the function fails silently. + + Args: + root (str): The root directory of the Django project. + + Returns: + None + """ + import os, re + + try: + # Check if Django is installed + import django + + # Check if manage.py exists + with open(os.path.join(root, "manage.py"), "r") as manage_py: + # Look for a line that sets the DJANGO_SETTINGS_MODULE environment variable + pattern = r"^os\.environ\.setdefault\((\'|\")DJANGO_SETTINGS_MODULE(\'|\"), (\'|\")(?P[\w.]+)(\'|\")\)$" + for line in manage_py.readlines(): + pattern_matched = re.match(pattern, line.strip()) + if pattern_matched is not None: + # Extract value for DJANGO_SETTINGS_MODULE + settings_path = str( + pattern_matched.groupdict().get("settings_path", "") + ) + # Set the DJANGO_SETTINGS_MODULE environment variable and initialize Django's settings + os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_path) + django.setup() + return + except (ModuleNotFoundError, FileNotFoundError): + return diff --git a/pythonFiles/visualstudio_py_testlauncher.py b/pythonFiles/visualstudio_py_testlauncher.py index 0b0ef3242f65..62170fa6bd1d 100644 --- a/pythonFiles/visualstudio_py_testlauncher.py +++ b/pythonFiles/visualstudio_py_testlauncher.py @@ -25,6 +25,10 @@ import traceback import unittest +sys.path.insert(1, os.path.abspath(__file__)) # this file + +from unittestadapter.utils import setup_django_test_env + try: import thread except: @@ -327,11 +331,15 @@ def main(): opts.us = "." if opts.up is None: opts.up = "test*.py" + # Setup django env to prevent missing django tests + setup_django_test_env(opts.us) tests = unittest.defaultTestLoader.discover(opts.us, opts.up) else: # loadTestsFromNames doesn't work well (with duplicate file names or class names) # Easier approach is find the test suite and use that for running loader = unittest.TestLoader() + # Setup django env to prevent missing django tests + setup_django_test_env(opts.us) # opts.us will be passed in suites = loader.discover( opts.us, pattern=os.path.basename(opts.testFile), top_level_dir=opts.ut From 8be42eb5cc7f8c2306a029761c6a2b723e9c9aa6 Mon Sep 17 00:00:00 2001 From: mh-firouzjah Date: Wed, 26 Jul 2023 01:31:04 +0330 Subject: [PATCH 2/2] mh-firouzjah-patch: Updated django tests support - Extract the core logic to a new module - Make sure current work space exists in sys.path --- .../testing_tools/unittest_discovery.py | 2 +- pythonFiles/unittestadapter/discovery.py | 8 +-- .../unittestadapter/django_test_init.py | 55 +++++++++++++++++++ pythonFiles/unittestadapter/execution.py | 3 +- pythonFiles/unittestadapter/utils.py | 39 ------------- pythonFiles/visualstudio_py_testlauncher.py | 9 ++- 6 files changed, 64 insertions(+), 52 deletions(-) create mode 100644 pythonFiles/unittestadapter/django_test_init.py diff --git a/pythonFiles/testing_tools/unittest_discovery.py b/pythonFiles/testing_tools/unittest_discovery.py index 55f6e3b7692c..bebcf47cffd3 100644 --- a/pythonFiles/testing_tools/unittest_discovery.py +++ b/pythonFiles/testing_tools/unittest_discovery.py @@ -15,7 +15,7 @@ ), ) -from unittestadapter.utils import setup_django_test_env +from unittestadapter.django_test_init import setup_django_test_env start_dir = sys.argv[1] pattern = sys.argv[2] diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 00657d664934..fbdeabc019b8 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -23,12 +23,8 @@ from testing_tools import socket_manager # If I use from utils then there will be an import error in test_discovery.py. -from unittestadapter.utils import ( - TestNode, - build_test_tree, - parse_unittest_args, - setup_django_test_env, -) +from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args +from unittestadapter.django_test_init import setup_django_test_env # Add the lib path to sys.path to find the typing_extensions module. sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) diff --git a/pythonFiles/unittestadapter/django_test_init.py b/pythonFiles/unittestadapter/django_test_init.py new file mode 100644 index 000000000000..a7bc834f123c --- /dev/null +++ b/pythonFiles/unittestadapter/django_test_init.py @@ -0,0 +1,55 @@ +""" +This module sets up a Django environment to run Django tests. +""" + +import os +import re +import sys + + +def setup_django_test_env(workspace_directory="."): + """Configures the Django environment for running Django tests. + + If Django is not installed, workspace_directory is not in sys.path or + manage.py can not be found inside the given workspace_directory, the function fails quietly. + + Args: + workspace_directory (str): The current workspace directory that is expected to contain manage.py module + + Returns: + None + """ + + # To avoid false positive ModuleNotFoundError from django.setup() due to missing current workspace in sys.path + sys.path.insert(0, os.getcwd()) + + try: + import django + except ImportError: + return + + manage_py_module = os.path.join(workspace_directory, "manage.py") + if not os.path.exists(manage_py_module): + return + + dj_settings_module = None + + with open(manage_py_module, "r") as manage_py: + pattern = r"^os\.environ\.setdefault\((\'|\")DJANGO_SETTINGS_MODULE(\'|\"), (\'|\")(?P[\w.]+)(\'|\")\)$" + for line in manage_py.readlines(): + match_result = re.match(pattern, line.strip()) + if match_result is not None: + dj_settings_module = match_result.groupdict().get("settings_path", None) + break + + if dj_settings_module is None: + return + + os.environ.setdefault("DJANGO_SETTINGS_MODULE", dj_settings_module) + + try: + django.setup() + except ModuleNotFoundError: + return + + return diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index e10499a7e540..4b5ed176e248 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -25,7 +25,8 @@ sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) from testing_tools import socket_manager from typing_extensions import NotRequired, TypeAlias, TypedDict -from unittestadapter.utils import parse_unittest_args, setup_django_test_env +from unittestadapter.utils import parse_unittest_args +from unittestadapter.django_test_init import setup_django_test_env DEFAULT_PORT = "45454" diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index 7e263888b892..9c8b896a8d6e 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -233,42 +233,3 @@ def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]: parsed_args.pattern, parsed_args.top_level_directory, ) - - -def setup_django_test_env(root): - """Configure Django environment for running Django tests. - - It checks if Django is installed by attempting to import the `django` module. - Looks for `manage.py` file to extract the value of `DJANGO_SETTINGS_MODULE`. - Sets `DJANGO_SETTINGS_MODULE` environment variable and initializes Django setup. - If couldn't find the file or import django during this process, the function fails silently. - - Args: - root (str): The root directory of the Django project. - - Returns: - None - """ - import os, re - - try: - # Check if Django is installed - import django - - # Check if manage.py exists - with open(os.path.join(root, "manage.py"), "r") as manage_py: - # Look for a line that sets the DJANGO_SETTINGS_MODULE environment variable - pattern = r"^os\.environ\.setdefault\((\'|\")DJANGO_SETTINGS_MODULE(\'|\"), (\'|\")(?P[\w.]+)(\'|\")\)$" - for line in manage_py.readlines(): - pattern_matched = re.match(pattern, line.strip()) - if pattern_matched is not None: - # Extract value for DJANGO_SETTINGS_MODULE - settings_path = str( - pattern_matched.groupdict().get("settings_path", "") - ) - # Set the DJANGO_SETTINGS_MODULE environment variable and initialize Django's settings - os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_path) - django.setup() - return - except (ModuleNotFoundError, FileNotFoundError): - return diff --git a/pythonFiles/visualstudio_py_testlauncher.py b/pythonFiles/visualstudio_py_testlauncher.py index 62170fa6bd1d..411a082d1ff8 100644 --- a/pythonFiles/visualstudio_py_testlauncher.py +++ b/pythonFiles/visualstudio_py_testlauncher.py @@ -27,7 +27,7 @@ sys.path.insert(1, os.path.abspath(__file__)) # this file -from unittestadapter.utils import setup_django_test_env +from unittestadapter.django_test_init import setup_django_test_env try: import thread @@ -280,6 +280,9 @@ def main(): ) (opts, _) = parser.parse_args() + # Setup django env to prevent missing django tests + setup_django_test_env(getattr(opts, "us", ".") or ".") + sys.path[0] = os.getcwd() if opts.result_port: try: @@ -331,15 +334,11 @@ def main(): opts.us = "." if opts.up is None: opts.up = "test*.py" - # Setup django env to prevent missing django tests - setup_django_test_env(opts.us) tests = unittest.defaultTestLoader.discover(opts.us, opts.up) else: # loadTestsFromNames doesn't work well (with duplicate file names or class names) # Easier approach is find the test suite and use that for running loader = unittest.TestLoader() - # Setup django env to prevent missing django tests - setup_django_test_env(opts.us) # opts.us will be passed in suites = loader.discover( opts.us, pattern=os.path.basename(opts.testFile), top_level_dir=opts.ut