diff options
Diffstat (limited to 'qa')
38 files changed, 1407 insertions, 0 deletions
diff --git a/qa/__init__.py b/qa/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/qa/__init__.py diff --git a/qa/base.py b/qa/base.py new file mode 100644 index 0000000..05d85e5 --- /dev/null +++ b/qa/base.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return, +# pylint: disable=too-many-function-args,unexpected-keyword-arg + +import io +import os +import platform +import shutil +import sys +import tempfile +from datetime import datetime +from uuid import uuid4 + +import arrow + +try: + # python 2.x + from unittest2 import TestCase +except ImportError: + # python 3.x + from unittest import TestCase + +from qa.shell import git, gitlint, RunningCommand +from qa.utils import DEFAULT_ENCODING, ustr + + +class BaseTestCase(TestCase): + """ Base class of which all gitlint integration test classes are derived. + Provides a number of convenience methods. """ + + # In case of assert failures, print the full error message + maxDiff = None + tmp_git_repo = None + + GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]") + GIT_CONTEXT_ERROR_CODE = 254 + + @classmethod + def setUpClass(cls): + """ Sets up the integration tests by creating a new temporary git repository """ + cls.tmp_git_repos = [] + cls.tmp_git_repo = cls.create_tmp_git_repo() + + @classmethod + def tearDownClass(cls): + """ Cleans up the temporary git repositories """ + for repo in cls.tmp_git_repos: + shutil.rmtree(repo) + + def setUp(self): + self.tmpfiles = [] + + def tearDown(self): + for tmpfile in self.tmpfiles: + os.remove(tmpfile) + + def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name + self.assertIsInstance(output, RunningCommand) + output = ustr(output.stdout) + output = output.replace('\r', '') + self.assertMultiLineEqual(output, expected) + + @classmethod + def generate_temp_path(cls): + return os.path.realpath("/tmp/gitlint-test-{0}".format(datetime.now().strftime("%Y%m%d-%H%M%S-%f"))) + + @classmethod + def create_tmp_git_repo(cls): + """ Creates a temporary git repository and returns its directory path """ + tmp_git_repo = cls.generate_temp_path() + cls.tmp_git_repos.append(tmp_git_repo) + + git("init", tmp_git_repo) + # configuring name and email is required in every git repot + git("config", "user.name", "gitlint-test-user", _cwd=tmp_git_repo) + git("config", "user.email", "gitlint@test.com", _cwd=tmp_git_repo) + + # Git does not by default print unicode paths, fix that by setting core.quotePath to false + # http://stackoverflow.com/questions/34549040/git-not-displaying-unicode-file-names + # ftp://www.kernel.org/pub/software/scm/git/docs/git-config.html + git("config", "core.quotePath", "false", _cwd=tmp_git_repo) + + # Git on mac doesn't like unicode characters by default, so we need to set this option + # http://stackoverflow.com/questions/5581857/git-and-the-umlaut-problem-on-mac-os-x + git("config", "core.precomposeunicode", "true", _cwd=tmp_git_repo) + + return tmp_git_repo + + @staticmethod + def create_file(parent_dir): + """ Creates a file inside a passed directory. Returns filename.""" + test_filename = u"test-fïle-" + str(uuid4()) + io.open(os.path.join(parent_dir, test_filename), 'a', encoding=DEFAULT_ENCODING).close() + return test_filename + + def create_simple_commit(self, message, out=None, ok_code=None, env=None, git_repo=None, tty_in=False): + """ Creates a simple commit with an empty test file. + :param message: Commit message for the commit. """ + + git_repo = self.tmp_git_repo if git_repo is None else git_repo + + # Let's make sure that we copy the environment in which this python code was executed as environment + # variables can influence how git runs. + # This was needed to fix https://github.com/jorisroovers/gitlint/issues/15 as we need to make sure to use + # the PATH variable that contains the virtualenv's python binary. + environment = os.environ + if env: + environment.update(env) + + # Create file and add to git + test_filename = self.create_file(git_repo) + git("add", test_filename, _cwd=git_repo) + # https://amoffat.github.io/sh/#interactive-callbacks + if not ok_code: + ok_code = [0] + + git("commit", "-m", message, _cwd=git_repo, _err_to_out=True, _out=out, _tty_in=tty_in, + _ok_code=ok_code, _env=environment) + return test_filename + + def create_tmpfile(self, content): + """ Utility method to create temp files. These are cleaned at the end of the test """ + # Not using a context manager to avoid unneccessary identation in test code + tmpfile, tmpfilepath = tempfile.mkstemp() + self.tmpfiles.append(tmpfilepath) + with io.open(tmpfile, "w", encoding=DEFAULT_ENCODING) as f: + f.write(content) + return tmpfilepath + + @staticmethod + def get_example_path(filename=""): + examples_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../examples") + return os.path.join(examples_dir, filename) + + @staticmethod + def get_sample_path(filename=""): + samples_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "samples") + return os.path.join(samples_dir, filename) + + def get_last_commit_short_hash(self, git_repo=None): + git_repo = self.tmp_git_repo if git_repo is None else git_repo + return git("rev-parse", "--short", "HEAD", _cwd=git_repo, _err_to_out=True).replace("\n", "") + + def get_last_commit_hash(self, git_repo=None): + git_repo = self.tmp_git_repo if git_repo is None else git_repo + return git("rev-parse", "HEAD", _cwd=git_repo, _err_to_out=True).replace("\n", "") + + @staticmethod + def get_expected(filename="", variable_dict=None): + """ Utility method to read an 'expected' file and return it as a string. Optionally replace template variables + specified by variable_dict. """ + expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected") + expected_path = os.path.join(expected_dir, filename) + expected = io.open(expected_path, encoding=DEFAULT_ENCODING).read() + + if variable_dict: + expected = expected.format(**variable_dict) + return expected + + @staticmethod + def get_system_info_dict(): + """ Returns a dict with items related to system values logged by `gitlint --debug` """ + expected_gitlint_version = gitlint("--version").replace("gitlint, version ", "").replace("\n", "") + expected_git_version = git("--version").replace("\n", "") + return {'platform': platform.platform(), 'python_version': sys.version, + 'git_version': expected_git_version, 'gitlint_version': expected_gitlint_version, + 'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB} + + def get_debug_vars_last_commit(self, git_repo=None): + """ Returns a dict with items related to `gitlint --debug` output for the last commit. """ + target_repo = git_repo if git_repo else self.tmp_git_repo + commit_sha = self.get_last_commit_hash(git_repo=target_repo) + expected_date = git("log", "-1", "--pretty=%ai", _tty_out=False, _cwd=target_repo) + expected_date = arrow.get(str(expected_date), "YYYY-MM-DD HH:mm:ss Z").format("YYYY-MM-DD HH:mm:ss Z") + + expected_kwargs = self.get_system_info_dict() + expected_kwargs.update({'target': target_repo, 'commit_sha': commit_sha, 'commit_date': expected_date}) + return expected_kwargs diff --git a/qa/expected/test_commits/test_ignore_commits_1 b/qa/expected/test_commits/test_ignore_commits_1 new file mode 100644 index 0000000..f9062c1 --- /dev/null +++ b/qa/expected/test_commits/test_ignore_commits_1 @@ -0,0 +1,11 @@ +Commit {commit_sha0}: +1: T3 Title has trailing punctuation (.): "Sïmple title4." + +Commit {commit_sha1}: +1: T5 Title contains the word 'WIP' (case-insensitive): "Sïmple WIP title3." + +Commit {commit_sha2}: +3: B5 Body message is too short (5<20): "Short" + +Commit {commit_sha3}: +1: T3 Title has trailing punctuation (.): "Sïmple title." diff --git a/qa/expected/test_commits/test_lint_head_1 b/qa/expected/test_commits/test_lint_head_1 new file mode 100644 index 0000000..d7ca594 --- /dev/null +++ b/qa/expected/test_commits/test_lint_head_1 @@ -0,0 +1,8 @@ +Commit {commit_sha0}: +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Sïmple title" + +Commit {commit_sha1}: +3: B6 Body message is missing + +Commit {commit_sha2}: +1: T3 Title has trailing punctuation (.): "Sïmple title." diff --git a/qa/expected/test_commits/test_lint_staged_msg_filename_1 b/qa/expected/test_commits/test_lint_staged_msg_filename_1 new file mode 100644 index 0000000..878bc4c --- /dev/null +++ b/qa/expected/test_commits/test_lint_staged_msg_filename_1 @@ -0,0 +1,73 @@ +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} +DEBUG: gitlint.cli Gitlint version: {gitlint_version} +DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli Configuration +config-path: None +[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: True +verbosity: 3 +debug: True +target: {target} +[RULES] + I1: ignore-by-title + ignore=all + regex=None + I2: ignore-by-body + ignore=all + 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 + T7: title-match-regex + regex=.* + 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= + M1: author-valid-email + regex=[^@ ]+@[^@ ]+\.[^@ ]+ + +DEBUG: gitlint.cli Fetching additional meta-data from staged commit +DEBUG: gitlint.cli Using --msg-filename. +DEBUG: gitlint.cli Linting 1 commit(s) +DEBUG: gitlint.lint Linting commit [SHA UNKNOWN] +DEBUG: gitlint.lint Commit Object +--- Commit Message ---- +WIP: from fïle test. +--- Meta info --------- +Author: gitlint-test-user <gitlint@test.com> +Date: {staged_date} +is-merge-commit: False +is-fixup-commit: False +is-squash-commit: False +is-revert-commit: False +Branches: ['master'] +Changed Files: {changed_files} +----------------------- +1: T3 Title has trailing punctuation (.): "WIP: from fïle test." +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: from fïle test." +3: B6 Body message is missing +DEBUG: gitlint.cli Exit Code = 3 diff --git a/qa/expected/test_commits/test_lint_staged_stdin_1 b/qa/expected/test_commits/test_lint_staged_stdin_1 new file mode 100644 index 0000000..3f178f8 --- /dev/null +++ b/qa/expected/test_commits/test_lint_staged_stdin_1 @@ -0,0 +1,75 @@ +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} +DEBUG: gitlint.cli Gitlint version: {gitlint_version} +DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli Configuration +config-path: None +[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: True +verbosity: 3 +debug: True +target: {target} +[RULES] + I1: ignore-by-title + ignore=all + regex=None + I2: ignore-by-body + ignore=all + 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 + T7: title-match-regex + regex=.* + 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= + M1: author-valid-email + regex=[^@ ]+@[^@ ]+\.[^@ ]+ + +DEBUG: gitlint.cli Fetching additional meta-data from staged commit +DEBUG: gitlint.cli Stdin data: 'WIP: Pïpe test. +' +DEBUG: gitlint.cli Stdin detected and not ignored. Using as input. +DEBUG: gitlint.cli Linting 1 commit(s) +DEBUG: gitlint.lint Linting commit [SHA UNKNOWN] +DEBUG: gitlint.lint Commit Object +--- Commit Message ---- +WIP: Pïpe test. +--- Meta info --------- +Author: gitlint-test-user <gitlint@test.com> +Date: {staged_date} +is-merge-commit: False +is-fixup-commit: False +is-squash-commit: False +is-revert-commit: False +Branches: ['master'] +Changed Files: {changed_files} +----------------------- +1: T3 Title has trailing punctuation (.): "WIP: Pïpe test." +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Pïpe test." +3: B6 Body message is missing +DEBUG: gitlint.cli Exit Code = 3 diff --git a/qa/expected/test_commits/test_violations_1 b/qa/expected/test_commits/test_violations_1 new file mode 100644 index 0000000..6f3f9e2 --- /dev/null +++ b/qa/expected/test_commits/test_violations_1 @@ -0,0 +1,7 @@ +Commit {commit_sha2}: +1: T3 Title has trailing punctuation (.): "Sïmple title3." +3: B6 Body message is missing + +Commit {commit_sha1}: +1: T3 Title has trailing punctuation (.): "Sïmple title2." +3: B6 Body message is missing diff --git a/qa/expected/test_config/test_config_from_file_1 b/qa/expected/test_config/test_config_from_file_1 new file mode 100644 index 0000000..6fe434a --- /dev/null +++ b/qa/expected/test_config/test_config_from_file_1 @@ -0,0 +1,5 @@ +1: T1 Title exceeds max length (42>20) +1: T5 Title contains the word 'WIP' (case-insensitive) +1: T5 Title contains the word 'thåt' (case-insensitive) +2: B4 Second line is not empty +3: B1 Line exceeds max length (48>30) diff --git a/qa/expected/test_config/test_config_from_file_debug_1 b/qa/expected/test_config/test_config_from_file_debug_1 new file mode 100644 index 0000000..443ee26 --- /dev/null +++ b/qa/expected/test_config/test_config_from_file_debug_1 @@ -0,0 +1,77 @@ +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} +DEBUG: gitlint.cli Gitlint version: {gitlint_version} +DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} +DEBUG: gitlint.cli Configuration +config-path: {config_path} +[GENERAL] +extra-path: None +contrib: [] +ignore: title-trailing-punctuation,B2 +ignore-merge-commits: True +ignore-fixup-commits: True +ignore-squash-commits: True +ignore-revert-commits: True +ignore-stdin: False +staged: False +verbosity: 2 +debug: True +target: {target} +[RULES] + I1: ignore-by-title + ignore=all + regex=None + I2: ignore-by-body + ignore=all + regex=None + T1: title-max-length + line-length=20 + T2: title-trailing-whitespace + T6: title-leading-whitespace + T3: title-trailing-punctuation + T4: title-hard-tab + T5: title-must-not-contain-word + words=WIP,thåt + T7: title-match-regex + regex=.* + B1: body-max-line-length + line-length=30 + 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= + 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.cli Linting 1 commit(s) +DEBUG: gitlint.lint Linting commit {commit_sha} +DEBUG: gitlint.lint Commit Object +--- Commit Message ---- +WIP: Thïs is a title thåt is a bit longer. +Content on the second line +This line of the body is here because we need it + +--- Meta info --------- +Author: gitlint-test-user <gitlint@test.com> +Date: {commit_date} +is-merge-commit: False +is-fixup-commit: False +is-squash-commit: False +is-revert-commit: False +Branches: ['master'] +Changed Files: {changed_files} +----------------------- +1: T1 Title exceeds max length (42>20) +1: T5 Title contains the word 'WIP' (case-insensitive) +1: T5 Title contains the word 'thåt' (case-insensitive) +2: B4 Second line is not empty +3: B1 Line exceeds max length (48>30) +DEBUG: gitlint.cli Exit Code = 5 diff --git a/qa/expected/test_config/test_set_rule_option_1 b/qa/expected/test_config/test_set_rule_option_1 new file mode 100644 index 0000000..10b5e50 --- /dev/null +++ b/qa/expected/test_config/test_set_rule_option_1 @@ -0,0 +1,3 @@ +1: T1 Title exceeds max length (16>5): "This ïs a title." +1: T3 Title has trailing punctuation (.): "This ïs a title." +3: B6 Body message is missing diff --git a/qa/expected/test_config/test_verbosity_1 b/qa/expected/test_config/test_verbosity_1 new file mode 100644 index 0000000..0202072 --- /dev/null +++ b/qa/expected/test_config/test_verbosity_1 @@ -0,0 +1,3 @@ +1: T3 Title has trailing punctuation (.) +1: T5 Title contains the word 'WIP' (case-insensitive) +2: B4 Second line is not empty diff --git a/qa/expected/test_config/test_verbosity_2 b/qa/expected/test_config/test_verbosity_2 new file mode 100644 index 0000000..5a54082 --- /dev/null +++ b/qa/expected/test_config/test_verbosity_2 @@ -0,0 +1,3 @@ +1: T3 Title has trailing punctuation (.): "WIP: Thïs is a title." +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thïs is a title." +2: B4 Second line is not empty: "Contënt on the second line" diff --git a/qa/expected/test_contrib/test_contrib_rules_1 b/qa/expected/test_contrib/test_contrib_rules_1 new file mode 100644 index 0000000..99b33b7 --- /dev/null +++ b/qa/expected/test_contrib/test_contrib_rules_1 @@ -0,0 +1,4 @@ +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: "WIP Thi$ is å title" +1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "WIP Thi$ is å title" +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title" diff --git a/qa/expected/test_contrib/test_contrib_rules_with_config_1 b/qa/expected/test_contrib/test_contrib_rules_with_config_1 new file mode 100644 index 0000000..21d467a --- /dev/null +++ b/qa/expected/test_contrib/test_contrib_rules_with_config_1 @@ -0,0 +1,4 @@ +1: CC1 Body does not contain a 'Signed-Off-By' line +1: CT1 Title does not start with one of föo, bår: "WIP Thi$ is å title" +1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "WIP Thi$ is å title" +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title" diff --git a/qa/expected/test_gitlint/test_msg_filename_1 b/qa/expected/test_gitlint/test_msg_filename_1 new file mode 100644 index 0000000..d01b23b --- /dev/null +++ b/qa/expected/test_gitlint/test_msg_filename_1 @@ -0,0 +1,3 @@ +1: T3 Title has trailing punctuation (.): "WIP: msg-fïlename test." +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: msg-fïlename test." +3: B6 Body message is missing diff --git a/qa/expected/test_gitlint/test_msg_filename_no_tty_1 b/qa/expected/test_gitlint/test_msg_filename_no_tty_1 new file mode 100644 index 0000000..4785e28 --- /dev/null +++ b/qa/expected/test_gitlint/test_msg_filename_no_tty_1 @@ -0,0 +1,3 @@ +1: T3 Title has trailing punctuation (.): "WIP: msg-fïlename NO TTY test." +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: msg-fïlename NO TTY test." +3: B6 Body message is missing diff --git a/qa/expected/test_gitlint/test_violations_1 b/qa/expected/test_gitlint/test_violations_1 new file mode 100644 index 0000000..7e55eda --- /dev/null +++ b/qa/expected/test_gitlint/test_violations_1 @@ -0,0 +1,3 @@ +1: T3 Title has trailing punctuation (.): "WIP: This ïs a title." +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: This ïs a title." +2: B4 Second line is not empty: "Content on the sëcond line" diff --git a/qa/expected/test_stdin/test_stdin_file_1 b/qa/expected/test_stdin/test_stdin_file_1 new file mode 100644 index 0000000..ea7fad2 --- /dev/null +++ b/qa/expected/test_stdin/test_stdin_file_1 @@ -0,0 +1,3 @@ +1: T3 Title has trailing punctuation (.): "WIP: STDIN ïs a file test." +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: STDIN ïs a file test." +3: B6 Body message is missing diff --git a/qa/expected/test_stdin/test_stdin_pipe_1 b/qa/expected/test_stdin/test_stdin_pipe_1 new file mode 100644 index 0000000..8714533 --- /dev/null +++ b/qa/expected/test_stdin/test_stdin_pipe_1 @@ -0,0 +1,3 @@ +1: T3 Title has trailing punctuation (.): "WIP: Pïpe test." +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Pïpe test." +3: B6 Body message is missing diff --git a/qa/expected/test_stdin/test_stdin_pipe_empty_1 b/qa/expected/test_stdin/test_stdin_pipe_empty_1 new file mode 100644 index 0000000..7e55eda --- /dev/null +++ b/qa/expected/test_stdin/test_stdin_pipe_empty_1 @@ -0,0 +1,3 @@ +1: T3 Title has trailing punctuation (.): "WIP: This ïs a title." +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: This ïs a title." +2: B4 Second line is not empty: "Content on the sëcond line" diff --git a/qa/expected/test_user_defined/test_user_defined_rules_examples_1 b/qa/expected/test_user_defined/test_user_defined_rules_examples_1 new file mode 100644 index 0000000..9d00445 --- /dev/null +++ b/qa/expected/test_user_defined/test_user_defined_rules_examples_1 @@ -0,0 +1,5 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title" +1: UC2 Body does not contain a 'Signed-Off-By' line +1: UC3 Branch name 'master' does not start with one of ['feature/', 'hotfix/', 'release/'] +1: UL1 Title contains the special character '$': "WIP: Thi$ is å title" +2: B4 Second line is not empty: "Content on the second line" diff --git a/qa/expected/test_user_defined/test_user_defined_rules_examples_with_config_1 b/qa/expected/test_user_defined/test_user_defined_rules_examples_with_config_1 new file mode 100644 index 0000000..a143715 --- /dev/null +++ b/qa/expected/test_user_defined/test_user_defined_rules_examples_with_config_1 @@ -0,0 +1,6 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title" +1: UC1 Body contains too many lines (2 > 1) +1: UC2 Body does not contain a 'Signed-Off-By' line +1: UC3 Branch name 'master' does not start with one of ['feature/', 'hotfix/', 'release/'] +1: UL1 Title contains the special character '$': "WIP: Thi$ is å title" +2: B4 Second line is not empty: "Content on the second line" diff --git a/qa/expected/test_user_defined/test_user_defined_rules_extra_1 b/qa/expected/test_user_defined/test_user_defined_rules_extra_1 new file mode 100644 index 0000000..65f3507 --- /dev/null +++ b/qa/expected/test_user_defined/test_user_defined_rules_extra_1 @@ -0,0 +1,5 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title" +1: UC1 GitContext.current_branch: master +1: UC1 GitContext.commentchar: # +1: UC2 GitCommit.branches: ['master'] +2: B4 Second line is not empty: "Content on the second line" diff --git a/qa/requirements.txt b/qa/requirements.txt new file mode 100644 index 0000000..f042dad --- /dev/null +++ b/qa/requirements.txt @@ -0,0 +1,4 @@ +sh==1.12.14 +pytest==4.6.3; +arrow==0.15.5; +gitlint # no version as you want to test the currently installed version diff --git a/qa/samples/config/contrib-enabled b/qa/samples/config/contrib-enabled new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/qa/samples/config/contrib-enabled diff --git a/qa/samples/config/gitlintconfig b/qa/samples/config/gitlintconfig new file mode 100644 index 0000000..a5ecb84 --- /dev/null +++ b/qa/samples/config/gitlintconfig @@ -0,0 +1,13 @@ +[general] +ignore=title-trailing-punctuation,B2 +verbosity = 2 + +[title-max-length] +line-length=20 + +[B1] +# B1 = body-max-line-length +line-length=30 + +[title-must-not-contain-word] +words=WIP,thåt
\ No newline at end of file diff --git a/qa/samples/config/ignore-release-commits b/qa/samples/config/ignore-release-commits new file mode 100644 index 0000000..5807c96 --- /dev/null +++ b/qa/samples/config/ignore-release-commits @@ -0,0 +1,7 @@ +[ignore-by-title] +regex=^Release(.*) +ignore=T5,T3 + +[ignore-by-body] +regex=(.*)relëase(.*) +ignore=T3,B3
\ No newline at end of file diff --git a/qa/samples/user_rules/extra/extra_rules.py b/qa/samples/user_rules/extra/extra_rules.py new file mode 100644 index 0000000..8109299 --- /dev/null +++ b/qa/samples/user_rules/extra/extra_rules.py @@ -0,0 +1,29 @@ +from gitlint.rules import CommitRule, RuleViolation +from gitlint.utils import sstr + + +class GitContextRule(CommitRule): + """ Rule that tests whether we can correctly access certain gitcontext properties """ + name = "gitcontext" + id = "UC1" + + def validate(self, commit): + violations = [ + RuleViolation(self.id, "GitContext.current_branch: {0}".format(commit.context.current_branch), line_nr=1), + RuleViolation(self.id, "GitContext.commentchar: {0}".format(commit.context.commentchar), line_nr=1) + ] + + return violations + + +class GitCommitRule(CommitRule): + """ Rule that tests whether we can correctly access certain commit properties """ + name = "gitcommit" + id = "UC2" + + def validate(self, commit): + violations = [ + RuleViolation(self.id, "GitCommit.branches: {0}".format(sstr(commit.branches)), line_nr=1), + ] + + return violations diff --git a/qa/samples/user_rules/incorrect_linerule/my_line_rule.py b/qa/samples/user_rules/incorrect_linerule/my_line_rule.py new file mode 100644 index 0000000..33e511f --- /dev/null +++ b/qa/samples/user_rules/incorrect_linerule/my_line_rule.py @@ -0,0 +1,8 @@ +from gitlint.rules import LineRule + + +class MyUserLineRule(LineRule): + id = "UC2" + name = "my-line-rule" + + # missing validate method, missing target attribute diff --git a/qa/shell.py b/qa/shell.py new file mode 100644 index 0000000..8ba6dc1 --- /dev/null +++ b/qa/shell.py @@ -0,0 +1,90 @@ + +# This code is mostly duplicated from the `gitlint.shell` module. We conciously duplicate this code as to not depend +# on gitlint internals for our integration testing framework. + +import subprocess +import sys +from qa.utils import ustr, USE_SH_LIB + +if USE_SH_LIB: + from sh import git, echo, gitlint # pylint: disable=unused-import,no-name-in-module,import-error + + # import exceptions separately, this makes it a little easier to mock them out in the unit tests + from sh import CommandNotFound, ErrorReturnCode, RunningCommand # pylint: disable=import-error +else: + + class CommandNotFound(Exception): + """ Exception indicating a command was not found during execution """ + pass + + class RunningCommand(object): + pass + + class ShResult(RunningCommand): + """ Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using + the builtin subprocess module. """ + + def __init__(self, full_cmd, stdout, stderr='', exitcode=0): + self.full_cmd = full_cmd + # TODO(jorisroovers): The 'sh' library by default will merge stdout and stderr. We mimic this behavior + # for now until we fully remove the 'sh' library. + self.stdout = stdout + ustr(stderr) + self.stderr = stderr + self.exit_code = exitcode + + def __str__(self): + return self.stdout + + class ErrorReturnCode(ShResult, Exception): + """ ShResult subclass for unexpected results (acts as an exception). """ + pass + + def git(*command_parts, **kwargs): + return run_command("git", *command_parts, **kwargs) + + def echo(*command_parts, **kwargs): + return run_command("echo", *command_parts, **kwargs) + + def gitlint(*command_parts, **kwargs): + return run_command("gitlint", *command_parts, **kwargs) + + def run_command(command, *args, **kwargs): + args = [command] + list(args) + result = _exec(*args, **kwargs) + # If we reach this point and the result has an exit_code that is larger than 0, this means that we didn't + # get an exception (which is the default sh behavior for non-zero exit codes) and so the user is expecting + # a non-zero exit code -> just return the entire result + if hasattr(result, 'exit_code') and result.exit_code > 0: + return result + return ustr(result) + + def _exec(*args, **kwargs): + if sys.version_info[0] == 2: + no_command_error = OSError # noqa pylint: disable=undefined-variable,invalid-name + else: + no_command_error = FileNotFoundError # noqa pylint: disable=undefined-variable + + pipe = subprocess.PIPE + popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs.get('_tty_out', False)} + if '_cwd' in kwargs: + popen_kwargs['cwd'] = kwargs['_cwd'] + + try: + p = subprocess.Popen(args, **popen_kwargs) + result = p.communicate() + except no_command_error: + raise CommandNotFound + + exit_code = p.returncode + stdout = ustr(result[0]) + stderr = result[1] # 'sh' does not decode the stderr bytes to unicode + full_cmd = '' if args is None else ' '.join(args) + + # If not _ok_code is specified, then only a 0 exit code is allowed + ok_exit_codes = kwargs.get('_ok_code', [0]) + + if exit_code in ok_exit_codes: + return ShResult(full_cmd, stdout, stderr, exit_code) + + # Unexpected error code => raise ErrorReturnCode + raise ErrorReturnCode(full_cmd, stdout, stderr, p.returncode) diff --git a/qa/test_commits.py b/qa/test_commits.py new file mode 100644 index 0000000..f485856 --- /dev/null +++ b/qa/test_commits.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# pylint: disable=too-many-function-args,unexpected-keyword-arg +import re + +import arrow + +from qa.shell import echo, git, gitlint +from qa.base import BaseTestCase +from qa.utils import sstr + + +class CommitsTests(BaseTestCase): + """ Integration tests for the --commits argument, i.e. linting multiple commits at once or linting specific commits + """ + + def test_successful(self): + """ Test linting multiple commits without violations """ + git("checkout", "-b", "test-branch-commits-base", _cwd=self.tmp_git_repo) + self.create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit") + git("checkout", "-b", "test-branch-commits", _cwd=self.tmp_git_repo) + self.create_simple_commit(u"Sïmple title2\n\nSimple bödy describing the commit2") + self.create_simple_commit(u"Sïmple title3\n\nSimple bödy describing the commit3") + output = gitlint("--commits", "test-branch-commits-base...test-branch-commits", + _cwd=self.tmp_git_repo, _tty_in=True) + self.assertEqualStdout(output, "") + + def test_violations(self): + """ Test linting multiple commits with violations """ + git("checkout", "-b", "test-branch-commits-violations-base", _cwd=self.tmp_git_repo) + self.create_simple_commit(u"Sïmple title.\n") + git("checkout", "-b", "test-branch-commits-violations", _cwd=self.tmp_git_repo) + + self.create_simple_commit(u"Sïmple title2.\n") + commit_sha1 = self.get_last_commit_hash()[:10] + self.create_simple_commit(u"Sïmple title3.\n") + commit_sha2 = self.get_last_commit_hash()[:10] + output = gitlint("--commits", "test-branch-commits-violations-base...test-branch-commits-violations", + _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) + + self.assertEqual(output.exit_code, 4) + expected_kwargs = {'commit_sha1': commit_sha1, 'commit_sha2': commit_sha2} + self.assertEqualStdout(output, self.get_expected("test_commits/test_violations_1", expected_kwargs)) + + def test_lint_single_commit(self): + """ Tests `gitlint --commits <sha>` """ + self.create_simple_commit(u"Sïmple title.\n") + self.create_simple_commit(u"Sïmple title2.\n") + commit_sha = self.get_last_commit_hash() + refspec = "{0}^...{0}".format(commit_sha) + self.create_simple_commit(u"Sïmple title3.\n") + output = gitlint("--commits", refspec, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) + expected = (u"1: T3 Title has trailing punctuation (.): \"Sïmple title2.\"\n" + + u"3: B6 Body message is missing\n") + self.assertEqual(output.exit_code, 2) + self.assertEqualStdout(output, expected) + + def test_lint_staged_stdin(self): + """ Tests linting a staged commit. Gitint should lint the passed commit message andfetch additional meta-data + from the underlying repository. The easiest way to test this is by inspecting `--debug` output. + This is the equivalent of doing: + echo "WIP: Pïpe test." | gitlint --staged --debug + """ + # Create a commit first, before we stage changes. This ensures the repo is properly initialized. + self.create_simple_commit(u"Sïmple title.\n") + + # Add some files, stage them: they should show up in the debug output as changed file + filename1 = self.create_file(self.tmp_git_repo) + git("add", filename1, _cwd=self.tmp_git_repo) + filename2 = self.create_file(self.tmp_git_repo) + git("add", filename2, _cwd=self.tmp_git_repo) + + output = gitlint(echo(u"WIP: Pïpe test."), "--staged", "--debug", + _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) + + # Determine variable parts of expected output + expected_kwargs = self.get_debug_vars_last_commit() + expected_kwargs.update({'changed_files': sstr(sorted([filename1, filename2]))}) + + # It's not really possible to determine the "Date: ..." line that is part of the debug output as this date + # is not taken from git but instead generated by gitlint itself. As a workaround, we extract the date from the + # gitlint output using a regex, parse the date to ensure the format is correct, and then pass that as an + # expected variable. + matches = re.search(r'^Date:\s+(.*)', str(output), re.MULTILINE) + if matches: + expected_date = arrow.get(str(matches.group(1)), "YYYY-MM-DD HH:mm:ss Z").format("YYYY-MM-DD HH:mm:ss Z") + expected_kwargs['staged_date'] = expected_date + + self.assertEqualStdout(output, self.get_expected("test_commits/test_lint_staged_stdin_1", expected_kwargs)) + self.assertEqual(output.exit_code, 3) + + def test_lint_staged_msg_filename(self): + """ Tests linting a staged commit. Gitint should lint the passed commit message andfetch additional meta-data + from the underlying repository. The easiest way to test this is by inspecting `--debug` output. + This is the equivalent of doing: + gitlint --msg-filename /tmp/my-commit-msg --staged --debug + """ + # Create a commit first, before we stage changes. This ensures the repo is properly initialized. + self.create_simple_commit(u"Sïmple title.\n") + + # Add some files, stage them: they should show up in the debug output as changed file + filename1 = self.create_file(self.tmp_git_repo) + git("add", filename1, _cwd=self.tmp_git_repo) + filename2 = self.create_file(self.tmp_git_repo) + git("add", filename2, _cwd=self.tmp_git_repo) + + tmp_commit_msg_file = self.create_tmpfile(u"WIP: from fïle test.") + + output = gitlint("--msg-filename", tmp_commit_msg_file, "--staged", "--debug", + _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) + + # Determine variable parts of expected output + expected_kwargs = self.get_debug_vars_last_commit() + expected_kwargs.update({'changed_files': sstr(sorted([filename1, filename2]))}) + + # It's not really possible to determine the "Date: ..." line that is part of the debug output as this date + # is not taken from git but instead generated by gitlint itself. As a workaround, we extract the date from the + # gitlint output using a regex, parse the date to ensure the format is correct, and then pass that as an + # expected variable. + matches = re.search(r'^Date:\s+(.*)', str(output), re.MULTILINE) + if matches: + expected_date = arrow.get(str(matches.group(1)), "YYYY-MM-DD HH:mm:ss Z").format("YYYY-MM-DD HH:mm:ss Z") + expected_kwargs['staged_date'] = expected_date + + expected = self.get_expected("test_commits/test_lint_staged_msg_filename_1", expected_kwargs) + self.assertEqualStdout(output, expected) + self.assertEqual(output.exit_code, 3) + + def test_lint_head(self): + """ Testing whether we can also recognize special refs like 'HEAD' """ + tmp_git_repo = self.create_tmp_git_repo() + self.create_simple_commit(u"Sïmple title.\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) + self.create_simple_commit(u"Sïmple title", git_repo=tmp_git_repo) + self.create_simple_commit(u"WIP: Sïmple title\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) + output = gitlint("--commits", "HEAD", _cwd=tmp_git_repo, _tty_in=True, _ok_code=[3]) + revlist = git("rev-list", "HEAD", _tty_in=True, _cwd=tmp_git_repo).split() + + expected_kwargs = {"commit_sha0": revlist[0][:10], "commit_sha1": revlist[1][:10], + "commit_sha2": revlist[2][:10]} + + self.assertEqualStdout(output, self.get_expected("test_commits/test_lint_head_1", expected_kwargs)) + + def test_ignore_commits(self): + """ Tests multiple commits of which some rules get igonored because of ignore-* rules """ + # Create repo and some commits + tmp_git_repo = self.create_tmp_git_repo() + self.create_simple_commit(u"Sïmple title.\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) + # Normally, this commit will give T3 (trailing-punctuation), T5 (WIP) and B5 (bod-too-short) violations + # But in this case only B5 because T3 and T5 are being ignored because of config + self.create_simple_commit(u"Release: WIP tïtle.\n\nShort", git_repo=tmp_git_repo) + # In the following 2 commits, the T3 violations are as normal + self.create_simple_commit( + u"Sïmple WIP title3.\n\nThis is \ta relëase commit\nMore info", git_repo=tmp_git_repo) + self.create_simple_commit(u"Sïmple title4.\n\nSimple bödy describing the commit4", git_repo=tmp_git_repo) + revlist = git("rev-list", "HEAD", _tty_in=True, _cwd=tmp_git_repo).split() + + config_path = self.get_sample_path("config/ignore-release-commits") + output = gitlint("--commits", "HEAD", "--config", config_path, _cwd=tmp_git_repo, _tty_in=True, _ok_code=[4]) + + expected_kwargs = {"commit_sha0": revlist[0][:10], "commit_sha1": revlist[1][:10], + "commit_sha2": revlist[2][:10], "commit_sha3": revlist[3][:10]} + self.assertEqualStdout(output, self.get_expected("test_commits/test_ignore_commits_1", expected_kwargs)) diff --git a/qa/test_config.py b/qa/test_config.py new file mode 100644 index 0000000..b893b1d --- /dev/null +++ b/qa/test_config.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# pylint: disable=too-many-function-args,unexpected-keyword-arg +from qa.shell import gitlint +from qa.base import BaseTestCase +from qa.utils import sstr + + +class ConfigTests(BaseTestCase): + """ Integration tests for gitlint configuration and configuration precedence. """ + + def test_ignore_by_id(self): + self.create_simple_commit(u"WIP: Thïs is a title.\nContënt on the second line") + output = gitlint("--ignore", "T5,B4", _tty_in=True, _cwd=self.tmp_git_repo, _ok_code=[1]) + expected = u"1: T3 Title has trailing punctuation (.): \"WIP: Thïs is a title.\"\n" + self.assertEqualStdout(output, expected) + + def test_ignore_by_name(self): + self.create_simple_commit(u"WIP: Thïs is a title.\nContënt on the second line") + output = gitlint("--ignore", "title-must-not-contain-word,body-first-line-empty", + _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) + expected = u"1: T3 Title has trailing punctuation (.): \"WIP: Thïs is a title.\"\n" + self.assertEqualStdout(output, expected) + + def test_verbosity(self): + self.create_simple_commit(u"WIP: Thïs is a title.\nContënt on the second line") + output = gitlint("-v", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) + + expected = u"1: T3\n1: T5\n2: B4\n" + self.assertEqualStdout(output, expected) + + output = gitlint("-vv", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) + self.assertEqualStdout(output, self.get_expected("test_config/test_verbosity_1")) + + output = gitlint("-vvv", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) + self.assertEqualStdout(output, self.get_expected("test_config/test_verbosity_2")) + + # test silent mode + output = gitlint("--silent", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) + self.assertEqualStdout(output, "") + + def test_set_rule_option(self): + self.create_simple_commit(u"This ïs a title.") + output = gitlint("-c", "title-max-length.line-length=5", _tty_in=True, _cwd=self.tmp_git_repo, _ok_code=[3]) + self.assertEqualStdout(output, self.get_expected("test_config/test_set_rule_option_1")) + + def test_config_from_file(self): + commit_msg = u"WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \ + "This line of the body is here because we need it" + self.create_simple_commit(commit_msg) + config_path = self.get_sample_path("config/gitlintconfig") + output = gitlint("--config", config_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) + self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_file_1")) + + def test_config_from_file_debug(self): + # Test bot on existing and new repo (we've had a bug in the past that was unique to empty repos) + repos = [self.tmp_git_repo, self.create_tmp_git_repo()] + for target_repo in repos: + commit_msg = u"WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \ + "This line of the body is here because we need it" + filename = self.create_simple_commit(commit_msg, git_repo=target_repo) + config_path = self.get_sample_path("config/gitlintconfig") + output = gitlint("--config", config_path, "--debug", _cwd=target_repo, _tty_in=True, _ok_code=[5]) + + expected_kwargs = self.get_debug_vars_last_commit(git_repo=target_repo) + expected_kwargs.update({'config_path': config_path, 'changed_files': sstr([filename])}) + self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_file_debug_1", + expected_kwargs)) diff --git a/qa/test_contrib.py b/qa/test_contrib.py new file mode 100644 index 0000000..e2b4bc5 --- /dev/null +++ b/qa/test_contrib.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# pylint: disable= +from qa.shell import gitlint +from qa.base import BaseTestCase + + +class ContribRuleTests(BaseTestCase): + """ Integration tests for contrib rules.""" + + def test_contrib_rules(self): + self.create_simple_commit(u"WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars") + output = gitlint("--contrib", "contrib-title-conventional-commits,CC1", + _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) + self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_1")) + + def test_contrib_rules_with_config(self): + self.create_simple_commit(u"WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars") + output = gitlint("--contrib", "contrib-title-conventional-commits,CC1", + "-c", u"contrib-title-conventional-commits.types=föo,bår", + _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) + self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_with_config_1")) + + def test_invalid_contrib_rules(self): + self.create_simple_commit("WIP: test") + output = gitlint("--contrib", u"föobar,CC1", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[255]) + self.assertEqualStdout(output, u"Config Error: No contrib rule with id or name 'föobar' found.\n") diff --git a/qa/test_gitlint.py b/qa/test_gitlint.py new file mode 100644 index 0000000..4762721 --- /dev/null +++ b/qa/test_gitlint.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# pylint: disable=too-many-function-args,unexpected-keyword-arg +import io +import os +from qa.shell import echo, git, gitlint +from qa.base import BaseTestCase +from qa.utils import DEFAULT_ENCODING + + +class IntegrationTests(BaseTestCase): + """ Simple set of integration tests for gitlint """ + + def test_successful(self): + # Test for STDIN with and without a TTY attached + self.create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit") + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _err_to_out=True) + self.assertEqualStdout(output, "") + + def test_successful_gitconfig(self): + """ Test gitlint when the underlying repo has specific git config set. + In the past, we've had issues with gitlint failing on some of these, so this acts as a regression test. """ + + # Different commentchar (Note: tried setting this to a special unicode char, but git doesn't like that) + git("config", "--add", "core.commentchar", "$", _cwd=self.tmp_git_repo) + self.create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit\n$after commentchar\t ignored") + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _err_to_out=True) + self.assertEqualStdout(output, "") + + def test_successful_merge_commit(self): + # Create branch on master + self.create_simple_commit(u"Cömmit on master\n\nSimple bödy") + + # Create test branch, add a commit and determine the commit hash + git("checkout", "-b", "test-branch", _cwd=self.tmp_git_repo) + git("checkout", "test-branch", _cwd=self.tmp_git_repo) + commit_title = u"Commit on test-brånch with a pretty long title that will cause issues when merging" + self.create_simple_commit(u"{0}\n\nSïmple body".format(commit_title)) + hash = self.get_last_commit_hash() + + # Checkout master and merge the commit + # We explicitly set the title of the merge commit to the title of the previous commit as this or similar + # behavior is what many tools do that handle merges (like github, gerrit, etc). + git("checkout", "master", _cwd=self.tmp_git_repo) + git("merge", "--no-ff", "-m", u"Merge '{0}'".format(commit_title), hash, _cwd=self.tmp_git_repo) + + # Run gitlint and assert output is empty + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True) + self.assertEqualStdout(output, "") + + # Assert that we do see the error if we disable the ignore-merge-commits option + output = gitlint("-c", "general.ignore-merge-commits=false", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) + self.assertEqual(output.exit_code, 1) + self.assertEqualStdout(output, + u"1: T1 Title exceeds max length (90>72): \"Merge '{0}'\"\n".format(commit_title)) + + def test_fixup_commit(self): + # Create a normal commit and assert that it has a violation + test_filename = self.create_simple_commit(u"Cömmit on WIP master\n\nSimple bödy that is long enough") + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) + expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP master\"\n" + self.assertEqualStdout(output, expected) + + # Make a small modification to the commit and commit it using fixup commit + with io.open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=DEFAULT_ENCODING) as fh: + # Wanted to write a unicode string, but that's obnoxious if you want to do it across Python 2 and 3. + # https://stackoverflow.com/questions/22392377/ + # error-writing-a-file-with-file-write-in-python-unicodeencodeerror + # So just keeping it simple - ASCII will here + fh.write(u"Appending some stuff\n") + + git("add", test_filename, _cwd=self.tmp_git_repo) + + git("commit", "--fixup", self.get_last_commit_hash(), _cwd=self.tmp_git_repo) + + # Assert that gitlint does not show an error for the fixup commit + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True) + # No need to check exit code, the command above throws an exception on > 0 exit codes + self.assertEqualStdout(output, "") + + # Make sure that if we set the ignore-fixup-commits option to false that we do still see the violations + output = gitlint("-c", "general.ignore-fixup-commits=false", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) + expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"fixup! Cömmit on WIP master\"\n" + \ + u"3: B6 Body message is missing\n" + + self.assertEqualStdout(output, expected) + + def test_revert_commit(self): + self.create_simple_commit(u"WIP: Cömmit on master.\n\nSimple bödy") + hash = self.get_last_commit_hash() + git("revert", hash, _cwd=self.tmp_git_repo) + + # Run gitlint and assert output is empty + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True) + self.assertEqualStdout(output, "") + + # Assert that we do see the error if we disable the ignore-revert-commits option + output = gitlint("-c", "general.ignore-revert-commits=false", + _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) + self.assertEqual(output.exit_code, 1) + expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Revert \"WIP: Cömmit on master.\"\"\n" + self.assertEqualStdout(output, expected) + + def test_squash_commit(self): + # Create a normal commit and assert that it has a violation + test_filename = self.create_simple_commit(u"Cömmit on WIP master\n\nSimple bödy that is long enough") + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1]) + expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP master\"\n" + self.assertEqualStdout(output, expected) + + # Make a small modification to the commit and commit it using squash commit + with io.open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=DEFAULT_ENCODING) as fh: + # Wanted to write a unicode string, but that's obnoxious if you want to do it across Python 2 and 3. + # https://stackoverflow.com/questions/22392377/ + # error-writing-a-file-with-file-write-in-python-unicodeencodeerror + # So just keeping it simple - ASCII will here + fh.write(u"Appending some stuff\n") + + git("add", test_filename, _cwd=self.tmp_git_repo) + + git("commit", "--squash", self.get_last_commit_hash(), "-m", u"Töo short body", _cwd=self.tmp_git_repo) + + # Assert that gitlint does not show an error for the fixup commit + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True) + # No need to check exit code, the command above throws an exception on > 0 exit codes + self.assertEqualStdout(output, "") + + # Make sure that if we set the ignore-squash-commits option to false that we do still see the violations + output = gitlint("-c", "general.ignore-squash-commits=false", + _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) + expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"squash! Cömmit on WIP master\"\n" + \ + u"3: B5 Body message is too short (14<20): \"Töo short body\"\n" + + self.assertEqualStdout(output, expected) + + def test_violations(self): + commit_msg = u"WIP: This ïs a title.\nContent on the sëcond line" + self.create_simple_commit(commit_msg) + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) + self.assertEqualStdout(output, self.get_expected("test_gitlint/test_violations_1")) + + def test_msg_filename(self): + tmp_commit_msg_file = self.create_tmpfile(u"WIP: msg-fïlename test.") + output = gitlint("--msg-filename", tmp_commit_msg_file, _tty_in=True, _ok_code=[3]) + self.assertEqualStdout(output, self.get_expected("test_gitlint/test_msg_filename_1")) + + def test_msg_filename_no_tty(self): + """ Make sure --msg-filename option also works with no TTY attached """ + tmp_commit_msg_file = self.create_tmpfile(u"WIP: msg-fïlename NO TTY test.") + + # We need to set _err_to_out explicitly for sh to merge stdout and stderr output in case there's + # no TTY attached to STDIN + # http://amoffat.github.io/sh/sections/special_arguments.html?highlight=_tty_in#err-to-out + # We need to pass some whitespace to _in as sh will otherwise hang, see + # https://github.com/amoffat/sh/issues/427 + output = gitlint("--msg-filename", tmp_commit_msg_file, _in=" ", + _tty_in=False, _err_to_out=True, _ok_code=[3]) + + self.assertEqualStdout(output, self.get_expected("test_gitlint/test_msg_filename_no_tty_1")) + + def test_git_errors(self): + # Repo has no commits: caused by `git log` + empty_git_repo = self.create_tmp_git_repo() + output = gitlint(_cwd=empty_git_repo, _tty_in=True, _ok_code=[self.GIT_CONTEXT_ERROR_CODE]) + + expected = u"Current branch has no commits. Gitlint requires at least one commit to function.\n" + self.assertEqualStdout(output, expected) + + # Repo has no commits: caused by `git rev-parse` + output = gitlint(echo(u"WIP: Pïpe test."), "--staged", _cwd=empty_git_repo, _tty_in=False, + _err_to_out=True, _ok_code=[self.GIT_CONTEXT_ERROR_CODE]) + self.assertEqualStdout(output, expected) diff --git a/qa/test_hooks.py b/qa/test_hooks.py new file mode 100644 index 0000000..a41580b --- /dev/null +++ b/qa/test_hooks.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# pylint: disable=too-many-function-args,unexpected-keyword-arg +import os +from qa.shell import git, gitlint +from qa.base import BaseTestCase + + +class HookTests(BaseTestCase): + """ Integration tests for gitlint commitmsg hooks""" + + VIOLATIONS = ['gitlint: checking commit message...\n', + u'1: T3 Title has trailing punctuation (.): "WIP: This ïs a title."\n', + u'1: T5 Title contains the word \'WIP\' (case-insensitive): "WIP: This ïs a title."\n', + u'2: B4 Second line is not empty: "Contënt on the second line"\n', + '3: B6 Body message is missing\n', + '-----------------------------------------------\n', + 'gitlint: \x1b[31mYour commit message contains the above violations.\x1b[0m\n'] + + def setUp(self): + self.responses = [] + self.response_index = 0 + self.githook_output = [] + + # The '--staged' flag used in the commit-msg hook fetches additional information from the underlying + # git repo which means there already needs to be a commit in the repo + # (as gitlint --staged doesn't work against empty repos) + self.create_simple_commit(u"Commït Title\n\nCommit Body explaining commit.") + + # install git commit-msg hook and assert output + output_installed = gitlint("install-hook", _cwd=self.tmp_git_repo) + expected_installed = u"Successfully installed gitlint commit-msg hook in %s/.git/hooks/commit-msg\n" % \ + self.tmp_git_repo + self.assertEqualStdout(output_installed, expected_installed) + + def tearDown(self): + # uninstall git commit-msg hook and assert output + output_uninstalled = gitlint("uninstall-hook", _cwd=self.tmp_git_repo) + expected_uninstalled = u"Successfully uninstalled gitlint commit-msg hook from %s/.git/hooks/commit-msg\n" % \ + self.tmp_git_repo + self.assertEqualStdout(output_uninstalled, expected_uninstalled) + + def _violations(self): + # Make a copy of the violations array so that we don't inadvertently edit it in the test (like I did :D) + return list(self.VIOLATIONS) + + # callback function that captures git commit-msg hook output + + def _interact(self, line, stdin): + self.githook_output.append(line) + # Answer 'yes' to question to keep violating commit-msg + if "Your commit message contains the above violations" in line: + response = self.responses[self.response_index] + stdin.put("{0}\n".format(response)) + self.response_index = (self.response_index + 1) % len(self.responses) + + def test_commit_hook_continue(self): + self.responses = ["y"] + test_filename = self.create_simple_commit(u"WIP: This ïs a title.\nContënt on the second line", + out=self._interact, tty_in=True) + + # Determine short commit-msg hash, needed to determine expected output + short_hash = self.get_last_commit_short_hash() + + expected_output = self._violations() + expected_output += ["Continue with commit anyways (this keeps the current commit message)? " + + "[y(es)/n(no)/e(dit)] " + + u"[master %s] WIP: This ïs a title. Contënt on the second line\n" + % short_hash, + " 1 file changed, 0 insertions(+), 0 deletions(-)\n", + u" create mode 100644 %s\n" % test_filename] + + assert len(self.githook_output) == len(expected_output) + for output, expected in zip(self.githook_output, expected_output): + self.assertMultiLineEqual( + output.replace('\r', ''), + expected.replace('\r', '')) + + def test_commit_hook_abort(self): + self.responses = ["n"] + test_filename = self.create_simple_commit(u"WIP: This ïs a title.\nContënt on the second line", + out=self._interact, ok_code=1, tty_in=True) + git("rm", "-f", test_filename, _cwd=self.tmp_git_repo) + + # Determine short commit-msg hash, needed to determine expected output + + expected_output = self._violations() + expected_output += ["Continue with commit anyways (this keeps the current commit message)? " + + "[y(es)/n(no)/e(dit)] " + + "Commit aborted.\n", + "Your commit message: \n", + "-----------------------------------------------\n", + u"WIP: This ïs a title.\n", + u"Contënt on the second line\n", + "-----------------------------------------------\n"] + + self.assertListEqual(expected_output, self.githook_output) + + def test_commit_hook_edit(self): + self.responses = ["e", "y"] + env = {"EDITOR": ":"} + test_filename = self.create_simple_commit(u"WIP: This ïs a title.\nContënt on the second line", + out=self._interact, env=env, tty_in=True) + git("rm", "-f", test_filename, _cwd=self.tmp_git_repo) + + short_hash = git("rev-parse", "--short", "HEAD", _cwd=self.tmp_git_repo, _tty_in=True).replace("\n", "") + + # Determine short commit-msg hash, needed to determine expected output + + expected_output = self._violations() + expected_output += ['Continue with commit anyways (this keeps the current commit message)? ' + + '[y(es)/n(no)/e(dit)] ' + self._violations()[0]] + expected_output += self._violations()[1:] + expected_output += ['Continue with commit anyways (this keeps the current commit message)? ' + + "[y(es)/n(no)/e(dit)] " + + u"[master %s] WIP: This ïs a title. Contënt on the second line\n" % short_hash, + " 1 file changed, 0 insertions(+), 0 deletions(-)\n", + u" create mode 100644 %s\n" % test_filename] + + assert len(self.githook_output) == len(expected_output) + for output, expected in zip(self.githook_output, expected_output): + self.assertMultiLineEqual( + output.replace('\r', ''), + expected.replace('\r', '')) + + def test_commit_hook_worktree(self): + """ Tests that hook installation and un-installation also work in git worktrees. + Test steps: + ```sh + git init <tmpdir> + cd <tmpdir> + git worktree add <worktree-tempdir> + cd <worktree-tempdir> + gitlint install-hook + gitlint uninstall-hook + ``` + """ + tmp_git_repo = self.create_tmp_git_repo() + self.create_simple_commit(u"Simple title\n\nContënt in the body", git_repo=tmp_git_repo) + + worktree_dir = self.generate_temp_path() + self.tmp_git_repos.append(worktree_dir) # make sure we clean up the worktree afterwards + + git("worktree", "add", worktree_dir, _cwd=tmp_git_repo, _tty_in=True) + + output_installed = gitlint("install-hook", _cwd=worktree_dir) + expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg") + expected_msg = "Successfully installed gitlint commit-msg hook in {0}\n".format(expected_hook_path) + self.assertEqual(output_installed, expected_msg) + + output_uninstalled = gitlint("uninstall-hook", _cwd=worktree_dir) + expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg") + expected_msg = "Successfully uninstalled gitlint commit-msg hook from {0}\n".format(expected_hook_path) + self.assertEqual(output_uninstalled, expected_msg) diff --git a/qa/test_stdin.py b/qa/test_stdin.py new file mode 100644 index 0000000..fff636f --- /dev/null +++ b/qa/test_stdin.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# pylint: disable=too-many-function-args,unexpected-keyword-arg +import io +import subprocess +from qa.shell import echo, gitlint +from qa.base import BaseTestCase +from qa.utils import ustr, DEFAULT_ENCODING + + +class StdInTests(BaseTestCase): + """ Integration tests for various STDIN scenarios for gitlint """ + + def test_stdin_pipe(self): + """ Test piping input into gitlint. + This is the equivalent of doing: + $ echo "foo" | gitlint + """ + # NOTE: There is no use in testing this with _tty_in=True, because if you pipe something into a command + # there never is a TTY connected to stdin (per definition). We're setting _tty_in=False here to be explicit + # but note that this is always true when piping something into a command. + output = gitlint(echo(u"WIP: Pïpe test."), + _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) + self.assertEqualStdout(output, self.get_expected("test_stdin/test_stdin_pipe_1")) + + def test_stdin_pipe_empty(self): + """ Test the scenario where no TTY is attached an nothing is piped into gitlint. This occurs in + CI runners like Jenkins and Gitlab, see https://github.com/jorisroovers/gitlint/issues/42 for details. + This is the equivalent of doing: + $ echo -n "" | gitlint + """ + commit_msg = u"WIP: This ïs a title.\nContent on the sëcond line" + self.create_simple_commit(commit_msg) + + # We need to set _err_to_out explicitly for sh to merge stdout and stderr output in case there's + # no TTY attached to STDIN + # http://amoffat.github.io/sh/sections/special_arguments.html?highlight=_tty_in#err-to-out + output = gitlint(echo("-n", ""), _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) + + self.assertEqual(ustr(output), self.get_expected("test_stdin/test_stdin_pipe_empty_1")) + + def test_stdin_file(self): + """ Test the scenario where STDIN is a regular file (stat.S_ISREG = True) + This is the equivalent of doing: + $ gitlint < myfile + """ + tmp_commit_msg_file = self.create_tmpfile(u"WIP: STDIN ïs a file test.") + + with io.open(tmp_commit_msg_file, encoding=DEFAULT_ENCODING) as file_handle: + + # We need to use subprocess.Popen() here instead of sh because when passing a file_handle to sh, it will + # deal with reading the file itself instead of passing it on to gitlint as a STDIN. Since we're trying to + # test for the condition where stat.S_ISREG == True that won't work for us here. + p = subprocess.Popen(u"gitlint", stdin=file_handle, cwd=self.tmp_git_repo, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + output, _ = p.communicate() + self.assertEqual(ustr(output), self.get_expected("test_stdin/test_stdin_file_1")) diff --git a/qa/test_user_defined.py b/qa/test_user_defined.py new file mode 100644 index 0000000..cf7effd --- /dev/null +++ b/qa/test_user_defined.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# pylint: disable=too-many-function-args,unexpected-keyword-arg +from qa.shell import gitlint +from qa.base import BaseTestCase + + +class UserDefinedRuleTests(BaseTestCase): + """ Integration tests for user-defined rules.""" + + def test_user_defined_rules_examples(self): + extra_path = self.get_example_path() + commit_msg = u"WIP: Thi$ is å title\nContent on the second line" + self.create_simple_commit(commit_msg) + output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) + self.assertEqualStdout(output, self.get_expected("test_user_defined/test_user_defined_rules_examples_1")) + + def test_user_defined_rules_examples_with_config(self): + extra_path = self.get_example_path() + commit_msg = u"WIP: Thi$ is å title\nContent on the second line" + self.create_simple_commit(commit_msg) + output = gitlint("--extra-path", extra_path, "-c", "body-max-line-count.max-line-count=1", + _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[6]) + expected_path = "test_user_defined/test_user_defined_rules_examples_with_config_1" + self.assertEqualStdout(output, self.get_expected(expected_path)) + + def test_user_defined_rules_extra(self): + extra_path = self.get_sample_path("user_rules/extra") + commit_msg = u"WIP: Thi$ is å title\nContent on the second line" + self.create_simple_commit(commit_msg) + output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) + self.assertEqualStdout(output, self.get_expected("test_user_defined/test_user_defined_rules_extra_1")) + + def test_invalid_user_defined_rules(self): + extra_path = self.get_sample_path("user_rules/incorrect_linerule") + self.create_simple_commit("WIP: test") + output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[255]) + self.assertEqualStdout(output, + "Config Error: User-defined rule class 'MyUserLineRule' must have a 'validate' method\n") diff --git a/qa/utils.py b/qa/utils.py new file mode 100644 index 0000000..eb9869a --- /dev/null +++ b/qa/utils.py @@ -0,0 +1,99 @@ +# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return +import platform +import sys +import os + +import locale + +######################################################################################################################## +# PLATFORM_IS_WINDOWS + + +def platform_is_windows(): + return "windows" in platform.system().lower() + + +PLATFORM_IS_WINDOWS = platform_is_windows() + +######################################################################################################################## +# USE_SH_LIB +# Determine whether to use the `sh` library +# On windows we won't want to use the sh library since it's not supported - instead we'll use our own shell module. +# However, we want to be able to overwrite this behavior for testing using the GITLINT_QA_USE_SH_LIB env var. + + +def use_sh_library(): + gitlint_use_sh_lib_env = os.environ.get('GITLINT_QA_USE_SH_LIB', None) + if gitlint_use_sh_lib_env: + return gitlint_use_sh_lib_env == "1" + return not PLATFORM_IS_WINDOWS + + +USE_SH_LIB = use_sh_library() + +######################################################################################################################## +# DEFAULT_ENCODING + + +def getpreferredencoding(): + """ Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars + on windows and falls back to UTF-8. """ + default_encoding = locale.getpreferredencoding() or "UTF-8" + + # On Windows, we mimic git/linux by trying to read the LC_ALL, LC_CTYPE, LANG env vars manually + # (on Linux/MacOS the `getpreferredencoding()` call will take care of this). + # We fallback to UTF-8 + if PLATFORM_IS_WINDOWS: + default_encoding = "UTF-8" + for env_var in ["LC_ALL", "LC_CTYPE", "LANG"]: + encoding = os.environ.get(env_var, False) + if encoding: + # Support dotted (C.UTF-8) and non-dotted (C or UTF-8) charsets: + # If encoding contains a dot: split and use second part, otherwise use everything + dot_index = encoding.find(".") + if dot_index != -1: + default_encoding = encoding[dot_index + 1:] + else: + default_encoding = encoding + break + + return default_encoding + + +DEFAULT_ENCODING = getpreferredencoding() + +######################################################################################################################## +# Unicode utility functions + + +def ustr(obj): + """ Python 2 and 3 utility method that converts an obj to unicode in python 2 and to a str object in python 3""" + if sys.version_info[0] == 2: + # If we are getting a string, then do an explicit decode + # else, just call the unicode method of the object + if type(obj) in [str, basestring]: # pragma: no cover # noqa + return unicode(obj, DEFAULT_ENCODING) # pragma: no cover # noqa + else: + return unicode(obj) # pragma: no cover # noqa + else: + if type(obj) in [bytes]: + return obj.decode(DEFAULT_ENCODING) + else: + return str(obj) + + +def sstr(obj): + """ Python 2 and 3 utility method that converts an obj to a DEFAULT_ENCODING encoded string in python 2 + and to unicode in python 3. + Especially useful for implementing __str__ methods in python 2: http://stackoverflow.com/a/1307210/381010""" + if sys.version_info[0] == 2: + # For lists in python2, remove unicode string representation characters. + # i.e. ensure lists are printed as ['a', 'b'] and not [u'a', u'b'] + if type(obj) in [list]: + return [sstr(item) for item in obj] # pragma: no cover # noqa + + return unicode(obj).encode(DEFAULT_ENCODING) # pragma: no cover # noqa + else: + return obj # pragma: no cover + +######################################################################################################################## |