Merge lp:~jml/pkgme/user-friendly-backend-error-972329 into lp:pkgme

Proposed by Jonathan Lange
Status: Merged
Approved by: Jonathan Lange
Approved revision: 137
Merged at revision: 131
Proposed branch: lp:~jml/pkgme/user-friendly-backend-error-972329
Merge into: lp:pkgme
Diff against target: 205 lines (+86/-12)
6 files modified
doc/backends/index.txt (+27/-0)
pkgme/backend.py (+8/-1)
pkgme/run_script.py (+20/-2)
pkgme/tests/test_backend.py (+10/-0)
pkgme/tests/test_debuild.py (+11/-7)
pkgme/tests/test_run_script.py (+10/-2)
To merge this branch: bzr merge lp:~jml/pkgme/user-friendly-backend-error-972329
Reviewer Review Type Date Requested Status
James Westby Approve
Review via email: mp+120793@code.launchpad.net

Commit message

Allow backends to indicate that their errors are for end-user consumption.

Description of the change

Following the advice on bug 972329, this patch allows backends to indicate that their error is meant for end-user consumption by exiting with a pre-defined error code.

It subclasses ScriptFailed with a new exception, ScriptUserError, which essentially forwards the output from the failed script as-is.

The special return code for dispatching is defined in ScriptUserError as a class variable. I just made up the code, but it can be anything. Once we decide on it though, we're stuck with it.

This means that if, say, the binary backend indicated it had an unknown dependency using this mechanism, the error would look like::

  ERROR: Unknown dependency: "libtinfo.so.5"

I also changed the handling on the 'want' script to just re-raise this error, rather than wrapping in AssertionError.

Thanks,
jml

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

11 +If any of your backend scripts return a non-zero error code, pkgme will treat
12 +that as an error and will abort packaging and dump any output from the failed
13 +script in an error message to the user.

Might be worth saying something like "... because it's an exceptional circumstance that the user shouldn't see, so it's providing the most information to help you debug your backend.", and having the next paragraph explain why you may want to have user-friendly errors?

Thanks,

James

review: Approve
137. By Jonathan Lange

