blob: ae2f7482e7912d178fb211b038f33b5f9fedc9aa [file] [log] [blame]
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import shutil
import sys
import tempfile
import unittest
import mock
from pyfakefs import fake_filesystem_unittest
import py_utils
from py_utils import cloud_storage
from py_utils import lock
_CLOUD_STORAGE_GLOBAL_LOCK_PATH = os.path.join(
os.path.dirname(__file__), 'cloud_storage_global_lock.py')
def _FakeReadHash(_):
return 'hashthis!'
def _FakeCalulateHashMatchesRead(_):
return 'hashthis!'
def _FakeCalulateHashNewHash(_):
return 'omgnewhash'
class BaseFakeFsUnitTest(fake_filesystem_unittest.TestCase):
def setUp(self):
self.original_environ = os.environ.copy()
os.environ['DISABLE_CLOUD_STORAGE_IO'] = ''
self.setUpPyfakefs()
self.fs.CreateFile(
os.path.join(py_utils.GetCatapultDir(),
'third_party', 'gsutil', 'gsutil'))
def CreateFiles(self, file_paths):
for f in file_paths:
self.fs.CreateFile(f)
def tearDown(self):
self.tearDownPyfakefs()
os.environ = self.original_environ
def _FakeRunCommand(self, cmd):
pass
def _FakeGet(self, bucket, remote_path, local_path):
pass
class CloudStorageFakeFsUnitTest(BaseFakeFsUnitTest):
def _AssertRunCommandRaisesError(self, communicate_strs, error):
with mock.patch('py_utils.cloud_storage.subprocess.Popen') as popen:
p_mock = mock.Mock()
popen.return_value = p_mock
p_mock.returncode = 1
for stderr in communicate_strs:
p_mock.communicate.return_value = ('', stderr)
self.assertRaises(error, cloud_storage._RunCommand, [])
def testRunCommandCredentialsError(self):
strs = ['You are attempting to access protected data with no configured',
'Failure: No handler was ready to authenticate.']
self._AssertRunCommandRaisesError(strs, cloud_storage.CredentialsError)
def testRunCommandPermissionError(self):
strs = ['status=403', 'status 403', '403 Forbidden']
self._AssertRunCommandRaisesError(strs, cloud_storage.PermissionError)
def testRunCommandNotFoundError(self):
strs = ['InvalidUriError', 'No such object', 'No URLs matched',
'One or more URLs matched no', 'InvalidUriError']
self._AssertRunCommandRaisesError(strs, cloud_storage.NotFoundError)
def testRunCommandServerError(self):
strs = ['500 Internal Server Error']
self._AssertRunCommandRaisesError(strs, cloud_storage.ServerError)
def testRunCommandGenericError(self):
strs = ['Random string']
self._AssertRunCommandRaisesError(strs, cloud_storage.CloudStorageError)
def testInsertCreatesValidCloudUrl(self):
orig_run_command = cloud_storage._RunCommand
try:
cloud_storage._RunCommand = self._FakeRunCommand
remote_path = 'test-remote-path.html'
local_path = 'test-local-path.html'
cloud_url = cloud_storage.Insert(cloud_storage.PUBLIC_BUCKET,
remote_path, local_path)
self.assertEqual('https://console.developers.google.com/m/cloudstorage'
'/b/chromium-telemetry/o/test-remote-path.html',
cloud_url)
finally:
cloud_storage._RunCommand = orig_run_command
@mock.patch('py_utils.cloud_storage.subprocess')
def testExistsReturnsFalse(self, subprocess_mock):
p_mock = mock.Mock()
subprocess_mock.Popen.return_value = p_mock
p_mock.communicate.return_value = (
'',
'CommandException: One or more URLs matched no objects.\n')
p_mock.returncode_result = 1
self.assertFalse(cloud_storage.Exists('fake bucket',
'fake remote path'))
@unittest.skipIf(sys.platform.startswith('win'),
'https://github.com/catapult-project/catapult/issues/1861')
def testGetFilesInDirectoryIfChanged(self):
self.CreateFiles([
'real_dir_path/dir1/1file1.sha1',
'real_dir_path/dir1/1file2.txt',
'real_dir_path/dir1/1file3.sha1',
'real_dir_path/dir2/2file.txt',
'real_dir_path/dir3/3file1.sha1'])
def IncrementFilesUpdated(*_):
IncrementFilesUpdated.files_updated += 1
IncrementFilesUpdated.files_updated = 0
orig_get_if_changed = cloud_storage.GetIfChanged
cloud_storage.GetIfChanged = IncrementFilesUpdated
try:
self.assertRaises(ValueError, cloud_storage.GetFilesInDirectoryIfChanged,
os.path.abspath(os.sep), cloud_storage.PUBLIC_BUCKET)
self.assertEqual(0, IncrementFilesUpdated.files_updated)
self.assertRaises(ValueError, cloud_storage.GetFilesInDirectoryIfChanged,
'fake_dir_path', cloud_storage.PUBLIC_BUCKET)
self.assertEqual(0, IncrementFilesUpdated.files_updated)
cloud_storage.GetFilesInDirectoryIfChanged('real_dir_path',
cloud_storage.PUBLIC_BUCKET)
self.assertEqual(3, IncrementFilesUpdated.files_updated)
finally:
cloud_storage.GetIfChanged = orig_get_if_changed
def testCopy(self):
orig_run_command = cloud_storage._RunCommand
def AssertCorrectRunCommandArgs(args):
self.assertEqual(expected_args, args)
cloud_storage._RunCommand = AssertCorrectRunCommandArgs
expected_args = ['cp', 'gs://bucket1/remote_path1',
'gs://bucket2/remote_path2']
try:
cloud_storage.Copy('bucket1', 'bucket2', 'remote_path1', 'remote_path2')
finally:
cloud_storage._RunCommand = orig_run_command
@mock.patch('py_utils.cloud_storage._FileLock')
def testDisableCloudStorageIo(self, unused_lock_mock):
os.environ['DISABLE_CLOUD_STORAGE_IO'] = '1'
dir_path = 'real_dir_path'
self.fs.CreateDirectory(dir_path)
file_path = os.path.join(dir_path, 'file1')
file_path_sha = file_path + '.sha1'
def CleanTimeStampFile():
os.remove(file_path + '.fetchts')
self.CreateFiles([file_path, file_path_sha])
with open(file_path_sha, 'w') as f:
f.write('hash1234')
with self.assertRaises(cloud_storage.CloudStorageIODisabled):
cloud_storage.Copy('bucket1', 'bucket2', 'remote_path1', 'remote_path2')
with self.assertRaises(cloud_storage.CloudStorageIODisabled):
cloud_storage.Get('bucket', 'foo', file_path)
with self.assertRaises(cloud_storage.CloudStorageIODisabled):
cloud_storage.GetIfChanged(file_path, 'foo')
with self.assertRaises(cloud_storage.CloudStorageIODisabled):
cloud_storage.GetIfHashChanged('bar', file_path, 'bucket', 'hash1234')
with self.assertRaises(cloud_storage.CloudStorageIODisabled):
cloud_storage.Insert('bucket', 'foo', file_path)
CleanTimeStampFile()
with self.assertRaises(cloud_storage.CloudStorageIODisabled):
cloud_storage.GetFilesInDirectoryIfChanged(dir_path, 'bucket')
class GetIfChangedTests(BaseFakeFsUnitTest):
def setUp(self):
super(GetIfChangedTests, self).setUp()
self._orig_read_hash = cloud_storage.ReadHash
self._orig_calculate_hash = cloud_storage.CalculateHash
def tearDown(self):
super(GetIfChangedTests, self).tearDown()
cloud_storage.CalculateHash = self._orig_calculate_hash
cloud_storage.ReadHash = self._orig_read_hash
@mock.patch('py_utils.cloud_storage._FileLock')
@mock.patch('py_utils.cloud_storage._GetLocked')
def testHashPathDoesNotExists(self, unused_get_locked, unused_lock_mock):
cloud_storage.ReadHash = _FakeReadHash
cloud_storage.CalculateHash = _FakeCalulateHashMatchesRead
file_path = 'test-file-path.wpr'
cloud_storage._GetLocked = self._FakeGet
# hash_path doesn't exist.
self.assertFalse(cloud_storage.GetIfChanged(file_path,
cloud_storage.PUBLIC_BUCKET))
@mock.patch('py_utils.cloud_storage._FileLock')
@mock.patch('py_utils.cloud_storage._GetLocked')
def testHashPathExistsButFilePathDoesNot(
self, unused_get_locked, unused_lock_mock):
cloud_storage.ReadHash = _FakeReadHash
cloud_storage.CalculateHash = _FakeCalulateHashMatchesRead
file_path = 'test-file-path.wpr'
hash_path = file_path + '.sha1'
# hash_path exists, but file_path doesn't.
self.CreateFiles([hash_path])
self.assertTrue(cloud_storage.GetIfChanged(file_path,
cloud_storage.PUBLIC_BUCKET))
@mock.patch('py_utils.cloud_storage._FileLock')
@mock.patch('py_utils.cloud_storage._GetLocked')
def testHashPathAndFileHashExistWithSameHash(
self, unused_get_locked, unused_lock_mock):
cloud_storage.ReadHash = _FakeReadHash
cloud_storage.CalculateHash = _FakeCalulateHashMatchesRead
file_path = 'test-file-path.wpr'
# hash_path and file_path exist, and have same hash.
self.CreateFiles([file_path])
self.assertFalse(cloud_storage.GetIfChanged(file_path,
cloud_storage.PUBLIC_BUCKET))
@mock.patch('py_utils.cloud_storage._FileLock')
@mock.patch('py_utils.cloud_storage._GetLocked')
def testHashPathAndFileHashExistWithDifferentHash(
self, mock_get_locked, unused_get_locked):
cloud_storage.ReadHash = _FakeReadHash
cloud_storage.CalculateHash = _FakeCalulateHashNewHash
file_path = 'test-file-path.wpr'
hash_path = file_path + '.sha1'
def _FakeGetLocked(bucket, expected_hash, file_path):
del bucket, expected_hash, file_path # unused
cloud_storage.CalculateHash = _FakeCalulateHashMatchesRead
mock_get_locked.side_effect = _FakeGetLocked
self.CreateFiles([file_path, hash_path])
# hash_path and file_path exist, and have different hashes.
self.assertTrue(cloud_storage.GetIfChanged(file_path,
cloud_storage.PUBLIC_BUCKET))
@mock.patch('py_utils.cloud_storage._FileLock')
@mock.patch('py_utils.cloud_storage.CalculateHash')
@mock.patch('py_utils.cloud_storage._GetLocked')
def testNoHashComputationNeededUponSecondCall(
self, mock_get_locked, mock_calculate_hash, unused_get_locked):
mock_calculate_hash.side_effect = _FakeCalulateHashNewHash
cloud_storage.ReadHash = _FakeReadHash
file_path = 'test-file-path.wpr'
hash_path = file_path + '.sha1'
def _FakeGetLocked(bucket, expected_hash, file_path):
del bucket, expected_hash, file_path # unused
cloud_storage.CalculateHash = _FakeCalulateHashMatchesRead
mock_get_locked.side_effect = _FakeGetLocked
self.CreateFiles([file_path, hash_path])
# hash_path and file_path exist, and have different hashes. This first call
# will invoke a fetch.
self.assertTrue(cloud_storage.GetIfChanged(file_path,
cloud_storage.PUBLIC_BUCKET))
# The fetch left a .fetchts file on machine.
self.assertTrue(os.path.exists(file_path + '.fetchts'))
# Subsequent invocations of GetIfChanged should not invoke CalculateHash.
mock_calculate_hash.assert_not_called()
self.assertFalse(cloud_storage.GetIfChanged(file_path,
cloud_storage.PUBLIC_BUCKET))
self.assertFalse(cloud_storage.GetIfChanged(file_path,
cloud_storage.PUBLIC_BUCKET))
@mock.patch('py_utils.cloud_storage._FileLock')
@mock.patch('py_utils.cloud_storage.CalculateHash')
@mock.patch('py_utils.cloud_storage._GetLocked')
def testRefetchingFileUponHashFileChange(
self, mock_get_locked, mock_calculate_hash, unused_get_locked):
mock_calculate_hash.side_effect = _FakeCalulateHashNewHash
cloud_storage.ReadHash = _FakeReadHash
file_path = 'test-file-path.wpr'
hash_path = file_path + '.sha1'
def _FakeGetLocked(bucket, expected_hash, file_path):
del bucket, expected_hash, file_path # unused
cloud_storage.CalculateHash = _FakeCalulateHashMatchesRead
mock_get_locked.side_effect = _FakeGetLocked
self.CreateFiles([file_path, hash_path])
# hash_path and file_path exist, and have different hashes. This first call
# will invoke a fetch.
self.assertTrue(cloud_storage.GetIfChanged(file_path,
cloud_storage.PUBLIC_BUCKET))
# The fetch left a .fetchts file on machine.
self.assertTrue(os.path.exists(file_path + '.fetchts'))
with open(file_path + '.fetchts') as f:
fetchts = float(f.read())
# Updating the .sha1 hash_path file with the new hash after .fetchts
# is created.
file_obj = self.fs.GetObject(hash_path)
file_obj.SetMTime(fetchts + 100)
cloud_storage.ReadHash = lambda _: 'hashNeW'
def _FakeGetLockedNewHash(bucket, expected_hash, file_path):
del bucket, expected_hash, file_path # unused
cloud_storage.CalculateHash = lambda _: 'hashNeW'
mock_get_locked.side_effect = _FakeGetLockedNewHash
# hash_path and file_path exist, and have different hashes. This first call
# will invoke a fetch.
self.assertTrue(cloud_storage.GetIfChanged(file_path,
cloud_storage.PUBLIC_BUCKET))
class CloudStorageRealFsUnitTest(unittest.TestCase):
def setUp(self):
self.original_environ = os.environ.copy()
os.environ['DISABLE_CLOUD_STORAGE_IO'] = ''
def tearDown(self):
os.environ = self.original_environ
@mock.patch('py_utils.cloud_storage.LOCK_ACQUISITION_TIMEOUT', .005)
def testGetPseudoLockUnavailableCausesTimeout(self):
with tempfile.NamedTemporaryFile(suffix='.pseudo_lock') as pseudo_lock_fd:
with lock.FileLock(pseudo_lock_fd, lock.LOCK_EX | lock.LOCK_NB):
with self.assertRaises(py_utils.TimeoutException):
file_path = pseudo_lock_fd.name.replace('.pseudo_lock', '')
cloud_storage.GetIfChanged(file_path, cloud_storage.PUBLIC_BUCKET)
@mock.patch('py_utils.cloud_storage.LOCK_ACQUISITION_TIMEOUT', .005)
def testGetGlobalLockUnavailableCausesTimeout(self):
with open(_CLOUD_STORAGE_GLOBAL_LOCK_PATH) as global_lock_fd:
with lock.FileLock(global_lock_fd, lock.LOCK_EX | lock.LOCK_NB):
tmp_dir = tempfile.mkdtemp()
try:
file_path = os.path.join(tmp_dir, 'foo')
with self.assertRaises(py_utils.TimeoutException):
cloud_storage.GetIfChanged(file_path, cloud_storage.PUBLIC_BUCKET)
finally:
shutil.rmtree(tmp_dir)
class CloudStorageErrorHandlingTest(unittest.TestCase):
def runTest(self):
self.assertIsInstance(cloud_storage.GetErrorObjectForCloudStorageStderr(
'ServiceException: 401 Anonymous users does not have '
'storage.objects.get access to object chrome-partner-telemetry'),
cloud_storage.CredentialsError)
self.assertIsInstance(cloud_storage.GetErrorObjectForCloudStorageStderr(
'403 Caller does not have storage.objects.list access to bucket '
'chrome-telemetry'), cloud_storage.PermissionError)