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

WIP: remove dependency on sh for integration tests #394

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:

- name: Integration tests (GITLINT_QA_USE_SH_LIB=0)
run: |
hatch run qa:integration-tests --ignore qa/test_hooks.py qa
hatch run qa:integration-tests -k "not(test_commit_hook_continue or test_commit_hook_abort or test_commit_hook_edit)" qa
env:
GITLINT_QA_USE_SH_LIB: 0

Expand Down Expand Up @@ -134,7 +134,7 @@ jobs:
- 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
hatch run qa:integration-tests -k "not (test_commit_hook_continue or test_commit_hook_abort or test_commit_hook_edit or test_lint_staged_stdin or test_stdin_file or test_stdin_pipe_empty)" qa

- name: Build test (gitlint)
run: |
Expand Down
72 changes: 62 additions & 10 deletions qa/shell.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# This code is mostly duplicated from the `gitlint.shell` module. We consciously duplicate this code as to not depend
# on gitlint internals for our integration testing framework.

import asyncio
import queue
import subprocess
from qa.utils import USE_SH_LIB, DEFAULT_ENCODING

Expand Down Expand Up @@ -75,27 +77,77 @@ def run_command(command, *args, **kwargs):

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

stdin_input = None
stdin = None
if len(args) > 1 and isinstance(args[1], ShResult):
stdin_input = args[1].stdout
stdin = 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:
if stdin_input:
result = p.communicate(stdin_input)
else:

async def read_stream(p, stream, iofunc, q, timeout=0.3):
line = bytearray()
try:
while True:
char = await asyncio.wait_for(stream.read(1), timeout)
line += bytearray(char)
if char == b"\n":
decoded = line.decode(DEFAULT_ENCODING)
iofunc(decoded, q)
line = bytearray()
except asyncio.TimeoutError:
decoded = line.decode(DEFAULT_ENCODING)
iofunc(decoded, q)

async def write_stdin(p, q):
# inputstr = await q.get()
inputstr = await asyncio.wait_for(q.get(), 0.25)
p.stdin.write(inputstr.encode(DEFAULT_ENCODING))
await p.stdin.drain()

if kwargs.get("_out", None):
# redirect stderr to stdout (this will ensure we capture the last git output lines which are printed to stdout, not stderr)
popen_kwargs["stderr"] = asyncio.subprocess.STDOUT

async def start_process():
p = await asyncio.create_subprocess_exec(*args, **popen_kwargs)

q = asyncio.Queue()

await asyncio.gather(
# read_stream(p, p.stderr, kwargs["_out"], q),
read_stream(p, p.stdout, kwargs["_out"], q),
)
print("after gather1")
await asyncio.gather(write_stdin(p, q))
print("after gather2")

# Manually answer the prompt here, for some reason I can't get this to work via stdin
await asyncio.sleep(2)

await asyncio.gather(
read_stream(p, p.stdout, kwargs["_out"], q),
)
print("after gather 3")
await p.wait()
print("process finished")

asyncio.run(start_process())
return
elif stdin:
with subprocess.Popen(args, **popen_kwargs) as p:
result = p.communicate(stdin)
else:
with subprocess.Popen(args, **popen_kwargs) as p:
result = p.communicate()

except FileNotFoundError as exc:
Expand Down
27 changes: 16 additions & 11 deletions qa/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,16 @@ def setUp(self):

# install git commit-msg hook and assert output
output_installed = gitlint("install-hook", _cwd=self.tmp_git_repo)
expected_installed = (
f"Successfully installed gitlint commit-msg hook in {self.tmp_git_repo}/.git/hooks/commit-msg\n"
)
commit_msg_hook_path = os.path.join(self.tmp_git_repo, ".git", "hooks", "commit-msg")
expected_installed = f"Successfully installed gitlint commit-msg hook in {commit_msg_hook_path}\n"

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 = (
f"Successfully uninstalled gitlint commit-msg hook from {self.tmp_git_repo}/.git/hooks/commit-msg\n"
)
commit_msg_hook_path = os.path.join(self.tmp_git_repo, ".git", "hooks", "commit-msg")
expected_uninstalled = f"Successfully uninstalled gitlint commit-msg hook from {commit_msg_hook_path}\n"

self.assertEqualStdout(output_uninstalled, expected_uninstalled)
super().tearDown()
Expand All @@ -54,10 +52,13 @@ def _violations(self):

def _interact(self, line, stdin):
self.githook_output.append(line)
print(line)
# Answer 'yes' to question to keep violating commit-msg
if "Your commit message contains violations" in line:
print("VIOLATIONS FOUND!")
response = self.responses[self.response_index]
stdin.put(f"{response}\n")
stdin.put_nowait(f"{response}\r\n")
print("AFTER PUT")
self.response_index = (self.response_index + 1) % len(self.responses)

def test_commit_hook_no_violations(self):
Expand Down Expand Up @@ -93,6 +94,10 @@ def test_commit_hook_continue(self):
" 1 file changed, 0 insertions(+), 0 deletions(-)\n",
f" create mode 100644 {test_filename}\n",
]
print("EXPECTED OUTPUT")
print(expected_output)
print("ACTUAL OUTPUT")
print(self.githook_output)

for output, expected in zip(self.githook_output, expected_output):
self.assertMultiLineEqual(output.replace("\r", ""), expected.replace("\r", ""))
Expand Down Expand Up @@ -171,10 +176,10 @@ def test_commit_hook_worktree(self):

output_installed = gitlint("install-hook", _cwd=worktree_dir)
expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg")
expected_msg = f"Successfully installed gitlint commit-msg hook in {expected_hook_path}\r\n"
self.assertEqual(output_installed, expected_msg)
expected_msg = f"Successfully installed gitlint commit-msg hook in {expected_hook_path}\n"
self.assertEqualStdout(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 = f"Successfully uninstalled gitlint commit-msg hook from {expected_hook_path}\r\n"
self.assertEqual(output_uninstalled, expected_msg)
expected_msg = f"Successfully uninstalled gitlint commit-msg hook from {expected_hook_path}\n"
self.assertEqualStdout(output_uninstalled, expected_msg)