summaryrefslogtreecommitdiffstats
path: root/gitlint-core/gitlint/tests/rules/test_user_rules.py
blob: fc8d423fbba453a5b7292ddc8fbbab3ff851b766 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
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))