diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000..f4bbb59 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,44 @@ +name: CodSpeed + +permissions: + contents: read + +on: + push: + branches: + - "main" + paths: + - "commit_check/**" + - "tests/**" + - ".github/workflows/codspeed.yml" + - "pyproject.toml" + pull_request: + branches: + - "main" + paths: + - "commit_check/**" + - "tests/**" + - ".github/workflows/codspeed.yml" + - "pyproject.toml" + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: pip install -e .[test] + + - name: Run benchmarks + uses: CodSpeedHQ/action@v3 + with: + token: ${{ secrets.CODSPEED_TOKEN }} + run: pytest tests/ --codspeed diff --git a/pyproject.toml b/pyproject.toml index 3f0759e..8c0db4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ tracker = "https://github.com/commit-check/commit-check/issues" [project.optional-dependencies] dev = ['nox'] -test = ['coverage', 'pytest', 'pytest-mock'] +test = ['coverage', 'pytest', 'pytest-mock', 'pytest-codspeed'] docs = ['sphinx-immaterial', 'sphinx-autobuild'] [tool.setuptools] diff --git a/tests/author_test.py b/tests/author_test.py index 8936223..fcc4939 100644 --- a/tests/author_test.py +++ b/tests/author_test.py @@ -1,3 +1,4 @@ +import pytest from commit_check import PASS, FAIL from commit_check.author import check_author @@ -11,6 +12,7 @@ class TestAuthorName: fake_author_value_an = "fake_author_name" fake_accented_author_value_an = "fáké_áúthór_námé" + @pytest.mark.benchmark def test_check_author(self, mocker): # Must call get_commit_info, re.match. checks = [{ @@ -30,6 +32,7 @@ def test_check_author(self, mocker): assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 + @pytest.mark.benchmark def test_check_author_with_accented_letters(self, mocker): # Must call get_commit_info, re.match. checks = [{ @@ -49,6 +52,7 @@ def test_check_author_with_accented_letters(self, mocker): assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 + @pytest.mark.benchmark def test_check_author_with_empty_checks(self, mocker): # Must NOT call get_commit_info, re.match. with `checks` param with length 0. checks = [] @@ -65,6 +69,7 @@ def test_check_author_with_empty_checks(self, mocker): assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_author_with_different_check(self, mocker): # Must NOT call get_commit_info, re.match with not `author_name`. checks = [{ @@ -84,6 +89,7 @@ def test_check_author_with_different_check(self, mocker): assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_author_with_len0_regex(self, mocker, capfd): # Must NOT call get_commit_info, re.match with `regex` with length 0. checks = [ @@ -107,6 +113,7 @@ def test_check_author_with_len0_regex(self, mocker, capfd): out, _ = capfd.readouterr() assert "Not found regex for author_name." in out + @pytest.mark.benchmark def test_check_author_with_result_none(self, mocker): # Must call print_error_message, print_suggestion when re.match returns NONE. checks = [{ @@ -140,6 +147,7 @@ class TestAuthorEmail: # used by get_commit_info mock fake_author_value_ae = "fake_author_email" + @pytest.mark.benchmark def test_check_author(self, mocker): # Must call get_commit_info, re.match. checks = [{ @@ -159,6 +167,7 @@ def test_check_author(self, mocker): assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 + @pytest.mark.benchmark def test_check_author_with_empty_checks(self, mocker): # Must NOT call get_commit_info, re.match. with `checks` param with length 0. checks = [] @@ -175,6 +184,7 @@ def test_check_author_with_empty_checks(self, mocker): assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_author_with_different_check(self, mocker): # Must NOT call get_commit_info, re.match with not `author_email`. checks = [{ @@ -194,6 +204,7 @@ def test_check_author_with_different_check(self, mocker): assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_author_with_len0_regex(self, mocker, capfd): # Must NOT call get_commit_info, re.match with `regex` with length 0. checks = [ @@ -217,6 +228,7 @@ def test_check_author_with_len0_regex(self, mocker, capfd): out, _ = capfd.readouterr() assert "Not found regex for author_email." in out + @pytest.mark.benchmark def test_check_author_with_result_none(self, mocker): # Must call print_error_message, print_suggestion when re.match returns NONE. checks = [{ diff --git a/tests/branch_test.py b/tests/branch_test.py index 083c5bd..34e3a3b 100644 --- a/tests/branch_test.py +++ b/tests/branch_test.py @@ -1,3 +1,4 @@ +import pytest from commit_check import PASS, FAIL from commit_check.branch import check_branch, check_merge_base @@ -7,6 +8,7 @@ class TestCheckBranch: + @pytest.mark.benchmark def test_check_branch(self, mocker): # Must call get_branch_name, re.match at once. checks = [{ @@ -26,6 +28,7 @@ def test_check_branch(self, mocker): assert m_get_branch_name.call_count == 1 assert m_re_match.call_count == 1 + @pytest.mark.benchmark def test_check_branch_with_empty_checks(self, mocker): # Must NOT call get_branch_name, re.match with `checks` param with length 0. checks = [] @@ -42,6 +45,7 @@ def test_check_branch_with_empty_checks(self, mocker): assert m_get_branch_name.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_branch_with_different_check(self, mocker): # Must NOT call get_branch_name, re.match with not `branch`. checks = [{ @@ -61,6 +65,7 @@ def test_check_branch_with_different_check(self, mocker): assert m_get_branch_name.call_count == 0 assert m_re_match.call_count == 0 + @pytest.mark.benchmark def test_check_branch_with_len0_regex(self, mocker, capfd): # Must NOT call get_branch_name, re.match with `regex` with length 0. checks = [ @@ -84,6 +89,7 @@ def test_check_branch_with_len0_regex(self, mocker, capfd): out, _ = capfd.readouterr() assert "Not found regex for branch naming." in out + @pytest.mark.benchmark def test_check_branch_with_result_none(self, mocker): # Must call print_error_message, print_suggestion when re.match returns NONE. checks = [{ @@ -115,6 +121,7 @@ def test_check_branch_with_result_none(self, mocker): class TestCheckMergeBase: + @pytest.mark.benchmark def test_check_merge_base_with_empty_checks(self, mocker): checks = [] m_check_merge = mocker.patch(f"{LOCATION}.check_merge_base") @@ -122,7 +129,7 @@ def test_check_merge_base_with_empty_checks(self, mocker): assert retval == PASS assert m_check_merge.call_count == 0 - + @pytest.mark.benchmark def test_check_merge_base_with_empty_regex(self, mocker): checks = [{ "check": "merge_base", @@ -133,6 +140,7 @@ def test_check_merge_base_with_empty_regex(self, mocker): assert retval == PASS assert m_check_merge.call_count == 0 + @pytest.mark.benchmark def test_check_merge_base_with_different_check(self, mocker): checks = [{ "check": "branch", @@ -143,6 +151,7 @@ def test_check_merge_base_with_different_check(self, mocker): assert retval == PASS assert m_check_merge.call_count == 0 + @pytest.mark.benchmark def test_check_merge_base_fail_with_messages(self, mocker, capfd): checks = [{ "check": "merge_base", diff --git a/tests/commit_test.py b/tests/commit_test.py index a458636..c733968 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -1,3 +1,4 @@ +import pytest from commit_check import PASS, FAIL from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff @@ -9,11 +10,13 @@ MSG_FILE = '.git/COMMIT_EDITMSG' +@pytest.mark.benchmark def test_get_default_commit_msg_file(mocker): retval = get_default_commit_msg_file() assert retval == ".git/COMMIT_EDITMSG" +@pytest.mark.benchmark def test_read_commit_msg_from_existing_file(tmp_path): # Create a temporary file with a known content commit_msg_content = "Test commit message content." @@ -24,12 +27,14 @@ def test_read_commit_msg_from_existing_file(tmp_path): assert result == commit_msg_content +@pytest.mark.benchmark def test_read_commit_msg_file_not_found(mocker): m_commits_info = mocker.patch('commit_check.util.get_commit_info', return_value='mocked_commits_info') read_commit_msg("non_existent_file.txt") assert m_commits_info.call_count == 0 +@pytest.mark.benchmark def test_check_commit_msg_no_commit_msg_file(mocker): mock_get_default_commit_msg_file = mocker.patch( "commit_check.commit.get_default_commit_msg_file", @@ -49,6 +54,7 @@ def test_check_commit_msg_no_commit_msg_file(mocker): assert result == 0 +@pytest.mark.benchmark def test_check_commit_with_empty_checks(mocker): checks = [] m_re_match = mocker.patch( @@ -60,6 +66,7 @@ def test_check_commit_with_empty_checks(mocker): assert m_re_match.call_count == 0 +@pytest.mark.benchmark def test_check_commit_with_different_check(mocker): checks = [{ "check": "branch", @@ -74,6 +81,7 @@ def test_check_commit_with_different_check(mocker): assert m_re_match.call_count == 0 +@pytest.mark.benchmark def test_check_commit_with_len0_regex(mocker, capfd): checks = [ { @@ -92,6 +100,7 @@ def test_check_commit_with_len0_regex(mocker, capfd): assert "Not found regex for commit message." in out +@pytest.mark.benchmark def test_check_commit_with_result_none(mocker): checks = [{ "check": "message", @@ -116,6 +125,7 @@ def test_check_commit_with_result_none(mocker): assert m_print_suggestion.call_count == 1 +@pytest.mark.benchmark def test_check_commit_signoff(mocker): checks = [{ "check": "commit_signoff", @@ -140,6 +150,7 @@ def test_check_commit_signoff(mocker): assert m_print_suggestion.call_count == 1 +@pytest.mark.benchmark def test_check_commit_signoff_with_empty_regex(mocker): checks = [{ "check": "commit_signoff", @@ -156,6 +167,7 @@ def test_check_commit_signoff_with_empty_regex(mocker): assert m_re_match.call_count == 0 +@pytest.mark.benchmark def test_check_commit_signoff_with_empty_checks(mocker): checks = [] m_re_match = mocker.patch( diff --git a/tests/error_test.py b/tests/error_test.py index d0a2880..ab9e948 100644 --- a/tests/error_test.py +++ b/tests/error_test.py @@ -3,6 +3,7 @@ from commit_check.error import error_handler, log_and_exit +@pytest.mark.benchmark def test_error_handler_RuntimeError(): with pytest.raises(SystemExit) as exit_info: with error_handler(): @@ -10,6 +11,7 @@ def test_error_handler_RuntimeError(): assert exit_info.value.code == 1 +@pytest.mark.benchmark def test_error_handler_KeyboardInterrupt(): with pytest.raises(SystemExit) as exit_info: with error_handler(): @@ -17,6 +19,7 @@ def test_error_handler_KeyboardInterrupt(): assert exit_info.value.code == 130 +@pytest.mark.benchmark def test_error_handler_unexpected_error(): with pytest.raises(SystemExit) as exit_info: with error_handler(): @@ -24,6 +27,7 @@ def test_error_handler_unexpected_error(): assert exit_info.value.code == 3 +@pytest.mark.benchmark def test_error_handler_cannot_access(mocker): with pytest.raises(SystemExit): store_dir = "/fake/commit-check" @@ -50,6 +54,7 @@ def test_error_handler_cannot_access(mocker): mock_open().write.assert_any_call(f"Failed to write to log at {log_path}\n") +@pytest.mark.benchmark @pytest.mark.xfail def test_log_and_exit(monkeypatch): monkeypatch.setenv("COMMIT_CHECK_HOME", "") diff --git a/tests/main_test.py b/tests/main_test.py index ca77032..c232efd 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -7,6 +7,7 @@ class TestMain: + @pytest.mark.benchmark @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count", [ ([CMD, "--message"], 1, 0, 0, 0, 0), ([CMD, "--branch"], 0, 1, 0, 0, 0), @@ -53,6 +54,7 @@ def test_main( assert m_check_commit_signoff.call_count == check_commit_signoff_call_count assert m_check_merge_base.call_count == check_merge_base_call_count + @pytest.mark.benchmark def test_main_help(self, mocker, capfd): mocker.patch( "commit_check.main.validate_config", @@ -78,6 +80,7 @@ def test_main_help(self, mocker, capfd): stdout, _ = capfd.readouterr() assert "usage: " in stdout + @pytest.mark.benchmark def test_main_version(self, mocker): mocker.patch( "commit_check.main.validate_config", @@ -101,6 +104,7 @@ def test_main_version(self, mocker): assert m_check_commit_signoff.call_count == 0 assert m_check_merge_base.call_count == 0 + @pytest.mark.benchmark def test_main_validate_config_ret_none(self, mocker): mocker.patch( "commit_check.main.validate_config", @@ -116,6 +120,7 @@ def test_main_validate_config_ret_none(self, mocker): assert m_check_commit.call_count == 1 assert m_check_commit.call_args[0][0] == DEFAULT_CONFIG["checks"] + @pytest.mark.benchmark @pytest.mark.parametrize( "argv, message_result, branch_result, author_name_result, author_email_result, commit_signoff_result, merge_base_result, final_result", [ diff --git a/tests/util_test.py b/tests/util_test.py index e42ba24..c426831 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -15,6 +15,7 @@ class TestUtil: class TestGetBranchName: + @pytest.mark.benchmark def test_get_branch_name(self, mocker): # Must call cmd_output with given argument. m_cmd_output = mocker.patch( @@ -28,6 +29,7 @@ def test_get_branch_name(self, mocker): ] assert retval == "fake_branch_name" + @pytest.mark.benchmark def test_get_branch_name_with_exception(self, mocker): # Must return empty string when exception raises in cmd_output. m_cmd_output = mocker.patch( @@ -48,6 +50,7 @@ def test_get_branch_name_with_exception(self, mocker): assert retval == "" class TestHasCommits: + @pytest.mark.benchmark def test_has_commits_true(self, mocker): # Must return True when git rev-parse HEAD succeeds m_subprocess_run = mocker.patch( @@ -66,6 +69,7 @@ def test_has_commits_true(self, mocker): } assert retval is True + @pytest.mark.benchmark def test_has_commits_false(self, mocker): # Must return False when git rev-parse HEAD fails m_subprocess_run = mocker.patch( @@ -85,6 +89,7 @@ def test_has_commits_false(self, mocker): assert retval is False class TestGitMergeBase: + @pytest.mark.benchmark @pytest.mark.parametrize("returncode,expected", [ (0, 0), # ancestor exists (1, 1), # no ancestor @@ -109,6 +114,7 @@ def test_git_merge_base(self, mocker, returncode, expected): assert result == expected class TestGetCommitInfo: + @pytest.mark.benchmark @pytest.mark.parametrize("format_string", [ ("s"), ("an"), @@ -133,6 +139,7 @@ def test_get_commit_info(self, mocker, format_string): ] assert retval == " fake commit message " + @pytest.mark.benchmark def test_get_commit_info_no_commits(self, mocker): # Must return 'Repo has no commits yet.' when there are no commits. m_has_commits = mocker.patch( @@ -149,6 +156,7 @@ def test_get_commit_info_no_commits(self, mocker): assert m_cmd_output.call_count == 0 # Should not call cmd_output assert retval == "Repo has no commits yet." + @pytest.mark.benchmark def test_get_commit_info_with_exception(self, mocker): # Must return empty string when exception raises in cmd_output. m_has_commits = mocker.patch( @@ -182,6 +190,7 @@ def __init__(self, returncode, stdout, stderr): self.stdout = stdout self.stderr = stderr + @pytest.mark.benchmark def test_cmd_output(self, mocker): # Must subprocess.run with given argument. m_subprocess_run = mocker.patch( @@ -192,6 +201,7 @@ def test_cmd_output(self, mocker): assert m_subprocess_run.call_count == 1 assert retval == "ok" + @pytest.mark.benchmark @pytest.mark.parametrize("returncode, stdout, stderr", [ (1, "ok", "err"), (0, None, "err"), @@ -216,6 +226,7 @@ def test_cmd_output_err(self, mocker, returncode, stdout, stderr): "stdout": PIPE } + @pytest.mark.benchmark @pytest.mark.parametrize("returncode, stdout, stderr", [ (1, "ok", ""), (0, None, ""), @@ -241,6 +252,7 @@ def test_cmd_output_err_with_len0_stderr(self, mocker, returncode, stdout, stder } class TestValidateConfig: + @pytest.mark.benchmark def test_validate_config(self, mocker): # Must call yaml.safe_load. mocker.patch("builtins.open") @@ -253,6 +265,7 @@ def test_validate_config(self, mocker): assert m_yaml_safe_load.call_count == 1 assert retval == dummy_resp + @pytest.mark.benchmark def test_validate_config_file_not_found(self, mocker): # Must return empty dictionary when FileNotFoundError raises in built-in open. mocker.patch("builtins.open").side_effect = FileNotFoundError @@ -262,6 +275,7 @@ def test_validate_config_file_not_found(self, mocker): assert retval == {} class TestPrintErrorMessage: + @pytest.mark.benchmark def test_print_error_header(self, capfd): # Must print on stdout with given argument. print_error_header() @@ -269,6 +283,7 @@ def test_print_error_header(self, capfd): assert "Commit rejected by Commit-Check" in stdout assert "Commit rejected." in stdout + @pytest.mark.benchmark @pytest.mark.parametrize("check_type, type_failed_msg", [ ("message", "check failed =>"), ("branch", "check failed =>"), @@ -294,12 +309,14 @@ def test_print_error_message(self, capfd, check_type, type_failed_msg): assert dummy_error in stdout class TestPrintSuggestion: + @pytest.mark.benchmark def test_print_suggestion(self, capfd): # Must print on stdout with given argument. print_suggestion("dummy suggest") stdout, _ = capfd.readouterr() assert "Suggest:" in stdout + @pytest.mark.benchmark def test_print_suggestion_exit1(self, capfd): # Must exit with 1 when "" passed with pytest.raises(SystemExit) as e:
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: