Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed django tests discovery part #22

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
18 changes: 18 additions & 0 deletions pythonFiles/testing_tools/unittest_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@
top_level_dir = sys.argv[3] if len(sys.argv) >= 4 else None
sys.path.insert(0, os.getcwd())

import os.path

sys.path.insert(
1,
os.path.dirname( # pythonFiles
os.path.dirname( # pythonFiles/testing_tools
os.path.abspath(__file__) # this file
)
),
)

from django_runner import setup_django_env

django_test_enabled = os.environ.get("DJANGO_TEST_ENABLED", "False")
if django_test_enabled.lower() == "true":
print(f"DJANGO TEST DECLEARED = {django_test_enabled}")
django_env_enabled = setup_django_env(start_dir)
print(f"DJANGO ENV ENABLED = {django_env_enabled}")

def get_sourceline(obj):
try:
Expand Down
14 changes: 11 additions & 3 deletions pythonFiles/unittestadapter/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import json
import os
import pathlib
import pathlib # TODO: pathlib added in python v3.4 - this file used to used os.path.dirname | commit/0b6fc5b44c70fed32294b460e3ea45854ae220e4
import sys
import traceback
import unittest
Expand All @@ -19,6 +19,8 @@
# 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.django_runner import setup_django_env

DEFAULT_PORT = 45454


Expand Down Expand Up @@ -46,7 +48,7 @@ def discover_tests(
- cwd: Absolute path to the test start directory;
- uuid: UUID sent by the caller of the Python script, that needs to be sent back as an integrity check;
- status: Test discovery status, can be "success" or "error";
- tests: Discoverered tests if any, not present otherwise. Note that the status can be "error" but the payload can still contain tests;
- tests: Discovered tests if any, not present otherwise. Note that the status can be "error" but the payload can still contain tests;
- error: Discovery error if any, not present otherwise.

Payload format for a successful discovery:
Expand Down Expand Up @@ -78,7 +80,7 @@ def discover_tests(
loader = unittest.TestLoader()
suite = loader.discover(start_dir, pattern, top_level_dir)

tests, error = build_test_tree(suite, cwd) # test tree built succesfully here.
tests, error = build_test_tree(suite, cwd) # test tree built successfully here.

except Exception:
error.append(traceback.format_exc())
Expand Down Expand Up @@ -121,6 +123,12 @@ def post_response(

start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :])

django_test_enabled = os.environ.get("DJANGO_TEST_ENABLED", "False")
if django_test_enabled.lower() == "true":
print(f"DJANGO TEST DECLEARED = {django_test_enabled}")
django_env_enabled = setup_django_env(start_dir)
print(f"DJANGO ENV ENABLED = {django_env_enabled}")

testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT))
testUuid = os.environ.get("TEST_UUID")
if testPort is DEFAULT_PORT:
Expand Down
112 changes: 112 additions & 0 deletions pythonFiles/unittestadapter/django_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import subprocess
import os
import sys
from typing import Union

from pythonFiles.unittestadapter.execution import VSCodeUnittestError

def setup_django_env(start_dir: Union[str, None]):
"""Configures the Django environment to run Django tests.

If Django is not installed or if manage.py can not be found, the function fails quietly.

Args:
start_dir (str): The root directory of the Django project.

Returns:
boolean: either succeeded or failed.
"""

# 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 False

# Get path to manage.py if set as an env var, otherwise use the default
manage_py_path = os.environ.get("MANAGE_PY_PATH")

if manage_py_path is None:
# Search for default manage.py path at the root of the workspace
if not start_dir:
print(
"Error running Django, no start_dir provided or value for MANAGE_PY_PATH"
)

cwd = os.path.abspath(start_dir)
manage_py_path = os.path.join(cwd, "manage.py")

django_settings_module = os.environ.get("DJANGO_SETTINGS_MODULE", None)

if django_settings_module is None:
print("Warning running Django, missing django settings module in environment, reading from manage.py")

import re
try:
with open(manage_py_path, "r") as f:
manage_py_module = f.readlines()
except FileNotFoundError:
print("Error running Django, manage.py not found")
return False

