Selaa lähdekoodia

Implement reclient metrics uploading

This cl will be submitted after the cl to add the metadata to the
reproxy.cfg file has been submitted to chromium/src: crrev/c/4513215

Bug: b/281504726
Change-Id: Ifa6d5f56d4a85ccb9ec8e4f70207d8b6b9382e89
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/4516023
Commit-Queue: Ben Segall <bentekkie@google.com>
Reviewed-by: Philipp Wollermann <philwo@google.com>
Reviewed-by: Junji Watanabe <jwata@google.com>
Reviewed-by: Takuto Ikuta <tikuta@chromium.org>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Ben Segall 2 vuotta sitten
vanhempi
commit
530d86d40b
8 muutettua tiedostoa jossa 556 lisäystä ja 2 poistoa
  1. 3 0
      .gitignore
  2. 2 0
      OWNERS
  3. 28 1
      reclient_helper.py
  4. 61 0
      reclient_metrics.README.md
  5. 143 0
      reclient_metrics.py
  6. 2 0
      tests/OWNERS
  7. 88 1
      tests/ninja_reclient_test.py
  8. 229 0
      tests/reclient_metrics_test.py

+ 3 - 0
.gitignore

@@ -89,6 +89,9 @@ testing_support/google_appengine
 # Ignore the ninjalog upload config.
 # Ignore the ninjalog upload config.
 /ninjalog.cfg
 /ninjalog.cfg
 
 
+# Ignore reclient metrics upload config.
+/reclient_metrics.cfg
+
 # Ignore git traces produced by git push on git-cl upload.
 # Ignore git traces produced by git push on git-cl upload.
 /traces
 /traces
 
 

+ 2 - 0
OWNERS

@@ -20,6 +20,8 @@ per-file ninjalog*=tikuta@chromium.org
 per-file post_build_ninja_summary.py=brucedawson@chromium.org
 per-file post_build_ninja_summary.py=brucedawson@chromium.org
 per-file reclient_helper.py=file://BUILD_OWNERS
 per-file reclient_helper.py=file://BUILD_OWNERS
 per-file reclient_helper.py=file://RECLIENT_OWNERS
 per-file reclient_helper.py=file://RECLIENT_OWNERS
+per-file reclient_metrics.py=file://BUILD_OWNERS
+per-file reclient_metrics.py=file://RECLIENT_OWNERS
 per-file reclientreport*=file://RECLIENT_OWNERS
 per-file reclientreport*=file://RECLIENT_OWNERS
 
 
 per-file presubmit*.py=brucedawson@chromium.org
 per-file presubmit*.py=brucedawson@chromium.org

+ 28 - 1
reclient_helper.py

@@ -13,6 +13,7 @@ import subprocess
 import sys
 import sys
 
 
 import gclient_paths
 import gclient_paths
+import reclient_metrics
 
 
 
 
 def find_reclient_bin_dir():
 def find_reclient_bin_dir():
@@ -90,6 +91,25 @@ def find_cache_dir(tmp_dir):
   return os.path.join(tmp_dir, 'cache')
   return os.path.join(tmp_dir, 'cache')
 
 
 
 
+def set_reproxy_metrics_flags():
+  """Helper to setup metrics collection flags for reproxy.
+
+  The following env vars are set if not already set:
+    RBE_metrics_project=chromium-reclient-metrics
+    RBE_invocation_id=$AUTONINJA_BUILD_ID
+    RBE_metrics_table=rbe_metrics.builds
+    RBE_metrics_labels=source=developer
+    RBE_metrics_prefix=go.chromium.org
+  """
+  autoninja_id = os.environ.get("AUTONINJA_BUILD_ID")
+  if autoninja_id is not None:
+    os.environ.setdefault("RBE_invocation_id", autoninja_id)
+  os.environ.setdefault("RBE_metrics_project", "chromium-reclient-metrics")
+  os.environ.setdefault("RBE_metrics_table", "rbe_metrics.builds")
+  os.environ.setdefault("RBE_metrics_labels", "source=developer")
+  os.environ.setdefault("RBE_metrics_prefix", "go.chromium.org")
+
+
 def set_reproxy_path_flags(out_dir, make_dirs=True):
 def set_reproxy_path_flags(out_dir, make_dirs=True):
   """Helper to setup the logs and cache directories for reclient.
   """Helper to setup the logs and cache directories for reclient.
 
 
