Skip to content

Commit 40e2551

Browse files
committed
intro logic to amend .owlbot-hermetic.yaml created from template with config.
1 parent b756ba5 commit 40e2551

File tree

3 files changed

+275
-12
lines changed

3 files changed

+275
-12
lines changed

hermetic_build/library_generation/tests/utilities_unit_tests.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
from common.model.gapic_inputs import GapicInputs
2727
from common.model.generation_config import GenerationConfig
2828
from common.model.library_config import LibraryConfig
29+
from common.model.owlbot_yaml_config import (
30+
OwlbotYamlConfig,
31+
OwlbotYamlAdditionRemoval,
32+
DeepCopyRegexItem,
33+
)
2934
from library_generation.tests.test_utils import FileComparator
3035
from library_generation.tests.test_utils import cleanup
3136

@@ -73,6 +78,18 @@ class UtilitiesTest(unittest.TestCase):
7378
Unit tests for utilities.py
7479
"""
7580

81+
content = """
82+
deep-remove-regex:
83+
- "/java-connectgateway/proto-google-.*/src"
84+
- "/java-connectgateway/google-.*/src"
85+
deep-preserve-regex:
86+
- "/java-connectgateway/google-.*/src/test/java/com/google/cloud/.*/v.*/it/IT.*Test.java"
87+
deep-copy-regex:
88+
- source: "/google/cloud/gkeconnect/gateway/(v.*)/.*-java/proto-google-.*/src"
89+
dest: "/owl-bot-staging/java-connectgateway/$1/proto-google-cloud-connectgateway-$1/src"
90+
api-name: connectgateway
91+
"""
92+
7693
CONFIGURATION_YAML_PATH = os.path.join(
7794
script_dir,
7895
"resources",
@@ -302,6 +319,104 @@ def test_prepare_repo_split_repo_success(self):
302319
self.assertEqual(["misc"], library_path)
303320
shutil.rmtree(repo_config.output_folder)
304321

322+
def test_apply_owlbot_config_remove_deep_remove_and_preserve(self):
323+
config = OwlbotYamlConfig(
324+
removals=OwlbotYamlAdditionRemoval(
325+
deep_remove_regex=["/java-connectgateway/proto-google-.*/src"],
326+
deep_preserve_regex=[
327+
"/java-connectgateway/google-.*/src/test/java/com/google/cloud/.*/v.*/it/IT.*Test.java"
328+
],
329+
)
330+
)
331+
expected_content = """
332+
deep-remove-regex:
333+
- "/java-connectgateway/google-.*/src"
334+
deep-preserve-regex:
335+
deep-copy-regex:
336+
- source: "/google/cloud/gkeconnect/gateway/(v.*)/.*-java/proto-google-.*/src"
337+
dest: "/owl-bot-staging/java-connectgateway/$1/proto-google-cloud-connectgateway-$1/src"
338+
api-name: connectgateway
339+
"""
340+
self.assertEqual(
341+
util.apply_owlbot_config(self.content, config), expected_content
342+
)
343+
344+
def test_apply_owlbot_config_remove_deep_copy_regex(self):
345+
item1 = DeepCopyRegexItem(
346+
source="/google/cloud/gkeconnect/gateway/(v.*)/.*-java/proto-google-.*/src",
347+
dest="/owl-bot-staging/java-connectgateway/$1/proto-google-cloud-connectgateway-$1/src",
348+
)
349+
350+
config = OwlbotYamlConfig(
351+
removals=OwlbotYamlAdditionRemoval(deep_copy_regex=[item1])
352+
)
353+
expected_content = """
354+
deep-remove-regex:
355+
- "/java-connectgateway/proto-google-.*/src"
356+
- "/java-connectgateway/google-.*/src"
357+
deep-preserve-regex:
358+
- "/java-connectgateway/google-.*/src/test/java/com/google/cloud/.*/v.*/it/IT.*Test.java"
359+
deep-copy-regex:
360+
api-name: connectgateway
361+
"""
362+
self.assertEqual(
363+
util.apply_owlbot_config(self.content, config), expected_content
364+
)
365+
366+
def test_apply_owlbot_config_add_deep_remove_and_preserve(self):
367+
config = OwlbotYamlConfig(
368+
additions=OwlbotYamlAdditionRemoval(
369+
deep_remove_regex=["/new/path"],
370+
deep_preserve_regex=["/new/path/to/preserve"],
371+
)
372+
)
373+
expected_content = """
374+
deep-remove-regex:
375+
- "/new/path"
376+
- "/java-connectgateway/proto-google-.*/src"
377+
- "/java-connectgateway/google-.*/src"
378+
deep-preserve-regex:
379+
- "/new/path/to/preserve"
380+
- "/java-connectgateway/google-.*/src/test/java/com/google/cloud/.*/v.*/it/IT.*Test.java"
381+
deep-copy-regex:
382+
- source: "/google/cloud/gkeconnect/gateway/(v.*)/.*-java/proto-google-.*/src"
383+
dest: "/owl-bot-staging/java-connectgateway/$1/proto-google-cloud-connectgateway-$1/src"
384+
api-name: connectgateway
385+
"""
386+
self.assertEqual(
387+
util.apply_owlbot_config(self.content, config), expected_content
388+
)
389+
390+
def test_apply_owlbot_config_add_deep_copy_regex(self):
391+
392+
item1 = DeepCopyRegexItem(source="/path/to/copy", dest="/dest/to/copy")
393+
config = OwlbotYamlConfig(
394+
additions=OwlbotYamlAdditionRemoval(deep_copy_regex=[item1])
395+
)
396+
expected_content = """
397+
deep-remove-regex:
398+
- "/java-connectgateway/proto-google-.*/src"
399+
- "/java-connectgateway/google-.*/src"
400+
deep-preserve-regex:
401+
- "/java-connectgateway/google-.*/src/test/java/com/google/cloud/.*/v.*/it/IT.*Test.java"
402+
deep-copy-regex:
403+
- source: "/path/to/copy"
404+
dest: "/dest/to/copy"
405+
- source: "/google/cloud/gkeconnect/gateway/(v.*)/.*-java/proto-google-.*/src"
406+
dest: "/owl-bot-staging/java-connectgateway/$1/proto-google-cloud-connectgateway-$1/src"
407+
api-name: connectgateway
408+
"""
409+
self.assertEqual(
410+
util.apply_owlbot_config(self.content, config), expected_content
411+
)
412+
413+
def test_apply_owlbot_config_no_config(self):
414+
config = None
415+
expected_content = self.content
416+
self.assertEqual(
417+
util.apply_owlbot_config(self.content, config), expected_content
418+
)
419+
305420
def __setup_postprocessing_prerequisite_files(
306421
self,
307422
combination: int,

hermetic_build/library_generation/utils/file_render.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ def render(template_name: str, output_name: str, **kwargs):
1919
template = jinja_env.get_template(template_name)
2020
t = template.stream(kwargs)
2121
directory = os.path.dirname(output_name)
22-
if not os.path.isdir(directory):
23-
os.makedirs(directory)
22+
os.makedirs(directory, exist_ok=True)
2423
t.dump(str(output_name))
24+
25+
26+
def render_to_str(template_name: str, **kwargs) -> str:
27+
"""
28+
Renders a Jinja2 template and returns the output as a string.
29+
30+
Args:
31+
template_name: The name of the Jinja2 template file.
32+
**kwargs: Keyword arguments containing the data to pass to the template.
33+
34+
Returns:
35+
The rendered template content as a string.
36+
"""
37+
template = jinja_env.get_template(template_name)
38+
rendered_content = template.render(**kwargs)
39+
return rendered_content

hermetic_build/library_generation/utils/utilities.py

Lines changed: 143 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,24 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import json
15+
import os
16+
import re
1517
import sys
1618
import subprocess
17-
import os
1819
from pathlib import Path
19-
from typing import Any
20+
from typing import Any, Optional
2021

2122
from common.model.generation_config import GenerationConfig
2223
from common.model.library_config import LibraryConfig
23-
from typing import List
2424
from library_generation.model.repo_config import RepoConfig
25-
from library_generation.utils.file_render import render
25+
from library_generation.utils.file_render import render, render_to_str
2626
from library_generation.utils.proto_path_utils import remove_version_from
2727

2828
script_dir = os.path.dirname(os.path.realpath(__file__))
2929
SDK_PLATFORM_JAVA = "googleapis/sdk-platform-java"
3030

3131

32-
def create_argument(arg_key: str, arg_container: object) -> List[str]:
32+
def create_argument(arg_key: str, arg_container: object) -> list[str]:
3333
"""
3434
Generates a list of two elements [argument, value], or returns
3535
an empty array if arg_val is None
@@ -41,7 +41,7 @@ def create_argument(arg_key: str, arg_container: object) -> List[str]:
4141

