diff options
Diffstat (limited to '')
52 files changed, 1062 insertions, 200 deletions
diff --git a/gitlint/tests/base.py b/gitlint/tests/base.py index add4d71..c8f68c4 100644 --- a/gitlint/tests/base.py +++ b/gitlint/tests/base.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- +import contextlib import copy import io import logging import os import re +import shutil +import tempfile try: # python 2.x @@ -21,7 +24,7 @@ except ImportError: from unittest.mock import patch # pylint: disable=no-name-in-module, import-error from gitlint.git import GitContext -from gitlint.utils import ustr, LOG_FORMAT, DEFAULT_ENCODING +from gitlint.utils import ustr, IS_PY2, LOG_FORMAT, DEFAULT_ENCODING # unittest2's assertRaisesRegex doesn't do unicode comparison. @@ -57,6 +60,15 @@ class BaseTestCase(unittest.TestCase): logging.getLogger('gitlint').propagate = False @staticmethod + @contextlib.contextmanager + def tempdir(): + tmpdir = tempfile.mkdtemp() + try: + yield tmpdir + finally: + shutil.rmtree(tmpdir) + + @staticmethod def get_sample_path(filename=""): # Don't join up empty files names because this will add a trailing slash if filename == "": @@ -73,6 +85,15 @@ class BaseTestCase(unittest.TestCase): return sample @staticmethod + def patch_input(side_effect): + """ Patches the built-in input() with a provided side-effect """ + module_path = "builtins.input" + if IS_PY2: + module_path = "__builtin__.raw_input" + patched_module = patch(module_path, side_effect=side_effect) + return patched_module + + @staticmethod def get_expected(filename="", variable_dict=None): """ Utility method to read an expected file from gitlint/tests/expected and return it as a string. Optionally replace template variables specified by variable_dict. """ @@ -129,6 +150,24 @@ class BaseTestCase(unittest.TestCase): return super(BaseTestCase, self).assertRaisesRegex(expected_exception, re.escape(expected_regex), *args, **kwargs) + @contextlib.contextmanager + def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name + """ Asserts an exception has occurred with a given error message """ + try: + yield + except expected_exception as exc: + exception_msg = ustr(exc) + if exception_msg != expected_msg: + error = u"Right exception, wrong message:\n got: {0}\n expected: {1}" + raise self.fail(error.format(exception_msg, expected_msg)) + # else: everything is fine, just return + return + except Exception as exc: + raise self.fail(u"Expected '{0}' got '{1}'".format(expected_exception.__name__, exc.__class__.__name__)) + + # No exception raised while we expected one + raise self.fail("Expected to raise {0}, didn't get an exception at all".format(expected_exception.__name__)) + def object_equality_test(self, obj, attr_list, ctor_kwargs=None): """ Helper function to easily implement object equality tests. Creates an object clone for every passed attribute and checks for (in)equality diff --git a/gitlint/tests/cli/test_cli.py b/gitlint/tests/cli/test_cli.py index 4d47f35..88bcfb7 100644 --- a/gitlint/tests/cli/test_cli.py +++ b/gitlint/tests/cli/test_cli.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -import contextlib + import io import os import sys import platform -import shutil -import tempfile import arrow @@ -34,15 +32,6 @@ from gitlint import __version__ from gitlint.utils import DEFAULT_ENCODING -@contextlib.contextmanager -def tempdir(): - tmpdir = tempfile.mkdtemp() - try: - yield tmpdir - finally: - shutil.rmtree(tmpdir) - - class CLITests(BaseTestCase): USAGE_ERROR_CODE = 253 GIT_CONTEXT_ERROR_CODE = 254 @@ -64,7 +53,8 @@ class CLITests(BaseTestCase): def get_system_info_dict(): """ Returns a dict with items related to system values logged by `gitlint --debug` """ return {'platform': platform.platform(), "python_version": sys.version, 'gitlint_version': __version__, - 'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'target': os.path.realpath(os.getcwd())} + 'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'target': os.path.realpath(os.getcwd()), + 'DEFAULT_ENCODING': DEFAULT_ENCODING} def test_version(self): """ Test for --version option """ @@ -118,7 +108,7 @@ class CLITests(BaseTestCase): with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["--commits", "foo...bar"]) - self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_lint_multiple_commits_1")) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_multiple_commits_1")) self.assertEqual(result.exit_code, 3) @patch('gitlint.cli.get_stdin_data', return_value=False) @@ -152,7 +142,7 @@ class CLITests(BaseTestCase): with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["--commits", "foo...bar"]) # We expect that the second commit has no failures because of 'gitlint-ignore: T3' in its commit msg body - self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_lint_multiple_commits_config_1")) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_multiple_commits_config_1")) self.assertEqual(result.exit_code, 3) @patch('gitlint.cli.get_stdin_data', return_value=False) @@ -205,7 +195,7 @@ class CLITests(BaseTestCase): """ Test for linting when a message is passed via stdin """ with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli) - self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_input_stream_1")) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_input_stream_1")) self.assertEqual(result.exit_code, 3) self.assertEqual(result.output, "") @@ -215,11 +205,11 @@ class CLITests(BaseTestCase): This tests specifically that git commit meta is not fetched when not passing --staged """ with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["--debug"]) - self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_input_stream_debug_1")) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_input_stream_debug_1")) self.assertEqual(result.exit_code, 3) self.assertEqual(result.output, "") expected_kwargs = self.get_system_info_dict() - expected_logs = self.get_expected('test_cli/test_input_stream_debug_2', expected_kwargs) + expected_logs = self.get_expected('cli/test_cli/test_input_stream_debug_2', expected_kwargs) self.assert_logged(expected_logs) @patch('gitlint.cli.get_stdin_data', return_value="Should be ignored\n") @@ -259,12 +249,12 @@ class CLITests(BaseTestCase): with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["--debug", "--staged"]) - self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_lint_staged_stdin_1")) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_staged_stdin_1")) self.assertEqual(result.exit_code, 3) self.assertEqual(result.output, "") expected_kwargs = self.get_system_info_dict() - expected_logs = self.get_expected('test_cli/test_lint_staged_stdin_2', expected_kwargs) + expected_logs = self.get_expected('cli/test_cli/test_lint_staged_stdin_2', expected_kwargs) self.assert_logged(expected_logs) @patch('arrow.now', return_value=arrow.get("2020-02-19T12:18:46.675182+01:00")) @@ -280,19 +270,19 @@ class CLITests(BaseTestCase): u"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree ] - with tempdir() as tmpdir: + with self.tempdir() as tmpdir: msg_filename = os.path.join(tmpdir, "msg") with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f: f.write(u"WIP: msg-filename tïtle\n") with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["--debug", "--staged", "--msg-filename", msg_filename]) - self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_lint_staged_msg_filename_1")) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_staged_msg_filename_1")) self.assertEqual(result.exit_code, 2) self.assertEqual(result.output, "") expected_kwargs = self.get_system_info_dict() - expected_logs = self.get_expected('test_cli/test_lint_staged_msg_filename_2', expected_kwargs) + expected_logs = self.get_expected('cli/test_cli/test_lint_staged_msg_filename_2', expected_kwargs) self.assert_logged(expected_logs) @patch('gitlint.cli.get_stdin_data', return_value=False) @@ -306,7 +296,7 @@ class CLITests(BaseTestCase): def test_msg_filename(self, _): expected_output = u"3: B6 Body message is missing\n" - with tempdir() as tmpdir: + with self.tempdir() as tmpdir: msg_filename = os.path.join(tmpdir, "msg") with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f: f.write(u"Commït title\n") @@ -375,7 +365,7 @@ class CLITests(BaseTestCase): u"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha> u"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree u"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00abc\n" - u"föo\nbar", + u"föobar\nbar", u"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha> u"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree ] @@ -394,7 +384,7 @@ class CLITests(BaseTestCase): expected_kwargs = self.get_system_info_dict() expected_kwargs.update({'config_path': config_path}) - expected_logs = self.get_expected('test_cli/test_debug_1', expected_kwargs) + expected_logs = self.get_expected('cli/test_cli/test_debug_1', expected_kwargs) self.assert_logged(expected_logs) @patch('gitlint.cli.get_stdin_data', return_value=u"Test tïtle\n") @@ -403,7 +393,7 @@ class CLITests(BaseTestCase): # Test extra-path pointing to a directory with patch('gitlint.display.stderr', new=StringIO()) as stderr: extra_path = self.get_sample_path("user_rules") - result = self.cli.invoke(cli.cli, ["--extra-path", extra_path, "--debug"]) + result = self.cli.invoke(cli.cli, ["--extra-path", extra_path]) expected_output = u"1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \ "3: B6 Body message is missing\n" self.assertEqual(stderr.getvalue(), expected_output) @@ -412,7 +402,7 @@ class CLITests(BaseTestCase): # Test extra-path pointing to a file with patch('gitlint.display.stderr', new=StringIO()) as stderr: extra_path = self.get_sample_path(os.path.join("user_rules", "my_commit_rules.py")) - result = self.cli.invoke(cli.cli, ["--extra-path", extra_path, "--debug"]) + result = self.cli.invoke(cli.cli, ["--extra-path", extra_path]) expected_output = u"1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \ "3: B6 Body message is missing\n" self.assertEqual(stderr.getvalue(), expected_output) @@ -423,7 +413,7 @@ class CLITests(BaseTestCase): # Test enabled contrib rules with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["--contrib", "contrib-title-conventional-commits,CC1"]) - expected_output = self.get_expected('test_cli/test_contrib_1') + expected_output = self.get_expected('cli/test_cli/test_contrib_1') self.assertEqual(stderr.getvalue(), expected_output) self.assertEqual(result.exit_code, 3) @@ -469,13 +459,14 @@ class CLITests(BaseTestCase): @patch('gitlint.cli.get_stdin_data', return_value=False) def test_target(self, _): """ Test for the --target option """ - os.environ["LANGUAGE"] = "C" # Force language to english so we can check for error message - result = self.cli.invoke(cli.cli, ["--target", "/tmp"]) - # We expect gitlint to tell us that /tmp is not a git repo (this proves that it takes the target parameter - # into account). - expected_path = os.path.realpath("/tmp") - self.assertEqual(result.output, "%s is not a git repository.\n" % expected_path) - self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE) + with self.tempdir() as tmpdir: + tmpdir_path = os.path.realpath(tmpdir) + os.environ["LANGUAGE"] = "C" # Force language to english so we can check for error message + result = self.cli.invoke(cli.cli, ["--target", tmpdir_path]) + # We expect gitlint to tell us that /tmp is not a git repo (this proves that it takes the target parameter + # into account). + self.assertEqual(result.output, "%s is not a git repository.\n" % tmpdir_path) + self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE) def test_target_negative(self): """ Negative test for the --target option """ @@ -539,3 +530,18 @@ class CLITests(BaseTestCase): self.assert_log_contains(u"DEBUG: gitlint.cli No commits in range \"master...HEAD\"") self.assertEqual(result.exit_code, 0) + + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP: tëst tïtle") + def test_named_rules(self, _): + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + config_path = self.get_sample_path(os.path.join("config", "named-rules")) + result = self.cli.invoke(cli.cli, ["--config", config_path, "--debug"]) + self.assertEqual(result.output, "") + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_named_rules_1")) + self.assertEqual(result.exit_code, 4) + + # Assert debug logs are correct + expected_kwargs = self.get_system_info_dict() + expected_kwargs.update({'config_path': config_path}) + expected_logs = self.get_expected('cli/test_cli/test_named_rules_2', expected_kwargs) + self.assert_logged(expected_logs) diff --git a/gitlint/tests/cli/test_cli_hooks.py b/gitlint/tests/cli/test_cli_hooks.py index 0564808..b5e7fc4 100644 --- a/gitlint/tests/cli/test_cli_hooks.py +++ b/gitlint/tests/cli/test_cli_hooks.py @@ -1,11 +1,19 @@ # -*- coding: utf-8 -*- +import io import os from click.testing import CliRunner try: # python 2.x + from StringIO import StringIO +except ImportError: + # python 3.x + from io import StringIO # pylint: disable=ungrouped-imports + +try: + # python 2.x from mock import patch except ImportError: # python 3.x @@ -16,6 +24,8 @@ from gitlint import cli from gitlint import hooks from gitlint import config +from gitlint.utils import DEFAULT_ENCODING + class CLIHookTests(BaseTestCase): USAGE_ERROR_CODE = 253 @@ -94,3 +104,165 @@ class CLIHookTests(BaseTestCase): expected_config = config.LintConfig() expected_config.target = os.path.realpath(os.getcwd()) uninstall_hook.assert_called_once_with(expected_config) + + def test_hook_no_tty(self): + """ Test for run-hook subcommand. + When no TTY is available (like is the case for this test), the hook will abort after the first check. + """ + + # No need to patch git as we're passing a msg-filename to run-hook, so no git calls are made. + # Note that this is the case when passing --staged as well, but that's tested as part of the integration tests + # (=end-to-end scenario). + + # Ideally we'd be able to assert that run-hook internally calls the lint cli command, but couldn't make + # that work. Have tried many different variatons of mocking and patching without avail. For now, we just + # check the output which indirectly proves the same thing. + + with self.tempdir() as tmpdir: + msg_filename = os.path.join(tmpdir, u"hür") + with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f: + f.write(u"WIP: tïtle\n") + + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) + self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_no_tty_1_stdout')) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_no_tty_1_stderr")) + + # exit code is 1 because aborted (no stdin available) + self.assertEqual(result.exit_code, 1) + + @patch('gitlint.cli.shell') + def test_hook_edit(self, shell): + """ Test for run-hook subcommand, answering 'e(dit)' after commit-hook """ + + set_editors = [None, u"myeditor"] + expected_editors = [u"vim -n", u"myeditor"] + commit_messages = [u"WIP: höok edit 1", u"WIP: höok edit 2"] + + for i in range(0, len(set_editors)): + if set_editors[i]: + os.environ['EDITOR'] = set_editors[i] + + with self.patch_input(['e', 'e', 'n']): + with self.tempdir() as tmpdir: + msg_filename = os.path.realpath(os.path.join(tmpdir, u"hür")) + with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f: + f.write(commit_messages[i] + "\n") + + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) + self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_edit_1_stdout', + {"commit_msg": commit_messages[i]})) + expected = self.get_expected("cli/test_cli_hooks/test_hook_edit_1_stderr", + {"commit_msg": commit_messages[i]}) + self.assertEqual(stderr.getvalue(), expected) + + # exit code = number of violations + self.assertEqual(result.exit_code, 2) + + shell.assert_called_with(expected_editors[i] + " " + msg_filename) + self.assert_log_contains(u"DEBUG: gitlint.cli run-hook: editing commit message") + self.assert_log_contains(u"DEBUG: gitlint.cli run-hook: {0} {1}".format(expected_editors[i], + msg_filename)) + + def test_hook_no(self): + """ Test for run-hook subcommand, answering 'n(o)' after commit-hook """ + + with self.patch_input(['n']): + with self.tempdir() as tmpdir: + msg_filename = os.path.join(tmpdir, u"hür") + with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f: + f.write(u"WIP: höok no\n") + + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) + self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_no_1_stdout')) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_no_1_stderr")) + + # We decided not to keep the commit message: hook returns number of violations (>0) + # This will cause git to abort the commit + self.assertEqual(result.exit_code, 2) + self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message declined") + + def test_hook_yes(self): + """ Test for run-hook subcommand, answering 'y(es)' after commit-hook """ + with self.patch_input(['y']): + with self.tempdir() as tmpdir: + msg_filename = os.path.join(tmpdir, u"hür") + with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f: + f.write(u"WIP: höok yes\n") + + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) + self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_yes_1_stdout')) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_yes_1_stderr")) + + # Exit code is 0 because we decide to keep the commit message + # This will cause git to keep the commit + self.assertEqual(result.exit_code, 0) + self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message accepted") + + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP: Test hook stdin tïtle\n") + def test_hook_stdin_violations(self, _): + """ Test for passing stdin data to run-hook, expecting some violations. Equivalent of: + $ echo "WIP: Test hook stdin tïtle" | gitlint run-hook + """ + + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["run-hook"]) + expected_stderr = self.get_expected('cli/test_cli_hooks/test_hook_stdin_violations_1_stderr') + self.assertEqual(stderr.getvalue(), expected_stderr) + self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_stdin_violations_1_stdout')) + # Hook will auto-abort because we're using stdin. Abort = exit code 1 + self.assertEqual(result.exit_code, 1) + + @patch('gitlint.cli.get_stdin_data', return_value=u"Test tïtle\n\nTest bödy that is long enough") + def test_hook_stdin_no_violations(self, _): + """ Test for passing stdin data to run-hook, expecting *NO* violations, Equivalent of: + $ echo -e "Test tïtle\n\nTest bödy that is long enough" | gitlint run-hook + """ + + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["run-hook"]) + self.assertEqual(stderr.getvalue(), "") # no errors = no stderr output + expected_stdout = self.get_expected('cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout') + self.assertEqual(result.output, expected_stdout) + self.assertEqual(result.exit_code, 0) + + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP: Test hook config tïtle\n") + def test_hook_config(self, _): + """ Test that gitlint still respects config when running run-hook, equivalent of: + $ echo "WIP: Test hook config tïtle" | gitlint -c title-max-length.line-length=5 --ignore B6 run-hook + """ + + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["-c", "title-max-length.line-length=5", "--ignore", "B6", "run-hook"]) + self.assertEqual(stderr.getvalue(), self.get_expected('cli/test_cli_hooks/test_hook_config_1_stderr')) + self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_config_1_stdout')) + # Hook will auto-abort because we're using stdin. Abort = exit code 1 + self.assertEqual(result.exit_code, 1) + + @patch('gitlint.cli.get_stdin_data', return_value=False) + @patch('gitlint.git.sh') + def test_hook_local_commit(self, sh, _): + """ Test running the hook on the last commit-msg from the local repo, equivalent of: + $ gitlint run-hook + and then choosing 'e' + """ + sh.git.side_effect = [ + "6f29bf81a8322a04071bb794666e48c443a90360", + u"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n" + u"WIP: commït-title\n\ncommït-body", + u"#", # git config --get core.commentchar + u"commit-1-branch-1\ncommit-1-branch-2\n", + u"file1.txt\npåth/to/file2.txt\n" + ] + + with self.patch_input(['e']): + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["run-hook"]) + expected = self.get_expected('cli/test_cli_hooks/test_hook_local_commit_1_stderr') + self.assertEqual(stderr.getvalue(), expected) + self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_local_commit_1_stdout')) + # If we can't edit the message, run-hook follows regular gitlint behavior and exit code = # violations + self.assertEqual(result.exit_code, 2) diff --git a/gitlint/tests/config/test_config.py b/gitlint/tests/config/test_config.py index d3fdc2c..b981a86 100644 --- a/gitlint/tests/config/test_config.py +++ b/gitlint/tests/config/test_config.py @@ -30,18 +30,18 @@ class LintConfigTests(BaseTestCase): # non-existing rule expected_error_msg = u"No such rule 'föobar'" - with self.assertRaisesRegex(LintConfigError, expected_error_msg): + with self.assertRaisesMessage(LintConfigError, expected_error_msg): config.set_rule_option(u'föobar', u'lïne-length', 60) # non-existing option expected_error_msg = u"Rule 'title-max-length' has no option 'föobar'" - with self.assertRaisesRegex(LintConfigError, expected_error_msg): + with self.assertRaisesMessage(LintConfigError, expected_error_msg): config.set_rule_option('title-max-length', u'föobar', 60) # invalid option value expected_error_msg = u"'föo' is not a valid value for option 'title-max-length.line-length'. " + \ u"Option 'line-length' must be a positive integer (current value: 'föo')." - with self.assertRaisesRegex(LintConfigError, expected_error_msg): + with self.assertRaisesMessage(LintConfigError, expected_error_msg): config.set_rule_option('title-max-length', 'line-length', u"föo") def test_set_general_option(self): @@ -124,7 +124,7 @@ class LintConfigTests(BaseTestCase): expected_rule_option = options.ListOption( "types", - ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert"], + ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"], "Comma separated list of allowed commit types.", ) @@ -151,14 +151,14 @@ class LintConfigTests(BaseTestCase): def test_contrib_negative(self): config = LintConfig() # non-existent contrib rule - with self.assertRaisesRegex(LintConfigError, u"No contrib rule with id or name 'föo' found."): + with self.assertRaisesMessage(LintConfigError, u"No contrib rule with id or name 'föo' found."): config.contrib = u"contrib-title-conventional-commits,föo" # UserRuleError, RuleOptionError should be re-raised as LintConfigErrors side_effects = [rules.UserRuleError(u"üser-rule"), options.RuleOptionError(u"rüle-option")] for side_effect in side_effects: with patch('gitlint.config.rule_finder.find_rule_classes', side_effect=side_effect): - with self.assertRaisesRegex(LintConfigError, ustr(side_effect)): + with self.assertRaisesMessage(LintConfigError, ustr(side_effect)): config.contrib = u"contrib-title-conventional-commits" def test_extra_path(self): @@ -185,36 +185,36 @@ class LintConfigTests(BaseTestCase): config = LintConfig() regex = u"Option extra-path must be either an existing directory or file (current value: 'föo/bar')" # incorrect extra_path - with self.assertRaisesRegex(LintConfigError, regex): + with self.assertRaisesMessage(LintConfigError, regex): config.extra_path = u"föo/bar" # extra path contains classes with errors - with self.assertRaisesRegex(LintConfigError, - "User-defined rule class 'MyUserLineRule' must have a 'validate' method"): + with self.assertRaisesMessage(LintConfigError, + "User-defined rule class 'MyUserLineRule' must have a 'validate' method"): config.extra_path = self.get_sample_path("user_rules/incorrect_linerule") def test_set_general_option_negative(self): config = LintConfig() # Note that we shouldn't test whether we can set unicode because python just doesn't allow unicode attributes - with self.assertRaisesRegex(LintConfigError, "'foo' is not a valid gitlint option"): + with self.assertRaisesMessage(LintConfigError, "'foo' is not a valid gitlint option"): config.set_general_option("foo", u"bår") # try setting _config_path, this is a real attribute of LintConfig, but the code should prevent it from # being set - with self.assertRaisesRegex(LintConfigError, "'_config_path' is not a valid gitlint option"): + with self.assertRaisesMessage(LintConfigError, "'_config_path' is not a valid gitlint option"): config.set_general_option("_config_path", u"bår") # invalid verbosity incorrect_values = [-1, u"föo"] for value in incorrect_values: expected_msg = u"Option 'verbosity' must be a positive integer (current value: '{0}')".format(value) - with self.assertRaisesRegex(LintConfigError, expected_msg): + with self.assertRaisesMessage(LintConfigError, expected_msg): config.verbosity = value incorrect_values = [4] for value in incorrect_values: - with self.assertRaisesRegex(LintConfigError, "Option 'verbosity' must be set between 0 and 3"): + with self.assertRaisesMessage(LintConfigError, "Option 'verbosity' must be set between 0 and 3"): config.verbosity = value # invalid ignore_xxx_commits @@ -224,8 +224,8 @@ class LintConfigTests(BaseTestCase): for attribute in ignore_attributes: for value in incorrect_values: option_name = attribute.replace("_", "-") - with self.assertRaisesRegex(LintConfigError, - "Option '{0}' must be either 'true' or 'false'".format(option_name)): + with self.assertRaisesMessage(LintConfigError, + "Option '{0}' must be either 'true' or 'false'".format(option_name)): setattr(config, attribute, value) # invalid ignore -> not here because ignore is a ListOption which converts everything to a string before @@ -234,15 +234,15 @@ class LintConfigTests(BaseTestCase): # invalid boolean options for attribute in ['debug', 'staged', 'ignore_stdin']: option_name = attribute.replace("_", "-") - with self.assertRaisesRegex(LintConfigError, - "Option '{0}' must be either 'true' or 'false'".format(option_name)): + with self.assertRaisesMessage(LintConfigError, + "Option '{0}' must be either 'true' or 'false'".format(option_name)): setattr(config, attribute, u"föobar") # extra-path has its own negative test # invalid target - with self.assertRaisesRegex(LintConfigError, - u"Option target must be an existing directory (current value: 'föo/bar')"): + with self.assertRaisesMessage(LintConfigError, + u"Option target must be an existing directory (current value: 'föo/bar')"): config.target = u"föo/bar" def test_ignore_independent_from_rules(self): @@ -254,6 +254,30 @@ class LintConfigTests(BaseTestCase): self.assertEqual(config.ignore, ["T1", "T2"]) self.assertSequenceEqual(config.rules, original_rules) + def test_config_equality(self): + self.assertEqual(LintConfig(), LintConfig()) + self.assertNotEqual(LintConfig(), LintConfigGenerator()) + + # Ensure LintConfig are not equal if they differ on their attributes + attrs = [("verbosity", 1), ("rules", []), ("ignore_stdin", True), ("debug", True), + ("ignore", ["T1"]), ("staged", True), ("_config_path", self.get_sample_path()), + ("ignore_merge_commits", False), ("ignore_fixup_commits", False), + ("ignore_squash_commits", False), ("ignore_revert_commits", False), + ("extra_path", self.get_sample_path("user_rules")), ("target", self.get_sample_path()), + ("contrib", ["CC1"])] + for attr, val in attrs: + config = LintConfig() + setattr(config, attr, val) + self.assertNotEqual(LintConfig(), config) + + # Other attributes don't matter + config1 = LintConfig() + config2 = LintConfig() + config1.foo = u"bår" + self.assertEqual(config1, config2) + config2.foo = u"dūr" + self.assertEqual(config1, config2) + class LintConfigGeneratorTests(BaseTestCase): @staticmethod diff --git a/gitlint/tests/config/test_config_builder.py b/gitlint/tests/config/test_config_builder.py index 051a52f..5a28c9f 100644 --- a/gitlint/tests/config/test_config_builder.py +++ b/gitlint/tests/config/test_config_builder.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- +import copy from gitlint.tests.base import BaseTestCase from gitlint.config import LintConfig, LintConfigBuilder, LintConfigError +from gitlint import rules + class LintConfigBuilderTests(BaseTestCase): def test_set_option(self): @@ -88,12 +91,13 @@ class LintConfigBuilderTests(BaseTestCase): # bad config file load foo_path = self.get_sample_path(u"föo") expected_error_msg = u"Invalid file path: {0}".format(foo_path) - with self.assertRaisesRegex(LintConfigError, expected_error_msg): + with self.assertRaisesMessage(LintConfigError, expected_error_msg): config_builder.set_from_config_file(foo_path) # error during file parsing path = self.get_sample_path("config/no-sections") expected_error_msg = u"File contains no section headers." + # We only match the start of the message here, since the exact message can vary depending on platform with self.assertRaisesRegex(LintConfigError, expected_error_msg): config_builder.set_from_config_file(path) @@ -102,7 +106,7 @@ class LintConfigBuilderTests(BaseTestCase): config_builder = LintConfigBuilder() config_builder.set_from_config_file(path) expected_error_msg = u"No such rule 'föobar'" - with self.assertRaisesRegex(LintConfigError, expected_error_msg): + with self.assertRaisesMessage(LintConfigError, expected_error_msg): config_builder.build() # non-existing general option @@ -110,7 +114,7 @@ class LintConfigBuilderTests(BaseTestCase): config_builder = LintConfigBuilder() config_builder.set_from_config_file(path) expected_error_msg = u"'foo' is not a valid gitlint option" - with self.assertRaisesRegex(LintConfigError, expected_error_msg): + with self.assertRaisesMessage(LintConfigError, expected_error_msg): config_builder.build() # non-existing option @@ -118,7 +122,7 @@ class LintConfigBuilderTests(BaseTestCase): config_builder = LintConfigBuilder() config_builder.set_from_config_file(path) expected_error_msg = u"Rule 'title-max-length' has no option 'föobar'" - with self.assertRaisesRegex(LintConfigError, expected_error_msg): + with self.assertRaisesMessage(LintConfigError, expected_error_msg): config_builder.build() # invalid option value @@ -127,7 +131,7 @@ class LintConfigBuilderTests(BaseTestCase): config_builder.set_from_config_file(path) expected_error_msg = u"'föo' is not a valid value for option 'title-max-length.line-length'. " + \ u"Option 'line-length' must be a positive integer (current value: 'föo')." - with self.assertRaisesRegex(LintConfigError, expected_error_msg): + with self.assertRaisesMessage(LintConfigError, expected_error_msg): config_builder.build() def test_set_config_from_string_list(self): @@ -150,27 +154,27 @@ class LintConfigBuilderTests(BaseTestCase): # assert error on incorrect rule - this happens at build time config_builder.set_config_from_string_list([u"föo.bar=1"]) - with self.assertRaisesRegex(LintConfigError, u"No such rule 'föo'"): + with self.assertRaisesMessage(LintConfigError, u"No such rule 'föo'"): config_builder.build() # no equal sign expected_msg = u"'föo.bar' is an invalid configuration option. Use '<rule>.<option>=<value>'" - with self.assertRaisesRegex(LintConfigError, expected_msg): + with self.assertRaisesMessage(LintConfigError, expected_msg): config_builder.set_config_from_string_list([u"föo.bar"]) # missing value expected_msg = u"'föo.bar=' is an invalid configuration option. Use '<rule>.<option>=<value>'" - with self.assertRaisesRegex(LintConfigError, expected_msg): + with self.assertRaisesMessage(LintConfigError, expected_msg): config_builder.set_config_from_string_list([u"föo.bar="]) # space instead of equal sign expected_msg = u"'föo.bar 1' is an invalid configuration option. Use '<rule>.<option>=<value>'" - with self.assertRaisesRegex(LintConfigError, expected_msg): + with self.assertRaisesMessage(LintConfigError, expected_msg): config_builder.set_config_from_string_list([u"föo.bar 1"]) # no period between rule and option names expected_msg = u"'föobar=1' is an invalid configuration option. Use '<rule>.<option>=<value>'" - with self.assertRaisesRegex(LintConfigError, expected_msg): + with self.assertRaisesMessage(LintConfigError, expected_msg): config_builder.set_config_from_string_list([u'föobar=1']) def test_rebuild_config(self): @@ -201,3 +205,60 @@ class LintConfigBuilderTests(BaseTestCase): # Modify the original and make sure we're not modifying the clone (i.e. check that the copy is a deep copy) config_builder.set_option('title-max-length', 'line-length', 120) self.assertDictEqual(cloned_builder._config_blueprint, expected) + + def test_named_rules(self): + # Store a copy of the default rules from the config, so we can reference it later + config_builder = LintConfigBuilder() + config = config_builder.build() + default_rules = copy.deepcopy(config.rules) + self.assertEqual(default_rules, config.rules) # deepcopy should be equal + + # Add a named rule by setting an option in the config builder that follows the named rule pattern + # Assert that whitespace in the rule name is stripped + rule_qualifiers = [u'T7:my-extra-rüle', u' T7 : my-extra-rüle ', u'\tT7:\tmy-extra-rüle\t', + u'T7:\t\n \tmy-extra-rüle\t\n\n', u"title-match-regex:my-extra-rüle"] + for rule_qualifier in rule_qualifiers: + config_builder = LintConfigBuilder() + config_builder.set_option(rule_qualifier, 'regex', u"föo") + + expected_rules = copy.deepcopy(default_rules) + my_rule = rules.TitleRegexMatches({'regex': u"föo"}) + my_rule.id = rules.TitleRegexMatches.id + u":my-extra-rüle" + my_rule.name = rules.TitleRegexMatches.name + u":my-extra-rüle" + expected_rules._rules[u'T7:my-extra-rüle'] = my_rule + self.assertEqual(config_builder.build().rules, expected_rules) + + # assert that changing an option on the newly added rule is passed correctly to the RuleCollection + # we try this with all different rule qualifiers to ensure they all are normalized and map + # to the same rule + for other_rule_qualifier in rule_qualifiers: + cb = config_builder.clone() + cb.set_option(other_rule_qualifier, 'regex', other_rule_qualifier + u"bōr") + # before setting the expected rule option value correctly, the RuleCollection should be different + self.assertNotEqual(cb.build().rules, expected_rules) + # after setting the option on the expected rule, it should be equal + my_rule.options['regex'].set(other_rule_qualifier + u"bōr") + self.assertEqual(cb.build().rules, expected_rules) + my_rule.options['regex'].set(u"wrong") + + def test_named_rules_negative(self): + # T7 = title-match-regex + # Invalid rule name + for invalid_name in ["", " ", " ", "\t", "\n", u"å b", u"å:b", u"åb:", u":åb"]: + config_builder = LintConfigBuilder() + config_builder.set_option(u"T7:{0}".format(invalid_name), 'regex', u"tëst") + expected_msg = u"The rule-name part in 'T7:{0}' cannot contain whitespace, colons or be empty" + with self.assertRaisesMessage(LintConfigError, expected_msg.format(invalid_name)): + config_builder.build() + + # Invalid parent rule name + config_builder = LintConfigBuilder() + config_builder.set_option(u"Ž123:foöbar", u"fåke-option", u"fåke-value") + with self.assertRaisesMessage(LintConfigError, u"No such rule 'Ž123' (named rule: 'Ž123:foöbar')"): + config_builder.build() + + # Invalid option name (this is the same as with regular rules) + config_builder = LintConfigBuilder() + config_builder.set_option(u"T7:foöbar", u"blå", u"my-rëgex") + with self.assertRaisesMessage(LintConfigError, u"Rule 'T7:foöbar' has no option 'blå'"): + config_builder.build() diff --git a/gitlint/tests/config/test_config_precedence.py b/gitlint/tests/config/test_config_precedence.py index 9689e55..a0eeccd 100644 --- a/gitlint/tests/config/test_config_precedence.py +++ b/gitlint/tests/config/test_config_precedence.py @@ -25,40 +25,48 @@ class LintConfigPrecedenceTests(BaseTestCase): def setUp(self): self.cli = CliRunner() - @patch('gitlint.cli.get_stdin_data', return_value=u"WIP\n\nThis is å test message\n") + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP:fö\n\nThis is å test message\n") def test_config_precedence(self, _): # TODO(jroovers): this test really only test verbosity, we need to do some refactoring to gitlint.cli # to more easily test everything # Test that the config precedence is followed: # 1. commandline convenience flags - # 2. commandline -c flags - # 3. config file - # 4. default config + # 2. environment variables + # 3. commandline -c flags + # 4. config file + # 5. default config config_path = self.get_sample_path("config/gitlintconfig") # 1. commandline convenience flags with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["-vvv", "-c", "general.verbosity=2", "--config", config_path]) self.assertEqual(result.output, "") - self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP\"\n") + self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n") - # 2. commandline -c flags + # 2. environment variables + with patch('gitlint.display.stderr', new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path], + env={"GITLINT_VERBOSITY": "3"}) + self.assertEqual(result.output, "") + self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n") + + # 3. commandline -c flags with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path]) self.assertEqual(result.output, "") self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive)\n") - # 3. config file + # 4. config file with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli, ["--config", config_path]) self.assertEqual(result.output, "") self.assertEqual(stderr.getvalue(), "1: T5\n") - # 4. default config + # 5. default config with patch('gitlint.display.stderr', new=StringIO()) as stderr: result = self.cli.invoke(cli.cli) self.assertEqual(result.output, "") - self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP\"\n") + self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n") @patch('gitlint.cli.get_stdin_data', return_value=u"WIP: This is å test") def test_ignore_precedence(self, get_stdin_data): diff --git a/qa/samples/config/contrib-enabled b/gitlint/tests/contrib/rules/__init__.py index e69de29..e69de29 100644 --- a/qa/samples/config/contrib-enabled +++ b/gitlint/tests/contrib/rules/__init__.py diff --git a/gitlint/tests/contrib/test_conventional_commit.py b/gitlint/tests/contrib/rules/test_conventional_commit.py index ea808fd..001af32 100644 --- a/gitlint/tests/contrib/test_conventional_commit.py +++ b/gitlint/tests/contrib/rules/test_conventional_commit.py @@ -19,13 +19,13 @@ class ContribConventionalCommitTests(BaseTestCase): rule = ConventionalCommit() # No violations when using a correct type and format - for type in ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert"]: + for type in ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"]: violations = rule.validate(type + u": föo", None) self.assertListEqual([], violations) # assert violation on wrong type expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs," - " style, refactor, perf, test, revert", u"bår: foo") + " style, refactor, perf, test, revert, ci, build", u"bår: foo") violations = rule.validate(u"bår: foo", None) self.assertListEqual([expected_violation], violations) diff --git a/gitlint/tests/contrib/test_signedoff_by.py b/gitlint/tests/contrib/rules/test_signedoff_by.py index 934aec5..934aec5 100644 --- a/gitlint/tests/contrib/test_signedoff_by.py +++ b/gitlint/tests/contrib/rules/test_signedoff_by.py diff --git a/gitlint/tests/contrib/test_contrib_rules.py b/gitlint/tests/contrib/test_contrib_rules.py index 3fa4048..84db2d5 100644 --- a/gitlint/tests/contrib/test_contrib_rules.py +++ b/gitlint/tests/contrib/test_contrib_rules.py @@ -3,7 +3,7 @@ import os from gitlint.tests.base import BaseTestCase from gitlint.contrib import rules as contrib_rules -from gitlint.tests import contrib as contrib_tests +from gitlint.tests.contrib import rules as contrib_tests from gitlint import rule_finder, rules from gitlint.utils import ustr diff --git a/gitlint/tests/expected/test_cli/test_contrib_1 b/gitlint/tests/expected/cli/test_cli/test_contrib_1 index ea5d353..cdfb821 100644 --- a/gitlint/tests/expected/test_cli/test_contrib_1 +++ b/gitlint/tests/expected/cli/test_cli/test_contrib_1 @@ -1,3 +1,3 @@ 1: CC1 Body does not contain a 'Signed-Off-By' line -1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert: "Test tïtle" +1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build: "Test tïtle" 1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "Test tïtle" diff --git a/gitlint/tests/expected/test_cli/test_debug_1 b/gitlint/tests/expected/cli/test_cli/test_debug_1 index 612f78e..a95a58d 100644 --- a/gitlint/tests/expected/test_cli/test_debug_1 +++ b/gitlint/tests/expected/cli/test_cli/test_debug_1 @@ -4,6 +4,7 @@ DEBUG: gitlint.cli Python version: {python_version} DEBUG: gitlint.cli Git version: git version 1.2.3 DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} DEBUG: gitlint.cli Configuration config-path: {config_path} [GENERAL] @@ -26,6 +27,8 @@ target: {target} I2: ignore-by-body ignore=all regex=None + I3: ignore-body-lines + regex=None T1: title-max-length line-length=20 T2: title-trailing-whitespace @@ -35,7 +38,9 @@ target: {target} T5: title-must-not-contain-word words=WIP,bögus T7: title-match-regex - regex=.* + regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=30 B5: body-min-length @@ -47,12 +52,19 @@ target: {target} B4: body-first-line-empty B7: body-changed-file-mention files= + B8: body-match-regex + regex=None M1: author-valid-email regex=[^@ ]+@[^@ ]+\.[^@ ]+ DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Using the local repo. +DEBUG: gitlint.git ('rev-list', 'foo...bar') DEBUG: gitlint.cli Linting 3 commit(s) +DEBUG: gitlint.git ('log', '6f29bf81a8322a04071bb794666e48c443a90360', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B') +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') DEBUG: gitlint.lint Linting commit 6f29bf81a8322a04071bb794666e48c443a90360 +DEBUG: gitlint.git ('branch', '--contains', '6f29bf81a8322a04071bb794666e48c443a90360') +DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '6f29bf81a8322a04071bb794666e48c443a90360') DEBUG: gitlint.lint Commit Object --- Commit Message ---- commït-title1 @@ -68,7 +80,10 @@ is-revert-commit: False Branches: ['commit-1-branch-1', 'commit-1-branch-2'] Changed Files: ['commit-1/file-1', 'commit-1/file-2'] ----------------------- +DEBUG: gitlint.git ('log', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B') DEBUG: gitlint.lint Linting commit 25053ccec5e28e1bb8f7551fdbb5ab213ada2401 +DEBUG: gitlint.git ('branch', '--contains', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401') +DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401') DEBUG: gitlint.lint Commit Object --- Commit Message ---- commït-title2. @@ -84,10 +99,13 @@ is-revert-commit: False Branches: ['commit-2-branch-1', 'commit-2-branch-2'] Changed Files: ['commit-2/file-1', 'commit-2/file-2'] ----------------------- +DEBUG: gitlint.git ('log', '4da2656b0dadc76c7ee3fd0243a96cb64007f125', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B') DEBUG: gitlint.lint Linting commit 4da2656b0dadc76c7ee3fd0243a96cb64007f125 +DEBUG: gitlint.git ('branch', '--contains', '4da2656b0dadc76c7ee3fd0243a96cb64007f125') +DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '4da2656b0dadc76c7ee3fd0243a96cb64007f125') DEBUG: gitlint.lint Commit Object --- Commit Message ---- -föo +föobar bar --- Meta info --------- Author: test åuthor3 <test-email3@föo.com> diff --git a/gitlint/tests/expected/test_cli/test_input_stream_1 b/gitlint/tests/expected/cli/test_cli/test_input_stream_1 index 4326729..4326729 100644 --- a/gitlint/tests/expected/test_cli/test_input_stream_1 +++ b/gitlint/tests/expected/cli/test_cli/test_input_stream_1 diff --git a/gitlint/tests/expected/test_cli/test_input_stream_debug_1 b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_1 index 4326729..4326729 100644 --- a/gitlint/tests/expected/test_cli/test_input_stream_debug_1 +++ b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_1 diff --git a/gitlint/tests/expected/test_cli/test_input_stream_debug_2 b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 index a9028e1..c05d147 100644 --- a/gitlint/tests/expected/test_cli/test_input_stream_debug_2 +++ b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 @@ -4,6 +4,7 @@ DEBUG: gitlint.cli Python version: {python_version} DEBUG: gitlint.cli Git version: git version 1.2.3 DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] @@ -26,6 +27,8 @@ target: {target} I2: ignore-by-body ignore=all regex=None + I3: ignore-body-lines + regex=None T1: title-max-length line-length=72 T2: title-trailing-whitespace @@ -35,7 +38,9 @@ target: {target} T5: title-must-not-contain-word words=WIP T7: title-match-regex - regex=.* + regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length @@ -47,12 +52,15 @@ target: {target} B4: body-first-line-empty B7: body-changed-file-mention files= + B8: body-match-regex + regex=None M1: author-valid-email regex=[^@ ]+@[^@ ]+\.[^@ ]+ DEBUG: gitlint.cli Stdin data: 'WIP: tïtle ' DEBUG: gitlint.cli Stdin detected and not ignored. Using as input. +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') DEBUG: gitlint.cli Linting 1 commit(s) DEBUG: gitlint.lint Linting commit [SHA UNKNOWN] DEBUG: gitlint.lint Commit Object diff --git a/gitlint/tests/expected/test_cli/test_lint_multiple_commits_1 b/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_1 index be3288b..be3288b 100644 --- a/gitlint/tests/expected/test_cli/test_lint_multiple_commits_1 +++ b/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_1 diff --git a/gitlint/tests/expected/test_cli/test_lint_multiple_commits_config_1 b/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_config_1 index 1bf0503..1bf0503 100644 --- a/gitlint/tests/expected/test_cli/test_lint_multiple_commits_config_1 +++ b/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_config_1 diff --git a/gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_1 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_1 index 9a9091b..9a9091b 100644 --- a/gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_1 +++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_1 diff --git a/gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_2 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 index 3e5dcb6..e8e9f33 100644 --- a/gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_2 +++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 @@ -4,6 +4,7 @@ DEBUG: gitlint.cli Python version: {python_version} DEBUG: gitlint.cli Git version: git version 1.2.3 DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] @@ -26,6 +27,8 @@ target: {target} I2: ignore-by-body ignore=all regex=None + I3: ignore-body-lines + regex=None T1: title-max-length line-length=72 T2: title-trailing-whitespace @@ -35,7 +38,9 @@ target: {target} T5: title-must-not-contain-word words=WIP T7: title-match-regex - regex=.* + regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length @@ -47,13 +52,20 @@ target: {target} B4: body-first-line-empty B7: body-changed-file-mention files= + B8: body-match-regex + regex=None M1: author-valid-email regex=[^@ ]+@[^@ ]+\.[^@ ]+ DEBUG: gitlint.cli Fetching additional meta-data from staged commit DEBUG: gitlint.cli Using --msg-filename. +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') DEBUG: gitlint.cli Linting 1 commit(s) DEBUG: gitlint.lint Linting commit [SHA UNKNOWN] +DEBUG: gitlint.git ('config', '--get', 'user.name') +DEBUG: gitlint.git ('config', '--get', 'user.email') +DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD') +DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r') DEBUG: gitlint.lint Commit Object --- Commit Message ---- WIP: msg-filename tïtle diff --git a/gitlint/tests/expected/test_cli/test_lint_staged_stdin_1 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_1 index 4326729..4326729 100644 --- a/gitlint/tests/expected/test_cli/test_lint_staged_stdin_1 +++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_1 diff --git a/gitlint/tests/expected/test_cli/test_lint_staged_stdin_2 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 index 03fd8c3..b822edc 100644 --- a/gitlint/tests/expected/test_cli/test_lint_staged_stdin_2 +++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 @@ -4,6 +4,7 @@ DEBUG: gitlint.cli Python version: {python_version} DEBUG: gitlint.cli Git version: git version 1.2.3 DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] @@ -26,6 +27,8 @@ target: {target} I2: ignore-by-body ignore=all regex=None + I3: ignore-body-lines + regex=None T1: title-max-length line-length=72 T2: title-trailing-whitespace @@ -35,7 +38,9 @@ target: {target} T5: title-must-not-contain-word words=WIP T7: title-match-regex - regex=.* + regex=None + T8: title-min-length + min-length=5 B1: body-max-line-length line-length=80 B5: body-min-length @@ -47,6 +52,8 @@ target: {target} B4: body-first-line-empty B7: body-changed-file-mention files= + B8: body-match-regex + regex=None M1: author-valid-email regex=[^@ ]+@[^@ ]+\.[^@ ]+ @@ -54,8 +61,13 @@ DEBUG: gitlint.cli Fetching additional meta-data from staged commit DEBUG: gitlint.cli Stdin data: 'WIP: tïtle ' DEBUG: gitlint.cli Stdin detected and not ignored. Using as input. +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') DEBUG: gitlint.cli Linting 1 commit(s) DEBUG: gitlint.lint Linting commit [SHA UNKNOWN] +DEBUG: gitlint.git ('config', '--get', 'user.name') +DEBUG: gitlint.git ('config', '--get', 'user.email') +DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD') +DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r') DEBUG: gitlint.lint Commit Object --- Commit Message ---- WIP: tïtle diff --git a/gitlint/tests/expected/cli/test_cli/test_named_rules_1 b/gitlint/tests/expected/cli/test_cli/test_named_rules_1 new file mode 100644 index 0000000..a581d05 --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli/test_named_rules_1 @@ -0,0 +1,4 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: tëst tïtle" +1: T5:even-more-wörds Title contains the word 'tïtle' (case-insensitive): "WIP: tëst tïtle" +1: T5:extra-wörds Title contains the word 'tëst' (case-insensitive): "WIP: tëst tïtle" +3: B6 Body message is missing diff --git a/gitlint/tests/expected/cli/test_cli/test_named_rules_2 b/gitlint/tests/expected/cli/test_cli/test_named_rules_2 new file mode 100644 index 0000000..828e296 --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli/test_named_rules_2 @@ -0,0 +1,82 @@ +DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues +DEBUG: gitlint.cli Platform: {platform} +DEBUG: gitlint.cli Python version: {python_version} +DEBUG: gitlint.cli Git version: git version 1.2.3 +DEBUG: gitlint.cli Gitlint version: {gitlint_version} +DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} +DEBUG: gitlint.cli Configuration +config-path: {config_path} +[GENERAL] +extra-path: None +contrib: [] +ignore: +ignore-merge-commits: True +ignore-fixup-commits: True +ignore-squash-commits: True +ignore-revert-commits: True +ignore-stdin: False +staged: False +verbosity: 3 +debug: True +target: {target} +[RULES] + I1: ignore-by-title + ignore=all + regex=None + I2: ignore-by-body + ignore=all + regex=None + I3: ignore-body-lines + regex=None + T1: title-max-length + line-length=72 + T2: title-trailing-whitespace + T6: title-leading-whitespace + T3: title-trailing-punctuation + T4: title-hard-tab + T5: title-must-not-contain-word + words=WIP,bögus + T7: title-match-regex + regex=None + T8: title-min-length + min-length=5 + B1: body-max-line-length + line-length=80 + B5: body-min-length + min-length=20 + B6: body-is-missing + ignore-merge-commits=True + B2: body-trailing-whitespace + B3: body-hard-tab + B4: body-first-line-empty + B7: body-changed-file-mention + files= + B8: body-match-regex + regex=None + M1: author-valid-email + regex=[^@ ]+@[^@ ]+\.[^@ ]+ + T5:extra-wörds: title-must-not-contain-word:extra-wörds + words=hür,tëst + T5:even-more-wörds: title-must-not-contain-word:even-more-wörds + words=hür,tïtle + +DEBUG: gitlint.cli Stdin data: 'WIP: tëst tïtle' +DEBUG: gitlint.cli Stdin detected and not ignored. Using as input. +DEBUG: gitlint.git ('config', '--get', 'core.commentchar') +DEBUG: gitlint.cli Linting 1 commit(s) +DEBUG: gitlint.lint Linting commit [SHA UNKNOWN] +DEBUG: gitlint.lint Commit Object +--- Commit Message ---- +WIP: tëst tïtle +--- Meta info --------- +Author: None <None> +Date: None +is-merge-commit: False +is-fixup-commit: False +is-squash-commit: False +is-revert-commit: False +Branches: [] +Changed Files: [] +----------------------- +DEBUG: gitlint.cli Exit Code = 4
\ No newline at end of file diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stderr new file mode 100644 index 0000000..cfacd42 --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stderr @@ -0,0 +1,2 @@ +1: T1 Title exceeds max length (27>5): "WIP: Test hook config tïtle" +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Test hook config tïtle" diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stdout new file mode 100644 index 0000000..5d3f1fc --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stdout @@ -0,0 +1,5 @@ +gitlint: checking commit message... +----------------------------------------------- +gitlint: Your commit message contains the above violations. +Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] +Aborted! diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stderr new file mode 100644 index 0000000..3eb8fca --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stderr @@ -0,0 +1,6 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "{commit_msg}" +3: B6 Body message is missing +1: T5 Title contains the word 'WIP' (case-insensitive): "{commit_msg}" +3: B6 Body message is missing +1: T5 Title contains the word 'WIP' (case-insensitive): "{commit_msg}" +3: B6 Body message is missing diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stdout new file mode 100644 index 0000000..fa6b3bc --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stdout @@ -0,0 +1,14 @@ +gitlint: checking commit message... +----------------------------------------------- +gitlint: Your commit message contains the above violations. +Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] gitlint: checking commit message... +----------------------------------------------- +gitlint: Your commit message contains the above violations. +Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] gitlint: checking commit message... +----------------------------------------------- +gitlint: Your commit message contains the above violations. +Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] Commit aborted. +Your commit message: +----------------------------------------------- +{commit_msg} +----------------------------------------------- diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stderr new file mode 100644 index 0000000..11c3cd8 --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stderr @@ -0,0 +1,2 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: commït-title" +3: B5 Body message is too short (11<20): "commït-body" diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stdout new file mode 100644 index 0000000..a95bfea --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stdout @@ -0,0 +1,4 @@ +gitlint: checking commit message... +----------------------------------------------- +gitlint: Your commit message contains the above violations. +Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] Editing only possible when --msg-filename is specified. diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stderr new file mode 100644 index 0000000..6d0c9cf --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stderr @@ -0,0 +1,2 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: höok no" +3: B6 Body message is missing diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stdout new file mode 100644 index 0000000..9cc53c1 --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stdout @@ -0,0 +1,8 @@ +gitlint: checking commit message... +----------------------------------------------- +gitlint: Your commit message contains the above violations. +Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] Commit aborted. +Your commit message: +----------------------------------------------- +WIP: höok no +----------------------------------------------- diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stderr new file mode 100644 index 0000000..a8d8760 --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stderr @@ -0,0 +1,2 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: tïtle" +3: B6 Body message is missing diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stdout new file mode 100644 index 0000000..5d3f1fc --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stdout @@ -0,0 +1,5 @@ +gitlint: checking commit message... +----------------------------------------------- +gitlint: Your commit message contains the above violations. +Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] +Aborted! diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout new file mode 100644 index 0000000..da1ef0b --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout @@ -0,0 +1,2 @@ +gitlint: checking commit message... +gitlint: OK (no violations in commit message) diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stderr new file mode 100644 index 0000000..1404f4a --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stderr @@ -0,0 +1,2 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Test hook stdin tïtle" +3: B6 Body message is missing diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stdout new file mode 100644 index 0000000..5d3f1fc --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stdout @@ -0,0 +1,5 @@ +gitlint: checking commit message... +----------------------------------------------- +gitlint: Your commit message contains the above violations. +Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] +Aborted! diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stderr new file mode 100644 index 0000000..da6f874 --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stderr @@ -0,0 +1,2 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: höok yes" +3: B6 Body message is missing diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stdout new file mode 100644 index 0000000..bb753b0 --- /dev/null +++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stdout @@ -0,0 +1,4 @@ +gitlint: checking commit message... +----------------------------------------------- +gitlint: Your commit message contains the above violations. +Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)]
\ No newline at end of file diff --git a/gitlint/tests/git/test_git.py b/gitlint/tests/git/test_git.py index 297b10c..1830119 100644 --- a/gitlint/tests/git/test_git.py +++ b/gitlint/tests/git/test_git.py @@ -27,7 +27,7 @@ class GitTests(BaseTestCase): sh.git.side_effect = CommandNotFound("git") expected_msg = "'git' command not found. You need to install git to use gitlint on a local repository. " + \ "See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git." - with self.assertRaisesRegex(GitNotInstalledError, expected_msg): + with self.assertRaisesMessage(GitNotInstalledError, expected_msg): GitContext.from_local_repository(u"fåke/path") # assert that commit message was read using git command @@ -39,7 +39,7 @@ class GitTests(BaseTestCase): err = b"fatal: Not a git repository (or any of the parent directories): .git" sh.git.side_effect = ErrorReturnCode("git log -1 --pretty=%H", b"", err) - with self.assertRaisesRegex(GitContextError, u"fåke/path is not a git repository."): + with self.assertRaisesMessage(GitContextError, u"fåke/path is not a git repository."): GitContext.from_local_repository(u"fåke/path") # assert that commit message was read using git command @@ -50,7 +50,7 @@ class GitTests(BaseTestCase): sh.git.side_effect = ErrorReturnCode("git log -1 --pretty=%H", b"", err) expected_msg = u"An error occurred while executing 'git log -1 --pretty=%H': {0}".format(err) - with self.assertRaisesRegex(GitContextError, expected_msg): + with self.assertRaisesMessage(GitContextError, expected_msg): GitContext.from_local_repository(u"fåke/path") # assert that commit message was read using git command @@ -64,7 +64,7 @@ class GitTests(BaseTestCase): sh.git.side_effect = ErrorReturnCode("git log -1 --pretty=%H", b"", err) expected_msg = u"Current branch has no commits. Gitlint requires at least one commit to function." - with self.assertRaisesRegex(GitContextError, expected_msg): + with self.assertRaisesMessage(GitContextError, expected_msg): GitContext.from_local_repository(u"fåke/path") # assert that commit message was read using git command @@ -82,7 +82,7 @@ class GitTests(BaseTestCase): ErrorReturnCode("rev-parse --abbrev-ref HEAD", b"", err) ] - with self.assertRaisesRegex(GitContextError, expected_msg): + with self.assertRaisesMessage(GitContextError, expected_msg): context = GitContext.from_commit_msg(u"test") context.current_branch diff --git a/gitlint/tests/git/test_git_commit.py b/gitlint/tests/git/test_git_commit.py index dc83ccb..5f87a8e 100644 --- a/gitlint/tests/git/test_git_commit.py +++ b/gitlint/tests/git/test_git_commit.py @@ -14,7 +14,9 @@ except ImportError: from unittest.mock import patch, call # pylint: disable=no-name-in-module, import-error from gitlint.tests.base import BaseTestCase -from gitlint.git import GitContext, GitCommit, LocalGitCommit, StagedLocalGitCommit, GitCommitMessage +from gitlint.git import GitContext, GitCommit, GitContextError, LocalGitCommit, StagedLocalGitCommit, GitCommitMessage +from gitlint.shell import ErrorReturnCode +from gitlint.utils import ustr class GitCommitTests(BaseTestCase): @@ -479,12 +481,46 @@ class GitCommitTests(BaseTestCase): self.assertListEqual(last_commit.changed_files, ["file1.txt", u"påth/to/file2.txt"]) self.assertListEqual(sh.git.mock_calls, expected_calls[0:5]) + @patch('gitlint.git.sh') + def test_staged_commit_with_missing_username(self, sh): + # StagedLocalGitCommit() + + sh.git.side_effect = [ + u"#", # git config --get core.commentchar + ErrorReturnCode('git config --get user.name', b"", b""), + ] + + expected_msg = "Missing git configuration: please set user.name" + with self.assertRaisesMessage(GitContextError, expected_msg): + ctx = GitContext.from_staged_commit(u"Foōbar 123\n\ncömmit-body\n", u"fåke/path") + [ustr(commit) for commit in ctx.commits] + + @patch('gitlint.git.sh') + def test_staged_commit_with_missing_email(self, sh): + # StagedLocalGitCommit() + + sh.git.side_effect = [ + u"#", # git config --get core.commentchar + u"test åuthor\n", # git config --get user.name + ErrorReturnCode('git config --get user.name', b"", b""), + ] + + expected_msg = "Missing git configuration: please set user.email" + with self.assertRaisesMessage(GitContextError, expected_msg): + ctx = GitContext.from_staged_commit(u"Foōbar 123\n\ncömmit-body\n", u"fåke/path") + [ustr(commit) for commit in ctx.commits] + def test_gitcommitmessage_equality(self): commit_message1 = GitCommitMessage(GitContext(), u"tëst\n\nfoo", u"tëst\n\nfoo", u"tēst", ["", u"föo"]) attrs = ['original', 'full', 'title', 'body'] self.object_equality_test(commit_message1, attrs, {"context": commit_message1.context}) - def test_gitcommit_equality(self): + @patch("gitlint.git._git") + def test_gitcommit_equality(self, git): + # git will be called to setup the context (commentchar and current_branch), just return the same value + # This only matters to test gitcontext equality, not gitcommit equality + git.return_value = u"foöbar" + # Test simple equality case now = datetime.datetime.utcnow() context1 = GitContext() diff --git a/gitlint/tests/rules/test_body_rules.py b/gitlint/tests/rules/test_body_rules.py index fcb1b30..f46760b 100644 --- a/gitlint/tests/rules/test_body_rules.py +++ b/gitlint/tests/rules/test_body_rules.py @@ -178,3 +178,49 @@ class BodyRuleTests(BaseTestCase): 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(u"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': u"^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': u"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': u"(.*)Föo(.*)"}) + violations = rule.validate(commit) + self.assertIsNone(violations) + + # assert violation on non-matching body + rule = rules.BodyRegexMatches({'regex': u"^Tëst(.*)Foo"}) + violations = rule.validate(commit) + expected_violation = rules.RuleViolation("B8", u"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 = [u"åbc", u"åbc\n", u"åbc\nföo\n", u"åbc\n\n", u"åbc\nföo\nblå", u"å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/tests/rules/test_configuration_rules.py b/gitlint/tests/rules/test_configuration_rules.py index 73d42f3..121cb3a 100644 --- a/gitlint/tests/rules/test_configuration_rules.py +++ b/gitlint/tests/rules/test_configuration_rules.py @@ -67,5 +67,41 @@ class ConfigurationRuleTests(BaseTestCase): rule.apply(config, commit) self.assertEqual(config, expected_config) - expected_log_message = u"DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \ + expected_log_message = u"DEBUG: gitlint.rules Ignoring commit because of rule 'I2': " + \ u"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_body_lines(self): + commit1 = self.gitcommit(u"Tïtle\n\nThis is\n a relëase body\n line") + commit2 = self.gitcommit(u"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": u"(.*)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(u"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(u"DEBUG: gitlint.rules Ignoring line ' a relëase body' because it " + + u"matches '(.*)relëase(.*)'") + + # Non-Matching regex: no changes expected + commit1 = self.gitcommit(u"Tïtle\n\nThis is\n a relëase body\n line") + rule = rules.IgnoreBodyLines({"regex": u"(.*)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/tests/rules/test_meta_rules.py b/gitlint/tests/rules/test_meta_rules.py index c94b8b3..987aa88 100644 --- a/gitlint/tests/rules/test_meta_rules.py +++ b/gitlint/tests/rules/test_meta_rules.py @@ -32,6 +32,15 @@ class MetaRuleTests(BaseTestCase): [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 = [u"föo", None, u"hür dür"] + for email in emailadresses: + commit = self.gitcommit(u"", author_email=email) + violations = rule.validate(commit) + self.assertIsNone(violations) + # Custom domain rule = AuthorValidEmail({'regex': u"[^@]+@bår.com"}) valid_email_addresses = [ diff --git a/gitlint/tests/rules/test_rules.py b/gitlint/tests/rules/test_rules.py index 89caa27..58ee1c3 100644 --- a/gitlint/tests/rules/test_rules.py +++ b/gitlint/tests/rules/test_rules.py @@ -13,6 +13,11 @@ class RuleTests(BaseTestCase): setattr(rule, attr, u"åbc") self.assertNotEqual(Rule(), rule) + def test_rule_log(self): + rule = Rule() + rule.log.debug(u"Tēst message") + self.assert_log_contains(u"DEBUG: gitlint.rules Tēst message") + def test_rule_violation_equality(self): violation1 = RuleViolation(u"ïd1", u"My messåge", u"My cöntent", 1) self.object_equality_test(violation1, ["rule_id", "message", "content", "line_nr"]) diff --git a/gitlint/tests/rules/test_title_rules.py b/gitlint/tests/rules/test_title_rules.py index 07d2323..049735e 100644 --- a/gitlint/tests/rules/test_title_rules.py +++ b/gitlint/tests/rules/test_title_rules.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from gitlint.tests.base import BaseTestCase from gitlint.rules import TitleMaxLength, TitleTrailingWhitespace, TitleHardTab, TitleMustNotContainWord, \ - TitleTrailingPunctuation, TitleLeadingWhitespace, TitleRegexMatches, RuleViolation + TitleTrailingPunctuation, TitleLeadingWhitespace, TitleRegexMatches, RuleViolation, TitleMinLength class TitleRuleTests(BaseTestCase): @@ -152,3 +152,35 @@ class TitleRuleTests(BaseTestCase): violations = rule.validate(commit.message.title, commit) expected_violation = RuleViolation("T7", u"Title does not match regex (^UÅ[0-9]*)", u"US1234: åbc") self.assertListEqual(violations, [expected_violation]) + + def test_min_line_length(self): + rule = TitleMinLength() + + # assert no error + violation = rule.validate(u"å" * 72, None) + self.assertIsNone(violation) + + # assert error on line length < 5 + expected_violation = RuleViolation("T8", "Title is too short (4<5)", u"å" * 4, 1) + violations = rule.validate(u"å" * 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(u"å" * 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(u"å" * 3, None) + self.assertIsNone(violations) + + # assert raise on 2 + expected_violation = RuleViolation("T8", "Title is too short (2<3)", u"å" * 2, 1) + violations = rule.validate(u"å" * 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/tests/rules/test_user_rules.py b/gitlint/tests/rules/test_user_rules.py index 57c03a0..52d0283 100644 --- a/gitlint/tests/rules/test_user_rules.py +++ b/gitlint/tests/rules/test_user_rules.py @@ -92,7 +92,7 @@ class UserRuleTests(BaseTestCase): find_rule_classes(user_rule_path) def test_find_rule_classes_nonexisting_path(self): - with self.assertRaisesRegex(UserRuleError, u"Invalid extra-path: föo/bar"): + with self.assertRaisesMessage(UserRuleError, u"Invalid extra-path: föo/bar"): find_rule_classes(u"föo/bar") def test_assert_valid_rule_class(self): @@ -111,15 +111,23 @@ class UserRuleTests(BaseTestCase): def validate(self): pass + class MyConfigurationRuleClass(rules.ConfigurationRule): + id = 'UC3' + name = u'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.assertRaisesRegex(UserRuleError, - "User-defined rule class 'MyUserLineRule' must have a 'validate' method"): + 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): @@ -127,76 +135,101 @@ class UserRuleTests(BaseTestCase): class MyRuleClass(object): pass - expected_msg = "User-defined rule class 'MyRuleClass' must extend from gitlint.rules.LineRule " + \ - "or gitlint.rules.CommitRule" - with self.assertRaisesRegex(UserRuleError, expected_msg): + 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): - class MyRuleClass(rules.LineRule): - pass - # Rule class must have an id - expected_msg = "User-defined rule class 'MyRuleClass' must have an 'id' attribute" - with self.assertRaisesRegex(UserRuleError, expected_msg): - assert_valid_rule_class(MyRuleClass) + for parent_class in [rules.LineRule, rules.CommitRule]: - # Rule ids must be non-empty - MyRuleClass.id = "" - with self.assertRaisesRegex(UserRuleError, expected_msg): - assert_valid_rule_class(MyRuleClass) + class MyRuleClass(parent_class): + pass - # Rule ids must not start with one of the reserved id letters - for letter in ["T", "R", "B", "M"]: - MyRuleClass.id = letter + "1" - expected_msg = "The id '{0}' of 'MyRuleClass' is invalid. Gitlint reserves ids starting with R,T,B,M" - with self.assertRaisesRegex(UserRuleError, expected_msg.format(letter)): + # 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 = "The id '{0}' of 'MyRuleClass' is invalid. Gitlint reserves ids starting with R,T,B,M,I" + with self.assertRaisesMessage(UserRuleError, expected_msg.format(letter)): + assert_valid_rule_class(MyRuleClass) + def test_assert_valid_rule_class_negative_name(self): - class MyRuleClass(rules.LineRule): - id = "UC1" + for parent_class in [rules.LineRule, rules.CommitRule]: - # Rule class must have an name - expected_msg = "User-defined rule class 'MyRuleClass' must have a 'name' attribute" - with self.assertRaisesRegex(UserRuleError, expected_msg): - assert_valid_rule_class(MyRuleClass) + class MyRuleClass(parent_class): + id = "UC1" - # Rule names must be non-empty - MyRuleClass.name = "" - with self.assertRaisesRegex(UserRuleError, expected_msg): - assert_valid_rule_class(MyRuleClass) + # Rule class must have an 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): - class MyRuleClass(rules.LineRule): - id = "UC1" - name = u"my-rüle-class" - # if set, option_spec must be a list of gitlint options - MyRuleClass.options_spec = u"föo" - expected_msg = "The options_spec attribute of user-defined rule class 'MyRuleClass' must be a list " + \ - "of gitlint.options.RuleOption" - with self.assertRaisesRegex(UserRuleError, expected_msg): - assert_valid_rule_class(MyRuleClass) + for parent_class in [rules.LineRule, rules.CommitRule]: - # option_spec is a list, but not of gitlint options - MyRuleClass.options_spec = [u"föo", 123] # pylint: disable=bad-option-value,redefined-variable-type - with self.assertRaisesRegex(UserRuleError, expected_msg): - assert_valid_rule_class(MyRuleClass) + class MyRuleClass(parent_class): + id = "UC1" + name = u"my-rüle-class" + + # if set, option_spec must be a list of gitlint options + MyRuleClass.options_spec = u"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 = [u"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): - class MyRuleClass(rules.LineRule): - id = "UC1" + + baseclasses = [rules.LineRule, rules.CommitRule] + for clazz in baseclasses: + class MyRuleClass(clazz): + id = "UC1" + name = u"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 = u"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 = u"my-rüle-class" - with self.assertRaisesRegex(UserRuleError, - "User-defined rule class 'MyRuleClass' must have a 'validate' method"): + 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 = u"föo" - with self.assertRaisesRegex(UserRuleError, - "User-defined rule class 'MyRuleClass' must have a 'validate' method"): + with self.assertRaisesMessage(UserRuleError, expected_msg): assert_valid_rule_class(MyRuleClass) def test_assert_valid_rule_class_negative_target(self): @@ -210,12 +243,12 @@ class UserRuleTests(BaseTestCase): # 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.assertRaisesRegex(UserRuleError, expected_msg): + with self.assertRaisesMessage(UserRuleError, expected_msg): assert_valid_rule_class(MyRuleClass) # invalid target MyRuleClass.target = u"föo" - with self.assertRaisesRegex(UserRuleError, expected_msg): + with self.assertRaisesMessage(UserRuleError, expected_msg): assert_valid_rule_class(MyRuleClass) # valid target, no exception should be raised diff --git a/gitlint/tests/samples/commit_message/no-violations b/gitlint/tests/samples/commit_message/no-violations new file mode 100644 index 0000000..33c73b9 --- /dev/null +++ b/gitlint/tests/samples/commit_message/no-violations @@ -0,0 +1,6 @@ +Normal Commit Tïtle + +Nörmal body that contains a few lines of text describing the changes in the +commit without violating any of gitlint's rules. + +Sïgned-Off-By: foo@bar.com diff --git a/gitlint/tests/samples/config/named-rules b/gitlint/tests/samples/config/named-rules new file mode 100644 index 0000000..73ab0d2 --- /dev/null +++ b/gitlint/tests/samples/config/named-rules @@ -0,0 +1,8 @@ +[title-must-not-contain-word] +words=WIP,bögus + +[title-must-not-contain-word:extra-wörds] +words=hür,tëst + +[T5:even-more-wörds] +words=hür,tïtle
\ No newline at end of file diff --git a/gitlint/tests/test_hooks.py b/gitlint/tests/test_hooks.py index 08bd730..62f55e5 100644 --- a/gitlint/tests/test_hooks.py +++ b/gitlint/tests/test_hooks.py @@ -58,8 +58,8 @@ class HookTests(BaseTestCase): git_hooks_dir.return_value = os.path.join(u"/föo", u"bar", ".git", "hooks") # mock that current dir is not a git repo isdir.return_value = False - expected_msg = u"{0} is not a git repository".format(lint_config.target) - with self.assertRaisesRegex(GitHookInstallerError, expected_msg): + expected_msg = u"{0} is not a git repository.".format(lint_config.target) + with self.assertRaisesMessage(GitHookInstallerError, expected_msg): GitHookInstaller.install_commit_msg_hook(lint_config) isdir.assert_called_with(git_hooks_dir.return_value) path_exists.assert_not_called() @@ -71,7 +71,7 @@ class HookTests(BaseTestCase): expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH) expected_msg = u"There is already a commit-msg hook file present in {0}.\n".format(expected_dst) + \ "gitlint currently does not support appending to an existing commit-msg file." - with self.assertRaisesRegex(GitHookInstallerError, expected_msg): + with self.assertRaisesMessage(GitHookInstallerError, expected_msg): GitHookInstaller.install_commit_msg_hook(lint_config) @staticmethod @@ -104,8 +104,8 @@ class HookTests(BaseTestCase): # mock that the current directory is not a git repo isdir.return_value = False - expected_msg = u"{0} is not a git repository".format(lint_config.target) - with self.assertRaisesRegex(GitHookInstallerError, expected_msg): + expected_msg = u"{0} is not a git repository.".format(lint_config.target) + with self.assertRaisesMessage(GitHookInstallerError, expected_msg): GitHookInstaller.uninstall_commit_msg_hook(lint_config) isdir.assert_called_with(git_hooks_dir.return_value) path_exists.assert_not_called() @@ -116,7 +116,7 @@ class HookTests(BaseTestCase): path_exists.return_value = False expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH) expected_msg = u"There is no commit-msg hook present in {0}.".format(expected_dst) - with self.assertRaisesRegex(GitHookInstallerError, expected_msg): + with self.assertRaisesMessage(GitHookInstallerError, expected_msg): GitHookInstaller.uninstall_commit_msg_hook(lint_config) isdir.assert_called_with(git_hooks_dir.return_value) path_exists.assert_called_once_with(expected_dst) @@ -131,6 +131,6 @@ class HookTests(BaseTestCase): "(or it was modified).\nUninstallation of 3th party or modified gitlint hooks " + \ "is not supported." with patch('gitlint.hooks.io.open', mock_open(read_data=read_data), create=True): - with self.assertRaisesRegex(GitHookInstallerError, expected_msg): + with self.assertRaisesMessage(GitHookInstallerError, expected_msg): GitHookInstaller.uninstall_commit_msg_hook(lint_config) remove.assert_not_called() diff --git a/gitlint/tests/test_lint.py b/gitlint/tests/test_lint.py index bcdd984..3bf9a94 100644 --- a/gitlint/tests/test_lint.py +++ b/gitlint/tests/test_lint.py @@ -16,7 +16,7 @@ except ImportError: from gitlint.tests.base import BaseTestCase from gitlint.lint import GitLinter -from gitlint.rules import RuleViolation +from gitlint.rules import RuleViolation, TitleMustNotContainWord from gitlint.config import LintConfig, LintConfigBuilder @@ -103,7 +103,7 @@ class LintTests(BaseTestCase): self.assertListEqual(violations, expected) def test_lint_meta(self): - """ Lint sample2 but also add some metadata to the commit so we that get's linted as well """ + """ Lint sample2 but also add some metadata to the commit so we that gets linted as well """ linter = GitLinter(LintConfig()) gitcontext = self.gitcontext(self.get_sample("commit_message/sample2")) gitcontext.commits[0].author_email = u"foo bår" @@ -150,6 +150,25 @@ class LintTests(BaseTestCase): self.assertListEqual(violations, expected) + # Test ignoring body lines + lint_config = LintConfig() + linter = GitLinter(lint_config) + lint_config.set_rule_option("I3", "regex", u"(.*)tråiling(.*)") + violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample1"))) + expected_errors = [RuleViolation("T3", "Title has trailing punctuation (.)", + u"Commit title contåining 'WIP', as well as trailing punctuation.", 1), + RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", + u"Commit title contåining 'WIP', as well as trailing punctuation.", 1), + RuleViolation("B4", "Second line is not empty", "This line should be empty", 2), + RuleViolation("B1", "Line exceeds max length (135>80)", + "This is the first line of the commit message body and it is meant to test " + + "a line that exceeds the maximum line length of 80 characters.", 3), + RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 4), + RuleViolation("B3", "Line contains hard tab characters (\\t)", + "This line has a trailing tab.\t", 4)] + + self.assertListEqual(violations, expected_errors) + def test_lint_special_commit(self): for commit_type in ["merge", "revert", "squash", "fixup"]: commit = self.gitcommit(self.get_sample("commit_message/{0}".format(commit_type))) @@ -166,6 +185,31 @@ class LintTests(BaseTestCase): violations = linter.lint(commit) self.assertTrue(len(violations) > 0) + def test_lint_regex_rules(self): + """ Additional test for title-match-regex, body-match-regex """ + commit = self.gitcommit(self.get_sample("commit_message/no-violations")) + lintconfig = LintConfig() + linter = GitLinter(lintconfig) + violations = linter.lint(commit) + # No violations by default + self.assertListEqual(violations, []) + + # Matching regexes shouldn't be a problem + rule_regexes = [("title-match-regex", u"Tïtle$"), ("body-match-regex", u"Sïgned-Off-By: (.*)$")] + for rule_regex in rule_regexes: + lintconfig.set_rule_option(rule_regex[0], "regex", rule_regex[1]) + violations = linter.lint(commit) + self.assertListEqual(violations, []) + + # Non-matching regexes should return violations + rule_regexes = [("title-match-regex", ), ("body-match-regex",)] + lintconfig.set_rule_option("title-match-regex", "regex", u"^Tïtle") + lintconfig.set_rule_option("body-match-regex", "regex", u"Sügned-Off-By: (.*)$") + expected_violations = [RuleViolation("T7", u"Title does not match regex (^Tïtle)", u"Normal Commit Tïtle", 1), + RuleViolation("B8", u"Body does not match regex (Sügned-Off-By: (.*)$)", None, 6)] + violations = linter.lint(commit) + self.assertListEqual(violations, expected_violations) + def test_print_violations(self): violations = [RuleViolation("RULE_ID_1", u"Error Messåge 1", "Violating Content 1", None), RuleViolation("RULE_ID_2", "Error Message 2", u"Violåting Content 2", 2)] @@ -195,3 +239,49 @@ class LintTests(BaseTestCase): expected = u"-: RULE_ID_1 Error Messåge 1: \"Violating Content 1\"\n" + \ u"2: RULE_ID_2 Error Message 2: \"Violåting Content 2\"\n" self.assertEqual(expected, stderr.getvalue()) + + def test_named_rules(self): + """ Test that when named rules are present, both them and the original (non-named) rules executed """ + + lint_config = LintConfig() + for rule_name in [u"my-ïd", u"another-rule-ïd"]: + rule_id = TitleMustNotContainWord.id + ":" + rule_name + lint_config.rules.add_rule(TitleMustNotContainWord, rule_id) + lint_config.set_rule_option(rule_id, "words", [u"Föo"]) + linter = GitLinter(lint_config) + + violations = [RuleViolation("T5", u"Title contains the word 'WIP' (case-insensitive)", u"WIP: Föo bar", 1), + RuleViolation(u"T5:another-rule-ïd", u"Title contains the word 'Föo' (case-insensitive)", + u"WIP: Föo bar", 1), + RuleViolation(u"T5:my-ïd", u"Title contains the word 'Föo' (case-insensitive)", + u"WIP: Föo bar", 1)] + self.assertListEqual(violations, linter.lint(self.gitcommit(u"WIP: Föo bar\n\nFoo bår hur dur bla bla"))) + + def test_ignore_named_rules(self): + """ Test that named rules can be ignored """ + + # Add named rule to lint config + config_builder = LintConfigBuilder() + rule_id = TitleMustNotContainWord.id + u":my-ïd" + config_builder.set_option(rule_id, "words", [u"Föo"]) + lint_config = config_builder.build() + linter = GitLinter(lint_config) + commit = self.gitcommit(u"WIP: Föo bar\n\nFoo bår hur dur bla bla") + + # By default, we expect both the violations of the regular rule as well as the named rule to show up + violations = [RuleViolation("T5", u"Title contains the word 'WIP' (case-insensitive)", u"WIP: Föo bar", 1), + RuleViolation(u"T5:my-ïd", u"Title contains the word 'Föo' (case-insensitive)", + u"WIP: Föo bar", 1)] + self.assertListEqual(violations, linter.lint(commit)) + + # ignore regular rule: only named rule violations show up + lint_config.ignore = ["T5"] + self.assertListEqual(violations[1:], linter.lint(commit)) + + # ignore named rule by id: only regular rule violations show up + lint_config.ignore = [rule_id] + self.assertListEqual(violations[:-1], linter.lint(commit)) + + # ignore named rule by name: only regular rule violations show up + lint_config.ignore = [TitleMustNotContainWord.name + u":my-ïd"] + self.assertListEqual(violations[:-1], linter.lint(commit)) diff --git a/gitlint/tests/test_options.py b/gitlint/tests/test_options.py index 2c17226..68f0f8c 100644 --- a/gitlint/tests/test_options.py +++ b/gitlint/tests/test_options.py @@ -1,42 +1,51 @@ # -*- coding: utf-8 -*- import os +import re from gitlint.tests.base import BaseTestCase -from gitlint.options import IntOption, BoolOption, StrOption, ListOption, PathOption, RuleOptionError +from gitlint.options import IntOption, BoolOption, StrOption, ListOption, PathOption, RegexOption, RuleOptionError class RuleOptionTests(BaseTestCase): def test_option_equality(self): - # 2 options are equal if their name, value and description match - option1 = IntOption("test-option", 123, u"Test Dëscription") - option2 = IntOption("test-option", 123, u"Test Dëscription") - self.assertEqual(option1, option2) - - # Not equal: name, description, value are different - self.assertNotEqual(option1, IntOption("test-option1", 123, u"Test Dëscription")) - self.assertNotEqual(option1, IntOption("test-option", 1234, u"Test Dëscription")) - self.assertNotEqual(option1, IntOption("test-option", 123, u"Test Dëscription2")) + options = {IntOption: 123, StrOption: u"foöbar", BoolOption: False, ListOption: ["a", "b"], + PathOption: ".", RegexOption: u"^foöbar(.*)"} + for clazz, val in options.items(): + # 2 options are equal if their name, value and description match + option1 = clazz(u"test-öption", val, u"Test Dëscription") + option2 = clazz(u"test-öption", val, u"Test Dëscription") + self.assertEqual(option1, option2) + + # Not equal: class, name, description, value are different + self.assertNotEqual(option1, IntOption(u"tëst-option1", 123, u"Test Dëscription")) + self.assertNotEqual(option1, StrOption(u"tëst-option1", u"åbc", u"Test Dëscription")) + self.assertNotEqual(option1, StrOption(u"tëst-option", u"åbcd", u"Test Dëscription")) + self.assertNotEqual(option1, StrOption(u"tëst-option", u"åbc", u"Test Dëscription2")) def test_int_option(self): # normal behavior - option = IntOption("test-name", 123, "Test Description") + option = IntOption(u"tëst-name", 123, u"Tëst Description") + self.assertEqual(option.name, u"tëst-name") + self.assertEqual(option.description, u"Tëst Description") self.assertEqual(option.value, 123) - self.assertEqual(option.name, "test-name") - self.assertEqual(option.description, "Test Description") # re-set value option.set(456) self.assertEqual(option.value, 456) + # set to None + option.set(None) + self.assertEqual(option.value, None) + # error on negative int when not allowed - expected_error = u"Option 'test-name' must be a positive integer (current value: '-123')" - with self.assertRaisesRegex(RuleOptionError, expected_error): + expected_error = u"Option 'tëst-name' must be a positive integer (current value: '-123')" + with self.assertRaisesMessage(RuleOptionError, expected_error): option.set(-123) # error on non-int value - expected_error = u"Option 'test-name' must be a positive integer (current value: 'foo')" - with self.assertRaisesRegex(RuleOptionError, expected_error): + expected_error = u"Option 'tëst-name' must be a positive integer (current value: 'foo')" + with self.assertRaisesMessage(RuleOptionError, expected_error): option.set("foo") # no error on negative value when allowed and negative int is passed @@ -46,15 +55,15 @@ class RuleOptionTests(BaseTestCase): # error on non-int value when negative int is allowed expected_error = u"Option 'test-name' must be an integer (current value: 'foo')" - with self.assertRaisesRegex(RuleOptionError, expected_error): + with self.assertRaisesMessage(RuleOptionError, expected_error): option.set("foo") def test_str_option(self): # normal behavior - option = StrOption("test-name", u"föo", "Test Description") + option = StrOption(u"tëst-name", u"föo", u"Tëst Description") + self.assertEqual(option.name, u"tëst-name") + self.assertEqual(option.description, u"Tëst Description") self.assertEqual(option.value, u"föo") - self.assertEqual(option.name, "test-name") - self.assertEqual(option.description, "Test Description") # re-set value option.set(u"bår") @@ -68,9 +77,15 @@ class RuleOptionTests(BaseTestCase): option.set(-123) self.assertEqual(option.value, "-123") + # None value + option.set(None) + self.assertEqual(option.value, None) + def test_boolean_option(self): # normal behavior - option = BoolOption("test-name", "true", "Test Description") + option = BoolOption(u"tëst-name", "true", u"Tëst Description") + self.assertEqual(option.name, u"tëst-name") + self.assertEqual(option.description, u"Tëst Description") self.assertEqual(option.value, True) # re-set value @@ -82,14 +97,16 @@ class RuleOptionTests(BaseTestCase): self.assertEqual(option.value, True) # error on incorrect value - incorrect_values = [1, -1, "foo", u"bår", ["foo"], {'foo': "bar"}] + incorrect_values = [1, -1, "foo", u"bår", ["foo"], {'foo': "bar"}, None] for value in incorrect_values: - with self.assertRaisesRegex(RuleOptionError, "Option 'test-name' must be either 'true' or 'false'"): + with self.assertRaisesMessage(RuleOptionError, u"Option 'tëst-name' must be either 'true' or 'false'"): option.set(value) def test_list_option(self): # normal behavior - option = ListOption("test-name", u"å,b,c,d", "Test Description") + option = ListOption(u"tëst-name", u"å,b,c,d", u"Tëst Description") + self.assertEqual(option.name, u"tëst-name") + self.assertEqual(option.description, u"Tëst Description") self.assertListEqual(option.value, [u"å", u"b", u"c", u"d"]) # re-set value @@ -100,6 +117,10 @@ class RuleOptionTests(BaseTestCase): option.set([u"foo", u"bår", u"test"]) self.assertListEqual(option.value, [u"foo", u"bår", u"test"]) + # None + option.set(None) + self.assertIsNone(option.value) + # empty string option.set("") self.assertListEqual(option.value, []) @@ -129,40 +150,44 @@ class RuleOptionTests(BaseTestCase): self.assertListEqual(option.value, ["123"]) def test_path_option(self): - option = PathOption("test-directory", ".", u"Test Description", type=u"dir") + option = PathOption(u"tëst-directory", ".", u"Tëst Description", type=u"dir") + self.assertEqual(option.name, u"tëst-directory") + self.assertEqual(option.description, u"Tëst Description") self.assertEqual(option.value, os.getcwd()) - self.assertEqual(option.name, "test-directory") - self.assertEqual(option.description, u"Test Description") self.assertEqual(option.type, u"dir") # re-set value option.set(self.SAMPLES_DIR) self.assertEqual(option.value, self.SAMPLES_DIR) + # set to None + option.set(None) + self.assertIsNone(option.value) + # set to int - expected = u"Option test-directory must be an existing directory (current value: '1234')" - with self.assertRaisesRegex(RuleOptionError, expected): + expected = u"Option tëst-directory must be an existing directory (current value: '1234')" + with self.assertRaisesMessage(RuleOptionError, expected): option.set(1234) # set to non-existing directory non_existing_path = os.path.join(u"/föo", u"bar") - expected = u"Option test-directory must be an existing directory (current value: '{0}')" - with self.assertRaisesRegex(RuleOptionError, expected.format(non_existing_path)): + expected = u"Option tëst-directory must be an existing directory (current value: '{0}')" + with self.assertRaisesMessage(RuleOptionError, expected.format(non_existing_path)): option.set(non_existing_path) # set to a file, should raise exception since option.type = dir sample_path = self.get_sample_path(os.path.join("commit_message", "sample1")) - expected = u"Option test-directory must be an existing directory (current value: '{0}')".format(sample_path) - with self.assertRaisesRegex(RuleOptionError, expected): + expected = u"Option tëst-directory must be an existing directory (current value: '{0}')".format(sample_path) + with self.assertRaisesMessage(RuleOptionError, expected): option.set(sample_path) # set option.type = file, file should now be accepted, directories not option.type = u"file" option.set(sample_path) self.assertEqual(option.value, sample_path) - expected = u"Option test-directory must be an existing file (current value: '{0}')".format( + expected = u"Option tëst-directory must be an existing file (current value: '{0}')".format( self.get_sample_path()) - with self.assertRaisesRegex(RuleOptionError, expected): + with self.assertRaisesMessage(RuleOptionError, expected): option.set(self.get_sample_path()) # set option.type = both, files and directories should now be accepted @@ -174,6 +199,27 @@ class RuleOptionTests(BaseTestCase): # Expect exception if path type is invalid option.type = u'föo' - expected = u"Option test-directory type must be one of: 'file', 'dir', 'both' (current: 'föo')" - with self.assertRaisesRegex(RuleOptionError, expected): + expected = u"Option tëst-directory type must be one of: 'file', 'dir', 'both' (current: 'föo')" + with self.assertRaisesMessage(RuleOptionError, expected): option.set("haha") + + def test_regex_option(self): + # normal behavior + option = RegexOption(u"tëst-regex", u"^myrëgex(.*)foo$", u"Tëst Regex Description") + self.assertEqual(option.name, u"tëst-regex") + self.assertEqual(option.description, u"Tëst Regex Description") + self.assertEqual(option.value, re.compile(u"^myrëgex(.*)foo$", re.UNICODE)) + + # re-set value + option.set(u"[0-9]föbar.*") + self.assertEqual(option.value, re.compile(u"[0-9]föbar.*", re.UNICODE)) + + # set None + option.set(None) + self.assertIsNone(option.value) + + # error on invalid regex + incorrect_values = [u"foo(", 123, -1] + for value in incorrect_values: + with self.assertRaisesRegex(RuleOptionError, u"Invalid regular expression"): + option.set(value) diff --git a/gitlint/tests/test_utils.py b/gitlint/tests/test_utils.py index 6f667c2..5841b63 100644 --- a/gitlint/tests/test_utils.py +++ b/gitlint/tests/test_utils.py @@ -60,19 +60,23 @@ class UtilsTests(BaseTestCase): patched_env.get.side_effect = mocked_get # Assert getpreferredencoding reads env vars in order: LC_ALL, LC_CTYPE, LANG - mock_env = {"LC_ALL": u"lc_all_välue", "LC_CTYPE": u"foo", "LANG": u"bar"} - self.assertEqual(utils.getpreferredencoding(), u"lc_all_välue") - mock_env = {"LC_CTYPE": u"lc_ctype_välue", "LANG": u"hur"} - self.assertEqual(utils.getpreferredencoding(), u"lc_ctype_välue") - mock_env = {"LANG": u"lang_välue"} - self.assertEqual(utils.getpreferredencoding(), u"lang_välue") + mock_env = {"LC_ALL": u"ASCII", "LC_CTYPE": u"UTF-16", "LANG": u"CP1251"} + self.assertEqual(utils.getpreferredencoding(), u"ASCII") + mock_env = {"LC_CTYPE": u"UTF-16", "LANG": u"CP1251"} + self.assertEqual(utils.getpreferredencoding(), u"UTF-16") + mock_env = {"LANG": u"CP1251"} + self.assertEqual(utils.getpreferredencoding(), u"CP1251") # Assert split on dot - mock_env = {"LANG": u"foo.bär"} - self.assertEqual(utils.getpreferredencoding(), u"bär") + mock_env = {"LANG": u"foo.UTF-16"} + self.assertEqual(utils.getpreferredencoding(), u"UTF-16") # assert default encoding is UTF-8 mock_env = {} self.assertEqual(utils.getpreferredencoding(), "UTF-8") mock_env = {"FOO": u"föo"} self.assertEqual(utils.getpreferredencoding(), "UTF-8") + + # assert fallback encoding is UTF-8 in case we set an unavailable encoding + mock_env = {"LC_ALL": u"foo"} + self.assertEqual(utils.getpreferredencoding(), u"UTF-8") |