Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
110 changes: 109 additions & 1 deletion .generator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

BUILD_REQUEST_FILE = "build-request.json"
GENERATE_REQUEST_FILE = "generate-request.json"
CONFIGURE_REQUEST_FILE = "configure-request.json"
RELEASE_INIT_REQUEST_FILE = "release-init-request.json"
STATE_YAML_FILE = "state.yaml"

Expand Down Expand Up @@ -114,6 +115,103 @@ def _write_json_file(path: str, updated_content: Dict):
json.dump(updated_content, f, indent=2)
f.write("\n")

def _add_new_library_source_roots(library_config: Dict, library_id: str) -> None:
"""Adds the default source_roots to the library configuration if not present.

Args:
library_config(Dict): The library configuration.
library_id(str): The id of the library.
"""
if library_config["source_roots"] is None:
library_config["source_roots"] = [f"packages/{library_id}"]


def _add_new_library_preserve_regex(library_config: Dict, library_id: str) -> None:
"""Adds the default preserve_regex to the library configuration if not present.

Args:
library_config(Dict): The library configuration.
library_id(str): The id of the library.
"""
if library_config["preserve_regex"] is None:
library_config["preserve_regex"] = [
f"packages/{library_id}/CHANGELOG.md",
"docs/CHANGELOG.md",
"docs/README.rst",
"samples/README.txt",
"tar.gz",
"gapic_version.py",
"scripts/client-post-processing",
"samples/snippets/README.rst",
"tests/system",
]


def _add_new_library_remove_regex(library_config: Dict, library_id: str) -> None:
"""Adds the default remove_regex to the library configuration if not present.

Args:
library_config(Dict): The library configuration.
library_id(str): The id of the library.
"""
if library_config["remove_regex"] is None:
library_config["remove_regex"] = [f"packages/{library_id}"]


def _add_new_library_tag_format(library_config: Dict) -> None:
"""Adds the default tag_format to the library configuration if not present.

Args:
library_config(Dict): The library configuration.
"""
if "tag_format" not in library_config:
library_config["tag_format"] = "{{id}}-v{{version}}"


def _get_new_library_config(request_data: Dict) -> Dict:
"""Finds and returns the configuration for a new library.

Args:
request_data(Dict): The request data from which to extract the new
library config.

Returns:
Dict: The unmodified configuration of a new library, or an empty
dictionary if not found.
"""
for library_config in request_data.get("libraries", []):
all_apis = library_config.get("apis", [])
for api in all_apis:
if api.get("status") == "new":
return library_config
return {}


def _prepare_new_library_config(library_config: Dict) -> Dict:
"""
Prepares the new library's configuration by removing temporary keys and
adding default values.

Args:
library_config (Dict): The raw library configuration.

Returns:
Dict: The prepared library configuration.
"""
# remove status key from new library config.
all_apis = library_config.get("apis", [])
for api in all_apis:
if "status" in api:
del api["status"]

library_id = _get_library_id(library_config)
_add_new_library_source_roots(library_config, library_id)
_add_new_library_preserve_regex(library_config, library_id)
_add_new_library_remove_regex(library_config, library_id)
_add_new_library_tag_format(library_config)

return library_config


