Merge lp:~frankban/charms/oneiric/buildbot-master/run-helper into lp:~yellow/charms/oneiric/buildbot-master/trunk

Proposed by Francesco Banconi
Status: Needs review
Proposed branch: lp:~frankban/charms/oneiric/buildbot-master/run-helper
Merge into: lp:~yellow/charms/oneiric/buildbot-master/trunk
Diff against target: 183 lines (+112/-15)
2 files modified
hooks/helpers.py (+64/-7)
hooks/tests.py (+48/-8)
To merge this branch: bzr merge lp:~frankban/charms/oneiric/buildbot-master/run-helper
Reviewer Review Type Date Requested Status
Gary Poster (community) Needs Information
Review via email: mp+94150@code.launchpad.net

Description of the change

Changes:
========

A new run helper accepting `verbose` and `ignore_errors` keyword arguments.
That function is compatible with the previous one, but can be used by both charms and lpsetup.

Tests:
======

$ python hooks/tests.py
........................
----------------------------------------------------------------------
Ran 24 tests in 0.088s

OK

$ python -m doctest hooks/helpers.py

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Hi Francesco. Thank you. As we discussed on the phone, let's see if we can drastically simplify this. I'll return to this if you tell me I should.

review: Needs Information

Unmerged revisions

33. By Francesco Banconi

