From 165bb2e839b3dc0b30a75e66c46e19cdea69e2bc Mon Sep 17 00:00:00 2001
From: Matt Reichhoff <mreichhoff@chromium.org>
Date: Tue, 16 Nov 2021 19:10:34 +0000
Subject: [PATCH] Update ios/build/bots/scripts to python3

Bug: 1262335
Change-Id: Ie3b32c64ef7369ecd915ae751197e230af0ee275
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3273475
Reviewed-by: Dirk Pranke <dpranke@google.com>
Reviewed-by: Zhaoyang Li <zhaoyangli@chromium.org>
Commit-Queue: Matt Reichhoff <mreichhoff@chromium.org>
Cr-Commit-Position: refs/heads/main@{#942247}
---
 ios/build/bots/scripts/PRESUBMIT.py           |  8 ++++-
 ios/build/bots/scripts/gtest_utils.py         |  4 +--
 ios/build/bots/scripts/iossim_util.py         | 19 +++++++-----
 ios/build/bots/scripts/iossim_util_test.py    |  4 +--
 ios/build/bots/scripts/result_sink_util.py    | 10 +++++-
 .../bots/scripts/result_sink_util_test.py     |  7 +++--
 ios/build/bots/scripts/run_test.py            |  1 +
 ios/build/bots/scripts/shard_util.py          | 19 ++++++++----
 ios/build/bots/scripts/shard_util_test.py     | 31 ++++++++++---------
 ios/build/bots/scripts/test_apps.py           |  4 +--
 ios/build/bots/scripts/test_result_util.py    |  4 +--
 .../bots/scripts/test_result_util_test.py     |  5 ++-
 ios/build/bots/scripts/test_runner.py         | 29 ++++++++++-------
 ios/build/bots/scripts/test_runner_test.py    |  6 ++--
 ios/build/bots/scripts/wpr_runner.py          |  3 +-
 ios/build/bots/scripts/wpr_runner_test.py     | 20 ++++++------
 ios/build/bots/scripts/xcode_log_parser.py    |  6 ++--
 .../bots/scripts/xcode_log_parser_test.py     |  6 ++--
 ios/build/bots/scripts/xcode_util.py          | 16 ++++++----
 ios/build/bots/scripts/xcode_util_test.py     |  8 ++---
 .../bots/scripts/xcodebuild_runner_test.py    |  4 +--
 21 files changed, 128 insertions(+), 86 deletions(-)

diff --git a/ios/build/bots/scripts/PRESUBMIT.py b/ios/build/bots/scripts/PRESUBMIT.py
index a03ba1eb0b94a..488b9c8da804e 100644
--- a/ios/build/bots/scripts/PRESUBMIT.py
+++ b/ios/build/bots/scripts/PRESUBMIT.py
@@ -15,7 +15,13 @@ def _RunTestRunnerUnitTests(input_api, output_api):
   files = ['.*_test.py$']
 
   return input_api.canned_checks.RunUnitTestsInDirectory(
-      input_api, output_api, '.', files_to_check=files)
+      input_api,
+      output_api,
+      '.',
+      files_to_check=files,
+      run_on_python2=not USE_PYTHON3,
+      run_on_python3=USE_PYTHON3,
+      skip_shebang_check=True)
 
 
 def CheckChange(input_api, output_api):
diff --git a/ios/build/bots/scripts/gtest_utils.py b/ios/build/bots/scripts/gtest_utils.py
index 65ced8daf6bed..c96ac7ee8d9cf 100644
--- a/ios/build/bots/scripts/gtest_utils.py
+++ b/ios/build/bots/scripts/gtest_utils.py
@@ -123,7 +123,7 @@ class GTestResult(object):
       self._crashed = True
 
     # At most one test can crash the entire app in a given parsing.
-    for test, log_lines in self._failed_tests.iteritems():
+    for test, log_lines in self._failed_tests.items():
       # A test with no output would have crashed. No output is replaced
       # by the GTestLogParser by a sentence indicating non-completion.
       if 'Did not complete.' in log_lines:
@@ -131,7 +131,7 @@ class GTestResult(object):
         self._crashed_test = test
 
     # A test marked as flaky may also have crashed the app.
-    for test, log_lines in self._flaked_tests.iteritems():
+    for test, log_lines in self._flaked_tests.items():
       if 'Did not complete.' in log_lines:
         self._crashed = True
         self._crashed_test = test
diff --git a/ios/build/bots/scripts/iossim_util.py b/ios/build/bots/scripts/iossim_util.py
index e049e0a7a8895..c0b895e1ee8ab 100644
--- a/ios/build/bots/scripts/iossim_util.py
+++ b/ios/build/bots/scripts/iossim_util.py
@@ -18,7 +18,9 @@ def _compose_simulator_name(platform, version):
 
 def get_simulator_list():
   """Gets list of available simulator as a dictionary."""
-  return json.loads(subprocess.check_output(['xcrun', 'simctl', 'list', '-j']))
+  return json.loads(
+      subprocess.check_output(['xcrun', 'simctl', 'list',
+                               '-j']).decode('utf-8'))
 
 
 def get_simulator(platform, version):
