diff --git a/.cloudbuild/library_generation/cloudbuild-test-library-generation.yaml b/.cloudbuild/library_generation/cloudbuild-test-library-generation.yaml deleted file mode 100644 index 6cdb333f19..0000000000 --- a/.cloudbuild/library_generation/cloudbuild-test-library-generation.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -timeout: 7200s # 2 hours -substitutions: - _TEST_IMAGE_ID: 'gcr.io/cloud-devrel-public-resources/java-library-generation:${COMMIT_SHA}' - -steps: - # Library generation build - - name: gcr.io/cloud-builders/docker - args: ["build", "-t", "${_TEST_IMAGE_ID}", "--file", ".cloudbuild/library_generation/library_generation.Dockerfile", "."] - id: library-generation-build - waitFor: ["-"] - - name: ${_TEST_IMAGE_ID} - entrypoint: bash - args: [ './library_generation/test/container_integration_tests.sh' ] - waitFor: [ "library-generation-build" ] - env: - - 'TEST_IMAGE_ID=${_TEST_IMAGE_ID}' - diff --git a/.github/workflows/verify_library_generation.yaml b/.github/workflows/verify_library_generation.yaml index b5707b443d..ca308461c0 100644 --- a/.github/workflows/verify_library_generation.yaml +++ b/.github/workflows/verify_library_generation.yaml @@ -48,8 +48,6 @@ jobs: shell: bash run: | set -x - git config --global user.email "github-workflow@github.com" - git config --global user.name "Github Workflow" python -m unittest library_generation/test/integration_tests.py unit_tests: strategy: diff --git a/library_generation/test/container_integration_tests.sh b/library_generation/test/container_integration_tests.sh deleted file mode 100644 index a60c733df9..0000000000 --- a/library_generation/test/container_integration_tests.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -# This is a wrapper of integration_tests.py that sets up the environment to run -# the script in a docker container - -set -xe -if [[ -z "${TEST_IMAGE_ID}" ]]; then - echo "required environemnt variable TEST_IMAGE_ID is not set" - exit 1 -fi - -repo_volumes="" -for repo in google-cloud-java java-bigtable; do - if [[ ! -d "${repo}" ]]; then - git clone "https://github.com/googleapis/${repo}" - fi - pushd "${repo}" - git reset --hard main - popd - - # We use a volume to hold the repositories used in the - # integration tests. This is because the test container creates a child - # container using the host machine's docker socket, meaning that we can only - # reference volumes created from within the host machine (i.e. the machine - # running this script) - # - # To summarize, we create a special volume that can be referenced both in the - # main container and in any child containers created by this one. - volume_name="repo-${repo}" - if [[ $(docker volume inspect "${volume_name}") != '[]' ]]; then - docker volume rm "${volume_name}" - fi - docker volume create \ - --name "${volume_name}" \ - --opt "type=none" \ - --opt "device=$(pwd)/${repo}" \ - --opt "o=bind" - - repo_volumes="${repo_volumes} -v ${volume_name}:/workspace/${repo}" -done - -docker run --rm \ - ${repo_volumes} \ - -v /tmp:/tmp \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -e "RUNNING_IN_DOCKER=true" \ - -e "REPO_BINDING_VOLUMES=${repo_volumes}" \ - -w "/src" \ - "${TEST_IMAGE_ID}" \ - python -m unittest /src/test/integration_tests.py diff --git a/library_generation/test/integration_tests.py b/library_generation/test/integration_tests.py old mode 100755 new mode 100644 index 9c1d84a33c..2acfa7d3b9 --- a/library_generation/test/integration_tests.py +++ b/library_generation/test/integration_tests.py @@ -12,34 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. import json +from filecmp import cmp +from filecmp import dircmp +from git import Repo import os import shutil +import subprocess import unittest from distutils.dir_util import copy_tree from distutils.file_util import copy_file -from filecmp import cmp -from filecmp import dircmp - -from git import Repo from pathlib import Path -from typing import List - -from library_generation.generate_pr_description import generate_pr_descriptions -from library_generation.generate_repo import generate_from_yaml -from library_generation.model.generation_config import from_yaml, GenerationConfig +from library_generation.model.generation_config import GenerationConfig +from library_generation.model.generation_config import from_yaml from library_generation.test.compare_poms import compare_xml -from library_generation.utils.utilities import ( - sh_util as shell_call, - run_process_and_print_output, -) +from library_generation.utils.utilities import sh_util as shell_call -config_name = "generation_config.yaml" script_dir = os.path.dirname(os.path.realpath(__file__)) -# for simplicity, the configuration files should be in a relative directory -# within config_dir named {repo}/generation_config.yaml, where repo is -# the name of the repository the target libraries live. -config_dir = f"{script_dir}/resources/integration" -golden_dir = f"{config_dir}/golden" +golden_dir = os.path.join(script_dir, "resources", "integration", "golden") +repo_root_dir = os.path.join(script_dir, "..", "..") +build_file = os.path.join( + repo_root_dir, ".cloudbuild", "library_generation", "library_generation.Dockerfile" +) +image_tag = "test-image:latest" repo_prefix = "https://github.com/googleapis" output_dir = shell_call("get_output_folder") # this map tells which branch of each repo should we use for our diff tests @@ -47,68 +41,54 @@ "google-cloud-java": "chore/test-hermetic-build", "java-bigtable": "chore/test-hermetic-build", } +config_dir = f"{script_dir}/resources/integration" +config_name = "generation_config.yaml" +monorepo_baseline_commit = "a17d4caf184b050d50cacf2b0d579ce72c31ce74" +split_repo_baseline_commit = "679060c64136e85b52838f53cfe612ce51e60d1d" class IntegrationTest(unittest.TestCase): - def test_get_commit_message_success(self): - repo_url = "https://github.com/googleapis/googleapis.git" - config_files = self.__get_config_files(config_dir) - monorepo_baseline_commit = "a17d4caf184b050d50cacf2b0d579ce72c31ce74" - split_repo_baseline_commit = "679060c64136e85b52838f53cfe612ce51e60d1d" - for repo, config_file in config_files: - baseline_commit = ( - monorepo_baseline_commit - if repo == "google-cloud-java" - else split_repo_baseline_commit - ) - description = generate_pr_descriptions( - generation_config_yaml=config_file, - repo_url=repo_url, - baseline_commit=baseline_commit, - ) - description_file = f"{config_dir}/{repo}/pr-description.txt" - if os.path.isfile(f"{description_file}"): - os.remove(f"{description_file}") - with open(f"{description_file}", "w+") as f: - f.write(description) - self.assertTrue( - cmp( - f"{config_dir}/{repo}/pr-description-golden.txt", - f"{description_file}", - ), - "The generated PR description does not match the expected golden file", - ) - os.remove(f"{description_file}") + def test_entry_point_running_in_container(self): + self.__build_image(docker_file=build_file, cwd=repo_root_dir) - def test_generate_repo(self): shutil.rmtree(f"{golden_dir}", ignore_errors=True) os.makedirs(f"{golden_dir}", exist_ok=True) config_files = self.__get_config_files(config_dir) for repo, config_file in config_files: config = from_yaml(config_file) + # 1. pull repository repo_dest = self.__pull_repo_to( Path(f"{output_dir}/{repo}"), repo, committish_map[repo] ) + # 2. prepare golden files library_names = self.__get_library_names_from_config(config) - # prepare golden files - for library_name in library_names: - if config.is_monorepo: - copy_tree( - f"{repo_dest}/{library_name}", f"{golden_dir}/{library_name}" - ) - copy_tree( - f"{repo_dest}/gapic-libraries-bom", - f"{golden_dir}/gapic-libraries-bom", - ) - copy_file(f"{repo_dest}/pom.xml", golden_dir) - else: - copy_tree(f"{repo_dest}", f"{golden_dir}/{library_name}") - generate_from_yaml( - generation_config_yaml=config_file, repository_path=repo_dest + self.__prepare_golden_files( + config=config, library_names=library_names, repo_dest=repo_dest ) - # compare result + # 3. bind repository and configuration to docker volumes + self.__bind_device_to_volumes( + volume_name=f"repo-{repo}", device_dir=f"{output_dir}/{repo}" + ) + self.__bind_device_to_volumes( + volume_name=f"config-{repo}", device_dir=f"{golden_dir}/../{repo}" + ) + repo_volumes = f"-v repo-{repo}:/workspace/{repo} -v config-{repo}:/workspace/config-{repo}" + # 4. run entry_point.py in docker container + baseline_commit = ( + monorepo_baseline_commit + if repo == "google-cloud-java" + else split_repo_baseline_commit + ) + self.__run_entry_point_in_docker_container( + repo=repo, + repo_volumes=repo_volumes, + baseline_commit=baseline_commit, + ) + # 5. compare generation result with golden files print( - "Generation finished successfully. Will now compare differences between generated and existing libraries" + "Generation finished successfully. " + "Will now compare differences between generated and existing " + "libraries" ) for library_name in library_names: actual_library = ( @@ -181,36 +161,35 @@ def test_generate_repo(self): ) ) print(" pom.xml comparison succeed.") + # compare PR description + description_file = f"{config_dir}/{repo}/pr_description.txt" + self.assertTrue( + cmp( + f"{config_dir}/{repo}/pr-description-golden.txt", + f"{description_file}", + ), + "The generated PR description does not match the expected golden file", + ) + print(" PR description comparison succeed.") @classmethod - def __pull_repo_to(cls, default_dest: Path, repo: str, committish: str) -> str: - if "RUNNING_IN_DOCKER" in os.environ: - # the docker image expects the repo to be in /workspace - dest_in_docker = f"/workspace/{repo}" - run_process_and_print_output( - [ - "git", - "config", - "--global", - "--add", - "safe.directory", - dest_in_docker, - ], - f"Add /workspace/{repo} to safe directories", - ) - dest = Path(dest_in_docker) - repo = Repo(dest) - else: - dest = default_dest - shutil.rmtree(dest, ignore_errors=True) - repo_url = f"{repo_prefix}/{repo}" - print(f"Cloning repository {repo_url}") - repo = Repo.clone_from(repo_url, dest) + def __build_image(cls, docker_file: str, cwd: str): + subprocess.check_call( + ["docker", "build", "--rm", "-f", docker_file, "-t", image_tag, "."], + cwd=cwd, + ) + + @classmethod + def __pull_repo_to(cls, dest: Path, repo: str, committish: str) -> str: + shutil.rmtree(dest, ignore_errors=True) + repo_url = f"{repo_prefix}/{repo}" + print(f"Cloning repository {repo_url}") + repo = Repo.clone_from(repo_url, dest) repo.git.checkout(committish) return str(dest) @classmethod - def __get_library_names_from_config(cls, config: GenerationConfig) -> List[str]: + def __get_library_names_from_config(cls, config: GenerationConfig) -> list[str]: library_names = [] for library in config.libraries: library_names.append(f"java-{library.get_library_name()}") @@ -218,7 +197,112 @@ def __get_library_names_from_config(cls, config: GenerationConfig) -> List[str]: return library_names @classmethod - def __get_config_files(cls, path: str) -> List[tuple[str, str]]: + def __prepare_golden_files( + cls, config: GenerationConfig, library_names: list[str], repo_dest: str + ): + for library_name in library_names: + if config.is_monorepo: + copy_tree(f"{repo_dest}/{library_name}", f"{golden_dir}/{library_name}") + copy_tree( + f"{repo_dest}/gapic-libraries-bom", + f"{golden_dir}/gapic-libraries-bom", + ) + copy_file(f"{repo_dest}/pom.xml", golden_dir) + else: + copy_tree(f"{repo_dest}", f"{golden_dir}/{library_name}") + + @classmethod + def __bind_device_to_volumes(cls, volume_name: str, device_dir: str): + # We use a volume to hold the repositories used in the integration + # tests. This is because the test container creates a child container + # using the host machine's docker socket, meaning that we can only + # reference volumes created from within the host machine (i.e. the + # machine running this script). + # + # To summarize, we create a special volume that can be referenced both + # in the main container and in any child containers created by this one. + + # use subprocess.run because we don't care about the return value (we + # want to remove the volume in any case). + subprocess.run(["docker", "volume", "rm", volume_name]) + subprocess.check_call( + [ + "docker", + "volume", + "create", + "--name", + volume_name, + "--opt", + "type=none", + "--opt", + f"device={device_dir}", + "--opt", + "o=bind", + ] + ) + + @classmethod + def __run_entry_point_in_docker_container( + cls, repo: str, repo_volumes: str, baseline_commit: str + ): + subprocess.check_call( + [ + "docker", + "run", + "--rm", + "-v", + f"repo-{repo}:/workspace/{repo}", + "-v", + f"config-{repo}:/workspace/config-{repo}", + "-v", + "/tmp:/tmp", + "-v", + "/var/run/docker.sock:/var/run/docker.sock", + "-e", + "RUNNING_IN_DOCKER=true", + "-e", + f"REPO_BINDING_VOLUMES={repo_volumes}", + "-w", + "/src", + image_tag, + "python", + "/src/generate_repo.py", + "generate", + f"--generation-config-yaml=/workspace/config-{repo}/{config_name}", + f"--repository-path=/workspace/{repo}", + ] + ) + + subprocess.check_call( + [ + "docker", + "run", + "--rm", + "-v", + f"repo-{repo}:/workspace/{repo}", + "-v", + f"config-{repo}:/workspace/config-{repo}", + "-v", + "/tmp:/tmp", + "-v", + "/var/run/docker.sock:/var/run/docker.sock", + "-e", + "RUNNING_IN_DOCKER=true", + "-e", + f"REPO_BINDING_VOLUMES={repo_volumes}", + "-w", + "/src", + image_tag, + "python", + "/src/generate_pr_description.py", + "generate", + f"--generation-config-yaml=/workspace/config-{repo}/{config_name}", + f"--baseline-commit={baseline_commit}", + ] + ) + + @classmethod + def __get_config_files(cls, path: str) -> list[tuple[str, str]]: config_files = [] for sub_dir in Path(path).resolve().iterdir(): if sub_dir.is_file(): @@ -228,7 +312,6 @@ def __get_config_files(cls, path: str) -> List[tuple[str, str]]: continue config = f"{sub_dir}/{config_name}" config_files.append((repo, config)) - return config_files @classmethod @@ -238,7 +321,7 @@ def __compare_json_files(cls, expected: str, actual: str) -> bool: ) == cls.__load_json_to_sorted_list(actual) @classmethod - def __load_json_to_sorted_list(cls, path: str) -> List[tuple]: + def __load_json_to_sorted_list(cls, path: str) -> list[tuple]: with open(path) as f: data = json.load(f) res = [(key, value) for key, value in data.items()] @@ -249,9 +332,9 @@ def __load_json_to_sorted_list(cls, path: str) -> List[tuple]: def __recursive_diff_files( cls, dcmp: dircmp, - diff_files: List[str], - left_only: List[str], - right_only: List[str], + diff_files: list[str], + left_only: list[str], + right_only: list[str], dirname: str = "", ): """