@@ -157,12 +177,19 @@ def build_context(argv):
           file=sys.stderr)
           file=sys.stderr)
     yield 1
     yield 1
     return
     return
+
+  ninja_out = find_ninja_out_dir(argv)
+
   try:
   try:
-    set_reproxy_path_flags(find_ninja_out_dir(argv))
+    set_reproxy_path_flags(ninja_out)
   except OSError:
   except OSError:
     print("Error creating reproxy_tmp in output dir", file=sys.stderr)
     print("Error creating reproxy_tmp in output dir", file=sys.stderr)
     yield 1
     yield 1
     return
     return
+
+  if reclient_metrics.check_status(ninja_out):
+    set_reproxy_metrics_flags()
+
   reproxy_ret_code = start_reproxy(reclient_cfg, reclient_bin_dir)
   reproxy_ret_code = start_reproxy(reclient_cfg, reclient_bin_dir)
   if reproxy_ret_code != 0:
   if reproxy_ret_code != 0:
     yield reproxy_ret_code
     yield reproxy_ret_code

+ 61 - 0
reclient_metrics.README.md

@@ -0,0 +1,61 @@
+# Reclient metric collection
+
+[TOC]
+
+## Overview
+
+When chromium developers enable download_remoteexec_cfg and run their build with use_remoteexec enabled,
+
+e.g.
+
+`.gclient`
+```
+solutions = [
+  {
+    "name": "src",
+    "url": "https://chromium.googlesource.com/chromium/src.git",
+    "managed": False,
+    "custom_deps": {},
+    "custom_vars": {
+ ...
+      "download_remoteexec_cfg": True,
+ ...
+    },
+    ...
+  },
+]
+```
+
+```
+$ gclient runhooks
+$ gn gen -C out/Release --args="use_remoteexec=true"
+$ autoninja -C out/Release chrome
+```
+
+reproxy uploads reclient's build metrics. The download_remoteexec_cfg gclient flag is only available for Google employees.
+
+Before uploading metrics, reproxy will show a message 10 times to warn users that we will collect build metrics.
+
+Users can opt in by running the following command.
+$ python3 reclient_metrics.py opt-in
+
+Users can opt out by running the following command.
+$ python3 reclient_metrics.py opt-out
+
+## What type of data are collected?
+
+We upload the contents of <ninja-out>/.reproxy_tmp/logs/rbe_metrics.txt.
+This contains
+* Flags passed to reproxy
+  * Auth related flags are filtered out by reproxy
+* Start and end time of build tasks
+* Aggregated durations and counts of events during remote build actions
+* OS (e.g. Win, Mac or Linux)
+* Number of cpu cores and the amount of RAM of the building machine
+
+We don't collect personally identifiable information
+(e.g. username, ip address).
+
+## Why are reproxy metrics collected? / How are the metrics collected?
+
+We (Chrome build team/Reclient team) collect build metrics to find slow build tasks that harm developer's productivity. Based on collected stats, we find the place/build tasks where we need to focus on. Also we use collected stats to track Chrome build performance on developer's machine. We'll use these stats to measure how much we can/can't improve build performance on developer machines.

+ 143 - 0
reclient_metrics.py

