Skip to content

Commit 10a6d15

Browse files
authored
refactor install and test (#44482)
* refactor install and test * refactored optional and i think it's working * minor clean * optional actually working properly * bug fix * bug fix * minor exception handling revisions
1 parent 70cf394 commit 10a6d15

File tree

2 files changed

+125
-121
lines changed

2 files changed

+125
-121
lines changed

eng/tools/azure-sdk-tools/azpysdk/install_and_test.py

Lines changed: 85 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -59,31 +59,11 @@ def run(self, args: argparse.Namespace) -> int:
5959
executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir)
6060
logger.info(f"Processing {package_name} using interpreter {executable}")
6161

62-
try:
63-
self._install_common_requirements(executable, package_dir)
64-
if self.should_install_dev_requirements():
65-
self.install_dev_reqs(executable, args, package_dir)
66-
self.after_dependencies_installed(executable, package_dir, staging_directory, args)
67-
except CalledProcessError as exc:
68-
logger.error(f"Failed to prepare dependencies for {package_name}: {exc}")
69-
results.append(exc.returncode)
70-
continue
71-
72-
try:
73-
create_package_and_install(
74-
distribution_directory=staging_directory,
75-
target_setup=package_dir,
76-
skip_install=False,
77-
cache_dir=None,
78-
work_dir=staging_directory,
79-
force_create=False,
80-
package_type=self.package_type,
81-
pre_download_disabled=False,
82-
python_executable=executable,
83-
)
84-
except CalledProcessError as exc:
85-
logger.error(f"Failed to build/install {self.package_type} for {package_name}: {exc}")
86-
results.append(1)
62+
install_result = self.install_all_requiremenmts(
63+
executable, staging_directory, package_name, package_dir, args
64+
)
65+
if install_result != 0:
66+
results.append(install_result)
8767
continue
8868

8969
try:
@@ -94,47 +74,93 @@ def run(self, args: argparse.Namespace) -> int:
9474
continue
9575

9676
pytest_args = self._build_pytest_args(package_dir, args)
97-
pytest_command = ["-m", "pytest", *pytest_args]
98-
pytest_result = self.run_venv_command(
99-
executable, pytest_command, cwd=staging_directory, immediately_dump=True
100-
)
101-
102-
if pytest_result.returncode != 0:
103-
if pytest_result.returncode == 5 and is_error_code_5_allowed(package_dir, package_name):
104-
logger.info(
105-
"pytest exited with code 5 for %s, which is allowed for management or opt-out packages.",
106-
package_name,
107-
)
108-
# Align with tox: skip coverage when tests are skipped entirely
109-
continue
110-
else:
111-
results.append(pytest_result.returncode)
112-
logger.error(f"pytest failed for {package_name} with exit code {pytest_result.returncode}.")
113-
continue
77+
pytest_result = self.run_pytest(executable, staging_directory, package_dir, package_name, pytest_args)
78+
if pytest_result != 0:
79+
results.append(pytest_result)
80+
continue
11481

11582
if not self.coverage_enabled:
11683
continue
11784

118-
coverage_command = [
119-
os.path.join(REPO_ROOT, "eng/tox/run_coverage.py"),
120-
"-t",
121-
package_dir,
122-
"-r",
123-
REPO_ROOT,
124-
]
125-
coverage_result = self.run_venv_command(executable, coverage_command, cwd=package_dir)
126-
if coverage_result.returncode != 0:
127-
logger.error(
128-
f"Coverage generation failed for {package_name} with exit code {coverage_result.returncode}."
129-
)
130-
if coverage_result.stdout:
131-
logger.error(coverage_result.stdout)
132-
if coverage_result.stderr:
133-
logger.error(coverage_result.stderr)
134-
results.append(coverage_result.returncode)
85+
coverage_result = self.check_coverage(executable, package_dir, package_name)
86+
if coverage_result != 0:
87+
results.append(coverage_result)
13588

13689
return max(results) if results else 0
13790

