Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: add a CLI tool to validate generation configuration #2691

Merged
merged 11 commits into from
Apr 26, 2024
24 changes: 24 additions & 0 deletions library_generation/cli/entry_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys

import click as click
from library_generation.generate_pr_description import generate_pr_descriptions
Expand Down Expand Up @@ -142,5 +143,28 @@ def generate(
)


@main.command()
@click.option(
"--generation-config-path",
required=False,
type=str,
help="""
Absolute or relative path to a generation_config.yaml.
Default to generation_config.yaml in the current working directory.
""",
)
def validate_generation_config(generation_config_path: str) -> None:
"""
Validate the given generation configuration.
"""
if generation_config_path is None:
generation_config_path = "generation_config.yaml"
try:
from_yaml(os.path.abspath(generation_config_path))
except ValueError as err:
print(err)
sys.exit(1)


if __name__ == "__main__":
main()
51 changes: 41 additions & 10 deletions library_generation/model/generation_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
from library_generation.model.library_config import LibraryConfig
from library_generation.model.gapic_config import GapicConfig

REPO_LEVEL_PARAMETER = "Repo level parameter"
LIBRARY_LEVEL_PARAMETER = "Library level parameter"
GAPIC_LEVEL_PARAMETER = "GAPIC level parameter"


class GenerationConfig:
"""
Expand Down Expand Up @@ -46,6 +50,7 @@ def __init__(
self.libraries = libraries
self.grpc_version = grpc_version
self.protoc_version = protoc_version
self.__validate()

def get_proto_path_to_library_name(self) -> dict[str, str]:
"""
Expand All @@ -62,6 +67,19 @@ def get_proto_path_to_library_name(self) -> dict[str, str]:
def is_monorepo(self) -> bool:
return len(self.libraries) > 1

def __validate(self) -> None:
seen_library_names = dict()
for library in self.libraries:
library_name = library.get_library_name()
if library_name in seen_library_names:
raise ValueError(
f"Both {library.name_pretty} and "
f"{seen_library_names.get(library_name)} have the same "
f"library name: {library_name}, please update one of the "
f"library to have a different library name."
)
seen_library_names[library_name] = library.name_pretty