@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+# Copyright 2023 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.
+"""This script manages the counter for how many times developers should
+be notified before uploading reclient metrics."""
+
+import json
+import os
+import subprocess
+import sys
+
+THIS_DIR = os.path.dirname(__file__)
+CONFIG = os.path.join(THIS_DIR, 'reclient_metrics.cfg')
+VERSION = 1
+
+
+def default_config():
+  return {
+      'is-googler': is_googler(),
+      'countdown': 10,
+      'version': VERSION,
+  }
+
+
+def load_config():
+  config = None
+  try:
+    with open(CONFIG) as f:
+      raw_config = json.load(f)
+      if raw_config['version'] == VERSION:
+        raw_config['countdown'] = max(0, raw_config['countdown'] - 1)
+        config = raw_config
+  except Exception:
+    pass
+  if not config:
+    config = default_config()
+  save_config(config)
+  return config
+
+
+def save_config(config):
+  with open(CONFIG, 'w') as f:
+    json.dump(config, f)
+
+
+def show_message(config, ninja_out):
+  print("""
+Your reclient metrics will be uploaded to the chromium build metrics database. The uploaded metrics will be used to analyze user side build performance.
+
+We upload the contents of {ninja_out_abs}.
+This contains
+* Flags passed to reproxy
+  * Auth related flags are filtered out by reproxy
+* Start and end time of build tasks
+* Aggregated durations and counts of events during remote build actions
+* OS (e.g. Win, Mac or Linux)
+* Number of cpu cores and the amount of RAM of the building machine
+
+Uploading reclient metrics will be started after you run autoninja another {config_count} time(s).
+
+If you don't want to upload reclient metrics, please run following command.
+$ python3 {file_path} opt-out
+
+If you want to allow upload reclient metrics from next autoninja run, please run the
+following command.
+$ python3 {file_path} opt-in
+
+If you have questions about this, please send an email to foundry-x@google.com
+
+You can find a more detailed explanation in
+{metrics_readme_path}
+or
+https://chromium.googlesource.com/chromium/tools/depot_tools/+/main/reclient_metrics.README.md
+""".format(
+      ninja_out_abs=os.path.abspath(
+          os.path.join(ninja_out, ".reproxy_tmp", "logs", "rbe_metrics.txt")),
+      config_count=config.get("countdown", 0),
+      file_path=__file__,
+      metrics_readme_path=os.path.abspath(
+          os.path.join(THIS_DIR, "reclient_metrics.README.md")),
+  ))
+
+
+def is_googler(config=None):
+  """Check whether this user is Googler or not."""
+  if config is not None and 'is-googler' in config:
+    return config['is-googler']
+  # Use cipd auth-info to check for googler status as
+  # downloading rewrapper configs already requires cipd to be logged in
+  p = subprocess.run('cipd auth-info',
+                     stdout=subprocess.PIPE,
+                     stderr=subprocess.PIPE,
+                     text=True,
+                     shell=True)
+  if p.returncode != 0:
+    return False
+  lines = p.stdout.splitlines()
+  if len(lines) == 0:
+    return False
+  l = lines[0]
+  # |l| will be like 'Logged in as <user>@google.com.' for googlers.
+  return l.startswith('Logged in as ') and l.endswith('@google.com.')
+
+
+def check_status(ninja_out):
+  """Checks metrics collections status and shows notice to user if needed.
+
+  Returns True if metrics should be collected."""
+  config = load_config()
+  if not is_googler(config):
+    return False
+  if 'opt-in' in config:
+    return config['opt-in']
+  if config.get("countdown", 0) > 0:
+    show_message(config, ninja_out)
+    return False
+  return True
+
+
+def main(argv):
+  cfg = load_config()
+
+  if not is_googler(cfg):
+    save_config(cfg)
+    return 0
+
+  if len(argv) == 2 and argv[1] == 'opt-in':
+    cfg['opt-in'] = True
+    cfg['countdown'] = 0
+    save_config(cfg)
+    print('reclient metrics upload is opted in.')
+    return 0
+
+  if len(argv) == 2 and argv[1] == 'opt-out':
+    cfg['opt-in'] = False
+    save_config(cfg)
+    print('reclient metrics upload is opted out.')
+    return 0
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv))

+ 2 - 0
tests/OWNERS

@@ -3,3 +3,5 @@ per-file autoninja_test.py=tikuta@chromium.org
 per-file ninjalog_uploader_test.py=tikuta@chromium.org
 per-file ninjalog_uploader_test.py=tikuta@chromium.org
 per-file ninja_reclient_test.py=file://BUILD_OWNERS
 per-file ninja_reclient_test.py=file://BUILD_OWNERS
 per-file ninja_reclient_test.py=file://RECLIENT_OWNERS
 per-file ninja_reclient_test.py=file://RECLIENT_OWNERS
