1
0
Fork 0

Adding upstream version 0.13.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-13 05:54:40 +01:00
parent 1805ece79d
commit d8f166e6bb
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
167 changed files with 15302 additions and 0 deletions

0
qa/__init__.py Normal file
View file

178
qa/base.py Normal file
View file

@ -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

View file

@ -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."

View file

@ -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."

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

4
qa/requirements.txt Normal file
View file

@ -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

View file

View file

@ -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

View file

@ -0,0 +1,7 @@
[ignore-by-title]
regex=^Release(.*)
ignore=T5,T3
[ignore-by-body]
regex=(.*)relëase(.*)
ignore=T3,B3

View file

@ -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

View file

@ -0,0 +1,8 @@
from gitlint.rules import LineRule
class MyUserLineRule(LineRule):
id = "UC2"
name = "my-line-rule"
# missing validate method, missing target attribute

90
qa/shell.py Normal file
View file

@ -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)

161
qa/test_commits.py Normal file
View file

@ -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))

67
qa/test_config.py Normal file
View file

@ -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))

26
qa/test_contrib.py Normal file
View file

@ -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")

171
qa/test_gitlint.py Normal file
View file

@ -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)

153
qa/test_hooks.py Normal file
View file

@ -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)

56
qa/test_stdin.py Normal file
View file

@ -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"))

38
qa/test_user_defined.py Normal file
View file

@ -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")

99
qa/utils.py Normal file
View file

@ -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
########################################################################################################################