def from_yaml(path_to_yaml: str) -> GenerationConfig:
"""
Expand All @@ -72,15 +90,15 @@ def from_yaml(path_to_yaml: str) -> GenerationConfig:
with open(path_to_yaml, "r") as file_stream:
config = yaml.safe_load(file_stream)

libraries = __required(config, "libraries")
libraries = __required(config, "libraries", REPO_LEVEL_PARAMETER)

parsed_libraries = list()
for library in libraries:
gapics = __required(library, "GAPICs")

parsed_gapics = list()
for gapic in gapics:
proto_path = __required(gapic, "proto_path")
proto_path = __required(gapic, "proto_path", GAPIC_LEVEL_PARAMETER)
new_gapic = GapicConfig(proto_path)
parsed_gapics.append(new_gapic)

Expand Down Expand Up @@ -114,23 +132,36 @@ def from_yaml(path_to_yaml: str) -> GenerationConfig:
parsed_libraries.append(new_library)

parsed_config = GenerationConfig(
gapic_generator_version=__required(config, "gapic_generator_version"),
gapic_generator_version=__required(
config, "gapic_generator_version", REPO_LEVEL_PARAMETER
),
grpc_version=__optional(config, "grpc_version", None),
protoc_version=__optional(config, "protoc_version", None),
googleapis_commitish=__required(config, "googleapis_commitish"),
libraries_bom_version=__required(config, "libraries_bom_version"),
owlbot_cli_image=__required(config, "owlbot_cli_image"),
synthtool_commitish=__required(config, "synthtool_commitish"),
template_excludes=__required(config, "template_excludes"),
googleapis_commitish=__required(
config, "googleapis_commitish", REPO_LEVEL_PARAMETER
),
libraries_bom_version=__required(
config, "libraries_bom_version", REPO_LEVEL_PARAMETER
),
owlbot_cli_image=__required(config, "owlbot_cli_image", REPO_LEVEL_PARAMETER),
synthtool_commitish=__required(
config, "synthtool_commitish", REPO_LEVEL_PARAMETER
),
template_excludes=__required(config, "template_excludes", REPO_LEVEL_PARAMETER),
libraries=parsed_libraries,
)

return parsed_config


def __required(config: Dict, key: str):
def __required(config: Dict, key: str, level: str = LIBRARY_LEVEL_PARAMETER):
if key not in config:
raise ValueError(f"required key {key} not found in yaml")
message = (
f"{level}, {key}, is not found in {config} in yaml."
blakeli0 marked this conversation as resolved.
Show resolved Hide resolved
if level != REPO_LEVEL_PARAMETER
else f"{level}, {key}, is not found in yaml."
)
raise ValueError(message)
return config[key]


Expand Down
35 changes: 34 additions & 1 deletion library_generation/test/cli/entry_point_unit_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import unittest
from click.testing import CliRunner
from library_generation.cli.entry_point import generate
from library_generation.cli.entry_point import generate, validate_generation_config

script_dir = os.path.dirname(os.path.realpath(__file__))
test_resource_dir = os.path.join(script_dir, "..", "resources", "test-config")


class EntryPointTest(unittest.TestCase):
Expand Down Expand Up @@ -44,3 +48,32 @@ def test_entry_point_with_baseline_without_current_raise_file_exception(self):
"current_generation_config is not specified when "
"baseline_generation_config is specified.",
)

def test_validate_generation_config_succeeds(
self,
):
runner = CliRunner()
# noinspection PyTypeChecker
result = runner.invoke(
validate_generation_config,
[f"--generation-config-path={test_resource_dir}/generation_config.yaml"],
)
self.assertEqual(0, result.exit_code)

def test_validate_generation_config_with_duplicate_library_name_raise_file_exception(
self,
):
runner = CliRunner()
# noinspection PyTypeChecker
result = runner.invoke(
validate_generation_config,
[
f"--generation-config-path={test_resource_dir}/generation_config_with_duplicate_library_name.yaml"
],
)
self.assertEqual(1, result.exit_code)
self.assertEqual(SystemExit, result.exc_info[0])
self.assertRegex(
result.output,
"have the same library name",
)
134 changes: 134 additions & 0 deletions library_generation/test/model/generation_config_unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,137 @@ def test_is_monorepo_with_two_libraries_returns_true(self):
libraries=[library_1, library_2],
)
self.assertTrue(config.is_monorepo())

def test_validate_with_duplicate_library_name_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"the same library name",
GenerationConfig,
gapic_generator_version="",
googleapis_commitish="",
libraries_bom_version="",
owlbot_cli_image="",
synthtool_commitish="",
template_excludes=[],
libraries=[
LibraryConfig(
api_shortname="secretmanager",
name_pretty="Secret API",
product_documentation="",
api_description="",
gapic_configs=list(),
),
LibraryConfig(
api_shortname="another-secret",
name_pretty="Another Secret API",
product_documentation="",
api_description="",
gapic_configs=list(),
library_name="secretmanager",
),
],
)

def test_from_yaml_without_gapic_generator_version_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Repo level parameter, gapic_generator_version",
from_yaml,
f"{test_config_dir}/config_without_generator.yaml",
)

def test_from_yaml_without_googleapis_commitish_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Repo level parameter, googleapis_commitish",
from_yaml,
f"{test_config_dir}/config_without_googleapis.yaml",
)

def test_from_yaml_without_libraries_bom_version_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Repo level parameter, libraries_bom_version",
from_yaml,
f"{test_config_dir}/config_without_libraries_bom_version.yaml",
)

def test_from_yaml_without_owlbot_cli_image_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Repo level parameter, owlbot_cli_image",
from_yaml,
f"{test_config_dir}/config_without_owlbot.yaml",
)

def test_from_yaml_without_synthtool_commitish_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Repo level parameter, synthtool_commitish",
from_yaml,
f"{test_config_dir}/config_without_synthtool.yaml",
)

def test_from_yaml_without_template_excludes_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Repo level parameter, template_excludes",
from_yaml,
f"{test_config_dir}/config_without_temp_excludes.yaml",
)

def test_from_yaml_without_libraries_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Repo level parameter, libraries",
from_yaml,
f"{test_config_dir}/config_without_libraries.yaml",
)

def test_from_yaml_without_api_shortname_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Library level parameter, api_shortname",
from_yaml,
f"{test_config_dir}/config_without_api_shortname.yaml",
)

def test_from_yaml_without_api_description_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Library level parameter, api_description",
from_yaml,
f"{test_config_dir}/config_without_api_description.yaml",
)

def test_from_yaml_without_name_pretty_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Library level parameter, name_pretty",
from_yaml,
f"{test_config_dir}/config_without_name_pretty.yaml",
)

def test_from_yaml_without_product_documentation_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Library level parameter, product_documentation",
from_yaml,
f"{test_config_dir}/config_without_product_docs.yaml",
)

def test_from_yaml_without_gapics_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"Library level parameter, GAPICs",
from_yaml,
f"{test_config_dir}/config_without_gapics.yaml",
)

def test_from_yaml_without_proto_path_raise_exception(self):
self.assertRaisesRegex(
ValueError,
"GAPIC level parameter, proto_path",
from_yaml,
f"{test_config_dir}/config_without_proto_path.yaml",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
gapic_generator_version: 2.34.0
protoc_version: 25.2
googleapis_commitish: 1a45bf7393b52407188c82e63101db7dc9c72026
libraries_bom_version: 26.37.0
owlbot_cli_image: sha256:623647ee79ac605858d09e60c1382a716c125fb776f69301b72de1cd35d49409
synthtool_commitish: 6612ab8f3afcd5e292aecd647f0fa68812c9f5b5
template_excludes:
- ".github/*"
- ".kokoro/*"
- "samples/*"
- "CODE_OF_CONDUCT.md"
- "CONTRIBUTING.md"
- "LICENSE"
- "SECURITY.md"
- "java.header"
- "license-checks.xml"
- "renovate.json"
- ".gitignore"
libraries:
- api_shortname: cloudasset
name_pretty: Cloud Asset
product_documentation: "https://cloud.google.com/resource-manager/docs/cloud-asset-inventory/overview"
api_description: "provides inventory services based on a time series database."
library_name: "asset"
client_documentation: "https://cloud.google.com/java/docs/reference/google-cloud-asset/latest/overview"
distribution_name: "com.google.cloud:google-cloud-asset"
release_level: "stable"
issue_tracker: "https://issuetracker.google.com/issues/new?component=187210&template=0"
api_reference: "https://cloud.google.com/resource-manager/docs/cloud-asset-inventory/overview"
codeowner_team: "@googleapis/analytics-dpe"
excluded_poms: proto-google-iam-v1-bom,google-iam-policy,proto-google-iam-v1
excluded_dependencies: google-iam-policy
GAPICs:
- proto_path: google/cloud/asset/v1
- proto_path: google/cloud/asset/v1p1beta1
- proto_path: google/cloud/asset/v1p2beta1
- proto_path: google/cloud/asset/v1p5beta1
- proto_path: google/cloud/asset/v1p7beta1

- api_shortname: another-cloudasset
name_pretty: Cloud Asset Inventory
product_documentation: "https://cloud.google.com/resource-manager/docs/cloud-asset-inventory/overview"
api_description: "provides inventory services based on a time series database."
library_name: "asset"
client_documentation: "https://cloud.google.com/java/docs/reference/google-cloud-asset/latest/overview"
distribution_name: "com.google.cloud:google-cloud-asset"
release_level: "stable"
issue_tracker: "https://issuetracker.google.com/issues/new?component=187210&template=0"
api_reference: "https://cloud.google.com/resource-manager/docs/cloud-asset-inventory/overview"
GAPICs:
- proto_path: google/cloud/asset/v1
Loading
Loading