From 9beae7348db40602207d7a59a83d413061d3d292 Mon Sep 17 00:00:00 2001 From: Mariano Fresno Date: Tue, 27 May 2025 10:44:45 -0300 Subject: [PATCH 1/3] chore: add test-and-build workflow, init file, dependencies, setup.py, and test suite - Created GitHub Actions workflow for testing and building the project. - Added `__init__.py` for package initialization. - Updated `requirements.txt` with new testing dependencies. - Created `setup.py` for package distribution. - Added test files for `assistant`, `convcommit`, `diff_generator`, `runner`, and utility functions. --- .github/workflows/test-and-build.yml | 88 ++++++++++++++++++++++++++++ __init__.py | 5 ++ requirements.txt | 5 +- setup.py | 25 ++++++++ tests/test_assistant.py | 44 ++++++++++++++ tests/test_convcommit.py | 75 ++++++++++++++++++++++++ tests/test_diff_generator.py | 51 ++++++++++++++++ tests/test_runner.py | 41 +++++++++++++ 8 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test-and-build.yml create mode 100644 __init__.py create mode 100644 setup.py create mode 100644 tests/test_assistant.py create mode 100644 tests/test_convcommit.py create mode 100644 tests/test_diff_generator.py create mode 100644 tests/test_runner.py diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml new file mode 100644 index 0000000..54f61e3 --- /dev/null +++ b/.github/workflows/test-and-build.yml @@ -0,0 +1,88 @@ +name: Test and Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.12] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests with coverage + run: | + pytest tests/ --cov=. --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: true + + build: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ghcr.io/mnofresno/convcommitgpt:latest + ghcr.io/mnofresno/convcommitgpt:${{ github.sha }} + platforms: linux/amd64,linux/arm64 + + test-install: + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Test installation + run: | + chmod +x test_install.sh + ./test_install.sh \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f195549 --- /dev/null +++ b/__init__.py @@ -0,0 +1,5 @@ +""" +ConvCommitGPT - A tool to generate commit messages using AI +""" + +__version__ = "1.0.0" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5fc415e..9571cef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ click==8.1.7 openai>=1.12.0 gitpython==3.1.43 -python-dotenv==1.0.1 \ No newline at end of file +python-dotenv==1.0.1 +pytest==8.0.0 +pytest-cov==4.1.0 +pytest-mock==3.12.0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..76c35ea --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup, find_packages + +setup( + name="convcommitgpt", + version="1.0.0", + packages=find_packages(), + install_requires=[ + "click==8.1.7", + "openai>=1.12.0", + "gitpython==3.1.43", + "python-dotenv==1.0.1", + ], + extras_require={ + "dev": [ + "pytest==8.0.0", + "pytest-cov==4.1.0", + "pytest-mock==3.12.0", + ], + }, + entry_points={ + "console_scripts": [ + "convcommit=convcommit:main", + ], + }, +) \ No newline at end of file diff --git a/tests/test_assistant.py b/tests/test_assistant.py new file mode 100644 index 0000000..08cf697 --- /dev/null +++ b/tests/test_assistant.py @@ -0,0 +1,44 @@ +import pytest +from unittest.mock import Mock, patch +from assistant import Assistant + +@pytest.fixture +def mock_response(): + return { + "choices": [ + { + "message": { + "content": "Test commit message" + } + } + ] + } + +@pytest.fixture +def assistant(): + return Assistant(api_url="http://test.com", api_key="test-key") + +def test_assistant_initialization(): + assistant = Assistant(api_url="http://test.com", api_key="test-key") + assert assistant.api_url == "http://test.com" + assert assistant.api_key == "test-key" + +@patch('assistant.httpx.post') +def test_assist_success(mock_post, assistant, mock_response): + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + result = assistant.assist("test diff", "test prompt", "test model") + + assert result == "Test commit message" + mock_post.assert_called_once() + +@patch('assistant.httpx.post') +def test_assist_error(mock_post, assistant): + mock_post.return_value.status_code = 400 + mock_post.return_value.text = "Error message" + + with pytest.raises(Exception) as exc_info: + assistant.assist("test diff", "test prompt", "test model") + + assert "Error message" in str(exc_info.value) \ No newline at end of file diff --git a/tests/test_convcommit.py b/tests/test_convcommit.py new file mode 100644 index 0000000..280bad7 --- /dev/null +++ b/tests/test_convcommit.py @@ -0,0 +1,75 @@ +import pytest +from unittest.mock import Mock, patch +from io import StringIO +from convcommit import main + +@pytest.fixture +def mock_diff_generator(): + with patch('convcommit.DiffGenerator') as mock: + mock.return_value.generate.return_value = "test diff" + yield mock + +@pytest.fixture +def mock_assistant(): + with patch('convcommit.Assistant') as mock: + instance = Mock() + instance.assist.return_value = "Test commit message" + mock.return_value = instance + yield mock + +@pytest.fixture +def mock_runner(): + with patch('convcommit.Runner') as mock: + instance = Mock() + instance.generate.return_value = "Test commit message" + mock.return_value = instance + yield mock + +def test_main_with_repository_path(mock_diff_generator, mock_assistant, mock_runner): + with patch('convcommit.click.echo') as mock_echo: + main( + repository_path="/test/repo", + prompt_file="test_prompt.md", + model="test-model", + openai_api_key="test-key", + base_url="http://test.com", + diff_from_stdin=None, + debug_diff=False, + max_bytes_in_diff=1024, + verbose=False + ) + + mock_echo.assert_called_with("Test commit message") + +def test_main_with_stdin(mock_assistant, mock_runner): + with patch('convcommit.click.echo') as mock_echo: + stdin = StringIO("test diff") + main( + repository_path=None, + prompt_file="test_prompt.md", + model="test-model", + openai_api_key="test-key", + base_url="http://test.com", + diff_from_stdin=stdin, + debug_diff=False, + max_bytes_in_diff=1024, + verbose=False + ) + + mock_echo.assert_called_with("Test commit message") + +def test_main_no_input(): + with pytest.raises(SystemExit) as exc_info: + main( + repository_path=None, + prompt_file="test_prompt.md", + model="test-model", + openai_api_key="test-key", + base_url="http://test.com", + diff_from_stdin=None, + debug_diff=False, + max_bytes_in_diff=1024, + verbose=False + ) + + assert exc_info.value.code == 1 \ No newline at end of file diff --git a/tests/test_diff_generator.py b/tests/test_diff_generator.py new file mode 100644 index 0000000..92c7e43 --- /dev/null +++ b/tests/test_diff_generator.py @@ -0,0 +1,51 @@ +import pytest +from pathlib import Path +from unittest.mock import Mock, patch +from diff_generator import DiffGenerator + +@pytest.fixture +def mock_git_diff(): + return """diff --git a/test.py b/test.py +index 1234567..89abcde 100644 +--- a/test.py ++++ b/test.py +@@ -1,2 +1,3 @@ + def test(): +- return True ++ return False +""" + +@pytest.fixture +def diff_generator(): + return DiffGenerator(max_bytes=1024) + +@patch('diff_generator.subprocess.run') +def test_generate_diff_success(mock_run, diff_generator, mock_git_diff): + mock_run.return_value = Mock( + returncode=0, + stdout=mock_git_diff.encode(), + stderr=b'' + ) + + result = diff_generator.generate(Path("/test/repo")) + + assert result == mock_git_diff + mock_run.assert_called_once() + +@patch('diff_generator.subprocess.run') +def test_generate_diff_error(mock_run, diff_generator): + mock_run.return_value = Mock( + returncode=1, + stdout=b'', + stderr=b'Error: not a git repository' + ) + + with pytest.raises(Exception) as exc_info: + diff_generator.generate(Path("/test/repo")) + + assert "Error: not a git repository" in str(exc_info.value) + +def test_max_bytes_limit(diff_generator): + long_diff = "x" * 2000 + result = diff_generator._limit_diff_size(long_diff) + assert len(result) <= 1024 \ No newline at end of file diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 0000000..c07c297 --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,41 @@ +import pytest +from unittest.mock import Mock, patch +from runner import Runner +from assistant import Assistant + +@pytest.fixture +def mock_assistant(): + return Mock(spec=Assistant) + +@pytest.fixture +def runner(mock_assistant): + return Runner(mock_assistant) + +def test_runner_initialization(): + assistant = Mock(spec=Assistant) + runner = Runner(assistant) + assert runner.assistant == assistant + +@patch('runner.open') +def test_generate_commit_message(mock_open, runner, mock_assistant): + # Mock file reading + mock_open.return_value.__enter__.return_value.read.return_value = "Test prompt" + + # Mock assistant response + mock_assistant.assist.return_value = "Test commit message" + + result = runner.generate("test diff", "test_prompt.md", "test-model") + + assert result == "Test commit message" + mock_assistant.assist.assert_called_once_with( + "test diff", + "Test prompt", + "test-model" + ) + +@patch('runner.open') +def test_generate_commit_message_file_not_found(mock_open, runner): + mock_open.side_effect = FileNotFoundError + + with pytest.raises(FileNotFoundError): + runner.generate("test diff", "nonexistent.md", "test-model") \ No newline at end of file From be5d160c69f7396987bfce32e760bfe28e6f1277 Mon Sep 17 00:00:00 2001 From: Mariano Fresno Date: Tue, 27 May 2025 10:49:30 -0300 Subject: [PATCH 2/3] Refactor build_and_push.sh, Dockerfile, and installation scripts to streamline the build process and enhance functionality. The build script now copies all files to the build directory and checks for essential files, including setup.py. The Dockerfile has been updated to copy all application files and install the package directly. Additionally, the installation script has been modified to download and extract the package directory, improving the installation process. New classes and methods have been added to support the assistant's functionality, including diff generation and commit message creation. --- Dockerfile | 8 +++++--- build_and_push.sh | 9 +++------ assistant.py => convcommitgpt/assistant.py | 0 convcommit.py => convcommitgpt/convcommit.py | 0 .../diff_generator.py | 0 runner.py => convcommitgpt/runner.py | 0 install.sh | 19 +++++++++++++------ setup.py | 2 +- tests/test_assistant.py | 2 +- tests/test_convcommit.py | 2 +- tests/test_diff_generator.py | 2 +- tests/test_runner.py | 4 ++-- 12 files changed, 27 insertions(+), 21 deletions(-) rename assistant.py => convcommitgpt/assistant.py (100%) rename convcommit.py => convcommitgpt/convcommit.py (100%) rename diff_generator.py => convcommitgpt/diff_generator.py (100%) rename runner.py => convcommitgpt/runner.py (100%) diff --git a/Dockerfile b/Dockerfile index 62545a8..23d6e0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,8 +16,10 @@ ENV OLLAMA_HOST=host.docker.internal ENV OLLAMA_PORT=11434 # Copy application files -COPY *.py ./ -COPY *.md ./ +COPY . . + +# Install the package +RUN pip install -e . # Set the entrypoint -ENTRYPOINT ["python", "convcommit.py"] +ENTRYPOINT ["python", "-m", "convcommitgpt.convcommit"] diff --git a/build_and_push.sh b/build_and_push.sh index 03a4563..1fb6130 100755 --- a/build_and_push.sh +++ b/build_and_push.sh @@ -54,17 +54,14 @@ prepare_build_dir() { BUILD_DIR=$(mktemp -d) print_message "Created build directory: $BUILD_DIR" "$GREEN" - # Copy all Python files, requirements.txt, and markdown files - cp *.py "$BUILD_DIR/" 2>/dev/null || true - cp requirements.txt "$BUILD_DIR/" 2>/dev/null || true - cp *.md "$BUILD_DIR/" 2>/dev/null || true - cp Dockerfile "$BUILD_DIR/" + # Copy all files to build directory + cp -r . "$BUILD_DIR/" # Change to build directory cd "$BUILD_DIR" # Verify essential files exist - if [ ! -f "requirements.txt" ] || [ ! -f "convcommit.py" ] || [ ! -f "Dockerfile" ]; then + if [ ! -f "setup.py" ] || [ ! -f "requirements.txt" ] || [ ! -f "Dockerfile" ]; then print_message "Error: Essential files are missing" "$RED" return 1 fi diff --git a/assistant.py b/convcommitgpt/assistant.py similarity index 100% rename from assistant.py rename to convcommitgpt/assistant.py diff --git a/convcommit.py b/convcommitgpt/convcommit.py similarity index 100% rename from convcommit.py rename to convcommitgpt/convcommit.py diff --git a/diff_generator.py b/convcommitgpt/diff_generator.py similarity index 100% rename from diff_generator.py rename to convcommitgpt/diff_generator.py diff --git a/runner.py b/convcommitgpt/runner.py similarity index 100% rename from runner.py rename to convcommitgpt/runner.py diff --git a/install.sh b/install.sh index 461ddf6..a649f96 100755 --- a/install.sh +++ b/install.sh @@ -121,11 +121,7 @@ download_files() { print_message "Downloading files..." "$YELLOW" cd /tmp local files=( - "convcommit.py" - "assistant.py" - "diff_generator.py" - "runner.py" - "spinner.py" + "setup.py" "requirements.txt" "instructions_prompt.md" "convcommit.sh" @@ -138,8 +134,19 @@ download_files() { fi done + # Download the package directory + if ! curl -sSL "https://github.com/mnofresno/convcommitgpt/archive/refs/heads/main.zip" -o "main.zip"; then + print_message "Error: Failed to download package" "$RED" + exit 1 + fi + + # Extract the package + unzip -q main.zip + rm main.zip + print_message "Copying files to installation directory..." "$YELLOW" - cp -r /tmp/*.py /tmp/*.txt /tmp/*.md /tmp/convcommit.sh ~/.local/lib/convcommitgpt/ + cp -r convcommitgpt-main/convcommitgpt ~/.local/lib/convcommitgpt/ + cp /tmp/*.py /tmp/*.txt /tmp/*.md /tmp/convcommit.sh ~/.local/lib/convcommitgpt/ cd - } diff --git a/setup.py b/setup.py index 76c35ea..78a9607 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ }, entry_points={ "console_scripts": [ - "convcommit=convcommit:main", + "convcommit=convcommitgpt.convcommit:main", ], }, ) \ No newline at end of file diff --git a/tests/test_assistant.py b/tests/test_assistant.py index 08cf697..758bfee 100644 --- a/tests/test_assistant.py +++ b/tests/test_assistant.py @@ -1,6 +1,6 @@ import pytest from unittest.mock import Mock, patch -from assistant import Assistant +from convcommitgpt.assistant import Assistant @pytest.fixture def mock_response(): diff --git a/tests/test_convcommit.py b/tests/test_convcommit.py index 280bad7..041a6fc 100644 --- a/tests/test_convcommit.py +++ b/tests/test_convcommit.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import Mock, patch from io import StringIO -from convcommit import main +from convcommitgpt.convcommit import main @pytest.fixture def mock_diff_generator(): diff --git a/tests/test_diff_generator.py b/tests/test_diff_generator.py index 92c7e43..3b467de 100644 --- a/tests/test_diff_generator.py +++ b/tests/test_diff_generator.py @@ -1,7 +1,7 @@ import pytest from pathlib import Path from unittest.mock import Mock, patch -from diff_generator import DiffGenerator +from convcommitgpt.diff_generator import DiffGenerator @pytest.fixture def mock_git_diff(): diff --git a/tests/test_runner.py b/tests/test_runner.py index c07c297..43394e8 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import Mock, patch -from runner import Runner -from assistant import Assistant +from convcommitgpt.runner import Runner +from convcommitgpt.assistant import Assistant @pytest.fixture def mock_assistant(): From 2793aab2ce430b5e15779ce105894f8722dbd498 Mon Sep 17 00:00:00 2001 From: Mariano Fresno Date: Tue, 27 May 2025 10:57:32 -0300 Subject: [PATCH 3/3] chore: update test-and-build workflow and add init file for tests - Modified the test-and-build workflow to include PYTHONPATH for pytest execution. - Added an `__init__.py` file to the tests directory to initialize the test suite for ConvCommitGPT. --- .github/workflows/test-and-build.yml | 2 +- tests/__init__.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index 54f61e3..68ef218 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -28,7 +28,7 @@ jobs: - name: Run tests with coverage run: | - pytest tests/ --cov=. --cov-report=xml + PYTHONPATH=$PYTHONPATH:$(pwd) pytest tests/ --cov=. --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..c13dbda --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Test suite for ConvCommitGPT +""" \ No newline at end of file