From 32e3d1e37cd6a379cc5767dc3aaa43053df9a151 Mon Sep 17 00:00:00 2001 From: Edward Lemur Date: Thu, 12 Jul 2018 00:54:05 +0000 Subject: [PATCH] Add a library for monitoring. Change-Id: I64c3d143186be938042c12e2455bdb034d3bcba8 Reviewed-on: https://chromium-review.googlesource.com/1079639 Commit-Queue: Edward Lesmes Reviewed-by: Aaron Gable --- .gitignore | 3 + gclient.py | 31 ++++ metrics.README.md | 49 ++++++ metrics.py | 194 ++++++++++++++++++++++ metrics_utils.py | 88 ++++++++++ tests/gclient_eval_unittest.py | 4 + tests/gclient_smoketest.py | 1 + tests/gclient_test.py | 4 + tests/metrics_test.py | 295 +++++++++++++++++++++++++++++++++ tests/roll_dep_test.py | 15 +- upload_metrics.py | 24 +++ 11 files changed, 704 insertions(+), 4 deletions(-) create mode 100644 metrics.README.md create mode 100644 metrics.py create mode 100644 metrics_utils.py create mode 100644 tests/metrics_test.py create mode 100644 upload_metrics.py diff --git a/.gitignore b/.gitignore index 568a8c524..fb5762abb 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,6 @@ testing_support/google_appengine # Ignore emacs / vim backup files. *~ + +# Ignore the monitoring config. It is unique for each user. +/metrics.cfg diff --git a/gclient.py b/gclient.py index 11a39b4f6..1a3054ee7 100755 --- a/gclient.py +++ b/gclient.py @@ -104,6 +104,7 @@ import gclient_eval import gclient_scm import gclient_utils import git_cache +import metrics from third_party.repo.progress import Progress import subcommand import subprocess2 @@ -2842,6 +2843,36 @@ def CMDverify(parser, args): 'dependencies from disallowed hosts; check your DEPS file.') return 0 + +@subcommand.epilog("""For more information on what metrics are we collecting and +why, please read metrics.README.md or visit +.""") +def CMDmetrics(parser, args): + """Reports, and optionally modifies, the status of metric collection.""" + parser.add_option('--opt-in', action='store_true', dest='enable_metrics', + help='Opt-in to metrics collection.', + default=None) + parser.add_option('--opt-out', action='store_false', dest='enable_metrics', + help='Opt-out of metrics collection.') + options, args = parser.parse_args(args) + if args: + parser.error('Unused arguments: "%s"' % '" "'.join(args)) + if not metrics.collector.config.is_googler: + print("You're not a Googler. Metrics collection is disabled for you.") + return 0 + + if options.enable_metrics is not None: + metrics.collector.config.opted_in = options.enable_metrics + + if metrics.collector.config.opted_in is None: + print("You haven't opted in or out of metrics collection.") + elif metrics.collector.config.opted_in: + print("You have opted in. Thanks!") + else: + print("You have opted out. Please consider opting in.") + return 0 + + class OptionParser(optparse.OptionParser): gclientfile_default = os.environ.get('GCLIENT_FILE', '.gclient') diff --git a/metrics.README.md b/metrics.README.md new file mode 100644 index 000000000..f6c970cce --- /dev/null +++ b/metrics.README.md @@ -0,0 +1,49 @@ +# Why am I seeing this message? + +We're starting to collect metrics about how developers use gclient and other +tools in depot\_tools to better understand the performance and failure modes of +the tools, as well of the pain points and workflows of depot\_tools users. + +Pleas consider opting in. It will allow us to know what features are the most +important, what features can we deprecate, and what features should we develop +to better cover your use case. + +You will be opted in by default after 10 executions of depot\_tools commands, +after which the message will change to let you know metrics collection is taking +place. + +## What metrics are you collecting? + +First, some words about what data we are **NOT** collecting: + +- We won’t record any information that identifies you personally. +- We won't record the command line flag values. +- We won't record information about the current directory or environment flags. + +The metrics we're collecting are: + +- A timestamp, with a week resolution. +- The age of your depot\_tools checkout, with a week resolution. +- Your version of Python (in the format major.minor.micro). +- The OS of your machine (i.e. win, linux or mac). +- The arch of your machine (e.g. x64, arm, etc). +- The command that you ran (e.g. `gclient sync`). +- The flag names (but not their values) that you passed to the command + (e.g. `--force`, `--revision`). +- The execution time. +- The exit code. +- The project you're working on. We only record data about projects you can + fetch using depot\_tools' fetch command (e.g. Chromium, WebRTC, V8, etc) +- The age of your project checkout, with a week resolution. +- What features are you using in your DEPS and .gclient files. For example: + - Are you setting `use\_relative\_paths=True`? + - Are you using `recursedeps`? + + +# How can I stop seeing this message? + +You will stop seeing it once you have explicitly opted in or out of depot\_tools +metrics collection. + +You can run `gclient metrics --opt-in` or `gclient metrics --opt-out` to do so. +And you can opt-in or out at any time. diff --git a/metrics.py b/metrics.py new file mode 100644 index 000000000..ccac340a1 --- /dev/null +++ b/metrics.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python +# Copyright (c) 2018 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 functools +import json +import os +import subprocess +import sys +import tempfile +import threading +import time +import traceback +import urllib2 + +import detect_host_arch +import gclient_utils +import metrics_utils + + +DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__)) +CONFIG_FILE = os.path.join(DEPOT_TOOLS, 'metrics.cfg') +UPLOAD_SCRIPT = os.path.join(DEPOT_TOOLS, 'upload_metrics.py') + +APP_URL = 'https://cit-cli-metrics.appspot.com' + +DISABLE_METRICS_COLLECTION = os.environ.get('DEPOT_TOOLS_METRICS') == '0' +DEFAULT_COUNTDOWN = 10 + +INVALID_CONFIG_WARNING = ( + 'WARNING: Your metrics.cfg file was invalid or nonexistent. A new one has ' + 'been created' +) + + +class _Config(object): + def __init__(self): + self._initialized = False + self._config = {} + + def _ensure_initialized(self): + if self._initialized: + return + + try: + config = json.loads(gclient_utils.FileRead(CONFIG_FILE)) + except (IOError, ValueError): + config = {} + + self._config = config.copy() + + if 'is-googler' not in self._config: + # /should-upload is only accessible from Google IPs, so we only need to + # check if we can reach the page. An external developer would get access + # denied. + try: + req = urllib2.urlopen(APP_URL + '/should-upload') + self._config['is-googler'] = req.getcode() == 200 + except (urllib2.URLError, urllib2.HTTPError): + self._config['is-googler'] = False + + # Make sure the config variables we need are present, and initialize them to + # safe values otherwise. + self._config.setdefault('countdown', DEFAULT_COUNTDOWN) + self._config.setdefault('opt-in', None) + + if config != self._config: + print INVALID_CONFIG_WARNING + self._write_config() + + self._initialized = True + + def _write_config(self): + gclient_utils.FileWrite(CONFIG_FILE, json.dumps(self._config)) + + @property + def is_googler(self): + self._ensure_initialized() + return self._config['is-googler'] + + @property + def opted_in(self): + self._ensure_initialized() + return self._config['opt-in'] + + @opted_in.setter + def opted_in(self, value): + self._ensure_initialized() + self._config['opt-in'] = value + self._write_config() + + @property + def countdown(self): + self._ensure_initialized() + return self._config['countdown'] + + def decrease_countdown(self): + self._ensure_initialized() + if self.countdown == 0: + return + self._config['countdown'] -= 1 + self._write_config() + + +class MetricsCollector(object): + def __init__(self): + self._metrics_lock = threading.Lock() + self._reported_metrics = {} + self._config = _Config() + + @property + def config(self): + return self._config + + def add(self, name, value): + with self._metrics_lock: + self._reported_metrics[name] = value + + def _upload_metrics_data(self): + """Upload the metrics data to the AppEngine app.""" + # We invoke a subprocess, and use stdin.write instead of communicate(), + # so that we are able to return immediately, leaving the upload running in + # the background. + p = subprocess.Popen([sys.executable, UPLOAD_SCRIPT], stdin=subprocess.PIPE) + p.stdin.write(json.dumps(self._reported_metrics)) + + def _collect_metrics(self, func, command_name, *args, **kwargs): + self.add('command', command_name) + try: + start = time.time() + func(*args, **kwargs) + exception = None + # pylint: disable=bare-except + except: + exception = sys.exc_info() + finally: + self.add('execution_time', time.time() - start) + + # Print the exception before the metrics notice, so that the notice is + # clearly visible even if gclient fails. + if exception and not isinstance(exception[1], SystemExit): + traceback.print_exception(*exception) + + exit_code = metrics_utils.return_code_from_exception(exception) + self.add('exit_code', exit_code) + + # Print the metrics notice only if the user has not explicitly opted in + # or out. + if self.config.opted_in is None: + metrics_utils.print_notice(self.config.countdown) + + # Add metrics regarding environment information. + self.add('timestamp', metrics_utils.seconds_to_weeks(time.time())) + self.add('python_version', metrics_utils.get_python_version()) + self.add('host_os', gclient_utils.GetMacWinOrLinux()) + self.add('host_arch', detect_host_arch.HostArch()) + self.add('depot_tools_age', metrics_utils.get_repo_timestamp(DEPOT_TOOLS)) + + self._upload_metrics_data() + sys.exit(exit_code) + + def collect_metrics(self, command_name): + """A decorator used to collect metrics over the life of a function. + + This decorator executes the function and collects metrics about the system + environment and the function performance. It also catches all the Exceptions + and invokes sys.exit once the function is done executing. + """ + def _decorator(func): + # Do this first so we don't have to read, and possibly create a config + # file. + if DISABLE_METRICS_COLLECTION: + return func + # If the user has opted out or the user is not a googler, then there is no + # need to do anything. + if self.config.opted_in == False or not self.config.is_googler: + return func + # If the user hasn't opted in or out, and the countdown is not yet 0, just + # display the notice. + if self.config.opted_in == None and self.config.countdown > 0: + metrics_utils.print_notice(self.config.countdown) + self.config.decrease_countdown() + return func + # Otherwise, collect the metrics. + # Needed to preserve the __name__ and __doc__ attributes of func. + @functools.wraps(func) + def _inner(*args, **kwargs): + self._collect_metrics(func, command_name, *args, **kwargs) + return _inner + return _decorator + + +collector = MetricsCollector() diff --git a/metrics_utils.py b/metrics_utils.py new file mode 100644 index 000000000..9f51182de --- /dev/null +++ b/metrics_utils.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# Copyright (c) 2018 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 scm +import subprocess2 +import sys + +from third_party import colorama + + +NOTICE_COUNTDOWN_HEADER = ( + '*****************************************************\n' + '* METRICS COLLECTION WILL START IN %2d EXECUTIONS *' +) +NOTICE_COLLECTION_HEADER = ( + '*****************************************************\n' + '* METRICS COLLECTION IS TAKING PLACE *' +) +NOTICE_FOOTER = ( + '* *\n' + '* For more information, and for how to disable this *\n' + '* message, please see metrics.README.md in your *\n' + '* depot_tools checkout. *\n' + '*****************************************************\n' +) + + +def get_python_version(): + """Return the python version in the major.minor.micro format.""" + return '{v.major}.{v.minor}.{v.micro}'.format(v=sys.version_info) + + +def return_code_from_exception(exception): + """Returns the exit code that would result of raising the exception.""" + if exception is None: + return 0 + if isinstance(exception[1], SystemExit): + return exception[1].code + return 1 + + +def seconds_to_weeks(duration): + """Transform a |duration| from seconds to weeks approximately. + + Drops the lowest 19 bits of the integer representation, which ammounts to + about 6 days. + """ + return int(duration) >> 19 + + +def get_repo_timestamp(path_to_repo): + """Get an approximate timestamp for the upstream of |path_to_repo|. + + Returns the top two bits of the timestamp of the HEAD for the upstream of the + branch path_to_repo is checked out at. + """ + # Get the upstream for the current branch. If we're not in a branch, fallback + # to HEAD. + try: + upstream = scm.GIT.GetUpstreamBranch(path_to_repo) + except subprocess2.CalledProcessError: + upstream = 'HEAD' + + # Get the timestamp of the HEAD for the upstream of the current branch. + p = subprocess2.Popen( + ['git', '-C', path_to_repo, 'log', '-n1', upstream, '--format=%at'], + stdout=subprocess2.PIPE, stderr=subprocess2.PIPE) + stdout, _ = p.communicate() + + # If there was an error, give up. + if p.returncode != 0: + return None + + # Get the age of the checkout in weeks. + return seconds_to_weeks(stdout.strip()) + + +def print_notice(countdown): + """Print a notice to let the user know the status of metrics collection.""" + colorama.init() + print colorama.Fore.RED + '\033[1m' + if countdown: + print NOTICE_COUNTDOWN_HEADER % countdown + else: + print NOTICE_COLLECTION_HEADER + print NOTICE_FOOTER + colorama.Style.RESET_ALL diff --git a/tests/gclient_eval_unittest.py b/tests/gclient_eval_unittest.py index e34879b32..d97c10ea9 100755 --- a/tests/gclient_eval_unittest.py +++ b/tests/gclient_eval_unittest.py @@ -14,6 +14,10 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from third_party import schema +import metrics +# We have to disable monitoring before importing gclient. +metrics.DISABLE_METRICS_COLLECTION = True + import gclient import gclient_eval diff --git a/tests/gclient_smoketest.py b/tests/gclient_smoketest.py index 5673b0e42..f95b6d709 100755 --- a/tests/gclient_smoketest.py +++ b/tests/gclient_smoketest.py @@ -37,6 +37,7 @@ class GClientSmokeBase(fake_repos.FakeReposTestBase): # Make sure it doesn't try to auto update when testing! self.env = os.environ.copy() self.env['DEPOT_TOOLS_UPDATE'] = '0' + self.env['DEPOT_TOOLS_METRICS'] = '0' def gclient(self, cmd, cwd=None): if not cwd: diff --git a/tests/gclient_test.py b/tests/gclient_test.py index a772a3613..3919e6491 100755 --- a/tests/gclient_test.py +++ b/tests/gclient_test.py @@ -18,6 +18,10 @@ import unittest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import metrics +# We have to disable monitoring before importing gclient. +metrics.DISABLE_METRICS_COLLECTION = True + import gclient import gclient_utils import gclient_scm diff --git a/tests/metrics_test.py b/tests/metrics_test.py new file mode 100644 index 000000000..26cd0e0b9 --- /dev/null +++ b/tests/metrics_test.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python +# Copyright (c) 2018 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 json +import os +import sys +import unittest + +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, ROOT_DIR) + +import metrics +import cStringIO + +from third_party import mock + + +class TimeMock(object): + def __init__(self): + self._count = 0 + + def __call__(self): + self._count += 1 + return self._count * 1000 + + +class MetricsCollectorTest(unittest.TestCase): + def setUp(self): + self.collector = metrics.MetricsCollector() + + # Keep track of the URL requests, file reads/writes and subprocess spawned. + self.urllib2 = mock.Mock() + self.print_notice = mock.Mock() + self.Popen = mock.Mock() + self.FileWrite = mock.Mock() + self.FileRead = mock.Mock() + + mock.patch('metrics.urllib2', self.urllib2).start() + mock.patch('metrics.subprocess.Popen', self.Popen).start() + mock.patch('metrics.gclient_utils.FileWrite', self.FileWrite).start() + mock.patch('metrics.gclient_utils.FileRead', self.FileRead).start() + mock.patch('metrics.metrics_utils.print_notice', self.print_notice).start() + + # Patch the methods used to get the system information, so we have a known + # environment. + mock.patch('metrics.tempfile.mkstemp', + lambda: (None, '/tmp/metrics.json')).start() + mock.patch('metrics.time.time', + TimeMock()).start() + mock.patch('metrics.metrics_utils.get_python_version', + lambda: '2.7.13').start() + mock.patch('metrics.gclient_utils.GetMacWinOrLinux', + lambda: 'linux').start() + mock.patch('metrics.detect_host_arch.HostArch', + lambda: 'x86').start() + mock.patch('metrics_utils.get_repo_timestamp', + lambda _: 1234).start() + + self.default_metrics = { + "python_version": "2.7.13", + "execution_time": 1000, + "timestamp": 0, + "exit_code": 0, + "command": "fun", + "depot_tools_age": 1234, + "host_arch": "x86", + "host_os": "linux", + } + + self.addCleanup(mock.patch.stopall) + + def assert_collects_metrics(self, update_metrics=None): + expected_metrics = self.default_metrics + self.default_metrics.update(update_metrics or {}) + # Assert we invoked the script to upload them. + self.Popen.assert_called_with( + [sys.executable, metrics.UPLOAD_SCRIPT], stdin=metrics.subprocess.PIPE) + # Assert we collected the right metrics. + write_call = self.Popen.return_value.stdin.write.call_args + collected_metrics = json.loads(write_call[0][0]) + self.assertEqual(collected_metrics, expected_metrics) + + + def test_collects_system_information(self): + """Tests that we collect information about the runtime environment.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 0, "opt-in": null}' + ] + @self.collector.collect_metrics('fun') + def fun(): + pass + + with self.assertRaises(SystemExit) as cm: + fun() + self.assertEqual(cm.exception.code, 0) + self.assert_collects_metrics() + + def test_collects_added_metrics(self): + """Tests that we can collect custom metrics.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 0, "opt-in": null}' + ] + @self.collector.collect_metrics('fun') + def fun(): + self.collector.add('foo', 'bar') + + with self.assertRaises(SystemExit) as cm: + fun() + self.assertEqual(cm.exception.code, 0) + self.assert_collects_metrics({'foo': 'bar'}) + + def test_collects_metrics_when_opted_in(self): + """Tests that metrics are collected when the user opts-in.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 1234, "opt-in": true}' + ] + @self.collector.collect_metrics('fun') + def fun(): + pass + + with self.assertRaises(SystemExit) as cm: + fun() + self.assertEqual(cm.exception.code, 0) + self.assert_collects_metrics() + + @mock.patch('metrics.DISABLE_METRICS_COLLECTION', True) + def test_metrics_collection_disabled(self): + """Tests that metrics collection can be disabled via a global variable.""" + @self.collector.collect_metrics('fun') + def fun(): + pass + + fun() + + # We shouldn't have tried to read the config file. + self.assertFalse(self.FileRead.called) + # Nor tried to upload any metrics. + self.assertFalse(self.Popen.called) + + def test_metrics_collection_disabled_not_googler(self): + """Tests that metrics collection is disabled for non googlers.""" + self.FileRead.side_effect = [ + '{"is-googler": false, "countdown": 0, "opt-in": null}' + ] + @self.collector.collect_metrics('fun') + def fun(): + pass + + fun() + + self.assertFalse(self.collector.config.is_googler) + self.assertIsNone(self.collector.config.opted_in) + self.assertEqual(self.collector.config.countdown, 0) + # Assert that we did not try to upload any metrics. + self.assertFalse(self.Popen.called) + + def test_metrics_collection_disabled_opted_out(self): + """Tests that metrics collection is disabled if the user opts out.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 0, "opt-in": false}' + ] + @self.collector.collect_metrics('fun') + def fun(): + pass + + fun() + + self.assertTrue(self.collector.config.is_googler) + self.assertFalse(self.collector.config.opted_in) + self.assertEqual(self.collector.config.countdown, 0) + # Assert that we did not try to upload any metrics. + self.assertFalse(self.Popen.called) + + def test_metrics_collection_disabled_non_zero_countdown(self): + """Tests that metrics collection is disabled until the countdown expires.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 1, "opt-in": null}' + ] + @self.collector.collect_metrics('fun') + def fun(): + pass + + fun() + + self.assertTrue(self.collector.config.is_googler) + self.assertFalse(self.collector.config.opted_in) + # The countdown should've decreased after the invocation. + self.assertEqual(self.collector.config.countdown, 0) + # Assert that we did not try to upload any metrics. + self.assertFalse(self.Popen.called) + + def test_prints_notice_non_zero_countdown(self): + """Tests that a notice is printed while the countdown is non-zero.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 1234, "opt-in": null}' + ] + @self.collector.collect_metrics('fun') + def fun(): + pass + fun() + self.print_notice.assert_called_once_with(1234) + + def test_prints_notice_zero_countdown(self): + """Tests that a notice is printed when the countdown reaches 0.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 0, "opt-in": null}' + ] + @self.collector.collect_metrics('fun') + def fun(): + pass + + with self.assertRaises(SystemExit) as cm: + fun() + self.assertEqual(cm.exception.code, 0) + self.print_notice.assert_called_once_with(0) + + def test_doesnt_print_notice_opted_in(self): + """Tests that a notice is not printed when the user opts-in.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 0, "opt-in": true}' + ] + @self.collector.collect_metrics('fun') + def fun(): + pass + + with self.assertRaises(SystemExit) as cm: + fun() + self.assertEqual(cm.exception.code, 0) + self.assertFalse(self.print_notice.called) + + def test_doesnt_print_notice_opted_out(self): + """Tests that a notice is not printed when the user opts-out.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 0, "opt-in": false}' + ] + @self.collector.collect_metrics('fun') + def fun(): + pass + + fun() + self.assertFalse(self.print_notice.called) + + def test_handles_exceptions(self): + """Tests that exception are caught and we exit with an appropriate code.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 0, "opt-in": true}' + ] + @self.collector.collect_metrics('fun') + def fun(): + raise ValueError + + # When an exception is raised, we should catch it, print the traceback and + # invoke sys.exit with a non-zero exit code. + with self.assertRaises(SystemExit) as cm: + fun() + self.assertEqual(cm.exception.code, 1) + self.assert_collects_metrics({'exit_code': 1}) + + def test_handles_system_exit(self): + """Tests that the sys.exit code is respected and metrics are collected.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 0, "opt-in": true}' + ] + @self.collector.collect_metrics('fun') + def fun(): + sys.exit(0) + + # When an exception is raised, we should catch it, print the traceback and + # invoke sys.exit with a non-zero exit code. + with self.assertRaises(SystemExit) as cm: + fun() + self.assertEqual(cm.exception.code, 0) + self.assert_collects_metrics({'exit_code': 0}) + + def test_handles_system_exit_non_zero(self): + """Tests that the sys.exit code is respected and metrics are collected.""" + self.FileRead.side_effect = [ + '{"is-googler": true, "countdown": 0, "opt-in": true}' + ] + @self.collector.collect_metrics('fun') + def fun(): + sys.exit(123) + + # When an exception is raised, we should catch it, print the traceback and + # invoke sys.exit with a non-zero exit code. + with self.assertRaises(SystemExit) as cm: + fun() + self.assertEqual(cm.exception.code, 123) + self.assert_collects_metrics({'exit_code': 123}) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/roll_dep_test.py b/tests/roll_dep_test.py index 56f97cb3f..42c6ed512 100644 --- a/tests/roll_dep_test.py +++ b/tests/roll_dep_test.py @@ -52,6 +52,7 @@ class RollDepTest(fake_repos.FakeReposTestBase): # Make sure it doesn't try to auto update when testing! self.env = os.environ.copy() self.env['DEPOT_TOOLS_UPDATE'] = '0' + self.env['DEPOT_TOOLS_METRICS'] = '0' self.enabled = self.FAKE_REPOS.set_up_git() self.src_dir = os.path.join(self.root_dir, 'src') @@ -64,7 +65,7 @@ class RollDepTest(fake_repos.FakeReposTestBase): def call(self, cmd, cwd=None): cwd = cwd or self.src_dir process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + stderr=subprocess.PIPE, env=self.env, shell=sys.platform.startswith('win')) stdout, stderr = process.communicate() logging.debug("XXX: %s\n%s\nXXX" % (' '.join(cmd), stdout)) @@ -75,9 +76,12 @@ class RollDepTest(fake_repos.FakeReposTestBase): def testRollsDep(self): if not self.enabled: return - stdout = self.call([ROLL_DEP, 'src/foo'])[0] + stdout, stderr, returncode = self.call([ROLL_DEP, 'src/foo']) expected_revision = self.githash('repo_2', 3) + self.assertEqual(stderr, '') + self.assertEqual(returncode, 0) + with open(os.path.join(self.src_dir, 'DEPS')) as f: contents = f.read() @@ -99,10 +103,13 @@ class RollDepTest(fake_repos.FakeReposTestBase): def testRollsDepToSpecificRevision(self): if not self.enabled: return - stdout = self.call([ROLL_DEP, 'src/foo', - '--roll-to', self.githash('repo_2', 2)])[0] + stdout, stderr, returncode = self.call( + [ROLL_DEP, 'src/foo', '--roll-to', self.githash('repo_2', 2)]) expected_revision = self.githash('repo_2', 2) + self.assertEqual(stderr, '') + self.assertEqual(returncode, 0) + with open(os.path.join(self.src_dir, 'DEPS')) as f: contents = f.read() diff --git a/upload_metrics.py b/upload_metrics.py new file mode 100644 index 000000000..4be5b2348 --- /dev/null +++ b/upload_metrics.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# Copyright (c) 2018 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 sys +import urllib2 + + +APP_URL = 'https://cit-cli-metrics.appspot.com' + + +def main(): + metrics = raw_input() + try: + urllib2.urlopen(APP_URL + '/upload', metrics) + except urllib2.HTTPError: + pass + + return 0 + + +if __name__ == '__main__': + sys.exit(main())