Merge lp:~jaypipes/glance/bug731304 into lp:~glance-coresec/glance/cactus-trunk
- bug731304
- Merge into cactus-trunk
Status: | Merged | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Jay Pipes | ||||||||||||||||||||
Approved revision: | 99 | ||||||||||||||||||||
Merged at revision: | 94 | ||||||||||||||||||||
Proposed branch: | lp:~jaypipes/glance/bug731304 | ||||||||||||||||||||
Merge into: | lp:~glance-coresec/glance/cactus-trunk | ||||||||||||||||||||
Diff against target: |
1329 lines (+896/-329) 11 files modified
MANIFEST.in (+1/-2) bin/glance (+1/-1) glance/server.py (+3/-1) glance/store/http.py (+1/-1) tests/functional/__init__.py (+225/-0) tests/functional/test_bin_glance.py (+152/-0) tests/functional/test_curl_api.py (+302/-0) tests/functional/test_logging.py (+79/-0) tests/functional/test_misc.py (+26/-315) tests/unit/test_utils.py (+67/-0) tests/utils.py (+39/-9) |
||||||||||||||||||||
To merge this branch: | bzr merge lp:~jaypipes/glance/bug731304 | ||||||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jay Pipes (community) | Approve | ||
Christopher MacGown (community) | Approve | ||
Ed Leafe (community) | Approve | ||
Rick Harris (community) | Approve | ||
Cory Wright | Pending | ||
Review via email: mp+53915@code.launchpad.net |
Commit message
Adds robust functional testing to Glance.
Description of the change
Adds robust functional testing to Glance.
- Add /tests/functional/*
- tests.functiona
any test that needs to execute against *actual* Glance
API and registry servers instead of stubbed out fakes.
- Adds functional test case that uses cURL to execute a
series of actions against the API server
- Adds functional test case that uses bin/glance to
execute a series of actions against the API server
Functional tests start the servers on unused ports, and the test runner configures the servers to use an image directory, log files, and pid files all contained in a test directory controlled by the FunctionalTest class. If a functional test fails, the user can check the test directory to see the log files that were written during the test. If the functional test passes, the test directory is destroyed.
Ed Leafe (ed-leafe) wrote : | # |
lgtm - great addition!
Christopher MacGown (0x44) wrote : | # |
Very nice, Jay.
OpenStack Infra (hudson-openstack) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
Jay Pipes (jaypipes) wrote : | # |
approving merge of trunk...seems the Glance tarmac job is hanging when running python setup.py test, but as usual, everything runs fine locally and I have no way of figuring out what's going on on the Jenkins box. :( grr.
Preview Diff
1 | === modified file 'MANIFEST.in' |
2 | --- MANIFEST.in 2011-02-21 22:11:27 +0000 |
3 | +++ MANIFEST.in 2011-03-21 19:26:04 +0000 |
4 | @@ -1,9 +1,8 @@ |
5 | -include LICENSE run_tests.sh ChangeLog |
6 | +include run_tests.sh |
7 | include README builddeb.sh |
8 | include MANIFEST.in pylintrc |
9 | include tests/__init__.py |
10 | include tests/stubs.py |
11 | -include tests/test_data.py |
12 | include tests/utils.py |
13 | include run_tests.py |
14 | include glance/registry/db/migrate_repo/migrate.cfg |
15 | |
16 | === modified file 'bin/glance' |
17 | --- bin/glance 2011-03-06 17:14:59 +0000 |
18 | +++ bin/glance 2011-03-21 19:26:04 +0000 |
19 | @@ -270,7 +270,7 @@ |
20 | |
21 | # Have to handle "boolean" values specially... |
22 | if 'is_public' in fields: |
23 | - image_meta['is_public'] = utils.int_from_bool_as_string( |
24 | + image_meta['is_public'] = utils.bool_from_string( |
25 | fields.pop('is_public')) |
26 | |
27 | # Add custom attributes, which are all the arguments remaining |
28 | |
29 | === modified file 'glance/server.py' |
30 | --- glance/server.py 2011-03-09 00:07:44 +0000 |
31 | +++ glance/server.py 2011-03-21 19:26:04 +0000 |
32 | @@ -163,7 +163,9 @@ |
33 | yield chunk |
34 | |
35 | res = Response(app_iter=image_iterator(), |
36 | - content_type="text/plain") |
37 | + content_type="application/octet-stream") |
38 | + # Using app_iter blanks content-length, so we set it here... |
39 | + res.headers.add('Content-Length', image['size']) |
40 | utils.inject_image_meta_into_headers(res, image) |
41 | return req.get_response(res) |
42 | |
43 | |
44 | === modified file 'glance/store/http.py' |
45 | --- glance/store/http.py 2011-01-27 04:19:13 +0000 |
46 | +++ glance/store/http.py 2011-03-21 19:26:04 +0000 |
47 | @@ -24,7 +24,7 @@ |
48 | """ An implementation of the HTTP Backend Adapter """ |
49 | |
50 | @classmethod |
51 | - def get(cls, parsed_uri, expected_size, conn_class=None): |
52 | + def get(cls, parsed_uri, expected_size, options=None, conn_class=None): |
53 | """Takes a parsed uri for an HTTP resource, fetches it, and yields the |
54 | data. |
55 | """ |
56 | |
57 | === added directory 'tests/functional' |
58 | === added file 'tests/functional/__init__.py' |
59 | --- tests/functional/__init__.py 1970-01-01 00:00:00 +0000 |
60 | +++ tests/functional/__init__.py 2011-03-21 19:26:04 +0000 |
61 | @@ -0,0 +1,225 @@ |
62 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
63 | + |
64 | +# Copyright 2011 OpenStack, LLC |
65 | +# All Rights Reserved. |
66 | +# |
67 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
68 | +# not use this file except in compliance with the License. You may obtain |
69 | +# a copy of the License at |
70 | +# |
71 | +# http://www.apache.org/licenses/LICENSE-2.0 |
72 | +# |
73 | +# Unless required by applicable law or agreed to in writing, software |
74 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
75 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
76 | +# License for the specific language governing permissions and limitations |
77 | +# under the License. |
78 | + |
79 | +""" |
80 | +Base test class for running non-stubbed tests (functional tests) |
81 | + |
82 | +The FunctionalTest class contains helper methods for starting the API |
83 | +and Registry server, grabbing the logs of each, cleaning up pidfiles, |
84 | +and spinning down the servers. |
85 | +""" |
86 | + |
87 | +import datetime |
88 | +import os |
89 | +import random |
90 | +import shutil |
91 | +import signal |
92 | +import socket |
93 | +import tempfile |
94 | +import time |
95 | +import unittest |
96 | + |
97 | +from tests.utils import execute, get_unused_port |
98 | + |
99 | + |
100 | +class FunctionalTest(unittest.TestCase): |
101 | + |
102 | + """ |
103 | + Base test class for any test that wants to test the actual |
104 | + servers and clients and not just the stubbed out interfaces |
105 | + """ |
106 | + |
107 | + def setUp(self): |
108 | + |
109 | + self.test_id = random.randint(0, 100000) |
110 | + self.test_dir = os.path.join("/", "tmp", "test.%d" % self.test_id) |
111 | + |
112 | + self.api_port = get_unused_port() |
113 | + self.api_pid_file = os.path.join(self.test_dir, |
114 | + "glance-api.pid") |
115 | + self.api_log_file = os.path.join(self.test_dir, "apilog") |
116 | + |
117 | + self.registry_port = get_unused_port() |
118 | + self.registry_pid_file = ("/tmp/test.%d/glance-registry.pid" |
119 | + % self.test_id) |
120 | + self.registry_log_file = os.path.join(self.test_dir, "registrylog") |
121 | + |
122 | + self.image_dir = "/tmp/test.%d/images" % self.test_id |
123 | + |
124 | + self.sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', |
125 | + "sqlite://") |
126 | + self.pid_files = [self.api_pid_file, |
127 | + self.registry_pid_file] |
128 | + self.files_to_destroy = [] |
129 | + |
130 | + def tearDown(self): |
131 | + self.cleanup() |
132 | + |
133 | + def cleanup(self): |
134 | + """ |
135 | + Makes sure anything we created or started up in the |
136 | + tests are destroyed or spun down |
137 | + """ |
138 | + |
139 | + for pid_file in self.pid_files: |
140 | + if os.path.exists(pid_file): |
141 | + pid = int(open(pid_file).read().strip()) |
142 | + try: |
143 | + os.killpg(pid, signal.SIGTERM) |
144 | + except: |
145 | + pass # Ignore if the process group is dead |
146 | + os.unlink(pid_file) |
147 | + |
148 | + for f in self.files_to_destroy: |
149 | + if os.path.exists(f): |
150 | + os.unlink(f) |
151 | + |
152 | + def start_servers(self, **kwargs): |
153 | + """ |
154 | + Starts the API and Registry servers (bin/glance-api and |
155 | + bin/glance-registry) on unused ports and returns a tuple |
156 | + of the (api_port, registry_port, conf_file_name). |
157 | + |
158 | + Any kwargs passed to this method will override the configuration |
159 | + value in the conf file used in starting the servers. |
160 | + """ |
161 | + self.cleanup() |
162 | + |
163 | + conf_override = self.__dict__.copy() |
164 | + if kwargs: |
165 | + conf_override.update(**kwargs) |
166 | + |
167 | + # A config file to use just for this test...we don't want |
168 | + # to trample on currently-running Glance servers, now do we? |
169 | + |
170 | + conf_file = tempfile.NamedTemporaryFile() |
171 | + conf_contents = """[DEFAULT] |
172 | +verbose = True |
173 | +debug = True |
174 | + |
175 | +[app:glance-api] |
176 | +paste.app_factory = glance.server:app_factory |
177 | +filesystem_store_datadir=%(image_dir)s |
178 | +default_store = file |
179 | +bind_host = 0.0.0.0 |
180 | +bind_port = %(api_port)s |
181 | +registry_host = 0.0.0.0 |
182 | +registry_port = %(registry_port)s |
183 | +log_file = %(api_log_file)s |
184 | + |
185 | +[app:glance-registry] |
186 | +paste.app_factory = glance.registry.server:app_factory |
187 | +bind_host = 0.0.0.0 |
188 | +bind_port = %(registry_port)s |
189 | +log_file = %(registry_log_file)s |
190 | +sql_connection = %(sql_connection)s |
191 | +sql_idle_timeout = 3600 |
192 | +""" % conf_override |
193 | + conf_file.write(conf_contents) |
194 | + conf_file.flush() |
195 | + self.conf_file_name = conf_file.name |
196 | + |
197 | + # Start up the API and default registry server |
198 | + cmd = ("./bin/glance-control api start " |
199 | + "%(conf_file_name)s --pid-file=%(api_pid_file)s" |
200 | + % self.__dict__) |
201 | + exitcode, out, err = execute(cmd) |
202 | + |
203 | + self.assertEqual(0, exitcode, |
204 | + "Failed to spin up the API server. " |
205 | + "Got: %s" % err) |
206 | + self.assertTrue("Starting glance-api with" in out) |
207 | + |
208 | + cmd = ("./bin/glance-control registry start " |
209 | + "%(conf_file_name)s --pid-file=%(registry_pid_file)s" |
210 | + % self.__dict__) |
211 | + exitcode, out, err = execute(cmd) |
212 | + |
213 | + self.assertEqual(0, exitcode, |
214 | + "Failed to spin up the Registry server. " |
215 | + "Got: %s" % err) |
216 | + self.assertTrue("Starting glance-registry with" in out) |
217 | + |
218 | + self.wait_for_servers() |
219 | + |
220 | + return self.api_port, self.registry_port, self.conf_file_name |
221 | + |
222 | + def ping_server(self, port): |
223 | + """ |
224 | + Simple ping on the port. If responsive, return True, else |
225 | + return False. |
226 | + |
227 | + :note We use raw sockets, not ping here, since ping uses ICMP and |
228 | + has no concept of ports... |
229 | + """ |
230 | + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
231 | + try: |
232 | + s.connect(("127.0.0.1", port)) |
233 | + s.close() |
234 | + return True |
235 | + except socket.error, e: |
236 | + return False |
237 | + |
238 | + def wait_for_servers(self, timeout=3): |
239 | + """ |
240 | + Tight loop, waiting for both API and registry server to be |
241 | + available on the ports. Returns when both are pingable. There |
242 | + is a timeout on waiting for the servers to come up. |
243 | + |
244 | + :param timeout: Optional, defaults to 3 |
245 | + """ |
246 | + now = datetime.datetime.now() |
247 | + timeout_time = now + datetime.timedelta(seconds=timeout) |
248 | + while (timeout_time > now): |
249 | + if self.ping_server(self.api_port) and\ |
250 | + self.ping_server(self.registry_port): |
251 | + return |
252 | + now = datetime.datetime.now() |
253 | + time.sleep(0.05) |
254 | + self.assertFalse(True, "Failed to start servers.") |
255 | + |
256 | + def stop_servers(self): |
257 | + """ |
258 | + Called to stop the started servers in a normal fashion. Note |
259 | + that cleanup() will stop the servers using a fairly draconian |
260 | + method of sending a SIGTERM signal to the servers. Here, we use |
261 | + the glance-control stop method to gracefully shut the server down. |
262 | + This method also asserts that the shutdown was clean, and so it |
263 | + is meant to be called during a normal test case sequence. |
264 | + """ |
265 | + |
266 | + # Spin down the API and default registry server |
267 | + cmd = ("./bin/glance-control api start " |
268 | + "%(conf_file_name)s --pid-file=%(api_pid_file)s" |
269 | + % self.__dict__) |
270 | + exitcode, out, err = execute(cmd) |
271 | + self.assertEqual(0, exitcode, |
272 | + "Failed to spin down the API server. " |
273 | + "Got: %s" % err) |
274 | + cmd = ("./bin/glance-control registry start " |
275 | + "%(conf_file_name)s --pid-file=%(registry_pid_file)s" |
276 | + % self.__dict__) |
277 | + exitcode, out, err = execute(cmd) |
278 | + self.assertEqual(0, exitcode, |
279 | + "Failed to spin down the Registry server. " |
280 | + "Got: %s" % err) |
281 | + |
282 | + # If all went well, then just remove the test directory. |
283 | + # We only want to check the logs and stuff if something |
284 | + # went wrong... |
285 | + if os.path.exists(self.test_dir): |
286 | + shutil.rmtree(self.test_dir) |
287 | |
288 | === added file 'tests/functional/test_bin_glance.py' |
289 | --- tests/functional/test_bin_glance.py 1970-01-01 00:00:00 +0000 |
290 | +++ tests/functional/test_bin_glance.py 2011-03-21 19:26:04 +0000 |
291 | @@ -0,0 +1,152 @@ |
292 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
293 | + |
294 | +# Copyright 2011 OpenStack, LLC |
295 | +# All Rights Reserved. |
296 | +# |
297 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
298 | +# not use this file except in compliance with the License. You may obtain |
299 | +# a copy of the License at |
300 | +# |
301 | +# http://www.apache.org/licenses/LICENSE-2.0 |
302 | +# |
303 | +# Unless required by applicable law or agreed to in writing, software |
304 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
305 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
306 | +# License for the specific language governing permissions and limitations |
307 | +# under the License. |
308 | + |
309 | +"""Functional test case that utilizes the bin/glance CLI tool""" |
310 | + |
311 | +import os |
312 | +import unittest |
313 | + |
314 | +from tests import functional |
315 | +from tests.utils import execute |
316 | + |
317 | + |
318 | +class TestBinGlance(functional.FunctionalTest): |
319 | + |
320 | + """Functional tests for the bin/glance CLI tool""" |
321 | + |
322 | + def test_add_list_delete_list(self): |
323 | + """ |
324 | + We test the following: |
325 | + |
326 | + 0. Verify no public images in index |
327 | + 1. Add a public image with a location attr |
328 | + and no image data |
329 | + 2. Check that image exists in index |
330 | + 3. Delete the image |
331 | + 4. Verify no longer in index |
332 | + """ |
333 | + |
334 | + self.cleanup() |
335 | + api_port, reg_port, conf_file = self.start_servers() |
336 | + |
337 | + # 0. Verify no public images |
338 | + cmd = "bin/glance --port=%d index" % api_port |
339 | + |
340 | + exitcode, out, err = execute(cmd) |
341 | + |
342 | + self.assertEqual(0, exitcode) |
343 | + self.assertEqual('No public images found.', out.strip()) |
344 | + |
345 | + # 1. Add public image |
346 | + cmd = "bin/glance --port=%d add is_public=True name=MyImage" % api_port |
347 | + |
348 | + exitcode, out, err = execute(cmd) |
349 | + |
350 | + self.assertEqual(0, exitcode) |
351 | + self.assertEqual('Added new image with ID: 1', out.strip()) |
352 | + |
353 | + # 2. Verify image added as public image |
354 | + cmd = "bin/glance --port=%d index" % api_port |
355 | + |
356 | + exitcode, out, err = execute(cmd) |
357 | + |
358 | + self.assertEqual(0, exitcode) |
359 | + lines = out.split("\n") |
360 | + first_line = lines[0] |
361 | + image_data_line = lines[3] |
362 | + self.assertEqual('Found 1 public images...', first_line) |
363 | + self.assertTrue('MyImage' in image_data_line) |
364 | + |
365 | + # 3. Delete the image |
366 | + cmd = "bin/glance --port=%d delete 1" % api_port |
367 | + |
368 | + exitcode, out, err = execute(cmd) |
369 | + |
370 | + self.assertEqual(0, exitcode) |
371 | + self.assertEqual('Deleted image 1', out.strip()) |
372 | + |
373 | + # 4. Verify no public images |
374 | + cmd = "bin/glance --port=%d index" % api_port |
375 | + |
376 | + exitcode, out, err = execute(cmd) |
377 | + |
378 | + self.assertEqual(0, exitcode) |
379 | + self.assertEqual('No public images found.', out.strip()) |
380 | + |
381 | + self.stop_servers() |
382 | + |
383 | + def test_add_list_update_list(self): |
384 | + """ |
385 | + Test for LP Bug #736295 |
386 | + We test the following: |
387 | + |
388 | + 0. Verify no public images in index |
389 | + 1. Add a NON-public image |
390 | + 2. Check that image does not appear in index |
391 | + 3. Update the image to be public |
392 | + 4. Check that image now appears in index |
393 | + """ |
394 | + |
395 | + self.cleanup() |
396 | + api_port, reg_port, conf_file = self.start_servers() |
397 | + |
398 | + # 0. Verify no public images |
399 | + cmd = "bin/glance --port=%d index" % api_port |
400 | + |
401 | + exitcode, out, err = execute(cmd) |
402 | + |
403 | + self.assertEqual(0, exitcode) |
404 | + self.assertEqual('No public images found.', out.strip()) |
405 | + |
406 | + # 1. Add public image |
407 | + cmd = "bin/glance --port=%d add name=MyImage" % api_port |
408 | + |
409 | + exitcode, out, err = execute(cmd) |
410 | + |
411 | + self.assertEqual(0, exitcode) |
412 | + self.assertEqual('Added new image with ID: 1', out.strip()) |
413 | + |
414 | + # 2. Verify image added as public image |
415 | + cmd = "bin/glance --port=%d index" % api_port |
416 | + |
417 | + exitcode, out, err = execute(cmd) |
418 | + |
419 | + self.assertEqual(0, exitcode) |
420 | + self.assertEqual('No public images found.', out.strip()) |
421 | + |
422 | + # 3. Update the image to make it public |
423 | + cmd = "bin/glance --port=%d update 1 is_public=True" % api_port |
424 | + |
425 | + exitcode, out, err = execute(cmd) |
426 | + |
427 | + self.assertEqual(0, exitcode) |
428 | + self.assertEqual('Updated image 1', out.strip()) |
429 | + |
430 | + # 4. Verify image 1 in list of public images |
431 | + cmd = "bin/glance --port=%d index" % api_port |
432 | + |
433 | + exitcode, out, err = execute(cmd) |
434 | + |
435 | + self.assertEqual(0, exitcode) |
436 | + lines = out.split("\n") |
437 | + first_line = lines[0] |
438 | + self.assertEqual('Found 1 public images...', first_line) |
439 | + |
440 | + image_data_line = lines[3] |
441 | + self.assertTrue('MyImage' in image_data_line) |
442 | + |
443 | + self.stop_servers() |
444 | |
445 | === added file 'tests/functional/test_curl_api.py' |
446 | --- tests/functional/test_curl_api.py 1970-01-01 00:00:00 +0000 |
447 | +++ tests/functional/test_curl_api.py 2011-03-21 19:26:04 +0000 |
448 | @@ -0,0 +1,302 @@ |
449 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
450 | + |
451 | +# Copyright 2011 OpenStack, LLC |
452 | +# All Rights Reserved. |
453 | +# |
454 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
455 | +# not use this file except in compliance with the License. You may obtain |
456 | +# a copy of the License at |
457 | +# |
458 | +# http://www.apache.org/licenses/LICENSE-2.0 |
459 | +# |
460 | +# Unless required by applicable law or agreed to in writing, software |
461 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
462 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
463 | +# License for the specific language governing permissions and limitations |
464 | +# under the License. |
465 | + |
466 | +"""Functional test case that utilizes cURL against the API server""" |
467 | + |
468 | +import json |
469 | +import os |
470 | +import unittest |
471 | + |
472 | +from tests import functional |
473 | +from tests.utils import execute |
474 | + |
475 | +FIVE_KB = 5 * 1024 |
476 | + |
477 | + |
478 | +class TestCurlApi(functional.FunctionalTest): |
479 | + |
480 | + """Functional tests using straight cURL against the API server""" |
481 | + |
482 | + def test_get_head_simple_post(self): |
483 | + """ |
484 | + We test the following sequential series of actions: |
485 | + |
486 | + 0. GET /images |
487 | + - Verify no public images |
488 | + 1. GET /images/detail |
489 | + - Verify no public images |
490 | + 2. HEAD /images/1 |
491 | + - Verify 404 returned |
492 | + 3. POST /images with public image named Image1 with a location |
493 | + attribute and no custom properties |
494 | + - Verify 201 returned |
495 | + 4. HEAD /images/1 |
496 | + - Verify HTTP headers have correct information we just added |
497 | + 5. GET /images/1 |
498 | + - Verify all information on image we just added is correct |
499 | + 6. GET /images |
500 | + - Verify the image we just added is returned |
501 | + 7. GET /images/detail |
502 | + - Verify the image we just added is returned |
503 | + 8. PUT /images/1 with custom properties of "distro" and "arch" |
504 | + - Verify 200 returned |
505 | + 9. GET /images/1 |
506 | + - Verify updated information about image was stored |
507 | + """ |
508 | + |
509 | + self.cleanup() |
510 | + api_port, reg_port, conf_file = self.start_servers() |
511 | + |
512 | + # 0. GET /images |
513 | + # Verify no public images |
514 | + cmd = "curl -g http://0.0.0.0:%d/images" % api_port |
515 | + |
516 | + exitcode, out, err = execute(cmd) |
517 | + |
518 | + self.assertEqual(0, exitcode) |
519 | + self.assertEqual('{"images": []}', out.strip()) |
520 | + |
521 | + # 1. GET /images/detail |
522 | + # Verify no public images |
523 | + cmd = "curl -g http://0.0.0.0:%d/images/detail" % api_port |
524 | + |
525 | + exitcode, out, err = execute(cmd) |
526 | + |
527 | + self.assertEqual(0, exitcode) |
528 | + self.assertEqual('{"images": []}', out.strip()) |
529 | + |
530 | + # 2. HEAD /images/1 |
531 | + # Verify 404 returned |
532 | + cmd = "curl -i -X HEAD http://0.0.0.0:%d/images/1" % api_port |
533 | + |
534 | + exitcode, out, err = execute(cmd) |
535 | + |
536 | + self.assertEqual(0, exitcode) |
537 | + |
538 | + lines = out.split("\r\n") |
539 | + status_line = lines[0] |
540 | + |
541 | + self.assertEqual("HTTP/1.1 404 Not Found", status_line) |
542 | + |
543 | + # 3. POST /images with public image named Image1 |
544 | + # attribute and no custom properties. Verify a 200 OK is returned |
545 | + image_data = "*" * FIVE_KB |
546 | + |
547 | + cmd = ("curl -i -X POST " |
548 | + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue |
549 | + "-H 'Content-Type: application/octet-stream' " |
550 | + "-H 'X-Image-Meta-Name: Image1' " |
551 | + "-H 'X-Image-Meta-Is-Public: True' " |
552 | + "--data-binary \"%s\" " |
553 | + "http://0.0.0.0:%d/images") % (image_data, api_port) |
554 | + |
555 | + exitcode, out, err = execute(cmd) |
556 | + self.assertEqual(0, exitcode) |
557 | + |
558 | + lines = out.split("\r\n") |
559 | + status_line = lines[0] |
560 | + |
561 | + self.assertEqual("HTTP/1.1 200 OK", status_line) |
562 | + |
563 | + # 4. HEAD /images |
564 | + # Verify image found now |
565 | + cmd = "curl -i -X HEAD http://0.0.0.0:%d/images/1" % api_port |
566 | + |
567 | + exitcode, out, err = execute(cmd) |
568 | + |
569 | + self.assertEqual(0, exitcode) |
570 | + |
571 | + lines = out.split("\r\n") |
572 | + status_line = lines[0] |
573 | + |
574 | + self.assertEqual("HTTP/1.1 200 OK", status_line) |
575 | + self.assertTrue("X-Image-Meta-Name: Image1" in out) |
576 | + |
577 | + # 5. GET /images/1 |
578 | + # Verify all information on image we just added is correct |
579 | + |
580 | + cmd = "curl -i -g http://0.0.0.0:%d/images/1" % api_port |
581 | + |
582 | + exitcode, out, err = execute(cmd) |
583 | + |
584 | + self.assertEqual(0, exitcode) |
585 | + |
586 | + lines = out.split("\r\n") |
587 | + |
588 | + self.assertEqual("HTTP/1.1 200 OK", lines.pop(0)) |
589 | + |
590 | + # Handle the headers |
591 | + image_headers = {} |
592 | + std_headers = {} |
593 | + other_lines = [] |
594 | + for line in lines: |
595 | + if line.strip() == '': |
596 | + continue |
597 | + if line.startswith("X-Image"): |
598 | + pieces = line.split(":") |
599 | + key = pieces[0].strip() |
600 | + value = ":".join(pieces[1:]).strip() |
601 | + image_headers[key] = value |
602 | + elif ':' in line: |
603 | + pieces = line.split(":") |
604 | + key = pieces[0].strip() |
605 | + value = ":".join(pieces[1:]).strip() |
606 | + std_headers[key] = value |
607 | + else: |
608 | + other_lines.append(line) |
609 | + |
610 | + expected_image_headers = { |
611 | + 'X-Image-Meta-Id': '1', |
612 | + 'X-Image-Meta-Name': 'Image1', |
613 | + 'X-Image-Meta-Is_public': 'True', |
614 | + 'X-Image-Meta-Status': 'active', |
615 | + 'X-Image-Meta-Disk_format': '', |
616 | + 'X-Image-Meta-Container_format': '', |
617 | + 'X-Image-Meta-Size': str(FIVE_KB), |
618 | + 'X-Image-Meta-Location': 'file://%s/1' % self.image_dir} |
619 | + |
620 | + expected_std_headers = { |
621 | + 'Content-Length': str(FIVE_KB), |
622 | + 'Content-Type': 'application/octet-stream'} |
623 | + |
624 | + for expected_key, expected_value in expected_image_headers.items(): |
625 | + self.assertTrue(expected_key in image_headers, |
626 | + "Failed to find key %s in image_headers" |
627 | + % expected_key) |
628 | + self.assertEqual(expected_value, image_headers[expected_key], |
629 | + "For key '%s' expected header value '%s'. Got '%s'" |
630 | + % (expected_key, |
631 | + expected_value, |
632 | + image_headers[expected_key])) |
633 | + |
634 | + for expected_key, expected_value in expected_std_headers.items(): |
635 | + self.assertTrue(expected_key in std_headers, |
636 | + "Failed to find key %s in std_headers" |
637 | + % expected_key) |
638 | + self.assertEqual(expected_value, std_headers[expected_key], |
639 | + "For key '%s' expected header value '%s'. Got '%s'" |
640 | + % (expected_key, |
641 | + expected_value, |
642 | + std_headers[expected_key])) |
643 | + |
644 | + # Now the image data... |
645 | + expected_image_data = "*" * FIVE_KB |
646 | + |
647 | + # Should only be a single "line" left, and |
648 | + # that's the image data |
649 | + self.assertEqual(1, len(other_lines)) |
650 | + self.assertEqual(expected_image_data, other_lines[0]) |
651 | + |
652 | + # 6. GET /images |
653 | + # Verify no public images |
654 | + cmd = "curl -g http://0.0.0.0:%d/images" % api_port |
655 | + |
656 | + exitcode, out, err = execute(cmd) |
657 | + |
658 | + self.assertEqual(0, exitcode) |
659 | + |
660 | + expected_result = {"images": [ |
661 | + {"container_format": None, |
662 | + "disk_format": None, |
663 | + "id": 1, |
664 | + "name": "Image1", |
665 | + "size": 5120}]} |
666 | + self.assertEqual(expected_result, json.loads(out.strip())) |
667 | + |
668 | + # 7. GET /images/detail |
669 | + # Verify image and all its metadata |
670 | + cmd = "curl -g http://0.0.0.0:%d/images/detail" % api_port |
671 | + |
672 | + exitcode, out, err = execute(cmd) |
673 | + |
674 | + self.assertEqual(0, exitcode) |
675 | + |
676 | + expected_image = { |
677 | + "status": "active", |
678 | + "name": "Image1", |
679 | + "deleted": False, |
680 | + "container_format": None, |
681 | + "disk_format": None, |
682 | + "id": 1, |
683 | + "location": "file://%s/1" % self.image_dir, |
684 | + "is_public": True, |
685 | + "deleted_at": None, |
686 | + "properties": {}, |
687 | + "size": 5120} |
688 | + |
689 | + image = json.loads(out.strip())['images'][0] |
690 | + |
691 | + for expected_key, expected_value in expected_image.items(): |
692 | + self.assertTrue(expected_key in image, |
693 | + "Failed to find key %s in image" |
694 | + % expected_key) |
695 | + self.assertEqual(expected_value, expected_image[expected_key], |
696 | + "For key '%s' expected header value '%s'. Got '%s'" |
697 | + % (expected_key, |
698 | + expected_value, |
699 | + image[expected_key])) |
700 | + |
701 | + # 8. PUT /images/1 with custom properties of "distro" and "arch" |
702 | + # Verify 200 returned |
703 | + |
704 | + cmd = ("curl -i -X PUT " |
705 | + "-H 'X-Image-Meta-Property-Distro: Ubuntu' " |
706 | + "-H 'X-Image-Meta-Property-Arch: x86_64' " |
707 | + "http://0.0.0.0:%d/images/1") % api_port |
708 | + |
709 | + exitcode, out, err = execute(cmd) |
710 | + self.assertEqual(0, exitcode) |
711 | + |
712 | + lines = out.split("\r\n") |
713 | + status_line = lines[0] |
714 | + |
715 | + self.assertEqual("HTTP/1.1 200 OK", status_line) |
716 | + |
717 | + # 9. GET /images/detail |
718 | + # Verify image and all its metadata |
719 | + cmd = "curl -g http://0.0.0.0:%d/images/detail" % api_port |
720 | + |
721 | + exitcode, out, err = execute(cmd) |
722 | + |
723 | + self.assertEqual(0, exitcode) |
724 | + |
725 | + expected_image = { |
726 | + "status": "active", |
727 | + "name": "Image1", |
728 | + "deleted": False, |
729 | + "container_format": None, |
730 | + "disk_format": None, |
731 | + "id": 1, |
732 | + "location": "file://%s/1" % self.image_dir, |
733 | + "is_public": True, |
734 | + "deleted_at": None, |
735 | + "properties": {'distro': 'Ubuntu', 'arch': 'x86_64'}, |
736 | + "size": 5120} |
737 | + |
738 | + image = json.loads(out.strip())['images'][0] |
739 | + |
740 | + for expected_key, expected_value in expected_image.items(): |
741 | + self.assertTrue(expected_key in image, |
742 | + "Failed to find key %s in image" |
743 | + % expected_key) |
744 | + self.assertEqual(expected_value, image[expected_key], |
745 | + "For key '%s' expected header value '%s'. Got '%s'" |
746 | + % (expected_key, |
747 | + expected_value, |
748 | + image[expected_key])) |
749 | + |
750 | + self.stop_servers() |
751 | |
752 | === added file 'tests/functional/test_logging.py' |
753 | --- tests/functional/test_logging.py 1970-01-01 00:00:00 +0000 |
754 | +++ tests/functional/test_logging.py 2011-03-21 19:26:04 +0000 |
755 | @@ -0,0 +1,79 @@ |
756 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
757 | + |
758 | +# Copyright 2011 OpenStack, LLC |
759 | +# All Rights Reserved. |
760 | +# |
761 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
762 | +# not use this file except in compliance with the License. You may obtain |
763 | +# a copy of the License at |
764 | +# |
765 | +# http://www.apache.org/licenses/LICENSE-2.0 |
766 | +# |
767 | +# Unless required by applicable law or agreed to in writing, software |
768 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
769 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
770 | +# License for the specific language governing permissions and limitations |
771 | +# under the License. |
772 | + |
773 | +import os |
774 | +import unittest |
775 | + |
776 | +from tests import functional |
777 | +from tests.utils import execute |
778 | + |
779 | + |
780 | +class TestLogging(functional.FunctionalTest): |
781 | + |
782 | + """Tests that logging can be configured correctly""" |
783 | + |
784 | + def test_logfile(self): |
785 | + """ |
786 | + A test that logging can be configured properly from the |
787 | + glance.conf file with the log_file option. |
788 | + |
789 | + We start both servers daemonized with a temporary config |
790 | + file that has some logging options in it. |
791 | + |
792 | + We then use curl to issue a few requests and verify that each server's |
793 | + logging statements were logged to the one log file |
794 | + """ |
795 | + self.cleanup() |
796 | + api_port, reg_port, conf_file = self.start_servers() |
797 | + |
798 | + cmd = "curl -X POST -H 'Content-Type: application/octet-stream' "\ |
799 | + "-H 'X-Image-Meta-Name: ImageName' "\ |
800 | + "-H 'X-Image-Meta-Disk-Format: Invalid' "\ |
801 | + "http://0.0.0.0:%d/images" % api_port |
802 | + ignored, out, err = execute(cmd) |
803 | + |
804 | + self.assertTrue('Invalid disk format' in out, |
805 | + "Could not find 'Invalid disk format' " |
806 | + "in output: %s" % out) |
807 | + |
808 | + self.assertTrue(os.path.exists(self.api_log_file), |
809 | + "API Logfile %s does not exist!" |
810 | + % self.api_log_file) |
811 | + self.assertTrue(os.path.exists(self.registry_log_file), |
812 | + "Registry Logfile %s does not exist!" |
813 | + % self.registry_log_file) |
814 | + |
815 | + api_logfile_contents = open(self.api_log_file, 'rb').read() |
816 | + registry_logfile_contents = open(self.registry_log_file, 'rb').read() |
817 | + |
818 | + # Check that BOTH the glance API and registry server |
819 | + # modules are logged to their respective logfiles. |
820 | + self.assertTrue('[glance.server]' |
821 | + in api_logfile_contents, |
822 | + "Could not find '[glance.server]' " |
823 | + "in API logfile: %s" % api_logfile_contents) |
824 | + self.assertTrue('[glance.registry.server]' |
825 | + in registry_logfile_contents, |
826 | + "Could not find '[glance.registry.server]' " |
827 | + "in Registry logfile: %s" % registry_logfile_contents) |
828 | + |
829 | + # Test that the error we caused above is in the log |
830 | + self.assertTrue('Invalid disk format' in api_logfile_contents, |
831 | + "Could not find 'Invalid disk format' " |
832 | + "in API logfile: %s" % api_logfile_contents) |
833 | + |
834 | + self.stop_servers() |
835 | |
836 | === renamed file 'tests/unit/test_misc.py' => 'tests/functional/test_misc.py' |
837 | --- tests/unit/test_misc.py 2011-03-16 16:11:56 +0000 |
838 | +++ tests/functional/test_misc.py 2011-03-21 19:26:04 +0000 |
839 | @@ -15,101 +15,16 @@ |
840 | # License for the specific language governing permissions and limitations |
841 | # under the License. |
842 | |
843 | -import os |
844 | -import shutil |
845 | -import signal |
846 | -import subprocess |
847 | -import tempfile |
848 | -import time |
849 | import unittest |
850 | |
851 | -from glance import utils |
852 | - |
853 | - |
854 | -def execute(cmd): |
855 | - env = os.environ.copy() |
856 | - # Make sure that we use the programs in the |
857 | - # current source directory's bin/ directory. |
858 | - env['PATH'] = os.path.join(os.getcwd(), 'bin') + ':' + env['PATH'] |
859 | - process = subprocess.Popen(cmd, |
860 | - shell=True, |
861 | - stdin=subprocess.PIPE, |
862 | - stdout=subprocess.PIPE, |
863 | - stderr=subprocess.PIPE, |
864 | - env=env) |
865 | - result = process.communicate() |
866 | - (out, err) = result |
867 | - exitcode = process.returncode |
868 | - if process.returncode != 0: |
869 | - msg = "Command %(cmd)s did not succeed. Returned an exit "\ |
870 | - "code of %(exitcode)d."\ |
871 | - "\n\nSTDOUT: %(out)s"\ |
872 | - "\n\nSTDERR: %(err)s" % locals() |
873 | - raise RuntimeError(msg) |
874 | - return exitcode, out, err |
875 | - |
876 | - |
877 | -class TestMiscellaneous(unittest.TestCase): |
878 | +from tests import functional |
879 | +from tests.utils import execute |
880 | + |
881 | + |
882 | +class TestMiscellaneous(functional.FunctionalTest): |
883 | |
884 | """Some random tests for various bugs and stuff""" |
885 | |
886 | - def tearDown(self): |
887 | - self._cleanup_test_servers() |
888 | - |
889 | - def _cleanup_test_servers(self): |
890 | - # Clean up any leftover test servers... |
891 | - pid_files = ('glance-api.pid', 'glance-registry.pid') |
892 | - for pid_file in pid_files: |
893 | - if os.path.exists(pid_file): |
894 | - pid = int(open(pid_file).read().strip()) |
895 | - try: |
896 | - os.killpg(pid, signal.SIGTERM) |
897 | - except: |
898 | - pass # Ignore if the process group is dead |
899 | - os.unlink(pid_file) |
900 | - |
901 | - def test_headers_are_unicode(self): |
902 | - """ |
903 | - Verifies that the headers returned by conversion code are unicode. |
904 | - |
905 | - Headers are passed via http in non-testing mode, which automatically |
906 | - converts them to unicode. Verifying that the method does the |
907 | - conversion proves that we aren't passing data that works in tests |
908 | - but will fail in production. |
909 | - """ |
910 | - fixture = {'name': 'fake public image', |
911 | - 'is_public': True, |
912 | - 'type': 'kernel', |
913 | - 'size': 19, |
914 | - 'location': "file:///tmp/glance-tests/2", |
915 | - 'properties': {'distro': 'Ubuntu 10.04 LTS'}} |
916 | - headers = utils.image_meta_to_http_headers(fixture) |
917 | - for k, v in headers.iteritems(): |
918 | - self.assert_(isinstance(v, unicode), "%s is not unicode" % v) |
919 | - |
920 | - def test_data_passed_properly_through_headers(self): |
921 | - """ |
922 | - Verifies that data is the same after being passed through headers |
923 | - """ |
924 | - fixture = {'name': 'fake public image', |
925 | - 'is_public': True, |
926 | - 'deleted': False, |
927 | - 'type': 'kernel', |
928 | - 'name': None, |
929 | - 'size': 19, |
930 | - 'location': "file:///tmp/glance-tests/2", |
931 | - 'properties': {'distro': 'Ubuntu 10.04 LTS'}} |
932 | - headers = utils.image_meta_to_http_headers(fixture) |
933 | - |
934 | - class FakeResponse(): |
935 | - pass |
936 | - |
937 | - response = FakeResponse() |
938 | - response.headers = headers |
939 | - result = utils.get_image_meta_from_headers(response) |
940 | - for k, v in fixture.iteritems(): |
941 | - self.assertEqual(v, result[k]) |
942 | - |
943 | def test_exception_not_eaten_from_registry_to_api(self): |
944 | """ |
945 | A test for LP bug #704854 -- Exception thrown by registry |
946 | @@ -125,228 +40,24 @@ |
947 | and verify that glance-upload doesn't eat the exception either... |
948 | """ |
949 | |
950 | - self._cleanup_test_servers() |
951 | - |
952 | - # Port numbers hopefully not used by anything... |
953 | - api_port = 32001 |
954 | - reg_port = 32000 |
955 | - image_dir = "/tmp/test.images.%d" % api_port |
956 | - sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', "sqlite://") |
957 | - if os.path.exists(image_dir): |
958 | - shutil.rmtree(image_dir) |
959 | - |
960 | - # A config file to use just for this test...we don't want |
961 | - # to trample on currently-running Glance servers, now do we? |
962 | - with tempfile.NamedTemporaryFile() as conf_file: |
963 | - conf_contents = """[DEFAULT] |
964 | -verbose = True |
965 | -debug = True |
966 | - |
967 | -[app:glance-api] |
968 | -paste.app_factory = glance.server:app_factory |
969 | -filesystem_store_datadir=%(image_dir)s |
970 | -default_store = file |
971 | -bind_host = 0.0.0.0 |
972 | -bind_port = %(api_port)s |
973 | -registry_host = 0.0.0.0 |
974 | -registry_port = %(reg_port)s |
975 | - |
976 | -[app:glance-registry] |
977 | -paste.app_factory = glance.registry.server:app_factory |
978 | -bind_host = 0.0.0.0 |
979 | -bind_port = %(reg_port)s |
980 | -sql_connection = %(sql_connection)s |
981 | -sql_idle_timeout = 3600 |
982 | -""" % locals() |
983 | - conf_file.write(conf_contents) |
984 | - conf_file.flush() |
985 | - conf_file_name = conf_file.name |
986 | - |
987 | - venv = "" |
988 | - if 'VIRTUAL_ENV' in os.environ: |
989 | - venv = "tools/with_venv.sh " |
990 | - |
991 | - # Start up the API and default registry server |
992 | - cmd = venv + "./bin/glance-control api start "\ |
993 | - "%s --pid-file=glance-api.pid" % conf_file_name |
994 | - exitcode, out, err = execute(cmd) |
995 | - |
996 | - self.assertEquals(0, exitcode) |
997 | - self.assertTrue("Starting glance-api with" in out) |
998 | - |
999 | - cmd = venv + "./bin/glance-control registry start "\ |
1000 | - "%s --pid-file=glance-registry.pid" % conf_file_name |
1001 | - exitcode, out, err = execute(cmd) |
1002 | - |
1003 | - self.assertEquals(0, exitcode) |
1004 | - self.assertTrue("Starting glance-registry with" in out) |
1005 | - |
1006 | - time.sleep(2) # Gotta give some time for spin up... |
1007 | - |
1008 | - cmd = "curl -g http://0.0.0.0:%d/images" % api_port |
1009 | - |
1010 | - exitcode, out, err = execute(cmd) |
1011 | - |
1012 | - self.assertEquals(0, exitcode) |
1013 | - self.assertEquals('{"images": []}', out.strip()) |
1014 | - |
1015 | - cmd = "curl -X POST -H 'Content-Type: application/octet-stream' "\ |
1016 | - "-H 'X-Image-Meta-Name: ImageName' "\ |
1017 | - "-H 'X-Image-Meta-Disk-Format: Invalid' "\ |
1018 | - "http://0.0.0.0:%d/images" % api_port |
1019 | - ignored, out, err = execute(cmd) |
1020 | - |
1021 | - self.assertTrue('Invalid disk format' in out, |
1022 | - "Could not find 'Invalid disk format' " |
1023 | - "in output: %s" % out) |
1024 | - |
1025 | - # Spin down the API and default registry server |
1026 | - cmd = "./bin/glance-control api stop "\ |
1027 | - "%s --pid-file=glance-api.pid" % conf_file_name |
1028 | - ignored, out, err = execute(cmd) |
1029 | - cmd = "./bin/glance-control registry stop "\ |
1030 | - "%s --pid-file=glance-registry.pid" % conf_file_name |
1031 | - ignored, out, err = execute(cmd) |
1032 | - |
1033 | - |
1034 | -# TODO(jaypipes): Move this to separate test file once |
1035 | -# LP Bug#731304 moves execute() out to a common file, etc |
1036 | -class TestLogging(unittest.TestCase): |
1037 | - |
1038 | - """Tests that logging can be configured correctly""" |
1039 | - |
1040 | - def setUp(self): |
1041 | - self.logfiles = [] |
1042 | - |
1043 | - def tearDown(self): |
1044 | - self._cleanup_test_servers() |
1045 | - self._cleanup_log_files() |
1046 | - |
1047 | - def _cleanup_test_servers(self): |
1048 | - # Clean up any leftover test servers... |
1049 | - pid_files = ('glance-api.pid', 'glance-registry.pid') |
1050 | - for pid_file in pid_files: |
1051 | - if os.path.exists(pid_file): |
1052 | - pid = int(open(pid_file).read().strip()) |
1053 | - try: |
1054 | - os.killpg(pid, signal.SIGTERM) |
1055 | - except: |
1056 | - pass # Ignore if the process group is dead |
1057 | - os.unlink(pid_file) |
1058 | - |
1059 | - def _cleanup_log_files(self): |
1060 | - for f in self.logfiles: |
1061 | - if os.path.exists(f): |
1062 | - os.unlink(f) |
1063 | - |
1064 | - def test_logfile(self): |
1065 | - """ |
1066 | - A test that logging can be configured properly from the |
1067 | - glance.conf file with the log_file option. |
1068 | - |
1069 | - We start both servers daemonized with a temporary config |
1070 | - file that has some logging options in it. |
1071 | - |
1072 | - We then use curl to issue a few requests and verify that each server's |
1073 | - logging statements were logged to the one log file |
1074 | - """ |
1075 | - logfile = "/tmp/test_logfile.log" |
1076 | - self.logfiles.append(logfile) |
1077 | - |
1078 | - if os.path.exists(logfile): |
1079 | - os.unlink(logfile) |
1080 | - |
1081 | - self._cleanup_test_servers() |
1082 | - |
1083 | - # Port numbers hopefully not used by anything... |
1084 | - api_port = 32001 |
1085 | - reg_port = 32000 |
1086 | - image_dir = "/tmp/test.images.%d" % api_port |
1087 | - if os.path.exists(image_dir): |
1088 | - shutil.rmtree(image_dir) |
1089 | - |
1090 | - # A config file to use just for this test...we don't want |
1091 | - # to trample on currently-running Glance servers, now do we? |
1092 | - with tempfile.NamedTemporaryFile() as conf_file: |
1093 | - conf_contents = """[DEFAULT] |
1094 | -verbose = True |
1095 | -debug = True |
1096 | -log_file = %(logfile)s |
1097 | - |
1098 | -[app:glance-api] |
1099 | -paste.app_factory = glance.server:app_factory |
1100 | -filesystem_store_datadir=%(image_dir)s |
1101 | -default_store = file |
1102 | -bind_host = 0.0.0.0 |
1103 | -bind_port = %(api_port)s |
1104 | -registry_host = 0.0.0.0 |
1105 | -registry_port = %(reg_port)s |
1106 | - |
1107 | -[app:glance-registry] |
1108 | -paste.app_factory = glance.registry.server:app_factory |
1109 | -bind_host = 0.0.0.0 |
1110 | -bind_port = %(reg_port)s |
1111 | -sql_connection = sqlite:// |
1112 | -sql_idle_timeout = 3600 |
1113 | -""" % locals() |
1114 | - conf_file.write(conf_contents) |
1115 | - conf_file.flush() |
1116 | - conf_file_name = conf_file.name |
1117 | - |
1118 | - venv = "" |
1119 | - if 'VIRTUAL_ENV' in os.environ: |
1120 | - venv = "tools/with_venv.sh " |
1121 | - |
1122 | - # Start up the API and default registry server |
1123 | - cmd = venv + "./bin/glance-control api start "\ |
1124 | - "%s --pid-file=glance-api.pid" % conf_file_name |
1125 | - exitcode, out, err = execute(cmd) |
1126 | - |
1127 | - self.assertEquals(0, exitcode) |
1128 | - self.assertTrue("Starting glance-api with" in out) |
1129 | - |
1130 | - cmd = venv + "./bin/glance-control registry start "\ |
1131 | - "%s --pid-file=glance-registry.pid" % conf_file_name |
1132 | - exitcode, out, err = execute(cmd) |
1133 | - |
1134 | - self.assertEquals(0, exitcode) |
1135 | - self.assertTrue("Starting glance-registry with" in out) |
1136 | - |
1137 | - time.sleep(2) # Gotta give some time for spin up... |
1138 | - |
1139 | - cmd = "curl -X POST -H 'Content-Type: application/octet-stream' "\ |
1140 | - "-H 'X-Image-Meta-Name: ImageName' "\ |
1141 | - "-H 'X-Image-Meta-Disk-Format: Invalid' "\ |
1142 | - "http://0.0.0.0:%d/images" % api_port |
1143 | - ignored, out, err = execute(cmd) |
1144 | - |
1145 | - self.assertTrue('Invalid disk format' in out, |
1146 | - "Could not find 'Invalid disk format' " |
1147 | - "in output: %s" % out) |
1148 | - |
1149 | - self.assertTrue(os.path.exists(logfile), |
1150 | - "Logfile %s does not exist!" % logfile) |
1151 | - |
1152 | - logfile_contents = open(logfile, 'rb').read() |
1153 | - |
1154 | - # Check that BOTH the glance API and registry server |
1155 | - # modules are logged to the file. |
1156 | - self.assertTrue('[glance.server]' in logfile_contents, |
1157 | - "Could not find '[glance.server]' " |
1158 | - "in logfile: %s" % logfile_contents) |
1159 | - self.assertTrue('[glance.registry.server]' in logfile_contents, |
1160 | - "Could not find '[glance.registry.server]' " |
1161 | - "in logfile: %s" % logfile_contents) |
1162 | - |
1163 | - # Test that the error we caused above is in the log |
1164 | - self.assertTrue('Invalid disk format' in logfile_contents, |
1165 | - "Could not find 'Invalid disk format' " |
1166 | - "in logfile: %s" % logfile_contents) |
1167 | - |
1168 | - # Spin down the API and default registry server |
1169 | - cmd = "./bin/glance-control api stop "\ |
1170 | - "%s --pid-file=glance-api.pid" % conf_file_name |
1171 | - ignored, out, err = execute(cmd) |
1172 | - cmd = "./bin/glance-control registry stop "\ |
1173 | - "%s --pid-file=glance-registry.pid" % conf_file_name |
1174 | - ignored, out, err = execute(cmd) |
1175 | + self.cleanup() |
1176 | + api_port, reg_port, conf_file = self.start_servers() |
1177 | + |
1178 | + cmd = "curl -g http://0.0.0.0:%d/images" % api_port |
1179 | + |
1180 | + exitcode, out, err = execute(cmd) |
1181 | + |
1182 | + self.assertEqual(0, exitcode) |
1183 | + self.assertEqual('{"images": []}', out.strip()) |
1184 | + |
1185 | + cmd = "curl -X POST -H 'Content-Type: application/octet-stream' "\ |
1186 | + "-H 'X-Image-Meta-Name: ImageName' "\ |
1187 | + "-H 'X-Image-Meta-Disk-Format: Invalid' "\ |
1188 | + "http://0.0.0.0:%d/images" % api_port |
1189 | + ignored, out, err = execute(cmd) |
1190 | + |
1191 | + self.assertTrue('Invalid disk format' in out, |
1192 | + "Could not find 'Invalid disk format' " |
1193 | + "in output: %s" % out) |
1194 | + |
1195 | + self.stop_servers() |
1196 | |
1197 | === added file 'tests/unit/test_utils.py' |
1198 | --- tests/unit/test_utils.py 1970-01-01 00:00:00 +0000 |
1199 | +++ tests/unit/test_utils.py 2011-03-21 19:26:04 +0000 |
1200 | @@ -0,0 +1,67 @@ |
1201 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1202 | + |
1203 | +# Copyright 2011 OpenStack, LLC |
1204 | +# All Rights Reserved. |
1205 | +# |
1206 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1207 | +# not use this file except in compliance with the License. You may obtain |
1208 | +# a copy of the License at |
1209 | +# |
1210 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1211 | +# |
1212 | +# Unless required by applicable law or agreed to in writing, software |
1213 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1214 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1215 | +# License for the specific language governing permissions and limitations |
1216 | +# under the License. |
1217 | + |
1218 | +import unittest |
1219 | + |
1220 | +from glance import utils |
1221 | + |
1222 | + |
1223 | +class TestUtils(unittest.TestCase): |
1224 | + |
1225 | + """Test routines in glance.utils""" |
1226 | + |
1227 | + def test_headers_are_unicode(self): |
1228 | + """ |
1229 | + Verifies that the headers returned by conversion code are unicode. |
1230 | + |
1231 | + Headers are passed via http in non-testing mode, which automatically |
1232 | + converts them to unicode. Verifying that the method does the |
1233 | + conversion proves that we aren't passing data that works in tests |
1234 | + but will fail in production. |
1235 | + """ |
1236 | + fixture = {'name': 'fake public image', |
1237 | + 'is_public': True, |
1238 | + 'type': 'kernel', |
1239 | + 'size': 19, |
1240 | + 'location': "file:///tmp/glance-tests/2", |
1241 | + 'properties': {'distro': 'Ubuntu 10.04 LTS'}} |
1242 | + headers = utils.image_meta_to_http_headers(fixture) |
1243 | + for k, v in headers.iteritems(): |
1244 | + self.assert_(isinstance(v, unicode), "%s is not unicode" % v) |
1245 | + |
1246 | + def test_data_passed_properly_through_headers(self): |
1247 | + """ |
1248 | + Verifies that data is the same after being passed through headers |
1249 | + """ |
1250 | + fixture = {'name': 'fake public image', |
1251 | + 'is_public': True, |
1252 | + 'deleted': False, |
1253 | + 'type': 'kernel', |
1254 | + 'name': None, |
1255 | + 'size': 19, |
1256 | + 'location': "file:///tmp/glance-tests/2", |
1257 | + 'properties': {'distro': 'Ubuntu 10.04 LTS'}} |
1258 | + headers = utils.image_meta_to_http_headers(fixture) |
1259 | + |
1260 | + class FakeResponse(): |
1261 | + pass |
1262 | + |
1263 | + response = FakeResponse() |
1264 | + response.headers = headers |
1265 | + result = utils.get_image_meta_from_headers(response) |
1266 | + for k, v in fixture.iteritems(): |
1267 | + self.assertEqual(v, result[k]) |
1268 | |
1269 | === modified file 'tests/utils.py' |
1270 | --- tests/utils.py 2011-01-27 04:19:13 +0000 |
1271 | +++ tests/utils.py 2011-03-21 19:26:04 +0000 |
1272 | @@ -1,6 +1,6 @@ |
1273 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1274 | |
1275 | -# Copyright 2010 OpenStack, LLC |
1276 | +# Copyright 2010-2011 OpenStack, LLC |
1277 | # All Rights Reserved. |
1278 | # |
1279 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
1280 | @@ -17,11 +17,41 @@ |
1281 | |
1282 | """Common utilities used in testing""" |
1283 | |
1284 | - |
1285 | -def is_swift_available(): |
1286 | - """Returns True if Swift/Cloudfiles is importable""" |
1287 | - try: |
1288 | - import swift |
1289 | - return True |
1290 | - except ImportError: |
1291 | - return False |
1292 | +import os |
1293 | +import socket |
1294 | +import subprocess |
1295 | + |
1296 | + |
1297 | +def execute(cmd): |
1298 | + env = os.environ.copy() |
1299 | + |
1300 | + # Make sure that we use the programs in the |
1301 | + # current source directory's bin/ directory. |
1302 | + env['PATH'] = os.path.join(os.getcwd(), 'bin') + ':' + env['PATH'] |
1303 | + process = subprocess.Popen(cmd, |
1304 | + shell=True, |
1305 | + stdin=subprocess.PIPE, |
1306 | + stdout=subprocess.PIPE, |
1307 | + stderr=subprocess.PIPE, |
1308 | + env=env) |
1309 | + result = process.communicate() |
1310 | + (out, err) = result |
1311 | + exitcode = process.returncode |
1312 | + if process.returncode != 0: |
1313 | + msg = "Command %(cmd)s did not succeed. Returned an exit "\ |
1314 | + "code of %(exitcode)d."\ |
1315 | + "\n\nSTDOUT: %(out)s"\ |
1316 | + "\n\nSTDERR: %(err)s" % locals() |
1317 | + raise RuntimeError(msg) |
1318 | + return exitcode, out, err |
1319 | + |
1320 | + |
1321 | +def get_unused_port(): |
1322 | + """ |
1323 | + Returns an unused port on localhost. |
1324 | + """ |
1325 | + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
1326 | + s.bind(('localhost', 0)) |
1327 | + addr, port = s.getsockname() |
1328 | + s.close() |
1329 | + return port |
Really great stuff here, Jay-- thanks!
No nits, just a suggestion that we may eventually want to add a --unittests-only option to ./run_tests.sh so we can iterate quickly as we're refactoring, though not necessary at the moment since the functional tests are completely surprisingly quickly.