OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 | 2 |
3 # Copyright 2015 The Chromium Authors. All rights reserved. | 3 # Copyright 2015 The Chromium Authors. All rights reserved. |
4 # Use of this source code is governed by a BSD-style license that can be | 4 # Use of this source code is governed by a BSD-style license that can be |
5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
6 | 6 |
7 """Tests that the tools/build annotated_run wrapper actually runs.""" | 7 """Tests that the tools/build annotated_run wrapper actually runs.""" |
8 | 8 |
| 9 import collections |
9 import json | 10 import json |
| 11 import logging |
10 import os | 12 import os |
11 import subprocess | 13 import subprocess |
| 14 import sys |
| 15 import tempfile |
12 import unittest | 16 import unittest |
13 | 17 |
| 18 import test_env # pylint: disable=W0403,W0611 |
| 19 |
| 20 import mock |
| 21 from common import chromium_utils |
| 22 from common import env |
| 23 from slave import annotated_run |
| 24 from slave import gce |
| 25 |
14 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 26 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
15 | 27 |
| 28 |
| 29 MockOptions = collections.namedtuple('MockOptions', |
| 30 ('dry_run', 'logdog_force', 'logdog_butler_path', 'logdog_annotee_path', |
| 31 'logdog_verbose', 'logdog_service_account_json')) |
| 32 |
| 33 |
16 class AnnotatedRunTest(unittest.TestCase): | 34 class AnnotatedRunTest(unittest.TestCase): |
17 def test_example(self): | 35 def test_example(self): |
18 build_properties = { | 36 build_properties = { |
19 'recipe': 'annotated_run_test', | 37 'recipe': 'annotated_run_test', |
20 'true_prop': True, | 38 'true_prop': True, |
21 'num_prop': 123, | 39 'num_prop': 123, |
22 'string_prop': '321', | 40 'string_prop': '321', |
23 'dict_prop': {'foo': 'bar'}, | 41 'dict_prop': {'foo': 'bar'}, |
24 } | 42 } |
25 | 43 |
26 script_path = os.path.join(BASE_DIR, 'annotated_run.py') | 44 script_path = os.path.join(BASE_DIR, 'annotated_run.py') |
27 exit_code = subprocess.call([ | 45 exit_code = subprocess.call([ |
28 'python', script_path, | 46 'python', script_path, |
29 '--build-properties=%s' % json.dumps(build_properties)]) | 47 '--build-properties=%s' % json.dumps(build_properties)]) |
30 self.assertEqual(exit_code, 0) | 48 self.assertEqual(exit_code, 0) |
31 | 49 |
| 50 @mock.patch('slave.annotated_run._run_command') |
| 51 @mock.patch('slave.annotated_run.main') |
| 52 @mock.patch('sys.platform', return_value='win') |
| 53 @mock.patch('tempfile.mkstemp', side_effect=Exception('failure')) |
| 54 @mock.patch('os.environ', {}) |
| 55 def test_update_scripts_must_run(self, _tempfile_mkstemp, _sys_platform, |
| 56 main, run_command): |
| 57 annotated_run.main.side_effect = Exception('Test error!') |
| 58 annotated_run._run_command.return_value = (0, "") |
| 59 annotated_run.shell_main(['annotated_run.py', 'foo']) |
| 60 |
| 61 gclient_path = os.path.join(env.Build, os.pardir, 'depot_tools', |
| 62 'gclient.bat') |
| 63 run_command.assert_has_calls([ |
| 64 mock.call([gclient_path, 'sync', '--force', '--verbose', '--jobs=2'], |
| 65 cwd=env.Build), |
| 66 mock.call([sys.executable, 'annotated_run.py', 'foo']), |
| 67 ]) |
| 68 main.assert_not_called() |
| 69 |
| 70 |
| 71 class _AnnotatedRunExecTestBase(unittest.TestCase): |
| 72 def setUp(self): |
| 73 logging.basicConfig(level=logging.ERROR+1) |
| 74 |
| 75 self.maxDiff = None |
| 76 self._patchers = [] |
| 77 for m in ( |
| 78 'slave.annotated_run._run_command', |
| 79 'os.path.exists', |
| 80 'os.getcwd', |
| 81 ): |
| 82 self._patchers.append(mock.patch(m)) |
| 83 for p in self._patchers: |
| 84 p.start() |
| 85 |
| 86 self.tdir = tempfile.mkdtemp() |
| 87 self.opts = MockOptions( |
| 88 dry_run=False, |
| 89 logdog_force=False, |
| 90 logdog_annotee_path=None, |
| 91 logdog_butler_path=None, |
| 92 logdog_verbose=False, |
| 93 logdog_service_account_json=None) |
| 94 self.config = annotated_run.Config( |
| 95 run_cmd=['run.py'], |
| 96 logdog_pubsub=None, |
| 97 logdog_platform=None, |
| 98 ) |
| 99 self.properties = { |
| 100 'recipe': 'example/recipe', |
| 101 'mastername': 'master.random', |
| 102 'buildername': 'builder', |
| 103 } |
| 104 self.cwd = os.path.join('home', 'user') |
| 105 self.rpy_path = os.path.join(env.Build, 'scripts', 'slave', 'recipes.py') |
| 106 self.recipe_args = [ |
| 107 sys.executable, '-u', self.rpy_path, 'run', |
| 108 '--workdir=%s' % (self.cwd,), |
| 109 '--properties-file=%s' % (self._tp('recipe_properties.json'),), |
| 110 'example/recipe'] |
| 111 |
| 112 # Use public recipes.py path. |
| 113 os.getcwd.return_value = self.cwd |
| 114 os.path.exists.return_value = False |
| 115 |
| 116 def tearDown(self): |
| 117 chromium_utils.RemoveDirectory(self.tdir) |
| 118 for p in reversed(self._patchers): |
| 119 p.stop() |
| 120 |
| 121 def _tp(self, *p): |
| 122 return os.path.join(*((self.tdir,) + p)) |
| 123 |
| 124 def _assertRecipeProperties(self, value): |
| 125 # Double-translate "value", since JSON converts strings to unicode. |
| 126 value = json.loads(json.dumps(value)) |
| 127 with open(self._tp('recipe_properties.json')) as fd: |
| 128 self.assertEqual(json.load(fd), value) |
| 129 |
| 130 |
| 131 class AnnotatedRunExecTest(_AnnotatedRunExecTestBase): |
| 132 |
| 133 def test_exec_successful(self): |
| 134 annotated_run._run_command.return_value = (0, '') |
| 135 |
| 136 rv = annotated_run._exec_recipe(self.opts, self.tdir, self.config, |
| 137 self.properties) |
| 138 self.assertEqual(rv, 0) |
| 139 self._assertRecipeProperties(self.properties) |
| 140 |
| 141 annotated_run._run_command.assert_called_once_with(self.recipe_args, |
| 142 dry_run=False) |
| 143 |
| 144 |
| 145 class AnnotatedRunLogDogExecTest(_AnnotatedRunExecTestBase): |
| 146 |
| 147 def setUp(self): |
| 148 super(AnnotatedRunLogDogExecTest, self).setUp() |
| 149 self._orig_whitelist = annotated_run.LOGDOG_WHITELIST_MASTER_BUILDERS |
| 150 annotated_run.LOGDOG_WHITELIST_MASTER_BUILDERS = { |
| 151 'master.some': [ |
| 152 'yesbuilder', |
| 153 ], |
| 154 |
| 155 'master.all': [ |
| 156 annotated_run.WHITELIST_ALL, |
| 157 ], |
| 158 } |
| 159 self.properties.update({ |
| 160 'mastername': 'master.some', |
| 161 'buildername': 'nobuilder', |
| 162 }) |
| 163 self.config = self.config._replace( |
| 164 logdog_pubsub=annotated_run.PubSubConfig(project='test', topic='logs'), |
| 165 logdog_platform=annotated_run.LogDogPlatform( |
| 166 butler=annotated_run.CipdBinary('cipd/butler', 'head', 'butler'), |
| 167 annotee=annotated_run.CipdBinary('cipd/annotee', 'head', 'annotee'), |
| 168 credential_path=os.path.join('path', 'to', 'creds.json'), |
| 169 streamserver='unix', |
| 170 ), |
| 171 ) |
| 172 self.is_gce = False |
| 173 |
| 174 def is_gce(): |
| 175 return self.is_gce |
| 176 is_gce_patch = mock.patch('slave.gce.Authenticator.is_gce', |
| 177 side_effect=is_gce) |
| 178 is_gce_patch.start() |
| 179 self._patchers.append(is_gce_patch) |
| 180 |
| 181 def tearDown(self): |
| 182 annotated_run.LOGDOG_WHITELIST_MASTER_BUILDERS = self._orig_whitelist |
| 183 super(AnnotatedRunLogDogExecTest, self).tearDown() |
| 184 |
| 185 def _assertAnnoteeCommand(self, value): |
| 186 # Double-translate "value", since JSON converts strings to unicode. |
| 187 value = json.loads(json.dumps(value)) |
| 188 with open(self._tp('logdog_bootstrap', 'annotee_cmd.json')) as fd: |
| 189 self.assertEqual(json.load(fd), value) |
| 190 |
| 191 def test_assert_logdog_whitelisted(self): |
| 192 self.assertRaises(annotated_run.LogDogNotBootstrapped, |
| 193 annotated_run._assert_logdog_whitelisted, 'master.undefined', 'any') |
| 194 self.assertRaises(annotated_run.LogDogNotBootstrapped, |
| 195 annotated_run._assert_logdog_whitelisted, 'master.some', 'nobuilder') |
| 196 annotated_run._assert_logdog_whitelisted('master.some', 'yesbuilder') |
| 197 annotated_run._assert_logdog_whitelisted('master.all', 'any') |
| 198 |
| 199 @mock.patch('slave.annotated_run.is_executable', return_value=True) |
| 200 @mock.patch('slave.annotated_run._get_service_account_json') |
| 201 def test_exec_with_whitelist_builder_runs_logdog(self, service_account, |
| 202 _is_executable): |
| 203 self.properties['buildername'] = 'yesbuilder' |
| 204 |
| 205 butler_path = self._tp('logdog_bootstrap', 'cipd', 'butler') |
| 206 annotee_path = self._tp('logdog_bootstrap', 'cipd', 'annotee') |
| 207 service_account.return_value = 'creds.json' |
| 208 annotated_run._run_command.return_value = (0, '') |
| 209 |
| 210 rv = annotated_run._exec_recipe(self.opts, self.tdir, self.config, |
| 211 self.properties) |
| 212 self.assertEqual(rv, 0) |
| 213 |
| 214 streamserver_uri = 'unix:%s' % (self._tp('butler.sock'),) |
| 215 service_account.assert_called_once_with( |
| 216 self.opts, self.config.logdog_platform.credential_path) |
| 217 annotated_run._run_command.assert_called_with( |
| 218 [butler_path, |
| 219 '-output', 'pubsub,project="test",topic="logs"', |
| 220 '-service-account-json', 'creds.json', |
| 221 'run', |
| 222 '-streamserver-uri', streamserver_uri, |
| 223 '--', |
| 224 annotee_path, |
| 225 '-butler-stream-server', streamserver_uri, |
| 226 '-json-args-path', self._tp('logdog_bootstrap', |
| 227 'annotee_cmd.json'), |
| 228 ], |
| 229 dry_run=False) |
| 230 self._assertRecipeProperties(self.properties) |
| 231 self._assertAnnoteeCommand(self.recipe_args) |
| 232 |
| 233 @mock.patch('slave.annotated_run._logdog_bootstrap', return_value=0) |
| 234 def test_runs_bootstrap_when_forced(self, lb): |
| 235 opts = self.opts._replace(logdog_force=True) |
| 236 rv = annotated_run._exec_recipe(opts, self.tdir, self.config, |
| 237 self.properties) |
| 238 self.assertEqual(rv, 0) |
| 239 lb.assert_called_once() |
| 240 annotated_run._run_command.assert_called_once() |
| 241 |
| 242 @mock.patch('slave.annotated_run._logdog_bootstrap', return_value=2) |
| 243 def test_forwards_error_code(self, lb): |
| 244 opts = self.opts._replace( |
| 245 logdog_force=True) |
| 246 rv = annotated_run._exec_recipe(opts, self.tdir, self.config, |
| 247 self.properties) |
| 248 self.assertEqual(rv, 2) |
| 249 lb.assert_called_once() |
| 250 |
| 251 @mock.patch('slave.annotated_run._logdog_bootstrap', |
| 252 side_effect=Exception('Unhandled situation.')) |
| 253 def test_runs_directly_if_bootstrap_fails(self, lb): |
| 254 annotated_run._run_command.return_value = (123, '') |
| 255 |
| 256 rv = annotated_run._exec_recipe(self.opts, self.tdir, self.config, |
| 257 self.properties) |
| 258 self.assertEqual(rv, 123) |
| 259 |
| 260 lb.assert_called_once() |
| 261 annotated_run._run_command.assert_called_once_with(self.recipe_args, |
| 262 dry_run=False) |
| 263 |
| 264 @mock.patch('slave.annotated_run._logdog_install_cipd') |
| 265 @mock.patch('slave.annotated_run._get_service_account_json') |
| 266 def test_runs_directly_if_logdog_error(self, service_account, cipd): |
| 267 self.properties['buildername'] = 'yesbuilder' |
| 268 |
| 269 cipd.return_value = ('butler', 'annotee') |
| 270 service_account.return_value = 'creds.json' |
| 271 def error_for_logdog(args, **kw): |
| 272 if len(args) > 0 and args[0] == 'butler': |
| 273 return (250, '') |
| 274 return (4, '') |
| 275 annotated_run._run_command.side_effect = error_for_logdog |
| 276 |
| 277 rv = annotated_run._exec_recipe(self.opts, self.tdir, self.config, |
| 278 self.properties) |
| 279 self.assertEqual(rv, 4) |
| 280 |
| 281 streamserver_uri = 'unix:%s' % (self._tp('butler.sock'),) |
| 282 service_account.assert_called_once_with( |
| 283 self.opts, self.config.logdog_platform.credential_path) |
| 284 annotated_run._run_command.assert_has_calls([ |
| 285 mock.call([ |
| 286 'butler', |
| 287 '-output', 'pubsub,project="test",topic="logs"', |
| 288 '-service-account-json', 'creds.json', |
| 289 'run', |
| 290 '-streamserver-uri', streamserver_uri, |
| 291 '--', |
| 292 'annotee', |
| 293 '-butler-stream-server', streamserver_uri, |
| 294 '-json-args-path', self._tp('logdog_bootstrap', |
| 295 'annotee_cmd.json'), |
| 296 ], |
| 297 dry_run=False), |
| 298 mock.call(self.recipe_args, dry_run=False)]) |
| 299 |
| 300 @mock.patch('os.path.isfile') |
| 301 def test_can_find_credentials(self, isfile): |
| 302 isfile.return_value = True |
| 303 |
| 304 service_account_json = annotated_run._get_service_account_json( |
| 305 self.opts, 'creds.json') |
| 306 self.assertEqual(service_account_json, 'creds.json') |
| 307 |
| 308 def test_uses_no_credentials_on_gce(self): |
| 309 self.is_gce = True |
| 310 service_account_json = annotated_run._get_service_account_json( |
| 311 self.opts, ('foo', 'bar')) |
| 312 self.assertIsNone(service_account_json) |
| 313 |
| 314 def test_cipd_install(self): |
| 315 annotated_run._run_command.return_value = (0, '') |
| 316 |
| 317 pkgs = annotated_run._logdog_install_cipd(self.tdir, |
| 318 annotated_run.CipdBinary('infra/foo', 'v0', 'foo'), |
| 319 annotated_run.CipdBinary('infra/bar', 'v1', 'baz'), |
| 320 ) |
| 321 self.assertEqual(pkgs, (self._tp('foo'), self._tp('baz'))) |
| 322 |
| 323 annotated_run._run_command.assert_called_once_with([ |
| 324 sys.executable, |
| 325 os.path.join(env.Build, 'scripts', 'slave', 'cipd.py'), |
| 326 '--dest-directory', self.tdir, |
| 327 '--json-output', os.path.join(self.tdir, 'packages.json'), |
| 328 '-P', 'infra/foo@v0', |
| 329 '-P', 'infra/bar@v1', |
| 330 ]) |
| 331 |
| 332 def test_cipd_install_failure_raises_bootstrap_error(self): |
| 333 annotated_run._run_command.return_value = (1, '') |
| 334 |
| 335 self.assertRaises(annotated_run.LogDogBootstrapError, |
| 336 annotated_run._logdog_install_cipd, |
| 337 self.tdir, |
| 338 annotated_run.CipdBinary('infra/foo', 'v0', 'foo'), |
| 339 annotated_run.CipdBinary('infra/bar', 'v1', 'baz'), |
| 340 ) |
| 341 |
| 342 |
32 if __name__ == '__main__': | 343 if __name__ == '__main__': |
33 unittest.main() | 344 unittest.main() |
OLD | NEW |