pattern = r"^os\.environ\.setdefault\((\'|\")DJANGO_SETTINGS_MODULE(\'|\"), (\'|\")(?P<settings_path>[\w.]+)(\'|\")\)$"
for line in manage_py_module:
matched = re.match(pattern, line.strip())
if matched is not None:
django_settings_module = matched.groupdict().get("settings_path", None)
break

if django_settings_module is None:
print("Error running Django, django settings module not found")
return False

os.environ.setdefault("DJANGO_SETTINGS_MODULE", django_settings_module)

try:
django.setup()
except ModuleNotFoundError:
print("Error running Django, Drat!")
return False

return True

def django_execution_runner(start_dir: Union[str, None]):

_ = setup_django_env(start_dir)

try:
# Get path to the custom_test_runner.py parent folder, add to sys.path.

# TODO: Check backward compatibility https://docs.python.org/3/library/pathlib.html -> New in version 3.4.
# import pathlib
# custom_test_runner_dir = pathlib.Path(__file__).parent
# sys.path.insert(0, custom_test_runner_dir)

sys.path.insert(
0,
os.path.dirname( # pythonFiles/unittestadapter
os.path.abspath(__file__) # this file
)
)
custom_test_runner = "django_test_runner.CustomTestRunner"

# Build command to run 'python manage.py test'.
python_executable = sys.executable
command = [
python_executable,
"manage.py",
"test",
"--testrunner",
custom_test_runner,
]
print("Running Django run tests with command: ", command)
try:
subprocess.run(" ".join(command), shell=True, check=True)
except subprocess.CalledProcessError as e:
print(f"Error running 'manage.py test': {e}")
raise VSCodeUnittestError(f"Error running 'manage.py test': {e}")
except Exception as e:
print(f"Error configuring Django test runner: {e}")
raise VSCodeUnittestError(f"Error configuring Django test runner: {e}")
21 changes: 21 additions & 0 deletions pythonFiles/unittestadapter/django_test_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.test.runner import DiscoverRunner
import sys
import os
import pathlib

script_dir = pathlib.Path(__file__).parent
sys.path.append(os.fspath(script_dir))

from execution import UnittestTestResult


class CustomTestRunner(DiscoverRunner):
def get_test_runner_kwargs(self):
print("get_test_runner_kwargs")
kwargs = super().get_test_runner_kwargs()
if kwargs["resultclass"] is not None:
raise ValueError(
"Resultclass already set, cannot use custom test runner design for VS Code compatibility."
)
kwargs["resultclass"] = UnittestTestResult
return kwargs
88 changes: 55 additions & 33 deletions pythonFiles/unittestadapter/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from testing_tools import process_json_util, socket_manager
from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict
from unittestadapter.utils import parse_unittest_args
from django_runner import django_execution_runner

ErrorType = Union[
Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None]
Expand All @@ -30,6 +31,13 @@
DEFAULT_PORT = 45454


class VSCodeUnittestError(Exception):
"""A custom exception class for pytest errors."""

def __init__(self, message):
super().__init__(message)


class TestOutcomeEnum(str, enum.Enum):
error = "error"
failure = "failure"
Expand Down Expand Up @@ -103,7 +111,6 @@ def formatResult(
subtest: Union[unittest.TestCase, None] = None,
):
tb = None

message = ""
# error is a tuple of the form returned by sys.exc_info(): (type, value, traceback).
if error is not None:
Expand All @@ -128,9 +135,16 @@ def formatResult(
"subtest": subtest.id() if subtest else None,
}
self.formatted[test_id] = result
if testPort == 0 or testUuid == 0:
print("Error sending response, port or uuid unknown to python server.")
send_run_data(result, testPort, testUuid)
testPort2 = int(os.environ.get("TEST_PORT", DEFAULT_PORT))
testUuid2 = os.environ.get("TEST_UUID")
if testPort2 == 0 or testUuid2 == 0:
print(
"Error sending response, port or uuid unknown to python server.",
testPort,
testUuid,
)

send_run_data(result, testPort2, testUuid2)


