Skip to content

Commit 71a451c

Browse files
Save env vars at first pytest hook to ensure theyre unedited (microsoft#22344)
fixes microsoft#22192. Now, all environment variables are accessed during the pytest_load_initial_conftests hook and then saved as global variables in the plugin. This ensures the port and uuid will still be saved even if a user safely or unsafely clears their environment variables during testing. --------- Co-authored-by: Karthik Nadig <[email protected]>
1 parent c2dec14 commit 71a451c

File tree

4 files changed

+146
-40
lines changed

4 files changed

+146
-40
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import os
5+
6+
7+
def test_clear_env(monkeypatch):
8+
# Clear all environment variables
9+
monkeypatch.setattr(os, "environ", {})
10+
11+
# Now os.environ should be empty
12+
assert not os.environ
13+
14+
# After the test finishes, the environment variables will be reset to their original state
15+
16+
17+
def test_check_env():
18+
# This test will have access to the original environment variables
19+
assert "PATH" in os.environ
20+
21+
22+
def test_clear_env_unsafe():
23+
# Clear all environment variables
24+
os.environ.clear()
25+
# Now os.environ should be empty
26+
assert not os.environ
27+
28+
29+
def test_check_env_unsafe():
30+
# ("PATH" in os.environ) is False here if it runs after test_clear_env_unsafe.
31+
# Regardless, this test will pass and TEST_PORT and TEST_UUID will still be set correctly
32+
assert "PATH" not in os.environ

pythonFiles/tests/pytestadapter/expected_execution_test_output.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,63 @@
624624
"subtest": None,
625625
},
626626
}
627+
628+
# This is the expected output for the test safe clear env vars file.
629+
# └── test_env_vars.py
630+
# └── test_clear_env: success
631+
# └── test_check_env: success
632+
633+
test_safe_clear_env_vars_path = TEST_DATA_PATH / "test_env_vars.py"
634+
safe_clear_env_vars_expected_execution_output = {
635+
get_absolute_test_id(
636+
"test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path
637+
): {
638+
"test": get_absolute_test_id(
639+
"test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path
640+
),
641+
"outcome": "success",
642+
"message": None,
643+
"traceback": None,
644+
"subtest": None,
645+
},
646+
get_absolute_test_id(
647+
"test_env_vars.py::test_check_env", test_safe_clear_env_vars_path
648+
): {
649+
"test": get_absolute_test_id(
650+
"test_env_vars.py::test_check_env", test_safe_clear_env_vars_path
651+
),
652+
"outcome": "success",
653+
"message": None,
654+
"traceback": None,
655+
"subtest": None,
656+
},
657+
}
658+
659+
# This is the expected output for the test unsafe clear env vars file.
660+
# └── test_env_vars.py
661+
# └── test_clear_env_unsafe: success
662+
# └── test_check_env_unsafe: success
663+
unsafe_clear_env_vars_expected_execution_output = {
664+
get_absolute_test_id(
665+
"test_env_vars.py::test_clear_env_unsafe", test_safe_clear_env_vars_path
666+
): {
667+
"test": get_absolute_test_id(
668+
"test_env_vars.py::test_clear_env_unsafe", test_safe_clear_env_vars_path
669+
),
670+
"outcome": "success",
671+
"message": None,
672+
"traceback": None,
673+
"subtest": None,
674+
},
675+
get_absolute_test_id(
676+
"test_env_vars.py::test_check_env_unsafe", test_safe_clear_env_vars_path
677+
): {
678+
"test": get_absolute_test_id(
679+
"test_env_vars.py::test_check_env_unsafe", test_safe_clear_env_vars_path
680+
),
681+
"outcome": "success",
682+
"message": None,
683+
"traceback": None,
684+
"subtest": None,
685+
},
686+
}

