| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2016 The LUCI Authors. All rights reserved. | 2 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 | 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. | 4 # that can be found in the LICENSE file. |
| 5 | 5 |
| 6 import base64 | 6 import base64 |
| 7 import json | 7 import json |
| 8 import unittest | 8 import unittest |
| 9 | 9 |
| 10 import test_env | 10 import test_env |
| 11 | 11 |
| 12 import mock | 12 import mock |
| 13 import subprocess42 | 13 import subprocess42 |
| 14 | 14 |
| 15 from recipe_engine import fetch | 15 from recipe_engine import fetch |
| 16 from recipe_engine import requests_ssl | 16 from recipe_engine import requests_ssl |
| 17 | 17 |
| 18 def mock_git_dir(*args): |
| 19 return mock.call('GIT', '-C', 'dir', *args) |
| 20 |
| 18 | 21 |
| 19 class TestGit(unittest.TestCase): | 22 class TestGit(unittest.TestCase): |
| 20 | 23 |
| 21 def setUp(self): | 24 def setUp(self): |
| 22 self._patchers = [ | 25 self._patchers = [ |
| 23 mock.patch('logging.warning'), | 26 mock.patch('logging.warning'), |
| 24 mock.patch('logging.exception'), | 27 mock.patch('logging.exception'), |
| 25 mock.patch('recipe_engine.fetch.GitBackend.Git._resolve_git', | 28 mock.patch('recipe_engine.fetch.GitBackend.Git._resolve_git', |
| 26 return_value='GIT'), | 29 return_value='GIT'), |
| 30 mock.patch('os.path.isdir', return_value=False), |
| 31 mock.patch('os.makedirs'), |
| 27 ] | 32 ] |
| 28 for p in self._patchers: | 33 for p in self._patchers: |
| 29 p.start() | 34 p.start() |
| 30 | 35 |
| 31 def tearDown(self): | 36 def tearDown(self): |
| 32 for p in reversed(self._patchers): | 37 for p in reversed(self._patchers): |
| 33 p.stop() | 38 p.stop() |
| 34 | 39 |
| 35 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') | 40 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 36 def test_fresh_clone(self, git): | 41 def test_fresh_fetch_branch(self, git): |
| 37 fetch.GitBackend().checkout( | 42 fetch.GitBackend().checkout( |
| 38 'repo', 'revision', 'dir', allow_fetch=True) | 43 'repo', 'refs/heads/master', 'dir', allow_fetch=True) |
| 39 git.assert_has_calls([ | 44 git.assert_has_calls([ |
| 40 mock.call('GIT', 'clone', '-q', 'repo', 'dir'), | 45 mock_git_dir('init'), |
| 41 mock.call('GIT', '-C', 'dir', 'config', 'remote.origin.url', 'repo'), | 46 mock_git_dir('fetch', 'repo', 'refs/heads/master'), |
| 42 mock.call('GIT', '-C', 'dir', 'rev-parse', '-q', '--verify', | 47 mock_git_dir('checkout', '-q', '-f', 'FETCH_HEAD'), |
| 43 'revision^{commit}'), | 48 ]) |
| 44 mock.call('GIT', '-C', 'dir', 'reset', '-q', '--hard', 'revision'), | 49 |
| 50 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 51 def test_fresh_fetch_commit(self, git): |
| 52 fetch.GitBackend().checkout( |
| 53 'repo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'dir', |
| 54 allow_fetch=True) |
| 55 git.assert_has_calls([ |
| 56 mock_git_dir('init'), |
| 57 mock_git_dir('fetch', 'repo', 'refs/heads/master'), |
| 58 mock_git_dir( |
| 59 'checkout', '-q', '-f', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), |
| 45 ]) | 60 ]) |
| 46 | 61 |
| 47 @mock.patch('time.sleep') | 62 @mock.patch('time.sleep') |
| 48 @mock.patch('os.path.isdir') | |
| 49 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') | 63 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 50 def test_fresh_clone_retries(self, git, isdir, sleep): | 64 def test_fresh_fetch_retries(self, git, sleep): |
| 51 isdir.return_value = False | |
| 52 | 65 |
| 53 clone_fails = [] | 66 fetch_fails = [] |
| 54 def fail_four_clones(*args): | 67 def fail_four_fetches(*args): |
| 55 if 'clone' in args and len(clone_fails) < 4: | 68 if 'fetch' in args and len(fetch_fails) < 4: |
| 56 clone_fails.append(True) | 69 fetch_fails.append(True) |
| 57 raise subprocess42.CalledProcessError(1, args) | 70 raise subprocess42.CalledProcessError(1, args) |
| 58 return None | 71 return None |
| 59 git.side_effect = fail_four_clones | 72 git.side_effect = fail_four_fetches |
| 60 | 73 |
| 61 fetch.GitBackend().checkout( | 74 fetch.GitBackend().checkout( |
| 62 'repo', 'revision', 'dir', allow_fetch=True) | 75 'repo', 'refs/heads/master', 'dir', allow_fetch=True) |
| 63 git.assert_has_calls([ | 76 git.assert_has_calls([ |
| 64 mock.call('GIT', 'clone', '-q', 'repo', 'dir')] * 5 + [ | 77 mock_git_dir('init'), |
| 65 mock.call('GIT', '-C', 'dir', 'config', 'remote.origin.url', 'repo'), | 78 mock_git_dir('fetch', 'repo', 'refs/heads/master'), |
| 66 mock.call('GIT', '-C', 'dir', 'rev-parse', '-q', '--verify', | 79 mock_git_dir('checkout', '-q', '-f', 'FETCH_HEAD'), |
| 67 'revision^{commit}'), | |
| 68 mock.call('GIT', '-C', 'dir', 'reset', '-q', '--hard', 'revision'), | |
| 69 ]) | 80 ]) |
| 81 self.assertEqual(len(fetch_fails), 4) |
| 70 | 82 |
| 71 @mock.patch('os.path.isdir') | 83 @mock.patch('os.path.isdir') |
| 72 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') | 84 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 73 def test_existing_checkout(self, git, isdir): | 85 def test_existing_checkout_commit_present(self, git, isdir): |
| 74 isdir.return_value = True | 86 isdir.return_value = True |
| 75 fetch.GitBackend().checkout( | 87 fetch.GitBackend().checkout( |
| 76 'repo', 'revision', 'dir', allow_fetch=True) | 88 'repo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'dir', |
| 89 allow_fetch=True) |
| 77 isdir.assert_has_calls([ | 90 isdir.assert_has_calls([ |
| 78 mock.call('dir'), | 91 mock.call('dir'), |
| 79 mock.call('dir/.git'), | 92 mock.call('dir/.git'), |
| 80 ]) | 93 ]) |
| 81 git.assert_has_calls([ | 94 git.assert_has_calls([ |
| 82 mock.call('GIT', '-C', 'dir', 'config', 'remote.origin.url', 'repo'), | 95 mock_git_dir( |
| 83 mock.call('GIT', '-C', 'dir', 'rev-parse', '-q', '--verify', | 96 'rev-parse', '-q', '--verify', |
| 84 'revision^{commit}'), | 97 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^{commit}'), |
| 85 mock.call('GIT', '-C', 'dir', 'reset', '-q', '--hard', 'revision'), | 98 mock_git_dir('checkout', '-q', '-f', |
| 99 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), |
| 100 ]) |
| 101 |
| 102 @mock.patch('os.path.isdir') |
| 103 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 104 def test_existing_checkout_commit_present_no_fetch(self, git, isdir): |
| 105 isdir.return_value = True |
| 106 fetch.GitBackend().checkout( |
| 107 'repo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'dir', |
| 108 allow_fetch=False) |
| 109 isdir.assert_has_calls([ |
| 110 mock.call('dir'), |
| 111 mock.call('dir/.git'), |
| 112 ]) |
| 113 git.assert_has_calls([ |
| 114 mock_git_dir( |
| 115 'rev-parse', '-q', '--verify', |
| 116 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^{commit}'), |
| 117 mock_git_dir('checkout', '-q', '-f', |
| 118 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), |
| 119 ]) |
| 120 |
| 121 @mock.patch('os.path.isdir') |
| 122 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 123 def test_existing_checkout_commit_not_present(self, git, isdir): |
| 124 isdir.return_value = True |
| 125 git.side_effect = [ |
| 126 subprocess42.CalledProcessError(1, ['git-rev-parse']), |
| 127 None, |
| 128 None, |
| 129 None, |
| 130 ] |
| 131 isdir.return_value = True |
| 132 fetch.GitBackend().checkout( |
| 133 'repo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'dir', |
| 134 allow_fetch=True) |
| 135 isdir.assert_has_calls([ |
| 136 mock.call('dir'), |
| 137 mock.call('dir/.git'), |
| 138 ]) |
| 139 git.assert_has_calls([ |
| 140 mock_git_dir( |
| 141 'rev-parse', '-q', '--verify', |
| 142 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^{commit}'), |
| 143 mock_git_dir('init'), |
| 144 mock_git_dir('fetch', 'repo', 'refs/heads/master'), |
| 145 mock_git_dir('checkout', '-q', '-f', |
| 146 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), |
| 147 ]) |
| 148 |
| 149 @mock.patch('os.path.isdir') |
| 150 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 151 def test_existing_checkout_commit_not_present_no_fetch(self, git, isdir): |
| 152 isdir.return_value = True |
| 153 git.side_effect = [ |
| 154 subprocess42.CalledProcessError(1, ['git-rev-parse']), |
| 155 ] |
| 156 isdir.return_value = True |
| 157 with self.assertRaises(fetch.FetchNotAllowedError): |
| 158 fetch.GitBackend().checkout( |
| 159 'repo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'dir', |
| 160 allow_fetch=False) |
| 161 isdir.assert_has_calls([ |
| 162 mock.call('dir'), |
| 163 mock.call('dir/.git'), |
| 164 ]) |
| 165 git.assert_has_calls([ |
| 166 mock_git_dir( |
| 167 'rev-parse', '-q', '--verify', |
| 168 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^{commit}'), |
| 169 ]) |
| 170 |
| 171 |
| 172 @mock.patch('os.path.isdir') |
| 173 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 174 def test_existing_checkout_branch(self, git, isdir): |
| 175 isdir.return_value = True |
| 176 fetch.GitBackend().checkout('repo', 'refs/heads/master', 'dir', |
| 177 allow_fetch=True) |
| 178 isdir.assert_has_calls([ |
| 179 mock.call('dir'), |
| 180 mock.call('dir/.git'), |
| 181 ]) |
| 182 git.assert_has_calls([ |
| 183 mock_git_dir('init'), |
| 184 mock_git_dir('fetch', 'repo', 'refs/heads/master'), |
| 185 mock_git_dir('checkout', '-q', '-f', 'FETCH_HEAD'), |
| 86 ]) | 186 ]) |
| 87 | 187 |
| 88 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') | 188 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 89 def test_clone_not_allowed(self, git): | 189 def test_fetch_not_allowed(self, git): |
| 90 with self.assertRaises(fetch.FetchNotAllowedError): | 190 with self.assertRaises(fetch.FetchNotAllowedError): |
| 91 fetch.GitBackend().checkout( | 191 fetch.GitBackend().checkout( |
| 92 'repo', 'revision', 'dir', allow_fetch=False) | 192 'repo', 'refs/heads/master', 'dir', allow_fetch=False) |
| 93 | 193 |
| 94 @mock.patch('os.path.isdir') | 194 @mock.patch('os.path.isdir') |
| 95 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') | 195 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 96 def test_unclean_filesystem(self, git, isdir): | 196 def test_unclean_filesystem(self, git, isdir): |
| 97 isdir.side_effect = [True, False] | 197 isdir.side_effect = [True, False] |
| 98 with self.assertRaises(fetch.UncleanFilesystemError): | 198 with self.assertRaises(fetch.UncleanFilesystemError): |
| 99 fetch.GitBackend().checkout( | 199 fetch.GitBackend().checkout( |
| 100 'repo', 'revision', 'dir', allow_fetch=False) | 200 'repo', 'refs/heads/master', 'dir', allow_fetch=False) |
| 101 isdir.assert_has_calls([ | 201 isdir.assert_has_calls([ |
| 102 mock.call('dir'), | 202 mock.call('dir'), |
| 103 mock.call('dir/.git'), | 203 mock.call('dir/.git'), |
| 104 ]) | 204 ]) |
| 105 | 205 |
| 106 @mock.patch('os.path.isdir') | |
| 107 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') | |
| 108 def test_origin_mismatch(self, git, isdir): | |
| 109 git.return_value = 'not-repo' | |
| 110 isdir.return_value = True | |
| 111 | |
| 112 # This should not raise UncleanFilesystemError, but instead | |
| 113 # set the right origin automatically. | |
| 114 fetch.GitBackend().checkout( | |
| 115 'repo', 'revision', 'dir', allow_fetch=False) | |
| 116 | |
| 117 isdir.assert_has_calls([ | |
| 118 mock.call('dir'), | |
| 119 mock.call('dir/.git'), | |
| 120 ]) | |
| 121 git.assert_has_calls([ | |
| 122 mock.call('GIT', '-C', 'dir', 'config', 'remote.origin.url', 'repo'), | |
| 123 ]) | |
| 124 | |
| 125 @mock.patch('os.path.isdir') | |
| 126 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') | |
| 127 def test_rev_parse_fail(self, git, isdir): | |
| 128 git.side_effect = [ | |
| 129 None, | |
| 130 subprocess42.CalledProcessError(1, ['fakecmd']), | |
| 131 None, | |
| 132 None, | |
| 133 ] | |
| 134 isdir.return_value = True | |
| 135 fetch.GitBackend().checkout( | |
| 136 'repo', 'revision', 'dir', allow_fetch=True) | |
| 137 isdir.assert_has_calls([ | |
| 138 mock.call('dir'), | |
| 139 mock.call('dir/.git'), | |
| 140 ]) | |
| 141 git.assert_has_calls([ | |
| 142 mock.call('GIT', '-C', 'dir', 'config', 'remote.origin.url', 'repo'), | |
| 143 mock.call('GIT', '-C', 'dir', 'rev-parse', '-q', '--verify', | |
| 144 'revision^{commit}'), | |
| 145 mock.call('GIT', '-C', 'dir', 'fetch'), | |
| 146 mock.call('GIT', '-C', 'dir', 'reset', '-q', '--hard', 'revision'), | |
| 147 ]) | |
| 148 | |
| 149 @mock.patch('os.path.isdir') | |
| 150 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') | |
| 151 def test_rev_parse_fetch_not_allowed(self, git, isdir): | |
| 152 git.side_effect = [ | |
| 153 None, | |
| 154 subprocess42.CalledProcessError(1, ['fakecmd']), | |
| 155 ] | |
| 156 isdir.return_value = True | |
| 157 with self.assertRaises(fetch.FetchNotAllowedError): | |
| 158 fetch.GitBackend().checkout( | |
| 159 'repo', 'revision', 'dir', allow_fetch=False) | |
| 160 isdir.assert_has_calls([ | |
| 161 mock.call('dir'), | |
| 162 mock.call('dir/.git'), | |
| 163 ]) | |
| 164 git.assert_has_calls([ | |
| 165 mock.call('GIT', '-C', 'dir', 'config', 'remote.origin.url', 'repo'), | |
| 166 mock.call('GIT', '-C', 'dir', 'rev-parse', '-q', '--verify', | |
| 167 'revision^{commit}'), | |
| 168 ]) | |
| 169 | |
| 170 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') | 206 @mock.patch('recipe_engine.fetch.GitBackend.Git._execute') |
| 171 def test_commit_metadata(self, git): | 207 def test_commit_metadata(self, git): |
| 172 git.side_effect = ['author', 'message'] | 208 git.side_effect = ['author', 'message'] |
| 173 result = fetch.GitBackend().commit_metadata( | 209 result = fetch.GitBackend().commit_metadata( |
| 174 'repo', 'revision', 'dir', allow_fetch=True) | 210 'repo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'dir', |
| 211 allow_fetch=True) |
| 175 self.assertEqual(result, { | 212 self.assertEqual(result, { |
| 176 'author': 'author', | 213 'author': 'author', |
| 177 'message': 'message', | 214 'message': 'message', |
| 178 }) | 215 }) |
| 179 git.assert_has_calls([ | 216 git.assert_has_calls([ |
| 180 mock.call('GIT', '-C', 'dir', 'show', '-s', '--pretty=%aE', 'revision'), | 217 mock_git_dir('show', '-s', '--pretty=%aE', |
| 181 mock.call('GIT', '-C', 'dir', 'show', '-s', '--pretty=%B', 'revision'), | 218 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), |
| 219 mock_git_dir('show', '-s', '--pretty=%B', |
| 220 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), |
| 182 ]) | 221 ]) |
| 183 | 222 |
| 184 | 223 |
| 185 class TestGitiles(unittest.TestCase): | 224 class TestGitiles(unittest.TestCase): |
| 186 def setUp(self): | 225 def setUp(self): |
| 187 requests_ssl.disable_check() | 226 requests_ssl.disable_check() |
| 188 | 227 |
| 189 @mock.patch('__builtin__.open', mock.mock_open()) | 228 @mock.patch('__builtin__.open', mock.mock_open()) |
| 190 @mock.patch('shutil.rmtree') | 229 @mock.patch('shutil.rmtree') |
| 191 @mock.patch('os.makedirs') | 230 @mock.patch('os.makedirs') |
| (...skipping 148 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 340 'author': 'author', | 379 'author': 'author', |
| 341 'message': 'message', | 380 'message': 'message', |
| 342 }) | 381 }) |
| 343 self.assertEqual(counts['sleeps'], 4) | 382 self.assertEqual(counts['sleeps'], 4) |
| 344 requests_get.assert_has_calls([ | 383 requests_get.assert_has_calls([ |
| 345 mock.call('repo/+/revision?format=JSON'), | 384 mock.call('repo/+/revision?format=JSON'), |
| 346 ] * 5) | 385 ] * 5) |
| 347 | 386 |
| 348 if __name__ == '__main__': | 387 if __name__ == '__main__': |
| 349 unittest.main() | 388 unittest.main() |
| OLD | NEW |