Repository: audreyr/cookiecutter (python)

Issue:
Empty hook file causes cryptic error
If you have a pre_gen_project.sh or a post_gen_project.sh file with no data in it, cookiecutter fails with an unhelpful traceback.

```
Traceback (most recent call last):
  File "/usr/local/bin/cookiecutter", line 11, in <module>
    sys.exit(main())
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 716, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 696, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 889, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 534, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/cli.py", line 100, in main
    config_file=user_config
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/main.py", line 140, in cookiecutter
    output_dir=output_dir
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/generate.py", line 273, in generate_files
    _run_hook_from_repo_dir(repo_dir, 'pre_gen_project', project_dir, context)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/generate.py", line 232, in _run_hook_from_repo_dir
    run_hook(hook_name, project_dir, context)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/hooks.py", line 116, in run_hook
    run_script_with_context(script, project_dir, context)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/hooks.py", line 101, in run_script_with_context
    run_script(temp.name, cwd)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/hooks.py", line 73, in run_script
    cwd=cwd
  File "/usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 656, in __init__
    _cleanup()
  File "/usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 1335, in _execute_child
    raise child_exception
OSError: [Errno 8] Exec format error
```


Pull request description:
Handle empty hook files
Address code review comments of #729 and use ``@pytest.skipif``.

⬆️ @michaeljoseph @jcarbaugh

Resolves #632 

Patch:
diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py
index bffd5d7..a3e3874 100644
--- a/cookiecutter/hooks.py
+++ b/cookiecutter/hooks.py
@@ -2,6 +2,7 @@
 
 """Functions for discovering and executing various cookiecutter hooks."""
 
+import errno
 import io
 import logging
 import os
@@ -79,15 +80,26 @@ def run_script(script_path, cwd='.'):
 
     utils.make_executable(script_path)
 
-    proc = subprocess.Popen(
-        script_command,
-        shell=run_thru_shell,
-        cwd=cwd
-    )
-    exit_status = proc.wait()
-    if exit_status != EXIT_SUCCESS:
+    try:
+        proc = subprocess.Popen(
+            script_command,
+            shell=run_thru_shell,
+            cwd=cwd
+        )
+        exit_status = proc.wait()
+        if exit_status != EXIT_SUCCESS:
+            raise FailedHookException(
+                'Hook script failed (exit status: {})'.format(exit_status)
+            )
+    except OSError as os_error:
+        if os_error.errno == errno.ENOEXEC:
+            raise FailedHookException(
+                'Hook script failed, might be an '
+                'empty file or missing a shebang'
+            )
         raise FailedHookException(
-            "Hook script failed (exit status: %d)" % exit_status)
+            'Hook script failed (error: {})'.format(os_error)
+        )
 
 
 def run_script_with_context(script_path, cwd, context):


Test patch:
diff --git a/tests/test-shellhooks-empty/hooks/pre_gen_project.sh b/tests/test-shellhooks-empty/hooks/pre_gen_project.sh
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test-shellhooks-empty/input{{cookiecutter.shellhooks}}/README.rst b/tests/test-shellhooks-empty/input{{cookiecutter.shellhooks}}/README.rst
new file mode 100644
index 0000000..8c02abb
--- /dev/null
+++ b/tests/test-shellhooks-empty/input{{cookiecutter.shellhooks}}/README.rst
@@ -0,0 +1,1 @@
+Empty, just like the hooks we are testing.
diff --git a/tests/test_generate_hooks.py b/tests/test_generate_hooks.py
index e6f9d3a..7617899 100644
--- a/tests/test_generate_hooks.py
+++ b/tests/test_generate_hooks.py
@@ -12,6 +12,7 @@ TestHooks.test_run_shell_hooks
 """
 
 from __future__ import unicode_literals
+import errno
 import os
 import sys
 import stat
@@ -19,6 +20,9 @@ import pytest
 
 from cookiecutter import generate
 from cookiecutter import utils
+from cookiecutter.exceptions import FailedHookException
+
+WINDOWS = sys.platform.startswith('win')
 
 
 @pytest.fixture(scope='function')
@@ -73,6 +77,44 @@ def test_run_python_hooks_cwd():
     assert os.path.exists('inputpyhooks/python_post.txt')
 
 
+@pytest.mark.skipif(WINDOWS, reason='OSError.errno=8 is not thrown on Windows')
+@pytest.mark.usefixtures('clean_system', 'remove_additional_folders')
+def test_empty_hooks():
+    # OSError.errno=8 is not thrown on Windows when the script is empty
+    # because it always runs through shell instead of needing a shebang.
+    with pytest.raises(FailedHookException) as excinfo:
+        generate.generate_files(
+            context={
+                'cookiecutter': {'shellhooks': 'shellhooks'}
+            },
+            repo_dir='tests/test-shellhooks-empty/',
+            overwrite_if_exists=True
+        )
+    assert 'shebang' in str(excinfo.value)
+
+
+@pytest.mark.usefixtures('clean_system', 'remove_additional_folders')
+def test_oserror_hooks(mocker):
+
+    message = 'Out of memory'
+
+    err = OSError(message)
+    err.errno = errno.ENOMEM
+
+    prompt = mocker.patch('subprocess.Popen')
+    prompt.side_effect = err
+
+    with pytest.raises(FailedHookException) as excinfo:
+        generate.generate_files(
+            context={
+                'cookiecutter': {'shellhooks': 'shellhooks'}
+            },
+            repo_dir='tests/test-shellhooks-empty/',
+            overwrite_if_exists=True
+        )
+    assert message in str(excinfo.value)
+
+
 def make_test_repo(name):
     hooks = os.path.join(name, 'hooks')
     template = os.path.join(name, 'input{{cookiecutter.shellhooks}}')