|
36 | 36 | _clean_up_files_after_post_processing, |
37 | 37 | _copy_files_needed_for_post_processing, |
38 | 38 | _create_main_version_header, |
39 | | - _get_library_dist_name, |
| 39 | + _determine_generator_command, |
40 | 40 | _determine_library_namespace, |
| 41 | + _generate_api, |
| 42 | + _get_api_generator_options, |
| 43 | + _get_library_dist_name, |
41 | 44 | _get_library_id, |
42 | 45 | _get_libraries_to_prepare_for_release, |
43 | 46 | _get_previous_version, |
44 | 47 | _process_changelog, |
45 | 48 | _process_version_file, |
| 49 | + _read_bazel_build_py_rule, |
46 | 50 | _read_json_file, |
47 | 51 | _read_text_file, |
48 | 52 | _run_individual_session, |
| 53 | + _run_generator_command, |
49 | 54 | _run_nox_sessions, |
50 | 55 | _run_post_processor, |
51 | 56 | _update_changelog_for_library, |
|
93 | 98 | }, |
94 | 99 | ] |
95 | 100 |
|
| 101 | +_MOCK_BAZEL_CONTENT = """load( |
| 102 | + "@com_google_googleapis_imports//:imports.bzl", |
| 103 | + "py_gapic_assembly_pkg", |
| 104 | + "py_gapic_library", |
| 105 | + "py_test", |
| 106 | +) |
| 107 | +
|
| 108 | +py_gapic_library( |
| 109 | + name = "language_py_gapic", |
| 110 | + srcs = [":language_proto"], |
| 111 | + grpc_service_config = "language_grpc_service_config.json", |
| 112 | + rest_numeric_enums = True, |
| 113 | + service_yaml = "language_v1.yaml", |
| 114 | + transport = "grpc+rest", |
| 115 | + deps = [ |
| 116 | + ], |
| 117 | + opt_args = [ |
| 118 | + "python-gapic-namespace=google.cloud", |
| 119 | + ], |
| 120 | +)""" |
| 121 | + |
96 | 122 |
|
97 | 123 | @pytest.fixture |
98 | 124 | def mock_generate_request_file(tmp_path, monkeypatch): |
@@ -136,34 +162,13 @@ def mock_build_request_file(tmp_path, monkeypatch): |
136 | 162 |
|
137 | 163 | @pytest.fixture |
138 | 164 | def mock_build_bazel_file(tmp_path, monkeypatch): |
139 | | - """Creates the mock request file at the correct path inside a temp dir.""" |
140 | | - # Create the path as expected by the script: .librarian/build-request.json |
| 165 | + """Creates the mock BUILD.bazel file at the correct path inside a temp dir.""" |
141 | 166 | bazel_build_path = f"{SOURCE_DIR}/google/cloud/language/v1/BUILD.bazel" |
142 | 167 | bazel_build_dir = tmp_path / Path(bazel_build_path).parent |
143 | 168 | os.makedirs(bazel_build_dir, exist_ok=True) |
144 | 169 | build_bazel_file = bazel_build_dir / os.path.basename(bazel_build_path) |
145 | 170 |
|
146 | | - build_bazel_content = """load( |
147 | | - "@com_google_googleapis_imports//:imports.bzl", |
148 | | - "py_gapic_assembly_pkg", |
149 | | - "py_gapic_library", |
150 | | - "py_test", |
151 | | -) |
152 | | -
|
153 | | -py_gapic_library( |
154 | | - name = "language_py_gapic", |
155 | | - srcs = [":language_proto"], |
156 | | - grpc_service_config = "language_grpc_service_config.json", |
157 | | - rest_numeric_enums = True, |
158 | | - service_yaml = "language_v1.yaml", |
159 | | - transport = "grpc+rest", |
160 | | - deps = [ |
161 | | - ], |
162 | | - opt_args = [ |
163 | | - "python-gapic-namespace=google.cloud", |
164 | | - ], |
165 | | -)""" |
166 | | - build_bazel_file.write_text(build_bazel_content) |
| 171 | + build_bazel_file.write_text(_MOCK_BAZEL_CONTENT) |
167 | 172 | return build_bazel_file |
168 | 173 |
|
169 | 174 |
|
@@ -285,6 +290,145 @@ def test_run_post_processor_success(mocker, caplog): |
285 | 290 | assert "Python post-processor ran successfully." in caplog.text |
286 | 291 |
|
287 | 292 |
|
| 293 | +def test_read_bazel_build_py_rule_success(mocker, mock_build_bazel_file): |
| 294 | + """Tests successful reading and parsing of a valid BUILD.bazel file.""" |
| 295 | + api_path = "google/cloud/language/v1" |
| 296 | + # Use the empty string as the source path, since the fixture has set the CWD to the temporary root. |
| 297 | + source_dir = "source" |
| 298 | + |
| 299 | + mocker.patch("cli._read_text_file", return_value=_MOCK_BAZEL_CONTENT) |
| 300 | + # The fixture already creates the file, so we just need to call the function |
| 301 | + py_gapic_config = _read_bazel_build_py_rule(api_path, source_dir) |
| 302 | + |
| 303 | + assert ( |
| 304 | + "language_py_gapic" not in py_gapic_config |
| 305 | + ) # Only rule attributes should be returned |
| 306 | + assert py_gapic_config["grpc_service_config"] == "language_grpc_service_config.json" |
| 307 | + assert py_gapic_config["rest_numeric_enums"] is True |
| 308 | + assert py_gapic_config["transport"] == "grpc+rest" |
| 309 | + assert py_gapic_config["opt_args"] == ["python-gapic-namespace=google.cloud"] |
| 310 | + |
| 311 | + |
| 312 | +def test_read_bazel_build_py_rule_not_found(mocker): |
| 313 | + """Tests failure when the BUILD.bazel file is missing.""" |
| 314 | + mocker.patch("cli._read_text_file", side_effect=FileNotFoundError) |
| 315 | + with pytest.raises(ValueError, match="BUILD.bazel file not found"): |
| 316 | + _read_bazel_build_py_rule("non/existent/v1", "source") |
| 317 | + |
| 318 | + |
| 319 | +def test_get_api_generator_options_all_options(): |
| 320 | + """Tests option extraction when all relevant fields are present.""" |
| 321 | + api_path = "google/cloud/language/v1" |
| 322 | + py_gapic_config = { |
| 323 | + "grpc_service_config": "config.json", |
| 324 | + "rest_numeric_enums": True, |
| 325 | + "service_yaml": "service.yaml", |
| 326 | + "transport": "grpc+rest", |
| 327 | + "opt_args": ["single_arg", "another_arg"], |
| 328 | + } |
| 329 | + options = _get_api_generator_options(api_path, py_gapic_config) |
| 330 | + |
| 331 | + expected = [ |
| 332 | + "retry-config=google/cloud/language/v1/config.json", |
| 333 | + "rest-numeric-enums=True", |
| 334 | + "service-yaml=google/cloud/language/v1/service.yaml", |
| 335 | + "transport=grpc+rest", |
| 336 | + "single_arg", |
| 337 | + "another_arg", |
| 338 | + ] |
| 339 | + assert sorted(options) == sorted(expected) |
| 340 | + |
| 341 | + |
| 342 | +def test_get_api_generator_options_minimal_options(): |
| 343 | + """Tests option extraction when only transport is present.""" |
| 344 | + api_path = "google/cloud/minimal/v1" |
| 345 | + py_gapic_config = { |
| 346 | + "transport": "grpc", |
| 347 | + } |
| 348 | + options = _get_api_generator_options(api_path, py_gapic_config) |
| 349 | + |
| 350 | + expected = ["transport=grpc"] |
| 351 | + assert options == expected |
| 352 | + |
| 353 | + |
| 354 | +def test_determine_generator_command_with_options(): |
| 355 | + """Tests command construction with options.""" |
| 356 | + api_path = "google/cloud/test/v1" |
| 357 | + tmp_dir = "/tmp/output/test" |
| 358 | + options = ["transport=grpc", "custom_option=foo"] |
| 359 | + command = _determine_generator_command(api_path, tmp_dir, options) |
| 360 | + |
| 361 | + expected_options = "--python_gapic_opt=metadata,transport=grpc,custom_option=foo" |
| 362 | + expected_command = ( |
| 363 | + f"protoc {api_path}/*.proto --python_gapic_out={tmp_dir} {expected_options}" |
| 364 | + ) |
| 365 | + assert command == expected_command |
| 366 | + |
| 367 | + |
| 368 | +def test_determine_generator_command_no_options(): |
| 369 | + """Tests command construction without extra options.""" |
| 370 | + api_path = "google/cloud/test/v1" |
| 371 | + tmp_dir = "/tmp/output/test" |
| 372 | + options = [] |
| 373 | + command = _determine_generator_command(api_path, tmp_dir, options) |
| 374 | + |
| 375 | + # Note: 'metadata' is always included if options list is empty or not |
| 376 | + # only if `generator_options` is not empty. If it is empty, the result is: |
| 377 | + expected_command_no_options = ( |
| 378 | + f"protoc {api_path}/*.proto --python_gapic_out={tmp_dir}" |
| 379 | + ) |
| 380 | + assert command == expected_command_no_options |
| 381 | + |
| 382 | + |
| 383 | +def test_run_generator_command_success(mocker): |
| 384 | + """Tests successful execution of the protoc command.""" |
| 385 | + mock_run = mocker.patch( |
| 386 | + "cli.subprocess.run", return_value=MagicMock(stdout="ok", stderr="", check=True) |
| 387 | + ) |
| 388 | + command = "protoc api/*.proto --python_gapic_out=/tmp/out" |
| 389 | + source = "/src" |
| 390 | + |
| 391 | + _run_generator_command(command, source) |
| 392 | + |
| 393 | + mock_run.assert_called_once_with( |
| 394 | + [command], cwd=source, shell=True, check=True, capture_output=True, text=True |
| 395 | + ) |
| 396 | + |
| 397 | + |
| 398 | +def test_run_generator_command_failure(mocker): |
| 399 | + """Tests failure when protoc command returns a non-zero exit code.""" |
| 400 | + mock_run = mocker.patch( |
| 401 | + "cli.subprocess.run", |
| 402 | + side_effect=subprocess.CalledProcessError(1, "protoc", stderr="error"), |
| 403 | + ) |
| 404 | + command = "protoc api/*.proto --python_gapic_out=/tmp/out" |
| 405 | + source = "/src" |
| 406 | + |
| 407 | + with pytest.raises(subprocess.CalledProcessError): |
| 408 | + _run_generator_command(command, source) |
| 409 | + |
| 410 | + |
| 411 | +def test_generate_api_success(mocker, caplog): |
| 412 | + caplog.set_level(logging.INFO) |
| 413 | + |
| 414 | + API_PATH = "google/cloud/language/v1" |
| 415 | + LIBRARY_ID = "google-cloud-language" |
| 416 | + SOURCE = "source" |
| 417 | + OUTPUT = "output" |
| 418 | + |
| 419 | + mock_run_post_processor = mocker.patch("cli._read_bazel_build_py_rule") |
| 420 | + mock_run_post_processor = mocker.patch("cli._get_api_generator_options") |
| 421 | + mock_copy_files_needed_for_post_processing = mocker.patch( |
| 422 | + "cli._determine_generator_command" |
| 423 | + ) |
| 424 | + mock_copy_files_needed_for_post_processing = mocker.patch( |
| 425 | + "cli._run_generator_command" |
| 426 | + ) |
| 427 | + mock_clean_up_files_after_post_processing = mocker.patch("shutil.copytree") |
| 428 | + |
| 429 | + _generate_api(API_PATH, LIBRARY_ID, SOURCE, OUTPUT) |
| 430 | + |
| 431 | + |
288 | 432 | def test_handle_generate_success( |
289 | 433 | caplog, mock_generate_request_file, mock_build_bazel_file, mocker |
290 | 434 | ): |
|
0 commit comments