Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix integration tests on windows #392

Merged
merged 12 commits into from
Dec 19, 2022
13 changes: 12 additions & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ jobs:
env:
GITLINT_USE_SH_LIB: 1

- name: Integration tests (GITLINT_QA_USE_SH_LIB=0)
run: |
hatch run qa:integration-tests --ignore qa/test_hooks.py qa
env:
GITLINT_QA_USE_SH_LIB: 0

- name: Build test (gitlint)
run: |
hatch build
Expand Down Expand Up @@ -125,11 +131,16 @@ jobs:
- name: Code linting (pylint)
run: hatch run test:lint

- name: Integration tests
run: |
hatch run qa:install-local
hatch run qa:integration-tests -k "not (HookTests or test_lint_staged_stdin or test_stdin_file or test_stdin_pipe_empty)" qa

- name: Build test (gitlint)
run: |
hatch build
hatch clean

- name: Build test (gitlint-core)
run: |
hatch build
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,18 @@ Run a set of integration tests against any gitlint binary (not just the one from
"""
detached = true
dependencies = [
"sh==1.14.3",
"pytest==7.2.0",
"arrow==1.2.3",
"sh==1.14.3; sys_platform != \"win32\"",
"pdbr==0.7.5; sys_platform != \"win32\"",
]

[tool.hatch.envs.qa.scripts]
# The integration tests can be ran against any gitlint binary, e.g. one installed from pypi (for post-release testing)
# This is why by default we don't install the local dev version of gitlint in the qa environment
# To run integration tests against the dev version of gitlint, use install-local first
install-local="pip install ./gitlint-core[trusted-deps]"
integration-tests = "pytest qa {args}"
install-local="pip install -e ./gitlint-core[trusted-deps]"
integration-tests = "pytest -rw -s {args:qa}"
i = "integration-tests"


Expand Down
14 changes: 8 additions & 6 deletions qa/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


from qa.shell import git, gitlint, RunningCommand
from qa.utils import DEFAULT_ENCODING
from qa.utils import DEFAULT_ENCODING, FILE_ENCODING, PLATFORM_IS_WINDOWS


class BaseTestCase(TestCase):
Expand All @@ -40,7 +40,8 @@ def tearDown(self):
for tmpfile in self.tmpfiles:
os.remove(tmpfile)
for repo in self.tmp_git_repos:
shutil.rmtree(repo)
# On windows we need to ignore errors because git might still be holding on to some files
shutil.rmtree(repo, ignore_errors=PLATFORM_IS_WINDOWS)

def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name
self.assertIsInstance(output, RunningCommand)
Expand Down Expand Up @@ -84,13 +85,13 @@ def create_file(parent_dir, content=None):
if isinstance(content, bytes):
open_kwargs = {"mode": "wb"}
else:
open_kwargs = {"mode": "w", "encoding": DEFAULT_ENCODING}
open_kwargs = {"mode": "w", "encoding": FILE_ENCODING}

with open(full_path, **open_kwargs) as f: # pylint: disable=unspecified-encoding
f.write(content)
else:
# pylint: disable=consider-using-with
open(full_path, "a", encoding=DEFAULT_ENCODING).close()
open(full_path, "a", encoding=FILE_ENCODING).close()

return test_filename

Expand Down Expand Up @@ -150,7 +151,7 @@ def create_tmpfile(self, content):
if isinstance(content, bytes):
open_kwargs = {"mode": "wb"}
else:
open_kwargs = {"mode": "w", "encoding": DEFAULT_ENCODING}
open_kwargs = {"mode": "w", "encoding": FILE_ENCODING}

with open(tmpfile, **open_kwargs) as f: # pylint: disable=unspecified-encoding
f.write(content)
Expand Down Expand Up @@ -181,7 +182,8 @@ def get_expected(filename="", variable_dict=None):
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)
with open(expected_path, encoding=DEFAULT_ENCODING) as file:
# Expected files are UTF-8 encoded (not dependent on the system's default encoding)
with open(expected_path, encoding=FILE_ENCODING) as file:
expected = file.read()

if variable_dict:
Expand Down
2 changes: 1 addition & 1 deletion qa/expected/test_commits/test_lint_staged_msg_filename_1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DEBUG: gitlint.git ('--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 DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli DEFAULT_ENCODING: UTF-8
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
Expand Down
2 changes: 1 addition & 1 deletion qa/expected/test_config/test_config_from_env_1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DEBUG: gitlint.git ('--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 DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli DEFAULT_ENCODING: UTF-8
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
Expand Down
2 changes: 1 addition & 1 deletion qa/expected/test_config/test_config_from_env_2
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DEBUG: gitlint.git ('--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 DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli DEFAULT_ENCODING: UTF-8
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
Expand Down
2 changes: 1 addition & 1 deletion qa/expected/test_config/test_config_from_file_debug_1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DEBUG: gitlint.git ('--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 DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli DEFAULT_ENCODING: UTF-8
DEBUG: gitlint.cli Configuration
config-path: {config_path}
[GENERAL]
Expand Down
2 changes: 1 addition & 1 deletion qa/expected/test_gitlint/test_commit_binary_file_1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DEBUG: gitlint.git ('--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 DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli DEFAULT_ENCODING: UTF-8
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
Expand Down
61 changes: 44 additions & 17 deletions qa/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,32 @@ 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 + stderr.decode(DEFAULT_ENCODING)
self.stderr = stderr
self._stdout = stdout + stderr
self._stderr = stderr
self.exit_code = exitcode

def __str__(self):
return self.stdout.decode(DEFAULT_ENCODING)

def __unicode__(self):
return self.stdout

@property
def stdout(self):
return self._stdout

@property
def stderr(self):
return self._stderr

def __getattr__(self, p): # pylint: disable=invalid-name
# https://github.com/amoffat/sh/blob/e0ed8e244e9d973ef4e0749b2b3c2695e7b5255b/sh.py#L952=
_unicode_methods = set(dir(str()))
if p in _unicode_methods:
return getattr(str(self), p)

raise AttributeError

class ErrorReturnCode(ShResult, Exception):
"""ShResult subclass for unexpected results (acts as an exception)."""

Expand All @@ -52,30 +71,38 @@ def 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 str(result)
return _exec(*args, **kwargs)

def _exec(*args, **kwargs):
pipe = subprocess.PIPE
popen_kwargs = {"stdout": pipe, "stderr": pipe, "shell": kwargs.get("_tty_out", False)}
if "_cwd" in kwargs:
popen_kwargs["cwd"] = kwargs["_cwd"]
if "_env" in kwargs:
popen_kwargs["env"] = kwargs["_env"]
popen_kwargs = {
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
"stdin": subprocess.PIPE,
"shell": kwargs.get("_tty_out", False),
"cwd": kwargs.get("_cwd", None),
"env": kwargs.get("_env", None),
}

stdin_input = None
if len(args) > 1 and isinstance(args[1], ShResult):
stdin_input = args[1].stdout
# pop args[1] from the array and use it as stdin
args = list(args)
args.pop(1)
popen_kwargs["stdin"] = subprocess.PIPE

try:
with subprocess.Popen(args, **popen_kwargs) as p:
result = p.communicate()
if stdin_input:
result = p.communicate(stdin_input)
else:
result = p.communicate()

except FileNotFoundError as exc:
raise CommandNotFound from exc

exit_code = p.returncode
stdout = result[0].decode(DEFAULT_ENCODING)
stdout = result[0]
stderr = result[1] # 'sh' does not decode the stderr bytes to unicode
full_cmd = "" if args is None else " ".join(args)

Expand Down
2 changes: 1 addition & 1 deletion qa/test_commits.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def test_lint_single_commit(self):
self.assertEqual(output.exit_code, 254)

def test_lint_staged_stdin(self):
"""Tests linting a staged commit. Gitint should lint the passed commit message andfetch additional meta-data
"""Tests linting a staged commit. Gitint should lint the passed commit message and fetch 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
Expand Down
7 changes: 3 additions & 4 deletions qa/test_config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# pylint: disable=too-many-function-args,unexpected-keyword-arg

import os
import re

from qa.shell import gitlint
from qa.base import BaseTestCase
from qa.utils import DEFAULT_ENCODING


class ConfigTests(BaseTestCase):
Expand Down Expand Up @@ -69,7 +68,7 @@ def test_config_from_file_debug(self):
"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")
config_path = self.get_sample_path(os.path.join("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)
Expand Down Expand Up @@ -128,7 +127,7 @@ def test_config_from_env(self):
# Extract date from actual output to insert it into the expected output
# We have to do this since there's no way for us to deterministically know that date otherwise
p = re.compile("Date: (.*)\n", re.UNICODE | re.MULTILINE)
result = p.search(output.stdout.decode(DEFAULT_ENCODING))
result = p.search(str(output))
date = result.group(1).strip()
expected_kwargs.update({"date": date})

Expand Down
2 changes: 1 addition & 1 deletion qa/test_gitlint.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def test_commit_binary_file(self):
binary_filename = self.create_simple_commit("Sïmple commit", file_contents=bytes([0x48, 0x00, 0x49, 0x00]))
output = gitlint(
"--debug",
_ok_code=1,
_ok_code=[1],
_cwd=self.tmp_git_repo,
)

Expand Down
6 changes: 3 additions & 3 deletions qa/test_stdin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import subprocess
from qa.shell import echo, gitlint
from qa.base import BaseTestCase
from qa.utils import DEFAULT_ENCODING
from qa.utils import FILE_ENCODING, DEFAULT_ENCODING


class StdInTests(BaseTestCase):
Expand Down Expand Up @@ -33,7 +33,7 @@ def test_stdin_pipe_empty(self):
# 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(output, self.get_expected("test_stdin/test_stdin_pipe_empty_1"))
self.assertEqualStdout(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)
Expand All @@ -42,7 +42,7 @@ def test_stdin_file(self):
"""
tmp_commit_msg_file = self.create_tmpfile("WIP: STDIN ïs a file test.")

with open(tmp_commit_msg_file, encoding=DEFAULT_ENCODING) as file_handle:
with open(tmp_commit_msg_file, encoding=FILE_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.
Expand Down
31 changes: 9 additions & 22 deletions qa/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,15 @@ def use_sh_library():


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
"""Use local.getpreferredencoding() or fallback to UTF-8."""
return locale.getpreferredencoding() or "UTF-8"


DEFAULT_ENCODING = getpreferredencoding()


########################################################################################################################
# FILE_ENCODING

# Encoding for reading/writing files within the tests, this is always UTF-8
FILE_ENCODING = "UTF-8"