4242

4343
def run_process_and_print_output(
44-
arguments: List[str] | str, job_name: str = "Job", exit_on_fail=True, **kwargs
44+
arguments: list[str] | str, job_name: str = "Job", exit_on_fail=True, **kwargs
4545
) -> Any:
4646
"""
4747
Runs a process with the given "arguments" list and prints its output.
@@ -68,7 +68,7 @@ def run_process_and_print_output(
6868

6969

7070
def run_process_and_get_output_string(
71-
arguments: List[str] | str, job_name: str = "Job", exit_on_fail=True, **kwargs
71+
arguments: list[str] | str, job_name: str = "Job", exit_on_fail=True, **kwargs
7272
) -> Any:
7373
"""
7474
Wrapper of run_process_and_print_output() that returns the merged
@@ -123,7 +123,7 @@ def eprint(*args, **kwargs):
123123

124124
def prepare_repo(
125125
gen_config: GenerationConfig,
126-
library_config: List[LibraryConfig],
126+
library_config: list[LibraryConfig],
127127
repo_path: str,
128128
language: str = "java",
129129
) -> RepoConfig:
@@ -276,14 +276,23 @@ def generate_postprocessing_prerequisite_files(
276276
else f"{library_path}/.github/{owlbot_yaml_file}"
277277
)
278278
if not os.path.exists(path_to_owlbot_yaml_file):
279-
render(
279+
# 1. Render the base content
280+
generated_content = render_to_str(
280281
template_name="owlbot.yaml.monorepo.j2",
281-
output_name=path_to_owlbot_yaml_file,
282282
artifact_id=artifact_id,
283283
proto_path=remove_version_from(proto_path),
284284
module_name=repo_metadata["repo_short"],
285285
api_shortname=library.api_shortname,
286+
owlbot_yaml=library.owlbot_yaml,
286287
)
288+
# 2. Apply additions and removals
289+
modified_content = apply_owlbot_config(generated_content, library.owlbot_yaml)
290+
291+
# 3. Write the modified content back to the file
292+
directory = os.path.dirname(path_to_owlbot_yaml_file)
293+
os.makedirs(directory, exist_ok=True)
294+
with open(path_to_owlbot_yaml_file, "w") as f:
295+
f.write(modified_content)
287296

288297
# generate owlbot.py
289298
py_file = "owlbot.py"
@@ -307,3 +316,127 @@ def generate_postprocessing_prerequisite_files(
307316
should_include_templates=True,
308317
template_excludes=template_excludes,
309318
)
319+
320+
321+
def apply_owlbot_config(
322+
content: str, owlbot_config: Optional["OwlbotYamlConfig"]
323+
) -> str:
324+
"""
325+
Applies the addition and removal configurations to the generated owlbot.yaml content.
326+
327+
Args:
328+
content: The generated owlbot.yaml content as a string.
329+
owlbot_config: The OwlbotYamlConfig object containing the addition and removal rules.
330+
331+
Returns:
332+
The modified owlbot.yaml content.
333+
"""
334+
if not owlbot_config:
335+
return content
336+
337+
modified_content = content
338+
339+
if owlbot_config.removals:
340+
if owlbot_config.removals.deep_remove_regex:
341+
for regex_pattern in owlbot_config.removals.deep_remove_regex:
342+
modified_content = re.sub(
343+
r'- "' + regex_pattern + r'"[^\S\n]*\n', "", modified_content
344+
)
345+
if owlbot_config.removals.deep_preserve_regex:
346+
for regex_pattern in owlbot_config.removals.deep_preserve_regex:
347+
modified_content = re.sub(
348+
r'- "' + regex_pattern + r'"[^\S\n]*\n', "", modified_content
349+
)
350+
if owlbot_config.removals.deep_copy_regex:
351+
for item in owlbot_config.removals.deep_copy_regex:
352+
# Construct the regex to match both source and dest lines
353+
source_regex = r'- source: "' + re.escape(item.source) + r'"\n'
354+
dest_regex = r'\s+dest: "' + re.escape(item.dest) + r'"\n'
355+
removal_regex = rf"{source_regex}{dest_regex}"
356+
modified_content = re.sub(
357+
removal_regex, "", modified_content, flags=re.MULTILINE
358+
)
359+
if owlbot_config.additions:
360+
if owlbot_config.additions.deep_remove_regex:
361+
modified_content = _add_items_after_key(
362+
content=modified_content,
363+
key="deep-remove-regex:",
364+
items=owlbot_config.additions.deep_remove_regex,
365+
quote=True,
366+
)
367+
if owlbot_config.additions.deep_preserve_regex:
368+
modified_content = _add_items_after_key(
369+
content=modified_content,
370+
key="deep-preserve-regex:",
371+
items=owlbot_config.additions.deep_preserve_regex,
372+
quote=True,
373+
)
374+
if owlbot_config.additions.deep_copy_regex:
375+
modified_content = _add_deep_copy_regex_items(
376+
content=modified_content,
377+
key="deep-copy-regex:",
378+
items=owlbot_config.additions.deep_copy_regex,
379+
)
380+
381+
return modified_content
382+
383+
384+
def _add_items_after_key(
385+
content: str, key: str, items: list[str], quote: bool = False
386+
) -> str:
387+
"""
388+
Adds items after a specified key in the content, following the template pattern.
389+
390+
Args:
391+
content: The generated owlbot.yaml content.
392+
key: The key after which to add the items (e.g., "deep-remove-regex:").
393+
items: The list of items to add.
394+
quote: Whether to enclose the item in double quotes.
395+
396+
Returns:
397+
The modified content.
398+
"""
399+
400+
def format_item(item: str) -> str:
401+
if quote:
402+
return f'- "{item}"\n'
403+
else:
404+
return f"- {item}\n"
405+
406+
pattern = re.compile(rf"^{re.escape(key)}", re.MULTILINE)
407+
match = pattern.search(content)
408+
409+
if match:
410+
insert_position = match.end() + 1 # Insert after the newline
411+
new_items = "".join(format_item(item) for item in items)
412+
return content[:insert_position] + new_items + content[insert_position:]
413+
else:
414+
return content # Key not found, return original content
415+
416+
417+
def _add_deep_copy_regex_items(
418+
content: str, key: str, items: list["DeepCopyRegexItem"]
419+
) -> str:
420+
"""
421+
Adds deep_copy_regex items after a specified key in the content.
422+
423+
Args:
424+
content: The generated owlbot.yaml content.
425+
key: The key after which to add the items (e.g., "deep-copy-regex:").
426+
items: The list of DeepCopyRegexItem objects to add.
427+
428+
Returns:
429+
The modified content.
430+
"""
431+
432+
pattern = re.compile(rf"^{re.escape(key)}", re.MULTILINE)
433+
match = pattern.search(content)
434+
435+
if match:
436+
insert_position = match.end() + 1 # Insert after the newline
437+
new_items = "".join(
438+
f'- source: "{item.source}"\n dest: "{item.dest}"\n' for item in items
439+
)
440+
return content[:insert_position] + new_items + content[insert_position:]
441+
else:
442+
return content

0 commit comments

Comments
 (0)