Fail gracefully on undecodable install output.

This commit is contained in:
Anthony Sottile 2016-01-12 09:51:40 -08:00
parent 75aaadd4c4
commit 2aaaddb5cc
9 changed files with 104 additions and 24 deletions

View file

@ -18,8 +18,19 @@ class PreCommitSystemExit(SystemExit):
pass pass
def _to_bytes(exc):
try:
return bytes(exc)
except Exception:
return five.text(exc).encode('UTF-8')
def _log_and_exit(msg, exc, formatted, write_fn=sys_stdout_write_wrapper): def _log_and_exit(msg, exc, formatted, write_fn=sys_stdout_write_wrapper):
error_msg = '{0}: {1}: {2}\n'.format(msg, type(exc).__name__, exc) error_msg = b''.join((
five.to_bytes(msg), b': ',
five.to_bytes(type(exc).__name__), b': ',
_to_bytes(exc), b'\n',
))
write_fn(error_msg) write_fn(error_msg)
write_fn('Check the log at ~/.pre-commit/pre-commit.log\n') write_fn('Check the log at ~/.pre-commit/pre-commit.log\n')
store = Store() store = Store()

View file

@ -45,13 +45,14 @@ def install_environment(
repo_cmd_runner.run(cmd) repo_cmd_runner.run(cmd)
with in_env(repo_cmd_runner, version) as node_env: with in_env(repo_cmd_runner, version) as node_env:
node_env.run("cd '{prefix}' && npm install -g") node_env.run("cd '{prefix}' && npm install -g", encoding=None)
if additional_dependencies: if additional_dependencies:
node_env.run( node_env.run(
"cd '{prefix}' && npm install -g " + "cd '{prefix}' && npm install -g " +
' '.join( ' '.join(
shell_escape(dep) for dep in additional_dependencies shell_escape(dep) for dep in additional_dependencies
) ),
encoding=None,
) )

View file

@ -63,13 +63,14 @@ def install_environment(
venv_cmd.extend(['-p', norm_version(version)]) venv_cmd.extend(['-p', norm_version(version)])
repo_cmd_runner.run(venv_cmd) repo_cmd_runner.run(venv_cmd)
with in_env(repo_cmd_runner, version) as env: with in_env(repo_cmd_runner, version) as env:
env.run("cd '{prefix}' && pip install .") env.run("cd '{prefix}' && pip install .", encoding=None)
if additional_dependencies: if additional_dependencies:
env.run( env.run(
"cd '{prefix}' && pip install " + "cd '{prefix}' && pip install " +
' '.join( ' '.join(
shell_escape(dep) for dep in additional_dependencies shell_escape(dep) for dep in additional_dependencies
) ),
encoding=None,
) )

View file

@ -95,13 +95,15 @@ def install_environment(
ruby_env.run( ruby_env.run(
'cd {prefix} && gem build *.gemspec && ' 'cd {prefix} && gem build *.gemspec && '
'gem install --no-ri --no-rdoc *.gem', 'gem install --no-ri --no-rdoc *.gem',
encoding=None,
) )
if additional_dependencies: if additional_dependencies:
ruby_env.run( ruby_env.run(
'cd {prefix} && gem install --no-ri --no-rdoc ' + 'cd {prefix} && gem install --no-ri --no-rdoc ' +
' '.join( ' '.join(
shell_escape(dep) for dep in additional_dependencies shell_escape(dep) for dep in additional_dependencies
) ),
encoding=None,
) )

View file

@ -124,26 +124,38 @@ class CalledProcessError(RuntimeError):
self.expected_returncode = expected_returncode self.expected_returncode = expected_returncode
self.output = output self.output = output
def __str__(self): def to_bytes(self):
output = [] output = []
for text in self.output: for maybe_text in self.output:
if text: if maybe_text:
output.append('\n ' + text.replace('\n', '\n ')) output.append(
b'\n ' +
five.to_bytes(maybe_text).replace(b'\n', b'\n ')
)
else: else:
output.append('(none)') output.append(b'(none)')
return ( return b''.join((
'Command: {0!r}\n' five.to_bytes(
'Return code: {1}\n' 'Command: {0!r}\n'
'Expected return code: {2}\n' 'Return code: {1}\n'
'Output: {3}\n' 'Expected return code: {2}\n'.format(
'Errors: {4}\n'.format( self.cmd, self.returncode, self.expected_returncode
self.cmd, )
self.returncode, ),
self.expected_returncode, b'Output: ', output[0], b'\n',
*output b'Errors: ', output[1], b'\n',
) ))
)
def to_text(self):
return self.to_bytes().decode('UTF-8')
if five.PY3: # pragma: no cover
__bytes__ = to_bytes
__str__ = to_text
else:
__str__ = to_bytes
__unicode__ = to_text
def cmd_output(*cmd, **kwargs): def cmd_output(*cmd, **kwargs):

View file

@ -0,0 +1,6 @@
- id: foo
name: Foo
entry: foo
language: python
language_version: python2.7
files: \.py$

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
from __future__ import unicode_literals
import sys
def main():
# Intentionally write mixed encoding to the output. This should not crash
# pre-commit and should write bytes to the output.
sys.stderr.write(''.encode('UTF-8') + '²'.encode('latin1') + b'\n')
# Return 1 to indicate failures
return 1
if __name__ == '__main__':
exit(main())

View file

@ -371,6 +371,33 @@ def test_stdout_write_bug_py26(
assert 'UnicodeDecodeError' not in stdout assert 'UnicodeDecodeError' not in stdout
def test_hook_install_failure(mock_out_store_directory, tempdir_factory):
git_path = make_consuming_repo(tempdir_factory, 'not_installable_repo')
with cwd(git_path):
install(Runner(git_path))
# Don't want to write to home directory
env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get())
_, stdout, _ = cmd_output(
'git', 'commit', '-m', 'Commit!',
# git commit puts pre-commit to stderr
stderr=subprocess.STDOUT,
env=env,
retcode=None,
encoding=None,
)
assert b'UnicodeDecodeError' not in stdout
# Doesn't actually happen, but a reasonable assertion
assert b'UnicodeEncodeError' not in stdout
# Sanity check our output
assert (
b'An unexpected error has occurred: CalledProcessError: ' in
stdout
)
assert ''.encode('UTF-8') + '²'.encode('latin1') in stdout
def test_get_changed_files(): def test_get_changed_files():
files = get_changed_files( files = get_changed_files(
'78c682a1d13ba20e7cb735313b9314a74365cd3a', '78c682a1d13ba20e7cb735313b9314a74365cd3a',

View file

@ -11,6 +11,7 @@ import mock
import pytest import pytest
from pre_commit import error_handler from pre_commit import error_handler
from pre_commit import five
from pre_commit.errors import FatalError from pre_commit.errors import FatalError
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
@ -82,7 +83,9 @@ def test_log_and_exit(mock_out_store_directory):
write_fn=mocked_write, write_fn=mocked_write,
) )
printed = ''.join(call[0][0] for call in mocked_write.call_args_list) printed = ''.join(
five.to_text(call[0][0]) for call in mocked_write.call_args_list
)
assert printed == ( assert printed == (
'msg: FatalError: hai\n' 'msg: FatalError: hai\n'
'Check the log at ~/.pre-commit/pre-commit.log\n' 'Check the log at ~/.pre-commit/pre-commit.log\n'