OLD | NEW |
(Empty) | |
| 1 # -*- coding: utf-8 -*- |
| 2 # Copyright (c) 2012 Thomas Parslow http://almostobsolete.net/ |
| 3 # |
| 4 # Permission is hereby granted, free of charge, to any person obtaining a |
| 5 # copy of this software and associated documentation files (the |
| 6 # "Software"), to deal in the Software without restriction, including |
| 7 # without limitation the rights to use, copy, modify, merge, publish, dis- |
| 8 # tribute, sublicense, and/or sell copies of the Software, and to permit |
| 9 # persons to whom the Software is furnished to do so, subject to the fol- |
| 10 # lowing conditions: |
| 11 # |
| 12 # The above copyright notice and this permission notice shall be included |
| 13 # in all copies or substantial portions of the Software. |
| 14 # |
| 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
| 17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
| 18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| 21 # IN THE SOFTWARE. |
| 22 # |
| 23 |
| 24 from tests.unit import unittest |
| 25 |
| 26 from mock import call, Mock, patch, sentinel |
| 27 |
| 28 from boto.glacier.layer1 import Layer1 |
| 29 from boto.glacier.layer2 import Layer2 |
| 30 import boto.glacier.vault |
| 31 from boto.glacier.vault import Vault |
| 32 from boto.glacier.vault import Job |
| 33 |
| 34 from StringIO import StringIO |
| 35 |
| 36 # Some fixture data from the Glacier docs |
| 37 FIXTURE_VAULT = { |
| 38 "CreationDate" : "2012-02-20T17:01:45.198Z", |
| 39 "LastInventoryDate" : "2012-03-20T17:03:43.221Z", |
| 40 "NumberOfArchives" : 192, |
| 41 "SizeInBytes" : 78088912, |
| 42 "VaultARN" : "arn:aws:glacier:us-east-1:012345678901:vaults/examplevault", |
| 43 "VaultName" : "examplevault" |
| 44 } |
| 45 |
| 46 FIXTURE_ARCHIVE_JOB = { |
| 47 "Action": "ArchiveRetrieval", |
| 48 "ArchiveId": ("NkbByEejwEggmBz2fTHgJrg0XBoDfjP4q6iu87-TjhqG6eGoOY9Z8i1_AUyUs" |
| 49 "uhPAdTqLHy8pTl5nfCFJmDl2yEZONi5L26Omw12vcs01MNGntHEQL8MBfGlqr" |
| 50 "EXAMPLEArchiveId"), |
| 51 "ArchiveSizeInBytes": 16777216, |
| 52 "Completed": False, |
| 53 "CreationDate": "2012-05-15T17:21:39.339Z", |
| 54 "CompletionDate": "2012-05-15T17:21:43.561Z", |
| 55 "InventorySizeInBytes": None, |
| 56 "JobDescription": "My ArchiveRetrieval Job", |
| 57 "JobId": ("HkF9p6o7yjhFx-K3CGl6fuSm6VzW9T7esGQfco8nUXVYwS0jlb5gq1JZ55yHgt5v" |
| 58 "P54ZShjoQzQVVh7vEXAMPLEjobID"), |
| 59 "SHA256TreeHash": ("beb0fe31a1c7ca8c6c04d574ea906e3f97b31fdca7571defb5b44dc" |
| 60 "a89b5af60"), |
| 61 "SNSTopic": "arn:aws:sns:us-east-1:012345678901:mytopic", |
| 62 "StatusCode": "InProgress", |
| 63 "StatusMessage": "Operation in progress.", |
| 64 "VaultARN": "arn:aws:glacier:us-east-1:012345678901:vaults/examplevault" |
| 65 } |
| 66 |
| 67 EXAMPLE_PART_LIST_RESULT_PAGE_1 = { |
| 68 "ArchiveDescription": "archive description 1", |
| 69 "CreationDate": "2012-03-20T17:03:43.221Z", |
| 70 "Marker": "MfgsKHVjbQ6EldVl72bn3_n5h2TaGZQUO-Qb3B9j3TITf7WajQ", |
| 71 "MultipartUploadId": "OW2fM5iVylEpFEMM9_HpKowRapC3vn5sSL39_396UW9zLFUWVrnRHa
PjUJddQ5OxSHVXjYtrN47NBZ-khxOjyEXAMPLE", |
| 72 "PartSizeInBytes": 4194304, |
| 73 "Parts": |
| 74 [ { |
| 75 "RangeInBytes": "4194304-8388607", |
| 76 "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" |
| 77 }], |
| 78 "VaultARN": "arn:aws:glacier:us-east-1:012345678901:vaults/demo1-vault" |
| 79 } |
| 80 |
| 81 # The documentation doesn't say whether the non-Parts fields are defined in |
| 82 # future pages, so assume they are not. |
| 83 EXAMPLE_PART_LIST_RESULT_PAGE_2 = { |
| 84 "ArchiveDescription": None, |
| 85 "CreationDate": None, |
| 86 "Marker": None, |
| 87 "MultipartUploadId": None, |
| 88 "PartSizeInBytes": None, |
| 89 "Parts": |
| 90 [ { |
| 91 "RangeInBytes": "0-4194303", |
| 92 "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" |
| 93 }], |
| 94 "VaultARN": None |
| 95 } |
| 96 |
| 97 EXAMPLE_PART_LIST_COMPLETE = { |
| 98 "ArchiveDescription": "archive description 1", |
| 99 "CreationDate": "2012-03-20T17:03:43.221Z", |
| 100 "Marker": None, |
| 101 "MultipartUploadId": "OW2fM5iVylEpFEMM9_HpKowRapC3vn5sSL39_396UW9zLFUWVrnRHa
PjUJddQ5OxSHVXjYtrN47NBZ-khxOjyEXAMPLE", |
| 102 "PartSizeInBytes": 4194304, |
| 103 "Parts": |
| 104 [ { |
| 105 "RangeInBytes": "4194304-8388607", |
| 106 "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" |
| 107 }, { |
| 108 "RangeInBytes": "0-4194303", |
| 109 "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" |
| 110 }], |
| 111 "VaultARN": "arn:aws:glacier:us-east-1:012345678901:vaults/demo1-vault" |
| 112 } |
| 113 |
| 114 |
| 115 class GlacierLayer2Base(unittest.TestCase): |
| 116 def setUp(self): |
| 117 self.mock_layer1 = Mock(spec=Layer1) |
| 118 |
| 119 |
| 120 class TestGlacierLayer2Connection(GlacierLayer2Base): |
| 121 def setUp(self): |
| 122 GlacierLayer2Base.setUp(self) |
| 123 self.layer2 = Layer2(layer1=self.mock_layer1) |
| 124 |
| 125 def test_create_vault(self): |
| 126 self.mock_layer1.describe_vault.return_value = FIXTURE_VAULT |
| 127 self.layer2.create_vault("My Vault") |
| 128 self.mock_layer1.create_vault.assert_called_with("My Vault") |
| 129 |
| 130 def test_get_vault(self): |
| 131 self.mock_layer1.describe_vault.return_value = FIXTURE_VAULT |
| 132 vault = self.layer2.get_vault("examplevault") |
| 133 self.assertEqual(vault.layer1, self.mock_layer1) |
| 134 self.assertEqual(vault.name, "examplevault") |
| 135 self.assertEqual(vault.size, 78088912) |
| 136 self.assertEqual(vault.number_of_archives, 192) |
| 137 |
| 138 def list_vaults(self): |
| 139 self.mock_layer1.list_vaults.return_value = [FIXTURE_VAULT] |
| 140 vaults = self.layer2.list_vaults() |
| 141 self.assertEqual(vaults[0].name, "examplevault") |
| 142 |
| 143 |
| 144 class TestVault(GlacierLayer2Base): |
| 145 def setUp(self): |
| 146 GlacierLayer2Base.setUp(self) |
| 147 self.vault = Vault(self.mock_layer1, FIXTURE_VAULT) |
| 148 |
| 149 # TODO: Tests for the other methods of uploading |
| 150 |
| 151 def test_create_archive_writer(self): |
| 152 self.mock_layer1.initiate_multipart_upload.return_value = { |
| 153 "UploadId": "UPLOADID"} |
| 154 writer = self.vault.create_archive_writer(description="stuff") |
| 155 self.mock_layer1.initiate_multipart_upload.assert_called_with( |
| 156 "examplevault", self.vault.DefaultPartSize, "stuff") |
| 157 self.assertEqual(writer.vault, self.vault) |
| 158 self.assertEqual(writer.upload_id, "UPLOADID") |
| 159 |
| 160 def test_delete_vault(self): |
| 161 self.vault.delete_archive("archive") |
| 162 self.mock_layer1.delete_archive.assert_called_with("examplevault", |
| 163 "archive") |
| 164 |
| 165 def test_get_job(self): |
| 166 self.mock_layer1.describe_job.return_value = FIXTURE_ARCHIVE_JOB |
| 167 job = self.vault.get_job( |
| 168 "NkbByEejwEggmBz2fTHgJrg0XBoDfjP4q6iu87-TjhqG6eGoOY9Z8i1_AUyUsuhPA" |
| 169 "dTqLHy8pTl5nfCFJmDl2yEZONi5L26Omw12vcs01MNGntHEQL8MBfGlqrEXAMPLEA" |
| 170 "rchiveId") |
| 171 self.assertEqual(job.action, "ArchiveRetrieval") |
| 172 |
| 173 def test_list_jobs(self): |
| 174 self.mock_layer1.list_jobs.return_value = { |
| 175 "JobList": [FIXTURE_ARCHIVE_JOB]} |
| 176 jobs = self.vault.list_jobs(False, "InProgress") |
| 177 self.mock_layer1.list_jobs.assert_called_with("examplevault", |
| 178 False, "InProgress") |
| 179 self.assertEqual(jobs[0].archive_id, |
| 180 "NkbByEejwEggmBz2fTHgJrg0XBoDfjP4q6iu87-TjhqG6eGoOY9Z" |
| 181 "8i1_AUyUsuhPAdTqLHy8pTl5nfCFJmDl2yEZONi5L26Omw12vcs0" |
| 182 "1MNGntHEQL8MBfGlqrEXAMPLEArchiveId") |
| 183 |
| 184 def test_list_all_parts_one_page(self): |
| 185 self.mock_layer1.list_parts.return_value = ( |
| 186 dict(EXAMPLE_PART_LIST_COMPLETE)) # take a copy |
| 187 parts_result = self.vault.list_all_parts(sentinel.upload_id) |
| 188 expected = [call('examplevault', sentinel.upload_id)] |
| 189 self.assertEquals(expected, self.mock_layer1.list_parts.call_args_list) |
| 190 self.assertEquals(EXAMPLE_PART_LIST_COMPLETE, parts_result) |
| 191 |
| 192 def test_list_all_parts_two_pages(self): |
| 193 self.mock_layer1.list_parts.side_effect = [ |
| 194 # take copies |
| 195 dict(EXAMPLE_PART_LIST_RESULT_PAGE_1), |
| 196 dict(EXAMPLE_PART_LIST_RESULT_PAGE_2) |
| 197 ] |
| 198 parts_result = self.vault.list_all_parts(sentinel.upload_id) |
| 199 expected = [call('examplevault', sentinel.upload_id), |
| 200 call('examplevault', sentinel.upload_id, |
| 201 marker=EXAMPLE_PART_LIST_RESULT_PAGE_1['Marker'])] |
| 202 self.assertEquals(expected, self.mock_layer1.list_parts.call_args_list) |
| 203 self.assertEquals(EXAMPLE_PART_LIST_COMPLETE, parts_result) |
| 204 |
| 205 @patch('boto.glacier.vault.resume_file_upload') |
| 206 def test_resume_archive_from_file(self, mock_resume_file_upload): |
| 207 part_size = 4 |
| 208 mock_list_parts = Mock() |
| 209 mock_list_parts.return_value = { |
| 210 'PartSizeInBytes': part_size, |
| 211 'Parts': [{ |
| 212 'RangeInBytes': '0-3', |
| 213 'SHA256TreeHash': '12', |
| 214 }, { |
| 215 'RangeInBytes': '4-6', |
| 216 'SHA256TreeHash': '34', |
| 217 }, |
| 218 ]} |
| 219 |
| 220 self.vault.list_all_parts = mock_list_parts |
| 221 self.vault.resume_archive_from_file( |
| 222 sentinel.upload_id, file_obj=sentinel.file_obj) |
| 223 mock_resume_file_upload.assert_called_once_with( |
| 224 self.vault, sentinel.upload_id, part_size, sentinel.file_obj, |
| 225 {0: '12'.decode('hex'), 1: '34'.decode('hex')}) |
| 226 |
| 227 |
| 228 class TestJob(GlacierLayer2Base): |
| 229 def setUp(self): |
| 230 GlacierLayer2Base.setUp(self) |
| 231 self.vault = Vault(self.mock_layer1, FIXTURE_VAULT) |
| 232 self.job = Job(self.vault, FIXTURE_ARCHIVE_JOB) |
| 233 |
| 234 def test_get_job_output(self): |
| 235 self.mock_layer1.get_job_output.return_value = "TEST_OUTPUT" |
| 236 self.job.get_output((0,100)) |
| 237 self.mock_layer1.get_job_output.assert_called_with( |
| 238 "examplevault", |
| 239 "HkF9p6o7yjhFx-K3CGl6fuSm6VzW9T7esGQfco8nUXVYwS0jlb5gq1JZ55yHgt5vP" |
| 240 "54ZShjoQzQVVh7vEXAMPLEjobID", (0,100)) |
| 241 |
| 242 class TestRangeStringParsing(unittest.TestCase): |
| 243 def test_simple_range(self): |
| 244 self.assertEquals( |
| 245 Vault._range_string_to_part_index('0-3', 4), 0) |
| 246 |
| 247 def test_range_one_too_big(self): |
| 248 # Off-by-one bug in Amazon's Glacier implementation |
| 249 # See: https://forums.aws.amazon.com/thread.jspa?threadID=106866&tstart=
0 |
| 250 # Workaround is to assume that if a (start, end] range appears to be |
| 251 # returned then that is what it is. |
| 252 self.assertEquals( |
| 253 Vault._range_string_to_part_index('0-4', 4), 0) |
| 254 |
| 255 def test_range_too_big(self): |
| 256 self.assertRaises( |
| 257 AssertionError, Vault._range_string_to_part_index, '0-5', 4) |
| 258 |
| 259 def test_range_start_mismatch(self): |
| 260 self.assertRaises( |
| 261 AssertionError, Vault._range_string_to_part_index, '1-3', 4) |
| 262 |
| 263 def test_range_end_mismatch(self): |
| 264 # End mismatch is OK, since the last part might be short |
| 265 self.assertEquals( |
| 266 Vault._range_string_to_part_index('0-2', 4), 0) |
OLD | NEW |