Repository: pre-commit/pre-commit
Pull request #1601 (2020-09-16 21:46:38)

Distinct exit codes for the various error cases
This PR attempts to follow the Linux/Bash convention for exit codes. See more at [Exit Codes With Special Meanings](https://tldp.org/LDP/abs/html/exitcodes.html) (You might need to use the [Google web cache](https://www.google.com/search?q=%22Exit+Codes+With+Special+Meanings%22&oq=%22Exit+Codes+With+Special+Meanings%22), because the resource seems to be down right now).

Basically we will have the following:
- `1` for standard error
- `130` for interrupted process by user
- `3` for any unexpected error

Bash/GNU examples which return exit code `130`:
```bash
tail -f /dev/null
# press Ctrl + C
echo $?
# 130

sleep 42
# press Ctrl + C
echo $?
# 130
```

int3l: Windows CI pipeline takes ages to complete 🎃 (25+ minutes)

Patch:
diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py
index afacab9..023dd35 100644
--- a/pre_commit/error_handler.py
+++ b/pre_commit/error_handler.py
@@ -12,7 +12,12 @@ from pre_commit.store import Store
 from pre_commit.util import force_bytes
 
 
-def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None:
+def _log_and_exit(
+    msg: str,
+    ret_code: int,
+    exc: BaseException,
+    formatted: str,
+) -> None:
     error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc)
     output.write_line_b(error_msg)
 
@@ -51,7 +56,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None:
         _log_line('```')
         _log_line(formatted.rstrip())
         _log_line('```')
-    raise SystemExit(1)
+    raise SystemExit(ret_code)
 
 
 @contextlib.contextmanager
@@ -60,9 +65,9 @@ def error_handler() -> Generator[None, None, None]:
         yield
     except (Exception, KeyboardInterrupt) as e:
         if isinstance(e, FatalError):
-            msg = 'An error has occurred'
+            msg, ret_code = 'An error has occurred', 1
         elif isinstance(e, KeyboardInterrupt):
-            msg = 'Interrupted (^C)'
+            msg, ret_code = 'Interrupted (^C)', 130
         else:
-            msg = 'An unexpected error has occurred'
-        _log_and_exit(msg, e, traceback.format_exc())
+            msg, ret_code = 'An unexpected error has occurred', 3
+        _log_and_exit(msg, ret_code, e, traceback.format_exc())


Test patch:
diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py
index 804701f..6b0bb86 100644
--- a/tests/error_handler_test.py
+++ b/tests/error_handler_test.py
@@ -33,6 +33,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit):
 
     mocked_log_and_exit.assert_called_once_with(
         'An error has occurred',
+        1,
         exc,
         # Tested below
         mock.ANY,
@@ -47,7 +48,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit):
         r'    raise exc\n'
         r'(pre_commit\.errors\.)?FatalError: just a test\n',
     )
-    pattern.assert_matches(mocked_log_and_exit.call_args[0][2])
+    pattern.assert_matches(mocked_log_and_exit.call_args[0][3])
 
 
 def test_error_handler_uncaught_error(mocked_log_and_exit):
@@ -57,6 +58,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit):
 
     mocked_log_and_exit.assert_called_once_with(
         'An unexpected error has occurred',
+        3,
         exc,
         # Tested below
         mock.ANY,
@@ -70,7 +72,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit):
         r'    raise exc\n'
         r'ValueError: another test\n',
     )
-    pattern.assert_matches(mocked_log_and_exit.call_args[0][2])
+    pattern.assert_matches(mocked_log_and_exit.call_args[0][3])
 
 
 def test_error_handler_keyboardinterrupt(mocked_log_and_exit):
@@ -80,6 +82,7 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit):
 
     mocked_log_and_exit.assert_called_once_with(
         'Interrupted (^C)',
+        130,
         exc,
         # Tested below
         mock.ANY,
@@ -93,7 +96,7 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit):
         r'    raise exc\n'
         r'KeyboardInterrupt\n',
     )
-    pattern.assert_matches(mocked_log_and_exit.call_args[0][2])
+    pattern.assert_matches(mocked_log_and_exit.call_args[0][3])
 
 
 def test_log_and_exit(cap_out, mock_store_dir):
@@ -103,8 +106,9 @@ def test_log_and_exit(cap_out, mock_store_dir):
         'pre_commit.errors.FatalError: hai\n'
     )
 
-    with pytest.raises(SystemExit):
-        error_handler._log_and_exit('msg', FatalError('hai'), tb)
+    with pytest.raises(SystemExit) as excinfo:
+        error_handler._log_and_exit('msg', 1, FatalError('hai'), tb)
+    assert excinfo.value.code == 1
 
     printed = cap_out.get()
     log_file = os.path.join(mock_store_dir, 'pre-commit.log')
@@ -170,7 +174,7 @@ def test_error_handler_no_tty(tempdir_factory):
         'from pre_commit.error_handler import error_handler\n'
         'with error_handler():\n'
         '    raise ValueError("\\u2603")\n',
-        retcode=1,
+        retcode=3,
         tempdir_factory=tempdir_factory,
         pre_commit_home=pre_commit_home,
     )