91+
def check_coverage(self, executable: str, package_dir: str, package_name: str) -> int:
92+
coverage_command = [
93+
os.path.join(REPO_ROOT, "eng/tox/run_coverage.py"),
94+
"-t",
95+
package_dir,
96+
"-r",
97+
REPO_ROOT,
98+
]
99+
coverage_result = self.run_venv_command(executable, coverage_command, cwd=package_dir)
100+
if coverage_result.returncode != 0:
101+
logger.error(f"Coverage generation failed for {package_name} with exit code {coverage_result.returncode}.")
102+
if coverage_result.stdout:
103+
logger.error(coverage_result.stdout)
104+
if coverage_result.stderr:
105+
logger.error(coverage_result.stderr)
106+
return coverage_result.returncode
107+
return 0
108+
109+
def run_pytest(
110+
self,
111+
executable: str,
112+
staging_directory: str,
113+
package_dir: str,
114+
package_name: str,
115+
pytest_args: List[str],
116+
cwd: Optional[str] = None,
117+
) -> int:
118+
pytest_command = ["-m", "pytest", *pytest_args]
119+
pytest_result = self.run_venv_command(
120+
executable, pytest_command, cwd=(cwd or staging_directory), immediately_dump=True
121+
)
122+
if pytest_result.returncode != 0:
123+
if pytest_result.returncode == 5 and is_error_code_5_allowed(package_dir, package_name):
124+
logger.info(
125+
"pytest exited with code 5 for %s, which is allowed for management or opt-out packages.",
126+
package_name,
127+
)
128+
# Align with tox: skip coverage when tests are skipped entirely
129+
return 0
130+
else:
131+
logger.error(f"pytest failed for {package_name} with exit code {pytest_result.returncode}.")
132+
return pytest_result.returncode
133+
return 0
134+
135+
def install_all_requiremenmts(
136+
self, executable: str, staging_directory: str, package_name: str, package_dir: str, args: argparse.Namespace
137+
) -> int:
138+
try:
139+
self._install_common_requirements(executable, package_dir)
140+
if self.should_install_dev_requirements():
141+
self.install_dev_reqs(executable, args, package_dir)
142+
self.after_dependencies_installed(executable, package_dir, staging_directory, args)
143+
except CalledProcessError as exc:
144+
logger.error(f"Failed to prepare dependencies for {package_name}: {exc}")
145+
return exc.returncode or 1
146+
147+
try:
148+
create_package_and_install(
149+
distribution_directory=staging_directory,
150+
target_setup=package_dir,
151+
skip_install=False,
152+
cache_dir=None,
153+
work_dir=staging_directory,
154+
force_create=False,
155+
package_type=self.package_type,
156+
pre_download_disabled=False,
157+
python_executable=executable,
158+
)
159+
except CalledProcessError as exc:
160+
logger.error(f"Failed to build/install {self.package_type} for {package_name}: {exc}")
161+
exit(1)
162+
return 0
163+
138164
def get_env_defaults(self) -> Dict[str, str]:
139165
defaults: Dict[str, str] = {}
140166
if self.proxy_url:

eng/tools/azure-sdk-tools/azpysdk/optional.py

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,22 @@
55

66
from typing import Optional, List
77

8-
from .Check import Check
8+
from .install_and_test import InstallAndTest
99
from ci_tools.functions import (
1010
install_into_venv,
1111
uninstall_from_venv,
12-
is_error_code_5_allowed,
1312
)
14-
from ci_tools.scenario.generation import create_package_and_install, prepare_environment
15-
from ci_tools.variables import discover_repo_root, in_ci, set_envvar_defaults
16-
from ci_tools.environment_exclusions import is_check_enabled
13+
from ci_tools.scenario.generation import prepare_environment
14+
from ci_tools.variables import discover_repo_root, set_envvar_defaults
1715
from ci_tools.parsing import get_config_setting
1816
from ci_tools.logging import logger
1917

2018
REPO_ROOT = discover_repo_root()
2119

2220

23-
class optional(Check):
21+
class optional(InstallAndTest):
2422
def __init__(self) -> None:
25-
super().__init__()
23+
super().__init__(package_type="sdist", proxy_url="http://localhost:5004", display_name="optional")
2624

2725
def register(
2826
self, subparsers: "argparse._SubParsersAction", parent_parsers: Optional[List[argparse.ArgumentParser]] = None
@@ -48,31 +46,29 @@ def run(self, args: argparse.Namespace) -> int:
4846
"""Run the optional check command."""
4947
logger.info("Running optional check...")
5048

51-
set_envvar_defaults({"PROXY_URL": "http://localhost:5004"})
49+
env_defaults = self.get_env_defaults()
50+
if env_defaults:
51+
set_envvar_defaults(env_defaults)
52+
5253
targeted = self.get_targeted_directories(args)
54+
if not targeted:
55+
logger.warning("No target packages discovered for optional check.")
56+
return 0
5357

5458
results: List[int] = []
5559

5660
for parsed in targeted:
5761
package_dir = parsed.folder
5862
package_name = parsed.name
5963
executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir)
60-
logger.info(f"Processing {package_name} for optional check")
61-
62-
if in_ci():
63-
if not is_check_enabled(package_dir, "optional", False):
64-
logger.info(f"Package {package_name} opts-out of optional check.")
65-
continue
66-
67-
try:
68-
self.install_dev_reqs(executable, args, package_dir)
69-
except CalledProcessError as exc:
70-
logger.error(f"Failed to install dependencies for {package_name}: {exc}")
71-
results.append(exc.returncode)
72-
continue
64+
logger.info(f"Processing {package_name} using interpreter {executable}")
7365