+per-file reclient_metrics_test.py=file://BUILD_OWNERS
+per-file reclient_metrics_test.py=file://RECLIENT_OWNERS

+ 88 - 1
tests/ninja_reclient_test.py

@@ -41,7 +41,9 @@ class NinjaReclientTest(trial_dir.TestCase):
   @unittest.mock.patch.dict(os.environ, {})
   @unittest.mock.patch.dict(os.environ, {})
   @unittest.mock.patch('subprocess.call', return_value=0)
   @unittest.mock.patch('subprocess.call', return_value=0)
   @unittest.mock.patch('ninja.main', return_value=0)
   @unittest.mock.patch('ninja.main', return_value=0)
-  def test_ninja_reclient(self, mock_ninja, mock_call):
+  @unittest.mock.patch('reclient_metrics.check_status', return_value=True)
+  def test_ninja_reclient_collect_metrics(self, mock_metrics_status, mock_ninja,
+                                          mock_call):
     reclient_bin_dir = os.path.join('src', 'buildtools', 'reclient')
     reclient_bin_dir = os.path.join('src', 'buildtools', 'reclient')
     reclient_cfg = os.path.join('src', 'buildtools', 'reclient_cfgs',
     reclient_cfg = os.path.join('src', 'buildtools', 'reclient_cfgs',
                                 'reproxy.cfg')
                                 'reproxy.cfg')
@@ -91,6 +93,91 @@ class NinjaReclientTest(trial_dir.TestCase):
               os.path.join(self.root_dir, "out", "a",
               os.path.join(self.root_dir, "out", "a",
                            ".reproxy_tmp").encode()).hexdigest())
                            ".reproxy_tmp").encode()).hexdigest())
 
 
