diff options
-rw-r--r-- | Vagrantfile | 13 | ||||
-rw-r--r-- | gitlint/cli.py | 59 | ||||
-rw-r--r-- | gitlint/tests/config/test_config_precedence.py | 22 | ||||
-rw-r--r-- | gitlint/tests/test_cli.py | 44 | ||||
-rw-r--r-- | qa/base.py | 39 | ||||
-rw-r--r-- | qa/expected/debug_output1 | 1 | ||||
-rw-r--r-- | qa/test_commits.py | 15 | ||||
-rw-r--r-- | qa/test_config.py | 16 | ||||
-rw-r--r-- | qa/test_gitlint.py | 64 | ||||
-rw-r--r-- | qa/test_stdin.py | 63 | ||||
-rw-r--r-- | qa/test_user_defined.py | 6 |
11 files changed, 238 insertions, 104 deletions
diff --git a/Vagrantfile b/Vagrantfile index e550167..0939a09 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -19,14 +19,25 @@ grep 'cd /vagrant' /home/vagrant/.bashrc || echo 'cd /vagrant' >> /home/vagrant/ grep 'source .venv27/bin/activate' /home/vagrant/.bashrc || echo 'source .venv27/bin/activate' >> /home/vagrant/.bashrc EOF +INSTALL_JENKINS=<<EOF +wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add - +sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list' +sudo apt-get update +sudo apt-get install -y jenkins +EOF + Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = "ubuntu/xenial64" config.vm.define "dev" do |dev| - dev.vm.provision "shell", inline: "#{INSTALL_DEPS}" + dev.vm.provision "gitlint", type: "shell", inline: "#{INSTALL_DEPS}" + # Use 'vagrant provision --provision-with jenkins' to only run jenkins install + dev.vm.provision "jenkins", type: "shell", inline: "#{INSTALL_JENKINS}" end + config.vm.network "forwarded_port", guest: 8080, host: 9080 + if Vagrant.has_plugin?("vagrant-cachier") config.cache.scope = :box end diff --git a/gitlint/cli.py b/gitlint/cli.py index 8f57090..1d22bf2 100644 --- a/gitlint/cli.py +++ b/gitlint/cli.py @@ -4,9 +4,8 @@ import copy import logging import os import platform -import select +import stat import sys - import click # Error codes @@ -94,17 +93,36 @@ def build_config(ctx, target, config_path, c, extra_path, ignore, verbose, silen ctx.exit(CONFIG_ERROR_CODE) # return CONFIG_ERROR_CODE on config error -def stdin_has_data(): - """ Helper function that indicates whether the stdin has data incoming or not """ - # This code was taken from: - # https://stackoverflow.com/questions/3762881/how-do-i-check-if-stdin-has-some-data - - # Caveat, this probably doesn't work on Windows because the code is dependent on the unix SELECT syscall. - # Details: https://docs.python.org/2/library/select.html#select.select - # This isn't a real problem now, because gitlint as a whole doesn't support Windows (see #20). - # If we ever do, we probably want to fall back to the old detection mechanism of reading from the local git repo - # in case there's no TTY connected to STDIN. - return select.select([sys.stdin, ], [], [], 0.0)[0] +def get_stdin_data(): + """ Helper function that returns data send to stdin or False if nothing is send """ + # STDIN can only be 3 different types of things ("modes") + # 1. An interactive terminal device (i.e. a TTY -> sys.stdin.isatty() or stat.S_ISCHR) + # 2. A (named) pipe (stat.S_ISFIFO) + # 3. A regular file (stat.S_ISREG) + # Technically, STDIN can also be other device type like a named unix socket (stat.S_ISSOCK), but we don't + # support that in gitlint (at least not today). + # + # Now, the behavior that we want is the following: + # If someone sends something directly to gitlint via a pipe or a regular file, read it. If not, read from the + # local repository. + # Note that we don't care about whether STDIN is a TTY or not, we only care whether data is via a pipe or regular + # file. + # However, in case STDIN is not a TTY, it HAS to be one of the 2 other things (pipe or regular file), even if + # no-one is actually sending anything to gitlint over them. In this case, we still want to read from the local + # repository. + # To support this use-case (which is common in CI runners such as Jenkins and Gitlab), we need to actually attempt + # to read from STDIN in case it's a pipe or regular file. In case that fails, then we'll fall back to reading + # from the local repo. + + mode = os.fstat(sys.stdin.fileno()).st_mode + stdin_is_pipe_or_file = stat.S_ISFIFO(mode) or stat.S_ISREG(mode) + if stdin_is_pipe_or_file: + input_data = sys.stdin.read() + # Only return the input data if there's actually something passed + # i.e. don't consider empty piped data + if len(input_data) != 0: + return ustr(input_data) + return False @click.group(invoke_without_command=True, epilog="When no COMMAND is specified, gitlint defaults to 'gitlint lint'.") @@ -162,13 +180,20 @@ def lint(ctx): lint_config = ctx.obj[0] msg_filename = ctx.obj[3] - # If we get data via stdin, then let's consider that our commit message, otherwise parse it from the local git repo. + # Let's determine where our input data is coming from: + # Order of precedence: + # 1. Any data specified via --msg-filename + # 2. Any data sent to stdin + # 3. Fallback to reading from local repository + stdin_input = get_stdin_data() if msg_filename: + LOG.debug("Attempting to read from --msg-filename.") gitcontext = GitContext.from_commit_msg(ustr(msg_filename.read())) - elif stdin_has_data(): - stdin_str = ustr(sys.stdin.read()) - gitcontext = GitContext.from_commit_msg(stdin_str) + elif stdin_input: + LOG.debug("No --msg-filename flag. Attempting to read from stdin.") + gitcontext = GitContext.from_commit_msg(stdin_input) else: + LOG.debug("No --msg-filename flag, no or empty data passed to stdin. Attempting to read from the local repo.") gitcontext = GitContext.from_local_repository(lint_config.target, ctx.obj[2]) number_of_commits = len(gitcontext.commits) diff --git a/gitlint/tests/config/test_config_precedence.py b/gitlint/tests/config/test_config_precedence.py index 49a24ad..9689e55 100644 --- a/gitlint/tests/config/test_config_precedence.py +++ b/gitlint/tests/config/test_config_precedence.py @@ -25,7 +25,7 @@ class LintConfigPrecedenceTests(BaseTestCase): def setUp(self): self.cli = CliRunner() - @patch('gitlint.cli.stdin_has_data', return_value=True) + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP\n\nThis is å test message\n") def test_config_precedence(self, _): # TODO(jroovers): this test really only test verbosity, we need to do some refactoring to gitlint.cli # to more easily test everything @@ -34,40 +34,37 @@ class LintConfigPrecedenceTests(BaseTestCase): # 2. commandline -c flags # 3. config file # 4. default config - input_text = u"WIP\n\nThis is å test message\n" config_path = self.get_sample_path("config/gitlintconfig") # 1. commandline convenience flags with patch('gitlint.display.stderr', new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, ["-vvv", "-c", "general.verbosity=2", "--config", config_path], - input=input_text) + result = self.cli.invoke(cli.cli, ["-vvv", "-c", "general.verbosity=2", "--config", config_path]) self.assertEqual(result.output, "") self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP\"\n") # 2. commandline -c flags with patch('gitlint.display.stderr', new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path], input=input_text) + result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path]) self.assertEqual(result.output, "") self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive)\n") # 3. config file with patch('gitlint.display.stderr', new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, ["--config", config_path], input=input_text) + result = self.cli.invoke(cli.cli, ["--config", config_path]) self.assertEqual(result.output, "") self.assertEqual(stderr.getvalue(), "1: T5\n") # 4. default config with patch('gitlint.display.stderr', new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, input=input_text) + result = self.cli.invoke(cli.cli) self.assertEqual(result.output, "") self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP\"\n") - @patch('gitlint.cli.stdin_has_data', return_value=True) - def test_ignore_precedence(self, _): + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP: This is å test") + def test_ignore_precedence(self, get_stdin_data): with patch('gitlint.display.stderr', new=StringIO()) as stderr: # --ignore takes precedence over -c general.ignore - result = self.cli.invoke(cli.cli, ["-c", "general.ignore=T5", "--ignore", "B6"], - input=u"WIP: This is å test") + result = self.cli.invoke(cli.cli, ["-c", "general.ignore=T5", "--ignore", "B6"]) self.assertEqual(result.output, "") self.assertEqual(result.exit_code, 1) # We still expect the T5 violation, but no B6 violation as --ignore overwrites -c general.ignore @@ -76,10 +73,11 @@ class LintConfigPrecedenceTests(BaseTestCase): # test that we can also still configure a rule that is first ignored but then not with patch('gitlint.display.stderr', new=StringIO()) as stderr: + get_stdin_data.return_value = u"This is å test" # --ignore takes precedence over -c general.ignore result = self.cli.invoke(cli.cli, ["-c", "general.ignore=title-max-length", "-c", "title-max-length.line-length=5", - "--ignore", "B6"], input=u"This is å test") + "--ignore", "B6"]) self.assertEqual(result.output, "") self.assertEqual(result.exit_code, 1) diff --git a/gitlint/tests/test_cli.py b/gitlint/tests/test_cli.py index 2f2ffd6..0bef488 100644 --- a/gitlint/tests/test_cli.py +++ b/gitlint/tests/test_cli.py @@ -63,7 +63,7 @@ class CLITests(BaseTestCase): result = self.cli.invoke(cli.cli, ["--version"]) self.assertEqual(result.output.split("\n")[0], "cli, version {0}".format(__version__)) - @patch('gitlint.cli.stdin_has_data', return_value=False) + @patch('gitlint.cli.get_stdin_data', return_value=False) @patch('gitlint.git.sh') def test_lint(self, sh, _): """ Test for basic simple linting functionality """ @@ -77,7 +77,7 @@ class CLITests(BaseTestCase): self.assertEqual(stderr.getvalue(), u'3: B5 Body message is too short (11<20): "commït-body"\n') self.assertEqual(result.exit_code, 1) - @patch('gitlint.cli.stdin_has_data', return_value=False) + @patch('gitlint.cli.get_stdin_data', return_value=False) @patch('gitlint.git.sh') def test_lint_multiple_commits(self, sh, _): """ Test for --commits option """ @@ -107,7 +107,7 @@ class CLITests(BaseTestCase): self.assertEqual(stderr.getvalue(), expected) self.assertEqual(result.exit_code, 3) - @patch('gitlint.cli.stdin_has_data', return_value=False) + @patch('gitlint.cli.get_stdin_data', return_value=False) @patch('gitlint.git.sh') def test_lint_multiple_commits_config(self, sh, _): """ Test for --commits option where some of the commits have gitlint config in the commit message """ @@ -138,7 +138,7 @@ class CLITests(BaseTestCase): self.assertEqual(stderr.getvalue(), expected) self.assertEqual(result.exit_code, 3) - @patch('gitlint.cli.stdin_has_data', return_value=False) + @patch('gitlint.cli.get_stdin_data', return_value=False) @patch('gitlint.git.sh') def test_lint_multiple_commits_configuration_rules(self, sh, _): """ Test for --commits option where where we have configured gitlint to ignore certain rules for certain commits @@ -175,7 +175,7 @@ class CLITests(BaseTestCase): self.assertEqual(stderr.getvalue(), expected) self.assertEqual(result.exit_code, 2) - @patch('gitlint.cli.stdin_has_data', return_value=True) + @patch('gitlint.cli.get_stdin_data', return_value=u'WIP: tïtle \n') def test_input_stream(self, _): """ Test for linting when a message is passed via stdin """ expected_output = u"1: T2 Title has trailing whitespace: \"WIP: tïtle \"\n" + \ @@ -183,12 +183,13 @@ class CLITests(BaseTestCase): u"3: B6 Body message is missing\n" with patch('gitlint.display.stderr', new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, input=u'WIP: tïtle \n') + result = self.cli.invoke(cli.cli) self.assertEqual(stderr.getvalue(), expected_output) self.assertEqual(result.exit_code, 3) self.assertEqual(result.output, "") - def test_from_filename(self): + @patch('gitlint.cli.get_stdin_data', return_value=False) + def test_msg_filename(self, _): expected_output = u"3: B6 Body message is missing\n" with tempdir() as tmpdir: @@ -197,28 +198,27 @@ class CLITests(BaseTestCase): f.write("Commït title\n") with patch('gitlint.display.stderr', new=StringIO()) as stderr: - args = ["--msg-filename", msg_filename] - result = self.cli.invoke(cli.cli, args) + result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename]) self.assertEqual(stderr.getvalue(), expected_output) self.assertEqual(result.exit_code, 1) self.assertEqual(result.output, "") - @patch('gitlint.cli.stdin_has_data', return_value=True) + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP: tïtle \n") def test_silent_mode(self, _): """ Test for --silent option """ with patch('gitlint.display.stderr', new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, ["--silent"], input=u"WIP: tïtle \n") + result = self.cli.invoke(cli.cli, ["--silent"]) self.assertEqual(stderr.getvalue(), "") self.assertEqual(result.exit_code, 3) self.assertEqual(result.output, "") - @patch('gitlint.cli.stdin_has_data', return_value=True) + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP: tïtle \n") def test_verbosity(self, _): """ Test for --verbosity option """ # We only test -v and -vv, more testing is really not required here # -v with patch('gitlint.display.stderr', new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, ["-v"], input=u"WIP: tïtle \n") + result = self.cli.invoke(cli.cli, ["-v"]) self.assertEqual(stderr.getvalue(), "1: T2\n1: T5\n3: B6\n") self.assertEqual(result.exit_code, 3) self.assertEqual(result.output, "") @@ -241,7 +241,7 @@ class CLITests(BaseTestCase): self.assertEqual(result.exit_code, CLITests.CONFIG_ERROR_CODE) self.assertEqual(result.output, "Config Error: Option 'verbosity' must be set between 0 and 3\n") - @patch('gitlint.cli.stdin_has_data', return_value=False) + @patch('gitlint.cli.get_stdin_data', return_value=False) @patch('gitlint.git.sh') def test_debug(self, sh, _): """ Test for --debug option """ @@ -278,6 +278,8 @@ class CLITests(BaseTestCase): u"DEBUG: gitlint.cli Gitlint version: {0}".format(__version__), self.get_expected('debug_configuration_output1', {'config_path': config_path, 'target': os.path.abspath(os.getcwd())}), + u"DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. " + + u"Attempting to read from the local repo.", u"DEBUG: gitlint.lint Linting commit 6f29bf81a8322a04071bb794666e48c443a90360", u"DEBUG: gitlint.lint Commit Object\nAuthor: test åuthor1 <test-email1@föo.com>\n" + u"Date: 2016-12-03 15:28:15+01:00\ncommït-title1\n\ncommït-body1", @@ -291,13 +293,13 @@ class CLITests(BaseTestCase): self.assert_logged(expected_logs) - @patch('gitlint.cli.stdin_has_data', return_value=True) + @patch('gitlint.cli.get_stdin_data', return_value=u"Test tïtle\n") def test_extra_path(self, _): """ Test for --extra-path flag """ # Test extra-path pointing to a directory with patch('gitlint.display.stderr', new=StringIO()) as stderr: extra_path = self.get_sample_path("user_rules") - result = self.cli.invoke(cli.cli, ["--extra-path", extra_path, "--debug"], input=u"Test tïtle\n") + result = self.cli.invoke(cli.cli, ["--extra-path", extra_path, "--debug"]) expected_output = u"1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \ "3: B6 Body message is missing\n" self.assertEqual(stderr.getvalue(), expected_output) @@ -312,12 +314,12 @@ class CLITests(BaseTestCase): self.assertEqual(stderr.getvalue(), expected_output) self.assertEqual(result.exit_code, 2) - @patch('gitlint.cli.stdin_has_data', return_value=True) + @patch('gitlint.cli.get_stdin_data', return_value=u"WIP: tëst") def test_config_file(self, _): """ Test for --config option """ with patch('gitlint.display.stderr', new=StringIO()) as stderr: config_path = self.get_sample_path("config/gitlintconfig") - result = self.cli.invoke(cli.cli, ["--config", config_path], input=u"WIP: tëst") + result = self.cli.invoke(cli.cli, ["--config", config_path]) self.assertEqual(result.output, "") self.assertEqual(stderr.getvalue(), "1: T5\n3: B6\n") self.assertEqual(result.exit_code, 2) @@ -345,7 +347,7 @@ class CLITests(BaseTestCase): result = self.cli.invoke(cli.cli, ["--config", config_path]) self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE) - @patch('gitlint.cli.stdin_has_data', return_value=False) + @patch('gitlint.cli.get_stdin_data', return_value=False) def test_target(self, _): """ Test for the --target option """ os.environ["LANGUAGE"] = "C" @@ -399,7 +401,7 @@ class CLITests(BaseTestCase): "Error: File \"{0}\" already exists.\n".format(sample_path) self.assertEqual(result.output, expected_msg) - @patch('gitlint.cli.stdin_has_data', return_value=False) + @patch('gitlint.cli.get_stdin_data', return_value=False) @patch('gitlint.git.sh') def test_git_error(self, sh, _): """ Tests that the cli handles git errors properly """ @@ -407,7 +409,7 @@ class CLITests(BaseTestCase): result = self.cli.invoke(cli.cli) self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE) - @patch('gitlint.cli.stdin_has_data', return_value=False) + @patch('gitlint.cli.get_stdin_data', return_value=False) @patch('gitlint.git.sh') def test_no_commits_in_range(self, sh, _): """ Test for --commits with the specified range being empty. """ @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- +# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable import os import sys +import tempfile from datetime import datetime from uuid import uuid4 @@ -9,6 +11,19 @@ from unittest2 import TestCase from sh import git, rm, touch, DEFAULT_ENCODING # pylint: disable=no-name-in-module +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 # pylint: disable=unidiomatic-typecheck + return unicode(obj, DEFAULT_ENCODING) # pragma: no cover # noqa + else: + return unicode(obj) # pragma: no cover # noqa + else: + return str(obj) + + class BaseTestCase(TestCase): """ Base class of which all gitlint integration test classes are derived. Provides a number of convenience methods. """ @@ -17,6 +32,13 @@ class BaseTestCase(TestCase): maxDiff = None tmp_git_repo = None + def setUp(self): + self.tmpfiles = [] + + def tearDown(self): + for tmpfile in self.tmpfiles: + os.remove(tmpfile) + @classmethod def setUpClass(cls): """ Sets up the integration tests by creating a new temporary git repository """ @@ -73,6 +95,15 @@ class BaseTestCase(TestCase): _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 os.fdopen(tmpfile, "w") 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") @@ -104,3 +135,11 @@ class BaseTestCase(TestCase): if variable_dict: expected = expected.format(**variable_dict) return expected + + @staticmethod + def mock_stdin(): + """ Convenience method to create a Mock stdin object to deal with https://github.com/amoffat/sh/issues/427 """ + class MockInput(object): + def read(self, _size): # pylint: disable=no-self-use + return + return MockInput() diff --git a/qa/expected/debug_output1 b/qa/expected/debug_output1 index 93d7ff7..3ac7bff 100644 --- a/qa/expected/debug_output1 +++ b/qa/expected/debug_output1 @@ -44,6 +44,7 @@ target: {target} M1: author-valid-email regex=[^@ ]+@[^@ ]+\.[^@ ]+ +DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Attempting to read from the local repo. DEBUG: gitlint.lint Linting commit {commit_sha} DEBUG: gitlint.lint Commit Object Author: gitlint-test-user <gitlint@test.com> diff --git a/qa/test_commits.py b/qa/test_commits.py index ddff660..4620121 100644 --- a/qa/test_commits.py +++ b/qa/test_commits.py @@ -16,7 +16,7 @@ class CommitsTests(BaseTestCase): 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, _err_to_out=True) + _cwd=self.tmp_git_repo, _tty_in=True) self.assertEqual(output, "") def test_violations(self): @@ -30,7 +30,7 @@ class CommitsTests(BaseTestCase): 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, _err_to_out=True, _ok_code=[4]) + _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) expected = (u"Commit {0}:\n".format(commit_sha2) + u"1: T3 Title has trailing punctuation (.): \"Sïmple title3.\"\n" + u"3: B6 Body message is missing\n" @@ -48,7 +48,7 @@ class CommitsTests(BaseTestCase): 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, _err_to_out=True, _ok_code=[2]) + 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) @@ -60,8 +60,8 @@ class CommitsTests(BaseTestCase): 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, _err_to_out=True, _ok_code=[3]) - revlist = git("rev-list", "HEAD", _err_to_out=True, _cwd=tmp_git_repo).split() + 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 = ( u"Commit {0}:\n".format(revlist[0][:10]) + @@ -86,11 +86,10 @@ class CommitsTests(BaseTestCase): 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", _err_to_out=True, _cwd=tmp_git_repo).split() + 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=False, _err_to_out=True, _ok_code=[4]) + output = gitlint("--commits", "HEAD", "--config", config_path, _cwd=tmp_git_repo, _tty_in=True, _ok_code=[4]) expected = ( u"Commit {0}:\n".format(revlist[0][:10]) + diff --git a/qa/test_config.py b/qa/test_config.py index 3120934..b7cb733 100644 --- a/qa/test_config.py +++ b/qa/test_config.py @@ -14,30 +14,30 @@ class ConfigTests(BaseTestCase): 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", _cwd=self.tmp_git_repo, _err_to_out=True, _ok_code=[1]) + 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.assertEqual(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, _err_to_out=True, _ok_code=[1]) + _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.assertEqual(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, _err_to_out=True, _ok_code=[3]) + output = gitlint("-v", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) expected = u"1: T3\n1: T5\n2: B4\n" self.assertEqual(output, expected) - output = gitlint("-vv", _cwd=self.tmp_git_repo, _err_to_out=True, _ok_code=[3]) + output = gitlint("-vv", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) expected = u"1: T3 Title has trailing punctuation (.)\n" + \ u"1: T5 Title contains the word 'WIP' (case-insensitive)\n" + \ u"2: B4 Second line is not empty\n" self.assertEqual(output, expected) - output = gitlint("-vvv", _cwd=self.tmp_git_repo, _err_to_out=True, _ok_code=[3]) + output = gitlint("-vvv", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) expected = u"1: T3 Title has trailing punctuation (.): \"WIP: Thïs is a title.\"\n" + \ u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: Thïs is a title.\"\n" + \ u"2: B4 Second line is not empty: \"Contënt on the second line\"\n" @@ -49,7 +49,7 @@ class ConfigTests(BaseTestCase): def test_set_rule_option(self): self._create_simple_commit(u"This ïs a title.") - output = gitlint("-c", "title-max-length.line-length=5", _cwd=self.tmp_git_repo, _err_to_out=True, _ok_code=[3]) + output = gitlint("-c", "title-max-length.line-length=5", _tty_in=True, _cwd=self.tmp_git_repo, _ok_code=[3]) expected = u"1: T1 Title exceeds max length (16>5): \"This ïs a title.\"\n" + \ u"1: T3 Title has trailing punctuation (.): \"This ïs a title.\"\n" + \ "3: B6 Body message is missing\n" @@ -60,7 +60,7 @@ class ConfigTests(BaseTestCase): "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, _err_to_out=True, _ok_code=[5]) + output = gitlint("--config", config_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) expected = "1: T1 Title exceeds max length (42>20)\n" + \ "1: T5 Title contains the word 'WIP' (case-insensitive)\n" + \ @@ -76,7 +76,7 @@ class ConfigTests(BaseTestCase): self._create_simple_commit(commit_msg) commit_sha = self.get_last_commit_hash() config_path = self.get_sample_path("config/gitlintconfig") - output = gitlint("--config", config_path, "--debug", _cwd=self.tmp_git_repo, _err_to_out=True, _ok_code=[5]) + output = gitlint("--config", config_path, "--debug", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) expected_date = git("log", "-1", "--pretty=%ai", _cwd=self.tmp_git_repo) expected_date = arrow.get(str(expected_date), "YYYY-MM-DD HH:mm:ss Z").datetime diff --git a/qa/test_gitlint.py b/qa/test_gitlint.py index d6a80ae..f61b281 100644 --- a/qa/test_gitlint.py +++ b/qa/test_gitlint.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from datetime import datetime import os -from sh import git, gitlint, echo # pylint: disable=no-name-in-module +from sh import git, gitlint # pylint: disable=no-name-in-module from qa.base import BaseTestCase @@ -10,10 +9,9 @@ class IntegrationTests(BaseTestCase): def test_successful(self): # Test for STDIN with and without a TTY attached - for has_tty in [True, False]: - self._create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit") - output = gitlint(_cwd=self.tmp_git_repo, _tty_in=has_tty, _err_to_out=True) - self.assertEqual(output, "") + 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.assertEqual(output, "") def test_successful_merge_commit(self): # Create branch on master @@ -105,40 +103,38 @@ class IntegrationTests(BaseTestCase): self.assertEqual(output, expected) def test_violations(self): - # Test for STDIN with and without a TTY attached - for has_tty in [True, False]: - 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(_cwd=self.tmp_git_repo, _tty_in=has_tty, _err_to_out=True, _ok_code=[3]) - - expected = 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: \"Content on the sëcond line\"\n" - self.assertEqual(output, expected) - - def test_pipe_input(self): - # 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). - output = gitlint(echo(u"WIP: Pïpe test."), _tty_in=False, _err_to_out=True, _ok_code=[3]) - - expected = u"1: T3 Title has trailing punctuation (.): \"WIP: Pïpe test.\"\n" + \ - u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: Pïpe test.\"\n" + \ - u"3: B6 Body message is missing\n" + 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]) + expected = 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: \"Content on the sëcond line\"\n" self.assertEqual(output, expected) - def test_from_file(self): - tmp_commit_msg_file = os.path.realpath("/tmp/commitmsg-%s" % datetime.now().strftime("%Y%m%d-%H%M%S")) - with open(tmp_commit_msg_file, "w") as f: - f.write("WIP: msg-fïlename test.") + def test_msg_filename(self): + tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename test.") - output = gitlint("--msg-filename", tmp_commit_msg_file, _tty_in=False, _err_to_out=True, _ok_code=[3]) + output = gitlint("--msg-filename", tmp_commit_msg_file, _tty_in=True, _ok_code=[3]) expected = u"1: T3 Title has trailing punctuation (.): \"WIP: msg-fïlename test.\"\n" + \ - u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: msg-fïlename test.\"\n" + \ + u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: msg-fïlename test.\"\n" + \ + u"3: B6 Body message is missing\n" + + self.assertEqual(output, expected) + + 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("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 + output = gitlint("--msg-filename", tmp_commit_msg_file, _in=self.mock_stdin(), + _tty_in=False, _err_to_out=True, _ok_code=[3]) + + expected = u"1: T3 Title has trailing punctuation (.): \"WIP: msg-fïlename NO TTY test.\"\n" + \ + u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: msg-fïlename NO TTY test.\"\n" + \ u"3: B6 Body message is missing\n" self.assertEqual(output, expected) diff --git a/qa/test_stdin.py b/qa/test_stdin.py new file mode 100644 index 0000000..317b53c --- /dev/null +++ b/qa/test_stdin.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +import subprocess +from sh import gitlint, echo # pylint: disable=no-name-in-module +from qa.base import BaseTestCase, ustr + + +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: + |