Merge lp:~bac/charms/oneiric/buildbot-master/bbm-pst into lp:~yellow/charms/oneiric/buildbot-master/trunk

Proposed by Brad Crittenden
Status: Merged
Approved by: Graham Binns
Approved revision: 37
Merged at revision: 37
Proposed branch: lp:~bac/charms/oneiric/buildbot-master/bbm-pst
Merge into: lp:~yellow/charms/oneiric/buildbot-master/trunk
Diff against target: 748 lines (+107/-429)
8 files modified
hooks/buildbot-relation-changed (+1/-1)
hooks/config-changed (+15/-11)
hooks/helpers.py (+13/-237)
hooks/install (+66/-6)
hooks/local.py (+5/-3)
hooks/start (+2/-1)
hooks/stop (+3/-2)
hooks/tests.py (+2/-168)
To merge this branch: bzr merge lp:~bac/charms/oneiric/buildbot-master/bbm-pst
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+95279@code.launchpad.net

Description of the change

Move generic, non-juju-related methods out of helpers and into a new package python-shell-toolbox in the ~yellow PPA.

The install hook must now add the apt repository for the PPA, install the package, and ensure local and helpers are not imported until that is done since they depend on shelltoolbox. The other hooks can just import and use shelltoolbox normally since we can assume the install hook did its job.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Approved based on conversation with Brad on G+.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/buildbot-relation-changed'
--- hooks/buildbot-relation-changed 2012-02-13 12:26:29 +0000
+++ hooks/buildbot-relation-changed 2012-02-29 22:49:25 +0000
@@ -4,6 +4,7 @@
4# GNU Affero General Public License version 3 (see the file LICENSE).4# GNU Affero General Public License version 3 (see the file LICENSE).
55
6import os6import os
7from shelltoolbox import su
78
8from helpers import (9from helpers import (
9 log,10 log,
@@ -11,7 +12,6 @@
11 log_exit,12 log_exit,
12 relation_get,13 relation_get,
13 relation_set,14 relation_set,
14 su,
15 )15 )
16from local import (16from local import (
17 buildbot_reconfig,17 buildbot_reconfig,
1818
=== modified file 'hooks/config-changed'
--- hooks/config-changed 2012-02-23 21:50:41 +0000
+++ hooks/config-changed 2012-02-29 22:49:25 +0000
@@ -7,23 +7,24 @@
7import json7import json
8import os8import os
9import os.path9import os.path
10from shelltoolbox import (
11 apt_get_install,
12 command,
13 DictDiffer,
14 get_user_ids,
15 install_extra_repositories,
16 run,
17 su,
18 )
10import shutil19import shutil
11import subprocess20import subprocess
12import sys21import sys
1322
14from helpers import (23from helpers import (
15 apt_get_install,
16 cd,
17 command,
18 DictDiffer,
19 get_config,24 get_config,
20 get_user_ids,
21 install_extra_repository,
22 log,25 log,
23 log_entry,26 log_entry,
24 log_exit,27 log_exit,
25 run,
26 su,
27 )28 )
28from local import (29from local import (
29 buildbot_create,30 buildbot_create,
@@ -31,8 +32,6 @@
31 config_json,32 config_json,
32 fetch_history,33 fetch_history,
33 generate_string,34 generate_string,
34 get_bucket,
35 get_key,
36 get_wrapper_cfg_path,35 get_wrapper_cfg_path,
37 put_history,36 put_history,
38 slave_json,37 slave_json,
@@ -75,7 +74,12 @@
75 added_or_changed = diff.added_or_changed74 added_or_changed = diff.added_or_changed
7675
77 if extra_repo and 'extra-repository' in added_or_changed:76 if extra_repo and 'extra-repository' in added_or_changed:
78 install_extra_repository(extra_repo)77 try:
78 install_extra_repositories(extra_repo)
79 except subprocess.CalledProcessError as e:
80 log('Error adding repository: ' + extra_repo)
81 log(e)
82 raise
79 restart_required = True83 restart_required = True
80 if extra_pkgs and 'extra-packages' in added_or_changed:84 if extra_pkgs and 'extra-packages' in added_or_changed:
81 apt_get_install(85 apt_get_install(
8286
=== modified file 'hooks/helpers.py'
--- hooks/helpers.py 2012-02-14 17:00:53 +0000
+++ hooks/helpers.py 2012-02-29 22:49:25 +0000
@@ -5,37 +5,24 @@
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'apt_get_install',
9 'cd',
10 'command',
11 'DictDiffer',
12 'get_config',8 'get_config',
13 'get_user_ids',
14 'get_user_home',
15 'get_value_from_line',
16 'grep',
17 'install_extra_repository',
18 'log',9 'log',
19 'log_entry',10 'log_entry',
20 'log_exit',11 'log_exit',
21 'run',
22 'relation_get',12 'relation_get',
23 'relation_set',13 'relation_set',
24 'su',
25 'unit_info',14 'unit_info',
26 ]15 ]
2716
28from collections import namedtuple17from collections import namedtuple
29from contextlib import contextmanager
30import json18import json
31import operator19import operator
32import os20from shelltoolbox import (
33import pwd21 command,
34import re22 run,
35import subprocess23 script_name,
36import sys24 )
37import tempfile25import tempfile
38from textwrap import dedent
39import time26import time
40import urllib227import urllib2
41import yaml28import yaml
@@ -43,49 +30,15 @@
4330
44Env = namedtuple('Env', 'uid gid home')31Env = namedtuple('Env', 'uid gid home')
4532
46
47def run(*args):
48 """Run the command with the given arguments.
49
50 The first argument is the path to the command to run, subsequent arguments
51 are command-line arguments to be passed.
52 """
53 process = subprocess.Popen(args, stdout=subprocess.PIPE,
54 stderr=subprocess.PIPE, close_fds=True)
55 stdout, stderr = process.communicate()
56 if process.returncode:
57 raise subprocess.CalledProcessError(
58 process.returncode, repr(args), output=stdout+stderr)
59 return stdout
60
61
62def command(*base_args):
63 """Return a callable that will run the given command with any arguments.
64
65 The first argument is the path to the command to run, subsequent arguments
66 are command-line arguments to "bake into" the returned callable.
67
68 The callable runs the given executable and also takes arguments that will
69 be appeneded to the "baked in" arguments.
70
71 For example, this code will list a file named "foo" (if it exists):
72
73 ls_foo = command('/bin/ls', 'foo')
74 ls_foo()
75
76 While this invocation will list "foo" and "bar" (assuming they exist):
77
78 ls_foo('bar')
79 """
80 def callable_command(*args):
81 all_args = base_args + args
82 return run(*all_args)
83
84 return callable_command
85
86
87log = command('juju-log')33log = command('juju-log')
88apt_get_install = command('apt-get', 'install', '-y', '--force-yes')34
35
36def log_entry():
37 log("--> Entering {}".format(script_name()))
38
39
40def log_exit():
41 log("<-- Exiting {}".format(script_name()))
8942
9043
91def get_config():44def get_config():
@@ -93,16 +46,6 @@
93 return json.loads(config_get())46 return json.loads(config_get())
9447
9548
96def install_extra_repository(extra_repository):
97 try:
98 run('apt-add-repository', extra_repository)
99 run('apt-get', 'update')
100 except subprocess.CalledProcessError as e:
101 log('Error adding repository: ' + extra_repository)
102 log(e)
103 raise
104
105
106def relation_get(*args):49def relation_get(*args):
107 cmd = command('relation-get')50 cmd = command('relation-get')
108 return cmd(*args).strip()51 return cmd(*args).strip()
@@ -114,96 +57,6 @@
114 return cmd(*args)57 return cmd(*args)
11558
11659
117def grep(content, filename):
118 with open(filename) as f:
119 for line in f:
120 if re.match(content, line):
121 return line.strip()
122
123
124def get_value_from_line(line):
125 return line.split('=')[1].strip('"\' ')
126
127
128def script_name():
129 return os.path.basename(sys.argv[0])
130
131
132def log_entry():
133 log("<-- Entering {}".format(script_name()))
134
135
136def log_exit():
137 log("--> Exiting {}".format(script_name()))
138
139
140class DictDiffer:
141 """
142 Calculate the difference between two dictionaries as:
143 (1) items added
144 (2) items removed
145 (3) keys same in both but changed values
146 (4) keys same in both and unchanged values
147 """
148
149 # Based on answer by hughdbrown at:
150 # http://stackoverflow.com/questions/1165352
151
152 def __init__(self, current_dict, past_dict):
153 self.current_dict = current_dict
154 self.past_dict = past_dict
155 self.set_current = set(current_dict)
156 self.set_past = set(past_dict)
157 self.intersect = self.set_current.intersection(self.set_past)
158
159 @property
160 def added(self):
161 return self.set_current - self.intersect
162
163 @property
164 def removed(self):
165 return self.set_past - self.intersect
166
167 @property
168 def changed(self):
169 return set(key for key in self.intersect
170 if self.past_dict[key] != self.current_dict[key])
171 @property
172 def unchanged(self):
173 return set(key for key in self.intersect
174 if self.past_dict[key] == self.current_dict[key])
175 @property
176 def modified(self):
177 return self.current_dict != self.past_dict
178
179 @property
180 def added_or_changed(self):
181 return self.added.union(self.changed)
182
183 def _changes(self, keys):
184 new = {}
185 old = {}
186 for k in keys:
187 new[k] = self.current_dict.get(k)
188 old[k] = self.past_dict.get(k)
189 return "%s -> %s" % (old, new)
190
191 def __str__(self):
192 if self.modified:
193 s = dedent("""\
194 added: %s
195 removed: %s
196 changed: %s
197 unchanged: %s""") % (
198 self._changes(self.added),
199 self._changes(self.removed),
200 self._changes(self.changed),
201 list(self.unchanged))
202 else:
203 s = "no changes"
204 return s
205
206
207def make_charm_config_file(charm_config):60def make_charm_config_file(charm_config):
208 charm_config_file = tempfile.NamedTemporaryFile()61 charm_config_file = tempfile.NamedTemporaryFile()
209 charm_config_file.write(yaml.dump(charm_config))62 charm_config_file.write(yaml.dump(charm_config))
@@ -312,80 +165,3 @@
312 if time.time() - start_time >= timeout:165 if time.time() - start_time >= timeout:
313 raise RuntimeError('timeout waiting for contents of ' + url)166 raise RuntimeError('timeout waiting for contents of ' + url)
314 time.sleep(0.1)167 time.sleep(0.1)
315
316
317class Serializer:
318 """Handle JSON (de)serialization."""
319
320 def __init__(self, path, default=None, serialize=None, deserialize=None):
321 self.path = path
322 self.default = default or {}
323 self.serialize = serialize or json.dump
324 self.deserialize = deserialize or json.load
325
326 def exists(self):
327 return os.path.exists(self.path)
328
329 def get(self):
330 if self.exists():
331 with open(self.path) as f:
332 return self.deserialize(f)
333 return self.default
334
335 def set(self, data):
336 with open(self.path, 'w') as f:
337 self.serialize(data, f)
338
339
340@contextmanager
341def cd(directory):
342 """A context manager to temporary change current working dir, e.g.::
343
344 >>> import os
345 >>> os.chdir('/tmp')
346 >>> with cd('/bin'): print os.getcwd()
347 /bin
348 >>> os.getcwd()
349 '/tmp'
350 """
351 cwd = os.getcwd()
352 os.chdir(directory)
353 yield
354 os.chdir(cwd)
355
356
357def get_user_ids(user):
358 """Return the uid and gid of given `user`, e.g.::
359
360 >>> get_user_ids('root')
361 (0, 0)
362 """
363 userdata = pwd.getpwnam(user)
364 return userdata.pw_uid, userdata.pw_gid
365
366
367def get_user_home(user):
368 """Return the home directory of the given `user`.
369
370 >>> get_user_home('root')
371 '/root'
372 """
373 return pwd.getpwnam(user).pw_dir
374
375
376@contextmanager
377def su(user):
378 """A context manager to temporary run the script as a different user."""
379 uid, gid = get_user_ids(user)
380 os.setegid(gid)
381 os.seteuid(uid)
382 current_home = os.getenv('HOME')
383 home = get_user_home(user)
384 os.environ['HOME'] = home
385 try:
386 yield Env(uid, gid, home)
387 finally:
388 os.setegid(os.getgid())
389 os.seteuid(os.getuid())
390 if current_home is not None:
391 os.environ['HOME'] = current_home
392168
=== modified file 'hooks/install'
--- hooks/install 2012-02-22 19:42:50 +0000
+++ hooks/install 2012-02-29 22:49:25 +0000
@@ -5,15 +5,75 @@
55
6import os6import os
7import shutil7import shutil
8from subprocess import CalledProcessError8import subprocess
9
10
11def run(*args):
12 """Run the command with the given arguments.
13
14 The first argument is the path to the command to run, subsequent arguments
15 are command-line arguments to be passed.
16 """
17 process = subprocess.Popen(args, stdout=subprocess.PIPE,
18 stderr=subprocess.PIPE, close_fds=True)
19 stdout, stderr = process.communicate()
20 if process.returncode:
21 raise subprocess.CalledProcessError(
22 process.returncode, repr(args), output=stdout+stderr)
23 return stdout
24
25
26def command(*base_args):
27 """Return a callable that will run the given command with any arguments.
28
29 The first argument is the path to the command to run, subsequent arguments
30 are command-line arguments to "bake into" the returned callable.
31
32 The callable runs the given executable and also takes arguments that will
33 be appeneded to the "baked in" arguments.
34
35 For example, this code will list a file named "foo" (if it exists):
36
37 ls_foo = command('/bin/ls', 'foo')
38 ls_foo()
39
40 While this invocation will list "foo" and "bar" (assuming they exist):
41
42 ls_foo('bar')
43 """
44 def callable_command(*args):
45 all_args = base_args + args
46 return run(*all_args)
47
48 return callable_command
49
50
51log = command('juju-log')
52
53
54def install_extra_repository(extra_repository):
55 try:
56 run('apt-add-repository', extra_repository)
57 run('apt-get', 'update')
58 except subprocess.CalledProcessError as e:
59 log('Error adding repository: ' + extra_repository)
60 log(e)
61 raise
62
63
64def install_packages():
65 apt_get_install = command('apt-get', 'install', '-y', '--force-yes')
66 apt_get_install('bzr', 'python-boto')
67 install_extra_repository('ppa:yellow/ppa')
68 apt_get_install('python-shell-toolbox')
69
70
71install_packages()
972
10from helpers import (73from helpers import (
11 apt_get_install,
12 get_config,74 get_config,
13 log,
14 log_entry,75 log_entry,
15 log_exit,76 log_exit,
16 run,
17 )77 )
18from local import (78from local import (
19 config_json,79 config_json,
@@ -22,13 +82,12 @@
2282
2383
24def cleanup(buildbot_dir):84def cleanup(buildbot_dir):
25 apt_get_install('bzr', 'python-boto')
26 # Since we may be installing into a pre-existing service, ensure the85 # Since we may be installing into a pre-existing service, ensure the
27 # buildbot directory is removed.86 # buildbot directory is removed.
28 if os.path.exists(buildbot_dir):87 if os.path.exists(buildbot_dir):
29 try:88 try:
30 run('buildbot', 'stop', buildbot_dir)89 run('buildbot', 'stop', buildbot_dir)
31 except (CalledProcessError, OSError):90 except (subprocess.CalledProcessError, OSError):
32 # This usually happens because buildbot hasn't been91 # This usually happens because buildbot hasn't been
33 # installed yet, or that it wasn't running; just92 # installed yet, or that it wasn't running; just
34 # ignore the error.93 # ignore the error.
@@ -52,6 +111,7 @@
52111
53112
54if __name__ == '__main__':113if __name__ == '__main__':
114
55 log_entry()115 log_entry()
56 try:116 try:
57 main()117 main()
58118
=== modified file 'hooks/local.py'
--- hooks/local.py 2012-02-23 21:50:41 +0000
+++ hooks/local.py 2012-02-29 22:49:25 +0000
@@ -26,14 +26,16 @@
26import subprocess26import subprocess
27import uuid27import uuid
2828
29from helpers import (29from shelltoolbox import (
30 cd,30 cd,
31 get_config,
32 log,
33 run,31 run,
34 Serializer,32 Serializer,
35 su,33 su,
36 )34 )
35from helpers import (
36 get_config,
37 log,
38 )
3739
3840
39HTTP_PORT_PROTOCOL = '8010/TCP'41HTTP_PORT_PROTOCOL = '8010/TCP'
4042
=== modified file 'hooks/start'
--- hooks/start 2012-02-22 19:42:50 +0000
+++ hooks/start 2012-02-29 22:49:25 +0000
@@ -3,10 +3,11 @@
3# Copyright 2012 Canonical Ltd. This software is licensed under the3# Copyright 2012 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).4# GNU Affero General Public License version 3 (see the file LICENSE).
55
6from shelltoolbox import run
7
6from helpers import (8from helpers import (
7 log_entry,9 log_entry,
8 log_exit,10 log_exit,
9 run,
10 )11 )
11from local import HTTP_PORT_PROTOCOL12from local import HTTP_PORT_PROTOCOL
1213
1314
=== modified file 'hooks/stop'
--- hooks/stop 2012-02-22 19:42:50 +0000
+++ hooks/stop 2012-02-29 22:49:25 +0000
@@ -3,12 +3,13 @@
3# Copyright 2012 Canonical Ltd. This software is licensed under the3# Copyright 2012 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).4# GNU Affero General Public License version 3 (see the file LICENSE).
55
6from shelltoolbox import run
7
6from helpers import (8from helpers import (
7 get_config,9 get_config
8 log,10 log,
9 log_entry,11 log_entry,
10 log_exit,12 log_exit,
11 run,
12 )13 )
13from local import (14from local import (
14 HTTP_PORT_PROTOCOL,15 HTTP_PORT_PROTOCOL,
1516
=== modified file 'hooks/tests.py'
--- hooks/tests.py 2012-02-23 16:45:54 +0000
+++ hooks/tests.py 2012-02-29 22:49:25 +0000
@@ -12,14 +12,14 @@
12import tempfile12import tempfile
13import unittest13import unittest
1414
15from helpers import (15from shelltoolbox import (
16 cd,16 cd,
17 command,17 command,
18 DictDiffer,18 DictDiffer,
19 run,19 run,
20 su,20 su,
21 unit_info,
22 )21 )
22from helpers import unit_info
23from local import (23from local import (
24 get_bucket,24 get_bucket,
25 get_key,25 get_key,
@@ -28,95 +28,6 @@
28 )28 )
2929
3030
31class TestRun(unittest.TestCase):
32
33 def testSimpleCommand(self):
34 # Running a simple command (ls) works and running the command
35 # produces a string.
36 self.assertIsInstance(run('/bin/ls'), str)
37
38 def testStdoutReturned(self):
39 # Running a simple command (ls) works and running the command
40 # produces a string.
41 self.assertIn('Usage:', run('/bin/ls', '--help'))
42
43 def testCalledProcessErrorRaised(self):
44 # If an error occurs a CalledProcessError is raised with the return
45 # code, command executed, and the output of the command.
46 with self.assertRaises(CalledProcessError) as info:
47 run('ls', '--not a valid switch')
48 exception = info.exception
49 self.assertEqual(2, exception.returncode)
50 self.assertEqual("('ls', '--not a valid switch')", exception.cmd)
51 self.assertIn('unrecognized option', exception.output)
52
53
54class TestCommand(unittest.TestCase):
55
56 def testSimpleCommand(self):
57 # Creating a simple command (ls) works and running the command
58 # produces a string.
59 ls = command('/bin/ls')
60 self.assertIsInstance(ls(), str)
61
62 def testArguments(self):
63 # Arguments can be passed to commands.
64 ls = command('/bin/ls')
65 self.assertIn('Usage:', ls('--help'))
66
67 def testMissingExecutable(self):
68 # If the command does not exist, an OSError (No such file or
69 # directory) is raised.
70 bad = command('this command does not exist')
71 with self.assertRaises(OSError) as info:
72 bad()
73 self.assertEqual(2, info.exception.errno)
74
75 def testError(self):
76 # If the command returns a non-zero exit code, an exception is raised.
77 ls = command('/bin/ls')
78 with self.assertRaises(CalledProcessError):
79 ls('--not a valid switch')
80
81 def testBakedInArguments(self):
82 # Arguments can be passed when creating the command as well as when
83 # executing it.
84 ll = command('/bin/ls', '-l')
85 self.assertIn('rw', ll()) # Assumes a file is r/w in the pwd.
86 self.assertIn('Usage:', ll('--help'))
87
88 def testQuoting(self):
89 # There is no need to quote special shell characters in commands.
90 ls = command('/bin/ls')
91 ls('--help', '>')
92
93
94class TestDictDiffer(unittest.TestCase):
95
96 def testStr(self):
97 a = dict(cow='moo', pig='oink')
98 b = dict(cow='moo', pig='oinkoink', horse='nay')
99 diff = DictDiffer(b, a)
100 s = str(diff)
101 self.assertIn("added: {'horse': None} -> {'horse': 'nay'}", s)
102 self.assertIn("removed: {} -> {}", s)
103 self.assertIn("changed: {'pig': 'oink'} -> {'pig': 'oinkoink'}", s)
104 self.assertIn("unchanged: ['cow']", s)
105
106 def testStrUnmodified(self):
107 a = dict(cow='moo', pig='oink')
108 diff = DictDiffer(a, a)
109 s = str(diff)
110 self.assertEquals('no changes', s)
111
112 def testAddedOrChanged(self):
113 a = dict(cow='moo', pig='oink')
114 b = dict(cow='moo', pig='oinkoink', horse='nay')
115 diff = DictDiffer(b, a)
116 expected = set(['horse', 'pig'])
117 self.assertEquals(expected, diff.added_or_changed)
118
119
120class TestUnit_info(unittest.TestCase):31class TestUnit_info(unittest.TestCase):
12132
122 def make_data(self, state='started'):33 def make_data(self, state='started'):
@@ -153,83 +64,6 @@
153 self.assertNotEqual('started', state)64 self.assertNotEqual('started', state)
15465
15566
156current_euid = os.geteuid()
157current_egid = os.getegid()
158current_home = os.environ['HOME']
159example_euid = current_euid + 1
160example_egid = current_egid + 1
161example_home = '/var/lib/example'
162userinfo = {'example_user': dict(
163 ids=(example_euid, example_egid), home=example_home)}
164effective_values = dict(uid=current_euid, gid=current_egid)
165
166
167def stub_os_seteuid(value):
168 effective_values['uid'] = value
169
170
171def stub_os_setegid(value):
172 effective_values['gid'] = value
173
174
175class TestSuContextManager(unittest.TestCase):
176
177 def setUp(self):
178 import helpers
179 self.os_seteuid = os.seteuid
180 self.os_setegid = os.setegid
181 self.helpers_get_user_ids = helpers.get_user_ids
182 self.helpers_get_user_home = helpers.get_user_home
183 os.seteuid = stub_os_seteuid
184 os.setegid = stub_os_setegid
185 helpers.get_user_ids = lambda user: userinfo[user]['ids']
186 helpers.get_user_home = lambda user: userinfo[user]['home']
187
188 def tearDown(self):
189 import helpers
190 os.seteuid = self.os_seteuid
191 os.setegid = self.os_setegid
192 helpers.get_user_ids = self.helpers_get_user_ids
193 helpers.get_user_home = self.helpers_get_user_home
194
195 def testChange(self):
196 with su('example_user'):
197 self.assertEqual(example_euid, effective_values['uid'])
198 self.assertEqual(example_egid, effective_values['gid'])
199 self.assertEqual(example_home, os.environ['HOME'])
200
201 def testEnvironment(self):
202 with su('example_user') as e:
203 self.assertEqual(example_euid, e.uid)
204 self.assertEqual(example_egid, e.gid)
205 self.assertEqual(example_home, e.home)
206
207 def testRevert(self):
208 with su('example_user'):
209 pass
210 self.assertEqual(current_euid, effective_values['uid'])
211 self.assertEqual(current_egid, effective_values['gid'])
212 self.assertEqual(current_home, os.environ['HOME'])
213
214 def testRevertAfterFailure(self):
215 try:
216 with su('example_user'):
217 raise RuntimeError()
218 except RuntimeError:
219 self.assertEqual(current_euid, effective_values['uid'])
220 self.assertEqual(current_egid, effective_values['gid'])
221 self.assertEqual(current_home, os.environ['HOME'])
222
223
224class TestCdContextManager(unittest.TestCase):
225 def test_cd(self):
226 curdir = os.getcwd()
227 self.assertNotEqual('/var', curdir)
228 with cd('/var'):
229 self.assertEqual('/var', os.getcwd())
230 self.assertEqual(curdir, os.getcwd())
231
232
233class StubBucket:67class StubBucket:
234 """Stub S3 Bucket."""68 """Stub S3 Bucket."""
235 def __init__(self, name):69 def __init__(self, name):

Subscribers

People subscribed via source and target branches

to all changes: