diff options
Diffstat (limited to 'gitlint-core/gitlint/tests/rules')
-rw-r--r-- | gitlint-core/gitlint/tests/rules/__init__.py | 0 | ||||
-rw-r--r-- | gitlint-core/gitlint/tests/rules/test_body_rules.py | 236 | ||||
-rw-r--r-- | gitlint-core/gitlint/tests/rules/test_configuration_rules.py | 140 | ||||
-rw-r--r-- | gitlint-core/gitlint/tests/rules/test_meta_rules.py | 59 | ||||
-rw-r--r-- | gitlint-core/gitlint/tests/rules/test_rules.py | 23 | ||||
-rw-r--r-- | gitlint-core/gitlint/tests/rules/test_title_rules.py | 186 | ||||
-rw-r--r-- | gitlint-core/gitlint/tests/rules/test_user_rules.py | 256 |
7 files changed, 900 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..812c74a --- /dev/null +++ b/gitlint-core/gitlint/tests/rules/test_body_rules.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +from gitlint.tests.base import BaseTestCase +from gitlint import rules + + +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{0}\n".format("å" * 21)) # pylint: disable=consider-using-f-string + 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{0}\n".format("å" * 8)) # pylint: disable=consider-using-f-string + 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..9302da5 --- /dev/null +++ b/gitlint-core/gitlint/tests/rules/test_configuration_rules.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +from gitlint.tests.base import BaseTestCase +from gitlint import rules +from gitlint.config import LintConfig + + +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_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \ + "Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: all" + self.assert_log_contains(expected_log_message) + + # 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_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \ + "Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: T1,B2" + + 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_message = "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_log_contains(expected_log_message) + + # 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_message = "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_log_contains(expected_log_message) + + 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 + + # 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_message = ("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_log_contains(expected_log_message) + + # 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_message = ("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_log_contains(expected_log_message) + + 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 + self.assert_log_contains("DEBUG: gitlint.rules Ignoring line ' a relëase body' because it " + + "matches '(.*)relëase(.*)'") + + # 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..568ca3f --- /dev/null +++ b/gitlint-core/gitlint/tests/rules/test_meta_rules.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from gitlint.tests.base import BaseTestCase +from gitlint.rules import AuthorValidEmail, RuleViolation + + +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)]) + + 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)]) 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..6fcf9bc --- /dev/null +++ b/gitlint-core/gitlint/tests/rules/test_rules.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from gitlint.tests.base import BaseTestCase +from gitlint.rules import Rule, RuleViolation + + +class RuleTests(BaseTestCase): + + 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() + rule.log.debug("Tēst message") + self.assert_log_contains("DEBUG: gitlint.rules Tēst 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..10b4aab --- /dev/null +++ b/gitlint-core/gitlint/tests/rules/test_title_rules.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +from gitlint.tests.base import BaseTestCase +from gitlint.rules import TitleMaxLength, TitleTrailingWhitespace, TitleHardTab, TitleMustNotContainWord, \ + TitleTrailingPunctuation, TitleLeadingWhitespace, TitleRegexMatches, RuleViolation, TitleMinLength + + +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..d66a7cc --- /dev/null +++ b/gitlint-core/gitlint/tests/rules/test_user_rules.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- + +import os +import sys + +from gitlint.tests.base import BaseTestCase +from gitlint.rule_finder import find_rule_classes, assert_valid_rule_class +from gitlint.rules import UserRuleError + +from gitlint import options, rules + + +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 + + class MyCommitRuleClass(rules.CommitRule): + id = 'UC2' + name = 'my-cömmit-rule' + + def validate(self): + pass + + class MyConfigurationRuleClass(rules.ConfigurationRule): + id = 'UC3' + name = 'my-cönfiguration-rule' + + def apply(self): + pass + + # 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] # pylint: disable=bad-option-value,redefined-variable-type + 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) + + # validate attribute - not a method + MyRuleClass.validate = "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 + + # 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 # pylint: disable=bad-option-value,redefined-variable-type + self.assertIsNone(assert_valid_rule_class(MyRuleClass)) |