def handle_configure(
librarian: str = LIBRARIAN_DIR,
Expand Down Expand Up @@ -144,7 +242,17 @@ def handle_configure(
Raises:
ValueError: If configuring a new library fails.
"""
# TODO(https://github.com/googleapis/librarian/issues/466): Implement configure command and update docstring.
try:
# configure-request.json contains the library definitions.
request_data = _read_json_file(f"{librarian}/{CONFIGURE_REQUEST_FILE}")
new_library_config = _get_new_library_config(request_data)
prepared_config = _prepare_new_library_config(new_library_config)

# Write the new library configuration to configure-response.json.
_write_json_file(f"{librarian}/configure-response.json", prepared_config)

except Exception as e:
raise ValueError("Configuring a new library failed.") from e
logger.info("'configure' command executed.")


Expand Down
135 changes: 124 additions & 11 deletions .generator/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from cli import (
GENERATE_REQUEST_FILE,
BUILD_REQUEST_FILE,
CONFIGURE_REQUEST_FILE,
RELEASE_INIT_REQUEST_FILE,
SOURCE_DIR,
STATE_YAML_FILE,
Expand All @@ -43,7 +44,9 @@
_get_library_dist_name,
_get_library_id,
_get_libraries_to_prepare_for_release,
_get_new_library_config,
_get_previous_version,
_prepare_new_library_config,
_process_changelog,
_process_version_file,
_read_bazel_build_py_rule,
Expand Down Expand Up @@ -160,6 +163,35 @@ def mock_build_request_file(tmp_path, monkeypatch):
return request_file


@pytest.fixture
def mock_configure_request_data():
"""Returns mock data for configure-request.json."""
return {
"libraries": [
{
"id": "google-cloud-language",
"apis": [{"path": "google/cloud/language/v1", "status": "new"}],
}
]
}


@pytest.fixture
def mock_configure_request_file(tmp_path, monkeypatch, mock_configure_request_data):
"""Creates the mock request file at the correct path inside a temp dir."""
# Create the path as expected by the script: .librarian/configure-request.json
request_path = f"{LIBRARIAN_DIR}/{CONFIGURE_REQUEST_FILE}"
request_dir = tmp_path / os.path.dirname(request_path)
request_dir.mkdir(parents=True, exist_ok=True)
request_file = request_dir / os.path.basename(request_path)

request_file.write_text(json.dumps(mock_configure_request_data))

# Change the current working directory to the temp path for the test.
monkeypatch.chdir(tmp_path)
return request_file


@pytest.fixture
def mock_build_bazel_file(tmp_path, monkeypatch):
"""Creates the mock BUILD.bazel file at the correct path inside a temp dir."""
Expand Down Expand Up @@ -236,6 +268,98 @@ def mock_state_file(tmp_path, monkeypatch):
return request_file


def test_handle_configure_success(mock_configure_request_file, mocker):
"""Tests the successful execution path of handle_configure."""
mock_write_json = mocker.patch("cli._write_json_file")
mock_prepare_config = mocker.patch(
"cli._prepare_new_library_config", return_value={"id": "prepared"}
)

handle_configure()

mock_prepare_config.assert_called_once()
mock_write_json.assert_called_once_with(
f"{LIBRARIAN_DIR}/configure-response.json", {"id": "prepared"}
)


def test_handle_configure_no_new_library(mocker):
"""Tests that handle_configure fails if no new library is found."""
mocker.patch("cli._read_json_file", return_value={"libraries": []})
# The call to _prepare_new_library_config with an empty dict will raise a ValueError
# because _get_library_id will fail.
with pytest.raises(ValueError, match="Configuring a new library failed."):
handle_configure()


def test_get_new_library_config_found(mock_configure_request_data):
"""Tests that the new library configuration is returned when found."""
config = _get_new_library_config(mock_configure_request_data)
assert config["id"] == "google-cloud-language"
# Assert that the config is NOT modified
assert "status" in config["apis"][0]


def test_get_new_library_config_not_found():
"""Tests that an empty dictionary is returned when no new library is found."""
request_data = {
"libraries": [
{"id": "existing-library", "apis": [{"path": "path/v1", "status": "existing"}]},
]
}
config = _get_new_library_config(request_data)
assert config == {}


def test_get_new_library_config_empty_input():
"""Tests that an empty dictionary is returned for empty input."""
config = _get_new_library_config({})
assert config == {}


def test_prepare_new_library_config():
"""Tests the preparation of a new library's configuration."""
raw_config = {
"id": "google-cloud-language",
"apis": [{"path": "google/cloud/language/v1", "status": "new"}],
"source_roots": None,
"preserve_regex": None,
"remove_regex": None,
}

prepared_config = _prepare_new_library_config(raw_config)

# Check that status is removed
assert "status" not in prepared_config["apis"][0]
# Check that defaults are added
assert prepared_config["source_roots"] == ["packages/google-cloud-language"]
assert "packages/google-cloud-language/CHANGELOG.md" in prepared_config["preserve_regex"]
assert prepared_config["remove_regex"] == ["packages/google-cloud-language"]
assert prepared_config["tag_format"] == "{{id}}-v{{version}}"


def test_prepare_new_library_config_preserves_existing_values():
"""Tests that existing values in the config are not overwritten."""
raw_config = {
"id": "google-cloud-language",
"apis": [{"path": "google/cloud/language/v1", "status": "new"}],
"source_roots": ["packages/google-cloud-language-custom"],
"preserve_regex": ["custom/regex"],
"remove_regex": ["custom/remove"],
"tag_format": "custom-format-{{version}}",
}

prepared_config = _prepare_new_library_config(raw_config)

# Check that status is removed
assert "status" not in prepared_config["apis"][0]
# Check that existing values are preserved
assert prepared_config["source_roots"] == ["packages/google-cloud-language-custom"]
assert prepared_config["preserve_regex"] == ["custom/regex"]
assert prepared_config["remove_regex"] == ["custom/remove"]
assert prepared_config["tag_format"] == "custom-format-{{version}}"


def test_get_library_id_success():
"""Tests that _get_library_id returns the correct ID when present."""
request_data = {"id": "test-library", "name": "Test Library"}
Expand All @@ -261,17 +385,6 @@ def test_get_library_id_empty_id():
_get_library_id(request_data)


def test_handle_configure_success(caplog, mock_generate_request_file):
"""
Tests the successful execution path of handle_configure.
"""
caplog.set_level(logging.INFO)

handle_configure()

assert "'configure' command executed." in caplog.text


def test_run_post_processor_success(mocker, caplog):
"""
Tests that the post-processor helper calls the correct command.
Expand Down
Loading