diff --git a/tools/create_release.py b/tools/create_release.py index d3f07e8..12a1949 100755 --- a/tools/create_release.py +++ b/tools/create_release.py @@ -146,14 +146,16 @@ def assign_to_user( s.ok(f"Assigned to {self.github.actor()}") return stage.UserAbort(f"Returning to the user to {action}") - def report_failure(self, version: str, exception: Exception) -> None: + def report_failure( + self, version: str, exception: Exception | BaseException + ) -> None: """Report a failure to the release tracking issue.""" if not self.config.issue: return print(f"Reporting failure to tracking issue #{self.config.issue}...") instruction = f"❌ **Failure:** {exception}" - self.assign_to_user( + raise self.assign_to_user( None, version, action="fix the failure", @@ -166,7 +168,7 @@ def run(self) -> None: self.run_stages() except stage.UserAbort as e: print(e.message) - except Exception as e: + except (Exception, BaseException) as e: self.report_failure(self.version, e) raise e @@ -440,7 +442,8 @@ def stage_validate(self) -> None: validate_pr.Config( commit=not self.config.verify, release=self.config.production, - ) + ), + failures=[], ) def extract_issue_release_notes(self, body: str) -> str: @@ -989,6 +992,7 @@ def run_stages(self, version: str | None = None) -> None: self.stage_branch(version) self.stage_gitignore() self.stage_validate() + self.update_dashboard(version) self.stage_release_notes(version) self.stage_commit(version) self.stage_push() diff --git a/tools/create_release_test.py b/tools/create_release_test.py index b2577c6..7078d05 100644 --- a/tools/create_release_test.py +++ b/tools/create_release_test.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock from create_release import Config, Releaser +from lib import stage class TestDashboardRenderer(unittest.TestCase): @@ -70,13 +71,28 @@ def setUp(self) -> None: self.git = MagicMock() self.releaser = Releaser(self.config, self.git, self.github) + def test_run_catches_base_exception(self) -> None: + # Mock run_stages to raise SystemExit (a BaseException) + with unittest.mock.patch.object( + self.releaser, "run_stages", side_effect=SystemExit(1) + ), unittest.mock.patch.object( + self.releaser, "report_failure", side_effect=stage.UserAbort("failed") + ) as mock_report: + with self.assertRaises(stage.UserAbort): + self.releaser.run() + + mock_report.assert_called() + args, kwargs = mock_report.call_args + self.assertIsInstance(args[1], SystemExit) + def test_report_failure(self) -> None: self.github.actor.return_value = "human" self.github.get_issue.return_value = MagicMock( body="### Release progress\n[ ] ..." ) - self.releaser.report_failure("v1.0.0", Exception("Something went wrong")) + with self.assertRaises(stage.UserAbort): + self.releaser.report_failure("v1.0.0", Exception("Something went wrong")) # Check that issue was reassigned self.github.issue_unassign.assert_called_with(1, ["toktok-releaser"]) diff --git a/tools/validate_pr.py b/tools/validate_pr.py index 0bf0e69..ec5d010 100755 --- a/tools/validate_pr.py +++ b/tools/validate_pr.py @@ -310,7 +310,7 @@ def check_changelog(failures: list[str], config: Config) -> None: check.ok("The changelog is up-to-date") -def main(config: Config) -> None: +def main(config: Config, failures: list[str] | None = None) -> None: """Main entry point.""" actor = github.actor() if config.debug: @@ -326,29 +326,32 @@ def main(config: Config) -> None: print("\nRunning checks...\n") - failures: list[str] = [] + failed_checks: list[str] = failures if failures is not None else [] # If the PR branch looks like a version number, do checks for a release PR. if config.release or re.match(git.RELEASE_BRANCH_REGEX, github.head_ref()): print("This is a release PR.\n") - check_github_weblate_prs(failures) - check_flathub_descriptor_dependencies(failures, config) - check_toxcore_version(failures) - check_package_versions(failures, config) + check_github_weblate_prs(failed_checks) + check_flathub_descriptor_dependencies(failed_checks, config) + check_toxcore_version(failed_checks) + check_package_versions(failed_checks, config) else: print(f"This is not a release PR ({git.RELEASE_BRANCH_REGEX.pattern}).\n") - check_no_version_changes(failures) + check_no_version_changes(failed_checks) - check_changelog(failures, config) + check_changelog(failed_checks, config) if config.debug: print(f"\nDebug: {len(github.api_requests)} GitHub API requests made") - if failures: + if failed_checks: print("\nSome checks failed:") - for failure in failures: + for failure in failed_checks: print(f" - {failure}") - exit(1) + + if failures is None: + exit(1) + raise stage.InvalidState(f"{len(failed_checks)} checks failed") if __name__ == "__main__": diff --git a/tools/validate_pr_test.py b/tools/validate_pr_test.py index dad8b41..4777f50 100644 --- a/tools/validate_pr_test.py +++ b/tools/validate_pr_test.py @@ -2,15 +2,16 @@ # Copyright © 2026 The TokTok team import unittest import unittest.mock +from typing import Any -from validate_pr import (Config, check_changelog, parse_toxcore_version, - parse_version_diff, parse_weblate_prs) +import validate_pr +from lib import stage class TestCheckChangelog(unittest.TestCase): - @unittest.mock.patch("update_changelog.main") - @unittest.mock.patch("update_changelog.read_clog_toml", return_value={}) - @unittest.mock.patch("update_changelog.parse_config") + @unittest.mock.patch("validate_pr.update_changelog.main") + @unittest.mock.patch("validate_pr.update_changelog.read_clog_toml", return_value={}) + @unittest.mock.patch("validate_pr.update_changelog.parse_config") @unittest.mock.patch("validate_pr.github.head_ref") @unittest.mock.patch("validate_pr.has_diff", return_value=False) @unittest.mock.patch("validate_pr.stage.Stage") @@ -29,23 +30,23 @@ def test_check_changelog_production( # 1. Release config set to True mock_head_ref.return_value = "some-branch" - config = Config(commit=False, release=True) - check_changelog([], config) + config = validate_pr.Config(commit=False, release=True) + validate_pr.check_changelog([], config) self.assertTrue(clog_config.production) mock_clog_main.assert_called_with(clog_config) # 2. Release config False, but branch is a production release branch clog_config.production = False mock_head_ref.return_value = "release/v1.0.0" - config = Config(commit=False, release=False) - check_changelog([], config) + config = validate_pr.Config(commit=False, release=False) + validate_pr.check_changelog([], config) self.assertTrue(clog_config.production) # 3. Release config False, branch is an RC release branch clog_config.production = False mock_head_ref.return_value = "release/v1.0.0-rc.1" - config = Config(commit=False, release=False) - check_changelog([], config) + config = validate_pr.Config(commit=False, release=False) + validate_pr.check_changelog([], config) self.assertFalse(clog_config.production) @@ -65,15 +66,15 @@ def test_parse_weblate_prs(self) -> None: }, ] expected = [("Translation 1", "url1"), ("Translation 2", "url3")] - self.assertEqual(parse_weblate_prs(prs_data), expected) + self.assertEqual(validate_pr.parse_weblate_prs(prs_data), expected) def test_parse_toxcore_version(self) -> None: content = """#!/bin/bash TOXCORE_VERSION=0.2.20 SOME_OTHER_VAR=val """ - self.assertEqual(parse_toxcore_version(content), "0.2.20") - self.assertIsNone(parse_toxcore_version("no version here")) + self.assertEqual(validate_pr.parse_toxcore_version(content), "0.2.20") + self.assertIsNone(validate_pr.parse_toxcore_version("no version here")) def test_parse_version_diff(self) -> None: diff = """--- a/platform/linux/chat.tox.CiTools.appdata.xml @@ -81,7 +82,7 @@ def test_parse_version_diff(self) -> None: - + """ - minus, plus = parse_version_diff(diff) + minus, plus = validate_pr.parse_version_diff(diff) self.assertEqual(minus, ["1.18.0-rc.3"]) self.assertEqual(plus, ["1.18.0"]) @@ -89,10 +90,48 @@ def test_parse_version_diff_no_changes(self) -> None: diff = """some other changes -

line

+

new line

""" - minus, plus = parse_version_diff(diff) + minus, plus = validate_pr.parse_version_diff(diff) self.assertEqual(minus, []) self.assertEqual(plus, []) +class TestMain(unittest.TestCase): + @unittest.mock.patch("validate_pr.github.actor", return_value="human") + @unittest.mock.patch("validate_pr.github.head_ref", return_value="master") + @unittest.mock.patch("validate_pr.github.api_requests", new_callable=list) + @unittest.mock.patch("validate_pr.check_github_weblate_prs") + @unittest.mock.patch("validate_pr.check_flathub_descriptor_dependencies") + @unittest.mock.patch("validate_pr.check_toxcore_version") + @unittest.mock.patch("validate_pr.check_package_versions") + @unittest.mock.patch("validate_pr.check_no_version_changes") + @unittest.mock.patch("validate_pr.check_changelog") + def test_main_with_failures_list( + self, + mock_check_changelog: unittest.mock.MagicMock, + mock_check_no_version_changes: unittest.mock.MagicMock, + mock_check_package_versions: unittest.mock.MagicMock, + mock_check_toxcore_version: unittest.mock.MagicMock, + mock_check_flathub_descriptor_dependencies: unittest.mock.MagicMock, + mock_check_github_weblate_prs: unittest.mock.MagicMock, + mock_api_requests: list[Any], + mock_head_ref: unittest.mock.MagicMock, + mock_actor: unittest.mock.MagicMock, + ) -> None: + # Setup: check_changelog appends a failure + mock_check_changelog.side_effect = lambda f, c: f.append("Changelog failure") + + config = validate_pr.Config(commit=False, release=False) + + # 1. Test with failures=[] -> should raise stage.InvalidState + with self.assertRaises(stage.InvalidState) as cm: + validate_pr.main(config, failures=[]) + self.assertIn("1 checks failed", str(cm.exception)) + + # 2. Test with failures=None -> should call exit(1) + with self.assertRaises(SystemExit) as cm_exit: + validate_pr.main(config, failures=None) + self.assertEqual(cm_exit.exception.code, 1) + + if __name__ == "__main__": unittest.main()