Files
TheRock/build_tools/github_actions/tests/bump_automation_test.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

467 lines
18 KiB
Python

# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
import os
from pathlib import Path
import sys
import tempfile
import textwrap
import unittest
from unittest.mock import patch
sys.path.insert(0, os.fspath(Path(__file__).parent.parent))
from bump_automation import (
_clone_url,
close_stale_prs,
create_therock_bump,
generate_pr_body,
get_submodule_sha,
handle_push,
latest_commit,
submodule_changed,
update_ci_env_file,
update_ref_in_file,
)
class CloneUrlTest(unittest.TestCase):
def test_formats_url_with_token(self):
url = _clone_url("ROCm/TheRock", "mytoken")
self.assertEqual(
url, "https://x-access-token:mytoken@github.com/ROCm/TheRock.git"
)
def test_formats_url_with_different_repo(self):
url = _clone_url("ROCm/rocgdb", "tok123")
self.assertEqual(
url, "https://x-access-token:tok123@github.com/ROCm/rocgdb.git"
)
class GeneratePrBodyTest(unittest.TestCase):
def test_contains_repo_links(self):
body = generate_pr_body("ROCm/rocgdb", "aabbcc1", "ddeeff2")
self.assertIn("ROCm/rocgdb", body)
self.assertIn("aabbcc1", body)
self.assertIn("ddeeff2", body)
def test_contains_compare_url(self):
body = generate_pr_body("ROCm/rocgdb", "aabbcc1", "ddeeff2")
self.assertIn("compare/aabbcc1...ddeeff2", body)
class GetSubmoduleShaTest(unittest.TestCase):
def test_parses_sha_from_ls_tree_output(self):
ls_tree_output = "160000 commit abc123def456 debug-tools/rocgdb/source"
with patch("bump_automation.run", return_value=ls_tree_output):
sha = get_submodule_sha("HEAD", "debug-tools/rocgdb/source")
self.assertEqual(sha, "abc123def456")
class LatestCommitTest(unittest.TestCase):
def test_queries_default_branch_when_no_branch(self):
with patch(
"bump_automation.gh_api", return_value=[{"sha": "deadbeef"}]
) as mock_api:
sha = latest_commit("ROCm/rocm-systems", "token")
self.assertEqual(sha, "deadbeef")
self.assertEqual(mock_api.call_args.args[1], "repos/ROCm/rocm-systems/commits")
def test_queries_specific_branch_when_provided(self):
with patch(
"bump_automation.gh_api", return_value=[{"sha": "cafef00d"}]
) as mock_api:
sha = latest_commit("ROCm/rocgdb", "token", "amd-staging-rocgdb-16")
self.assertEqual(sha, "cafef00d")
self.assertEqual(
mock_api.call_args.args[1],
"repos/ROCm/rocgdb/commits?sha=amd-staging-rocgdb-16",
)
class SubmoduleChangedTest(unittest.TestCase):
def test_returns_true_when_diff_nonempty(self):
with patch("bump_automation.run", return_value="some diff output"):
self.assertTrue(submodule_changed("abc", "def", "rocm-systems"))
def test_returns_false_when_diff_empty(self):
with patch("bump_automation.run", return_value=""):
self.assertFalse(submodule_changed("abc", "def", "rocm-systems"))
def test_returns_false_when_diff_whitespace_only(self):
with patch("bump_automation.run", return_value=" \n "):
self.assertFalse(submodule_changed("abc", "def", "rocm-systems"))
class UpdateRefInFileTest(unittest.TestCase):
def _run(self, content: str) -> str:
with tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False) as f:
f.write(content)
path = f.name
try:
update_ref_in_file(path, "newsha1234567")
return Path(path).read_text()
finally:
os.unlink(path)
def test_updates_ref_line(self):
content = textwrap.dedent(
"""\
uses: actions/checkout@v3
with:
repository: "ROCm/TheRock"
ref: oldsha1234567 # 2024-01-01 commit
"""
)
result = self._run(content)
self.assertIn("ref: newsha1234567", result)
self.assertNotIn("oldsha1234567", result)
def test_preserves_other_lines(self):
content = textwrap.dedent(
"""\
uses: actions/checkout@v3
with:
repository: "ROCm/TheRock"
ref: oldsha1234567
other: value
"""
)
result = self._run(content)
self.assertIn("uses: actions/checkout@v3", result)
self.assertIn("other: value", result)
def test_handles_path_line_between_repository_and_ref(self):
content = textwrap.dedent(
"""\
with:
repository: "ROCm/TheRock"
path: "TheRock"
ref: oldsha1234567
"""
)
result = self._run(content)
self.assertIn('path: "TheRock"', result)
self.assertIn("ref: newsha1234567", result)
def test_no_change_when_no_matching_repository(self):
content = textwrap.dedent(
"""\
uses: actions/checkout@v3
with:
repository: "ROCm/SomeOtherRepo"
ref: oldsha1234567
"""
)
result = self._run(content)
self.assertIn("oldsha1234567", result)
def test_updates_multiple_occurrences(self):
content = textwrap.dedent(
"""\
- uses: actions/checkout@v3
with:
repository: "ROCm/TheRock"
ref: oldsha0000001
- uses: actions/checkout@v3
with:
repository: "ROCm/TheRock"
ref: oldsha0000002
"""
)
result = self._run(content)
self.assertEqual(result.count("ref: newsha1234567"), 2)
self.assertNotIn("oldsha0000001", result)
self.assertNotIn("oldsha0000002", result)
class UpdateCiEnvFileTest(unittest.TestCase):
def _run(self, content: str) -> str:
with tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False) as f:
f.write(content)
path = f.name
try:
update_ci_env_file(path, "newsha1234567")
return Path(path).read_text()
finally:
os.unlink(path)
def test_updates_value_under_therock_ref(self):
content = textwrap.dedent(
"""\
outputs:
therock-ref:
description: "TheRock commit ref"
value: "oldsha1234567" # 2024-01-01 commit
"""
)
result = self._run(content)
self.assertIn('"newsha1234567"', result)
self.assertNotIn("oldsha1234567", result)
def test_preserves_description_line(self):
content = textwrap.dedent(
"""\
outputs:
therock-ref:
description: "TheRock commit ref"
value: "oldsha1234567"
"""
)
result = self._run(content)
self.assertIn('description: "TheRock commit ref"', result)
def test_preserves_other_outputs(self):
content = textwrap.dedent(
"""\
outputs:
therock-ref:
description: "TheRock commit ref"
value: "oldsha1234567"
other-output:
value: "unchanged"
"""
)
result = self._run(content)
self.assertIn('"newsha1234567"', result)
self.assertIn('"unchanged"', result)
def test_no_change_when_no_therock_ref(self):
content = textwrap.dedent(
"""\
outputs:
some-other-ref:
value: "oldsha1234567"
"""
)
result = self._run(content)
self.assertIn("oldsha1234567", result)
class CloseStalePrsTest(unittest.TestCase):
def _make_pr(self, number: int, title: str) -> dict:
return {"number": number, "title": title}
def test_closes_matching_pr(self):
prs = [self._make_pr(42, "Bump rocm-systems from abc1234 to xyz5678")]
with patch("bump_automation.gh_api", return_value=prs) as mock_api:
close_stale_prs("rocm-systems", "abc1234567890", "token")
patch_calls = [
c for c in mock_api.call_args_list if c.kwargs.get("method") == "PATCH"
]
self.assertEqual(len(patch_calls), 1)
self.assertIn("pulls/42", patch_calls[0].args[1])
self.assertEqual(patch_calls[0].kwargs["data"]["state"], "closed")
def test_skips_non_matching_pr(self):
prs = [self._make_pr(99, "Bump rocm-libraries from def9876 to uvw5432")]
with patch("bump_automation.gh_api", return_value=prs) as mock_api:
close_stale_prs("rocm-systems", "abc1234567890", "token")
patch_calls = [
c for c in mock_api.call_args_list if c.kwargs.get("method") == "PATCH"
]
self.assertEqual(len(patch_calls), 0)
def test_posts_comment_before_closing(self):
prs = [self._make_pr(42, "Bump rocm-systems from abc1234 to xyz5678")]
with patch("bump_automation.gh_api", return_value=prs) as mock_api:
close_stale_prs("rocm-systems", "abc1234567890", "token")
post_calls = [
c for c in mock_api.call_args_list if c.kwargs.get("method") == "POST"
]
self.assertTrue(any("comments" in c.args[1] for c in post_calls))
class HandlePushTest(unittest.TestCase):
def test_noop_when_no_submodule_changed(self):
with patch("bump_automation.submodule_changed", return_value=False):
with patch("bump_automation.get_submodule_sha") as mock_sha:
handle_push("before", "after", {"systems": "t"})
mock_sha.assert_not_called()
def test_submodule_only_closes_stale_prs_then_returns(self):
def changed(before, after, path):
return path == "debug-tools/rocgdb/source"
with patch("bump_automation.submodule_changed", side_effect=changed):
with patch(
"bump_automation.get_submodule_sha", return_value="oldsha1234567"
):
with patch("bump_automation.close_stale_prs") as mock_close:
with patch(
"bump_automation.tempfile.TemporaryDirectory"
) as mock_tmp:
handle_push(
"before",
"after",
{
"systems": "systems-token",
"libraries": "libraries-token",
"rocgdb": "rocgdb-token",
},
)
mock_close.assert_called_once()
# rocgdb reuses the systems token, so close_stale_prs must receive it.
self.assertEqual(mock_close.call_args.args[2], "systems-token")
# submodule-only entries have no upstream ref files, so the handler must
# bail out before cloning the upstream repo.
mock_tmp.assert_not_called()
class CreateTheRockBumpTest(unittest.TestCase):
def test_skips_when_pr_already_open(self):
"""create_therock_bump must bail out without cloning when an open PR
already targets the exact bump branch for the latest commit."""
with patch("bump_automation.latest_commit", return_value="abc1234567890"):
with patch(
"bump_automation.gh_api",
return_value=[{"number": 99}],
) as mock_api:
with patch("bump_automation.tempfile.TemporaryDirectory") as mock_tmp:
create_therock_bump("rocm-systems", "token")
mock_tmp.assert_not_called()
# Only the open-PR check should have hit the API.
self.assertEqual(mock_api.call_count, 1)
endpoint = mock_api.call_args.args[1]
self.assertIn("pulls?state=open", endpoint)
self.assertIn("bump-rocm-systems-abc1234", endpoint)
def test_skips_when_submodule_already_at_latest(self):
"""create_therock_bump must bail out without cloning when the submodule
is already pinned to the latest upstream commit."""
latest_sha = "abc1234567890"
api_responses = [
[], # open-PR check returns nothing
]
def _gh_api_side_effect(*args, **kwargs):
return api_responses.pop(0) if api_responses else {}
with patch("bump_automation.latest_commit", return_value=latest_sha):
with patch("bump_automation.gh_api", side_effect=_gh_api_side_effect):
with patch("bump_automation.run") as mock_run:
with patch("bump_automation.os.chdir"):
with patch("bump_automation.os.path.exists", return_value=True):
with patch(
"bump_automation.get_submodule_sha",
return_value=latest_sha,
):
with patch(
"bump_automation.tempfile.TemporaryDirectory"
) as mock_tmp:
create_therock_bump("rocm-systems", "token")
# The clone is created before we detect the no-op, so TemporaryDirectory
# will have been called. What must NOT happen is any git commit or push.
git_calls = [c for c in mock_run.call_args_list if "commit" in c.args[0]]
self.assertEqual(
git_calls, [], "git commit must not run when already at latest"
)
push_calls = [c for c in mock_run.call_args_list if "push" in c.args[0]]
self.assertEqual(push_calls, [], "git push must not run when already at latest")
def test_proceeds_when_no_open_pr(self):
"""create_therock_bump must proceed to clone when no open PR exists."""
# First gh_api call is the open-PR check (returns []); subsequent calls
# are PR creation (returns a PR dict) and label addition (ignored).
api_responses = [[], {"number": 1}, {}]
def _gh_api_side_effect(*args, **kwargs):
return api_responses.pop(0) if api_responses else {}
with patch("bump_automation.latest_commit", return_value="abc1234567890"):
with patch(
"bump_automation.gh_api", side_effect=_gh_api_side_effect
) as mock_api:
with patch("bump_automation.run"):
with patch("bump_automation.os.chdir"):
with patch("bump_automation.os.path.exists", return_value=True):
with patch(
"bump_automation.get_submodule_sha",
return_value="old1234",
):
with patch(
"bump_automation.generate_pr_body",
return_value="body",
):
with patch("bump_automation._git_commit"):
create_therock_bump("rocm-systems", "token")
# Must have made at least the open-PR check + PR creation calls.
self.assertGreaterEqual(mock_api.call_count, 2)
class HandleScheduleTest(unittest.TestCase):
def test_rocgdb_maps_to_correct_submodule_and_token(self):
"""handle_schedule('rocgdb') must call create_therock_bump with
'debug-tools/rocgdb/source' and tokens['rocgdb'], not any other key."""
tokens = {
"systems": "systems-token",
"libraries": "libraries-token",
"rocgdb": "rocgdb-token",
}
with patch("bump_automation.create_therock_bump") as mock_bump:
from bump_automation import handle_schedule
handle_schedule(tokens, "rocgdb")
mock_bump.assert_called_once_with("debug-tools/rocgdb/source", "rocgdb-token")
def test_rocgdb_does_not_invoke_other_submodules(self):
tokens = {
"systems": "systems-token",
"libraries": "libraries-token",
"rocgdb": "rocgdb-token",
}
with patch("bump_automation.create_therock_bump") as mock_bump:
from bump_automation import handle_schedule
handle_schedule(tokens, "rocgdb")
called_submodules = [c.args[0] for c in mock_bump.call_args_list]
self.assertNotIn("rocm-systems", called_submodules)
self.assertNotIn("rocm-libraries", called_submodules)
class CreateTheRockBumpRocgdbConfigTest(unittest.TestCase):
def test_reads_repo_and_branch_from_submodule_config(self):
"""create_therock_bump('debug-tools/rocgdb/source') must derive repo and
branch from SUBMODULE_CONFIG, not hard-code them."""
api_responses = [[], {"number": 1}, {}]
def _gh_api_side_effect(*args, **kwargs):
return api_responses.pop(0) if api_responses else {}
with patch(
"bump_automation.latest_commit", return_value="abc1234567890"
) as mock_latest:
with patch("bump_automation.gh_api", side_effect=_gh_api_side_effect):
with patch("bump_automation.run"):
with patch("bump_automation.os.chdir"):
with patch("bump_automation.os.path.exists", return_value=True):
with patch(
"bump_automation.get_submodule_sha",
return_value="old1234",
):
with patch(
"bump_automation.generate_pr_body",
return_value="body",
):
with patch("bump_automation._git_commit"):
create_therock_bump(
"debug-tools/rocgdb/source", "token"
)
mock_latest.assert_called_once_with(
"ROCm/rocgdb", "token", "amd-staging-rocgdb-16"
)
if __name__ == "__main__":
unittest.main()