diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py new file mode 100644 index 0000000..d40827b --- /dev/null +++ b/tests/test_cli_commands.py @@ -0,0 +1,19 @@ +# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license + + +# Import CLI command modules +from actions import ( + first_interaction, + summarize_pr, + summarize_release, + update_markdown_code_blocks, +) + + +def test_importable_modules(): + """Test that all modules can be imported without errors.""" + # This is a simple test to ensure modules can be imported successfully + assert hasattr(first_interaction, "main") + assert hasattr(summarize_pr, "main") + assert hasattr(summarize_release, "main") + assert hasattr(update_markdown_code_blocks, "main") diff --git a/tests/test_common_utils.py b/tests/test_common_utils.py new file mode 100644 index 0000000..2fe36d6 --- /dev/null +++ b/tests/test_common_utils.py @@ -0,0 +1,63 @@ +# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license + +from unittest.mock import MagicMock, patch + +from actions.utils.common_utils import ( + allow_redirect, + brave_search, + clean_url, + remove_html_comments, +) + + +def test_remove_html_comments(): + """Test removing HTML comments from strings.""" + test_str = "Before After" + assert remove_html_comments(test_str) == "Before After" + + # Multiline comment + test_str = "Before\n\nAfter" + assert remove_html_comments(test_str) == "Before\n\nAfter" + + # No comments + test_str = "No comments here" + assert remove_html_comments(test_str) == "No comments here" + + +def test_clean_url(): + """Test cleaning URL strings.""" + # Test removing quotes and trailing characters + assert clean_url('"https://example.com"') == "https://example.com" + assert clean_url("'https://example.com'") == "https://example.com" + assert clean_url("https://example.com.") == "https://example.com" + assert clean_url("https://example.com,") == "https://example.com" + + # Test git URLs + assert clean_url("git+https://github.com/user/repo.git@main") == "https://github.com/user/repo" + + +def test_allow_redirect(): + """Test allowing URL redirects based on rules.""" + # Should not allow - start ignores + assert not allow_redirect("https://youtu.be/xyz", "https://youtube.com") + + # Should not allow - end ignores + assert not allow_redirect("https://example.com", "https://example.com/404") + + # Empty end URL + assert not allow_redirect("https://example.com", "") + + +@patch("requests.get") +def test_brave_search(mock_get): + """Test Brave search API integration.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "web": {"results": [{"url": "https://example.com"}, {"url": "https://example.org"}]} + } + mock_get.return_value = mock_response + + results = brave_search("test query", "test-api-key", count=2) + assert results == ["https://example.com", "https://example.org"] + mock_get.assert_called_once() diff --git a/tests/test_github_utils.py b/tests/test_github_utils.py new file mode 100644 index 0000000..cbbede8 --- /dev/null +++ b/tests/test_github_utils.py @@ -0,0 +1,64 @@ +# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license + +from unittest.mock import MagicMock, patch + +from actions.utils.github_utils import Action, check_pypi_version, ultralytics_actions_info + + +def test_action_init(): + """Test Action class initialization with default values.""" + with patch.dict("os.environ", {"GITHUB_TOKEN": "test-token", "GITHUB_EVENT_NAME": "push"}): + with patch( + "actions.utils.github_utils.Action._load_event_data", + return_value={"repository": {"full_name": "test/repo"}}, + ): + action = Action() + assert action.token == "test-token" + assert action.event_name == "push" + assert action.repository == "test/repo" + + +def test_action_request_methods(): + """Test Action HTTP request methods.""" + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + action = Action(token="test-token") + response = action.get("https://api.github.com/test") + + assert response == mock_response + mock_get.assert_called_once() + + +def test_load_event_data(): + """Test loading event data from file.""" + with patch("pathlib.Path.exists", return_value=True): + with patch("pathlib.Path.read_text", return_value='{"test": "data"}'): + action = Action() + data = action._load_event_data("fake_path") + assert data == {"test": "data"} + + +def test_check_pypi_version(): + """Test check_pypi_version function.""" + with patch("tomllib.load", return_value={"project": {"name": "test-package", "version": "1.0.0"}}): + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"info": {"version": "0.9.0"}} + mock_get.return_value = mock_response + + local_version, online_version, publish = check_pypi_version() + + assert local_version == "1.0.0" + assert online_version == "0.9.0" + assert publish is True + + +def test_ultralytics_actions_info(): + """Test ultralytics_actions_info function.""" + with patch("actions.utils.github_utils.Action.print_info") as mock_print_info: + ultralytics_actions_info() + mock_print_info.assert_called_once() diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..71e9500 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,84 @@ +# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license + +import importlib +import re + +import pytest + +from actions import __version__ +from actions.utils import ( + GITHUB_API_URL, + GITHUB_GRAPHQL_URL, + Action, + allow_redirect, + check_pypi_version, + get_completion, + remove_html_comments, + ultralytics_actions_info, +) + + +def test_version_format(): + """Test that version follows the expected format.""" + version_pattern = re.compile(r"^\d+\.\d+\.\d+$") + assert version_pattern.match(__version__), f"Version {__version__} does not match pattern" + + +def test_util_module_imports(): + """Test that all utilities can be imported from the utils module.""" + # Test the utils module exports + assert GITHUB_API_URL.startswith("https://api.github.com") + assert GITHUB_GRAPHQL_URL.startswith("https://api.github.com/graphql") + + # Check that Action class exists and is importable + assert hasattr(Action, "__init__") + assert hasattr(Action, "get") + assert hasattr(Action, "post") + + # Check function imports + assert callable(allow_redirect) + assert callable(check_pypi_version) + assert callable(get_completion) + assert callable(remove_html_comments) + assert callable(ultralytics_actions_info) + + +def test_all_modules_importable(): + """Test that all modules can be imported without errors.""" + modules = [ + "actions", + "actions.utils", + "actions.utils.common_utils", + "actions.utils.github_utils", + "actions.utils.openai_utils", + "actions.first_interaction", + "actions.summarize_pr", + "actions.summarize_release", + "actions.update_markdown_code_blocks", + "actions.dispatch_actions", + ] + + for module_name in modules: + module = importlib.import_module(module_name) + assert module is not None, f"Failed to import {module_name}" + + +def test_cli_entry_points(): + """Test that CLI entry points are defined in pyproject.toml.""" + from pathlib import Path + + pyproject_path = Path(__file__).parents[1] / "pyproject.toml" + if not pyproject_path.exists(): + pytest.skip("pyproject.toml not found") + + content = pyproject_path.read_text() + expected_names = [ + "ultralytics-actions-first-interaction", + "ultralytics-actions-summarize-pr", + "ultralytics-actions-summarize-release", + "ultralytics-actions-update-markdown-code-blocks", + "ultralytics-actions-info", + ] + + for name in expected_names: + assert name in content, f"Entry point {name} not found in pyproject.toml" diff --git a/tests/test_openai_utils.py b/tests/test_openai_utils.py new file mode 100644 index 0000000..5a0c3ba --- /dev/null +++ b/tests/test_openai_utils.py @@ -0,0 +1,65 @@ +# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license + +from unittest.mock import MagicMock, patch + +from actions.utils.openai_utils import get_completion, remove_outer_codeblocks + + +def test_remove_outer_codeblocks(): + """Test removing outer code block markers from strings.""" + # Test with python code block + input_str = "```python\ndef test():\n return True\n```" + expected = "def test():\n return True" + assert remove_outer_codeblocks(input_str) == expected + + # Test with no language specified + input_str = "```\ndef test():\n return True\n```" + expected = "def test():\n return True" + assert remove_outer_codeblocks(input_str) == expected + + # Test with no code blocks + input_str = "def test():\n return True" + assert remove_outer_codeblocks(input_str) == input_str + + +@patch("requests.post") +def test_get_completion(mock_post): + """Test OpenAI API completion function with mocked response.""" + # Setup mock response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"choices": [{"message": {"content": "Test response from OpenAI"}}]} + mock_post.return_value = mock_response + + # Test with basic messages + messages = [{"role": "system", "content": "You are a helpful assistant"}, {"role": "user", "content": "Hello"}] + + # Use a context manager for the environment variable + with patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}, clear=False): + with patch("actions.utils.openai_utils.OPENAI_API_KEY", "test-key"): + result = get_completion(messages, check_links=False) + + assert result == "Test response from OpenAI" + mock_post.assert_called_once() + + +@patch("requests.post") +@patch("actions.utils.openai_utils.check_links_in_string") +def test_get_completion_with_link_check(mock_check_links, mock_post): + """Test get_completion with link checking.""" + # Setup mocks + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"choices": [{"message": {"content": "Response with https://example.com link"}}]} + mock_post.return_value = mock_response + mock_check_links.return_value = True + + messages = [{"role": "user", "content": "Hello"}] + + # Use a context manager for the environment variable + with patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}, clear=False): + with patch("actions.utils.openai_utils.OPENAI_API_KEY", "test-key"): + result = get_completion(messages) + + assert result == "Response with https://example.com link" + mock_check_links.assert_called_once() diff --git a/tests/test_update_markdown_codeblocks.py b/tests/test_update_markdown_codeblocks.py new file mode 100644 index 0000000..606c459 --- /dev/null +++ b/tests/test_update_markdown_codeblocks.py @@ -0,0 +1,106 @@ +# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license + +from pathlib import Path +from unittest.mock import mock_open, patch + +from actions.update_markdown_code_blocks import ( + add_indentation, + extract_code_blocks, + generate_temp_filename, + process_markdown_file, + remove_indentation, +) + + +def test_extract_code_blocks(): + """Test extracting code blocks from markdown content.""" + # Test with Python and Bash code blocks + markdown_content = """ +# Test Markdown + +```python +def test(): + return True +``` + +And some bash code: + +```bash +echo "Hello World" +``` +""" + code_blocks = extract_code_blocks(markdown_content) + + assert len(code_blocks["python"]) == 1 + assert code_blocks["python"][0][1] == "def test():\n return True" + + assert len(code_blocks["bash"]) == 1 + assert code_blocks["bash"][0][1] == 'echo "Hello World"' + + +def test_remove_indentation(): + """Test removing indentation from code blocks.""" + code_block = " line 1\n line 2\n line 3" + result = remove_indentation(code_block, 4) + + assert result == "line 1\nline 2\nline 3" + + # Test with mixed indentation + code_block = " line 1\n line 2\n line 3" + result = remove_indentation(code_block, 2) + + assert result == " line 1\nline 2\n line 3" + + +def test_add_indentation(): + """Test adding indentation to code blocks.""" + code_block = "line 1\nline 2\nline 3" + result = add_indentation(code_block, 4) + + assert result == " line 1\n line 2\n line 3" + + # Test with empty lines + code_block = "line 1\n\nline 3" + result = add_indentation(code_block, 2) + + assert result == " line 1\n\n line 3" + + +def test_generate_temp_filename(): + """Test generating temporary filenames.""" + file_path = Path("docs/guide.md") + + filename = generate_temp_filename(file_path, 0, "python") + + assert "guide_docs_p0_" in filename + assert filename.endswith(".py") + + filename = generate_temp_filename(file_path, 1, "bash") + + assert "guide_docs_b1_" in filename + assert filename.endswith(".sh") + + +@patch("pathlib.Path.read_text") +@patch("pathlib.Path.write_text") +@patch("builtins.open", new_callable=mock_open) +def test_process_markdown_file(mock_file, mock_write, mock_read_text): + """Test processing markdown files.""" + mock_read_text.return_value = """ +# Test + +```python +def test(): + return True +``` +""" + + file_path = Path("test.md") + temp_dir = Path("temp") + + markdown_content, temp_files = process_markdown_file(file_path, temp_dir) + + assert markdown_content is not None + assert len(temp_files) == 1 + assert temp_files[0][1] == "def test():\n return True" + mock_file.assert_called_once()