@@ -127,7 +129,8 @@ def create_device_by_platform_and_version(platform, version):
   runtime = get_simulator_runtime_by_version(simulators, version)
   try:
     udid = subprocess.check_output(
-        ['xcrun', 'simctl', 'create', name, device_type, runtime]).rstrip()
+        ['xcrun', 'simctl', 'create', name, device_type,
+         runtime]).decode('utf-8').rstrip()
     LOGGER.info('Created simulator in first attempt with UDID: %s', udid)
     # Sometimes above command fails to create a simulator. Verify it and retry
     # once if first attempt failed.
@@ -135,7 +138,8 @@ def create_device_by_platform_and_version(platform, version):
       # Try to delete once to avoid duplicate in case of race condition.
       delete_simulator_by_udid(udid)
       udid = subprocess.check_output(
-          ['xcrun', 'simctl', 'create', name, device_type, runtime]).rstrip()
+          ['xcrun', 'simctl', 'create', name, device_type,
+           runtime]).decode('utf-8').rstrip()
       LOGGER.info('Created simulator in second attempt with UDID: %s', udid)
     return udid
   except subprocess.CalledProcessError as e:
@@ -152,7 +156,7 @@ def delete_simulator_by_udid(udid):
   LOGGER.info('Deleting simulator %s', udid)
   try:
     subprocess.check_output(['xcrun', 'simctl', 'delete', udid],
-                            stderr=subprocess.STDOUT)
+                            stderr=subprocess.STDOUT).decode('utf-8')
   except subprocess.CalledProcessError as e:
     # Logging error instead of throwing so we don't cause failures in case
     # this was indeed failing to clean up.
@@ -188,7 +192,7 @@ def get_home_directory(platform, version):
   """
   return subprocess.check_output(
       ['xcrun', 'simctl', 'getenv',
-       get_simulator(platform, version), 'HOME']).rstrip()
+       get_simulator(platform, version), 'HOME']).decode('utf-8').rstrip()
 
 
 def boot_simulator_if_not_booted(sim_udid):
@@ -207,7 +211,8 @@ def boot_simulator_if_not_booted(sim_udid):
         continue
       if device['state'] == 'Booted':
         return
-      subprocess.check_output(['xcrun', 'simctl', 'boot', sim_udid])
+      subprocess.check_output(['xcrun', 'simctl', 'boot',
+                               sim_udid]).decode('utf-8')
       return
   raise test_runner.SimulatorNotFoundError(
       'Not found simulator with "%s" UDID in devices %s' %
@@ -223,7 +228,7 @@ def get_app_data_directory(app_bundle_id, sim_udid):
   """
   return subprocess.check_output(
       ['xcrun', 'simctl', 'get_app_container', sim_udid, app_bundle_id,
-       'data']).rstrip()
+       'data']).decode('utf-8').rstrip()
 
 
 def is_device_with_udid_simulator(device_udid):
diff --git a/ios/build/bots/scripts/iossim_util_test.py b/ios/build/bots/scripts/iossim_util_test.py
index b64d3072bdc94..e358020a24bfc 100755
--- a/ios/build/bots/scripts/iossim_util_test.py
+++ b/ios/build/bots/scripts/iossim_util_test.py
@@ -166,7 +166,7 @@ class GetiOSSimUtil(test_runner_test.TestCase):
   @mock.patch('subprocess.check_output', autospec=True)
   def test_create_device_by_platform_and_version(self, subprocess_mock, _):
     """Ensures that command is correct."""