+    self.assertEqual(os.environ.get('RBE_metrics_project'),
+                     "chromium-reclient-metrics")
+    self.assertEqual(os.environ.get('RBE_metrics_table'), "rbe_metrics.builds")
+    self.assertEqual(os.environ.get('RBE_metrics_labels'), "source=developer")
+    self.assertEqual(os.environ.get('RBE_metrics_prefix'), "go.chromium.org")
+
+    mock_metrics_status.assert_called_once_with("out/a")
+    mock_ninja.assert_called_once_with(argv)
+    mock_call.assert_has_calls([
+        unittest.mock.call([
+            os.path.join(self.root_dir, reclient_bin_dir,
+                         'bootstrap' + gclient_paths.GetExeSuffix()),
+            "--re_proxy=" +
+            os.path.join(self.root_dir, reclient_bin_dir,
+                         'reproxy' + gclient_paths.GetExeSuffix()),
+            "--cfg=" + os.path.join(self.root_dir, reclient_cfg)
+        ]),
+        unittest.mock.call([
+            os.path.join(self.root_dir, reclient_bin_dir,
+                         'bootstrap' + gclient_paths.GetExeSuffix()),
+            "--shutdown", "--cfg=" + os.path.join(self.root_dir, reclient_cfg)
+        ]),
+    ])
+
+  @unittest.mock.patch.dict(os.environ, {})
+  @unittest.mock.patch('subprocess.call', return_value=0)
+  @unittest.mock.patch('ninja.main', return_value=0)
+  @unittest.mock.patch('reclient_metrics.check_status', return_value=False)
+  def test_ninja_reclient_do_not_collect_metrics(self, mock_metrics_status,
+                                                 mock_ninja, mock_call):
+    reclient_bin_dir = os.path.join('src', 'buildtools', 'reclient')
+    reclient_cfg = os.path.join('src', 'buildtools', 'reclient_cfgs',
+                                'reproxy.cfg')
+    write('.gclient', '')
+    write('.gclient_entries', 'entries = {"buildtools": "..."}')
+    write(os.path.join(reclient_bin_dir, 'version.txt'), '0.0')
+    write(reclient_cfg, '0.0')
+    argv = ["ninja_reclient.py", "-C", "out/a", "chrome"]
+
+    self.assertEqual(0, ninja_reclient.main(argv))
+
+    self.assertTrue(
+        os.path.isdir(os.path.join(self.root_dir, "out", "a", ".reproxy_tmp")))
+    self.assertTrue(
+        os.path.isdir(
+            os.path.join(
+                self.root_dir, ".reproxy_cache",
+                hashlib.md5(
+                    os.path.join(self.root_dir, "out", "a",
+                                 ".reproxy_tmp").encode()).hexdigest())))
+    self.assertTrue(
+        os.path.isdir(
+            os.path.join(self.root_dir, "out", "a", ".reproxy_tmp", "logs")))
+    self.assertEqual(
+        os.environ.get('RBE_output_dir'),
+        os.path.join(self.root_dir, "out", "a", ".reproxy_tmp", "logs"))
+    self.assertEqual(
+        os.environ.get('RBE_proxy_log_dir'),
+        os.path.join(self.root_dir, "out", "a", ".reproxy_tmp", "logs"))
+    self.assertEqual(
+        os.environ.get('RBE_cache_dir'),
+        os.path.join(
+            self.root_dir, ".reproxy_cache",
+            hashlib.md5(
+                os.path.join(self.root_dir, "out", "a",
+                             ".reproxy_tmp").encode()).hexdigest()))
+    if sys.platform.startswith('win'):
+      self.assertEqual(
+          os.environ.get('RBE_server_address'),
+          "pipe://%s/reproxy.pipe" % hashlib.md5(
+              os.path.join(self.root_dir, "out", "a",
+                           ".reproxy_tmp").encode()).hexdigest())
+    else:
+      self.assertEqual(
+          os.environ.get('RBE_server_address'),
+          "unix:///tmp/reproxy_%s.sock" % hashlib.sha256(
+              os.path.join(self.root_dir, "out", "a",
+                           ".reproxy_tmp").encode()).hexdigest())
+
+    self.assertEqual(os.environ.get('RBE_metrics_project'), None)
+    self.assertEqual(os.environ.get('RBE_metrics_table'), None)
+    self.assertEqual(os.environ.get('RBE_metrics_labels'), None)
+    self.assertEqual(os.environ.get('RBE_metrics_prefix'), None)
+
+    mock_metrics_status.assert_called_once_with("out/a")
     mock_ninja.assert_called_once_with(argv)
     mock_ninja.assert_called_once_with(argv)
     mock_call.assert_has_calls([
     mock_call.assert_has_calls([
         unittest.mock.call([
         unittest.mock.call([

+ 229 - 0
tests/reclient_metrics_test.py

@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023 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 io
+import os
+import os.path
+import sys
+import tempfile
+import unittest
+import unittest.mock
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, ROOT_DIR)
+
+import reclient_metrics
+
+
+class ReclientMetricsTest(unittest.TestCase):
+  def test_is_googler(self):
+    with unittest.mock.patch('subprocess.run') as run_mock:
+      run_mock.return_value.returncode = 0
+      run_mock.return_value.stdout = 'Logged in as abc@google.com.'
+      self.assertTrue(reclient_metrics.is_googler())
+
+    with unittest.mock.patch('subprocess.run') as run_mock:
+      run_mock.return_value.returncode = 1
+      self.assertFalse(reclient_metrics.is_googler())
+
+    with unittest.mock.patch('subprocess.run') as run_mock:
+      run_mock.return_value.returncode = 0
+      run_mock.return_value.stdout = ''
+      self.assertFalse(reclient_metrics.is_googler())
+
+    with unittest.mock.patch('subprocess.run') as run_mock:
+      run_mock.return_value.returncode = 0
+      run_mock.return_value.stdout = 'Logged in as foo@example.com.'
+      self.assertFalse(reclient_metrics.is_googler())
+
+    with unittest.mock.patch('subprocess.run') as run_mock:
+      self.assertTrue(reclient_metrics.is_googler({
+          'is-googler': True,
+      }))
+      self.assertFalse(reclient_metrics.is_googler({
+          'is-googler': False,
+      }))
+      run_mock.assert_not_called()
+
+  def test_load_and_save_config(self):
+    with tempfile.TemporaryDirectory() as tmpdir:
+      reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
+      with unittest.mock.patch('subprocess.run') as run_mock:
+        run_mock.return_value.returncode = 0
+        run_mock.return_value.stdout = 'Logged in as abc@google.com.'
+        cfg1 = reclient_metrics.load_config()
+        self.assertDictEqual(
+            cfg1, {
+                'is-googler': True,
+                'countdown': 10,
+                'version': reclient_metrics.VERSION,
+            })
+        reclient_metrics.save_config(cfg1)
+        cfg2 = reclient_metrics.load_config()
+        self.assertDictEqual(
+            cfg2, {
+                'is-googler': True,
+                'countdown': 9,
+                'version': reclient_metrics.VERSION,
+            })
+        run_mock.assert_called_once()
+
+  def test_check_status(self):
+    with tempfile.TemporaryDirectory() as tmpdir:
+      reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
+      with unittest.mock.patch('subprocess.run') as run_mock:
+        run_mock.return_value.returncode = 0
+        run_mock.return_value.stdout = 'Logged in as abc@google.com.'
+        with unittest.mock.patch('sys.stdout',
+                                 new=io.StringIO()) as stdout_mock:
+          self.assertFalse(reclient_metrics.check_status("outdir"))
+          self.assertIn("Your reclient metrics will", stdout_mock.getvalue())
+          self.assertIn(
+              os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
+              stdout_mock.getvalue())
+        run_mock.assert_called_once()
+
+    with tempfile.TemporaryDirectory() as tmpdir:
+      reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
+      with unittest.mock.patch('subprocess.run') as run_mock:
+        run_mock.return_value.returncode = 0
+        run_mock.return_value.stdout = 'Logged in as abc@google.com.'
+        for i in range(10):
+          with unittest.mock.patch('sys.stdout',
+                                   new=io.StringIO()) as stdout_mock:
+            self.assertFalse(reclient_metrics.check_status("outdir"))
+            self.assertIn("Your reclient metrics will", stdout_mock.getvalue())
+            self.assertIn(
+                os.path.join("outdir", ".reproxy_tmp", "logs",
+                             "rbe_metrics.txt"), stdout_mock.getvalue())
+            self.assertIn("you run autoninja another %d time(s)" % (10 - i),
+                          stdout_mock.getvalue())
+        with unittest.mock.patch('sys.stdout',
+                                 new=io.StringIO()) as stdout_mock:
+          self.assertTrue(reclient_metrics.check_status("outdir"))
+          self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
+          self.assertNotIn(
+              os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
+              stdout_mock.getvalue())
+        run_mock.assert_called_once()
+
+    with tempfile.TemporaryDirectory() as tmpdir:
+      reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
+      with unittest.mock.patch('subprocess.run') as run_mock:
+        run_mock.return_value.returncode = 0
+        run_mock.return_value.stdout = 'Logged in as abc@example.com.'
+        with unittest.mock.patch('sys.stdout',
+                                 new=io.StringIO()) as stdout_mock:
+          self.assertFalse(reclient_metrics.check_status("outdir"))
+          self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
+          self.assertNotIn(
+              os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
+              stdout_mock.getvalue())
+        run_mock.assert_called_once()
+
+    with tempfile.TemporaryDirectory() as tmpdir:
+      reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
+      with unittest.mock.patch('subprocess.run') as run_mock:
+        run_mock.return_value.returncode = 1
+        run_mock.return_value.stdout = ''
+        with unittest.mock.patch('sys.stdout',
+                                 new=io.StringIO()) as stdout_mock:
+          self.assertFalse(reclient_metrics.check_status("outdir"))
+          self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
+          self.assertNotIn(
+              os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
+              stdout_mock.getvalue())
+        run_mock.assert_called_once()
+
+    with tempfile.TemporaryDirectory() as tmpdir:
+      reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
+      with unittest.mock.patch('subprocess.run') as run_mock:
+        run_mock.return_value.returncode = 0
+        run_mock.return_value.stdout = 'Logged in as abc@google.com.'
+        reclient_metrics.main(["reclient_metrics.py", "opt-in"])
+        with unittest.mock.patch('sys.stdout',
+                                 new=io.StringIO()) as stdout_mock:
+          self.assertTrue(reclient_metrics.check_status("outdir"))
+          self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
+          self.assertNotIn(
+              os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
+              stdout_mock.getvalue())
+        run_mock.assert_called_once()
+
+    with tempfile.TemporaryDirectory() as tmpdir:
+      reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
+      with unittest.mock.patch('subprocess.run') as run_mock:
+        run_mock.return_value.returncode = 0
+        run_mock.return_value.stdout = 'Logged in as abc@google.com.'
+        for i in range(3):
+          with unittest.mock.patch('sys.stdout',
+                                   new=io.StringIO()) as stdout_mock:
+            self.assertFalse(reclient_metrics.check_status("outdir"))
+            self.assertIn("Your reclient metrics will", stdout_mock.getvalue())
+            self.assertIn(
+                os.path.join("outdir", ".reproxy_tmp", "logs",
+                             "rbe_metrics.txt"), stdout_mock.getvalue())
+            self.assertIn("you run autoninja another %d time(s)" % (10 - i),
+                          stdout_mock.getvalue())
+        reclient_metrics.main(["reclient_metrics.py", "opt-in"])
+        with unittest.mock.patch('sys.stdout',
+                                 new=io.StringIO()) as stdout_mock:
+          self.assertTrue(reclient_metrics.check_status("outdir"))
+          self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
+          self.assertNotIn(
+              os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
+              stdout_mock.getvalue())
+        run_mock.assert_called_once()
+
+    with tempfile.TemporaryDirectory() as tmpdir:
+      reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
+      with unittest.mock.patch('subprocess.run') as run_mock:
+        run_mock.return_value.returncode = 0
+        run_mock.return_value.stdout = 'Logged in as abc@example.com.'
+        with unittest.mock.patch('sys.stdout',
+                                 new=io.StringIO()) as stdout_mock:
+          self.assertFalse(reclient_metrics.check_status("outdir"))
+          self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
+          self.assertNotIn(
+              os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
+              stdout_mock.getvalue())
+        reclient_metrics.main(["reclient_metrics.py", "opt-in"])
+        with unittest.mock.patch('sys.stdout',
+                                 new=io.StringIO()) as stdout_mock:
+          self.assertFalse(reclient_metrics.check_status("outdir"))
+          self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
+          self.assertNotIn(
+              os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
+              stdout_mock.getvalue())
+        run_mock.assert_called_once()
+
+    with tempfile.TemporaryDirectory() as tmpdir:
+      reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
+      with unittest.mock.patch('subprocess.run') as run_mock:
+        run_mock.return_value.returncode = 0
+        run_mock.return_value.stdout = 'Logged in as abc@google.com.'
+        for i in range(3):
+          with unittest.mock.patch('sys.stdout',
+                                   new=io.StringIO()) as stdout_mock:
+            self.assertFalse(reclient_metrics.check_status("outdir"))
+            self.assertIn("Your reclient metrics will", stdout_mock.getvalue())
+            self.assertIn(
+                os.path.join("outdir", ".reproxy_tmp", "logs",
+                             "rbe_metrics.txt"), stdout_mock.getvalue())
+            self.assertIn("you run autoninja another %d time(s)" % (10 - i),
+                          stdout_mock.getvalue())
+        reclient_metrics.main(["reclient_metrics.py", "opt-out"])
+        with unittest.mock.patch('sys.stdout',
+                                 new=io.StringIO()) as stdout_mock:
+          self.assertFalse(reclient_metrics.check_status("outdir"))
+          self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
+          self.assertNotIn(
+              os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
+              stdout_mock.getvalue())
+        run_mock.assert_called_once()
+
+
+if __name__ == '__main__':
+  unittest.main()