Rephrase docs.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'doc/backends/index.txt'
2--- doc/backends/index.txt 2012-04-27 16:54:25 +0000
3+++ doc/backends/index.txt 2012-08-23 10:43:19 +0000
4@@ -171,6 +171,33 @@
5
6 Keys may be omitted from the output if the information cannot be provided.
7
8+Error handling
9+~~~~~~~~~~~~~~
10+
11+Backends generally should not raise errors. Instead, they should use their
12+``want`` scripts to figure out whether they could package something
13+successfully.
14+
15+Thus, if any of your backend scripts return a non-zero error code, pkgme will
16+treat that as a serious and fatal error. It will abort packaging and dump any
17+output from the failed script to the user, with an ugly error message and a
18+traceback. This is intended to provide you with the most information we can
19+to help you debug your backend, and is an exceptional circumstance that users
20+should never see.
21+
22+However, sometimes backends cannot guarantee in their ``want`` script that
23+they can actually package something. Perhaps the check would be very
24+expensive, or perhaps packaging relies on some network operation that
25+fails. In these cases, the user ought to get a nice error message, rather
26+than an ugly traceback.
27+
28+To provide the user with such an error, exit a backend script with return
29+code 42. Then the user will simply be shown ``ERROR:``, followed by any
30+output printed by the script. This gives you full control over the error
31+message that the user sees, and allows you to make the message as friendly or
32+as nasty as you would like.
33+
34+
35 Making your backend available to ``pkgme``
36 ------------------------------------------
37
38
39=== modified file 'pkgme/backend.py'
40--- pkgme/backend.py 2012-04-27 17:19:05 +0000
41+++ pkgme/backend.py 2012-08-23 10:43:19 +0000
42@@ -16,7 +16,12 @@
43 MultipleExternalHelpersInfo,
44 SingleExternalHelperInfo,
45 )
46-from pkgme.run_script import run_script, ScriptMissing, ScriptProblem
47+from pkgme.run_script import (
48+ run_script,
49+ ScriptMissing,
50+ ScriptProblem,
51+ ScriptUserError,
52+ )
53 from pkgme import trace
54
55
56@@ -193,6 +198,8 @@
57 def want(self, path):
58 try:
59 out = run_script(self.basepath, self.WANT_SCRIPT_NAME, path)
60+ except ScriptUserError:
61+ raise
62 except ScriptMissing:
63 raise AssertionError(
64 "Backend %s (%s) has no '%s' script"
65
66=== modified file 'pkgme/run_script.py'
67--- pkgme/run_script.py 2011-11-25 01:52:28 +0000
68+++ pkgme/run_script.py 2012-08-23 10:43:19 +0000
69@@ -50,6 +50,21 @@
70 for line in self.output_lines if line))
71
72
73+class ScriptUserError(ScriptFailed):
74+ """An error from a script meant for end-user consumption."""
75+
76+ # Scripts exit with this return code to indicate that the error is
77+ # intended for end users.
78+ RETURN_CODE = 42
79+
80+ def __init__(self, command, output_lines):
81+ super(ScriptUserError, self).__init__(
82+ command, self.RETURN_CODE, output_lines)
83+
84+ def __str__(self):
85+ return ''.join(self.output_lines)
86+
87+
88 def run_subprocess(cmd, cwd=None, env=None, to_write=None):
89 trace.debug("Running %s" % ' '.join(cmd))
90 # "Python ignores SIGPIPE on startup, because it prefers to check every
91@@ -80,9 +95,12 @@
92 else:
93 break
94 retcode = proc.wait()
95- if retcode != 0:
96+ if retcode == 0:
97+ return output
98+ elif retcode == ScriptUserError.RETURN_CODE:
99+ raise ScriptUserError(cmd, output)
100+ else:
101 raise ScriptFailed(cmd, retcode, output)
102- return output
103 finally:
104 signal.signal(signal.SIGPIPE, old_sig_pipe)
105
106
107=== modified file 'pkgme/tests/test_backend.py'
108--- pkgme/tests/test_backend.py 2012-04-27 17:19:05 +0000
109+++ pkgme/tests/test_backend.py 2012-08-23 10:43:19 +0000
110@@ -23,6 +23,7 @@
111 MultipleExternalHelpersInfo,
112 SingleExternalHelperInfo,
113 )
114+from pkgme.run_script import ScriptUserError
115 from pkgme.testing import (
116 ExecutableFileFixture,
117 FileFixture,
118@@ -163,6 +164,15 @@
119 % (name, tempdir.path, ExternalHelpersBackend.WANT_SCRIPT_NAME),
120 str(e))
121
122+ def test_want_script_returns_user_error(self):
123+ script = "#!/bin/sh\necho foo\nexit %s" % (
124+ ScriptUserError.RETURN_CODE,)
125+ tempdir = self.make_want_script(script)
126+ name = self.getUniqueString()
127+ backend = ExternalHelpersBackend(name, tempdir.path)
128+ e = self.assertRaises(ScriptUserError, backend.want, tempdir.path)
129+ self.assertEqual("foo\n", str(e))
130+
131 def test_want_script_returns_list(self):
132 script = (
133 "#!/usr/bin/python\n"
134
135=== modified file 'pkgme/tests/test_debuild.py'
136--- pkgme/tests/test_debuild.py 2012-07-27 08:06:50 +0000
137+++ pkgme/tests/test_debuild.py 2012-08-23 10:43:19 +0000
138@@ -20,7 +20,7 @@
139
140 def setUp(self, with_binary_data=False):
141 super(DebianTempDirFixture, self).setUp()
142- self.pkgdir = os.path.join(self.path, "testpkg")
143+ self.pkgdir = self.abspath("testpkg")
144 self.icon_path = os.path.join(self.pkgdir, "debian", "icons")
145 self.debian_source_path = os.path.join(self.pkgdir, "debian", "source")
146 # setup fake env
147@@ -50,7 +50,7 @@
148 "testpkg_0.1.orig.tar.gz", os.path.basename(result_path))
149 with tarfile.open(result_path) as f:
150 self.assertEqual(
151- ["testpkg-0.1", "testpkg-0.1/canary"],
152+ ["testpkg-0.1", "testpkg-0.1/canary"],
153 [m.name for m in f.getmembers()])
154
155
156@@ -67,13 +67,17 @@
157 bin_files = _find_binary_files_in_dir(os.path.join(
158 tempdir.pkgdir, "debian"))
159 self.assertEqual(
160- ["icons/foo.png", "icons/bar.png"], bin_files)
161+ sorted(["icons/foo.png", "icons/bar.png"]),
162+ sorted(bin_files))
163
164 def test_build_debian_source_include_binaries_content(self):
165 tempdir = self.useFixture(DebianTempDirFixture())
166 self._make_icons(tempdir)
167 build_debian_source_include_binaries_content(tempdir.pkgdir)
168- self.assertEqual(
169- "debian/icons/foo.png\ndebian/icons/bar.png",
170- open(os.path.join(tempdir.debian_source_path, "include-binaries")).read())
171-
172+ expected_binaries = sorted(
173+ ['debian/icons/foo.png', 'debian/icons/bar.png'])
174+ include_binaries = os.path.join(
175+ tempdir.debian_source_path, "include-binaries")
176+ with open(include_binaries) as f:
177+ found_binaries = sorted(line.strip() for line in f.readlines())
178+ self.assertEqual(expected_binaries, found_binaries)
179
180=== modified file 'pkgme/tests/test_run_script.py'
181--- pkgme/tests/test_run_script.py 2011-11-23 16:26:39 +0000
182+++ pkgme/tests/test_run_script.py 2012-08-23 10:43:19 +0000
183@@ -2,8 +2,10 @@
184
185 from testtools import TestCase
186
187-
188-from pkgme.run_script import ScriptFailed
189+from pkgme.run_script import (
190+ ScriptFailed,
191+ ScriptUserError,
192+ )
193
194
195 class TestErrors(TestCase):
196@@ -21,3 +23,9 @@
197 e = ScriptFailed(['foo', 'bar'], 1, ['line 1\n', 'line 2\n'])
198 e1 = pickle.loads(pickle.dumps(e))
199 self.assertEqual(str(e), str(e1))
200+
201+ def test_script_user_error(self):
202+ e = ScriptUserError(['foo', 'bar'], ['line 1\n', 'line 2\n'])
203+ self.assertEqual(
204+ 'line 1\n'
205+ 'line 2\n', str(e))

Subscribers

People subscribed via source and target branches