diff --git a/.github/scripts/tests/therock_configure_ci_test.py b/.github/scripts/tests/therock_configure_ci_test.py index 71d4c08689..5d872a1c24 100644 --- a/.github/scripts/tests/therock_configure_ci_test.py +++ b/.github/scripts/tests/therock_configure_ci_test.py @@ -2,72 +2,154 @@ from pathlib import Path import os import sys import unittest +from unittest.mock import MagicMock, patch sys.path.insert(0, os.fspath(Path(__file__).parent.parent)) import therock_configure_ci class ConfigureCITest(unittest.TestCase): - def test_pull_request(self): + @patch("subprocess.run") + def test_pull_request(self, mock_run): args = { "is_pull_request": True, - "input_subtrees": "projects/rocprim\nprojects/hipcub" + "base_ref": "HEAD^" } - project_to_run = therock_configure_ci.retrieve_projects(args) - self.assertEqual(len(project_to_run), 1) - - def test_pull_request_empty(self): - args = { - "is_pull_request": True, - "input_subtrees": "" - } - - project_to_run = therock_configure_ci.retrieve_projects(args) - self.assertEqual(len(project_to_run), 0) - - def test_workflow_dispatch(self): - args = { - "is_workflow_dispatch": True, - "input_projects": "projects/rocprim projects/hipcub" - } - - project_to_run = therock_configure_ci.retrieve_projects(args) - self.assertEqual(len(project_to_run), 1) - - def test_workflow_dispatch_bad_input(self): - args = { - "is_workflow_dispatch": True, - "input_projects": "projects/rocprim$$projects/hipcub" - } - - project_to_run = therock_configure_ci.retrieve_projects(args) - self.assertEqual(len(project_to_run), 0) - - def test_workflow_dispatch_all(self): - args = { - "is_workflow_dispatch": True, - "input_projects": "all" - } + mock_process = MagicMock() + mock_process.stdout = "projects/rocminfo/src/main.cpp" + mock_run.return_value = mock_process project_to_run = therock_configure_ci.retrieve_projects(args) self.assertGreaterEqual(len(project_to_run), 1) - def test_workflow_dispatch_empty(self): + @patch("subprocess.run") + def test_pull_request_empty(self, mock_run): + args = { + "is_pull_request": True, + "base_ref": "HEAD^" + } + + mock_process = MagicMock() + mock_process.stdout = "" + mock_run.return_value = mock_process + + project_to_run = therock_configure_ci.retrieve_projects(args) + # Empty modified_paths should return empty list (no changes = no CI) + self.assertEqual(len(project_to_run), 0) + + @patch("subprocess.run") + def test_workflow_dispatch(self, mock_run): args = { "is_workflow_dispatch": True, - "input_projects": "" + "input_projects": "projects/rocminfo projects/clr", + "base_ref": "HEAD^" } + mock_process = MagicMock() + mock_process.stdout = "projects/rocminfo/src/main.cpp" + mock_run.return_value = mock_process + + project_to_run = therock_configure_ci.retrieve_projects(args) + self.assertGreaterEqual(len(project_to_run), 1) + + @patch("subprocess.run") + def test_workflow_dispatch_bad_input(self, mock_run): + args = { + "is_workflow_dispatch": True, + "input_projects": "projects/invalid$$projects/fake", + "base_ref": "HEAD^" + } + + mock_process = MagicMock() + mock_process.stdout = "projects/rocminfo/src/main.cpp" + mock_run.return_value = mock_process + project_to_run = therock_configure_ci.retrieve_projects(args) self.assertEqual(len(project_to_run), 0) - def test_is_push(self): + @patch("subprocess.run") + def test_workflow_dispatch_all(self, mock_run): + args = { + "is_workflow_dispatch": True, + "input_projects": "all", + "base_ref": "HEAD^" + } + + mock_process = MagicMock() + mock_process.stdout = "projects/rocminfo/src/main.cpp" + mock_run.return_value = mock_process + + project_to_run = therock_configure_ci.retrieve_projects(args) + self.assertGreaterEqual(len(project_to_run), 1) + + @patch("subprocess.run") + def test_workflow_dispatch_empty(self, mock_run): + args = { + "is_workflow_dispatch": True, + "input_projects": "", + "base_ref": "HEAD^" + } + + mock_process = MagicMock() + mock_process.stdout = "projects/rocminfo/src/main.cpp" + mock_run.return_value = mock_process + + project_to_run = therock_configure_ci.retrieve_projects(args) + self.assertEqual(len(project_to_run), 0) + + @patch("subprocess.run") + def test_is_push(self, mock_run): args = { "is_push": True, + "base_ref": "HEAD^" } + mock_process = MagicMock() + mock_process.stdout = "projects/rocminfo/src/main.cpp" + mock_run.return_value = mock_process + project_to_run = therock_configure_ci.retrieve_projects(args) self.assertGreaterEqual(len(project_to_run), 1) + def test_is_path_skippable(self): + # Test skippable patterns + self.assertTrue(therock_configure_ci.is_path_skippable("README.md")) + self.assertTrue(therock_configure_ci.is_path_skippable("docs/guide.rst")) + self.assertTrue(therock_configure_ci.is_path_skippable("projects/rocminfo/README.md")) + self.assertTrue(therock_configure_ci.is_path_skippable("projects/rocminfo/docs/api.rst")) + self.assertTrue(therock_configure_ci.is_path_skippable(".gitignore")) + + # Test non-skippable patterns + self.assertFalse(therock_configure_ci.is_path_skippable("projects/rocminfo/src/main.cpp")) + self.assertFalse(therock_configure_ci.is_path_skippable("CMakeLists.txt")) + self.assertFalse(therock_configure_ci.is_path_skippable("projects/rocminfo/test/test.cpp")) + + def test_check_for_non_skippable_path(self): + # All skippable paths + skippable_paths = ["README.md", "docs/guide.rst", "projects/rocminfo/docs/api.md"] + self.assertFalse(therock_configure_ci.check_for_non_skippable_path(skippable_paths)) + + # Mixed paths (has non-skippable) + mixed_paths = ["README.md", "src/main.cpp"] + self.assertTrue(therock_configure_ci.check_for_non_skippable_path(mixed_paths)) + + # None input + self.assertFalse(therock_configure_ci.check_for_non_skippable_path(None)) + + @patch("subprocess.run") + def test_docs_only_change_returns_empty_list(self, mock_run): + args = { + "is_pull_request": True, + "base_ref": "HEAD^" + } + + # Mock git diff to return only doc files + mock_process = MagicMock() + mock_process.stdout = "README.md\ndocs/guide.rst\nprojects/rocprim/docs/api.md" + mock_run.return_value = mock_process + + project_to_run = therock_configure_ci.retrieve_projects(args) + self.assertEqual(len(project_to_run), 0) + if __name__ == "__main__": unittest.main() diff --git a/.github/scripts/therock_configure_ci.py b/.github/scripts/therock_configure_ci.py index 013507b976..4fa5656cd6 100644 --- a/.github/scripts/therock_configure_ci.py +++ b/.github/scripts/therock_configure_ci.py @@ -81,7 +81,52 @@ def check_for_workflow_file_related_to_ci(paths: Optional[Iterable[str]]) -> boo return any(is_path_workflow_file_related_to_ci(p) for p in paths) +# Paths matching any of these patterns are considered to have no influence over +# build or test workflows so any related jobs can be skipped if all paths +# modified by a commit/PR match a pattern in this list. +SKIPPABLE_PATH_PATTERNS = [ + "docs/*", + ".gitignore", + "*.md", + "*.rst", + "projects/*/docs/*", + "projects/*/.gitignore", + "projects/*/*.md", + "projects/*/*.rst", + "shared/*/docs/*", + "shared/*/.gitignore", + "shared/*/*.md", + "shared/*/*.rst", +] + + +def is_path_skippable(path: str) -> bool: + """Determines if a given relative path to a file matches any skippable patterns.""" + return any(fnmatch.fnmatch(path, pattern) for pattern in SKIPPABLE_PATH_PATTERNS) + + +def check_for_non_skippable_path(paths: Optional[Iterable[str]]) -> bool: + """Returns true if at least one path is not in the skippable set.""" + if paths is None: + return False + return any(not is_path_skippable(p) for p in paths) + + def retrieve_projects(args): + # Check if CI should be skipped based on modified paths + # (only for push and pull_request events, not workflow_dispatch or nightly) + if args.get("is_push") or args.get("is_pull_request"): + base_ref = args.get("base_ref") + modified_paths = get_modified_paths(base_ref) + + paths_set = set(modified_paths) + contains_non_skippable_files = check_for_non_skippable_path(paths_set) + + # If only skippable paths were modified, skip CI + if not contains_non_skippable_files: + logging.info("Only skippable paths were modified, skipping CI") + return [] + if args.get("is_pull_request"): subtrees = list(subtree_to_project_map.keys())