@@ -102,113 +102,122 @@ def unit(session):
102102# A callable class is necessary so that the session can be closed over
103103# instead of passed in, which simplifies the invocation via map.
104104class FragTester :
105- def __init__ (self , session , use_ads_templates , mypy_only = False ):
105+ def __init__ (self , session , use_ads_templates , output_dir : Path ):
106106 self .session = session
107107 self .use_ads_templates = use_ads_templates
108- self .mypy_only = mypy_only
109-
110- def __call__ (self , frag ):
111- with tempfile .TemporaryDirectory () as tmp_dir :
112- # Generate the fragment GAPIC.
113- outputs = []
114- templates = (
115- path .join (path .dirname (__file__ ), "gapic" , "ads-templates" )
116- if self .use_ads_templates
117- else "DEFAULT"
118- )
119- maybe_old_naming = ",old-naming" if self .use_ads_templates else ""
120-
121- session_args = [
122- "python" ,
123- "-m" ,
124- "grpc_tools.protoc" ,
125- f"--proto_path={ str (FRAG_DIR )} " ,
126- f"--python_gapic_out={ tmp_dir } " ,
127- f"--python_gapic_opt=transport=grpc+rest,python-gapic-templates={ templates } { maybe_old_naming } " ,
128- ]
108+ self .output_dir = output_dir
109+
110+ def generate (self , frag ):
111+ """Generate the GAPIC and install it."""
112+ # Create a unique sub-directory per fragment within our shared output_dir
113+ # so we don't have naming collisions between different fragments.
114+ frag_gen_dir = self .output_dir / frag .stem
115+ frag_gen_dir .mkdir (parents = True , exist_ok = True )
116+
117+ templates = (
118+ path .join (path .dirname (__file__ ), "gapic" , "ads-templates" )
119+ if self .use_ads_templates
120+ else "DEFAULT"
121+ )
122+ maybe_old_naming = ",old-naming" if self .use_ads_templates else ""
129123
130- outputs . append (
131- self . session . run (
132- * session_args ,
133- str ( frag ) ,
134- external = True ,
135- silent = True ,
136- )
137- )
124+ session_args = [
125+ "python" ,
126+ "-m" ,
127+ "grpc_tools.protoc" ,
128+ f"--proto_path= { str ( FRAG_DIR ) } " ,
129+ f"--python_gapic_out= { str ( frag_gen_dir ) } " ,
130+ f"--python_gapic_opt=transport=grpc+rest,python-gapic-templates= { templates } { maybe_old_naming } " ,
131+ ]
138132
139- # Install the generated fragment library.
140- if self .use_ads_templates :
141- self .session .install (tmp_dir , "-e" , "." , "-qqq" )
142- else :
143- # Use the constraints file for the specific python runtime version.
144- # We do this to make sure that we're testing against the lowest
145- # supported version of a dependency.
146- # This is needed to recreate the issue reported in
147- # https://github.com/googleapis/gapic-generator-python/issues/1748
148- # The ads templates do not have constraints files.
149- constraints_path = str (
150- f"{ tmp_dir } /testing/constraints-{ self .session .python } .txt"
151- )
152- self .session .install (tmp_dir , "-e" , "." , "-qqq" , "-r" , constraints_path )
153-
154- if self .mypy_only :
155- self .session .run ("mypy" , f"{ tmp_dir } /google" , "--check-untyped-defs" )
156- else :
157- # Run the fragment's generated unit tests.
158- # Don't bother parallelizing them: we already parallelize
159- # # the fragments, and there usually aren't too many tests per fragment.
160- outputs .append (
161- self .session .run (
162- "py.test" ,
163- "--quiet" ,
164- f"--cov-config={ str (Path (tmp_dir ) / '.coveragerc' )} " ,
165- "--cov-report=term" ,
166- "--cov-fail-under=100" ,
167- str (Path (tmp_dir ) / "tests" / "unit" ),
168- silent = True ,
169- )
170- )
133+ # Run Protoc
134+ self .session .run (
135+ * session_args ,
136+ str (frag ),
137+ external = True ,
138+ silent = True ,
139+ )
171140
172- return "" .join (outputs )
141+ # Install the generated fragment library into the Nox venv
142+ if self .use_ads_templates :
143+ self .session .install (str (frag_gen_dir ), "-e" , "." , "-qqq" )
144+ else :
145+ constraints_path = str (
146+ frag_gen_dir / "testing" / f"constraints-{ self .session .python } .txt"
147+ )
148+ self .session .install (str (frag_gen_dir ), "-e" , "." , "-qqq" , "-r" , constraints_path )
149+
150+ return str (frag_gen_dir )
173151
174152
175153@nox .session (python = ALL_PYTHON )
176154def fragment (session , use_ads_templates = False ):
155+ """Refactored: Generate all once, then run pytest and mypy in batches."""
177156 session .install (
178157 "coverage" ,
179158 "pytest" ,
180159 "pytest-cov" ,
181160 "pytest-xdist" ,
182161 "pytest-asyncio" ,
183162 "grpcio-tools" ,
163+ "mypy" ,
164+ "types-protobuf" ,
165+ "types-requests" ,
184166 )
185167 session .install ("-e" , "." )
186168
187- # The specific failure is `Plugin output is unparseable`
169+ # Address the specific failure for older python versions
188170 if session .python in ("3.9" , "3.10" ):
189171 session .install ("google-api-core<2.28" )
190172
191- frag_files = (
192- [Path (f ) for f in session .posargs ] if session .posargs else FRAGMENT_FILES
193- )
173+ # 1. Prepare fragment list
174+ frag_files = [Path (f ) for f in session .posargs ] if session .posargs else list (FRAGMENT_FILES )
175+
176+ # Exclude test_iam.proto for Ads templates which fails
177+ # There are known issues with ads mixins
178+ # https://github.com/googleapis/gapic-generator-python/issues/2182
179+ if use_ads_templates :
180+ frag_files = [f for f in frag_files if f .name != "test_iam.proto" ]
181+ session .log ("Excluding test_iam.proto for Ads templates." )
194182
195183 is_parallel = os .environ .get ("PARALLEL_FRAGMENT_TESTS" , "" ).lower () == "true"
196184
197- def run_tests ( mypy_only = False ) :
198- """Helper to handle the parallel vs sequential toggle."""
199- tester = FragTester (session , use_ads_templates , mypy_only = mypy_only )
185+ with tempfile . TemporaryDirectory () as base_tmp :
186+ output_path = Path ( base_tmp )
187+ tester = FragTester (session , use_ads_templates , output_dir = output_path )
200188
189+ session .log (f"Generating { len (frag_files )} fragments in { output_path } ..." )
201190 if is_parallel :
202191 with ThreadPoolExecutor () as p :
203- results = p .map (tester , frag_files )
204- session .log ("" .join (results ))
192+ p .map (tester .generate , frag_files )
205193 else :
206194 for frag in frag_files :
207- session .log (tester (frag ))
195+ tester .generate (frag )
196+
197+ # We search the temporary directory for all generated unit test folders.
198+ # This allows pytest to run all tests in one go, which is much faster.
199+ session .log ("Running batch unit tests..." )
200+ unit_test_dirs = [str (p ) for p in output_path .glob ("**/tests/unit" )]
201+
202+ if unit_test_dirs :
203+ session .run (
204+ "pytest" ,
205+ "-n=auto" , # Use pytest-xdist to parallelize the tests themselves
206+ "--quiet" ,
207+ * unit_test_dirs ,
208+ * session .posargs , # Pass through any extra pytest args
209+ )
210+ else :
211+ session .log ("No unit tests found to run." )
208212
209- run_tests (mypy_only = False )
210- session .install ("mypy" , "types-protobuf" , "types-requests" )
211- run_tests (mypy_only = True )
213+ # Mypy runs once on the entire directory tree.
214+ session .log ("Running batch mypy..." )
215+ session .run (
216+ "mypy" ,
217+ str (output_path ),
218+ "--check-untyped-defs" ,
219+ "--ignore-missing-imports" ,
220+ )
212221
213222
214223@nox .session (python = ALL_PYTHON )
0 commit comments