metrics_test.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. #!/usr/bin/env vpython3
  2. # Copyright (c) 2018 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. import json
  6. import os
  7. import sys
  8. import unittest
  9. from unittest import mock
  10. ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  11. sys.path.insert(0, ROOT_DIR)
  12. import metrics
  13. import metrics_utils
  14. # TODO: Should fix these warnings.
  15. # pylint: disable=line-too-long
  16. class TimeMock(object):
  17. def __init__(self):
  18. self._count = 0
  19. def __call__(self):
  20. self._count += 1
  21. return self._count * 1000
  22. class MetricsCollectorTest(unittest.TestCase):
  23. def setUp(self):
  24. self.config_file = os.path.join(ROOT_DIR, 'metrics.cfg')
  25. self.collector = metrics.MetricsCollector()
  26. # Keep track of the URL requests, file reads/writes and subprocess
  27. # spawned.
  28. self.urllib_request = mock.Mock()
  29. self.print_notice = mock.Mock()
  30. self.print_version_change = mock.Mock()
  31. self.Popen = mock.Mock()
  32. self.FileWrite = mock.Mock()
  33. self.FileRead = mock.Mock()
  34. # So that we don't have to update the tests every time we change the
  35. # version.
  36. mock.patch('metrics.metrics_utils.CURRENT_VERSION', 0).start()
  37. mock.patch('metrics.urllib.request', self.urllib_request).start()
  38. mock.patch('metrics.subprocess2.Popen', self.Popen).start()
  39. mock.patch('metrics.gclient_utils.FileWrite', self.FileWrite).start()
  40. mock.patch('metrics.gclient_utils.FileRead', self.FileRead).start()
  41. mock.patch('metrics.metrics_utils.print_notice',
  42. self.print_notice).start()
  43. mock.patch('metrics.metrics_utils.print_version_change',
  44. self.print_version_change).start()
  45. # Patch the methods used to get the system information, so we have a
  46. # known environment.
  47. mock.patch('metrics.time.time', TimeMock()).start()
  48. mock.patch('metrics.metrics_utils.get_python_version',
  49. lambda: '2.7.13').start()
  50. mock.patch('metrics.gclient_utils.GetOperatingSystem',
  51. lambda: 'linux').start()
  52. mock.patch('metrics.detect_host_arch.HostArch', lambda: 'x86').start()
  53. mock.patch('metrics_utils.get_repo_timestamp', lambda _: 1234).start()
  54. mock.patch('metrics_utils.get_git_version', lambda: '2.18.1').start()
  55. self.maxDiff = None
  56. self.default_metrics = {
  57. "metrics_version": 0,
  58. "python_version": "2.7.13",
  59. "git_version": "2.18.1",
  60. "execution_time": 1000,
  61. "timestamp": 3000,
  62. "exit_code": 0,
  63. "command": "fun",
  64. "depot_tools_age": 1234,
  65. "host_arch": "x86",
  66. "host_os": "linux",
  67. }
  68. self.addCleanup(mock.patch.stopall)
  69. def assert_writes_file(self, expected_filename, expected_content):
  70. self.assertEqual(len(self.FileWrite.mock_calls), 1)
  71. filename, content = self.FileWrite.mock_calls[0][1]
  72. self.assertEqual(filename, expected_filename)
  73. self.assertEqual(json.loads(content), expected_content)
  74. def test_writes_config_if_not_exists(self):
  75. self.FileRead.side_effect = [IOError(2, "No such file or directory")]
  76. mock_response = mock.Mock()
  77. self.urllib_request.urlopen.side_effect = [mock_response]
  78. mock_response.getcode.side_effect = [200]
  79. self.assertTrue(self.collector.config.is_googler)
  80. self.assertIsNone(self.collector.config.opted_in)
  81. self.assertEqual(self.collector.config.countdown, 10)
  82. self.assert_writes_file(self.config_file, {
  83. 'is-googler': True,
  84. 'countdown': 10,
  85. 'opt-in': None,
  86. 'version': 0
  87. })
  88. def test_writes_config_if_not_exists_non_googler(self):
  89. self.FileRead.side_effect = [IOError(2, "No such file or directory")]
  90. mock_response = mock.Mock()
  91. self.urllib_request.urlopen.side_effect = [mock_response]
  92. mock_response.getcode.side_effect = [403]
  93. self.assertFalse(self.collector.config.is_googler)
  94. self.assertIsNone(self.collector.config.opted_in)
  95. self.assertEqual(self.collector.config.countdown, 10)
  96. self.assert_writes_file(self.config_file, {
  97. 'is-googler': False,
  98. 'countdown': 10,
  99. 'opt-in': None,
  100. 'version': 0
  101. })
  102. def test_disables_metrics_if_cant_write_config(self):
  103. self.FileRead.side_effect = [IOError(2, 'No such file or directory')]
  104. mock_response = mock.Mock()
  105. self.urllib_request.urlopen.side_effect = [mock_response]
  106. mock_response.getcode.side_effect = [200]
  107. self.FileWrite.side_effect = [IOError(13, 'Permission denied.')]
  108. self.assertTrue(self.collector.config.is_googler)
  109. self.assertFalse(self.collector.config.opted_in)
  110. self.assertEqual(self.collector.config.countdown, 10)
  111. def assert_collects_metrics(self, update_metrics=None):
  112. expected_metrics = self.default_metrics
  113. self.default_metrics.update(update_metrics or {})
  114. # Assert we invoked the script to upload them.
  115. self.Popen.assert_called_with(['vpython3', metrics.UPLOAD_SCRIPT],
  116. stdin=metrics.subprocess2.PIPE)
  117. # Assert we collected the right metrics.
  118. write_call = self.Popen.return_value.stdin.write.call_args
  119. collected_metrics = json.loads(write_call[0][0])
  120. self.assertTrue(self.collector.collecting_metrics)
  121. self.assertEqual(collected_metrics, expected_metrics)
  122. def test_collects_system_information(self):
  123. """Tests that we collect information about the runtime environment."""
  124. self.FileRead.side_effect = [
  125. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  126. ]
  127. @self.collector.collect_metrics('fun')
  128. def fun():
  129. pass
  130. fun()
  131. self.assert_collects_metrics()
  132. def test_collects_added_metrics(self):
  133. """Tests that we can collect custom metrics."""
  134. self.FileRead.side_effect = [
  135. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  136. ]
  137. @self.collector.collect_metrics('fun')
  138. def fun():
  139. self.collector.add('foo', 'bar')
  140. fun()
  141. self.assert_collects_metrics({'foo': 'bar'})
  142. def test_collects_metrics_when_opted_in(self):
  143. """Tests that metrics are collected when the user opts-in."""
  144. self.FileRead.side_effect = [
  145. '{"is-googler": true, "countdown": 1234, "opt-in": true, "version": 0}'
  146. ]
  147. @self.collector.collect_metrics('fun')
  148. def fun():
  149. pass
  150. fun()
  151. self.assert_collects_metrics()
  152. @mock.patch('metrics_utils.REPORT_BUILD', 'p/b/b/1')
  153. def test_collects_metrics_report_build_set(self):
  154. """Tests that metrics are collected when REPORT_BUILD is set."""
  155. @self.collector.collect_metrics('fun')
  156. def fun():
  157. pass
  158. fun()
  159. self.assert_collects_metrics({
  160. 'bot_metrics': {
  161. 'build_id': 1,
  162. 'builder': {
  163. 'project': 'p',
  164. 'bucket': 'b',
  165. 'builder': 'b',
  166. }
  167. }
  168. })
  169. # We shouldn't have tried to read the config file.
  170. self.assertFalse(self.FileRead.called)
  171. @mock.patch('metrics_utils.COLLECT_METRICS', False)
  172. def test_metrics_collection_disabled(self):
  173. """Tests that metrics collection can be disabled via a global variable."""
  174. @self.collector.collect_metrics('fun')
  175. def fun():
  176. pass
  177. fun()
  178. self.assertFalse(self.collector.collecting_metrics)
  179. # We shouldn't have tried to read the config file.
  180. self.assertFalse(self.FileRead.called)
  181. # Nor tried to upload any metrics.
  182. self.assertFalse(self.Popen.called)
  183. def test_metrics_collection_disabled_not_googler(self):
  184. """Tests that metrics collection is disabled for non googlers."""
  185. self.FileRead.side_effect = [
  186. '{"is-googler": false, "countdown": 0, "opt-in": null, "version": 0}'
  187. ]
  188. @self.collector.collect_metrics('fun')
  189. def fun():
  190. pass
  191. fun()
  192. self.assertFalse(self.collector.collecting_metrics)
  193. self.assertFalse(self.collector.config.is_googler)
  194. self.assertIsNone(self.collector.config.opted_in)
  195. self.assertEqual(self.collector.config.countdown, 0)
  196. # Assert that we did not try to upload any metrics.
  197. self.assertFalse(self.Popen.called)
  198. def test_metrics_collection_disabled_opted_out(self):
  199. """Tests that metrics collection is disabled if the user opts out."""
  200. self.FileRead.side_effect = [
  201. '{"is-googler": true, "countdown": 0, "opt-in": false, "version": 0}'
  202. ]
  203. @self.collector.collect_metrics('fun')
  204. def fun():
  205. pass
  206. fun()
  207. self.assertFalse(self.collector.collecting_metrics)
  208. self.assertTrue(self.collector.config.is_googler)
  209. self.assertFalse(self.collector.config.opted_in)
  210. self.assertEqual(self.collector.config.countdown, 0)
  211. # Assert that we did not try to upload any metrics.
  212. self.assertFalse(self.Popen.called)
  213. def test_metrics_collection_disabled_non_zero_countdown(self):
  214. """Tests that metrics collection is disabled until the countdown expires."""
  215. self.FileRead.side_effect = [
  216. '{"is-googler": true, "countdown": 1, "opt-in": null, "version": 0}'
  217. ]
  218. @self.collector.collect_metrics('fun')
  219. def fun():
  220. pass
  221. fun()
  222. self.assertFalse(self.collector.collecting_metrics)
  223. self.assertTrue(self.collector.config.is_googler)
  224. self.assertFalse(self.collector.config.opted_in)
  225. self.assertEqual(self.collector.config.countdown, 1)
  226. # Assert that we did not try to upload any metrics.
  227. self.assertFalse(self.Popen.called)
  228. def test_handles_exceptions(self):
  229. """Tests that exception are caught and we exit with an appropriate code."""
  230. self.FileRead.side_effect = [
  231. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  232. ]
  233. @self.collector.collect_metrics('fun')
  234. def fun():
  235. raise ValueError
  236. # When an exception is raised, we should catch it, update exit-code,
  237. # collect metrics, and re-raise it.
  238. with self.assertRaises(ValueError):
  239. fun()
  240. self.assert_collects_metrics({'exit_code': 1})
  241. def test_handles_system_exit(self):
  242. """Tests that the sys.exit code is respected and metrics are collected."""
  243. self.FileRead.side_effect = [
  244. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  245. ]
  246. @self.collector.collect_metrics('fun')
  247. def fun():
  248. sys.exit(0)
  249. # When an exception is raised, we should catch it, update exit-code,
  250. # collect metrics, and re-raise it.
  251. with self.assertRaises(SystemExit) as cm:
  252. fun()
  253. self.assertEqual(cm.exception.code, 0)
  254. self.assert_collects_metrics({'exit_code': 0})
  255. def test_handles_keyboard_interrupt(self):
  256. """Tests that KeyboardInterrupt exits with 130 and metrics are collected."""
  257. self.FileRead.side_effect = [
  258. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  259. ]
  260. @self.collector.collect_metrics('fun')
  261. def fun():
  262. raise KeyboardInterrupt
  263. # When an exception is raised, we should catch it, update exit-code,
  264. # collect metrics, and re-raise it.
  265. with self.assertRaises(KeyboardInterrupt):
  266. fun()
  267. self.assert_collects_metrics({'exit_code': 130})
  268. def test_handles_system_exit_non_zero(self):
  269. """Tests that the sys.exit code is respected and metrics are collected."""
  270. self.FileRead.side_effect = [
  271. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  272. ]
  273. @self.collector.collect_metrics('fun')
  274. def fun():
  275. sys.exit(123)
  276. # When an exception is raised, we should catch it, update exit-code,
  277. # collect metrics, and re-raise it.
  278. with self.assertRaises(SystemExit) as cm:
  279. fun()
  280. self.assertEqual(cm.exception.code, 123)
  281. self.assert_collects_metrics({'exit_code': 123})
  282. def test_prints_notice_non_zero_countdown(self):
  283. """Tests that a notice is printed while the countdown is non-zero."""
  284. self.FileRead.side_effect = [
  285. '{"is-googler": true, "countdown": 1234, "opt-in": null, "version": 0}'
  286. ]
  287. with self.assertRaises(SystemExit) as cm:
  288. with self.collector.print_notice_and_exit():
  289. pass
  290. self.assertEqual(cm.exception.code, 0)
  291. self.print_notice.assert_called_once_with(1234)
  292. def test_prints_notice_zero_countdown(self):
  293. """Tests that a notice is printed when the countdown reaches 0."""
  294. self.FileRead.side_effect = [
  295. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  296. ]
  297. with self.assertRaises(SystemExit) as cm:
  298. with self.collector.print_notice_and_exit():
  299. pass
  300. self.assertEqual(cm.exception.code, 0)
  301. self.print_notice.assert_called_once_with(0)
  302. def test_doesnt_print_notice_opted_in(self):
  303. """Tests that a notice is not printed when the user opts-in."""
  304. self.FileRead.side_effect = [
  305. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  306. ]
  307. with self.assertRaises(SystemExit) as cm:
  308. with self.collector.print_notice_and_exit():
  309. pass
  310. self.assertEqual(cm.exception.code, 0)
  311. self.assertFalse(self.print_notice.called)
  312. def test_doesnt_print_notice_opted_out(self):
  313. """Tests that a notice is not printed when the user opts-out."""
  314. self.FileRead.side_effect = [
  315. '{"is-googler": true, "countdown": 0, "opt-in": false, "version": 0}'
  316. ]
  317. with self.assertRaises(SystemExit) as cm:
  318. with self.collector.print_notice_and_exit():
  319. pass
  320. self.assertEqual(cm.exception.code, 0)
  321. self.assertFalse(self.print_notice.called)
  322. @mock.patch('metrics_utils.COLLECT_METRICS', False)
  323. def test_doesnt_print_notice_disable_metrics_collection(self):
  324. with self.assertRaises(SystemExit) as cm:
  325. with self.collector.print_notice_and_exit():
  326. pass
  327. self.assertEqual(cm.exception.code, 0)
  328. self.assertFalse(self.print_notice.called)
  329. # We shouldn't have tried to read the config file.
  330. self.assertFalse(self.FileRead.called)
  331. @mock.patch('metrics_utils.REPORT_BUILD', 'p/b/b/1')
  332. def test_doesnt_print_notice_report_build(self):
  333. with self.assertRaises(SystemExit) as cm:
  334. with self.collector.print_notice_and_exit():
  335. pass
  336. self.assertEqual(cm.exception.code, 0)
  337. self.assertFalse(self.print_notice.called)
  338. # We shouldn't have tried to read the config file.
  339. self.assertFalse(self.FileRead.called)
  340. def test_print_notice_handles_exceptions(self):
  341. """Tests that exception are caught and we exit with an appropriate code."""
  342. self.FileRead.side_effect = [
  343. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  344. ]
  345. # print_notice should catch the exception, print it and invoke
  346. # sys.exit()
  347. with self.assertRaises(SystemExit) as cm:
  348. with self.collector.print_notice_and_exit():
  349. raise ValueError
  350. self.assertEqual(cm.exception.code, 1)
  351. self.assertTrue(self.print_notice.called)
  352. def test_print_notice_handles_system_exit(self):
  353. """Tests that the sys.exit code is respected and a notice is displayed."""
  354. self.FileRead.side_effect = [
  355. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  356. ]
  357. # print_notice should catch the exception, print it and invoke
  358. # sys.exit()
  359. with self.assertRaises(SystemExit) as cm:
  360. with self.collector.print_notice_and_exit():
  361. sys.exit(0)
  362. self.assertEqual(cm.exception.code, 0)
  363. self.assertTrue(self.print_notice.called)
  364. def test_print_notice_handles_system_exit_non_zero(self):
  365. """Tests that the sys.exit code is respected and a notice is displayed."""
  366. self.FileRead.side_effect = [
  367. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  368. ]
  369. # When an exception is raised, we should catch it, update exit-code,
  370. # collect metrics, and re-raise it.
  371. with self.assertRaises(SystemExit) as cm:
  372. with self.collector.print_notice_and_exit():
  373. sys.exit(123)
  374. self.assertEqual(cm.exception.code, 123)
  375. self.assertTrue(self.print_notice.called)
  376. def test_counts_down(self):
  377. """Tests that the countdown works correctly."""
  378. self.FileRead.side_effect = [
  379. '{"is-googler": true, "countdown": 10, "opt-in": null, "version": 0}'
  380. ]
  381. # We define multiple functions to ensure it has no impact on countdown.
  382. @self.collector.collect_metrics('barn')
  383. def _barn():
  384. pass
  385. @self.collector.collect_metrics('fun')
  386. def _fun():
  387. pass
  388. def foo_main():
  389. pass
  390. # Assert that the countdown hasn't decrease yet.
  391. self.assertFalse(self.FileWrite.called)
  392. self.assertEqual(self.collector.config.countdown, 10)
  393. with self.assertRaises(SystemExit) as cm:
  394. with self.collector.print_notice_and_exit():
  395. foo_main()
  396. self.assertEqual(cm.exception.code, 0)
  397. # Assert that the countdown decreased by one, and the config file was
  398. # updated.
  399. self.assertEqual(self.collector.config.countdown, 9)
  400. self.print_notice.assert_called_once_with(10)
  401. self.assert_writes_file(self.config_file, {
  402. 'is-googler': True,
  403. 'countdown': 9,
  404. 'opt-in': None,
  405. 'version': 0
  406. })
  407. def test_nested_functions(self):
  408. """Tests that a function can call another function for which metrics are
  409. collected."""
  410. self.FileRead.side_effect = [
  411. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  412. ]
  413. @self.collector.collect_metrics('barn')
  414. def barn():
  415. self.collector.add('barn-metric', 1)
  416. return 1000
  417. @self.collector.collect_metrics('fun')
  418. def fun():
  419. result = barn()
  420. self.collector.add('fun-metric', result + 1)
  421. fun()
  422. # Assert that we collected metrics for fun, but not for barn.
  423. self.assert_collects_metrics({'fun-metric': 1001})
  424. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  425. def test_version_change_from_hasnt_decided(self):
  426. # The user has not decided yet, and the countdown hasn't reached 0, so
  427. # we're not collecting metrics.
  428. self.FileRead.side_effect = [
  429. '{"is-googler": true, "countdown": 9, "opt-in": null, "version": 0}'
  430. ]
  431. with self.assertRaises(SystemExit) as cm:
  432. with self.collector.print_notice_and_exit():
  433. self.collector.add('foo-metric', 1)
  434. self.assertEqual(cm.exception.code, 0)
  435. # We display the notice informing the user of the changes.
  436. self.print_version_change.assert_called_once_with(0)
  437. # But the countdown is not reset.
  438. self.assert_writes_file(self.config_file, {
  439. 'is-googler': True,
  440. 'countdown': 8,
  441. 'opt-in': None,
  442. 'version': 0
  443. })
  444. # And no metrics are uploaded.
  445. self.assertFalse(self.Popen.called)
  446. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  447. def test_version_change_from_opted_in_by_default(self):
  448. # The user has not decided yet, but the countdown has reached 0, and
  449. # we're collecting metrics.
  450. self.FileRead.side_effect = [
  451. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  452. ]
  453. with self.assertRaises(SystemExit) as cm:
  454. with self.collector.print_notice_and_exit():
  455. self.collector.add('foo-metric', 1)
  456. self.assertEqual(cm.exception.code, 0)
  457. # We display the notice informing the user of the changes.
  458. self.print_version_change.assert_called_once_with(0)
  459. # We reset the countdown.
  460. self.assert_writes_file(self.config_file, {
  461. 'is-googler': True,
  462. 'countdown': 9,
  463. 'opt-in': None,
  464. 'version': 0
  465. })
  466. # No metrics are uploaded.
  467. self.assertFalse(self.Popen.called)
  468. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  469. def test_version_change_from_opted_in(self):
  470. # The user has opted in, and we're collecting metrics.
  471. self.FileRead.side_effect = [
  472. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  473. ]
  474. with self.assertRaises(SystemExit) as cm:
  475. with self.collector.print_notice_and_exit():
  476. self.collector.add('foo-metric', 1)
  477. self.assertEqual(cm.exception.code, 0)
  478. # We display the notice informing the user of the changes.
  479. self.print_version_change.assert_called_once_with(0)
  480. # We reset the countdown.
  481. self.assert_writes_file(self.config_file, {
  482. 'is-googler': True,
  483. 'countdown': 9,
  484. 'opt-in': None,
  485. 'version': 0
  486. })
  487. # No metrics are uploaded.
  488. self.assertFalse(self.Popen.called)
  489. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  490. def test_version_change_from_opted_out(self):
  491. # The user has opted out and we're not collecting metrics.
  492. self.FileRead.side_effect = [
  493. '{"is-googler": true, "countdown": 0, "opt-in": false, "version": 0}'
  494. ]
  495. with self.assertRaises(SystemExit) as cm:
  496. with self.collector.print_notice_and_exit():
  497. self.collector.add('foo-metric', 1)
  498. self.assertEqual(cm.exception.code, 0)
  499. # We don't display any notice.
  500. self.assertFalse(self.print_version_change.called)
  501. self.assertFalse(self.print_notice.called)
  502. # We don't upload any metrics.
  503. self.assertFalse(self.Popen.called)
  504. # We don't modify the config.
  505. self.assertFalse(self.FileWrite.called)
  506. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  507. def test_version_change_non_googler(self):
  508. # The user is not a googler and we're not collecting metrics.
  509. self.FileRead.side_effect = [
  510. '{"is-googler": false, "countdown": 10, "opt-in": null, "version": 0}'
  511. ]
  512. with self.assertRaises(SystemExit) as cm:
  513. with self.collector.print_notice_and_exit():
  514. self.collector.add('foo-metric', 1)
  515. self.assertEqual(cm.exception.code, 0)
  516. # We don't display any notice.
  517. self.assertFalse(self.print_version_change.called)
  518. self.assertFalse(self.print_notice.called)
  519. # We don't upload any metrics.
  520. self.assertFalse(self.Popen.called)
  521. # We don't modify the config.
  522. self.assertFalse(self.FileWrite.called)
  523. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  524. def test_opting_in_updates_version(self):
  525. # The user is seeing the notice telling him of the version changes.
  526. self.FileRead.side_effect = [
  527. '{"is-googler": true, "countdown": 8, "opt-in": null, "version": 0}'
  528. ]
  529. self.collector.config.opted_in = True
  530. # We don't display any notice.
  531. self.assertFalse(self.print_version_change.called)
  532. self.assertFalse(self.print_notice.called)
  533. # We don't upload any metrics.
  534. self.assertFalse(self.Popen.called)
  535. # We update the version and opt-in the user.
  536. self.assert_writes_file(self.config_file, {
  537. 'is-googler': True,
  538. 'countdown': 8,
  539. 'opt-in': True,
  540. 'version': 5
  541. })
  542. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  543. def test_opting_in_by_default_updates_version(self):
  544. # The user will be opted in by default on the next execution.
  545. self.FileRead.side_effect = [
  546. '{"is-googler": true, "countdown": 1, "opt-in": null, "version": 0}'
  547. ]
  548. with self.assertRaises(SystemExit) as cm:
  549. with self.collector.print_notice_and_exit():
  550. self.collector.add('foo-metric', 1)
  551. self.assertEqual(cm.exception.code, 0)
  552. # We display the notices.
  553. self.print_notice.assert_called_once_with(1)
  554. self.print_version_change.assert_called_once_with(0)
  555. # We don't upload any metrics.
  556. self.assertFalse(self.Popen.called)
  557. # We update the version and set the countdown to 0. In subsequent runs,
  558. # we'll start collecting metrics.
  559. self.assert_writes_file(self.config_file, {
  560. 'is-googler': True,
  561. 'countdown': 0,
  562. 'opt-in': None,
  563. 'version': 5
  564. })
  565. def test_add_repeated(self):
  566. """Tests that we can add repeated metrics."""
  567. self.FileRead.side_effect = [
  568. '{"is-googler": true, "countdown": 0, "opt-in": true}'
  569. ]
  570. @self.collector.collect_metrics('fun')
  571. def fun():
  572. self.collector.add_repeated('fun', 1)
  573. self.collector.add_repeated('fun', 2)
  574. self.collector.add_repeated('fun', 5)
  575. fun()
  576. # Assert that we collected all metrics for fun.
  577. self.assert_collects_metrics({'fun': [1, 2, 5]})
  578. class MetricsUtilsTest(unittest.TestCase):
  579. def test_extracts_host(self):
  580. """Test that we extract the host from the requested URI."""
  581. # Regular case
  582. http_metrics = metrics_utils.extract_http_metrics(
  583. 'https://chromium-review.googlesource.com/foo/bar?q=baz', '', 0, 0)
  584. self.assertEqual('chromium-review.googlesource.com',
  585. http_metrics['host'])
  586. # Unexpected host
  587. http_metrics = metrics_utils.extract_http_metrics(
  588. 'https://foo-review.googlesource.com/', '', 0, 0)
  589. self.assertNotIn('host', http_metrics)
  590. def test_extracts_path(self):
  591. """Test that we extract the matching path from the requested URI."""
  592. # Regular case
  593. http_metrics = metrics_utils.extract_http_metrics(
  594. 'https://review.example.com/changes/1234/revisions/deadbeef/commit',
  595. '', 0, 0)
  596. self.assertEqual('changes/revisions/commit', http_metrics['path'])
  597. # No matching paths
  598. http_metrics = metrics_utils.extract_http_metrics(
  599. 'https://review.example.com/changes/1234/unexpected/path', '', 0, 0)
  600. self.assertNotIn('path', http_metrics)
  601. def test_extracts_path_changes(self):
  602. """Tests that we extract paths for /changes/."""
  603. # /changes/<change-id>
  604. http_metrics = metrics_utils.extract_http_metrics(
  605. 'https://review.example.com/changes/proj%2Fsrc%7Emain%7EI1234abcd',
  606. '', 0, 0)
  607. self.assertEqual('changes', http_metrics['path'])
  608. # /changes/?q=<something>
  609. http_metrics = metrics_utils.extract_http_metrics(
  610. 'https://review.example.com/changes/?q=owner:me+OR+cc:me', '', 0, 0)
  611. self.assertEqual('changes', http_metrics['path'])
  612. # /changes/#<something>
  613. http_metrics = metrics_utils.extract_http_metrics(
  614. 'https://review.example.com/changes/#something', '', 0, 0)
  615. self.assertEqual('changes', http_metrics['path'])
  616. # /changes/<change-id>/<anything> does not map to changes.
  617. http_metrics = metrics_utils.extract_http_metrics(
  618. 'https://review.example.com/changes/12345678/message', '', 0, 0)
  619. self.assertNotEqual('changes', http_metrics['path'])
  620. def test_extracts_arguments(self):
  621. """Test that we can extract arguments from the requested URI."""
  622. # Regular case
  623. http_metrics = metrics_utils.extract_http_metrics(
  624. 'https://review.example.com/?q=123&foo=bar&o=ALL_REVISIONS', '', 0,
  625. 0)
  626. self.assertEqual(['ALL_REVISIONS'], http_metrics['arguments'])
  627. # Some unexpected arguments are filtered out.
  628. http_metrics = metrics_utils.extract_http_metrics(
  629. 'https://review.example.com/?o=ALL_REVISIONS&o=LABELS&o=UNEXPECTED',
  630. '', 0, 0)
  631. self.assertEqual(['ALL_REVISIONS', 'LABELS'], http_metrics['arguments'])
  632. # No valid arguments, so arguments is not present
  633. http_metrics = metrics_utils.extract_http_metrics(
  634. 'https://review.example.com/?o=bar&baz=1', '', 0, 0)
  635. self.assertNotIn('arguments', http_metrics)
  636. # No valid arguments, so arguments is not present
  637. http_metrics = metrics_utils.extract_http_metrics(
  638. 'https://review.example.com/?foo=bar&baz=1', '', 0, 0)
  639. self.assertNotIn('arguments', http_metrics)
  640. def test_validates_method(self):
  641. """Test that we validate the HTTP method used."""
  642. # Regular case
  643. http_metrics = metrics_utils.extract_http_metrics('', 'POST', 0, 0)
  644. self.assertEqual('POST', http_metrics['method'])
  645. # Unexpected method is not reported
  646. http_metrics = metrics_utils.extract_http_metrics('', 'DEMAND', 0, 0)
  647. self.assertNotIn('method', http_metrics)
  648. def test_status(self):
  649. """Tests that the response status we passed is returned."""
  650. http_metrics = metrics_utils.extract_http_metrics('', '', 123, 0)
  651. self.assertEqual(123, http_metrics['status'])
  652. http_metrics = metrics_utils.extract_http_metrics('', '', 404, 0)
  653. self.assertEqual(404, http_metrics['status'])
  654. def test_response_time(self):
  655. """Tests that the response time we passed is returned."""
  656. http_metrics = metrics_utils.extract_http_metrics('', '', 0, 0.25)
  657. self.assertEqual(0.25, http_metrics['response_time'])
  658. http_metrics = metrics_utils.extract_http_metrics('', '', 0, 12345.25)
  659. self.assertEqual(12345.25, http_metrics['response_time'])
  660. @mock.patch('metrics_utils.subprocess2.Popen')
  661. def test_get_git_version(self, mockPopen):
  662. """Tests that we can get the git version."""
  663. mockProcess = mock.Mock()
  664. mockProcess.communicate.side_effect = [(b'git version 2.18.0.123.foo',
  665. '')]
  666. mockPopen.side_effect = [mockProcess]
  667. self.assertEqual('2.18.0', metrics_utils.get_git_version())
  668. @mock.patch('metrics_utils.subprocess2.Popen')
  669. def test_get_git_version_unrecognized(self, mockPopen):
  670. """Tests that we can get the git version."""
  671. mockProcess = mock.Mock()
  672. mockProcess.communicate.side_effect = [(b'Blah blah blah', 'blah blah')]
  673. mockPopen.side_effect = [mockProcess]
  674. self.assertIsNone(metrics_utils.get_git_version())
  675. def test_extract_known_subcommand_args(self):
  676. """Tests that we can extract known subcommand args."""
  677. result = metrics_utils.extract_known_subcommand_args([
  678. 'm=Fix issue with ccs', 'cc=foo@example.com', 'cc=bar@example.com'
  679. ])
  680. self.assertEqual(['cc', 'cc', 'm'], result)
  681. result = metrics_utils.extract_known_subcommand_args([
  682. 'm=Some title mentioning cc and hashtag', 'notify=NONE', 'private'
  683. ])
  684. self.assertEqual(['m', 'notify=NONE', 'private'], result)
  685. result = metrics_utils.extract_known_subcommand_args(
  686. ['foo=bar', 'another_unkwnon_arg'])
  687. self.assertEqual([], result)
  688. if __name__ == '__main__':
  689. unittest.main()