|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import re |
3 | 4 | import sys |
| 5 | +from collections.abc import Mapping |
4 | 6 | from io import StringIO |
| 7 | +from typing import Any |
5 | 8 |
|
6 | 9 | import pytest |
7 | 10 | from pytest_mock import MockFixture |
8 | 11 |
|
9 | 12 | from commitizen import cli, commands, git |
| 13 | +from commitizen.cz import registry |
| 14 | +from commitizen.cz.base import BaseCommitizen, ValidationResult |
10 | 15 | from commitizen.exceptions import ( |
11 | 16 | CommitMessageLengthExceededError, |
12 | 17 | InvalidCommandArgumentError, |
13 | 18 | InvalidCommitMessageError, |
14 | 19 | NoCommitsFoundError, |
15 | 20 | ) |
| 21 | +from commitizen.question import CzQuestion |
16 | 22 | from tests.utils import create_file_and_commit, skip_below_py_3_13 |
17 | 23 |
|
18 | 24 | COMMIT_LOG = [ |
@@ -519,6 +525,89 @@ def test_check_command_cli_overrides_config_message_length_limit( |
519 | 525 | ) |
520 | 526 |
|
521 | 527 |
|
| 528 | +class ValidationCz(BaseCommitizen): |
| 529 | + def questions(self) -> list[CzQuestion]: |
| 530 | + return [ |
| 531 | + {"type": "input", "name": "commit", "message": "Initial commit:\n"}, |
| 532 | + {"type": "input", "name": "issue_nb", "message": "ABC-123"}, |
| 533 | + ] |
| 534 | + |
| 535 | + def message(self, answers: Mapping[str, Any]) -> str: |
| 536 | + return f"{answers['issue_nb']}: {answers['commit']}" |
| 537 | + |
| 538 | + def schema(self) -> str: |
| 539 | + return "<issue_nb>: <commit>" |
| 540 | + |
| 541 | + def schema_pattern(self) -> str: |
| 542 | + return r"^(?P<issue_nb>[A-Z]{3}-\d+): (?P<commit>.*)$" |
| 543 | + |
| 544 | + def example(self) -> str: |
| 545 | + return "ABC-123: fixed a bug" |
| 546 | + |
| 547 | + def info(self) -> str: |
| 548 | + return "Commit message must start with an issue number like ABC-123" |
| 549 | + |
| 550 | + def validate_commit_message( |
| 551 | + self, |
| 552 | + *, |
| 553 | + commit_msg: str, |
| 554 | + pattern: re.Pattern[str], |
| 555 | + allow_abort: bool, |
| 556 | + allowed_prefixes: list[str], |
| 557 | + max_msg_length: int | None, |
| 558 | + commit_hash: str, |
| 559 | + ) -> ValidationResult: |
| 560 | + """Validate commit message against the pattern.""" |
| 561 | + if not commit_msg: |
| 562 | + return ValidationResult( |
| 563 | + allow_abort, [] if allow_abort else ["commit message is empty"] |
| 564 | + ) |
| 565 | + |
| 566 | + if any(map(commit_msg.startswith, allowed_prefixes)): |
| 567 | + return ValidationResult(True, []) |
| 568 | + |
| 569 | + if max_msg_length: |
| 570 | + msg_len = len(commit_msg.partition("\n")[0].strip()) |
| 571 | + if msg_len > max_msg_length: |
| 572 | + # TODO: capitalize the first letter of the error message for consistency in v5 |
| 573 | + raise CommitMessageLengthExceededError( |
| 574 | + f"commit validation: failed!\n" |
| 575 | + f"commit message length exceeds the limit.\n" |
| 576 | + f'commit "{commit_hash}": "{commit_msg}"\n' |
| 577 | + f"message length limit: {max_msg_length} (actual: {msg_len})" |
| 578 | + ) |
| 579 | + |
| 580 | + return ValidationResult( |
| 581 | + bool(pattern.match(commit_msg)), [f"pattern: {pattern.pattern}"] |
| 582 | + ) |
| 583 | + |
| 584 | + def format_exception_message( |
| 585 | + self, invalid_commits: list[tuple[git.GitCommit, list]] |
| 586 | + ) -> str: |
| 587 | + """Format commit errors.""" |
| 588 | + displayed_msgs_content = "\n".join( |
| 589 | + [ |
| 590 | + ( |
| 591 | + f'commit "{commit.rev}": "{commit.message}"\nerrors:\n\n'.join( |
| 592 | + f"- {error}" for error in errors |
| 593 | + ) |
| 594 | + ) |
| 595 | + for (commit, errors) in invalid_commits |
| 596 | + ] |
| 597 | + ) |
| 598 | + return ( |
| 599 | + "commit validation: failed!\n" |
| 600 | + "please enter a commit message in the commitizen format.\n" |
| 601 | + f"{displayed_msgs_content}" |
| 602 | + ) |
| 603 | + |
| 604 | + |
| 605 | +@pytest.fixture |
| 606 | +def use_cz_custom_validator(mocker): |
| 607 | + new_cz = {**registry, "cz_custom_validator": ValidationCz} |
| 608 | + mocker.patch.dict("commitizen.cz.registry", new_cz) |
| 609 | + |
| 610 | + |
522 | 611 | @pytest.mark.usefixtures("use_cz_custom_validator") |
523 | 612 | def test_check_command_with_custom_validator_succeed(mocker: MockFixture, capsys): |
524 | 613 | testargs = [ |
@@ -552,9 +641,13 @@ def test_check_command_with_custom_validator_failed(mocker: MockFixture): |
552 | 641 | mocker.patch.object(sys, "argv", testargs) |
553 | 642 | mocker.patch( |
554 | 643 | "commitizen.commands.check.open", |
555 | | - mocker.mock_open(read_data="ABC-123 add commitizen pre-commit hook"), |
| 644 | + mocker.mock_open( |
| 645 | + read_data="123-ABC issue id has wrong format and misses colon" |
| 646 | + ), |
556 | 647 | ) |
557 | 648 | with pytest.raises(InvalidCommitMessageError) as excinfo: |
558 | 649 | cli.main() |
559 | | - assert "commit validation: failed!" in str(excinfo.value) |
560 | | - assert "pattern: " in str(excinfo.value) |
| 650 | + assert "commit validation: failed!" in str(excinfo.value), ( |
| 651 | + "Pattern validation unexpectedly passed" |
| 652 | + ) |
| 653 | + assert "pattern: " in str(excinfo.value), "Pattern not found in error message" |
0 commit comments