Skip to content

Commit fe18bf6

Browse files
committed
add tests
1 parent 097a54c commit fe18bf6

File tree

2 files changed

+176
-34
lines changed

2 files changed

+176
-34
lines changed

.generator/cli.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,6 @@ def handle_generate(
275275
logger.info("'generate' command executed.")
276276

277277

278-
279278
def _read_bazel_build_py_rule(api_path: str, source: str) -> Dict:
280279
"""
281280
Reads and parses the BUILD.bazel file to find the Python GAPIC rule content.
@@ -299,9 +298,6 @@ def _read_bazel_build_py_rule(api_path: str, source: str) -> Dict:
299298
result = parse_googleapis_content.parse_content(content)
300299
py_gapic_entries = [key for key in result.keys() if key.endswith("_py_gapic")]
301300

302-
if not py_gapic_entries:
303-
raise ValueError(f"No '_py_gapic' rule found in {build_file_path}")
304-
305301
# Assuming only one _py_gapic rule per BUILD file for a given language
306302
return result[py_gapic_entries[0]]
307303

@@ -346,7 +342,9 @@ def _get_api_generator_options(api_path: str, py_gapic_config: Dict) -> List[str
346342
return generator_options
347343

348344

349-
def _determine_generator_command(api_path: str, tmp_dir: str, generator_options: List[str]) -> str:
345+
def _determine_generator_command(
346+
api_path: str, tmp_dir: str, generator_options: List[str]
347+
) -> str:
350348
"""
351349
Constructs the full protoc command string.
352350
@@ -387,7 +385,7 @@ def _run_generator_command(generator_command: str, source: str):
387385
shell=True,
388386
check=True,
389387
capture_output=True,
390-
text=True
388+
text=True,
391389
)
392390

393391

@@ -405,12 +403,12 @@ def _generate_api(api_path: str, library_id: str, source: str, output: str):
405403
generator_options = _get_api_generator_options(api_path, py_gapic_config)
406404

407405
with tempfile.TemporaryDirectory() as tmp_dir:
408-
generator_command = _determine_generator_command(api_path, tmp_dir, generator_options)
406+
generator_command = _determine_generator_command(
407+
api_path, tmp_dir, generator_options
408+
)
409409
_run_generator_command(generator_command, source)
410410
api_version = api_path.split("/")[-1]
411-
staging_dir = os.path.join(
412-
output, "owl-bot-staging", library_id, api_version
413-
)
411+
staging_dir = os.path.join(output, "owl-bot-staging", library_id, api_version)
414412
shutil.copytree(tmp_dir, staging_dir)
415413

416414

.generator/test_cli.py

Lines changed: 168 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,21 @@
3636
_clean_up_files_after_post_processing,
3737
_copy_files_needed_for_post_processing,
3838
_create_main_version_header,
39-
_get_library_dist_name,
39+
_determine_generator_command,
4040
_determine_library_namespace,
41+
_generate_api,
42+
_get_api_generator_options,
43+
_get_library_dist_name,
4144
_get_library_id,
4245
_get_libraries_to_prepare_for_release,
4346
_get_previous_version,
4447
_process_changelog,
4548
_process_version_file,
49+
_read_bazel_build_py_rule,
4650
_read_json_file,
4751
_read_text_file,
4852
_run_individual_session,
53+
_run_generator_command,
4954
_run_nox_sessions,
5055
_run_post_processor,
5156
_update_changelog_for_library,
@@ -93,6 +98,27 @@
9398
},
9499
]
95100

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+
96122

97123
@pytest.fixture
98124
def mock_generate_request_file(tmp_path, monkeypatch):
@@ -136,34 +162,13 @@ def mock_build_request_file(tmp_path, monkeypatch):
136162

137163
@pytest.fixture
138164
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."""
141166
bazel_build_path = f"{SOURCE_DIR}/google/cloud/language/v1/BUILD.bazel"
142167
bazel_build_dir = tmp_path / Path(bazel_build_path).parent
143168
os.makedirs(bazel_build_dir, exist_ok=True)
144169
build_bazel_file = bazel_build_dir / os.path.basename(bazel_build_path)
145170

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)
167172
return build_bazel_file
168173

169174

@@ -285,6 +290,145 @@ def test_run_post_processor_success(mocker, caplog):
285290
assert "Python post-processor ran successfully." in caplog.text
286291

287292

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+
288432
def test_handle_generate_success(
289433
caplog, mock_generate_request_file, mock_build_bazel_file, mocker
290434
):

0 commit comments

Comments
 (0)