diff options
Diffstat (limited to 'gitlint-core/gitlint/tests/rules/test_user_rules.py')
-rw-r--r-- | gitlint-core/gitlint/tests/rules/test_user_rules.py | 266 |
1 files changed, 266 insertions, 0 deletions
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)) |