Skip to content

Commit b911949

Browse files
authored
fix: better error messages on build failures (#146)
* fix: better error messages on build failures Why is this change necessary? * Generic "Binary Validation Failed" does not add enough value How does it address the issue? * Showcases which binary has failed and shows which paths were looked at. What side effects does this change have? * None. * fix: explicit error messages on binary validation failure - fail workflow explicitly if binary resolution failed * fix: surface error messages for all binary validation failures * fix: fix tests for unittest imports * fix: make pylint happy * fix: appveyor black version
1 parent 8c9d476 commit b911949

File tree

12 files changed

+105
-22
lines changed

12 files changed

+105
-22
lines changed

.appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ for:
101101
- sh: "PATH=/opt/gradle/gradle-5.5/bin:$PATH"
102102

103103
# Install black
104-
- sh: "wget -O /tmp/black https://github.com/python/black/releases/download/19.3b0/black"
104+
- sh: "wget -O /tmp/black https://github.com/python/black/releases/download/19.10b0/black"
105105
- sh: "chmod +x /tmp/black"
106106
- sh: "/tmp/black --version"
107107

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ wheels/
257257
# Installer logs
258258
pip-log.txt
259259
pip-delete-this-directory.txt
260-
260+
pip-wheel-metadata/
261261
# Unit test / coverage reports
262262
htmlcov/
263263
.tox/
@@ -389,4 +389,4 @@ $RECYCLE.BIN/
389389

390390
tests/integration/workflows/go_dep/data/src/*/vendor/*
391391

392-
# End of https://www.gitignore.io/api/osx,node,macos,linux,python,windows,pycharm,intellij,sublimetext,visualstudiocode
392+
# End of https://www.gitignore.io/api/osx,node,macos,linux,python,windows,pycharm,intellij,sublimetext,visualstudiocode

aws_lambda_builders/workflow.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,39 @@ def sanitize(func):
4040

4141
@functools.wraps(func)
4242
def wrapper(self, *args, **kwargs):
43-
valid_paths = []
43+
valid_paths = {}
44+
invalid_paths = {}
4445
# NOTE: we need to access binaries to get paths and resolvers, before validating.
45-
binaries_copy = self.binaries
46-
for binary, binary_path in binaries_copy.items():
47-
validator = binary_path.validator
48-
exec_paths = binary_path.resolver.exec_paths if not binary_path.path_provided else binary_path.binary_path
46+
for binary, binary_checker in self.binaries.items():
47+
invalid_paths[binary] = []
48+
try:
49+
exec_paths = (
50+
binary_checker.resolver.exec_paths
51+
if not binary_checker.path_provided
52+
else binary_checker.binary_path
53+
)
54+
except ValueError as ex:
55+
raise WorkflowFailedError(workflow_name=self.NAME, action_name="Resolver", reason=str(ex))
4956
for executable_path in exec_paths:
50-
valid_path = None
5157
try:
52-
valid_path = validator.validate(executable_path)
58+
valid_path = binary_checker.validator.validate(executable_path)
59+
if valid_path:
60+
valid_paths[binary] = valid_path
5361
except MisMatchRuntimeError as ex:
5462
LOG.debug("Invalid executable for %s at %s", binary, executable_path, exc_info=str(ex))
55-
if valid_path:
56-
binary_path.binary_path = valid_path
57-
valid_paths.append(valid_path)
63+
invalid_paths[binary].append(executable_path)
64+
if valid_paths.get(binary, None):
65+
binary_checker.binary_path = valid_paths[binary]
5866
break
59-
self.binaries = binaries_copy
6067
if len(self.binaries) != len(valid_paths):
61-
raise WorkflowFailedError(workflow_name=self.NAME, action_name=None, reason="Binary validation failed!")
68+
validation_failed_binaries = set(self.binaries.keys()).difference(valid_paths.keys())
69+
messages = []
70+
for validation_failed_binary in validation_failed_binaries:
71+
message = "Binary validation failed for {0}, searched for {0} in following locations : {1} which did not satisfy constraints for runtime: {2}. Do you have {0} for runtime: {2} on your PATH?".format(
72+
validation_failed_binary, invalid_paths[validation_failed_binary], self.runtime
73+
)
74+
messages.append(message)
75+
raise WorkflowFailedError(workflow_name=self.NAME, action_name="Validation", reason="\n".join(messages))
6276
func(self, *args, **kwargs)
6377

6478
return wrapper
@@ -261,6 +275,7 @@ def run(self):
261275

262276
raise WorkflowFailedError(workflow_name=self.NAME, action_name=action.NAME, reason=str(ex))
263277
except Exception as ex:
278+
264279
LOG.debug("%s raised unhandled exception", action_info, exc_info=ex)
265280

266281
raise WorkflowUnknownError(workflow_name=self.NAME, action_name=action.NAME, reason=str(ex))

aws_lambda_builders/workflows/python_pip/actions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError
66
from aws_lambda_builders.workflows.python_pip.utils import OSUtils
7+
from .exceptions import MissingPipError
78
from .packager import PythonPipDependencyBuilder, PackagerError, DependencyBuilder, SubprocessPip, PipRunner
89

910

@@ -24,7 +25,10 @@ def __init__(self, artifacts_dir, scratch_dir, manifest_path, runtime, binaries)
2425
def execute(self):
2526
os_utils = OSUtils()
2627
python_path = self.binaries[self.LANGUAGE].binary_path
27-
pip = SubprocessPip(osutils=os_utils, python_exe=python_path)
28+
try:
29+
pip = SubprocessPip(osutils=os_utils, python_exe=python_path)
30+
except MissingPipError as ex:
31+
raise ActionFailedError(str(ex))
2832
pip_runner = PipRunner(python_exe=python_path, pip=pip)
2933
dependency_builder = DependencyBuilder(osutils=os_utils, pip_runner=pip_runner, runtime=self.runtime)
3034

aws_lambda_builders/workflows/python_pip/compat.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22

3+
from aws_lambda_builders.workflows.python_pip.exceptions import MissingPipError
34
from aws_lambda_builders.workflows.python_pip.utils import OSUtils
45

56

@@ -8,6 +9,8 @@ def pip_import_string(python_exe):
89
cmd = [python_exe, "-c", "import pip; print(pip.__version__)"]
910
p = os_utils.popen(cmd, stdout=os_utils.pipe, stderr=os_utils.pipe)
1011
stdout, stderr = p.communicate()
12+
if not p.returncode == 0:
13+
raise MissingPipError(python_path=python_exe)
1114
pip_version = stdout.decode("utf-8").strip()
1215
pip_major_version = int(pip_version.split(".")[0])
1316
pip_minor_version = int(pip_version.split(".")[1])
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""
2+
Python pip specific workflow exceptions.
3+
"""
4+
from aws_lambda_builders.exceptions import LambdaBuilderError
5+
6+
7+
class MissingPipError(LambdaBuilderError):
8+
MESSAGE = "pip executable not found in your python environment at {python_path}"

tests/functional/testdata/cwd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import os
22

3-
print (os.getcwd())
3+
print(os.getcwd())

tests/integration/workflows/go_modules/test_go.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_builds_project_without_dependencies(self):
3737
)
3838
expected_files = {"no-deps-main"}
3939
output_files = set(os.listdir(self.artifacts_dir))
40-
print (output_files)
40+
print(output_files)
4141
self.assertEquals(expected_files, output_files)
4242

4343
def test_builds_project_with_dependencies(self):

tests/integration/workflows/python_pip/test_python_pip.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def test_mismatch_runtime_python_project(self):
6464
runtime=self.runtime_mismatch[self.runtime],
6565
)
6666
except WorkflowFailedError as ex:
67-
self.assertIn("Binary validation failed!", str(ex))
67+
self.assertIn("Binary validation failed", str(ex))
6868

6969
def test_runtime_validate_python_project_fail_open_unsupported_runtime(self):
7070
with self.assertRaises(WorkflowFailedError):

tests/unit/test_workflow.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import os
22
import sys
33
from unittest import TestCase
4-
from mock import Mock, call
4+
5+
from mock import Mock, MagicMock, call
56

67
try:
78
import pathlib
@@ -12,7 +13,7 @@
1213
from aws_lambda_builders.validator import RuntimeValidator
1314
from aws_lambda_builders.workflow import BaseWorkflow, Capability
1415
from aws_lambda_builders.registry import get_workflow, DEFAULT_REGISTRY
15-
from aws_lambda_builders.exceptions import WorkflowFailedError, WorkflowUnknownError
16+
from aws_lambda_builders.exceptions import WorkflowFailedError, WorkflowUnknownError, MisMatchRuntimeError
1617
from aws_lambda_builders.actions import ActionFailedError
1718

1819

@@ -206,6 +207,42 @@ def test_must_execute_actions_in_sequence(self):
206207
)
207208
self.assertTrue(validator_mock.validate.call_count, 1)
208209

210+
def test_must_fail_workflow_binary_resolution_failure(self):
211+
action_mock = Mock()
212+
validator_mock = Mock()
213+
validator_mock.validate = Mock()
214+
validator_mock.validate.return_value = None
215+
resolver_mock = Mock()
216+
resolver_mock.exec_paths = MagicMock(side_effect=ValueError("Binary could not be resolved"))
217+
binaries_mock = Mock()
218+
binaries_mock.return_value = []
219+
220+
self.work.get_validators = lambda: validator_mock
221+
self.work.get_resolvers = lambda: resolver_mock
222+
self.work.actions = [action_mock.action1, action_mock.action2, action_mock.action3]
223+
self.work.binaries = {"binary": BinaryPath(resolver=resolver_mock, validator=validator_mock, binary="binary")}
224+
with self.assertRaises(WorkflowFailedError) as ex:
225+
self.work.run()
226+
227+
def test_must_fail_workflow_binary_validation_failure(self):
228+
action_mock = Mock()
229+
validator_mock = Mock()
230+
validator_mock.validate = Mock()
231+
validator_mock.validate = MagicMock(
232+
side_effect=MisMatchRuntimeError(language="test", required_runtime="test1", runtime_path="/usr/bin/binary")
233+
)
234+
resolver_mock = Mock()
235+
resolver_mock.exec_paths = ["/usr/bin/binary"]
236+
binaries_mock = Mock()
237+
binaries_mock.return_value = []
238+
239+
self.work.get_validators = lambda: validator_mock
240+
self.work.get_resolvers = lambda: resolver_mock
241+
self.work.actions = [action_mock.action1, action_mock.action2, action_mock.action3]
242+
self.work.binaries = {"binary": BinaryPath(resolver=resolver_mock, validator=validator_mock, binary="binary")}
243+
with self.assertRaises(WorkflowFailedError) as ex:
244+
self.work.run()
245+
209246
def test_must_raise_with_no_actions(self):
210247
self.work.actions = []
211248

0 commit comments

Comments
 (0)