metrics_test.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. #!/usr/bin/env python
  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. ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  10. sys.path.insert(0, ROOT_DIR)
  11. import metrics
  12. import cStringIO
  13. from third_party import mock
  14. class TimeMock(object):
  15. def __init__(self):
  16. self._count = 0
  17. def __call__(self):
  18. self._count += 1
  19. return self._count * 1000
  20. class MetricsCollectorTest(unittest.TestCase):
  21. def setUp(self):
  22. self.config_file = os.path.join(ROOT_DIR, 'metrics.cfg')
  23. self.collector = metrics.MetricsCollector()
  24. # Keep track of the URL requests, file reads/writes and subprocess spawned.
  25. self.urllib2 = mock.Mock()
  26. self.print_notice = mock.Mock()
  27. self.print_version_change = mock.Mock()
  28. self.Popen = mock.Mock()
  29. self.FileWrite = mock.Mock()
  30. self.FileRead = mock.Mock()
  31. mock.patch('metrics.urllib2', self.urllib2).start()
  32. mock.patch('metrics.subprocess.Popen', self.Popen).start()
  33. mock.patch('metrics.gclient_utils.FileWrite', self.FileWrite).start()
  34. mock.patch('metrics.gclient_utils.FileRead', self.FileRead).start()
  35. mock.patch('metrics.metrics_utils.print_notice', self.print_notice).start()
  36. mock.patch(
  37. 'metrics.metrics_utils.print_version_change',
  38. self.print_version_change).start()
  39. # Patch the methods used to get the system information, so we have a known
  40. # environment.
  41. mock.patch('metrics.tempfile.mkstemp',
  42. lambda: (None, '/tmp/metrics.json')).start()
  43. mock.patch('metrics.time.time',
  44. TimeMock()).start()
  45. mock.patch('metrics.metrics_utils.get_python_version',
  46. lambda: '2.7.13').start()
  47. mock.patch('metrics.gclient_utils.GetMacWinOrLinux',
  48. lambda: 'linux').start()
  49. mock.patch('metrics.detect_host_arch.HostArch',
  50. lambda: 'x86').start()
  51. mock.patch('metrics_utils.get_repo_timestamp',
  52. lambda _: 1234).start()
  53. self.default_metrics = {
  54. "python_version": "2.7.13",
  55. "execution_time": 1000,
  56. "timestamp": 0,
  57. "exit_code": 0,
  58. "command": "fun",
  59. "depot_tools_age": 1234,
  60. "host_arch": "x86",
  61. "host_os": "linux",
  62. }
  63. self.addCleanup(mock.patch.stopall)
  64. def assert_writes_file(self, expected_filename, expected_content):
  65. self.assertEqual(len(self.FileWrite.mock_calls), 1)
  66. filename, content = self.FileWrite.mock_calls[0][1]
  67. self.assertEqual(filename, expected_filename)
  68. self.assertEqual(json.loads(content), expected_content)
  69. def test_writes_config_if_not_exists(self):
  70. self.FileRead.side_effect = [IOError(2, "No such file or directory")]
  71. mock_response = mock.Mock()
  72. self.urllib2.urlopen.side_effect = [mock_response]
  73. mock_response.getcode.side_effect = [200]
  74. self.assertTrue(self.collector.config.is_googler)
  75. self.assertIsNone(self.collector.config.opted_in)
  76. self.assertEqual(self.collector.config.countdown, 10)
  77. self.assert_writes_file(
  78. self.config_file,
  79. {'is-googler': True, 'countdown': 10, 'opt-in': None, 'version': 0})
  80. def test_writes_config_if_not_exists_non_googler(self):
  81. self.FileRead.side_effect = [IOError(2, "No such file or directory")]
  82. mock_response = mock.Mock()
  83. self.urllib2.urlopen.side_effect = [mock_response]
  84. mock_response.getcode.side_effect = [403]
  85. self.assertFalse(self.collector.config.is_googler)
  86. self.assertIsNone(self.collector.config.opted_in)
  87. self.assertEqual(self.collector.config.countdown, 10)
  88. self.assert_writes_file(
  89. self.config_file,
  90. {'is-googler': False, 'countdown': 10, 'opt-in': None, 'version': 0})
  91. def test_disables_metrics_if_cant_write_config(self):
  92. self.FileRead.side_effect = [IOError(2, 'No such file or directory')]
  93. mock_response = mock.Mock()
  94. self.urllib2.urlopen.side_effect = [mock_response]
  95. mock_response.getcode.side_effect = [200]
  96. self.FileWrite.side_effect = [IOError(13, 'Permission denied.')]
  97. self.assertTrue(self.collector.config.is_googler)
  98. self.assertFalse(self.collector.config.opted_in)
  99. self.assertEqual(self.collector.config.countdown, 10)
  100. def assert_collects_metrics(self, update_metrics=None):
  101. expected_metrics = self.default_metrics
  102. self.default_metrics.update(update_metrics or {})
  103. # Assert we invoked the script to upload them.
  104. self.Popen.assert_called_with(
  105. [sys.executable, metrics.UPLOAD_SCRIPT], stdin=metrics.subprocess.PIPE)
  106. # Assert we collected the right metrics.
  107. write_call = self.Popen.return_value.stdin.write.call_args
  108. collected_metrics = json.loads(write_call[0][0])
  109. self.assertTrue(self.collector.collecting_metrics)
  110. self.assertEqual(collected_metrics, expected_metrics)
  111. def test_collects_system_information(self):
  112. """Tests that we collect information about the runtime environment."""
  113. self.FileRead.side_effect = [
  114. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  115. ]
  116. @self.collector.collect_metrics('fun')
  117. def fun():
  118. pass
  119. fun()
  120. self.assert_collects_metrics()
  121. def test_collects_added_metrics(self):
  122. """Tests that we can collect custom metrics."""
  123. self.FileRead.side_effect = [
  124. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  125. ]
  126. @self.collector.collect_metrics('fun')
  127. def fun():
  128. self.collector.add('foo', 'bar')
  129. fun()
  130. self.assert_collects_metrics({'foo': 'bar'})
  131. def test_collects_metrics_when_opted_in(self):
  132. """Tests that metrics are collected when the user opts-in."""
  133. self.FileRead.side_effect = [
  134. '{"is-googler": true, "countdown": 1234, "opt-in": true, "version": 0}'
  135. ]
  136. @self.collector.collect_metrics('fun')
  137. def fun():
  138. pass
  139. fun()
  140. self.assert_collects_metrics()
  141. @mock.patch('metrics.DISABLE_METRICS_COLLECTION', True)
  142. def test_metrics_collection_disabled(self):
  143. """Tests that metrics collection can be disabled via a global variable."""
  144. @self.collector.collect_metrics('fun')
  145. def fun():
  146. pass
  147. fun()
  148. self.assertFalse(self.collector.collecting_metrics)
  149. # We shouldn't have tried to read the config file.
  150. self.assertFalse(self.FileRead.called)
  151. # Nor tried to upload any metrics.
  152. self.assertFalse(self.Popen.called)
  153. def test_metrics_collection_disabled_not_googler(self):
  154. """Tests that metrics collection is disabled for non googlers."""
  155. self.FileRead.side_effect = [
  156. '{"is-googler": false, "countdown": 0, "opt-in": null, "version": 0}'
  157. ]
  158. @self.collector.collect_metrics('fun')
  159. def fun():
  160. pass
  161. fun()
  162. self.assertFalse(self.collector.collecting_metrics)
  163. self.assertFalse(self.collector.config.is_googler)
  164. self.assertIsNone(self.collector.config.opted_in)
  165. self.assertEqual(self.collector.config.countdown, 0)
  166. # Assert that we did not try to upload any metrics.
  167. self.assertFalse(self.Popen.called)
  168. def test_metrics_collection_disabled_opted_out(self):
  169. """Tests that metrics collection is disabled if the user opts out."""
  170. self.FileRead.side_effect = [
  171. '{"is-googler": true, "countdown": 0, "opt-in": false, "version": 0}'
  172. ]
  173. @self.collector.collect_metrics('fun')
  174. def fun():
  175. pass
  176. fun()
  177. self.assertFalse(self.collector.collecting_metrics)
  178. self.assertTrue(self.collector.config.is_googler)
  179. self.assertFalse(self.collector.config.opted_in)
  180. self.assertEqual(self.collector.config.countdown, 0)
  181. # Assert that we did not try to upload any metrics.
  182. self.assertFalse(self.Popen.called)
  183. def test_metrics_collection_disabled_non_zero_countdown(self):
  184. """Tests that metrics collection is disabled until the countdown expires."""
  185. self.FileRead.side_effect = [
  186. '{"is-googler": true, "countdown": 1, "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.assertTrue(self.collector.config.is_googler)
  194. self.assertFalse(self.collector.config.opted_in)
  195. self.assertEqual(self.collector.config.countdown, 1)
  196. # Assert that we did not try to upload any metrics.
  197. self.assertFalse(self.Popen.called)
  198. def test_handles_exceptions(self):
  199. """Tests that exception are caught and we exit with an appropriate code."""
  200. self.FileRead.side_effect = [
  201. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  202. ]
  203. @self.collector.collect_metrics('fun')
  204. def fun():
  205. raise ValueError
  206. # When an exception is raised, we should catch it, update exit-code,
  207. # collect metrics, and re-raise it.
  208. with self.assertRaises(ValueError):
  209. fun()
  210. self.assert_collects_metrics({'exit_code': 1})
  211. def test_handles_system_exit(self):
  212. """Tests that the sys.exit code is respected and metrics are collected."""
  213. self.FileRead.side_effect = [
  214. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  215. ]
  216. @self.collector.collect_metrics('fun')
  217. def fun():
  218. sys.exit(0)
  219. # When an exception is raised, we should catch it, update exit-code,
  220. # collect metrics, and re-raise it.
  221. with self.assertRaises(SystemExit) as cm:
  222. fun()
  223. self.assertEqual(cm.exception.code, 0)
  224. self.assert_collects_metrics({'exit_code': 0})
  225. def test_handles_system_exit_non_zero(self):
  226. """Tests that the sys.exit code is respected and metrics are collected."""
  227. self.FileRead.side_effect = [
  228. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  229. ]
  230. @self.collector.collect_metrics('fun')
  231. def fun():
  232. sys.exit(123)
  233. # When an exception is raised, we should catch it, update exit-code,
  234. # collect metrics, and re-raise it.
  235. with self.assertRaises(SystemExit) as cm:
  236. fun()
  237. self.assertEqual(cm.exception.code, 123)
  238. self.assert_collects_metrics({'exit_code': 123})
  239. def test_prints_notice_non_zero_countdown(self):
  240. """Tests that a notice is printed while the countdown is non-zero."""
  241. self.FileRead.side_effect = [
  242. '{"is-googler": true, "countdown": 1234, "opt-in": null, "version": 0}'
  243. ]
  244. with self.assertRaises(SystemExit) as cm:
  245. with self.collector.print_notice_and_exit():
  246. pass
  247. self.assertEqual(cm.exception.code, 0)
  248. self.print_notice.assert_called_once_with(1234)
  249. def test_prints_notice_zero_countdown(self):
  250. """Tests that a notice is printed when the countdown reaches 0."""
  251. self.FileRead.side_effect = [
  252. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  253. ]
  254. with self.assertRaises(SystemExit) as cm:
  255. with self.collector.print_notice_and_exit():
  256. pass
  257. self.assertEqual(cm.exception.code, 0)
  258. self.print_notice.assert_called_once_with(0)
  259. def test_doesnt_print_notice_opted_in(self):
  260. """Tests that a notice is not printed when the user opts-in."""
  261. self.FileRead.side_effect = [
  262. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  263. ]
  264. with self.assertRaises(SystemExit) as cm:
  265. with self.collector.print_notice_and_exit():
  266. pass
  267. self.assertEqual(cm.exception.code, 0)
  268. self.assertFalse(self.print_notice.called)
  269. def test_doesnt_print_notice_opted_out(self):
  270. """Tests that a notice is not printed when the user opts-out."""
  271. self.FileRead.side_effect = [
  272. '{"is-googler": true, "countdown": 0, "opt-in": false, "version": 0}'
  273. ]
  274. with self.assertRaises(SystemExit) as cm:
  275. with self.collector.print_notice_and_exit():
  276. pass
  277. self.assertEqual(cm.exception.code, 0)
  278. self.assertFalse(self.print_notice.called)
  279. @mock.patch('metrics.DISABLE_METRICS_COLLECTION', True)
  280. def test_doesnt_print_notice_disable_metrics_collection(self):
  281. with self.assertRaises(SystemExit) as cm:
  282. with self.collector.print_notice_and_exit():
  283. pass
  284. self.assertEqual(cm.exception.code, 0)
  285. self.assertFalse(self.print_notice.called)
  286. # We shouldn't have tried to read the config file.
  287. self.assertFalse(self.FileRead.called)
  288. def test_print_notice_handles_exceptions(self):
  289. """Tests that exception are caught and we exit with an appropriate code."""
  290. self.FileRead.side_effect = [
  291. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  292. ]
  293. # print_notice should catch the exception, print it and invoke sys.exit()
  294. with self.assertRaises(SystemExit) as cm:
  295. with self.collector.print_notice_and_exit():
  296. raise ValueError
  297. self.assertEqual(cm.exception.code, 1)
  298. self.assertTrue(self.print_notice.called)
  299. def test_print_notice_handles_system_exit(self):
  300. """Tests that the sys.exit code is respected and a notice is displayed."""
  301. self.FileRead.side_effect = [
  302. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  303. ]
  304. # print_notice should catch the exception, print it and invoke sys.exit()
  305. with self.assertRaises(SystemExit) as cm:
  306. with self.collector.print_notice_and_exit():
  307. sys.exit(0)
  308. self.assertEqual(cm.exception.code, 0)
  309. self.assertTrue(self.print_notice.called)
  310. def test_print_notice_handles_system_exit_non_zero(self):
  311. """Tests that the sys.exit code is respected and a notice is displayed."""
  312. self.FileRead.side_effect = [
  313. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  314. ]
  315. # When an exception is raised, we should catch it, update exit-code,
  316. # collect metrics, and re-raise it.
  317. with self.assertRaises(SystemExit) as cm:
  318. with self.collector.print_notice_and_exit():
  319. sys.exit(123)
  320. self.assertEqual(cm.exception.code, 123)
  321. self.assertTrue(self.print_notice.called)
  322. def test_counts_down(self):
  323. """Tests that the countdown works correctly."""
  324. self.FileRead.side_effect = [
  325. '{"is-googler": true, "countdown": 10, "opt-in": null, "version": 0}'
  326. ]
  327. # We define multiple functions to ensure it has no impact on countdown.
  328. @self.collector.collect_metrics('barn')
  329. def _barn():
  330. pass
  331. @self.collector.collect_metrics('fun')
  332. def _fun():
  333. pass
  334. def foo_main():
  335. pass
  336. # Assert that the countdown hasn't decrease yet.
  337. self.assertFalse(self.FileWrite.called)
  338. self.assertEqual(self.collector.config.countdown, 10)
  339. with self.assertRaises(SystemExit) as cm:
  340. with self.collector.print_notice_and_exit():
  341. foo_main()
  342. self.assertEqual(cm.exception.code, 0)
  343. # Assert that the countdown decreased by one, and the config file was
  344. # updated.
  345. self.assertEqual(self.collector.config.countdown, 9)
  346. self.print_notice.assert_called_once_with(10)
  347. self.assert_writes_file(
  348. self.config_file,
  349. {'is-googler': True, 'countdown': 9, 'opt-in': None, 'version': 0})
  350. def test_nested_functions(self):
  351. """Tests that a function can call another function for which metrics are
  352. collected."""
  353. self.FileRead.side_effect = [
  354. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  355. ]
  356. @self.collector.collect_metrics('barn')
  357. def barn():
  358. self.collector.add('barn-metric', 1)
  359. return 1000
  360. @self.collector.collect_metrics('fun')
  361. def fun():
  362. result = barn()
  363. self.collector.add('fun-metric', result + 1)
  364. fun()
  365. # Assert that we collected metrics for fun, but not for barn.
  366. self.assert_collects_metrics({'fun-metric': 1001})
  367. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  368. def test_version_change_from_hasnt_decided(self):
  369. # The user has not decided yet, and the countdown hasn't reached 0, so we're
  370. # not collecting metrics.
  371. self.FileRead.side_effect = [
  372. '{"is-googler": true, "countdown": 9, "opt-in": null, "version": 0}'
  373. ]
  374. with self.assertRaises(SystemExit) as cm:
  375. with self.collector.print_notice_and_exit():
  376. self.collector.add('foo-metric', 1)
  377. self.assertEqual(cm.exception.code, 0)
  378. # We display the notice informing the user of the changes.
  379. self.print_version_change.assert_called_once_with(0)
  380. # But the countdown is not reset.
  381. self.assert_writes_file(
  382. self.config_file,
  383. {'is-googler': True, 'countdown': 8, 'opt-in': None, 'version': 0})
  384. # And no metrics are uploaded.
  385. self.assertFalse(self.Popen.called)
  386. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  387. def test_version_change_from_opted_in_by_default(self):
  388. # The user has not decided yet, but the countdown has reached 0, and we're
  389. # collecting metrics.
  390. self.FileRead.side_effect = [
  391. '{"is-googler": true, "countdown": 0, "opt-in": null, "version": 0}'
  392. ]
  393. with self.assertRaises(SystemExit) as cm:
  394. with self.collector.print_notice_and_exit():
  395. self.collector.add('foo-metric', 1)
  396. self.assertEqual(cm.exception.code, 0)
  397. # We display the notice informing the user of the changes.
  398. self.print_version_change.assert_called_once_with(0)
  399. # We reset the countdown.
  400. self.assert_writes_file(
  401. self.config_file,
  402. {'is-googler': True, 'countdown': 9, 'opt-in': None, 'version': 0})
  403. # No metrics are uploaded.
  404. self.assertFalse(self.Popen.called)
  405. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  406. def test_version_change_from_opted_in(self):
  407. # The user has opted in, and we're collecting metrics.
  408. self.FileRead.side_effect = [
  409. '{"is-googler": true, "countdown": 0, "opt-in": true, "version": 0}'
  410. ]
  411. with self.assertRaises(SystemExit) as cm:
  412. with self.collector.print_notice_and_exit():
  413. self.collector.add('foo-metric', 1)
  414. self.assertEqual(cm.exception.code, 0)
  415. # We display the notice informing the user of the changes.
  416. self.print_version_change.assert_called_once_with(0)
  417. # We reset the countdown.
  418. self.assert_writes_file(
  419. self.config_file,
  420. {'is-googler': True, 'countdown': 9, 'opt-in': None, 'version': 0})
  421. # No metrics are uploaded.
  422. self.assertFalse(self.Popen.called)
  423. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  424. def test_version_change_from_opted_out(self):
  425. # The user has opted out and we're not collecting metrics.
  426. self.FileRead.side_effect = [
  427. '{"is-googler": true, "countdown": 0, "opt-in": false, "version": 0}'
  428. ]
  429. with self.assertRaises(SystemExit) as cm:
  430. with self.collector.print_notice_and_exit():
  431. self.collector.add('foo-metric', 1)
  432. self.assertEqual(cm.exception.code, 0)
  433. # We don't display any notice.
  434. self.assertFalse(self.print_version_change.called)
  435. self.assertFalse(self.print_notice.called)
  436. # We don't upload any metrics.
  437. self.assertFalse(self.Popen.called)
  438. # We don't modify the config.
  439. self.assertFalse(self.FileWrite.called)
  440. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  441. def test_version_change_non_googler(self):
  442. # The user is not a googler and we're not collecting metrics.
  443. self.FileRead.side_effect = [
  444. '{"is-googler": false, "countdown": 10, "opt-in": null, "version": 0}'
  445. ]
  446. with self.assertRaises(SystemExit) as cm:
  447. with self.collector.print_notice_and_exit():
  448. self.collector.add('foo-metric', 1)
  449. self.assertEqual(cm.exception.code, 0)
  450. # We don't display any notice.
  451. self.assertFalse(self.print_version_change.called)
  452. self.assertFalse(self.print_notice.called)
  453. # We don't upload any metrics.
  454. self.assertFalse(self.Popen.called)
  455. # We don't modify the config.
  456. self.assertFalse(self.FileWrite.called)
  457. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  458. def test_opting_in_updates_version(self):
  459. # The user is seeing the notice telling him of the version changes.
  460. self.FileRead.side_effect = [
  461. '{"is-googler": true, "countdown": 8, "opt-in": null, "version": 0}'
  462. ]
  463. self.collector.config.opted_in = True
  464. # We don't display any notice.
  465. self.assertFalse(self.print_version_change.called)
  466. self.assertFalse(self.print_notice.called)
  467. # We don't upload any metrics.
  468. self.assertFalse(self.Popen.called)
  469. # We update the version and opt-in the user.
  470. self.assert_writes_file(
  471. self.config_file,
  472. {'is-googler': True, 'countdown': 8, 'opt-in': True, 'version': 5})
  473. @mock.patch('metrics.metrics_utils.CURRENT_VERSION', 5)
  474. def test_opting_in_by_default_updates_version(self):
  475. # The user will be opted in by default on the next execution.
  476. self.FileRead.side_effect = [
  477. '{"is-googler": true, "countdown": 1, "opt-in": null, "version": 0}'
  478. ]
  479. with self.assertRaises(SystemExit) as cm:
  480. with self.collector.print_notice_and_exit():
  481. self.collector.add('foo-metric', 1)
  482. self.assertEqual(cm.exception.code, 0)
  483. # We display the notices.
  484. self.print_notice.assert_called_once_with(1)
  485. self.print_version_change.assert_called_once_with(0)
  486. # We don't upload any metrics.
  487. self.assertFalse(self.Popen.called)
  488. # We update the version and set the countdown to 0. In subsequent runs,
  489. # we'll start collecting metrics.
  490. self.assert_writes_file(
  491. self.config_file,
  492. {'is-googler': True, 'countdown': 0, 'opt-in': None, 'version': 5})
  493. if __name__ == '__main__':
  494. unittest.main()