Updated run helper.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/helpers.py'
2--- hooks/helpers.py 2012-02-14 17:00:53 +0000
3+++ hooks/helpers.py 2012-02-22 13:11:43 +0000
4@@ -44,19 +44,76 @@
5 Env = namedtuple('Env', 'uid gid home')
6
7
8-def run(*args):
9+def run(*args, **kwargs):
10 """Run the command with the given arguments.
11
12 The first argument is the path to the command to run, subsequent arguments
13 are command-line arguments to be passed.
14+
15+ Usually the output is captured by a pipe and returned::
16+
17+ >>> run('echo', 'output')
18+ 'output\\n'
19+
20+ If the keyword argument `verbose` is True, the output and error are
21+ printed to stdout and stderr, and the return code is returned::
22+
23+ >>> run('ls', '-l', verbose=True, stdout=subprocess.PIPE)
24+ 0
25+
26+ A `subprocess.CalledProcessError` exception is raised if the return
27+ code is not zero::
28+
29+ >>> run('ls', '--not a valid switch') # doctest: +ELLIPSIS
30+ Traceback (most recent call last):
31+ CalledProcessError: Command 'ls --not a valid switch' ...
32+
33+ A keyword argument `exception` can be passed to raise another exception.
34+ However, the new exception must accept retcode, cmd and output args::
35+
36+ >>> class MyException(subprocess.CalledProcessError): pass
37+ >>> run('ls', '--not a valid switch',
38+ ... exception=MyException) # doctest: +ELLIPSIS
39+ Traceback (most recent call last):
40+ MyException: Command 'ls --not a valid switch' ...
41+
42+ If the keyword argument `ignore_errors` is True, then the stderr is
43+ returned in case of errors::
44+
45+ >>> run('ls', '--not a valid switch',
46+ ... ignore_errors=True) # doctest: +ELLIPSIS
47+ "ls: unrecognized option '--not a valid switch'..."
48+
49+ It is possible to combine `ignore_errors` and `verbose` to get the
50+ error code::
51+
52+ >>> recode = run(
53+ ... 'ls', '--not a valid switch',
54+ ... ignore_errors=True, verbose=True, stderr=subprocess.PIPE)
55+ >>> recode == 2
56+ True
57+
58+ None arguments are ignored::
59+
60+ >>> run(None, 'ls', None, '-l', verbose=True, stdout=subprocess.PIPE)
61+ 0
62 """
63- process = subprocess.Popen(args, stdout=subprocess.PIPE,
64- stderr=subprocess.PIPE, close_fds=True)
65+ exception = kwargs.pop('exception', subprocess.CalledProcessError)
66+ ignore_errors = kwargs.pop('ignore_errors', False)
67+ verbose = kwargs.pop('verbose', False)
68+ default = None if verbose else subprocess.PIPE
69+ args = [i for i in args if i is not None]
70+ process = subprocess.Popen(
71+ args, stdout=kwargs.pop('stdout', default),
72+ stderr=kwargs.pop('stderr', default), close_fds=True,
73+ **kwargs)
74 stdout, stderr = process.communicate()
75- if process.returncode:
76- raise subprocess.CalledProcessError(
77- process.returncode, repr(args), output=stdout+stderr)
78- return stdout
79+ retcode = process.poll()
80+ if retcode and not ignore_errors:
81+ cmd = ' '.join(i if i.strip() else "'{}'".format(i) for i in args)
82+ output = None if verbose else stdout + stderr
83+ raise exception(retcode, cmd, output=output)
84+ return retcode if verbose else stdout + stderr
85
86
87 def command(*base_args):
88
89=== modified file 'hooks/tests.py'
90--- hooks/tests.py 2012-02-10 19:43:46 +0000
91+++ hooks/tests.py 2012-02-22 13:11:43 +0000
92@@ -1,5 +1,6 @@
93 import os
94-from subprocess import CalledProcessError
95+import subprocess
96+import tempfile
97 import unittest
98
99 from helpers import (
100@@ -10,10 +11,12 @@
101 unit_info,
102 )
103
104-from subprocess import CalledProcessError
105
106 class TestRun(unittest.TestCase):
107
108+ invalid_command = ('/bin/ls', '--not a valid switch')
109+ valid_command = ('/bin/ls', '--help')
110+
111 def testSimpleCommand(self):
112 # Running a simple command (ls) works and running the command
113 # produces a string.
114@@ -22,18 +25,55 @@
115 def testStdoutReturned(self):
116 # Running a simple command (ls) works and running the command
117 # produces a string.
118- self.assertIn('Usage:', run('/bin/ls', '--help'))
119+ self.assertIn('Usage:', run(*self.valid_command))
120
121- def testSimpleCommand(self):
122+ def testCommandError(self):
123 # If an error occurs a CalledProcessError is raised with the return
124 # code, command executed, and the output of the command.
125- with self.assertRaises(CalledProcessError) as info:
126- run('ls', '--not a valid switch')
127+ with self.assertRaises(subprocess.CalledProcessError) as info:
128+ run(*self.invalid_command)
129 exception = info.exception
130 self.assertEqual(2, exception.returncode)
131- self.assertEqual("('ls', '--not a valid switch')", exception.cmd)
132+ self.assertEqual(' '.join(self.invalid_command), exception.cmd)
133 self.assertIn('unrecognized option', exception.output)
134
135+ def testCustomException(self):
136+ # In case of errors a custom exception is raised if provided as
137+ # `exception`.
138+ class MyException(subprocess.CalledProcessError): pass
139+ self.assertRaises(
140+ MyException, run, *self.invalid_command, exception=MyException)
141+
142+ def testVerbose(self):
143+ # If `verbose` keyword argument is given, the output is printed to
144+ # stdout and the function returns command retcode.
145+ with tempfile.TemporaryFile(mode='r+') as stdout:
146+ retcode = run(*self.valid_command, verbose=True, stdout=stdout)
147+ stdout.seek(0)
148+ self.assertIn('Usage:', stdout.read())
149+ self.assertEqual(0, retcode)
150+
151+ def testIgnoreErrors(self):
152+ # If `ignore_errors` keyword argument is given, if an error occurs
153+ # the function returns stdout + stderr and no exception is raised.
154+ error = run(*self.invalid_command, ignore_errors=True)
155+ self.assertIn('unrecognized option', error)
156+
157+ def testVerboseIgnoreErrors(self):
158+ # If an error occurs it is possible to print it and obtain the error
159+ # code combining `verbose` and `ignore_errors` keyword arguments.
160+ with tempfile.TemporaryFile(mode='r+') as stderr:
161+ retcode = run(
162+ *self.invalid_command, verbose=True,
163+ ignore_errors=True, stderr=stderr)
164+ stderr.seek(0)
165+ self.assertIn('unrecognized option', stderr.read())
166+ self.assertEqual(2, retcode)
167+
168+ def testNoneArguments(self):
169+ # None arguments are ignored.
170+ self.assertIn('Usage:', run(None, '/bin/ls', None, '--help'))
171+
172
173 class TestCommand(unittest.TestCase):
174
175@@ -59,7 +99,7 @@
176 def testError(self):
177 # If the command returns a non-zero exit code, an exception is raised.
178 ls = command('/bin/ls')
179- with self.assertRaises(CalledProcessError):
180+ with self.assertRaises(subprocess.CalledProcessError):
181 ls('--not a valid switch')
182
183 def testBakedInArguments(self):

Subscribers

People subscribed via source and target branches