-    subprocess_mock.return_value = 'NEW_UDID'
+    subprocess_mock.return_value = b'NEW_UDID'
     self.assertEqual(
         'NEW_UDID',
         iossim_util.create_device_by_platform_and_version(
@@ -195,7 +195,7 @@ class GetiOSSimUtil(test_runner_test.TestCase):
   @mock.patch('subprocess.check_output', autospec=True)
   def test_get_home_directory(self, subprocess_mock, _):
     """Ensures that command is correct."""
-    subprocess_mock.return_value = 'HOME_DIRECTORY'
+    subprocess_mock.return_value = b'HOME_DIRECTORY'
     self.assertEqual('HOME_DIRECTORY',
                      iossim_util.get_home_directory('iPhone 11', '13.2.2'))
     self.assertEqual([
diff --git a/ios/build/bots/scripts/result_sink_util.py b/ios/build/bots/scripts/result_sink_util.py
index 3a8a85e19d265..f7c30c5105662 100644
--- a/ios/build/bots/scripts/result_sink_util.py
+++ b/ios/build/bots/scripts/result_sink_util.py
@@ -9,6 +9,7 @@ import json
 import logging
 import os
 import requests
+import sys
 
 LOGGER = logging.getLogger(__name__)
 # VALID_STATUSES is a list of valid status values for test_result['status'].
@@ -71,10 +72,17 @@ def _compose_test_result(test_id,
       } for name in file_artifacts
   }
   if test_log:
+    message = ''
+    if sys.version_info.major < 3:
+      message = base64.b64encode(test_log)
+    else:
+      # Python3 b64encode takes and returns bytes. The result must be
+      # serializable in order for the eventual json.dumps to succeed
+      message = base64.b64encode(test_log.encode('utf-8')).decode('utf-8')
     test_result['summaryHtml'] = '<text-artifact artifact-id="Test Log" />'
     test_result['artifacts'].update({
         'Test Log': {
-            'contents': base64.b64encode(test_log)
+            'contents': message
         },
     })
   if not test_result['artifacts']:
diff --git a/ios/build/bots/scripts/result_sink_util_test.py b/ios/build/bots/scripts/result_sink_util_test.py
index 3155899fcbea3..cb4ba7c13f154 100755
--- a/ios/build/bots/scripts/result_sink_util_test.py
+++ b/ios/build/bots/scripts/result_sink_util_test.py
@@ -62,7 +62,8 @@ class UnitTest(unittest.TestCase):
         'summaryHtml': '<text-artifact artifact-id="Test Log" />',
         'artifacts': {
             'Test Log': {
-                'contents': base64.b64encode(short_log)
+                'contents':
+                    base64.b64encode(short_log.encode('utf-8')).decode('utf-8')
             },
             'name': {
                 'filePath': '/path/to/name'
@@ -89,7 +90,9 @@ class UnitTest(unittest.TestCase):
         'summaryHtml': '<text-artifact artifact-id="Test Log" />',
         'artifacts': {
             'Test Log': {
-                'contents': base64.b64encode(len_4128_str)
+                'contents':
+                    base64.b64encode(len_4128_str.encode('utf-8')
+                                    ).decode('utf-8')
             },
         },
         'tags': [],
diff --git a/ios/build/bots/scripts/run_test.py b/ios/build/bots/scripts/run_test.py
index 2f7a831417889..9e0eed204281e 100755
--- a/ios/build/bots/scripts/run_test.py
+++ b/ios/build/bots/scripts/run_test.py
@@ -283,6 +283,7 @@ class RunnerInstallXcodeTest(test_runner_test.TestCase):
     self.runner.args.xcode_build_version = 'testXcodeVersion'
     self.runner.args.runtime_cache_prefix = 'test/runtime-ios-'
     self.runner.args.version = '14.4'
+    self.runner.args.out_dir = 'out/dir'
 
   @mock.patch('test_runner.defaults_delete')
   @mock.patch('json.dump')
diff --git a/ios/build/bots/scripts/shard_util.py b/ios/build/bots/scripts/shard_util.py
index 659d4f1f2d330..9ccc3bc489f90 100644
--- a/ios/build/bots/scripts/shard_util.py
+++ b/ios/build/bots/scripts/shard_util.py
@@ -7,6 +7,7 @@ import logging
 import os
 import re
 import subprocess
+import sys
 
 import test_runner_errors
 
@@ -119,7 +120,7 @@ def fetch_test_names_for_release(stdout):
       //build/scripts/slave/recipe_modules/ios/api.py
 
     Args:
-        stdout: (string) response of 'otool -ov'
+        stdout: (bytes) response of 'otool -ov'
 
     Returns:
         (list) a list of (TestCase, testMethod), containing disabled tests.
@@ -130,6 +131,10 @@ def fetch_test_names_for_release(stdout):
   # 1. Parse test class names.
   # 2. If they are not in ignored list, parse test method names.
   # 3. Calculate test count per test class.
+  # |stdout| will be bytes on python3, and therefore must be decoded prior
+  # to running a regex.
+  if sys.version_info.major == 3:
+    stdout = stdout.decode('utf-8')
   res = re.split(TEST_CLASS_RELEASE_APP_PATTERN, stdout)
   # Ignore 1st element in split since it does not have any test class data
   test_classes_output = res[1:]
@@ -163,16 +168,18 @@ def fetch_test_names_for_debug(stdout):
      format of (TestCase, testMethod) including disabled tests, in debug app.
 
     Args:
-        stdout: (string) response of 'otool -ov'
+        stdout: (bytes) response of 'otool -ov'
 
     Returns:
         (list) a list of (TestCase, testMethod), containing disabled tests.
     """
-  test_names = TEST_NAMES_DEBUG_APP_PATTERN.findall(stdout.decode('utf-8'))
+  # |stdout| will be bytes on python3, and therefore must be decoded prior
+  # to running a regex.
+  if sys.version_info.major == 3:
+    stdout = stdout.decode('utf-8')
+  test_names = TEST_NAMES_DEBUG_APP_PATTERN.findall(stdout)
   test_names = list(
-      map(
-          lambda test_name: (test_name[0].encode('utf-8'), test_name[1].encode(
-              'utf-8')), test_names))
+      map(lambda test_name: (test_name[0], test_name[1]), test_names))
   return list(
       filter(lambda test_name: test_name[0] not in IGNORED_CLASSES, test_names))
 
diff --git a/ios/build/bots/scripts/shard_util_test.py b/ios/build/bots/scripts/shard_util_test.py
index c4757f3946c3f..24bd53464246d 100755
--- a/ios/build/bots/scripts/shard_util_test.py
+++ b/ios/build/bots/scripts/shard_util_test.py
@@ -27,7 +27,7 @@ DEBUG_APP_OTOOL_OUTPUT = '\n'.join([
     'imp 0x1075e6887 -[ToolBarTestCase testH]',
     'imp 0x1075e6887 -[ToolBarTestCase DISABLED_testI]',
     'imp 0x1075e6887 -[ToolBarTestCase FLAKY_testJ]', 'version 0'
-])
+]).encode('utf-8')
 
 # Debug app otool output format in Xcode 11.4 toolchain.
 DEBUG_APP_OTOOL_OUTPUT_114 = '\n'.join([
@@ -49,7 +49,7 @@ DEBUG_APP_OTOOL_OUTPUT_114 = '\n'.join([
     '    imp     0x1075e6887 -[ToolBarTestCase testH]',
     '    imp     0x1075e6887 -[ToolBarTestCase DISABLED_testI]',
     '    imp     0x1075e6887 -[ToolBarTestCase FLAKY_testJ]', 'version 0'
-])
+]).encode('utf-8')
 
 RELEASE_APP_OTOOL_OUTPUT = '\n'.join([
     'Meta Class', 'name 0x1064b8438 CacheTestCase',
@@ -73,7 +73,7 @@ RELEASE_APP_OTOOL_OUTPUT = '\n'.join([
     'name 0x1075e6887 testG', 'name 0x1075e6887 testH',
     'name 0x1075e6887 DISABLED_testI', 'name 0x1075e6887 FLAKY_testJ',
     'name 0x1064b8438 ToolBarTestCase', 'baseProtocols 0x0', 'version 0'
-])
+]).encode('utf-8')
 
 RELEASE_APP_OTOOL_OUTPUT_CLASS_NOT_IN_PAIRS = '\n'.join([
     'Meta Class', 'name 0x1064b8438 CacheTestCase',
@@ -96,7 +96,7 @@ RELEASE_APP_OTOOL_OUTPUT_CLASS_NOT_IN_PAIRS = '\n'.join([
     'name 0x1075e6887 testG', 'name 0x1075e6887 testH',
     'name 0x1075e6887 DISABLED_testI', 'name 0x1075e6887 FLAKY_testJ',
     'name 0x1064b8438 ToolBarTestCase', 'baseProtocols 0x0', 'version 0'
-])
+]).encode('utf-8')
 
 # Release app otool output format in Xcode 11.4 toolchain.
 RELEASE_APP_OTOOL_OUTPUT_114 = '\n'.join([
@@ -123,7 +123,7 @@ RELEASE_APP_OTOOL_OUTPUT_114 = '\n'.join([
     '    name    0x1075e6887 DISABLED_testI',
     '    name    0x1075e6887 FLAKY_testJ',
     '    name    0x1064b8438 ToolBarTestCase', 'baseProtocols 0x0', 'version 0'
-])
+]).encode('utf-8')
 
 
 class TestShardUtil(unittest.TestCase):
@@ -179,13 +179,14 @@ class TestShardUtil(unittest.TestCase):
     for test_name in expected_test_names:
       self.assertTrue(test_name in resp)
 
-    test_cases = map(lambda (test_case, test_method): test_case, resp)
-
+    test_cases = [test_case for (test_case, _) in resp]
     # ({'CacheTestCase': 3, 'TabUITestCase': 2, 'PasswordsTestCase': 1,
     # 'KeyboardTestCase': 1, 'ToolBarTestCase': 3})
     counts = collections.Counter(test_cases).most_common()
     name, _ = counts[0]
-    self.assertEqual(name, 'ToolBarTestCase')
+    # CacheTestCase and ToolBarTestCase each have 3 entries.
+    # In case of ties, most_common() returns the first encountered at index 0.
+    self.assertEqual(name, 'CacheTestCase')
 
   def test_fetch_test_counts_release(self):
     """Ensures that the release output is formatted correctly"""
@@ -207,7 +208,7 @@ class TestShardUtil(unittest.TestCase):
     for test_name in expected_test_names:
       self.assertTrue(test_name in resp)
 
-    test_cases = map(lambda (test_case, test_method): test_case, resp)
+    test_cases = [test_case for (test_case, _) in resp]
     # ({'KeyboardTest': 3, 'CacheTestCase': 3,
     # 'ToolBarTestCase': 4})
     counts = collections.Counter(test_cases).most_common()
@@ -243,13 +244,15 @@ class TestShardUtil(unittest.TestCase):
     for test_name in expected_test_names:
       self.assertTrue(test_name in resp)
 
-    test_cases = map(lambda (test_case, test_method): test_case, resp)
+    test_cases = [test_case for (test_case, _) in resp]
 
     # ({'CacheTestCase': 3, 'TabUITestCase': 2, 'PasswordsTestCase': 1,
     # 'KeyboardTestCase': 1, 'ToolBarTestCase': 3})
     counts = collections.Counter(test_cases).most_common()
     name, _ = counts[0]
-    self.assertEqual(name, 'ToolBarTestCase')
+    # CacheTestCase and ToolBarTestCase each have 3 entries.
+    # In case of ties, most_common() returns the first encountered at index 0.
+    self.assertEqual(name, 'CacheTestCase')
 
   def test_fetch_test_counts_release_114(self):
     """Test the release output from otool in Xcode 11.4"""
@@ -271,7 +274,7 @@ class TestShardUtil(unittest.TestCase):
     for test_name in expected_test_names:
       self.assertTrue(test_name in resp)
 
-    test_cases = map(lambda (test_case, test_method): test_case, resp)
+    test_cases = [test_case for (test_case, _) in resp]
     # ({'KeyboardTest': 3, 'CacheTestCase': 3,
     # 'ToolBarTestCase': 4})
     counts = collections.Counter(test_cases).most_common()
@@ -281,7 +284,7 @@ class TestShardUtil(unittest.TestCase):
   def test_balance_into_sublists_debug(self):
     """Ensure the balancing algorithm works"""
     resp = shard_util.fetch_test_names_for_debug(DEBUG_APP_OTOOL_OUTPUT)
-    test_cases = map(lambda (test_case, test_method): test_case, resp)
+    test_cases = [test_case for (test_case, _) in resp]
     test_counts = collections.Counter(test_cases)
 
     sublists_1 = shard_util.balance_into_sublists(test_counts, 1)
@@ -304,7 +307,7 @@ class TestShardUtil(unittest.TestCase):
   def test_balance_into_sublists_release(self):
     """Ensure the balancing algorithm works"""
     resp = shard_util.fetch_test_names_for_release(RELEASE_APP_OTOOL_OUTPUT)
-    test_cases = map(lambda (test_case, test_method): test_case, resp)
+    test_cases = [test_case for (test_case, _) in resp]
     test_counts = collections.Counter(test_cases)
 
     sublists_3 = shard_util.balance_into_sublists(test_counts, 3)
diff --git a/ios/build/bots/scripts/test_apps.py b/ios/build/bots/scripts/test_apps.py
index ee82f29addeef..08c95b6015dc2 100644
--- a/ios/build/bots/scripts/test_apps.py
+++ b/ios/build/bots/scripts/test_apps.py
@@ -49,7 +49,7 @@ def get_bundle_id(app_path):
       '-c',
       'Print:CFBundleIdentifier',
       os.path.join(app_path, 'Info.plist'),
-  ]).rstrip().decode("utf-8")
+  ]).decode("utf-8").rstrip()
 
 
 def is_running_rosetta():
@@ -61,7 +61,7 @@ def is_running_rosetta():
     running on an Intel machine.
   """
   translated = subprocess.check_output(
-      ['sysctl', '-i', '-b', 'sysctl.proc_translated'])
+      ['sysctl', '-i', '-b', 'sysctl.proc_translated']).decode('utf-8')
   # "sysctl -b" is expected to return a 4-byte integer response. 1 means the
   # current process is running under Rosetta, 0 means it is not. On x86_64
   # machines, this variable does not exist at all, so "-i" is used to return a
diff --git a/ios/build/bots/scripts/test_result_util.py b/ios/build/bots/scripts/test_result_util.py
index 7f61ee31658f6..a16cfc813eab8 100644
--- a/ios/build/bots/scripts/test_result_util.py
+++ b/ios/build/bots/scripts/test_result_util.py
@@ -400,9 +400,9 @@ class ResultCollection(object):
       logs['flaked tests'] = flaked
     if failed:
       logs['failed tests'] = failed
-    for test, log_lines in failed.iteritems():
+    for test, log_lines in failed.items():
       logs[test] = log_lines
-    for test, log_lines in flaked.iteritems():
+    for test, log_lines in flaked.items():
       logs[test] = log_lines
 
     return logs
diff --git a/ios/build/bots/scripts/test_result_util_test.py b/ios/build/bots/scripts/test_result_util_test.py
index ff23d9e6a45ae..a3f057077ec68 100755
--- a/ios/build/bots/scripts/test_result_util_test.py
+++ b/ios/build/bots/scripts/test_result_util_test.py
@@ -39,13 +39,12 @@ class UtilTest(test_runner_test.TestCase):
     """Tests _validate_kwargs."""
     with self.assertRaises(AssertionError) as context:
       TestResult('name', TestStatus.PASS, unknown='foo')
-    expected_message = (
-        'Invalid keyword argument(s) in set([\'unknown\']) passed in!')
+    expected_message = ("Invalid keyword argument(s) in {'unknown'} passed in!")
     self.assertTrue(expected_message in str(context.exception))
     with self.assertRaises(AssertionError) as context:
       ResultCollection(test_log='foo')
     expected_message = (
-        'Invalid keyword argument(s) in set([\'test_log\']) passed in!')
+        "Invalid keyword argument(s) in {'test_log'} passed in!")
     self.assertTrue(expected_message in str(context.exception))
 
   def test_validate_test_status(self):
diff --git a/ios/build/bots/scripts/test_runner.py b/ios/build/bots/scripts/test_runner.py
index 183c71b0c9a7c..9efb5038b4f5b 100644
--- a/ios/build/bots/scripts/test_runner.py
+++ b/ios/build/bots/scripts/test_runner.py
@@ -148,9 +148,9 @@ def get_device_ios_version(udid):
   Returns:
     Device UDID.
   """
-  return subprocess.check_output(['ideviceinfo',
-                                  '--udid', udid,
-                                  '-k', 'ProductVersion']).strip()
+  return subprocess.check_output(
+      ['ideviceinfo', '--udid', udid, '-k',
+       'ProductVersion']).decode('utf-8').strip()
 
 
 def defaults_write(d, key, value):
@@ -254,6 +254,10 @@ def print_process_output(proc,
       timer.cancel()
     if not line:
       break
+    # |line| will be bytes on python3, and therefore must be decoded prior
+    # to rstrip.
+    if sys.version_info.major == 3:
+      line = line.decode('utf-8')
     line = line.rstrip()
     out.append(line)
     if parser:
@@ -263,9 +267,6 @@ def print_process_output(proc,
 
   if parser:
     parser.Finalize()
-  if sys.version_info.major == 3:
-    for index in range(len(out)):
-      out[index] = out[index].decode('utf-8')
   LOGGER.debug('Finished print_process_output.')
   return out
 
@@ -282,7 +283,8 @@ def get_current_xcode_info():
   try:
     out = subprocess.check_output(['xcodebuild', '-version']).splitlines()
     version, build_version = out[0].split(' ')[-1], out[1].split(' ')[-1]
-    path = subprocess.check_output(['xcode-select', '--print-path']).rstrip()
+    path = subprocess.check_output(['xcode-select',
+                                    '--print-path']).decode('utf-8').rstrip()
   except subprocess.CalledProcessError:
     version = build_version = path = None
 
@@ -368,7 +370,8 @@ class TestRunner(object):
     """removes any proxy settings which may remain from a previous run."""
     LOGGER.info('Removing any proxy settings.')
     network_services = subprocess.check_output(
-        ['networksetup', '-listallnetworkservices']).strip().split('\n')
+        ['networksetup',
+         '-listallnetworkservices']).decode('utf-8').strip().split('\n')
     if len(network_services) > 1:
       # We ignore the first line as it is a description of the command's output.
       network_services = network_services[1:]
@@ -610,7 +613,7 @@ class TestRunner(object):
       if self.retries and never_expected_tests:
         LOGGER.warning('%s tests failed and will be retried.\n',
                        len(never_expected_tests))
-        for i in xrange(self.retries):
+        for i in range(self.retries):
           tests_to_retry = list(overall_result.never_expected_tests())
           for test in tests_to_retry:
             LOGGER.info('Retry #%s for %s.\n', i + 1, test)
@@ -761,9 +764,10 @@ class SimulatorTestRunner(TestRunner):
         if os.path.exists(docs_dir) and os.path.exists(metadata_plist):
           cfbundleid = subprocess.check_output([
               '/usr/libexec/PlistBuddy',
-              '-c', 'Print:MCMMetadataIdentifier',
+              '-c',
+              'Print:MCMMetadataIdentifier',
               metadata_plist,
-          ]).rstrip()
+          ]).decode('utf-8').rstrip()
           if cfbundleid == self.cfbundleid:
             shutil.copytree(docs_dir, os.path.join(self.out_dir, 'Documents'))
             return
@@ -917,7 +921,8 @@ class DeviceTestRunner(TestRunner):
     """
     super(DeviceTestRunner, self).__init__(app_path, out_dir, **kwargs)
 
-    self.udid = subprocess.check_output(['idevice_id', '--list']).rstrip()
+    self.udid = subprocess.check_output(['idevice_id',
+                                         '--list']).decode('utf-8').rstrip()
     if len(self.udid.splitlines()) != 1:
       raise DeviceDetectionError(self.udid)
 
diff --git a/ios/build/bots/scripts/test_runner_test.py b/ios/build/bots/scripts/test_runner_test.py
index aa96e55fa22d0..3fde2c5fe899f 100755
--- a/ios/build/bots/scripts/test_runner_test.py
+++ b/ios/build/bots/scripts/test_runner_test.py
@@ -47,7 +47,7 @@ class TestCase(unittest.TestCase):
     super(TestCase, self).tearDown(*args, **kwargs)
 
     for obj in self._mocks:
-      for member, original_value in self._mocks[obj].iteritems():
+      for member, original_value in self._mocks[obj].items():
         setattr(obj, member, original_value)
 
 
@@ -281,8 +281,8 @@ class DeviceTestRunnerTest(TestCase):
     self.mock(test_runner, 'get_current_xcode_info', lambda: {
         'version': 'test version', 'build': 'test build', 'path': 'test/path'})
     self.mock(test_runner, 'install_xcode', install_xcode)
-    self.mock(test_runner.subprocess, 'check_output',
-              lambda _: 'fake-bundle-id')
+    self.mock(test_runner.subprocess,
+              'check_output', lambda _: b'fake-bundle-id')
     self.mock(os.path, 'abspath', lambda path: '/abs/path/to/%s' % path)
     self.mock(os.path, 'exists', lambda _: True)
     self.mock(os, 'listdir', lambda _: [])
diff --git a/ios/build/bots/scripts/wpr_runner.py b/ios/build/bots/scripts/wpr_runner.py
index 06bb30aaf1383..ad6a20c569102 100644
--- a/ios/build/bots/scripts/wpr_runner.py
+++ b/ios/build/bots/scripts/wpr_runner.py
@@ -380,7 +380,8 @@ class WprProxySimulatorTestRunner(test_runner.SimulatorTestRunner):
     # We route all network adapters through the proxy, since it is easier than
     # determining which network adapter is being used currently.
     network_services = subprocess.check_output(
-        ['networksetup', '-listallnetworkservices']).strip().split('\n')
+        ['networksetup',
+         '-listallnetworkservices']).decode('utf-8').strip().split('\n')
     if len(network_services) > 1:
       # We ignore the first line as it is a description of the command's output.
       network_services = network_services[1:]
diff --git a/ios/build/bots/scripts/wpr_runner_test.py b/ios/build/bots/scripts/wpr_runner_test.py
index f38758ec7192b..b66e74eae33f7 100755
--- a/ios/build/bots/scripts/wpr_runner_test.py
+++ b/ios/build/bots/scripts/wpr_runner_test.py
@@ -24,8 +24,8 @@ class WprProxySimulatorTestRunnerTest(test_runner_test.TestCase):
 
     self.mock(test_runner, 'get_current_xcode_info', lambda: {
         'version': 'test version', 'build': 'test build', 'path': 'test/path'})
-    self.mock(test_runner.subprocess, 'check_output',
-              lambda _: 'fake-bundle-id')
+    self.mock(test_runner.subprocess,
+              'check_output', lambda _: b'fake-bundle-id')
     self.mock(os.path, 'abspath', lambda path: '/abs/path/to/%s' % path)
     self.mock(os.path, 'exists', lambda _: True)
     self.mock(test_runner.TestRunner, 'set_sigterm_handler',
@@ -113,14 +113,14 @@ class WprProxySimulatorTestRunnerTest(test_runner_test.TestCase):
       def __init__(self):
         self.line_index = 0
         self.lines = [
-            'Test Case \'-[a 1]\' started.',
-            'Test Case \'-[a 1]\' has uninteresting logs.',
-            'Test Case \'-[a 1]\' passed (0.1 seconds)',
-            'Test Case \'-[b 2]\' started.',
-            'Test Case \'-[b 2]\' passed (0.1 seconds)',
-            'Test Case \'-[c 3]\' started.',
-            'Test Case \'-[c 3]\' has interesting failure info.',
-            'Test Case \'-[c 3]\' failed (0.1 seconds)',
+            b'Test Case \'-[a 1]\' started.',
+            b'Test Case \'-[a 1]\' has uninteresting logs.',
+            b'Test Case \'-[a 1]\' passed (0.1 seconds)',
+            b'Test Case \'-[b 2]\' started.',
+            b'Test Case \'-[b 2]\' passed (0.1 seconds)',
+            b'Test Case \'-[c 3]\' started.',
+            b'Test Case \'-[c 3]\' has interesting failure info.',
+            b'Test Case \'-[c 3]\' failed (0.1 seconds)',
         ]
 
       def readline(self):
diff --git a/ios/build/bots/scripts/xcode_log_parser.py b/ios/build/bots/scripts/xcode_log_parser.py
index 66fe7774c008c..d757891e0b462 100644
--- a/ios/build/bots/scripts/xcode_log_parser.py
+++ b/ios/build/bots/scripts/xcode_log_parser.py
@@ -169,7 +169,7 @@ class Xcode11LogParser(object):
     id_params = ['--id', ref_id] if ref_id else []
     xcresult_command = ['xcresulttool', 'get', '--format', 'json',
                         '--path', xcresult_path] + id_params
-    return subprocess.check_output(xcresult_command).strip()
+    return subprocess.check_output(xcresult_command).decode('utf-8').strip()
 
   @staticmethod
   def _list_of_failed_tests(actions_invocation_record, excluded=None):
@@ -388,7 +388,7 @@ class Xcode11LogParser(object):
                       test['identifier']
                       ['_value']] = test['summaryRef']['id']['_value']
 
-    for test, summary_ref_id in test_summary_refs.iteritems():
+    for test, summary_ref_id in test_summary_refs.items():
       # See SINGLE_TEST_SUMMARY_REF in xcode_log_parser_test.py for an example
       # of |test_summary|.
       test_summary = json.loads(
@@ -457,7 +457,7 @@ class Xcode11LogParser(object):
         'xcresulttool', 'export', '--type', output_type, '--id', ref_id,
         '--path', xcresult, '--output-path', output_path
     ]
-    subprocess.check_output(export_command).strip()
+    subprocess.check_output(export_command).decode('utf-8').strip()
 
   @staticmethod
   def _extract_attachments(test,
diff --git a/ios/build/bots/scripts/xcode_log_parser_test.py b/ios/build/bots/scripts/xcode_log_parser_test.py
index 7f7eb407f6d0f..fd298cb48ef52 100755
--- a/ios/build/bots/scripts/xcode_log_parser_test.py
+++ b/ios/build/bots/scripts/xcode_log_parser_test.py
@@ -85,7 +85,7 @@ XCRESULT_ROOT = """
   }
 }"""
 
-REF_ID = """
+REF_ID = b"""
   {
     "actions": {
       "_values": [{
@@ -435,7 +435,7 @@ class XCode11LogParserTest(test_runner_test.TestCase):
 
   @mock.patch('subprocess.check_output', autospec=True)
   def testXcresulttoolGetRoot(self, mock_process):
-    mock_process.return_value = '%JSON%'
+    mock_process.return_value = b'%JSON%'
     xcode_log_parser.Xcode11LogParser()._xcresulttool_get('xcresult_path')
     self.assertTrue(
         os.path.join(XCODE11_DICT['path'], 'usr', 'bin') in os.environ['PATH'])
@@ -445,7 +445,7 @@ class XCode11LogParserTest(test_runner_test.TestCase):
 
   @mock.patch('subprocess.check_output', autospec=True)
   def testXcresulttoolGetRef(self, mock_process):
-    mock_process.side_effect = [REF_ID, 'JSON']
+    mock_process.side_effect = [REF_ID, b'JSON']
     xcode_log_parser.Xcode11LogParser()._xcresulttool_get('xcresult_path',
                                                           'testsRef')
     self.assertEqual(
diff --git a/ios/build/bots/scripts/xcode_util.py b/ios/build/bots/scripts/xcode_util.py
index 008df06391199..41e60300f17b0 100644
--- a/ios/build/bots/scripts/xcode_util.py
+++ b/ios/build/bots/scripts/xcode_util.py
@@ -32,7 +32,8 @@ def _using_new_mac_toolchain(mac_toolchain):
       mac_toolchain,
       'help',
   ]
-  output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+  output = subprocess.check_output(
+      cmd, stderr=subprocess.STDOUT).decode('utf-8')
 
   # "install-runtime" presents as a command line switch in help output in the
   # new mac_toolchain.
@@ -181,12 +182,14 @@ def select(xcode_app_path):
       xcode_app_path,
   ]
   LOGGER.debug('Selecting XCode with command %s and "xcrun simctl list".' % cmd)
-  output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+  output = subprocess.check_output(
+      cmd, stderr=subprocess.STDOUT).decode('utf-8')
 
   # This is to avoid issues caused by mixed usage of different Xcode versions on
   # one machine.
   xcrun_simctl_cmd = ['xcrun', 'simctl', 'list']
-  output += subprocess.check_output(xcrun_simctl_cmd, stderr=subprocess.STDOUT)
+  output += subprocess.check_output(
+      xcrun_simctl_cmd, stderr=subprocess.STDOUT).decode('utf-8')
 
   return output
 
@@ -316,15 +319,16 @@ def version():
   ]
   LOGGER.debug('Checking XCode version with command: %s' % cmd)
 
-  output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+  output = subprocess.check_output(
+      cmd, stderr=subprocess.STDOUT).decode('utf-8')
   output = output.splitlines()
   # output sample:
   # Xcode 12.0
   # Build version 12A6159
   LOGGER.info(output)
 
-  version = output[0].decode('UTF-8').split(' ')[1]
-  build_version = output[1].decode('UTF-8').split(' ')[2].lower()
+  version = output[0].split(' ')[1]
+  build_version = output[1].split(' ')[2].lower()
 
   return version, build_version
 
diff --git a/ios/build/bots/scripts/xcode_util_test.py b/ios/build/bots/scripts/xcode_util_test.py
index a11541900ae8c..043c29ccbe555 100755
--- a/ios/build/bots/scripts/xcode_util_test.py
+++ b/ios/build/bots/scripts/xcode_util_test.py
@@ -14,10 +14,10 @@ import test_runner_test
 import xcode_util
 
 
-_XCODEBUILD_VERSION_OUTPUT_12 = """Xcode 12.4
+_XCODEBUILD_VERSION_OUTPUT_12 = b"""Xcode 12.4
 Build version 12D4e
 """
-_XCODEBUILD_VERSION_OUTPUT_13 = """Xcode 13.0
+_XCODEBUILD_VERSION_OUTPUT_13 = b"""Xcode 13.0
 Build version 13A5155e
 """
 
@@ -189,7 +189,7 @@ class HelperFunctionTests(XcodeUtilTest):
 
   @mock.patch('subprocess.check_output', autospec=True)
   def test_using_new_mac_toolchain(self, mock_check_output):
-    mock_check_output.return_value = """
+    mock_check_output.return_value = b"""
 Mac OS / iOS toolchain management
 
 Usage:  mac_toolchain [command] [arguments]
@@ -207,7 +207,7 @@ Use "mac_toolchain help [command]" for more information about a command."""
 
   @mock.patch('subprocess.check_output', autospec=True)
   def test_using_new_legacy_toolchain(self, mock_check_output):
-    mock_check_output.return_value = """
+    mock_check_output.return_value = b"""
 Mac OS / iOS toolchain management
 
 Usage:  mac_toolchain [command] [arguments]
diff --git a/ios/build/bots/scripts/xcodebuild_runner_test.py b/ios/build/bots/scripts/xcodebuild_runner_test.py
index 67b5b61d623bf..38779460dfdc0 100755
--- a/ios/build/bots/scripts/xcodebuild_runner_test.py
+++ b/ios/build/bots/scripts/xcodebuild_runner_test.py
@@ -141,8 +141,8 @@ class DeviceXcodeTestRunnerTest(test_runner_test.TestCase):
 
     self.mock(result_sink_util.ResultSinkClient,
               'post', lambda *args, **kwargs: None)
-    self.mock(test_runner.subprocess, 'check_output', lambda _: 'fake-output')
-    self.mock(test_runner.subprocess, 'check_call', lambda _: 'fake-out')
+    self.mock(test_runner.subprocess, 'check_output', lambda _: b'fake-output')
+    self.mock(test_runner.subprocess, 'check_call', lambda _: b'fake-out')
     self.mock(test_runner.subprocess,
               'Popen', lambda cmd, env, stdout, stderr: 'fake-out')
     self.mock(test_runner.TestRunner, 'set_sigterm_handler',