class TestExecutionStatus(str, enum.Enum):
Expand Down Expand Up @@ -303,36 +317,44 @@ def post_response(

testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT))
testUuid = os.environ.get("TEST_UUID")
if testPort is DEFAULT_PORT:
print(
"Error[vscode-unittest]: TEST_PORT is not set.",
" TEST_UUID = ",
testUuid,
)
if testUuid is None:
print(
"Error[vscode-unittest]: TEST_UUID is not set.",
" TEST_PORT = ",
testPort,
)
testUuid = "unknown"
if test_ids_from_buffer:
# Perform test execution.
payload = run_tests(
start_dir, test_ids_from_buffer, pattern, top_level_dir, testUuid
)
else:
cwd = os.path.abspath(start_dir)
status = TestExecutionStatus.error
try:
if testPort is DEFAULT_PORT:
raise VSCodeUnittestError(
"Error[vscode-unittest]: TEST_PORT is not set.",
" TEST_UUID = ",
testUuid,
)
if testUuid is None:
raise VSCodeUnittestError(
"Error[vscode-unittest]: TEST_UUID is not set.",
" TEST_PORT = ",
testPort,
)
if test_ids_from_buffer:
# Perform test execution.

# Check to see if we are running django tests.
django_test_enabled = os.environ.get("DJANGO_TEST_ENABLED")
print("DJANGO_TEST_ENABLED = ", django_test_enabled)
if django_test_enabled and django_test_enabled.lower() == "true":
# run django runner
print("running django runner")
django_execution_runner(start_dir)
else:
print("running unittest runner")
payload = run_tests(
start_dir, test_ids_from_buffer, pattern, top_level_dir, testUuid
)
else:
raise VSCodeUnittestError("No test ids received from buffer")
except Exception as exception:
payload: PayloadDict = {
"cwd": cwd,
"status": status,
"error": "No test ids received from buffer",
"cwd": os.path.abspath(start_dir) if start_dir else None,
"status": TestExecutionStatus.error,
"error": exception,
"result": None,
}
post_response(payload, testPort, "unknown")

eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True}
if testUuid is None:
print("Error sending response, uuid unknown to python server.")
post_response(eot_payload, testPort, "unknown")
else:
post_response(eot_payload, testPort, testUuid)
post_response(eot_payload, testPort, testUuid)
7 changes: 6 additions & 1 deletion src/client/interpreter/activation/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,12 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
}
if (result.stderr) {
if (returnedEnv) {
traceWarn('Got env variables but with errors', result.stderr);
traceWarn('Got env variables but with errors', result.stderr, returnedEnv);
if (result.stderr.includes('running scripts is disabled')) {
throw new Error(
`Skipping returned result when powershell execution is disabled, stderr ${result.stderr} for ${command}`,
);
}
} else {
throw new Error(`StdErr from ShellExec, ${result.stderr} for ${command}`);
}
Expand Down
1 change: 1 addition & 0 deletions src/client/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum EventName {
PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT',
PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT',
CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT',
TERMINAL_DEACTIVATE_PROMPT = 'TERMINAL_DEACTIVATE_PROMPT',
REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT',
ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH',
ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION',
Expand Down
19 changes: 18 additions & 1 deletion src/client/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1226,13 +1226,30 @@ export interface IEventNamePropertyMapping {
selection: 'Allow' | 'Close' | undefined;
};
/**
* Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed.
* Telemetry event sent with details when user clicks the prompt with the following message:
*
* 'We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we suggest the "terminal.integrated.inheritEnv" setting to be changed to false. Would you like to update this setting?'
*/
/* __GDPR__
"conda_inherit_env_prompt" : {
"selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" }
}
*/
[EventName.TERMINAL_DEACTIVATE_PROMPT]: {
/**
* `Yes` When 'Allow' option is selected
* `Close` When 'Close' option is selected
*/
selection: 'Edit script' | "Don't show again" | undefined;
};
/**
* Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed.
*/
/* __GDPR__
"require_jupyter_prompt" : {
"selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" }
}
*/
[EventName.REQUIRE_JUPYTER_PROMPT]: {
/**
* `Yes` When 'Yes' option is selected
Expand Down
Loading