Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .generator/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ RUN apt-get update && \
# For downloading secure files
wget \
ca-certificates \
# For running bazelisk commands
openjdk-17-jdk \
# --- Critical libraries for a complete Python build ---
libssl-dev \
zlib1g-dev \
Expand All @@ -35,6 +37,7 @@ RUN apt-get update && \

# Set up environment variables for tool versions to make updates easier.
ENV PYTHON_VERSION=3.11.5
ENV BAZELISK_VERSION=v1.26.0

# Create a symbolic link for `python3` to point to our specific version.
ENV PATH /usr/local/bin/python3.11:$PATH
Expand All @@ -50,7 +53,9 @@ RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VER

# TODO(https://github.com/googleapis/librarian/issues/904): Install protoc for gencode.

# TODO(https://github.com/googleapis/librarian/issues/903): Install Bazelisk for building a client library.
# Install Bazelisk
RUN wget https://github.com/bazelbuild/bazelisk/releases/download/${BAZELISK_VERSION}/bazelisk-linux-amd64 -O /usr/local/bin/bazelisk && \
chmod +x /usr/local/bin/bazelisk

# TODO(https://github.com/googleapis/librarian/issues/902): Create a dedicate non-root user and
# switch to the non-root user to run subsequent commands.
Expand Down
51 changes: 46 additions & 5 deletions .generator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
import logging
import os
import sys
import subprocess

logger = logging.getLogger()

LIBRARIAN_DIR = "librarian"
GENERATE_REQUEST_FILE = "generate-request.json"
SOURCE_DIR = "source"


def _read_json_file(path):
Expand All @@ -47,6 +49,39 @@ def handle_configure():
logger.info("'configure' command executed.")


def _determine_bazel_rule(api_path):
"""Executes a `bazelisk query` to find a Bazel rule.

Args:
api_path (str): The API path to query for.

Returns:
str: The discovered Bazel rule.

Raises:
Exception: If the subprocess call fails or returns an empty result.
"""
logger.info(f"Determining Bazel rule for api_path: '{api_path}'")
try:
query = f'filter("-py$", kind("rule", //{api_path}/...:*))'
command = ["bazelisk", "query", query]
result = subprocess.run(
command,
cwd=f"{SOURCE_DIR}/googleapis",
capture_output=True,
text=True,
check=True,
)
bazel_rule = result.stdout.strip()
if not bazel_rule:
raise ValueError(f"Bazel query `{query}` returned an empty bazel rule.")

logger.info(f"Found Bazel rule: {bazel_rule}")
return bazel_rule
except Exception as e:
raise ValueError(f"Bazelisk query `{query}` failed") from e


def handle_generate():
"""The main coordinator for the code generation process.

Expand All @@ -61,12 +96,18 @@ def handle_generate():
# Read a generate-request.json file
try:
request_data = _read_json_file(f"{LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}")
except Exception as e:
raise ValueError(
f"failed to read {LIBRARIAN_DIR}/{GENERATE_REQUEST_FILE}"
) from e
library_id = request_data.get("id")
if not library_id:
raise ValueError("Request file is missing required 'id' field.")

for api in request_data.get("apis", []):
api_path = api.get("path")
if api_path:
bazel_rule = _determine_bazel_rule(api_path)

logger.info(json.dumps(request_data, indent=2))
logger.info(json.dumps(request_data, indent=2))
except Exception as e:
raise ValueError("Generation failed.") from e

# TODO(https://github.com/googleapis/librarian/issues/448): Implement generate command and update docstring.
logger.info("'generate' command executed.")
Expand Down
45 changes: 41 additions & 4 deletions .generator/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import pytest
import json
import logging
import subprocess

from unittest.mock import mock_open
from unittest.mock import mock_open, MagicMock

from cli import (
_read_json_file,
_determine_bazel_rule,
handle_generate,
handle_build,
handle_configure,
Expand Down Expand Up @@ -60,16 +62,51 @@ def test_handle_configure_success(caplog, mock_generate_request_file):
assert "'configure' command executed." in caplog.text


def test_handle_generate_success(caplog, mock_generate_request_file):
def test_determine_bazel_rule_success(mocker, caplog):
"""
Tests the happy path of _determine_bazel_rule.
"""
caplog.set_level(logging.INFO)
mock_result = MagicMock(
stdout="//google/cloud/language/v1:google-cloud-language-v1-py\n"
)
mocker.patch("cli.subprocess.run", return_value=mock_result)

rule = _determine_bazel_rule("google/cloud/language/v1")

assert rule == "//google/cloud/language/v1:google-cloud-language-v1-py"
assert "Found Bazel rule" in caplog.text


def test_determine_bazel_rule_command_fails(mocker, caplog):
"""
Tests that an exception is raised if the subprocess command fails.
"""
caplog.set_level(logging.INFO)
mocker.patch(
"cli.subprocess.run",
side_effect=subprocess.CalledProcessError(1, "cmd", stderr="Bazel error"),
)

with pytest.raises(ValueError):
_determine_bazel_rule("google/cloud/language/v1")

assert "Found Bazel rule" not in caplog.text


def test_handle_generate_success(caplog, mock_generate_request_file, mocker):
"""
Tests the successful execution path of handle_generate.
"""
caplog.set_level(logging.INFO)

mock_determine_rule = mocker.patch(
"cli._determine_bazel_rule", return_value="mock-rule"
)

handle_generate()

assert "google-cloud-language" in caplog.text
assert "'generate' command executed." in caplog.text
mock_determine_rule.assert_called_once_with("google/cloud/language/v1")


def test_handle_generate_fail(caplog):
Expand Down
Loading