11import argparse
2- import tempfile
32import os
4- from typing import Optional , List , Any
53import sys
6- from subprocess import run
4+ from subprocess import CalledProcessError
5+ from typing import List , Optional
76
87from .Check import Check
98
109from ci_tools .functions import is_error_code_5_allowed , install_into_venv
11- from ci_tools .variables import set_envvar_defaults
12- from ci_tools .parsing import ParsedSetup
1310from ci_tools .scenario .generation import create_package_and_install
11+ from ci_tools .variables import discover_repo_root , set_envvar_defaults
1412from ci_tools .logging import logger
1513
14+ REPO_ROOT = discover_repo_root ()
15+
16+ PACKAGING_REQUIREMENTS = [
17+ "wheel==0.45.1" ,
18+ "packaging==24.2" ,
19+ "urllib3==2.2.3" ,
20+ "tomli==2.2.1" ,
21+ "build==1.2.2.post1" ,
22+ "pkginfo==1.12.1.2" ,
23+ ]
24+
25+ TEST_TOOLS_REQUIREMENTS = os .path .join (REPO_ROOT , "eng/test_tools.txt" )
26+
1627
1728class whl (Check ):
1829 def __init__ (self ) -> None :
@@ -27,64 +38,120 @@ def register(
2738 parents = parent_parsers or []
2839 p = subparsers .add_parser ("whl" , parents = parents , help = "Run the whl check" )
2940 p .set_defaults (func = self .run )
30- # TODO add mark_args, and other parameters
41+ p .add_argument (
42+ "--pytest-args" ,
43+ nargs = argparse .REMAINDER ,
44+ help = "Additional arguments forwarded to pytest." ,
45+ )
3146
3247 def run (self , args : argparse .Namespace ) -> int :
3348 """Run the whl check command."""
3449 logger .info ("Running whl check..." )
3550
36- set_envvar_defaults ()
51+ set_envvar_defaults ({ "PROXY_URL" : "http://localhost:5001" } )
3752
3853 targeted = self .get_targeted_directories (args )
54+ if not targeted :
55+ logger .warning ("No target packages discovered for whl check." )
56+ return 0
3957
40- results : List [ int ] = []
58+ overall_result = 0
4159
4260 for parsed in targeted :
43- pkg = parsed .folder
44- executable , staging_directory = self .get_executable (args .isolate , args .command , sys .executable , pkg )
45-
46- logger .info (f"Invoking check with { executable } " )
47-
48- self .install_dev_reqs (executable , args , pkg )
49-
50- create_package_and_install (
51- distribution_directory = staging_directory ,
52- target_setup = pkg ,
53- skip_install = False ,
54- cache_dir = None ,
55- work_dir = staging_directory ,
56- force_create = False ,
57- package_type = "wheel" ,
58- pre_download_disabled = False ,
59- python_executable = executable ,
60- )
61-
62- # TODO: split sys.argv[1:] on -- and pass in everything after the -- as additional arguments
63- # TODO: handle mark_args
64- logger .info (f"Invoke pytest for { pkg } " )
65- exit_code = run (
66- [executable , "-m" , "pytest" , "." ]
67- + [
68- "-rsfE" ,
69- f"--junitxml={ pkg } /test-junit-{ args .command } .xml" ,
70- "--verbose" ,
71- "--cov-branch" ,
72- "--durations=10" ,
73- "--ignore=azure" ,
74- "--ignore-glob=.venv*" ,
75- "--ignore=build" ,
76- "--ignore=.eggs" ,
77- "--ignore=samples" ,
78- ],
79- cwd = pkg ,
80- ).returncode
81-
82- if exit_code != 0 :
83- if exit_code == 5 and is_error_code_5_allowed (parsed .folder , parsed .name ):
84- logger .info ("Exit code 5 is allowed, continuing execution." )
85- else :
86- logger .info (f"pytest failed with exit code { exit_code } ." )
87- results .append (exit_code )
88-
89- # final result is the worst case of all the results
90- return max (results )
61+ package_dir = parsed .folder
62+ package_name = parsed .name
63+
64+ executable , staging_directory = self .get_executable (args .isolate , args .command , sys .executable , package_dir )
65+ logger .info (f"Processing { package_name } using interpreter { executable } " )
66+
67+ try :
68+ self ._install_common_requirements (executable , package_dir )
69+ self .install_dev_reqs (executable , args , package_dir )
70+ except CalledProcessError as exc :
71+ logger .error (f"Failed to install dependencies for { package_name } : { exc } " )
72+ overall_result = max (overall_result , exc .returncode or 1 )
73+ continue
74+
75+ try :
76+ create_package_and_install (
77+ distribution_directory = staging_directory ,
78+ target_setup = package_dir ,
79+ skip_install = False ,
80+ cache_dir = None ,
81+ work_dir = staging_directory ,
82+ force_create = False ,
83+ package_type = "wheel" ,
84+ pre_download_disabled = False ,
85+ python_executable = executable ,
86+ )
87+ except CalledProcessError as exc :
88+ logger .error (f"Failed to build/install wheel for { package_name } : { exc } " )
89+ overall_result = max (overall_result , exc .returncode or 1 )
90+ continue
91+
92+ pytest_args = self ._build_pytest_args (package_dir , args )
93+ pytest_command = ["-m" , "pytest" , * pytest_args ]
94+ pytest_result = self .run_venv_command (executable , pytest_command , cwd = package_dir , immediately_dump = True )
95+
96+ if pytest_result .returncode != 0 :
97+ if pytest_result .returncode == 5 and is_error_code_5_allowed (package_dir , package_name ):
98+ logger .info (
99+ "pytest exited with code 5 for %s, which is allowed for management or opt-out packages." ,
100+ package_name ,
101+ )
102+ # Align with tox: skip coverage when tests are skipped entirely
103+ continue
104+
105+ logger .error (f"pytest failed for { package_name } with exit code { pytest_result .returncode } ." )
106+ continue
107+
108+ coverage_command = [
109+ os .path .join (REPO_ROOT , "eng/tox/run_coverage.py" ),
110+ "-t" ,
111+ package_dir ,
112+ "-r" ,
113+ REPO_ROOT ,
114+ ]
115+ coverage_result = self .run_venv_command (executable , coverage_command , cwd = package_dir )
116+ if coverage_result .returncode != 0 :
117+ logger .error (
118+ f"Coverage generation failed for { package_name } with exit code { coverage_result .returncode } ."
119+ )
120+ if coverage_result .stdout :
121+ logger .error (coverage_result .stdout )
122+ if coverage_result .stderr :
123+ logger .error (coverage_result .stderr )
124+ overall_result = max (overall_result , coverage_result .returncode )
125+
126+ return overall_result
127+
128+ def _install_common_requirements (self , executable : str , package_dir : str ) -> None :
129+ install_into_venv (executable , PACKAGING_REQUIREMENTS , package_dir )
130+
131+ if os .path .exists (TEST_TOOLS_REQUIREMENTS ):
132+ install_into_venv (executable , ["-r" , TEST_TOOLS_REQUIREMENTS ], package_dir )
133+ else :
134+ logger .warning (f"Test tools requirements file not found at { TEST_TOOLS_REQUIREMENTS } ." )
135+
136+ def _build_pytest_args (self , package_dir : str , args : argparse .Namespace ) -> List [str ]:
137+ log_level = os .getenv ("PYTEST_LOG_LEVEL" , "51" )
138+ junit_path = os .path .join (package_dir , f"test-junit-{ args .command } .xml" )
139+
140+ default_args = [
141+ "-rsfE" ,
142+ f"--junitxml={ junit_path } " ,
143+ "--verbose" ,
144+ "--cov-branch" ,
145+ "--durations=10" ,
146+ "--ignore=azure" ,
147+ "--ignore=.tox" ,
148+ "--ignore-glob=.venv*" ,
149+ "--ignore=build" ,
150+ "--ignore=.eggs" ,
151+ "--ignore=samples" ,
152+ f"--log-cli-level={ log_level } " ,
153+ ]
154+
155+ additional = args .pytest_args if args .pytest_args else []
156+
157+ return [* default_args , * additional , package_dir ]
0 commit comments