7466
try:
75-
self.prepare_and_test_optional(package_name, package_dir, staging_directory, args.optional)
67+
result = self.prepare_and_test_optional(
68+
package_name, package_dir, staging_directory, args.optional, args
69+
)
70+
if result != 0:
71+
results.append(result)
7672
except Exception as e:
7773
logger.error(f"Optional check for package {package_name} failed with exception: {e}")
7874
results.append(1)
@@ -83,16 +79,19 @@ def run(self, args: argparse.Namespace) -> int:
8379
# TODO copying from generation.py, remove old code later
8480
# TODO remove pytest() function from ci_tools.functions as it was only used in the old version of this logic
8581
def prepare_and_test_optional(
86-
self, package_name: str, package_dir: str, temp_dir: str, target_env_name: str
87-
) -> None:
82+
self, package_name: str, package_dir: str, temp_dir: str, target_env_name: str, args: argparse.Namespace
83+
) -> int:
8884
"""
8985
Prepare and test the optional environment for the given package.
9086
"""
9187
optional_configs = get_config_setting(package_dir, "optional")
9288

89+
if not isinstance(optional_configs, list):
90+
optional_configs = []
91+
9392
if len(optional_configs) == 0:
9493
logger.info(f"No optional environments detected in pyproject.toml within {package_dir}.")
95-
exit(0)
94+
return 0
9695

9796
config_results = []
9897

@@ -109,26 +108,18 @@ def prepare_and_test_optional(
109108

110109
environment_exe = prepare_environment(package_dir, temp_dir, env_name)
111110

112-
create_package_and_install(
113-
distribution_directory=temp_dir,
114-
target_setup=package_dir,
115-
skip_install=False,
116-
cache_dir=None,
117-
work_dir=temp_dir,
118-
force_create=False,
119-
package_type="sdist",
120-
pre_download_disabled=False,
121-
python_executable=environment_exe,
122-
)
123-
dev_reqs = os.path.join(package_dir, "dev_requirements.txt")
124-
test_tools = os.path.join(REPO_ROOT, "eng", "test_tools.txt")
125-
126-
# install the dev requirements and test_tools requirements files to ensure tests can run
111+
# install package and testing requirements
127112
try:
128-
install_into_venv(environment_exe, ["-r", dev_reqs, "-r", test_tools], package_dir)
113+
install_result = self.install_all_requiremenmts(
114+
environment_exe, temp_dir, package_name, package_dir, args
115+
)
116+
if install_result != 0:
117+
logger.error(f"Failed to install base requirements for {package_name} in optional env {env_name}.")
118+
config_results.append(False)
119+
break
129120
except CalledProcessError as exc:
130121
logger.error(
131-
f"Unable to complete installation of dev_requirements.txt and/or test_tools.txt for {package_name}, check command output above."
122+
f"Failed to install base requirements for {package_name} in optional env {env_name}: {exc}"
132123
)
133124
config_results.append(False)
134125
break
@@ -181,30 +172,16 @@ def prepare_and_test_optional(
181172

182173
logger.info(f"Invoking tests for package {package_name} and optional environment {env_name}")
183174

184-
pytest_command = ["-m", "pytest", *pytest_args]
185-
pytest_result = self.run_venv_command(
186-
environment_exe, pytest_command, cwd=package_dir, immediately_dump=True
187-
)
188-
189-
if pytest_result.returncode != 0:
190-
if pytest_result.returncode == 5 and is_error_code_5_allowed(package_dir, package_name):
191-
logger.info(
192-
"pytest exited with code 5 for %s, which is allowed for management or opt-out packages.",
193-
package_name,
194-
)
195-
# Align with tox: skip coverage when tests are skipped entirely
196-
continue
197-
logger.error(
198-
f"pytest failed for {package_name} and optional environment {env_name} with exit code {pytest_result.returncode}."
175+
try:
176+
pytest_result = self.run_pytest(
177+
environment_exe, temp_dir, package_dir, package_name, pytest_args, cwd=package_dir
199178
)
179+
config_results.append(True if pytest_result == 0 else False)
180+
except CalledProcessError as exc:
200181
config_results.append(False)
201-
else:
202-
logger.info(f"pytest succeeded for {package_name} and optional environment {env_name}.")
203-
config_results.append(True)
204182

205183
if all(config_results):
206184
logger.info(f"All optional environment(s) for {package_name} completed successfully.")
207-
exit(0)
208185
else:
209186
for i, config in enumerate(optional_configs):
210187
if i >= len(config_results):
@@ -214,4 +191,5 @@ def prepare_and_test_optional(
214191
logger.error(
215192
f"Optional environment {config_name} for {package_name} completed with non-zero exit-code. Check test results above."
216193
)
217-
exit(1)
194+
return 1
195+
return 0

0 commit comments

Comments
 (0)