Merge lp:~jaypipes/glance/testing-overhaul into lp:~hudson-openstack/glance/trunk
- testing-overhaul
- Merge into trunk
Proposed by
Jay Pipes
Status: | Merged |
---|---|
Approved by: | Jay Pipes |
Approved revision: | 14 |
Merged at revision: | 12 |
Proposed branch: | lp:~jaypipes/glance/testing-overhaul |
Merge into: | lp:~hudson-openstack/glance/trunk |
Diff against target: |
897 lines (+519/-129) 15 files modified
.bzrignore (+1/-0) glance/parallax/controllers.py (+0/-8) glance/teller/backends/__init__.py (+2/-15) glance/teller/backends/http.py (+0/-2) glance/teller/backends/swift.py (+7/-5) glance/teller/controllers.py (+5/-2) glance/teller/registries.py (+12/-28) run_tests.sh (+66/-0) tests/stubs.py (+149/-0) tests/unit/test_teller_api.py (+34/-10) tests/unit/test_teller_backends.py (+60/-59) tests/utils.py (+27/-0) tools/install_venv.py (+136/-0) tools/pip-requires (+16/-0) tools/with_venv.sh (+4/-0) |
To merge this branch: | bzr merge lp:~jaypipes/glance/testing-overhaul |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Rick Harris (community) | Approve | ||
Review via email: mp+38147@code.launchpad.net |
Commit message
Description of the change
This patch overhauls the testing in Glance:
* Adds Nova-like run_tests.sh and tools/* files to automatically run unit tests in a virtual environment. Just do ./run_tests.sh -V from project root. All required dependencies will be installed into a new virtual environment at .glance-venv.
* Adds proper mocking and stubouts using pymox. This removes the need for all the FakeParallaxAdapter and similar code. Unit tests now call stubs.stub_
To post a comment you must log in.
Revision history for this message
Jay Pipes (jaypipes) wrote : | # |
ya, see the IRC convo with mtaylor about unittest2. Wasn't using it's features yet, so I axed it.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2010-09-27 22:43:04 +0000 |
3 | +++ .bzrignore 2010-10-11 19:36:15 +0000 |
4 | @@ -1,2 +1,3 @@ |
5 | *.pyc |
6 | glance.egg-info |
7 | +glance.sqlite |
8 | |
9 | === modified file 'glance/parallax/controllers.py' |
10 | --- glance/parallax/controllers.py 2010-10-04 22:18:12 +0000 |
11 | +++ glance/parallax/controllers.py 2010-10-11 19:36:15 +0000 |
12 | @@ -97,11 +97,3 @@ |
13 | mapper.resource("image", "images", controller=ImageController(), |
14 | collection={'detail': 'GET'}) |
15 | super(API, self).__init__(mapper) |
16 | - |
17 | - |
18 | - |
19 | - |
20 | - |
21 | - |
22 | - |
23 | - |
24 | |
25 | === modified file 'glance/teller/backends/__init__.py' |
26 | --- glance/teller/backends/__init__.py 2010-10-04 22:18:12 +0000 |
27 | +++ glance/teller/backends/__init__.py 2010-10-11 19:36:15 +0000 |
28 | @@ -39,20 +39,9 @@ |
29 | CHUNKSIZE = 4096 |
30 | |
31 | |
32 | -class TestStrBackend(Backend): |
33 | - """ Backend used for testing """ |
34 | - |
35 | - @classmethod |
36 | - def get(cls, parsed_uri, expected_size): |
37 | - """ |
38 | - teststr://data |
39 | - """ |
40 | - yield parsed_uri.path |
41 | - |
42 | - |
43 | class FilesystemBackend(Backend): |
44 | @classmethod |
45 | - def get(cls, parsed_uri, expected_size, opener=lambda p: open(p, "b")): |
46 | + def get(cls, parsed_uri, expected_size, opener=lambda p: open(p, "rb")): |
47 | """ Filesystem-based backend |
48 | |
49 | file:///path/to/file.tar.gz.0 |
50 | @@ -72,8 +61,7 @@ |
51 | "file": FilesystemBackend, |
52 | "http": HTTPBackend, |
53 | "https": HTTPBackend, |
54 | - "swift": SwiftBackend, |
55 | - "teststr": TestStrBackend |
56 | + "swift": SwiftBackend |
57 | } |
58 | |
59 | parsed_uri = urlparse.urlparse(uri) |
60 | @@ -85,4 +73,3 @@ |
61 | raise UnsupportedBackend("No backend found for '%s'" % scheme) |
62 | |
63 | return backend.get(parsed_uri, **kwargs) |
64 | - |
65 | |
66 | === modified file 'glance/teller/backends/http.py' |
67 | --- glance/teller/backends/http.py 2010-10-04 22:18:12 +0000 |
68 | +++ glance/teller/backends/http.py 2010-10-11 19:36:15 +0000 |
69 | @@ -43,5 +43,3 @@ |
70 | return backends._file_iter(conn.getresponse(), cls.CHUNKSIZE) |
71 | finally: |
72 | conn.close() |
73 | - |
74 | - |
75 | |
76 | === modified file 'glance/teller/backends/swift.py' |
77 | --- glance/teller/backends/swift.py 2010-10-04 22:18:12 +0000 |
78 | +++ glance/teller/backends/swift.py 2010-10-11 19:36:15 +0000 |
79 | @@ -15,7 +15,6 @@ |
80 | # License for the specific language governing permissions and limitations |
81 | # under the License. |
82 | |
83 | -import cloudfiles |
84 | from glance.teller.backends import Backend, BackendException |
85 | |
86 | |
87 | @@ -39,6 +38,10 @@ |
88 | if conn_class: |
89 | pass # Use the provided conn_class |
90 | else: |
91 | + # Import cloudfiles here because stubout will replace this call |
92 | + # with a faked swift client in the unittests, avoiding import |
93 | + # errors if the test system does not have cloudfiles installed |
94 | + import cloudfiles |
95 | conn_class = cloudfiles |
96 | |
97 | swift_conn = conn_class.get_connection(username=user, api_key=api_key, |
98 | @@ -64,14 +67,15 @@ |
99 | 3) reassemble authurl |
100 | """ |
101 | path = parsed_uri.path.lstrip('//') |
102 | + netloc = parsed_uri.netloc |
103 | |
104 | try: |
105 | - creds, path = path.split('@') |
106 | + creds, netloc = netloc.split('@') |
107 | user, api_key = creds.split(':') |
108 | path_parts = path.split('/') |
109 | file = path_parts.pop() |
110 | container = path_parts.pop() |
111 | - except ValueError: |
112 | + except (ValueError, IndexError): |
113 | raise BackendException( |
114 | "Expected four values to unpack in: swift:%s. " |
115 | "Should have received something like: %s." |
116 | @@ -80,5 +84,3 @@ |
117 | authurl = "https://%s" % '/'.join(path_parts) |
118 | |
119 | return user, api_key, authurl, container, file |
120 | - |
121 | - |
122 | |
123 | === modified file 'glance/teller/controllers.py' |
124 | --- glance/teller/controllers.py 2010-10-04 22:18:12 +0000 |
125 | +++ glance/teller/controllers.py 2010-10-11 19:36:15 +0000 |
126 | @@ -18,8 +18,11 @@ |
127 | Teller Image controller |
128 | """ |
129 | |
130 | +import logging |
131 | + |
132 | import routes |
133 | from webob import exc, Response |
134 | + |
135 | from glance.common import wsgi |
136 | from glance.common import exception |
137 | from glance.parallax import db |
138 | @@ -50,7 +53,8 @@ |
139 | |
140 | try: |
141 | image = registries.lookup_by_registry(registry, uri) |
142 | - except registries.UnknownRegistryAdapter: |
143 | + logging.debug("Found image registry for URI: %s. Got: %s", uri, image) |
144 | + except registries.UnknownImageRegistry: |
145 | return exc.HTTPBadRequest(body="Unknown registry '%s'" % registry, |
146 | request=req, |
147 | content_type="text/plain") |
148 | @@ -98,4 +102,3 @@ |
149 | mapper.resource("image", "image", controller=ImageController(), |
150 | collection={'detail': 'GET'}) |
151 | super(API, self).__init__(mapper) |
152 | - |
153 | |
154 | === modified file 'glance/teller/registries.py' |
155 | --- glance/teller/registries.py 2010-10-04 22:18:12 +0000 |
156 | +++ glance/teller/registries.py 2010-10-11 19:36:15 +0000 |
157 | @@ -20,17 +20,17 @@ |
158 | import urlparse |
159 | |
160 | |
161 | -class RegistryAdapterException(Exception): |
162 | +class ImageRegistryException(Exception): |
163 | """ Base class for all RegistryAdapter exceptions """ |
164 | pass |
165 | |
166 | |
167 | -class UnknownRegistryAdapter(RegistryAdapterException): |
168 | +class UnknownImageRegistry(ImageRegistryException): |
169 | """ Raised if we don't recognize the requested Registry protocol """ |
170 | pass |
171 | |
172 | |
173 | -class RegistryAdapter(object): |
174 | +class ImageRegistry(object): |
175 | """ Base class for all image endpoints """ |
176 | |
177 | @classmethod |
178 | @@ -41,9 +41,9 @@ |
179 | raise NotImplementedError |
180 | |
181 | |
182 | -class ParallaxAdapter(RegistryAdapter): |
183 | +class Parallax(ImageRegistry): |
184 | """ |
185 | - ParallaxAdapter stuff |
186 | + Parallax stuff |
187 | """ |
188 | |
189 | @classmethod |
190 | @@ -59,7 +59,7 @@ |
191 | elif scheme == 'https': |
192 | conn_class = httplib.HTTPSConnection |
193 | else: |
194 | - raise RegistryAdapterException( |
195 | + raise ImageRegistryException( |
196 | "Unrecognized scheme '%s'" % scheme) |
197 | |
198 | conn = conn_class(parsed_uri.netloc) |
199 | @@ -74,40 +74,24 @@ |
200 | try: |
201 | return image_json["image"] |
202 | except KeyError: |
203 | - raise RegistryAdapterException("Missing 'image' key") |
204 | + raise ImageRegistryException("Missing 'image' key") |
205 | + except Exception: # gaierror |
206 | + return None |
207 | finally: |
208 | conn.close() |
209 | |
210 | |
211 | -class FakeParallaxAdapter(ParallaxAdapter): |
212 | - """ |
213 | - A Mock ParallaxAdapter returns a mocked response for any uri with |
214 | - one or more 'success' and None for everything else. |
215 | - """ |
216 | - |
217 | - @classmethod |
218 | - def lookup(cls, parsed_uri): |
219 | - if parsed_uri.netloc.count("success"): |
220 | - # A successful attempt |
221 | - files = [dict(location="teststr://chunk0", size=1235), |
222 | - dict(location="teststr://chunk1", size=12345)] |
223 | - |
224 | - return dict(files=files) |
225 | - |
226 | - |
227 | REGISTRY_ADAPTERS = { |
228 | - 'parallax': ParallaxAdapter, |
229 | - 'fake_parallax': FakeParallaxAdapter |
230 | + 'parallax': Parallax |
231 | } |
232 | |
233 | + |
234 | def lookup_by_registry(registry, image_uri): |
235 | """ Convenience function to lookup based on a registry protocol """ |
236 | try: |
237 | adapter = REGISTRY_ADAPTERS[registry] |
238 | except KeyError: |
239 | - raise UnknownRegistryAdapter("'%s' not found" % registry) |
240 | + raise UnknownImageRegistry("'%s' not found" % registry) |
241 | |
242 | parsed_uri = urlparse.urlparse(image_uri) |
243 | return adapter.lookup(parsed_uri) |
244 | - |
245 | - |
246 | |
247 | === added file 'run_tests.sh' |
248 | --- run_tests.sh 1970-01-01 00:00:00 +0000 |
249 | +++ run_tests.sh 2010-10-11 19:36:15 +0000 |
250 | @@ -0,0 +1,66 @@ |
251 | +#!/bin/bash |
252 | + |
253 | +function usage { |
254 | + echo "Usage: $0 [OPTION]..." |
255 | + echo "Run Glance's test suite(s)" |
256 | + echo "" |
257 | + echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" |
258 | + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" |
259 | + echo " -h, --help Print this usage message" |
260 | + echo "" |
261 | + echo "Note: with no options specified, the script will try to run the tests in a virtual environment," |
262 | + echo " If no virtualenv is found, the script will ask if you would like to create one. If you " |
263 | + echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." |
264 | + exit |
265 | +} |
266 | + |
267 | +function process_options { |
268 | + array=$1 |
269 | + elements=${#array[@]} |
270 | + for (( x=0;x<$elements;x++)); do |
271 | + process_option ${array[${x}]} |
272 | + done |
273 | +} |
274 | + |
275 | +function process_option { |
276 | + option=$1 |
277 | + case $option in |
278 | + -h|--help) usage;; |
279 | + -V|--virtual-env) let always_venv=1; let never_venv=0;; |
280 | + -N|--no-virtual-env) let always_venv=0; let never_venv=1;; |
281 | + esac |
282 | +} |
283 | + |
284 | +venv=.glance-venv |
285 | +with_venv=tools/with_venv.sh |
286 | +always_venv=0 |
287 | +never_venv=0 |
288 | +options=("$@") |
289 | + |
290 | +process_options $options |
291 | + |
292 | +if [ $never_venv -eq 1 ]; then |
293 | + # Just run the test suites in current environment |
294 | + python run_tests.py |
295 | + exit |
296 | +fi |
297 | + |
298 | +if [ -e ${venv} ]; then |
299 | + ${with_venv} nosetests |
300 | +else |
301 | + if [ $always_venv -eq 1 ]; then |
302 | + # Automatically install the virtualenv |
303 | + python tools/install_venv.py |
304 | + else |
305 | + echo -e "No virtual environment found...create one? (Y/n) \c" |
306 | + read use_ve |
307 | + if [ "x$use_ve" = "xY" ]; then |
308 | + # Install the virtualenv and run the test suite in it |
309 | + python tools/install_venv.py |
310 | + else |
311 | + nosetests |
312 | + exit |
313 | + fi |
314 | + fi |
315 | + ${with_venv} nosetests |
316 | +fi |
317 | |
318 | === added file 'tests/stubs.py' |
319 | --- tests/stubs.py 1970-01-01 00:00:00 +0000 |
320 | +++ tests/stubs.py 2010-10-11 19:36:15 +0000 |
321 | @@ -0,0 +1,149 @@ |
322 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
323 | + |
324 | +# Copyright 2010 OpenStack, LLC |
325 | +# All Rights Reserved. |
326 | +# |
327 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
328 | +# not use this file except in compliance with the License. You may obtain |
329 | +# a copy of the License at |
330 | +# |
331 | +# http://www.apache.org/licenses/LICENSE-2.0 |
332 | +# |
333 | +# Unless required by applicable law or agreed to in writing, software |
334 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
335 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
336 | +# License for the specific language governing permissions and limitations |
337 | +# under the License. |
338 | + |
339 | +"""Stubouts, mocks and fixtures for the test suite""" |
340 | + |
341 | +import httplib |
342 | +import StringIO |
343 | + |
344 | +import stubout |
345 | + |
346 | +import glance.teller.backends.swift |
347 | + |
348 | +def stub_out_http_backend(stubs): |
349 | + """Stubs out the httplib.HTTPRequest.getresponse to return |
350 | + faked-out data instead of grabbing actual contents of a resource |
351 | + |
352 | + The stubbed getresponse() returns an iterator over |
353 | + the data "I am a teapot, short and stout\n" |
354 | + |
355 | + :param stubs: Set of stubout stubs |
356 | + |
357 | + """ |
358 | + |
359 | + class FakeHTTPConnection(object): |
360 | + |
361 | + DATA = 'I am a teapot, short and stout\n' |
362 | + |
363 | + def getresponse(self): |
364 | + return StringIO.StringIO(self.DATA) |
365 | + |
366 | + def request(self, *_args, **_kwargs): |
367 | + pass |
368 | + |
369 | + fake_http_conn = FakeHTTPConnection() |
370 | + stubs.Set(httplib.HTTPConnection, 'request', |
371 | + fake_http_conn.request) |
372 | + stubs.Set(httplib.HTTPSConnection, 'request', |
373 | + fake_http_conn.request) |
374 | + stubs.Set(httplib.HTTPConnection, 'getresponse', |
375 | + fake_http_conn.getresponse) |
376 | + stubs.Set(httplib.HTTPSConnection, 'getresponse', |
377 | + fake_http_conn.getresponse) |
378 | + |
379 | + |
380 | +def stub_out_filesystem_backend(stubs): |
381 | + """Stubs out the Filesystem Teller service to return fake |
382 | + data from files. |
383 | + |
384 | + The stubbed service always yields the following fixture:: |
385 | + |
386 | + //chunk0 |
387 | + //chunk1 |
388 | + |
389 | + :param stubs: Set of stubout stubs |
390 | + |
391 | + """ |
392 | + class FakeFilesystemBackend(object): |
393 | + |
394 | + @classmethod |
395 | + def get(cls, parsed_uri, expected_size, conn_class=None): |
396 | + |
397 | + return StringIO.StringIO(parsed_uri.path) |
398 | + |
399 | + fake_filesystem_backend = FakeFilesystemBackend() |
400 | + stubs.Set(glance.teller.backends.FilesystemBackend, 'get', |
401 | + fake_filesystem_backend.get) |
402 | + |
403 | + |
404 | +def stub_out_swift_backend(stubs): |
405 | + """Stubs out the Swift Teller backend with fake data |
406 | + and calls. |
407 | + |
408 | + The stubbed swift backend provides back an iterator over |
409 | + the data "I am a teapot, short and stout\n" |
410 | + |
411 | + :param stubs: Set of stubout stubs |
412 | + |
413 | + """ |
414 | + class FakeSwiftAuth(object): |
415 | + pass |
416 | + class FakeSwiftConnection(object): |
417 | + pass |
418 | + |
419 | + class FakeSwiftBackend(object): |
420 | + |
421 | + CHUNK_SIZE = 2 |
422 | + DATA = 'I am a teapot, short and stout\n' |
423 | + |
424 | + @classmethod |
425 | + def get(cls, parsed_uri, expected_size, conn_class=None): |
426 | + SwiftBackend = glance.teller.backends.swift.SwiftBackend |
427 | + |
428 | + # raise BackendException if URI is bad. |
429 | + (user, api_key, authurl, container, file) = \ |
430 | + SwiftBackend.parse_swift_tokens(parsed_uri) |
431 | + |
432 | + def chunk_it(): |
433 | + for i in xrange(0, len(cls.DATA), cls.CHUNK_SIZE): |
434 | + yield cls.DATA[i:i+cls.CHUNK_SIZE] |
435 | + |
436 | + return chunk_it() |
437 | + |
438 | + fake_swift_backend = FakeSwiftBackend() |
439 | + stubs.Set(glance.teller.backends.swift.SwiftBackend, 'get', |
440 | + fake_swift_backend.get) |
441 | + |
442 | + |
443 | +def stub_out_parallax(stubs): |
444 | + """Stubs out the Parallax registry with fake data returns. |
445 | + |
446 | + The stubbed Parallax always returns the following fixture:: |
447 | + |
448 | + {'files': [ |
449 | + {'location': 'file:///chunk0', 'size': 12345}, |
450 | + {'location': 'file:///chunk1', 'size': 1235} |
451 | + ]} |
452 | + |
453 | + :param stubs: Set of stubout stubs |
454 | + |
455 | + """ |
456 | + class FakeParallax(object): |
457 | + |
458 | + DATA = \ |
459 | + {'files': [ |
460 | + {'location': 'file:///chunk0', 'size': 12345}, |
461 | + {'location': 'file:///chunk1', 'size': 1235} |
462 | + ]} |
463 | + |
464 | + @classmethod |
465 | + def lookup(cls, _parsed_uri): |
466 | + return cls.DATA |
467 | + |
468 | + fake_parallax_registry = FakeParallax() |
469 | + stubs.Set(glance.teller.registries.Parallax, 'lookup', |
470 | + fake_parallax_registry.lookup) |
471 | |
472 | === modified file 'tests/unit/test_teller_api.py' |
473 | --- tests/unit/test_teller_api.py 2010-10-01 23:07:46 +0000 |
474 | +++ tests/unit/test_teller_api.py 2010-10-11 19:36:15 +0000 |
475 | @@ -1,11 +1,38 @@ |
476 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
477 | + |
478 | +# Copyright 2010 OpenStack, LLC |
479 | +# All Rights Reserved. |
480 | +# |
481 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
482 | +# not use this file except in compliance with the License. You may obtain |
483 | +# a copy of the License at |
484 | +# |
485 | +# http://www.apache.org/licenses/LICENSE-2.0 |
486 | +# |
487 | +# Unless required by applicable law or agreed to in writing, software |
488 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
489 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
490 | +# License for the specific language governing permissions and limitations |
491 | +# under the License. |
492 | + |
493 | +import stubout |
494 | import unittest |
495 | from webob import Request, exc |
496 | + |
497 | from glance.teller import controllers |
498 | +from tests import stubs |
499 | + |
500 | |
501 | class TestImageController(unittest.TestCase): |
502 | def setUp(self): |
503 | + """Establish a clean test environment""" |
504 | + self.stubs = stubout.StubOutForTesting() |
505 | self.image_controller = controllers.ImageController() |
506 | |
507 | + def tearDown(self): |
508 | + """Clear the test environment""" |
509 | + self.stubs.UnsetAll() |
510 | + |
511 | def test_index_image_with_no_uri_should_raise_http_bad_request(self): |
512 | # uri must be specified |
513 | request = Request.blank("/image") |
514 | @@ -21,21 +48,18 @@ |
515 | |
516 | def test_index_image_where_image_exists_should_return_the_data(self): |
517 | # FIXME: need urllib.quote here? |
518 | - image_uri = "http://parallax-success/myacct/my-image" |
519 | + stubs.stub_out_parallax(self.stubs) |
520 | + stubs.stub_out_filesystem_backend(self.stubs) |
521 | + image_uri = "http://parallax/myacct/my-image" |
522 | request = self._make_request(image_uri) |
523 | response = self.image_controller.index(request) |
524 | - self.assertEqual("//chunk0//chunk1", response.body) |
525 | + self.assertEqual("/chunk0/chunk1", response.body) |
526 | |
527 | def test_index_image_where_image_doesnt_exist_should_raise_not_found(self): |
528 | - image_uri = "http://parallax-failure/myacct/does-not-exist" |
529 | + image_uri = "http://bad-parallax-uri/myacct/does-not-exist" |
530 | request = self._make_request(image_uri) |
531 | self.assertRaises(exc.HTTPNotFound, self.image_controller.index, |
532 | request) |
533 | |
534 | - def _make_request(self, image_uri, registry="fake_parallax"): |
535 | - return Request.blank( |
536 | - "/image?uri=%s®istry=%s" % (image_uri, registry)) |
537 | - |
538 | - |
539 | -if __name__ == "__main__": |
540 | - unittest.main() |
541 | + def _make_request(self, image_uri, registry="parallax"): |
542 | + return Request.blank("/image?uri=%s®istry=%s" % (image_uri, registry)) |
543 | |
544 | === modified file 'tests/unit/test_teller_backends.py' |
545 | --- tests/unit/test_teller_backends.py 2010-10-02 03:36:15 +0000 |
546 | +++ tests/unit/test_teller_backends.py 2010-10-11 19:36:15 +0000 |
547 | @@ -16,20 +16,29 @@ |
548 | # under the License. |
549 | |
550 | from StringIO import StringIO |
551 | + |
552 | +import stubout |
553 | import unittest |
554 | |
555 | -from cloudfiles import Connection |
556 | -from cloudfiles.authentication import MockAuthentication as Auth |
557 | - |
558 | -from swiftfakehttp import CustomHTTPConnection |
559 | +from glance.teller.backends.swift import SwiftBackend |
560 | from glance.teller.backends import Backend, BackendException, get_from_backend |
561 | - |
562 | - |
563 | -class TestBackends(unittest.TestCase): |
564 | +from tests import stubs |
565 | + |
566 | +Backend.CHUNKSIZE = 2 |
567 | + |
568 | +class TestBackend(unittest.TestCase): |
569 | def setUp(self): |
570 | - Backend.CHUNKSIZE = 2 |
571 | - |
572 | - def test_filesystem_get_from_backend(self): |
573 | + """Establish a clean test environment""" |
574 | + self.stubs = stubout.StubOutForTesting() |
575 | + |
576 | + def tearDown(self): |
577 | + """Clear the test environment""" |
578 | + self.stubs.UnsetAll() |
579 | + |
580 | + |
581 | +class TestFilesystemBackend(TestBackend): |
582 | + |
583 | + def test_get(self): |
584 | class FakeFile(object): |
585 | def __enter__(self, *args, **kwargs): |
586 | return StringIO('fakedata') |
587 | @@ -43,65 +52,57 @@ |
588 | chunks = [c for c in fetcher] |
589 | self.assertEqual(chunks, ["fa", "ke", "da", "ta"]) |
590 | |
591 | - def test_http_get_from_backend(self): |
592 | - class FakeHTTPConnection(object): |
593 | - def __init__(self, *args, **kwargs): |
594 | - pass |
595 | - def request(self, *args, **kwargs): |
596 | - pass |
597 | - def getresponse(self): |
598 | - return StringIO('fakedata') |
599 | - def close(self): |
600 | - pass |
601 | - |
602 | - fetcher = get_from_backend("http://netloc/path/to/file.tar.gz", |
603 | - expected_size=8, |
604 | - conn_class=FakeHTTPConnection) |
605 | - |
606 | - chunks = [c for c in fetcher] |
607 | - self.assertEqual(chunks, ["fa", "ke", "da", "ta"]) |
608 | - |
609 | - def test_swift_get_from_backend(self): |
610 | - class FakeSwift(object): |
611 | - def __init__(self, *args, **kwargs): |
612 | - pass |
613 | - |
614 | - @classmethod |
615 | - def get_connection(self, *args, **kwargs): |
616 | - auth = Auth("user", "password") |
617 | - conn = Connection(auth=auth) |
618 | - conn.connection = CustomHTTPConnection("localhost", 8000) |
619 | - return conn |
620 | + |
621 | +class TestHTTPBackend(TestBackend): |
622 | + |
623 | + def setUp(self): |
624 | + super(TestHTTPBackend, self).setUp() |
625 | + stubs.stub_out_http_backend(self.stubs) |
626 | + |
627 | + def test_http_get(self): |
628 | + url = "http://netloc/path/to/file.tar.gz" |
629 | + expected_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s', |
630 | + 'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n'] |
631 | + fetcher = get_from_backend(url, |
632 | + expected_size=8) |
633 | + |
634 | + chunks = [c for c in fetcher] |
635 | + self.assertEqual(chunks, expected_returns) |
636 | + |
637 | + def test_https_get(self): |
638 | + url = "https://netloc/path/to/file.tar.gz" |
639 | + expected_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s', |
640 | + 'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n'] |
641 | + fetcher = get_from_backend(url, |
642 | + expected_size=8) |
643 | + |
644 | + chunks = [c for c in fetcher] |
645 | + self.assertEqual(chunks, expected_returns) |
646 | + |
647 | + |
648 | +class TestSwiftBackend(TestBackend): |
649 | + |
650 | + def setUp(self): |
651 | + super(TestSwiftBackend, self).setUp() |
652 | + stubs.stub_out_swift_backend(self.stubs) |
653 | + |
654 | + def test_get(self): |
655 | |
656 | swift_uri = "swift://user:password@localhost/container1/file.tar.gz" |
657 | - swift_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s', |
658 | - 'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n'] |
659 | + expected_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s', |
660 | + 'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n'] |
661 | |
662 | fetcher = get_from_backend(swift_uri, |
663 | expected_size=21, |
664 | - conn_class=FakeSwift) |
665 | + conn_class=SwiftBackend) |
666 | |
667 | chunks = [c for c in fetcher] |
668 | |
669 | - self.assertEqual(chunks, swift_returns) |
670 | - |
671 | - def test_swift_get_from_backend_with_bad_uri(self): |
672 | - class FakeSwift(object): |
673 | - def __init__(self, *args, **kwargs): |
674 | - pass |
675 | - |
676 | - @classmethod |
677 | - def get_connection(self, *args, **kwargs): |
678 | - auth = Auth("user", "password") |
679 | - conn = Connection(auth=auth) |
680 | - conn.connection = CustomHTTPConnection("localhost", 8000) |
681 | - return conn |
682 | + self.assertEqual(chunks, expected_returns) |
683 | + |
684 | + def test_get_bad_uri(self): |
685 | |
686 | swift_url = "swift://localhost/container1/file.tar.gz" |
687 | |
688 | self.assertRaises(BackendException, get_from_backend, |
689 | swift_url, expected_size=21) |
690 | - |
691 | - |
692 | -if __name__ == "__main__": |
693 | - unittest.main() |
694 | |
695 | === added file 'tests/utils.py' |
696 | --- tests/utils.py 1970-01-01 00:00:00 +0000 |
697 | +++ tests/utils.py 2010-10-11 19:36:15 +0000 |
698 | @@ -0,0 +1,27 @@ |
699 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
700 | + |
701 | +# Copyright 2010 OpenStack, LLC |
702 | +# All Rights Reserved. |
703 | +# |
704 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
705 | +# not use this file except in compliance with the License. You may obtain |
706 | +# a copy of the License at |
707 | +# |
708 | +# http://www.apache.org/licenses/LICENSE-2.0 |
709 | +# |
710 | +# Unless required by applicable law or agreed to in writing, software |
711 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
712 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
713 | +# License for the specific language governing permissions and limitations |
714 | +# under the License. |
715 | + |
716 | +"""Common utilities used in testing""" |
717 | + |
718 | + |
719 | +def is_cloudfiles_available(): |
720 | + """Returns True if Swift/Cloudfiles is importable""" |
721 | + try: |
722 | + import cloudfiles |
723 | + return True |
724 | + except ImportError: |
725 | + return False |
726 | |
727 | === added directory 'tools' |
728 | === added file 'tools/install_venv.py' |
729 | --- tools/install_venv.py 1970-01-01 00:00:00 +0000 |
730 | +++ tools/install_venv.py 2010-10-11 19:36:15 +0000 |
731 | @@ -0,0 +1,136 @@ |
732 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
733 | + |
734 | +# Copyright 2010 United States Government as represented by the |
735 | +# Administrator of the National Aeronautics and Space Administration. |
736 | +# All Rights Reserved. |
737 | +# |
738 | +# Copyright 2010 OpenStack, LLC |
739 | +# |
740 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
741 | +# not use this file except in compliance with the License. You may obtain |
742 | +# a copy of the License at |
743 | +# |
744 | +# http://www.apache.org/licenses/LICENSE-2.0 |
745 | +# |
746 | +# Unless required by applicable law or agreed to in writing, software |
747 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
748 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
749 | +# License for the specific language governing permissions and limitations |
750 | +# under the License. |
751 | + |
752 | +""" |
753 | +Installation script for Glance's development virtualenv |
754 | +""" |
755 | + |
756 | +import os |
757 | +import subprocess |
758 | +import sys |
759 | + |
760 | + |
761 | +ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) |
762 | +VENV = os.path.join(ROOT, '.glance-venv') |
763 | +PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') |
764 | +TWISTED_NOVA='http://nova.openstack.org/Twisted-10.0.0Nova.tar.gz' |
765 | + |
766 | +def die(message, *args): |
767 | + print >>sys.stderr, message % args |
768 | + sys.exit(1) |
769 | + |
770 | + |
771 | +def run_command(cmd, redirect_output=True, check_exit_code=True): |
772 | + """ |
773 | + Runs a command in an out-of-process shell, returning the |
774 | + output of that command. Working directory is ROOT. |
775 | + """ |
776 | + if redirect_output: |
777 | + stdout = subprocess.PIPE |
778 | + else: |
779 | + stdout = None |
780 | + |
781 | + proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) |
782 | + output = proc.communicate()[0] |
783 | + if check_exit_code and proc.returncode != 0: |
784 | + die('Command "%s" failed.\n%s', ' '.join(cmd), output) |
785 | + return output |
786 | + |
787 | + |
788 | +HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'], check_exit_code=False).strip()) |
789 | +HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'], check_exit_code=False).strip()) |
790 | + |
791 | + |
792 | +def check_dependencies(): |
793 | + """Make sure virtualenv is in the path.""" |
794 | + |
795 | + if not HAS_VIRTUALENV: |
796 | + print 'not found.' |
797 | + # Try installing it via easy_install... |
798 | + if HAS_EASY_INSTALL: |
799 | + print 'Installing virtualenv via easy_install...', |
800 | + if not run_command(['which', 'easy_install']): |
801 | + die('ERROR: virtualenv not found.\n\nGlance development requires virtualenv,' |
802 | + ' please install it using your favorite package management tool') |
803 | + print 'done.' |
804 | + print 'done.' |
805 | + |
806 | + |
807 | +def create_virtualenv(venv=VENV): |
808 | + """Creates the virtual environment and installs PIP only into the |
809 | + virtual environment |
810 | + """ |
811 | + print 'Creating venv...', |
812 | + run_command(['virtualenv', '-q', '--no-site-packages', VENV]) |
813 | + print 'done.' |
814 | + print 'Installing pip in virtualenv...', |
815 | + if not run_command(['tools/with_venv.sh', 'easy_install', 'pip']).strip(): |
816 | + die("Failed to install pip.") |
817 | + print 'done.' |
818 | + |
819 | + |
820 | +def install_dependencies(venv=VENV): |
821 | + print 'Installing dependencies with pip (this can take a while)...' |
822 | + |
823 | + # Install greenlet by hand - just listing it in the requires file does not |
824 | + # get it in stalled in the right order |
825 | + run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, 'greenlet'], |
826 | + redirect_output=False) |
827 | + run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, '-r', PIP_REQUIRES], |
828 | + redirect_output=False) |
829 | + run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, TWISTED_NOVA], |
830 | + redirect_output=False) |
831 | + |
832 | + # Tell the virtual env how to "import glance" |
833 | + pthfile = os.path.join(venv, "lib", "python2.6", "site-packages", "glance.pth") |
834 | + f = open(pthfile, 'w') |
835 | + f.write("%s\n" % ROOT) |
836 | + |
837 | + |
838 | +def print_help(): |
839 | + help = """ |
840 | + Glance development environment setup is complete. |
841 | + |
842 | + Glance development uses virtualenv to track and manage Python dependencies |
843 | + while in development and testing. |
844 | + |
845 | + To activate the Glance virtualenv for the extent of your current shell session |
846 | + you can run: |
847 | + |
848 | + $ source .glance-venv/bin/activate |
849 | + |
850 | + Or, if you prefer, you can run commands in the virtualenv on a case by case |
851 | + basis by running: |
852 | + |
853 | + $ tools/with_venv.sh <your command> |
854 | + |
855 | + Also, make test will automatically use the virtualenv. |
856 | + """ |
857 | + print help |
858 | + |
859 | + |
860 | +def main(argv): |
861 | + check_dependencies() |
862 | + create_virtualenv() |
863 | + install_dependencies() |
864 | + print_help() |
865 | + |
866 | +if __name__ == '__main__': |
867 | + main(sys.argv) |
868 | |
869 | === added file 'tools/pip-requires' |
870 | --- tools/pip-requires 1970-01-01 00:00:00 +0000 |
871 | +++ tools/pip-requires 2010-10-11 19:36:15 +0000 |
872 | @@ -0,0 +1,16 @@ |
873 | +greenlet>=0.3.1 |
874 | +SQLAlchemy>=0.6.3 |
875 | +pep8==0.5.0 |
876 | +pylint==0.19 |
877 | +anyjson |
878 | +eventlet>=0.9.12 |
879 | +lockfile |
880 | +python-daemon==1.5.5 |
881 | +python-gflags>=1.3 |
882 | +routes |
883 | +webob |
884 | +wsgiref |
885 | +zope.interface |
886 | +nose |
887 | +mox==0.5.0 |
888 | +-f http://pymox.googlecode.com/files/mox-0.5.0.tar.gz |
889 | |
890 | === added file 'tools/with_venv.sh' |
891 | --- tools/with_venv.sh 1970-01-01 00:00:00 +0000 |
892 | +++ tools/with_venv.sh 2010-10-11 19:36:15 +0000 |
893 | @@ -0,0 +1,4 @@ |
894 | +#!/bin/bash |
895 | +TOOLS=`dirname $0` |
896 | +VENV=$TOOLS/../.glance-venv |
897 | +source $VENV/bin/activate && $@ |
Excellent work. I really like:
* Renaming the Adapter classes to something a little less verbose
* Axing the TestStrBackend :) That was a nasty hack.
* The use of `PyMox`. Was not familiar with that, will have to read up on it. Looks very clean.
* run-tests, obviously, very handy
I was going to add a small nit with the aliasing of unittest2 as unittest since unlike cString and cPickle they aren't entirely compatible API's. However, looks like you *just* removed it entirely, so we're good.