| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2013 The Chromium Authors. All rights reserved. | 2 # Copyright 2013 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 # pylint: disable=W0223 | 6 # pylint: disable=W0223 |
| 7 # pylint: disable=W0231 | 7 # pylint: disable=W0231 |
| 8 | 8 |
| 9 import binascii | |
| 10 import hashlib | 9 import hashlib |
| 11 import json | 10 import json |
| 12 import logging | 11 import logging |
| 13 import os | 12 import os |
| 14 import random | |
| 15 import shutil | 13 import shutil |
| 16 import StringIO | 14 import StringIO |
| 17 import sys | 15 import sys |
| 18 import tempfile | 16 import tempfile |
| 19 import threading | 17 import threading |
| 20 import unittest | 18 import unittest |
| 19 import urllib |
| 21 import zlib | 20 import zlib |
| 22 | 21 |
| 23 BASE_PATH = os.path.dirname(os.path.abspath(__file__)) | 22 BASE_PATH = os.path.dirname(os.path.abspath(__file__)) |
| 24 ROOT_DIR = os.path.dirname(BASE_PATH) | 23 ROOT_DIR = os.path.dirname(BASE_PATH) |
| 25 sys.path.insert(0, ROOT_DIR) | 24 sys.path.insert(0, ROOT_DIR) |
| 26 | 25 |
| 27 import auto_stub | 26 import auto_stub |
| 28 import isolateserver | 27 import isolateserver |
| 29 | 28 |
| 30 from utils import threading_utils | 29 from utils import threading_utils |
| (...skipping 26 matching lines...) Expand all Loading... |
| 57 for i, n in enumerate(self._requests): | 56 for i, n in enumerate(self._requests): |
| 58 if n[0] == url: | 57 if n[0] == url: |
| 59 _, expected_kwargs, result = self._requests.pop(i) | 58 _, expected_kwargs, result = self._requests.pop(i) |
| 60 self.assertEqual(expected_kwargs, kwargs) | 59 self.assertEqual(expected_kwargs, kwargs) |
| 61 if result is not None: | 60 if result is not None: |
| 62 return isolateserver.net.HttpResponse.get_fake_response(result, url) | 61 return isolateserver.net.HttpResponse.get_fake_response(result, url) |
| 63 return None | 62 return None |
| 64 self.fail('Unknown request %s' % url) | 63 self.fail('Unknown request %s' % url) |
| 65 | 64 |
| 66 | 65 |
| 66 class TestZipCompression(TestCase): |
| 67 """Test zip_compress and zip_decompress generators.""" |
| 68 |
| 69 def test_compress_and_decompress(self): |
| 70 """Test data === decompress(compress(data)).""" |
| 71 original = [str(x) for x in xrange(0, 1000)] |
| 72 processed = isolateserver.zip_decompress( |
| 73 isolateserver.zip_compress(original)) |
| 74 self.assertEqual(''.join(original), ''.join(processed)) |
| 75 |
| 76 def test_zip_bomb(self): |
| 77 """Verify zip_decompress always returns small chunks.""" |
| 78 original = '\x00' * 100000 |
| 79 bomb = ''.join(isolateserver.zip_compress(original)) |
| 80 decompressed = [] |
| 81 chunk_size = 1000 |
| 82 for chunk in isolateserver.zip_decompress([bomb], chunk_size): |
| 83 self.assertLessEqual(len(chunk), chunk_size) |
| 84 decompressed.append(chunk) |
| 85 self.assertEqual(original, ''.join(decompressed)) |
| 86 |
| 87 def test_bad_zip_file(self): |
| 88 """Verify decompressing broken file raises IOError.""" |
| 89 with self.assertRaises(IOError): |
| 90 ''.join(isolateserver.zip_decompress(['Im not a zip file'])) |
| 91 |
| 92 |
| 93 class FakeItem(isolateserver.Item): |
| 94 def __init__(self, data, is_isolated=False): |
| 95 super(FakeItem, self).__init__( |
| 96 ALGO(data).hexdigest(), len(data), is_isolated) |
| 97 self.data = data |
| 98 |
| 99 def content(self, _chunk_size): |
| 100 return [self.data] |
| 101 |
| 102 @property |
| 103 def zipped(self): |
| 104 return zlib.compress(self.data, self.compression_level) |
| 105 |
| 106 |
| 67 class StorageTest(TestCase): | 107 class StorageTest(TestCase): |
| 68 """Tests for Storage methods.""" | 108 """Tests for Storage methods.""" |
| 69 | 109 |
| 70 @staticmethod | 110 @staticmethod |
| 71 def mock_push(side_effect=None): | 111 def mock_push(side_effect=None): |
| 72 """Returns StorageApi subclass with mocked 'push' method.""" | 112 """Returns StorageApi subclass with mocked 'push' method.""" |
| 73 class MockedStorageApi(isolateserver.StorageApi): | 113 class MockedStorageApi(isolateserver.StorageApi): |
| 74 def __init__(self): | 114 def __init__(self): |
| 75 self.pushed = [] | 115 self.pushed = [] |
| 76 def push(self, item, expected_size, content_generator, push_urls=None): | 116 def push(self, item, content, size): |
| 77 self.pushed.append( | 117 self.pushed.append((item, ''.join(content), size)) |
| 78 (item, expected_size, ''.join(content_generator), push_urls)) | |
| 79 if side_effect: | 118 if side_effect: |
| 80 side_effect() | 119 side_effect() |
| 81 return MockedStorageApi() | 120 return MockedStorageApi() |
| 82 | 121 |
| 83 def test_batch_files_for_check(self): | 122 def test_batch_items_for_check(self): |
| 84 items = { | 123 items = [ |
| 85 'foo': {'s': 12}, | 124 isolateserver.Item('foo', 12), |
| 86 'bar': {}, | 125 isolateserver.Item('blow', 0), |
| 87 'blow': {'s': 0}, | 126 isolateserver.Item('bizz', 1222), |
| 88 'bizz': {'s': 1222}, | 127 isolateserver.Item('buzz', 1223), |
| 89 'buzz': {'s': 1223}, | 128 ] |
| 90 } | |
| 91 expected = [ | 129 expected = [ |
| 92 [ | 130 [items[3], items[2], items[0], items[1]], |
| 93 ('buzz', {'s': 1223}), | |
| 94 ('bizz', {'s': 1222}), | |
| 95 ('foo', {'s': 12}), | |
| 96 ('blow', {'s': 0}), | |
| 97 ], | |
| 98 ] | 131 ] |
| 99 batches = list(isolateserver.Storage.batch_files_for_check(items)) | 132 batches = list(isolateserver.Storage.batch_items_for_check(items)) |
| 100 self.assertEqual(batches, expected) | 133 self.assertEqual(batches, expected) |
| 101 | 134 |
| 102 def test_get_missing_files(self): | 135 def test_get_missing_items(self): |
| 103 items = { | 136 items = [ |
| 104 'foo': {'s': 12}, | 137 isolateserver.Item('foo', 12), |
| 105 'bar': {}, | 138 isolateserver.Item('blow', 0), |
| 106 'blow': {'s': 0}, | 139 isolateserver.Item('bizz', 1222), |
| 107 'bizz': {'s': 1222}, | 140 isolateserver.Item('buzz', 1223), |
| 108 'buzz': {'s': 1223}, | 141 ] |
| 109 } | 142 missing = [ |
| 110 missing = { | 143 [items[2], items[3]], |
| 111 'bizz': {'s': 1222}, | 144 ] |
| 112 'buzz': {'s': 1223}, | |
| 113 } | |
| 114 fake_upload_urls = ('a', 'b') | |
| 115 | 145 |
| 116 class MockedStorageApi(isolateserver.StorageApi): | 146 class MockedStorageApi(isolateserver.StorageApi): |
| 117 def contains(self, files): | 147 def contains(self, _items): |
| 118 return [f + (fake_upload_urls,) for f in files if f[0] in missing] | 148 return missing |
| 119 storage = isolateserver.Storage(MockedStorageApi(), use_zip=False) | 149 storage = isolateserver.Storage(MockedStorageApi(), use_zip=False) |
| 120 | 150 |
| 121 # 'get_missing_files' is a generator, materialize its result in a list. | 151 # 'get_missing_items' is a generator, materialize its result in a list. |
| 122 result = list(storage.get_missing_files(items)) | 152 result = list(storage.get_missing_items(items)) |
| 123 | 153 self.assertEqual(missing, result) |
| 124 # Ensure it's a list of triplets. | |
| 125 self.assertTrue(all(len(x) == 3 for x in result)) | |
| 126 # Verify upload urls are set. | |
| 127 self.assertTrue(all(x[2] == fake_upload_urls for x in result)) | |
| 128 # 'get_missing_files' doesn't guarantee order of its results, so convert | |
| 129 # it to unordered dict and compare dicts. | |
| 130 self.assertEqual(dict(x[:2] for x in result), missing) | |
| 131 | 154 |
| 132 def test_async_push(self): | 155 def test_async_push(self): |
| 133 data_to_push = '1234567' | |
| 134 digest = ALGO(data_to_push).hexdigest() | |
| 135 compression_level = 5 | |
| 136 zipped = zlib.compress(data_to_push, compression_level) | |
| 137 push_urls = ('fake1', 'fake2') | |
| 138 | |
| 139 for use_zip in (False, True): | 156 for use_zip in (False, True): |
| 157 item = FakeItem('1234567') |
| 140 storage_api = self.mock_push() | 158 storage_api = self.mock_push() |
| 141 storage = isolateserver.Storage(storage_api, use_zip) | 159 storage = isolateserver.Storage(storage_api, use_zip) |
| 142 channel = threading_utils.TaskChannel() | 160 channel = threading_utils.TaskChannel() |
| 143 storage.async_push( | 161 storage.async_push(channel, 0, item) |
| 144 channel, 0, digest, len(data_to_push), [data_to_push], | |
| 145 compression_level, push_urls) | |
| 146 # Wait for push to finish. | 162 # Wait for push to finish. |
| 147 pushed_item = channel.pull() | 163 pushed_item = channel.pull() |
| 148 self.assertEqual(digest, pushed_item) | 164 self.assertEqual(item, pushed_item) |
| 149 # StorageApi.push was called with correct arguments. | 165 # StorageApi.push was called with correct arguments. |
| 150 if use_zip: | 166 if use_zip: |
| 151 expected_data = zipped | 167 expected_data = item.zipped |
| 152 expected_size = isolateserver.UNKNOWN_FILE_SIZE | 168 expected_size = isolateserver.UNKNOWN_FILE_SIZE |
| 153 else: | 169 else: |
| 154 expected_data = data_to_push | 170 expected_data = item.data |
| 155 expected_size = len(data_to_push) | 171 expected_size = len(item.data) |
| 156 self.assertEqual( | 172 self.assertEqual( |
| 157 [(digest, expected_size, expected_data, push_urls)], | 173 [(item, expected_data, expected_size)], |
| 158 storage_api.pushed) | 174 storage_api.pushed) |
| 159 | 175 |
| 160 def test_async_push_generator_errors(self): | 176 def test_async_push_generator_errors(self): |
| 161 class FakeException(Exception): | 177 class FakeException(Exception): |
| 162 pass | 178 pass |
| 163 | 179 |
| 164 def faulty_generator(): | 180 def faulty_generator(_chunk_size): |
| 165 yield 'Hi!' | 181 yield 'Hi!' |
| 166 raise FakeException('fake exception') | 182 raise FakeException('fake exception') |
| 167 | 183 |
| 168 for use_zip in (False, True): | 184 for use_zip in (False, True): |
| 185 item = FakeItem('') |
| 186 self.mock(item, 'content', faulty_generator) |
| 169 storage_api = self.mock_push() | 187 storage_api = self.mock_push() |
| 170 storage = isolateserver.Storage(storage_api, use_zip) | 188 storage = isolateserver.Storage(storage_api, use_zip) |
| 171 channel = threading_utils.TaskChannel() | 189 channel = threading_utils.TaskChannel() |
| 172 storage.async_push( | 190 storage.async_push(channel, 0, item) |
| 173 channel, 0, 'item', isolateserver.UNKNOWN_FILE_SIZE, | |
| 174 faulty_generator(), 0, None) | |
| 175 with self.assertRaises(FakeException): | 191 with self.assertRaises(FakeException): |
| 176 channel.pull() | 192 channel.pull() |
| 177 # StorageApi's push should never complete when data can not be read. | 193 # StorageApi's push should never complete when data can not be read. |
| 178 self.assertEqual(0, len(storage_api.pushed)) | 194 self.assertEqual(0, len(storage_api.pushed)) |
| 179 | 195 |
| 180 def test_async_push_upload_errors(self): | 196 def test_async_push_upload_errors(self): |
| 181 chunk = 'data_chunk' | 197 chunk = 'data_chunk' |
| 182 compression_level = 5 | |
| 183 zipped = zlib.compress(chunk, compression_level) | |
| 184 | 198 |
| 185 def _generator(): | 199 def _generator(_chunk_size): |
| 186 yield chunk | 200 yield chunk |
| 187 | 201 |
| 188 def push_side_effect(): | 202 def push_side_effect(): |
| 189 raise IOError('Nope') | 203 raise IOError('Nope') |
| 190 | 204 |
| 191 # TODO(vadimsh): Retrying push when fetching data from a generator is | 205 # TODO(vadimsh): Retrying push when fetching data from a generator is |
| 192 # broken now (it reuses same generator instance when retrying). | 206 # broken now (it reuses same generator instance when retrying). |
| 193 content_sources = ( | 207 content_sources = ( |
| 194 # generator(), | 208 # generator(), |
| 195 [chunk], | 209 lambda _chunk_size: [chunk], |
| 196 ) | 210 ) |
| 197 | 211 |
| 198 for use_zip in (False, True): | 212 for use_zip in (False, True): |
| 199 for source in content_sources: | 213 for source in content_sources: |
| 214 item = FakeItem(chunk) |
| 215 self.mock(item, 'content', source) |
| 200 storage_api = self.mock_push(push_side_effect) | 216 storage_api = self.mock_push(push_side_effect) |
| 201 storage = isolateserver.Storage(storage_api, use_zip) | 217 storage = isolateserver.Storage(storage_api, use_zip) |
| 202 channel = threading_utils.TaskChannel() | 218 channel = threading_utils.TaskChannel() |
| 203 storage.async_push( | 219 storage.async_push(channel, 0, item) |
| 204 channel, 0, 'item', isolateserver.UNKNOWN_FILE_SIZE, | |
| 205 source, compression_level, None) | |
| 206 with self.assertRaises(IOError): | 220 with self.assertRaises(IOError): |
| 207 channel.pull() | 221 channel.pull() |
| 208 # First initial attempt + all retries. | 222 # First initial attempt + all retries. |
| 209 attempts = 1 + isolateserver.WorkerPool.RETRIES | 223 attempts = 1 + isolateserver.WorkerPool.RETRIES |
| 210 # Single push attempt parameters. | 224 # Single push attempt parameters. |
| 211 expected_push = ( | 225 expected_push = ( |
| 212 'item', isolateserver.UNKNOWN_FILE_SIZE, | 226 item, |
| 213 zipped if use_zip else chunk, None) | 227 item.zipped if use_zip else item.data, |
| 228 isolateserver.UNKNOWN_FILE_SIZE if use_zip else item.size) |
| 214 # Ensure all pushes are attempted. | 229 # Ensure all pushes are attempted. |
| 215 self.assertEqual( | 230 self.assertEqual( |
| 216 [expected_push] * attempts, storage_api.pushed) | 231 [expected_push] * attempts, storage_api.pushed) |
| 217 | 232 |
| 218 def test_upload_tree(self): | 233 def test_upload_tree(self): |
| 219 root = 'root' | 234 root = 'root' |
| 220 files = { | 235 files = { |
| 221 'a': { | 236 'a': { |
| 222 's': 100, | 237 's': 100, |
| 223 'h': 'hash_a', | 238 'h': 'hash_a', |
| 224 }, | 239 }, |
| 225 'b': { | 240 'b': { |
| 226 's': 200, | 241 's': 200, |
| 227 'h': 'hash_b', | 242 'h': 'hash_b', |
| 228 }, | 243 }, |
| 229 'c': { | 244 'c': { |
| 230 's': 300, | 245 's': 300, |
| 231 'h': 'hash_c', | 246 'h': 'hash_c', |
| 232 }, | 247 }, |
| 233 } | 248 } |
| 234 push_urls = { | 249 files_data = dict((k, 'x' * files[k]['s']) for k in files) |
| 235 'a': ('upload_a', 'finalize_a'), | |
| 236 'b': ('upload_b', None), | |
| 237 'c': ('upload_c', None), | |
| 238 } | |
| 239 files_data = dict((k, k * files[k]['s']) for k in files) | |
| 240 missing = set(['a', 'b']) | 250 missing = set(['a', 'b']) |
| 241 | 251 |
| 242 # Files read by mocked_file_read. | 252 # Files read by mocked_file_read. |
| 243 read_calls = [] | 253 read_calls = [] |
| 244 # 'contains' calls. | 254 # 'contains' calls. |
| 245 contains_calls = [] | 255 contains_calls = [] |
| 246 # 'push' calls. | 256 # 'push' calls. |
| 247 push_calls = [] | 257 push_calls = [] |
| 248 | 258 |
| 249 def mocked_file_read(filepath, _chunk_size=0): | 259 def mocked_file_read(filepath, _chunk_size=0): |
| 250 self.assertEqual(root, os.path.dirname(filepath)) | 260 self.assertEqual(root, os.path.dirname(filepath)) |
| 251 filename = os.path.basename(filepath) | 261 filename = os.path.basename(filepath) |
| 252 self.assertIn(filename, files_data) | 262 self.assertIn(filename, files_data) |
| 253 read_calls.append(filename) | 263 read_calls.append(filename) |
| 254 return files_data[filename] | 264 return files_data[filename] |
| 255 self.mock(isolateserver, 'file_read', mocked_file_read) | 265 self.mock(isolateserver, 'file_read', mocked_file_read) |
| 256 | 266 |
| 257 class MockedStorageApi(isolateserver.StorageApi): | 267 class MockedStorageApi(isolateserver.StorageApi): |
| 258 def contains(self, files): | 268 def contains(self, items): |
| 259 contains_calls.append(files) | 269 contains_calls.append(items) |
| 260 return [f + (push_urls[f[0]],) for f in files if f[0] in missing] | 270 return [i for i in items if os.path.basename(i.path) in missing] |
| 261 | 271 |
| 262 def push(self, item, expected_size, content_generator, push_urls=None): | 272 def push(self, item, content, size): |
| 263 push_calls.append( | 273 push_calls.append((item, ''.join(content), size)) |
| 264 (item, expected_size, ''.join(content_generator), push_urls)) | |
| 265 | 274 |
| 266 storage_api = MockedStorageApi() | 275 storage_api = MockedStorageApi() |
| 267 storage = isolateserver.Storage(storage_api, use_zip=False) | 276 storage = isolateserver.Storage(storage_api, use_zip=False) |
| 268 storage.upload_tree(root, files) | 277 storage.upload_tree(root, files) |
| 269 | 278 |
| 270 # Was reading only missing files. | 279 # Was reading only missing files. |
| 271 self.assertEqual(missing, set(read_calls)) | 280 self.assertEqual(missing, set(read_calls)) |
| 272 # 'contains' checked for existence of all files. | 281 # 'contains' checked for existence of all files. |
| 273 self.assertEqual(files, dict(sum(contains_calls, []))) | 282 self.assertEqual( |
| 283 set(f['h'] for f in files.itervalues()), |
| 284 set(i.digest for i in sum(contains_calls, []))) |
| 274 # Pushed only missing files. | 285 # Pushed only missing files. |
| 275 self.assertEqual( | 286 self.assertEqual( |
| 276 set(files[name]['h'] for name in missing), | 287 set(files[name]['h'] for name in missing), |
| 277 set(call[0] for call in push_calls)) | 288 set(call[0].digest for call in push_calls)) |
| 278 # Pushing with correct data, size and push urls. | 289 # Pushing with correct data, size and push urls. |
| 279 for push_call in push_calls: | 290 for pushed_item, pushed_content, pushed_size in push_calls: |
| 280 digest = push_call[0] | |
| 281 filenames = [ | 291 filenames = [ |
| 282 name for name, metadata in files.iteritems() | 292 name for name, metadata in files.iteritems() |
| 283 if metadata['h'] == digest | 293 if metadata['h'] == pushed_item.digest |
| 284 ] | 294 ] |
| 285 self.assertEqual(1, len(filenames)) | 295 self.assertEqual(1, len(filenames)) |
| 286 filename = filenames[0] | 296 filename = filenames[0] |
| 287 data = files_data[filename] | 297 self.assertEqual(os.path.join(root, filename), pushed_item.path) |
| 288 self.assertEqual( | 298 self.assertEqual(files_data[filename], pushed_content) |
| 289 (digest, len(data), data, push_urls[filename]), | 299 self.assertEqual(len(files_data[filename]), pushed_size) |
| 290 push_call) | 300 |
| 291 | 301 |
| 292 | 302 class IsolateServerStorageApiTest(TestCase): |
| 293 class IsolateServerArchiveTest(TestCase): | 303 @staticmethod |
| 294 def setUp(self): | 304 def mock_handshake_request(server, token='fake token', error=None): |
| 295 super(IsolateServerArchiveTest, self).setUp() | 305 handshake_request = { |
| 296 self.mock(isolateserver, 'randomness', lambda: 'not_really_random') | 306 'client_app_version': isolateserver.__version__, |
| 297 self.mock(sys, 'stdout', StringIO.StringIO()) | 307 'fetcher': True, |
| 298 | 308 'protocol_version': isolateserver.ISOLATE_PROTOCOL_VERSION, |
| 299 def test_present(self): | 309 'pusher': True, |
| 310 } |
| 311 handshake_response = { |
| 312 'access_token': token, |
| 313 'error': error, |
| 314 'protocol_version': isolateserver.ISOLATE_PROTOCOL_VERSION, |
| 315 'server_app_version': 'mocked server T1000', |
| 316 } |
| 317 return ( |
| 318 server + '/content-gs/handshake', |
| 319 { |
| 320 'content_type': 'application/json', |
| 321 'method': 'POST', |
| 322 'data': json.dumps(handshake_request, separators=(',', ':')), |
| 323 }, |
| 324 json.dumps(handshake_response), |
| 325 ) |
| 326 |
| 327 @staticmethod |
| 328 def mock_fetch_request(server, namespace, item, data): |
| 329 return ( |
| 330 server + '/content-gs/retrieve/%s/%s' % (namespace, item), |
| 331 {'retry_404': True, 'read_timeout': 60}, |
| 332 data, |
| 333 ) |
| 334 |
| 335 @staticmethod |
| 336 def mock_contains_request(server, namespace, token, request, response): |
| 337 url = server + '/content-gs/pre-upload/%s?token=%s' % ( |
| 338 namespace, urllib.quote(token)) |
| 339 return ( |
| 340 url, |
| 341 { |
| 342 'data': json.dumps(request, separators=(',', ':')), |
| 343 'content_type': 'application/json', |
| 344 'method': 'POST', |
| 345 }, |
| 346 json.dumps(response), |
| 347 ) |
| 348 |
| 349 def test_server_capabilities_success(self): |
| 350 server = 'http://example.com' |
| 351 namespace = 'default' |
| 352 access_token = 'fake token' |
| 353 self._requests = [ |
| 354 self.mock_handshake_request(server, access_token), |
| 355 ] |
| 356 storage = isolateserver.IsolateServer(server, namespace) |
| 357 caps = storage.server_capabilities |
| 358 self.assertEqual(access_token, caps['access_token']) |
| 359 |
| 360 def test_server_capabilities_network_failure(self): |
| 361 self.mock(isolateserver.net, 'url_open', lambda *_args, **_kwargs: None) |
| 362 with self.assertRaises(isolateserver.MappingError): |
| 363 storage = isolateserver.IsolateServer('http://example.com', 'default') |
| 364 _ = storage.server_capabilities |
| 365 |
| 366 def test_server_capabilities_format_failure(self): |
| 367 server = 'http://example.com' |
| 368 namespace = 'default' |
| 369 handshake_req = self.mock_handshake_request(server) |
| 370 self._requests = [ |
| 371 (handshake_req[0], handshake_req[1], 'Im a bad response'), |
| 372 ] |
| 373 storage = isolateserver.IsolateServer(server, namespace) |
| 374 with self.assertRaises(isolateserver.MappingError): |
| 375 _ = storage.server_capabilities |
| 376 |
| 377 def test_server_capabilities_respects_error(self): |
| 378 server = 'http://example.com' |
| 379 namespace = 'default' |
| 380 error = 'Im sorry, Dave. Im afraid I cant do that.' |
| 381 self._requests = [ |
| 382 self.mock_handshake_request(server, error=error) |
| 383 ] |
| 384 storage = isolateserver.IsolateServer(server, namespace) |
| 385 with self.assertRaises(isolateserver.MappingError) as context: |
| 386 _ = storage.server_capabilities |
| 387 # Server error message should be reported to user. |
| 388 self.assertIn(error, str(context.exception)) |
| 389 |
| 390 def test_fetch_success_default(self): |
| 391 server = 'http://example.com' |
| 392 namespace = 'default' |
| 393 data = ''.join(str(x) for x in xrange(1000)) |
| 394 item = ALGO(data).hexdigest() |
| 395 self._requests = [ |
| 396 self.mock_fetch_request(server, namespace, item, data), |
| 397 ] |
| 398 storage = isolateserver.IsolateServer(server, namespace) |
| 399 fetched = ''.join(storage.fetch(item, len(data))) |
| 400 self.assertEqual(data, fetched) |
| 401 |
| 402 def test_fetch_success_default_gzip(self): |
| 403 server = 'http://example.com' |
| 404 namespace = 'default-gzip' |
| 405 data = ''.join(str(x) for x in xrange(1000)) |
| 406 item = ALGO(data).hexdigest() |
| 407 self._requests = [ |
| 408 self.mock_fetch_request(server, namespace, item, zlib.compress(data)), |
| 409 ] |
| 410 storage = isolateserver.IsolateServer(server, namespace) |
| 411 fetched = ''.join(storage.fetch(item, len(data))) |
| 412 self.assertEqual(data, fetched) |
| 413 |
| 414 def test_fetch_failure_missing(self): |
| 415 server = 'http://example.com' |
| 416 namespace = 'default' |
| 417 item = ALGO('something').hexdigest() |
| 418 self._requests = [ |
| 419 self.mock_fetch_request(server, namespace, item, None), |
| 420 ] |
| 421 storage = isolateserver.IsolateServer(server, namespace) |
| 422 with self.assertRaises(IOError): |
| 423 _ = ''.join(storage.fetch(item, isolateserver.UNKNOWN_FILE_SIZE)) |
| 424 |
| 425 def test_fetch_failure_bad_size(self): |
| 426 server = 'http://example.com' |
| 427 namespace = 'default' |
| 428 data = ''.join(str(x) for x in xrange(1000)) |
| 429 expected_size = len(data) |
| 430 item = ALGO(data).hexdigest() |
| 431 self._requests = [ |
| 432 self.mock_fetch_request(server, namespace, item, data[:100]), |
| 433 ] |
| 434 storage = isolateserver.IsolateServer(server, namespace) |
| 435 with self.assertRaises(IOError): |
| 436 _ = ''.join(storage.fetch(item, expected_size)) |
| 437 |
| 438 def test_fetch_failure_bad_zip(self): |
| 439 server = 'http://example.com' |
| 440 namespace = 'default-gzip' |
| 441 item = ALGO('something').hexdigest() |
| 442 self._requests = [ |
| 443 self.mock_fetch_request(server, namespace, item, 'Im not a zip'), |
| 444 ] |
| 445 storage = isolateserver.IsolateServer(server, namespace) |
| 446 with self.assertRaises(IOError): |
| 447 _ = ''.join(storage.fetch(item, isolateserver.UNKNOWN_FILE_SIZE)) |
| 448 |
| 449 def test_push_success(self): |
| 450 server = 'http://example.com' |
| 451 namespace = 'default' |
| 452 token = 'fake token' |
| 453 data = ''.join(str(x) for x in xrange(1000)) |
| 454 item = FakeItem(data) |
| 455 push_urls = (server + '/push_here', server + '/call_this') |
| 456 contains_request = [{'h': item.digest, 's': item.size, 'i': 0}] |
| 457 contains_response = [push_urls] |
| 458 self._requests = [ |
| 459 self.mock_handshake_request(server, token), |
| 460 self.mock_contains_request( |
| 461 server, namespace, token, contains_request, contains_response), |
| 462 ( |
| 463 push_urls[0], |
| 464 { |
| 465 'data': data, |
| 466 'content_type': 'application/octet-stream', |
| 467 'method': 'PUT', |
| 468 }, |
| 469 '' |
| 470 ), |
| 471 ( |
| 472 push_urls[1], |
| 473 { |
| 474 'data': '', |
| 475 'content_type': 'application/json', |
| 476 'method': 'POST', |
| 477 }, |
| 478 '' |
| 479 ), |
| 480 ] |
| 481 storage = isolateserver.IsolateServer(server, namespace) |
| 482 missing = storage.contains([item]) |
| 483 self.assertEqual([item], missing) |
| 484 storage.push(item, [data], len(data)) |
| 485 self.assertTrue(item.push_state.uploaded) |
| 486 self.assertTrue(item.push_state.finalized) |
| 487 |
| 488 def test_push_failure_upload(self): |
| 489 server = 'http://example.com' |
| 490 namespace = 'default' |
| 491 token = 'fake token' |
| 492 data = ''.join(str(x) for x in xrange(1000)) |
| 493 item = FakeItem(data) |
| 494 push_urls = (server + '/push_here', server + '/call_this') |
| 495 contains_request = [{'h': item.digest, 's': item.size, 'i': 0}] |
| 496 contains_response = [push_urls] |
| 497 self._requests = [ |
| 498 self.mock_handshake_request(server, token), |
| 499 self.mock_contains_request( |
| 500 server, namespace, token, contains_request, contains_response), |
| 501 ( |
| 502 push_urls[0], |
| 503 { |
| 504 'data': data, |
| 505 'content_type': 'application/octet-stream', |
| 506 'method': 'PUT', |
| 507 }, |
| 508 None |
| 509 ), |
| 510 ] |
| 511 storage = isolateserver.IsolateServer(server, namespace) |
| 512 missing = storage.contains([item]) |
| 513 self.assertEqual([item], missing) |
| 514 with self.assertRaises(IOError): |
| 515 storage.push(item, [data], len(data)) |
| 516 self.assertFalse(item.push_state.uploaded) |
| 517 self.assertFalse(item.push_state.finalized) |
| 518 |
| 519 def test_push_failure_finalize(self): |
| 520 server = 'http://example.com' |
| 521 namespace = 'default' |
| 522 token = 'fake token' |
| 523 data = ''.join(str(x) for x in xrange(1000)) |
| 524 item = FakeItem(data) |
| 525 push_urls = (server + '/push_here', server + '/call_this') |
| 526 contains_request = [{'h': item.digest, 's': item.size, 'i': 0}] |
| 527 contains_response = [push_urls] |
| 528 self._requests = [ |
| 529 self.mock_handshake_request(server, token), |
| 530 self.mock_contains_request( |
| 531 server, namespace, token, contains_request, contains_response), |
| 532 ( |
| 533 push_urls[0], |
| 534 { |
| 535 'data': data, |
| 536 'content_type': 'application/octet-stream', |
| 537 'method': 'PUT', |
| 538 }, |
| 539 '' |
| 540 ), |
| 541 ( |
| 542 push_urls[1], |
| 543 { |
| 544 'data': '', |
| 545 'content_type': 'application/json', |
| 546 'method': 'POST', |
| 547 }, |
| 548 None |
| 549 ), |
| 550 ] |
| 551 storage = isolateserver.IsolateServer(server, namespace) |
| 552 missing = storage.contains([item]) |
| 553 self.assertEqual([item], missing) |
| 554 with self.assertRaises(IOError): |
| 555 storage.push(item, [data], len(data)) |
| 556 self.assertTrue(item.push_state.uploaded) |
| 557 self.assertFalse(item.push_state.finalized) |
| 558 |
| 559 def test_contains_success(self): |
| 560 server = 'http://example.com' |
| 561 namespace = 'default' |
| 562 token = 'fake token' |
| 300 files = [ | 563 files = [ |
| 301 os.path.join(BASE_PATH, 'isolateserver', f) | 564 FakeItem('1', is_isolated=True), |
| 302 for f in ('small_file.txt', 'empty_file.txt') | 565 FakeItem('2' * 100), |
| 303 ] | 566 FakeItem('3' * 200), |
| 304 hash_encoded = ''.join( | 567 ] |
| 305 binascii.unhexlify(isolateserver.hash_file(f, ALGO)) for f in files) | 568 request = [ |
| 306 path = 'http://random/' | 569 {'h': files[0].digest, 's': files[0].size, 'i': 1}, |
| 307 self._requests = [ | 570 {'h': files[1].digest, 's': files[1].size, 'i': 0}, |
| 308 (path + 'content/get_token', {}, 'foo bar'), | 571 {'h': files[2].digest, 's': files[2].size, 'i': 0}, |
| 309 ( | 572 ] |
| 310 path + 'content/contains/default-gzip?token=foo%20bar', | 573 response = [ |
| 311 {'data': hash_encoded, 'content_type': 'application/octet-stream'}, | 574 None, |
| 312 '\1\1', | 575 ['http://example/upload_here_1', None], |
| 313 ), | 576 ['http://example/upload_here_2', 'http://example/call_this'], |
| 314 ] | 577 ] |
| 315 result = isolateserver.main(['archive', '--isolate-server', path] + files) | 578 missing = [ |
| 316 self.assertEqual(0, result) | 579 files[1], |
| 317 | 580 files[2], |
| 318 def test_missing(self): | 581 ] |
| 319 files = [ | 582 self._requests = [ |
| 320 os.path.join(BASE_PATH, 'isolateserver', f) | 583 self.mock_handshake_request(server, token), |
| 321 for f in ('small_file.txt', 'empty_file.txt') | 584 self.mock_contains_request(server, namespace, token, request, response), |
| 322 ] | 585 ] |
| 323 hashes = [isolateserver.hash_file(f, ALGO) for f in files] | 586 storage = isolateserver.IsolateServer(server, namespace) |
| 324 hash_encoded = ''.join(map(binascii.unhexlify, hashes)) | 587 result = storage.contains(files) |
| 325 compressed = [ | 588 self.assertEqual(missing, result) |
| 326 zlib.compress( | 589 self.assertEqual( |
| 327 open(f, 'rb').read(), | 590 [x for x in response if x], |
| 328 isolateserver.get_zip_compression_level(f)) | 591 [[i.push_state.upload_url, i.push_state.finalize_url] for i in missing]) |
| 329 for f in files | 592 |
| 330 ] | 593 def test_contains_network_failure(self): |
| 331 path = 'http://random/' | 594 server = 'http://example.com' |
| 332 self._requests = [ | 595 namespace = 'default' |
| 333 (path + 'content/get_token', {}, 'foo bar'), | 596 token = 'fake token' |
| 334 ( | 597 req = self.mock_contains_request(server, namespace, token, [], []) |
| 335 path + 'content/contains/default-gzip?token=foo%20bar', | 598 self._requests = [ |
| 336 {'data': hash_encoded, 'content_type': 'application/octet-stream'}, | 599 self.mock_handshake_request(server, token), |
| 337 '\0\0', | 600 (req[0], req[1], None), |
| 338 ), | 601 ] |
| 339 ( | 602 storage = isolateserver.IsolateServer(server, namespace) |
| 340 path + 'content/store/default-gzip/%s?token=foo%%20bar' % hashes[0], | 603 with self.assertRaises(isolateserver.MappingError): |
| 341 {'data': compressed[0], 'content_type': 'application/octet-stream'}, | 604 storage.contains([]) |
| 342 'ok', | 605 |
| 343 ), | 606 def test_contains_format_failure(self): |
| 344 ( | 607 server = 'http://example.com' |
| 345 path + 'content/store/default-gzip/%s?token=foo%%20bar' % hashes[1], | 608 namespace = 'default' |
| 346 {'data': compressed[1], 'content_type': 'application/octet-stream'}, | 609 token = 'fake token' |
| 347 'ok', | 610 self._requests = [ |
| 348 ), | 611 self.mock_handshake_request(server, token), |
| 349 ] | 612 self.mock_contains_request(server, namespace, token, [], [1, 2, 3]) |
| 350 result = isolateserver.main(['archive', '--isolate-server', path] + files) | 613 ] |
| 351 self.assertEqual(0, result) | 614 storage = isolateserver.IsolateServer(server, namespace) |
| 352 | 615 with self.assertRaises(isolateserver.MappingError): |
| 353 def test_large(self): | 616 storage.contains([]) |
| 354 content = '' | |
| 355 compressed = '' | |
| 356 while ( | |
| 357 len(compressed) <= isolateserver.MIN_SIZE_FOR_DIRECT_BLOBSTORE): | |
| 358 # The goal here is to generate a file, once compressed, is at least | |
| 359 # MIN_SIZE_FOR_DIRECT_BLOBSTORE. | |
| 360 content += ''.join(chr(random.randint(0, 255)) for _ in xrange(20*1024)) | |
| 361 compressed = zlib.compress( | |
| 362 content, isolateserver.get_zip_compression_level('foo.txt')) | |
| 363 | |
| 364 s = ALGO(content).hexdigest() | |
| 365 infiles = { | |
| 366 'foo.txt': { | |
| 367 's': len(content), | |
| 368 'h': s, | |
| 369 }, | |
| 370 } | |
| 371 path = 'http://random/' | |
| 372 hash_encoded = binascii.unhexlify(s) | |
| 373 content_type, body = isolateserver.encode_multipart_formdata( | |
| 374 [('token', 'foo bar')], [('content', s, compressed)]) | |
| 375 | |
| 376 self._requests = [ | |
| 377 (path + 'content/get_token', {}, 'foo bar'), | |
| 378 ( | |
| 379 path + 'content/contains/default-gzip?token=foo%20bar', | |
| 380 {'data': hash_encoded, 'content_type': 'application/octet-stream'}, | |
| 381 '\0', | |
| 382 ), | |
| 383 ( | |
| 384 path + 'content/generate_blobstore_url/default-gzip/%s' % s, | |
| 385 {'data': [('token', 'foo bar')]}, | |
| 386 'an_url/', | |
| 387 ), | |
| 388 ( | |
| 389 'an_url/', | |
| 390 {'data': body, 'content_type': content_type, 'retry_50x': False}, | |
| 391 'ok', | |
| 392 ), | |
| 393 ] | |
| 394 | |
| 395 # Setup mocks for zip_compress to return |compressed|. | |
| 396 self.mock(isolateserver, 'file_read', lambda *_: None) | |
| 397 self.mock(isolateserver, 'zip_compress', lambda *_: [compressed]) | |
| 398 result = isolateserver.upload_tree( | |
| 399 base_url=path, | |
| 400 indir=os.getcwd(), | |
| 401 infiles=infiles, | |
| 402 namespace='default-gzip') | |
| 403 | |
| 404 self.assertEqual(0, result) | |
| 405 | |
| 406 def test_upload_blobstore_simple(self): | |
| 407 # A tad over 20kb so it triggers uploading to the blob store. | |
| 408 content = '0123456789' * 21*1024 | |
| 409 s = ALGO(content).hexdigest() | |
| 410 path = 'http://example.com:80/' | |
| 411 data = [('token', 'a_token')] | |
| 412 content_type, body = isolateserver.encode_multipart_formdata( | |
| 413 data, [('content', s, content)]) | |
| 414 self._requests = [ | |
| 415 ( | |
| 416 path + 'content/get_token', | |
| 417 {}, | |
| 418 'a_token', | |
| 419 ), | |
| 420 ( | |
| 421 path + 'content/generate_blobstore_url/x/' + s, | |
| 422 {'data': data[:]}, | |
| 423 'http://example.com/an_url/', | |
| 424 ), | |
| 425 ( | |
| 426 'http://example.com/an_url/', | |
| 427 {'data': body, 'content_type': content_type, 'retry_50x': False}, | |
| 428 'ok42', | |
| 429 ), | |
| 430 ] | |
| 431 # |size| is currently ignored. | |
| 432 result = isolateserver.IsolateServer(path, 'x').push(s, -2, [content]) | |
| 433 self.assertEqual('ok42', result) | |
| 434 | |
| 435 def test_upload_blobstore_retry_500(self): | |
| 436 # A tad over 20kb so it triggers uploading to the blob store. | |
| 437 content = '0123456789' * 21*1024 | |
| 438 s = ALGO(content).hexdigest() | |
| 439 path = 'http://example.com:80/' | |
| 440 data = [('token', 'a_token')] | |
| 441 content_type, body = isolateserver.encode_multipart_formdata( | |
| 442 data, [('content', s, content)]) | |
| 443 self._requests = [ | |
| 444 ( | |
| 445 path + 'content/get_token', | |
| 446 {}, | |
| 447 'a_token', | |
| 448 ), | |
| 449 ( | |
| 450 path + 'content/generate_blobstore_url/x/' + s, | |
| 451 {'data': data[:]}, | |
| 452 'http://example.com/an_url/', | |
| 453 ), | |
| 454 ( | |
| 455 'http://example.com/an_url/', | |
| 456 {'data': body, 'content_type': content_type, 'retry_50x': False}, | |
| 457 # Let's say an HTTP 500 was returned. | |
| 458 None, | |
| 459 ), | |
| 460 # In that case, a new url must be generated since the last one may have | |
| 461 # been "consumed". | |
| 462 ( | |
| 463 path + 'content/generate_blobstore_url/x/' + s, | |
| 464 {'data': data[:]}, | |
| 465 'http://example.com/an_url_2/', | |
| 466 ), | |
| 467 ( | |
| 468 'http://example.com/an_url_2/', | |
| 469 {'data': body, 'content_type': content_type, 'retry_50x': False}, | |
| 470 'ok42', | |
| 471 ), | |
| 472 ] | |
| 473 # |size| is currently ignored. | |
| 474 result = isolateserver.IsolateServer(path, 'x').push(s, -2, [content]) | |
| 475 self.assertEqual('ok42', result) | |
| 476 | 617 |
| 477 | 618 |
| 478 class IsolateServerDownloadTest(TestCase): | 619 class IsolateServerDownloadTest(TestCase): |
| 479 tempdir = None | 620 tempdir = None |
| 480 | 621 |
| 481 def tearDown(self): | 622 def tearDown(self): |
| 482 try: | 623 try: |
| 483 if self.tempdir: | 624 if self.tempdir: |
| 484 shutil.rmtree(self.tempdir) | 625 shutil.rmtree(self.tempdir) |
| 485 finally: | 626 finally: |
| 486 super(IsolateServerDownloadTest, self).tearDown() | 627 super(IsolateServerDownloadTest, self).tearDown() |
| 487 | 628 |
| 488 def test_download_two_files(self): | 629 def test_download_two_files(self): |
| 489 # Test downloading two files. | 630 # Test downloading two files. |
| 490 actual = {} | 631 actual = {} |
| 491 def out(key, generator): | 632 def out(key, generator): |
| 492 actual[key] = ''.join(generator) | 633 actual[key] = ''.join(generator) |
| 493 self.mock(isolateserver, 'file_write', out) | 634 self.mock(isolateserver, 'file_write', out) |
| 494 server = 'http://example.com' | 635 server = 'http://example.com' |
| 495 self._requests = [ | 636 self._requests = [ |
| 496 ( | 637 ( |
| 497 server + '/content/retrieve/default-gzip/sha-1', | 638 server + '/content-gs/retrieve/default-gzip/sha-1', |
| 498 {'read_timeout': 60, 'retry_404': True}, | 639 {'read_timeout': 60, 'retry_404': True}, |
| 499 zlib.compress('Coucou'), | 640 zlib.compress('Coucou'), |
| 500 ), | 641 ), |
| 501 ( | 642 ( |
| 502 server + '/content/retrieve/default-gzip/sha-2', | 643 server + '/content-gs/retrieve/default-gzip/sha-2', |
| 503 {'read_timeout': 60, 'retry_404': True}, | 644 {'read_timeout': 60, 'retry_404': True}, |
| 504 zlib.compress('Bye Bye'), | 645 zlib.compress('Bye Bye'), |
| 505 ), | 646 ), |
| 506 ] | 647 ] |
| 507 cmd = [ | 648 cmd = [ |
| 508 'download', | 649 'download', |
| 509 '--isolate-server', server, | 650 '--isolate-server', server, |
| 510 '--target', ROOT_DIR, | 651 '--target', ROOT_DIR, |
| 511 '--file', 'sha-1', 'path/to/a', | 652 '--file', 'sha-1', 'path/to/a', |
| 512 '--file', 'sha-2', 'path/to/b', | 653 '--file', 'sha-2', 'path/to/b', |
| (...skipping 27 matching lines...) Expand all Loading... |
| 540 'files': dict( | 681 'files': dict( |
| 541 (k, {'h': ALGO(v).hexdigest(), 's': len(v)}) | 682 (k, {'h': ALGO(v).hexdigest(), 's': len(v)}) |
| 542 for k, v in files.iteritems()), | 683 for k, v in files.iteritems()), |
| 543 } | 684 } |
| 544 isolated_data = json.dumps(isolated, sort_keys=True, separators=(',',':')) | 685 isolated_data = json.dumps(isolated, sort_keys=True, separators=(',',':')) |
| 545 isolated_hash = ALGO(isolated_data).hexdigest() | 686 isolated_hash = ALGO(isolated_data).hexdigest() |
| 546 requests = [(v['h'], files[k]) for k, v in isolated['files'].iteritems()] | 687 requests = [(v['h'], files[k]) for k, v in isolated['files'].iteritems()] |
| 547 requests.append((isolated_hash, isolated_data)) | 688 requests.append((isolated_hash, isolated_data)) |
| 548 self._requests = [ | 689 self._requests = [ |
| 549 ( | 690 ( |
| 550 server + '/content/retrieve/default-gzip/' + h, | 691 server + '/content-gs/retrieve/default-gzip/' + h, |
| 551 { | 692 { |
| 552 'read_timeout': isolateserver.DOWNLOAD_READ_TIMEOUT, | 693 'read_timeout': isolateserver.DOWNLOAD_READ_TIMEOUT, |
| 553 'retry_404': True, | 694 'retry_404': True, |
| 554 }, | 695 }, |
| 555 zlib.compress(v), | 696 zlib.compress(v), |
| 556 ) for h, v in requests | 697 ) for h, v in requests |
| 557 ] | 698 ] |
| 558 cmd = [ | 699 cmd = [ |
| 559 'download', | 700 'download', |
| 560 '--isolate-server', server, | 701 '--isolate-server', server, |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 649 expected = gen_data(os.path.sep) | 790 expected = gen_data(os.path.sep) |
| 650 self.assertEqual(expected, actual) | 791 self.assertEqual(expected, actual) |
| 651 | 792 |
| 652 | 793 |
| 653 if __name__ == '__main__': | 794 if __name__ == '__main__': |
| 654 if '-v' in sys.argv: | 795 if '-v' in sys.argv: |
| 655 unittest.TestCase.maxDiff = None | 796 unittest.TestCase.maxDiff = None |
| 656 logging.basicConfig( | 797 logging.basicConfig( |
| 657 level=(logging.DEBUG if '-v' in sys.argv else logging.ERROR)) | 798 level=(logging.DEBUG if '-v' in sys.argv else logging.ERROR)) |
| 658 unittest.main() | 799 unittest.main() |
| OLD | NEW |