pythonFiles/tests/pytestadapter/test_execution.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ def test_bad_id_error_execution():
131131
@pytest.mark.parametrize(
132132
"test_ids, expected_const",
133133
[
134+
(
135+
[
136+
"test_env_vars.py::test_clear_env",
137+
"test_env_vars.py::test_check_env",
138+
],
139+
expected_execution_test_output.safe_clear_env_vars_expected_execution_output,
140+
),
134141
(
135142
[
136143
"skip_tests.py::test_something",

pythonFiles/vscode_pytest/__init__.py

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import os
77
import pathlib
88
import sys
9-
import time
109
import traceback
1110

1211
import pytest
@@ -56,9 +55,21 @@ def __init__(self, message):
5655
IS_DISCOVERY = False
5756
map_id_to_path = dict()
5857
collected_tests_so_far = list()
58+
TEST_PORT = os.getenv("TEST_PORT")
59+
TEST_UUID = os.getenv("TEST_UUID")
5960

6061

6162
def pytest_load_initial_conftests(early_config, parser, args):
63+
global TEST_PORT
64+
global TEST_UUID
65+
TEST_PORT = os.getenv("TEST_PORT")
66+
TEST_UUID = os.getenv("TEST_UUID")
67+
error_string = (
68+
"PYTEST ERROR: TEST_UUID and/or TEST_PORT are not set at the time of pytest starting. Please confirm these environment variables are not being"
69+
" changed or removed as they are required for successful test discovery and execution."
70+
f" \nTEST_UUID = {TEST_UUID}\nTEST_PORT = {TEST_PORT}\n"
71+
)
72+
print(error_string, file=sys.stderr)
6273
if "--collect-only" in args:
6374
global IS_DISCOVERY
6475
IS_DISCOVERY = True
@@ -689,58 +700,54 @@ def send_post_request(
689700
payload -- the payload data to be sent.
690701
cls_encoder -- a custom encoder if needed.
691702
"""
692-
testPort = os.getenv("TEST_PORT")
693-
testUuid = os.getenv("TEST_UUID")
694-
if testPort is None:
695-
print(
696-
"Error[vscode-pytest]: TEST_PORT is not set.",
697-
" TEST_UUID = ",
698-
testUuid,
699-
)
700-
testPort = DEFAULT_PORT
701-
if testUuid is None:
702-
print(
703-
"Error[vscode-pytest]: TEST_UUID is not set.",
704-
" TEST_PORT = ",
705-
testPort,
703+
global TEST_PORT
704+
global TEST_UUID
705+
if TEST_UUID is None or TEST_PORT is None:
706+
# if TEST_UUID or TEST_PORT is None, print an error and fail as these are both critical errors
707+
error_msg = (
708+
"PYTEST ERROR: TEST_UUID and/or TEST_PORT are not set at the time of pytest starting. Please confirm these environment variables are not being"
709+
" changed or removed as they are required for successful pytest discovery and execution."
710+
f" \nTEST_UUID = {TEST_UUID}\nTEST_PORT = {TEST_PORT}\n"
706711
)
707-
testUuid = "unknown"
708-
addr = ("localhost", int(testPort))
712+
print(error_msg, file=sys.stderr)
713+
raise VSCodePytestError(error_msg)
714+
715+
addr = ("localhost", int(TEST_PORT))
709716
global __socket
710717

711718
if __socket is None:
712719
try:
713720
__socket = socket_manager.SocketManager(addr)
714721
__socket.connect()
715722
except Exception as error:
716-
print(f"Plugin error connection error[vscode-pytest]: {error}")
723+
error_msg = f"Error attempting to connect to extension communication socket[vscode-pytest]: {error}"
724+
print(error_msg, file=sys.stderr)
725+
print(
726+
"If you are on a Windows machine, this error may be occurring if any of your tests clear environment variables"
727+
" as they are required to communicate with the extension. Please reference https://docs.pytest.org/en/stable/how-to/monkeypatch.html#monkeypatching-environment-variables"
728+
"for the correct way to clear environment variables during testing.\n",
729+
file=sys.stderr,
730+
)
717731
__socket = None
732+
raise VSCodePytestError(error_msg)
718733

719734
data = json.dumps(payload, cls=cls_encoder)
720735
request = f"""Content-Length: {len(data)}
721736
Content-Type: application/json
722-
Request-uuid: {testUuid}
737+
Request-uuid: {TEST_UUID}
723738
724739
{data}"""
725740

726-
max_retries = 3
727-
retries = 0
728-
while retries < max_retries:
729-
try:
730-
if __socket is not None and __socket.socket is not None:
731-
__socket.socket.sendall(request.encode("utf-8"))
732-
# print("Post request sent successfully!")
733-
# print("data sent", payload, "end of data")
734-
break # Exit the loop if the send was successful
735-
else:
736-
print("Plugin error connection error[vscode-pytest]")
737-
print(f"[vscode-pytest] data: {request}")
738-
except Exception as error:
739-
print(f"Plugin error connection error[vscode-pytest]: {error}")
740-
print(f"[vscode-pytest] data: {request}")
741-
retries += 1 # Increment retry counter
742-
if retries < max_retries:
743-
print(f"Retrying ({retries}/{max_retries}) in 2 seconds...")
744-
time.sleep(2) # Wait for a short duration before retrying
745-
else:
746-
print("Maximum retry attempts reached. Cannot send post request.")
741+
try:
742+
if __socket is not None and __socket.socket is not None:
743+
__socket.socket.sendall(request.encode("utf-8"))
744+
else:
745+
print(
746+
f"Plugin error connection error[vscode-pytest], socket is None \n[vscode-pytest] data: \n{request} \n",
747+
file=sys.stderr,
748+
)
749+
except Exception as error:
750+
print(
751+
f"Plugin error, exception thrown while attempting to send data[vscode-pytest]: {error} \n[vscode-pytest] data: \n{request}\n",
752+
file=sys.stderr,
753+
)

0 commit comments

Comments
 (0)