diff --git a/python_files/tests/unittestadapter/django_test_execution_script.py b/python_files/tests/unittestadapter/django_test_execution_script.py index 21dd945224ea..4893b45147a9 100644 --- a/python_files/tests/unittestadapter/django_test_execution_script.py +++ b/python_files/tests/unittestadapter/django_test_execution_script.py @@ -12,6 +12,12 @@ if __name__ == "__main__": args = sys.argv[1:] manage_py_path = args[0] - test_ids = args[1:] - # currently doesn't support additional args past test_ids. - django_execution_runner(manage_py_path, test_ids, []) + remaining = args[1:] + if "--" in remaining: + separator_index = remaining.index("--") + extra_args = remaining[:separator_index] + test_ids = remaining[separator_index + 1 :] + else: + extra_args = [] + test_ids = remaining + django_execution_runner(manage_py_path, test_ids, extra_args) diff --git a/python_files/tests/unittestadapter/test_discovery.py b/python_files/tests/unittestadapter/test_discovery.py index 5ef5c8aec754..886ddf89469a 100644 --- a/python_files/tests/unittestadapter/test_discovery.py +++ b/python_files/tests/unittestadapter/test_discovery.py @@ -328,6 +328,50 @@ def test_simple_django_collect(): ) +def test_django_collect_with_default_unittest_args(): + """Django discovery should succeed when passed the default unittest discovery + arguments (-v, -s, -p) that the extension sends when no custom unittestArgs are + configured. See https://github.com/microsoft/vscode-python/issues/24991. + """ + test_data_path: pathlib.Path = pathlib.Path(__file__).parent / ".data" + python_files_path: pathlib.Path = pathlib.Path(__file__).parent.parent.parent + discovery_script_path: str = os.fsdecode(python_files_path / "unittestadapter" / "discovery.py") + data_path: pathlib.Path = test_data_path / "simple_django" + manage_py_path: str = os.fsdecode(pathlib.Path(data_path, "manage.py")) + + actual = helpers.runner_with_cwd_env( + [ + discovery_script_path, + "--udiscovery", + "-v", + "-s", + ".", + "-p", + "test*.py", + ], + data_path, + {"MANAGE_PY_PATH": manage_py_path}, + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list is not None + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + assert actual_item.get("cwd") == os.fspath(data_path) + assert len(actual_item["tests"]["children"]) == 1 + assert actual_item["tests"]["children"][0]["children"][0]["id_"] == os.fsdecode( + pathlib.PurePath(test_data_path, "simple_django", "polls", "tests.py") + ) + assert ( + len(actual_item["tests"]["children"][0]["children"][0]["children"][0]["children"]) == 3 + ) + + def test_project_root_path_with_cwd_override() -> None: """Test unittest discovery with project_root_path parameter. diff --git a/python_files/tests/unittestadapter/test_execution.py b/python_files/tests/unittestadapter/test_execution.py index cab03f0b5dc4..46c2dca1dcb4 100644 --- a/python_files/tests/unittestadapter/test_execution.py +++ b/python_files/tests/unittestadapter/test_execution.py @@ -343,6 +343,45 @@ def test_basic_run_django(): assert id_result["outcome"] == "success" +def test_django_run_with_default_unittest_args(): + """Django test execution should succeed when passed the default unittest discovery + arguments (-v, -s, -p) that the extension sends when no custom unittestArgs are + configured. See https://github.com/microsoft/vscode-python/issues/24991. + """ + data_path: pathlib.Path = TEST_DATA_PATH / "simple_django" + manage_py_path: str = os.fsdecode(data_path / "manage.py") + execution_script: pathlib.Path = ( + pathlib.Path(__file__).parent / "django_test_execution_script.py" + ) + + test_ids = [ + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question", + ] + script_str = os.fsdecode(execution_script) + actual = helpers.runner_with_cwd_env( + [ + script_str, + manage_py_path, + "-v", + "-s", + ".", + "-p", + "test*.py", + "--", + *test_ids, + ], + data_path, + {"MANAGE_PY_PATH": manage_py_path}, + ) + assert actual + actual_list: List[Dict[str, Dict[str, Any]]] = actual + assert len(actual_list) == 1 + actual_item = actual_list[0] + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("cwd") == os.fspath(data_path) + assert actual_item["result"][test_ids[0]]["outcome"] == "success" + + def test_project_root_path_with_cwd_override(mock_send_run_data) -> None: # noqa: ARG001 """Test unittest execution with project_root_path parameter. diff --git a/python_files/unittestadapter/django_handler.py b/python_files/unittestadapter/django_handler.py index 574aee7af7fa..6701f271835e 100644 --- a/python_files/unittestadapter/django_handler.py +++ b/python_files/unittestadapter/django_handler.py @@ -28,6 +28,43 @@ def override_argv(argv: List[str]) -> Generator: sys.argv = original_argv +_START_DIRECTORY_FLAGS = ("-s", "--start-directory") +_VERBOSE_FLAGS = ("-v", "--verbose") +_QUIET_FLAGS = ("-q", "--quiet") + + +def _to_manage_py_args(args: List[str]) -> List[str]: + """Adapt unittest discovery/execution args for 'manage.py test'. + + Most unittest discovery flags (-p/--pattern, -t/--top-level-directory, + -f/--failfast, ...) are understood by Django's 'manage.py test' the same + way, so they -- and any Django-specific options a user adds to + unittestArgs, e.g. --keepdb, --settings -- are passed through unchanged. + Only two flags need adapting: + - -s/--start-directory has no Django equivalent, since Django discovers + tests across installed apps rather than a start directory, so it (and + its value) is dropped. + - -v/--verbose and -q/--quiet are value-less unittest flags, but Django's + -v/--verbosity requires an explicit value, so they're translated to an + explicit "-v 2"/"-v 0". + """ + manage_py_args: List[str] = [] + skip_next = False + for arg in args: + if skip_next: + skip_next = False + continue + if arg in _START_DIRECTORY_FLAGS: + skip_next = True + elif arg in _VERBOSE_FLAGS: + manage_py_args.extend(["-v", "2"]) + elif arg in _QUIET_FLAGS: + manage_py_args.extend(["-v", "0"]) + else: + manage_py_args.append(arg) + return manage_py_args + + def django_discovery_runner(manage_py_path: str, args: List[str]) -> None: # Attempt a small amount of validation on the manage.py path. if not pathlib.Path(manage_py_path).exists(): @@ -50,7 +87,7 @@ def django_discovery_runner(manage_py_path: str, args: List[str]) -> None: "test", "--testrunner=django_test_runner.CustomDiscoveryTestRunner", ] - command.extend(args) + command.extend(_to_manage_py_args(args)) print("Running Django tests with command:", command) subprocess_discovery = subprocess.run( @@ -93,7 +130,7 @@ def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List str(manage_path), "test", "--testrunner=django_test_runner.CustomExecutionTestRunner", - *args, + *_to_manage_py_args(args), *test_ids, ] print(f"Django manage.py arguments: {manage_argv}")