summaryrefslogtreecommitdiffstats
path: root/gitlint-core/gitlint/tests/rules
diff options
context:
space:
mode:
Diffstat (limited to 'gitlint-core/gitlint/tests/rules')
-rw-r--r--gitlint-core/gitlint/tests/rules/__init__.py0
-rw-r--r--gitlint-core/gitlint/tests/rules/test_body_rules.py235
-rw-r--r--gitlint-core/gitlint/tests/rules/test_configuration_rules.py178
-rw-r--r--gitlint-core/gitlint/tests/rules/test_meta_rules.py80
-rw-r--r--gitlint-core/gitlint/tests/rules/test_rules.py32
-rw-r--r--gitlint-core/gitlint/tests/rules/test_title_rules.py200
-rw-r--r--gitlint-core/gitlint/tests/rules/test_user_rules.py266
7 files changed, 991 insertions, 0 deletions
diff --git a/gitlint-core/gitlint/tests/rules/__init__.py b/gitlint-core/gitlint/tests/rules/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gitlint-core/gitlint/tests/rules/__init__.py
diff --git a/gitlint-core/gitlint/tests/rules/test_body_rules.py b/gitlint-core/gitlint/tests/rules/test_body_rules.py
new file mode 100644
index 0000000..c142e6e
--- /dev/null
+++ b/gitlint-core/gitlint/tests/rules/test_body_rules.py
@@ -0,0 +1,235 @@
+from gitlint import rules
+from gitlint.tests.base import BaseTestCase
+
+
+class BodyRuleTests(BaseTestCase):
+ def test_max_line_length(self):
+ rule = rules.BodyMaxLineLength()
+
+ # assert no error
+ violation = rule.validate("å" * 80, None)
+ self.assertIsNone(violation)
+
+ # assert error on line length > 80
+ expected_violation = rules.RuleViolation("B1", "Line exceeds max length (81>80)", "å" * 81)
+ violations = rule.validate("å" * 81, None)
+ self.assertListEqual(violations, [expected_violation])
+
+ # set line length to 120, and check no violation on length 73
+ rule = rules.BodyMaxLineLength({"line-length": 120})
+ violations = rule.validate("å" * 73, None)
+ self.assertIsNone(violations)
+
+ # assert raise on 121
+ expected_violation = rules.RuleViolation("B1", "Line exceeds max length (121>120)", "å" * 121)
+ violations = rule.validate("å" * 121, None)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_trailing_whitespace(self):
+ rule = rules.BodyTrailingWhitespace()
+
+ # assert no error
+ violations = rule.validate("å", None)
+ self.assertIsNone(violations)
+
+ # trailing space
+ expected_violation = rules.RuleViolation("B2", "Line has trailing whitespace", "å ")
+ violations = rule.validate("å ", None)
+ self.assertListEqual(violations, [expected_violation])
+
+ # trailing tab
+ expected_violation = rules.RuleViolation("B2", "Line has trailing whitespace", "å\t")
+ violations = rule.validate("å\t", None)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_hard_tabs(self):
+ rule = rules.BodyHardTab()
+
+ # assert no error
+ violations = rule.validate("This is ã test", None)
+ self.assertIsNone(violations)
+
+ # contains hard tab
+ expected_violation = rules.RuleViolation("B3", "Line contains hard tab characters (\\t)", "This is å\ttest")
+ violations = rule.validate("This is å\ttest", None)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_body_first_line_empty(self):
+ rule = rules.BodyFirstLineEmpty()
+
+ # assert no error
+ commit = self.gitcommit("Tïtle\n\nThis is the secōnd body line")
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # second line not empty
+ expected_violation = rules.RuleViolation("B4", "Second line is not empty", "nöt empty", 2)
+
+ commit = self.gitcommit("Tïtle\nnöt empty\nThis is the secönd body line")
+ violations = rule.validate(commit)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_body_min_length(self):
+ rule = rules.BodyMinLength()
+
+ # assert no error - body is long enough
+ commit = self.gitcommit("Title\n\nThis is the second body line\n")
+
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert no error - no body
+ commit = self.gitcommit("Tïtle\n")
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # body is too short
+ expected_violation = rules.RuleViolation("B5", "Body message is too short (8<20)", "töoshort", 3)
+
+ commit = self.gitcommit("Tïtle\n\ntöoshort\n")
+ violations = rule.validate(commit)
+ self.assertListEqual(violations, [expected_violation])
+
+ # assert error - short across multiple lines
+ expected_violation = rules.RuleViolation("B5", "Body message is too short (11<20)", "secöndthïrd", 3)
+ commit = self.gitcommit("Tïtle\n\nsecönd\nthïrd\n")
+ violations = rule.validate(commit)
+ self.assertListEqual(violations, [expected_violation])
+
+ # set line length to 120, and check violation on length 21
+ expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", "å" * 21, 3)
+
+ rule = rules.BodyMinLength({"min-length": 120})
+ commit = self.gitcommit("Title\n\n{}\n".format("å" * 21))
+ violations = rule.validate(commit)
+ self.assertListEqual(violations, [expected_violation])
+
+ # Make sure we don't get the error if the body-length is exactly the min-length
+ rule = rules.BodyMinLength({"min-length": 8})
+ commit = self.gitcommit("Tïtle\n\n{}\n".format("å" * 8))
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ def test_body_missing(self):
+ rule = rules.BodyMissing()
+
+ # assert no error - body is present
+ commit = self.gitcommit("Tïtle\n\nThis ïs the first body line\n")
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # body is too short
+ expected_violation = rules.RuleViolation("B6", "Body message is missing", None, 3)
+
+ commit = self.gitcommit("Tïtle\n")
+ violations = rule.validate(commit)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_body_missing_multiple_empty_new_lines(self):
+ rule = rules.BodyMissing()
+
+ # body is too short
+ expected_violation = rules.RuleViolation("B6", "Body message is missing", None, 3)
+
+ commit = self.gitcommit("Tïtle\n\n\n\n")
+ violations = rule.validate(commit)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_body_missing_merge_commit(self):
+ rule = rules.BodyMissing()
+
+ # assert no error - merge commit
+ commit = self.gitcommit("Merge: Tïtle\n")
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert error for merge commits if ignore-merge-commits is disabled
+ rule = rules.BodyMissing({"ignore-merge-commits": False})
+ violations = rule.validate(commit)
+ expected_violation = rules.RuleViolation("B6", "Body message is missing", None, 3)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_body_changed_file_mention(self):
+ rule = rules.BodyChangedFileMention()
+
+ # assert no error when no files have changed and no files need to be mentioned
+ commit = self.gitcommit("This is a test\n\nHere is a mention of föo/test.py")
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert no error when no files have changed but certain files need to be mentioned on change
+ rule = rules.BodyChangedFileMention({"files": "bar.txt,föo/test.py"})
+ commit = self.gitcommit("This is a test\n\nHere is a mention of föo/test.py")
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert no error if a file has changed and is mentioned
+ commit = self.gitcommit("This is a test\n\nHere is a mention of föo/test.py", ["föo/test.py"])
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert no error if multiple files have changed and are mentioned
+ commit_msg = "This is a test\n\nHere is a mention of föo/test.py\nAnd here is a mention of bar.txt"
+ commit = self.gitcommit(commit_msg, ["föo/test.py", "bar.txt"])
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert error if file has changed and is not mentioned
+ commit_msg = "This is a test\n\nHere is å mention of\nAnd here is a mention of bar.txt"
+ commit = self.gitcommit(commit_msg, ["föo/test.py", "bar.txt"])
+ violations = rule.validate(commit)
+ expected_violation = rules.RuleViolation("B7", "Body does not mention changed file 'föo/test.py'", None, 4)
+ self.assertEqual([expected_violation], violations)
+
+ # assert multiple errors if multiple files have changed and are not mentioned
+ commit_msg = "This is å test\n\nHere is a mention of\nAnd here is a mention of"
+ commit = self.gitcommit(commit_msg, ["föo/test.py", "bar.txt"])
+ violations = rule.validate(commit)
+ expected_violation_2 = rules.RuleViolation("B7", "Body does not mention changed file 'bar.txt'", None, 4)
+ self.assertEqual([expected_violation_2, expected_violation], violations)
+
+ def test_body_match_regex(self):
+ # We intentionally add 2 newlines at the end of our commit message as that's how git will pass the
+ # message. This way we also test that the rule strips off the last line.
+ commit = self.gitcommit("US1234: åbc\nIgnored\nBödy\nFöo\nMy-Commit-Tag: föo\n\n")
+
+ # assert no violation on default regex (=everything allowed)
+ rule = rules.BodyRegexMatches()
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert no violation on matching regex
+ # (also note that first body line - in between title and rest of body - is ignored)
+ rule = rules.BodyRegexMatches({"regex": "^Bödy(.*)"})
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert we can do end matching (and last empty line is ignored)
+ # (also note that first body line - in between title and rest of body - is ignored)
+ rule = rules.BodyRegexMatches({"regex": "My-Commit-Tag: föo$"})
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # common use-case: matching that a given line is present
+ rule = rules.BodyRegexMatches({"regex": "(.*)Föo(.*)"})
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert violation on non-matching body
+ rule = rules.BodyRegexMatches({"regex": "^Tëst(.*)Foo"})
+ violations = rule.validate(commit)
+ expected_violation = rules.RuleViolation("B8", "Body does not match regex (^Tëst(.*)Foo)", None, 6)
+ self.assertListEqual(violations, [expected_violation])
+
+ # assert no violation on None regex
+ rule = rules.BodyRegexMatches({"regex": None})
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # Assert no issues when there's no body or a weird body variation
+ bodies = ["åbc", "åbc\n", "åbc\nföo\n", "åbc\n\n", "åbc\nföo\nblå", "åbc\nföo\nblå\n"]
+ for body in bodies:
+ commit = self.gitcommit(body)
+ rule = rules.BodyRegexMatches({"regex": ".*"})
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
diff --git a/gitlint-core/gitlint/tests/rules/test_configuration_rules.py b/gitlint-core/gitlint/tests/rules/test_configuration_rules.py
new file mode 100644
index 0000000..5935a4a
--- /dev/null
+++ b/gitlint-core/gitlint/tests/rules/test_configuration_rules.py
@@ -0,0 +1,178 @@
+from gitlint import rules
+from gitlint.config import LintConfig
+from gitlint.tests.base import (
+ EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING,
+ BaseTestCase,
+)
+
+
+class ConfigurationRuleTests(BaseTestCase):
+ def test_ignore_by_title(self):
+ commit = self.gitcommit("Releäse\n\nThis is the secōnd body line")
+
+ # No regex specified -> Config shouldn't be changed
+ rule = rules.IgnoreByTitle()
+ config = LintConfig()
+ rule.apply(config, commit)
+ self.assertEqual(config, LintConfig())
+ self.assert_logged([]) # nothing logged -> nothing ignored
+
+ # Matching regex -> expect config to ignore all rules
+ rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)"})
+ expected_config = LintConfig()
+ expected_config.ignore = "all"
+ rule.apply(config, commit)
+ self.assertEqual(config, expected_config)
+
+ expected_log_messages = [
+ EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I1", "ignore-by-title"),
+ "DEBUG: gitlint.rules Ignoring commit because of rule 'I1': "
+ "Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: all",
+ ]
+ self.assert_logged(expected_log_messages)
+
+ # Matching regex with specific ignore
+ rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)", "ignore": "T1,B2"})
+ expected_config = LintConfig()
+ expected_config.ignore = "T1,B2"
+ rule.apply(config, commit)
+ self.assertEqual(config, expected_config)
+
+ expected_log_messages += [
+ "DEBUG: gitlint.rules Ignoring commit because of rule 'I1': "
+ "Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: T1,B2"
+ ]
+ self.assert_logged(expected_log_messages)
+
+ def test_ignore_by_body(self):
+ commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
+
+ # No regex specified -> Config shouldn't be changed
+ rule = rules.IgnoreByBody()
+ config = LintConfig()
+ rule.apply(config, commit)
+ self.assertEqual(config, LintConfig())
+ self.assert_logged([]) # nothing logged -> nothing ignored
+
+ # Matching regex -> expect config to ignore all rules
+ rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)"})
+ expected_config = LintConfig()
+ expected_config.ignore = "all"
+ rule.apply(config, commit)
+ self.assertEqual(config, expected_config)
+
+ expected_log_messages = [
+ EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I2", "ignore-by-body"),
+ "DEBUG: gitlint.rules Ignoring commit because of rule 'I2': "
+ "Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)',"
+ " ignoring rules: all",
+ ]
+ self.assert_logged(expected_log_messages)
+
+ # Matching regex with specific ignore
+ rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)", "ignore": "T1,B2"})
+ expected_config = LintConfig()
+ expected_config.ignore = "T1,B2"
+ rule.apply(config, commit)
+ self.assertEqual(config, expected_config)
+
+ expected_log_messages += [
+ "DEBUG: gitlint.rules Ignoring commit because of rule 'I2': "
+ "Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)', ignoring rules: T1,B2"
+ ]
+ self.assert_logged(expected_log_messages)
+
+ def test_ignore_by_author_name(self):
+ commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line", author_name="Tëst nåme")
+
+ # No regex specified -> Config shouldn't be changed
+ rule = rules.IgnoreByAuthorName()
+ config = LintConfig()
+ rule.apply(config, commit)
+ self.assertEqual(config, LintConfig())
+ self.assert_logged([]) # nothing logged -> nothing ignored
+
+ # No author available -> rule is skipped and warning logged
+ staged_commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
+ rule = rules.IgnoreByAuthorName({"regex": "foo"})
+ config = LintConfig()
+ rule.apply(config, staged_commit)
+ self.assertEqual(config, LintConfig())
+ expected_log_messages = [
+ "WARNING: gitlint.rules ignore-by-author-name - I4: skipping - commit.author_name unknown. "
+ "Suggested fix: Use the --staged flag (or set general.staged=True in .gitlint). "
+ "More details: https://jorisroovers.com/gitlint/configuration/#staged"
+ ]
+ self.assert_logged(expected_log_messages)
+
+ # Non-Matching regex -> expect config to stay the same
+ rule = rules.IgnoreByAuthorName({"regex": "foo"})
+ expected_config = LintConfig()
+ rule.apply(config, commit)
+ self.assertEqual(config, LintConfig())
+
+ # Matching regex -> expect config to ignore all rules
+ rule = rules.IgnoreByAuthorName({"regex": "(.*)ëst(.*)"})
+ expected_config = LintConfig()
+ expected_config.ignore = "all"
+ rule.apply(config, commit)
+ self.assertEqual(config, expected_config)
+
+ expected_log_messages += [
+ EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I4", "ignore-by-author-name"),
+ "DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
+ "Commit Author Name 'Tëst nåme' matches the regex '(.*)ëst(.*)',"
+ " ignoring rules: all",
+ ]
+ self.assert_logged(expected_log_messages)
+
+ # Matching regex with specific ignore
+ rule = rules.IgnoreByAuthorName({"regex": "(.*)nåme", "ignore": "T1,B2"})
+ expected_config = LintConfig()
+ expected_config.ignore = "T1,B2"
+ rule.apply(config, commit)
+ self.assertEqual(config, expected_config)
+
+ expected_log_messages += [
+ "DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
+ "Commit Author Name 'Tëst nåme' matches the regex '(.*)nåme', ignoring rules: T1,B2"
+ ]
+ self.assert_logged(expected_log_messages)
+
+ def test_ignore_body_lines(self):
+ commit1 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
+ commit2 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
+
+ # no regex specified, nothing should have happened:
+ # commit and config should remain identical, log should be empty
+ rule = rules.IgnoreBodyLines()
+ config = LintConfig()
+ rule.apply(config, commit1)
+ self.assertEqual(commit1, commit2)
+ self.assertEqual(config, LintConfig())
+ self.assert_logged([])
+
+ # Matching regex
+ rule = rules.IgnoreBodyLines({"regex": "(.*)relëase(.*)"})
+ config = LintConfig()
+ rule.apply(config, commit1)
+ # Our modified commit should be identical to a commit that doesn't contain the specific line
+ expected_commit = self.gitcommit("Tïtle\n\nThis is\n line")
+ # The original message isn't touched by this rule, this way we always have a way to reference back to it,
+ # so assert it's not modified by setting it to the same as commit1
+ expected_commit.message.original = commit1.message.original
+ self.assertEqual(commit1, expected_commit)
+ self.assertEqual(config, LintConfig()) # config shouldn't have been modified
+ expected_log_messages = [
+ EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I3", "ignore-body-lines"),
+ "DEBUG: gitlint.rules Ignoring line ' a relëase body' because it " + "matches '(.*)relëase(.*)'",
+ ]
+ self.assert_logged(expected_log_messages)
+
+ # Non-Matching regex: no changes expected
+ commit1 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
+ rule = rules.IgnoreBodyLines({"regex": "(.*)föobar(.*)"})
+ config = LintConfig()
+ rule.apply(config, commit1)
+ self.assertEqual(commit1, commit2)
+ self.assertEqual(config, LintConfig()) # config shouldn't have been modified
diff --git a/gitlint-core/gitlint/tests/rules/test_meta_rules.py b/gitlint-core/gitlint/tests/rules/test_meta_rules.py
new file mode 100644
index 0000000..a574aa3
--- /dev/null
+++ b/gitlint-core/gitlint/tests/rules/test_meta_rules.py
@@ -0,0 +1,80 @@
+from gitlint.rules import AuthorValidEmail, RuleViolation
+from gitlint.tests.base import (
+ EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING,
+ BaseTestCase,
+)
+
+
+class MetaRuleTests(BaseTestCase):
+ def test_author_valid_email_rule(self):
+ rule = AuthorValidEmail()
+
+ # valid email addresses
+ valid_email_addresses = [
+ "föo@bar.com",
+ "Jöhn.Doe@bar.com",
+ "jöhn+doe@bar.com",
+ "jöhn/doe@bar.com",
+ "jöhn.doe@subdomain.bar.com",
+ ]
+ for email in valid_email_addresses:
+ commit = self.gitcommit("", author_email=email)
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # No email address (=allowed for now, as gitlint also lints messages passed via stdin that don't have an
+ # email address)
+ commit = self.gitcommit("")
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # Invalid email addresses: no TLD, no domain, no @, space anywhere (=valid but not allowed by gitlint)
+ invalid_email_addresses = [
+ "föo@bar",
+ "JöhnDoe",
+ "Jöhn Doe",
+ "Jöhn Doe@foo.com",
+ " JöhnDoe@foo.com",
+ "JöhnDoe@ foo.com",
+ "JöhnDoe@foo. com",
+ "JöhnDoe@foo. com",
+ "@bår.com",
+ "föo@.com",
+ ]
+ for email in invalid_email_addresses:
+ commit = self.gitcommit("", author_email=email)
+ violations = rule.validate(commit)
+ self.assertListEqual(violations, [RuleViolation("M1", "Author email for commit is invalid", email)])
+
+ # Ensure nothing is logged, this relates specifically to a deprecation warning on the use of
+ # re.match vs re.search in the rules (see issue #254)
+ # If no custom regex is used, the rule uses the default regex in combination with re.search
+ self.assert_logged([])
+
+ def test_author_valid_email_rule_custom_regex(self):
+ # regex=None -> the rule isn't applied
+ rule = AuthorValidEmail()
+ rule.options["regex"].set(None)
+ emailadresses = ["föo", None, "hür dür"]
+ for email in emailadresses:
+ commit = self.gitcommit("", author_email=email)
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # Custom domain
+ rule = AuthorValidEmail({"regex": "[^@]+@bår.com"})
+ valid_email_addresses = ["föo@bår.com", "Jöhn.Doe@bår.com", "jöhn+doe@bår.com", "jöhn/doe@bår.com"]
+ for email in valid_email_addresses:
+ commit = self.gitcommit("", author_email=email)
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # Invalid email addresses
+ invalid_email_addresses = ["föo@hur.com"]
+ for email in invalid_email_addresses:
+ commit = self.gitcommit("", author_email=email)
+ violations = rule.validate(commit)
+ self.assertListEqual(violations, [RuleViolation("M1", "Author email for commit is invalid", email)])
+
+ # When a custom regex is used, a warning should be logged by default
+ self.assert_logged([EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("M1", "author-valid-email")])
diff --git a/gitlint-core/gitlint/tests/rules/test_rules.py b/gitlint-core/gitlint/tests/rules/test_rules.py
new file mode 100644
index 0000000..b401372
--- /dev/null
+++ b/gitlint-core/gitlint/tests/rules/test_rules.py
@@ -0,0 +1,32 @@
+from gitlint.rules import Rule, RuleViolation
+from gitlint.tests.base import BaseTestCase
+
+
+class RuleTests(BaseTestCase):
+ def test_ruleviolation__str__(self):
+ expected = '57: rule-ïd Tēst message: "Tēst content"'
+ self.assertEqual(str(RuleViolation("rule-ïd", "Tēst message", "Tēst content", 57)), expected)
+
+ def test_rule_equality(self):
+ self.assertEqual(Rule(), Rule())
+ # Ensure rules are not equal if they differ on their attributes
+ for attr in ["id", "name", "target", "options"]:
+ rule = Rule()
+ setattr(rule, attr, "åbc")
+ self.assertNotEqual(Rule(), rule)
+
+ def test_rule_log(self):
+ rule = Rule()
+ self.assertIsNone(rule._log)
+ rule.log.debug("Tēst message")
+ self.assert_log_contains("DEBUG: gitlint.rules Tēst message")
+
+ # Assert the same logger is reused when logging multiple messages
+ log = rule._log
+ rule.log.debug("Anöther message")
+ self.assertEqual(log, rule._log)
+ self.assert_log_contains("DEBUG: gitlint.rules Anöther message")
+
+ def test_rule_violation_equality(self):
+ violation1 = RuleViolation("ïd1", "My messåge", "My cöntent", 1)
+ self.object_equality_test(violation1, ["rule_id", "message", "content", "line_nr"])
diff --git a/gitlint-core/gitlint/tests/rules/test_title_rules.py b/gitlint-core/gitlint/tests/rules/test_title_rules.py
new file mode 100644
index 0000000..cba3851
--- /dev/null
+++ b/gitlint-core/gitlint/tests/rules/test_title_rules.py
@@ -0,0 +1,200 @@
+from gitlint.rules import (
+ RuleViolation,
+ TitleHardTab,
+ TitleLeadingWhitespace,
+ TitleMaxLength,
+ TitleMinLength,
+ TitleMustNotContainWord,
+ TitleRegexMatches,
+ TitleTrailingPunctuation,
+ TitleTrailingWhitespace,
+)
+from gitlint.tests.base import BaseTestCase
+
+
+class TitleRuleTests(BaseTestCase):
+ def test_max_line_length(self):
+ rule = TitleMaxLength()
+
+ # assert no error
+ violation = rule.validate("å" * 72, None)
+ self.assertIsNone(violation)
+
+ # assert error on line length > 72
+ expected_violation = RuleViolation("T1", "Title exceeds max length (73>72)", "å" * 73)
+ violations = rule.validate("å" * 73, None)
+ self.assertListEqual(violations, [expected_violation])
+
+ # set line length to 120, and check no violation on length 73
+ rule = TitleMaxLength({"line-length": 120})
+ violations = rule.validate("å" * 73, None)
+ self.assertIsNone(violations)
+
+ # assert raise on 121
+ expected_violation = RuleViolation("T1", "Title exceeds max length (121>120)", "å" * 121)
+ violations = rule.validate("å" * 121, None)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_trailing_whitespace(self):
+ rule = TitleTrailingWhitespace()
+
+ # assert no error
+ violations = rule.validate("å", None)
+ self.assertIsNone(violations)
+
+ # trailing space
+ expected_violation = RuleViolation("T2", "Title has trailing whitespace", "å ")
+ violations = rule.validate("å ", None)
+ self.assertListEqual(violations, [expected_violation])
+
+ # trailing tab
+ expected_violation = RuleViolation("T2", "Title has trailing whitespace", "å\t")
+ violations = rule.validate("å\t", None)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_hard_tabs(self):
+ rule = TitleHardTab()
+
+ # assert no error
+ violations = rule.validate("This is å test", None)
+ self.assertIsNone(violations)
+
+ # contains hard tab
+ expected_violation = RuleViolation("T4", "Title contains hard tab characters (\\t)", "This is å\ttest")
+ violations = rule.validate("This is å\ttest", None)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_trailing_punctuation(self):
+ rule = TitleTrailingPunctuation()
+
+ # assert no error
+ violations = rule.validate("This is å test", None)
+ self.assertIsNone(violations)
+
+ # assert errors for different punctuations
+ punctuation = "?:!.,;"
+ for char in punctuation:
+ line = "This is å test" + char # note that make sure to include some unicode!
+ gitcontext = self.gitcontext(line)
+ expected_violation = RuleViolation("T3", f"Title has trailing punctuation ({char})", line)
+ violations = rule.validate(line, gitcontext)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_title_must_not_contain_word(self):
+ rule = TitleMustNotContainWord()
+
+ # no violations
+ violations = rule.validate("This is å test", None)
+ self.assertIsNone(violations)
+
+ # no violation if WIP occurs inside a word
+ violations = rule.validate("This is å wiping test", None)
+ self.assertIsNone(violations)
+
+ # match literally
+ violations = rule.validate("WIP This is å test", None)
+ expected_violation = RuleViolation(
+ "T5", "Title contains the word 'WIP' (case-insensitive)", "WIP This is å test"
+ )
+ self.assertListEqual(violations, [expected_violation])
+
+ # match case insensitive
+ violations = rule.validate("wip This is å test", None)
+ expected_violation = RuleViolation(
+ "T5", "Title contains the word 'WIP' (case-insensitive)", "wip This is å test"
+ )
+ self.assertListEqual(violations, [expected_violation])
+
+ # match if there is a colon after the word
+ violations = rule.validate("WIP:This is å test", None)
+ expected_violation = RuleViolation(
+ "T5", "Title contains the word 'WIP' (case-insensitive)", "WIP:This is å test"
+ )
+ self.assertListEqual(violations, [expected_violation])
+
+ # match multiple words
+ rule = TitleMustNotContainWord({"words": "wip,test,å"})
+ violations = rule.validate("WIP:This is å test", None)
+ expected_violation = RuleViolation(
+ "T5", "Title contains the word 'wip' (case-insensitive)", "WIP:This is å test"
+ )
+ expected_violation2 = RuleViolation(
+ "T5", "Title contains the word 'test' (case-insensitive)", "WIP:This is å test"
+ )
+ expected_violation3 = RuleViolation(
+ "T5", "Title contains the word 'å' (case-insensitive)", "WIP:This is å test"
+ )
+ self.assertListEqual(violations, [expected_violation, expected_violation2, expected_violation3])
+
+ def test_leading_whitespace(self):
+ rule = TitleLeadingWhitespace()
+
+ # assert no error
+ violations = rule.validate("a", None)
+ self.assertIsNone(violations)
+
+ # leading space
+ expected_violation = RuleViolation("T6", "Title has leading whitespace", " a")
+ violations = rule.validate(" a", None)
+ self.assertListEqual(violations, [expected_violation])
+
+ # leading tab
+ expected_violation = RuleViolation("T6", "Title has leading whitespace", "\ta")
+ violations = rule.validate("\ta", None)
+ self.assertListEqual(violations, [expected_violation])
+
+ # unicode test
+ expected_violation = RuleViolation("T6", "Title has leading whitespace", " ☺")
+ violations = rule.validate(" ☺", None)
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_regex_matches(self):
+ commit = self.gitcommit("US1234: åbc\n")
+
+ # assert no violation on default regex (=everything allowed)
+ rule = TitleRegexMatches()
+ violations = rule.validate(commit.message.title, commit)
+ self.assertIsNone(violations)
+
+ # assert no violation on matching regex
+ rule = TitleRegexMatches({"regex": "^US[0-9]*: å"})
+ violations = rule.validate(commit.message.title, commit)
+ self.assertIsNone(violations)
+
+ # assert violation when no matching regex
+ rule = TitleRegexMatches({"regex": "^UÅ[0-9]*"})
+ violations = rule.validate(commit.message.title, commit)
+ expected_violation = RuleViolation("T7", "Title does not match regex (^UÅ[0-9]*)", "US1234: åbc")
+ self.assertListEqual(violations, [expected_violation])
+
+ def test_min_line_length(self):
+ rule = TitleMinLength()
+
+ # assert no error
+ violation = rule.validate("å" * 72, None)
+ self.assertIsNone(violation)
+
+ # assert error on line length < 5
+ expected_violation = RuleViolation("T8", "Title is too short (4<5)", "å" * 4, 1)
+ violations = rule.validate("å" * 4, None)
+ self.assertListEqual(violations, [expected_violation])
+
+ # set line length to 3, and check no violation on length 4
+ rule = TitleMinLength({"min-length": 3})
+ violations = rule.validate("å" * 4, None)
+ self.assertIsNone(violations)
+
+ # assert no violations on length 3 (this asserts we've implemented a *strict* less than)
+ rule = TitleMinLength({"min-length": 3})
+ violations = rule.validate("å" * 3, None)
+ self.assertIsNone(violations)
+
+ # assert raise on 2
+ expected_violation = RuleViolation("T8", "Title is too short (2<3)", "å" * 2, 1)
+ violations = rule.validate("å" * 2, None)
+ self.assertListEqual(violations, [expected_violation])
+
+ # assert raise on empty title
+ expected_violation = RuleViolation("T8", "Title is too short (0<3)", "", 1)
+ violations = rule.validate("", None)
+ self.assertListEqual(violations, [expected_violation])
diff --git a/gitlint-core/gitlint/tests/rules/test_user_rules.py b/gitlint-core/gitlint/tests/rules/test_user_rules.py
new file mode 100644
index 0000000..8086bea
--- /dev/null
+++ b/gitlint-core/gitlint/tests/rules/test_user_rules.py
@@ -0,0 +1,266 @@
+import os
+import sys
+
+from gitlint import options, rules
+from gitlint.rule_finder import assert_valid_rule_class, find_rule_classes
+from gitlint.rules import UserRuleError
+from gitlint.tests.base import BaseTestCase
+
+
+class UserRuleTests(BaseTestCase):
+ def test_find_rule_classes(self):
+ # Let's find some user classes!
+ user_rule_path = self.get_sample_path("user_rules")
+ classes = find_rule_classes(user_rule_path)
+
+ # Compare string representations because we can't import MyUserCommitRule here since samples/user_rules is not
+ # a proper python package
+ # Note that the following check effectively asserts that:
+ # - There is only 1 rule recognized and it is MyUserCommitRule
+ # - Other non-python files in the directory are ignored
+ # - Other members of the my_commit_rules module are ignored
+ # (such as func_should_be_ignored, global_variable_should_be_ignored)
+ # - Rules are loaded non-recursively (user_rules/import_exception directory is ignored)
+ self.assertEqual("[<class 'my_commit_rules.MyUserCommitRule'>]", str(classes))
+
+ # Assert that we added the new user_rules directory to the system path and modules
+ self.assertIn(user_rule_path, sys.path)
+ self.assertIn("my_commit_rules", sys.modules)
+
+ # Do some basic asserts on our user rule
+ self.assertEqual(classes[0].id, "UC1")
+ self.assertEqual(classes[0].name, "my-üser-commit-rule")
+ expected_option = options.IntOption("violation-count", 1, "Number of violåtions to return")
+ self.assertListEqual(classes[0].options_spec, [expected_option])
+ self.assertTrue(hasattr(classes[0], "validate"))
+
+ # Test that we can instantiate the class and can execute run the validate method and that it returns the
+ # expected result
+ rule_class = classes[0]()
+ violations = rule_class.validate("false-commit-object (ignored)")
+ self.assertListEqual(violations, [rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1)])
+
+ # Have it return more violations
+ rule_class.options["violation-count"].value = 2
+ violations = rule_class.validate("false-commit-object (ignored)")
+ self.assertListEqual(
+ violations,
+ [
+ rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1),
+ rules.RuleViolation("UC1", "Commit violåtion 2", "Contënt 2", 2),
+ ],
+ )
+
+ def test_extra_path_specified_by_file(self):
+ # Test that find_rule_classes can handle an extra path given as a file name instead of a directory
+ user_rule_path = self.get_sample_path("user_rules")
+ user_rule_module = os.path.join(user_rule_path, "my_commit_rules.py")
+ classes = find_rule_classes(user_rule_module)
+
+ rule_class = classes[0]()
+ violations = rule_class.validate("false-commit-object (ignored)")
+ self.assertListEqual(violations, [rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1)])
+
+ def test_rules_from_init_file(self):
+ # Test that we can import rules that are defined in __init__.py files
+ # This also tests that we can import rules from python packages. This use to cause issues with pypy
+ # So this is also a regression test for that.
+ user_rule_path = self.get_sample_path(os.path.join("user_rules", "parent_package"))
+ classes = find_rule_classes(user_rule_path)
+
+ # convert classes to strings and sort them so we can compare them
+ class_strings = sorted(str(clazz) for clazz in classes)
+ expected = ["<class 'my_commit_rules.MyUserCommitRule'>", "<class 'parent_package.InitFileRule'>"]
+ self.assertListEqual(class_strings, expected)
+
+ def test_empty_user_classes(self):
+ # Test that we don't find rules if we scan a different directory
+ user_rule_path = self.get_sample_path("config")
+ classes = find_rule_classes(user_rule_path)
+ self.assertListEqual(classes, [])
+
+ # Importantly, ensure that the directory is not added to the syspath as this happens only when we actually
+ # find modules
+ self.assertNotIn(user_rule_path, sys.path)
+
+ def test_failed_module_import(self):
+ # test importing a bogus module
+ user_rule_path = self.get_sample_path("user_rules/import_exception")
+ # We don't check the entire error message because that is different based on the python version and underlying
+ # operating system
+ expected_msg = "Error while importing extra-path module 'invalid_python'"
+ with self.assertRaisesRegex(UserRuleError, expected_msg):
+ find_rule_classes(user_rule_path)
+
+ def test_find_rule_classes_nonexisting_path(self):
+ with self.assertRaisesMessage(UserRuleError, "Invalid extra-path: föo/bar"):
+ find_rule_classes("föo/bar")
+
+ def test_assert_valid_rule_class(self):
+ class MyLineRuleClass(rules.LineRule):
+ id = "UC1"
+ name = "my-lïne-rule"
+ target = rules.CommitMessageTitle
+
+ def validate(self):
+ pass # pragma: nocover
+
+ class MyCommitRuleClass(rules.CommitRule):
+ id = "UC2"
+ name = "my-cömmit-rule"
+
+ def validate(self):
+ pass # pragma: nocover
+
+ class MyConfigurationRuleClass(rules.ConfigurationRule):
+ id = "UC3"
+ name = "my-cönfiguration-rule"
+
+ def apply(self):
+ pass # pragma: nocover
+
+ # Just assert that no error is raised
+ self.assertIsNone(assert_valid_rule_class(MyLineRuleClass))
+ self.assertIsNone(assert_valid_rule_class(MyCommitRuleClass))
+ self.assertIsNone(assert_valid_rule_class(MyConfigurationRuleClass))
+
+ def test_assert_valid_rule_class_negative(self):
+ # general test to make sure that incorrect rules will raise an exception
+ user_rule_path = self.get_sample_path("user_rules/incorrect_linerule")
+ with self.assertRaisesMessage(
+ UserRuleError, "User-defined rule class 'MyUserLineRule' must have a 'validate' method"
+ ):
+ find_rule_classes(user_rule_path)
+
+ def test_assert_valid_rule_class_negative_parent(self):
+ # rule class must extend from LineRule or CommitRule
+ class MyRuleClass:
+ pass
+
+ expected_msg = (
+ "User-defined rule class 'MyRuleClass' must extend from gitlint.rules.LineRule, "
+ "gitlint.rules.CommitRule or gitlint.rules.ConfigurationRule"
+ )
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ def test_assert_valid_rule_class_negative_id(self):
+ for parent_class in [rules.LineRule, rules.CommitRule]:
+
+ class MyRuleClass(parent_class):
+ pass
+
+ # Rule class must have an id
+ expected_msg = "User-defined rule class 'MyRuleClass' must have an 'id' attribute"
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ # Rule ids must be non-empty
+ MyRuleClass.id = ""
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ # Rule ids must not start with one of the reserved id letters
+ for letter in ["T", "R", "B", "M", "I"]:
+ MyRuleClass.id = letter + "1"
+ expected_msg = (
+ f"The id '{letter}' of 'MyRuleClass' is invalid. Gitlint reserves ids starting with R,T,B,M,I"
+ )
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ def test_assert_valid_rule_class_negative_name(self):
+ for parent_class in [rules.LineRule, rules.CommitRule]:
+
+ class MyRuleClass(parent_class):
+ id = "UC1"
+
+ # Rule class must have a name
+ expected_msg = "User-defined rule class 'MyRuleClass' must have a 'name' attribute"
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ # Rule names must be non-empty
+ MyRuleClass.name = ""
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ def test_assert_valid_rule_class_negative_option_spec(self):
+ for parent_class in [rules.LineRule, rules.CommitRule]:
+
+ class MyRuleClass(parent_class):
+ id = "UC1"
+ name = "my-rüle-class"
+
+ # if set, option_spec must be a list of gitlint options
+ MyRuleClass.options_spec = "föo"
+ expected_msg = (
+ "The options_spec attribute of user-defined rule class 'MyRuleClass' must be a list "
+ "of gitlint.options.RuleOption"
+ )
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ # option_spec is a list, but not of gitlint options
+ MyRuleClass.options_spec = ["föo", 123]
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ def test_assert_valid_rule_class_negative_validate(self):
+ baseclasses = [rules.LineRule, rules.CommitRule]
+ for clazz in baseclasses:
+
+ class MyRuleClass(clazz):
+ id = "UC1"
+ name = "my-rüle-class"
+
+ with self.assertRaisesMessage(
+ UserRuleError, "User-defined rule class 'MyRuleClass' must have a 'validate' method"
+ ):
+ assert_valid_rule_class(MyRuleClass)
+
+ # validate attribute - not a method
+ MyRuleClass.validate = "föo"
+ with self.assertRaisesMessage(
+ UserRuleError, "User-defined rule class 'MyRuleClass' must have a 'validate' method"
+ ):
+ assert_valid_rule_class(MyRuleClass)
+
+ def test_assert_valid_rule_class_negative_apply(self):
+ class MyRuleClass(rules.ConfigurationRule):
+ id = "UCR1"
+ name = "my-rüle-class"
+
+ expected_msg = "User-defined Configuration rule class 'MyRuleClass' must have an 'apply' method"
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ # apply attribute - not a method
+ MyRuleClass.apply = "föo"
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ def test_assert_valid_rule_class_negative_target(self):
+ class MyRuleClass(rules.LineRule):
+ id = "UC1"
+ name = "my-rüle-class"
+
+ def validate(self):
+ pass # pragma: nocover
+
+ # no target
+ expected_msg = (
+ "The target attribute of the user-defined LineRule class 'MyRuleClass' must be either "
+ "gitlint.rules.CommitMessageTitle or gitlint.rules.CommitMessageBody"
+ )
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ # invalid target
+ MyRuleClass.target = "föo"
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ # valid target, no exception should be raised
+ MyRuleClass.target = rules.CommitMessageTitle
+ self.assertIsNone(assert_valid_rule_class(MyRuleClass))