run_tests.py cleanup and simplification (#28808)

* only support cmake for CLanguage in run_tests.py

* add support for run_tests.py build step environ

* switch C/C++ run_tests.py build to build_cxx script

* CLanguage cleanup

* build C# entirely with build_csharp script

* move entire PHP build to build_php.sh

* fixup C# build on linux and mac

* run_dep_checks Makefile target is deprecated

* get rid of the "makefile" logic in run_tests.py

* fixup C# build on linux and mac

* XML_REPORT env variable is useless for --use_docker runs

* add a TODO

* move "main" functionality towards end of run_tests.py

* use self.args instead of global

* yapf format

* remove the no longer useful --update_submodules features of run_tests.py

* fix check_epollexclusive check in run_tests.py
diff --git a/tools/run_tests/helper_scripts/build_csharp.bat b/tools/run_tests/helper_scripts/build_csharp.bat
index 7721d80..6972ef0 100644
--- a/tools/run_tests/helper_scripts/build_csharp.bat
+++ b/tools/run_tests/helper_scripts/build_csharp.bat
@@ -14,7 +14,19 @@
 
 setlocal
 
-cd /d %~dp0\..\..\..\src\csharp
+cd /d %~dp0\..\..\..
+
+mkdir cmake
+cd cmake
+mkdir build
+cd build
+mkdir %ARCHITECTURE%
+cd %ARCHITECTURE%
+
+cmake -G "Visual Studio 14 2015" -A %ARCHITECTURE% -DgRPC_BUILD_TESTS=OFF -DgRPC_MSVC_STATIC_RUNTIME=ON  -DgRPC_XDS_USER_AGENT_IS_CSHARP=ON -DgRPC_BUILD_MSVC_MP_COUNT=%GRPC_RUN_TESTS_JOBS% ../../.. || goto :error
+cmake --build . --target grpc_csharp_ext --config %MSBUILD_CONFIG% || goto :error
+
+cd ..\..\..\src\csharp
 
 dotnet build --configuration %MSBUILD_CONFIG% Grpc.sln || goto :error
 
diff --git a/tools/run_tests/helper_scripts/build_csharp.sh b/tools/run_tests/helper_scripts/build_csharp.sh
index c6bee82..3e0a2fc 100755
--- a/tools/run_tests/helper_scripts/build_csharp.sh
+++ b/tools/run_tests/helper_scripts/build_csharp.sh
@@ -15,7 +15,16 @@
 
 set -ex
 
-cd "$(dirname "$0")/../../../src/csharp"
+cd "$(dirname "$0")/../../.."
+
+mkdir -p cmake/build
+pushd cmake/build
+
+cmake -DgRPC_BUILD_TESTS=OFF -DCMAKE_BUILD_TYPE="${MSBUILD_CONFIG}" -DgRPC_XDS_USER_AGENT_IS_CSHARP=ON ../..
+make -j"${GRPC_RUN_TESTS_JOBS}" grpc_csharp_ext
+
+popd
+pushd src/csharp
 
 if [ "$CONFIG" == "gcov" ]
 then
diff --git a/tools/run_tests/helper_scripts/pre_build_cmake.bat b/tools/run_tests/helper_scripts/build_cxx.bat
similarity index 77%
rename from tools/run_tests/helper_scripts/pre_build_cmake.bat
rename to tools/run_tests/helper_scripts/build_cxx.bat
index aea18c9..d695dd1 100644
--- a/tools/run_tests/helper_scripts/pre_build_cmake.bat
+++ b/tools/run_tests/helper_scripts/build_cxx.bat
@@ -1,4 +1,4 @@
-@rem Copyright 2017 gRPC authors.
+@rem Copyright 2022 The gRPC Authors
 @rem
 @rem Licensed under the Apache License, Version 2.0 (the "License");
 @rem you may not use this file except in compliance with the License.
@@ -23,6 +23,9 @@
 
 cmake -DgRPC_BUILD_TESTS=ON %* ../.. || goto :error
 
+@rem GRPC_RUN_TESTS_CXX_LANGUAGE_SUFFIX will be set to either "c" or "cxx"
+cmake --build . --target buildtests_%GRPC_RUN_TESTS_CXX_LANGUAGE_SUFFIX% --config %MSBUILD_CONFIG% || goto :error
+
 endlocal
 
 goto :EOF
diff --git a/tools/run_tests/helper_scripts/pre_build_cmake.sh b/tools/run_tests/helper_scripts/build_cxx.sh
similarity index 74%
rename from tools/run_tests/helper_scripts/pre_build_cmake.sh
rename to tools/run_tests/helper_scripts/build_cxx.sh
index 62eaf1f..af47959 100755
--- a/tools/run_tests/helper_scripts/pre_build_cmake.sh
+++ b/tools/run_tests/helper_scripts/build_cxx.sh
@@ -1,5 +1,5 @@
 #!/bin/bash
-# Copyright 2015 gRPC authors.
+# Copyright 2022 The gRPC Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -22,3 +22,6 @@
 
 # MSBUILD_CONFIG's values are suitable for cmake as well
 cmake -DgRPC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE="${MSBUILD_CONFIG}" "$@" ../..
+
+# GRPC_RUN_TESTS_CXX_LANGUAGE_SUFFIX will be set to either "c" or "cxx"
+make -j"${GRPC_RUN_TESTS_JOBS}" "buildtests_${GRPC_RUN_TESTS_CXX_LANGUAGE_SUFFIX}" "tools_${GRPC_RUN_TESTS_CXX_LANGUAGE_SUFFIX}" "check_epollexclusive"
diff --git a/tools/run_tests/helper_scripts/build_php.sh b/tools/run_tests/helper_scripts/build_php.sh
index 4add036..782a6c1 100755
--- a/tools/run_tests/helper_scripts/build_php.sh
+++ b/tools/run_tests/helper_scripts/build_php.sh
@@ -20,7 +20,10 @@
 # change to grpc repo root
 cd "$(dirname "$0")/../../.."
 
-root=$(pwd)
+# build C core first
+make -j"${GRPC_RUN_TESTS_JOBS}" EMBED_OPENSSL=true EMBED_ZLIB=true static_c shared_c
+
+repo_root="$(pwd)"
 export GRPC_LIB_SUBDIR=libs/$CONFIG
 export CFLAGS="-Wno-parentheses-equality"
 
@@ -30,8 +33,8 @@
 cd ext/grpc
 phpize
 if [ "$CONFIG" != "gcov" ] ; then
-  ./configure --enable-grpc="$root" --enable-tests
+  ./configure --enable-grpc="${repo_root}" --enable-tests
 else
-  ./configure --enable-grpc="$root" --enable-coverage --enable-tests
+  ./configure --enable-grpc="${repo_root}" --enable-coverage --enable-tests
 fi
-make
+make -j"${GRPC_RUN_TESTS_JOBS}"
diff --git a/tools/run_tests/helper_scripts/pre_build_csharp.bat b/tools/run_tests/helper_scripts/pre_build_csharp.bat
index 886fee5..d0b0ab4 100644
--- a/tools/run_tests/helper_scripts/pre_build_csharp.bat
+++ b/tools/run_tests/helper_scripts/pre_build_csharp.bat
@@ -16,21 +16,10 @@
 
 setlocal
 
-set ARCHITECTURE=%1
-
 @rem enter repo root
 cd /d %~dp0\..\..\..
 
-mkdir cmake
-cd cmake
-mkdir build
-cd build
-mkdir %ARCHITECTURE%
-cd %ARCHITECTURE%
-
-cmake -G "Visual Studio 14 2015" -A %ARCHITECTURE% -DgRPC_BUILD_TESTS=OFF -DgRPC_MSVC_STATIC_RUNTIME=ON  -DgRPC_XDS_USER_AGENT_IS_CSHARP=ON -DgRPC_BUILD_MSVC_MP_COUNT=4 ../../.. || goto :error
-
-cd ..\..\..\src\csharp
+cd src\csharp
 
 dotnet restore Grpc.sln || goto :error
 
diff --git a/tools/run_tests/helper_scripts/pre_build_csharp.sh b/tools/run_tests/helper_scripts/pre_build_csharp.sh
index e334d7e..eb98db2 100755
--- a/tools/run_tests/helper_scripts/pre_build_csharp.sh
+++ b/tools/run_tests/helper_scripts/pre_build_csharp.sh
@@ -18,11 +18,6 @@
 # cd to repository root
 cd "$(dirname "$0")/../../.."
 
-mkdir -p cmake/build
-cd cmake/build
-
-cmake -DgRPC_BUILD_TESTS=OFF -DCMAKE_BUILD_TYPE="${MSBUILD_CONFIG}" -DgRPC_XDS_USER_AGENT_IS_CSHARP=ON ../..
-
-cd ../../src/csharp
+cd src/csharp
 
 dotnet restore Grpc.sln
diff --git a/tools/run_tests/run_tests.py b/tools/run_tests/run_tests.py
index ebb8900..98a53a3 100755
--- a/tools/run_tests/run_tests.py
+++ b/tools/run_tests/run_tests.py
@@ -240,16 +240,14 @@
 
 class CLanguage(object):
 
-    def __init__(self, make_target, test_lang):
-        self.make_target = make_target
+    def __init__(self, lang_suffix, test_lang):
+        self.lang_suffix = lang_suffix
         self.platform = platform_string()
         self.test_lang = test_lang
 
     def configure(self, config, args):
         self.config = config
         self.args = args
-        self._make_options = []
-        self._use_cmake = True
         if self.platform == 'windows':
             _check_compiler(self.args.compiler, [
                 'default', 'cmake', 'cmake_vs2015', 'cmake_vs2017',
@@ -286,7 +284,7 @@
         out = []
         binaries = get_c_tests(self.args.travis, self.test_lang)
         for target in binaries:
-            if self._use_cmake and target.get('boringssl', False):
+            if target.get('boringssl', False):
                 # cmake doesn't build boringssl tests
                 continue
             auto_timeout_scaling = target.get('auto_timeout_scaling', True)
@@ -327,11 +325,8 @@
                     binary = 'cmake/build/%s/%s.exe' % (_MSBUILD_CONFIG[
                         self.config.build_config], target['name'])
                 else:
-                    if self._use_cmake:
-                        binary = 'cmake/build/%s' % target['name']
-                    else:
-                        binary = 'bins/%s/%s' % (self.config.build_config,
-                                                 target['name'])
+                    binary = 'cmake/build/%s' % target['name']
+
                 cpu_cost = target['cpu_cost']
                 if cpu_cost == 'capacity':
                     cpu_cost = multiprocessing.cpu_count()
@@ -419,32 +414,22 @@
                     print('\nWARNING: binary not found, skipping', binary)
         return sorted(out)
 
-    def make_targets(self):
-        if self.platform == 'windows':
-            # don't build tools on windows just yet
-            return ['buildtests_%s' % self.make_target]
-        return [
-            'buildtests_%s' % self.make_target,
-            'tools_%s' % self.make_target, 'check_epollexclusive'
-        ]
-
-    def make_options(self):
-        return self._make_options
-
     def pre_build_steps(self):
-        if self.platform == 'windows':
-            return [[
-                'tools\\run_tests\\helper_scripts\\pre_build_cmake.bat',
-                '-DgRPC_BUILD_MSVC_MP_COUNT=%d' % args.jobs
-            ] + self._cmake_configure_extra_args]
-        elif self._use_cmake:
-            return [['tools/run_tests/helper_scripts/pre_build_cmake.sh'] +
-                    self._cmake_configure_extra_args]
-        else:
-            return []
+        return []
 
     def build_steps(self):
-        return []
+        if self.platform == 'windows':
+            return [[
+                'tools\\run_tests\\helper_scripts\\build_cxx.bat',
+                '-DgRPC_BUILD_MSVC_MP_COUNT=%d' % self.args.jobs
+            ] + self._cmake_configure_extra_args]
+        else:
+            return [['tools/run_tests/helper_scripts/build_cxx.sh'] +
+                    self._cmake_configure_extra_args]
+
+    def build_steps_environ(self):
+        """Extra environment variables set for pre_build_steps and build_steps jobs."""
+        return {'GRPC_RUN_TESTS_CXX_LANGUAGE_SUFFIX': self.lang_suffix}
 
     def post_tests_steps(self):
         if self.platform == 'windows':
@@ -452,12 +437,6 @@
         else:
             return [['tools/run_tests/helper_scripts/post_tests_c.sh']]
 
-    def makefile_name(self):
-        if self._use_cmake:
-            return 'cmake/build/Makefile'
-        else:
-            return 'Makefile'
-
     def _clang_cmake_configure_extra_args(self, version_suffix=''):
         return [
             '-DCMAKE_C_COMPILER=clang%s' % version_suffix,
@@ -497,7 +476,7 @@
             self._docker_distro, _docker_arch_suffix(self.args.arch))
 
     def __str__(self):
-        return self.make_target
+        return self.lang_suffix
 
 
 # This tests Node on grpc/grpc-node and will become the standard for Node testing
@@ -545,21 +524,16 @@
     def pre_build_steps(self):
         return []
 
-    def make_targets(self):
-        return []
-
-    def make_options(self):
-        return []
-
     def build_steps(self):
         return []
 
+    def build_steps_environ(self):
+        """Extra environment variables set for pre_build_steps and build_steps jobs."""
+        return {}
+
     def post_tests_steps(self):
         return []
 
-    def makefile_name(self):
-        return 'Makefile'
-
     def dockerfile_dir(self):
         return 'tools/dockerfile/test/node_jessie_%s' % _docker_arch_suffix(
             self.args.arch)
@@ -574,7 +548,6 @@
         self.config = config
         self.args = args
         _check_compiler(self.args.compiler, ['default'])
-        self._make_options = ['EMBED_OPENSSL=true', 'EMBED_ZLIB=true']
 
     def test_specs(self):
         return [
@@ -585,21 +558,16 @@
     def pre_build_steps(self):
         return []
 
-    def make_targets(self):
-        return ['static_c', 'shared_c']
-
-    def make_options(self):
-        return self._make_options
-
     def build_steps(self):
         return [['tools/run_tests/helper_scripts/build_php.sh']]
 
+    def build_steps_environ(self):
+        """Extra environment variables set for pre_build_steps and build_steps jobs."""
+        return {}
+
     def post_tests_steps(self):
         return [['tools/run_tests/helper_scripts/post_tests_php.sh']]
 
-    def makefile_name(self):
-        return 'Makefile'
-
     def dockerfile_dir(self):
         return 'tools/dockerfile/test/php7_debian11_%s' % _docker_arch_suffix(
             self.args.arch)
@@ -671,24 +639,19 @@
     def pre_build_steps(self):
         return []
 
-    def make_targets(self):
-        return []
-
-    def make_options(self):
-        return []
-
     def build_steps(self):
         return [config.build for config in self.pythons]
 
+    def build_steps_environ(self):
+        """Extra environment variables set for pre_build_steps and build_steps jobs."""
+        return {}
+
     def post_tests_steps(self):
         if self.config.build_config != 'gcov':
             return []
         else:
             return [['tools/run_tests/helper_scripts/post_tests_python.sh']]
 
-    def makefile_name(self):
-        return 'Makefile'
-
     def dockerfile_dir(self):
         return 'tools/dockerfile/test/python_%s_%s' % (
             self._python_docker_distro_name(),
@@ -858,21 +821,16 @@
     def pre_build_steps(self):
         return [['tools/run_tests/helper_scripts/pre_build_ruby.sh']]
 
-    def make_targets(self):
-        return []
-
-    def make_options(self):
-        return []
-
     def build_steps(self):
         return [['tools/run_tests/helper_scripts/build_ruby.sh']]
 
+    def build_steps_environ(self):
+        """Extra environment variables set for pre_build_steps and build_steps jobs."""
+        return {}
+
     def post_tests_steps(self):
         return [['tools/run_tests/helper_scripts/post_tests_ruby.sh']]
 
-    def makefile_name(self):
-        return 'Makefile'
-
     def dockerfile_dir(self):
         return 'tools/dockerfile/test/ruby_debian11_%s' % _docker_arch_suffix(
             self.args.arch)
@@ -944,39 +902,29 @@
 
     def pre_build_steps(self):
         if self.platform == 'windows':
-            return [[
-                'tools\\run_tests\\helper_scripts\\pre_build_csharp.bat',
-                self._cmake_arch_option
-            ]]
+            return [['tools\\run_tests\\helper_scripts\\pre_build_csharp.bat']]
         else:
             return [['tools/run_tests/helper_scripts/pre_build_csharp.sh']]
 
-    def make_targets(self):
-        return ['grpc_csharp_ext']
-
-    def make_options(self):
-        return []
-
     def build_steps(self):
         if self.platform == 'windows':
             return [['tools\\run_tests\\helper_scripts\\build_csharp.bat']]
         else:
             return [['tools/run_tests/helper_scripts/build_csharp.sh']]
 
+    def build_steps_environ(self):
+        """Extra environment variables set for pre_build_steps and build_steps jobs."""
+        if self.platform == 'windows':
+            return {'ARCHITECTURE': self._cmake_arch_option}
+        else:
+            return {}
+
     def post_tests_steps(self):
         if self.platform == 'windows':
             return [['tools\\run_tests\\helper_scripts\\post_tests_csharp.bat']]
         else:
             return [['tools/run_tests/helper_scripts/post_tests_csharp.sh']]
 
-    def makefile_name(self):
-        if self.platform == 'windows':
-            return 'cmake/build/%s/Makefile' % self._cmake_arch_option
-        else:
-            # no need to set x86 specific flags as run_tests.py
-            # currently forbids x86 C# builds on both Linux and MacOS.
-            return 'cmake/build/Makefile'
-
     def dockerfile_dir(self):
         return 'tools/dockerfile/test/csharp_%s_%s' % (
             self._docker_distro, _docker_arch_suffix(self.args.arch))
@@ -1139,21 +1087,16 @@
     def pre_build_steps(self):
         return []
 
-    def make_targets(self):
-        return []
-
-    def make_options(self):
-        return []
-
     def build_steps(self):
         return []
 
+    def build_steps_environ(self):
+        """Extra environment variables set for pre_build_steps and build_steps jobs."""
+        return {}
+
     def post_tests_steps(self):
         return []
 
-    def makefile_name(self):
-        return 'Makefile'
-
     def dockerfile_dir(self):
         return None
 
@@ -1191,21 +1134,16 @@
     def pre_build_steps(self):
         return []
 
-    def make_targets(self):
-        return ['run_dep_checks']
-
-    def make_options(self):
-        return []
-
     def build_steps(self):
         return []
 
+    def build_steps_environ(self):
+        """Extra environment variables set for pre_build_steps and build_steps jobs."""
+        return {}
+
     def post_tests_steps(self):
         return []
 
-    def makefile_name(self):
-        return 'Makefile'
-
     def dockerfile_dir(self):
         return 'tools/dockerfile/test/sanity'
 
@@ -1237,6 +1175,16 @@
 }
 
 
+def _build_step_environ(cfg, extra_env={}):
+    """Environment variables set for each build step."""
+    environ = {'CONFIG': cfg, 'GRPC_RUN_TESTS_JOBS': str(args.jobs)}
+    msbuild_cfg = _MSBUILD_CONFIG.get(cfg)
+    if msbuild_cfg:
+        environ['MSBUILD_CONFIG'] = msbuild_cfg
+    environ.update(extra_env)
+    return environ
+
+
 def _windows_arch_option(arch):
     """Returns msbuild cmdline option for selected architecture."""
     if arch == 'default' or arch == 'x86':
@@ -1319,369 +1267,8 @@
     return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
 
 
-# parse command line
-argp = argparse.ArgumentParser(description='Run grpc tests.')
-argp.add_argument('-c',
-                  '--config',
-                  choices=sorted(_CONFIGS.keys()),
-                  default='opt')
-argp.add_argument(
-    '-n',
-    '--runs_per_test',
-    default=1,
-    type=runs_per_test_type,
-    help='A positive integer or "inf". If "inf", all tests will run in an '
-    'infinite loop. Especially useful in combination with "-f"')
-argp.add_argument('-r', '--regex', default='.*', type=str)
-argp.add_argument('--regex_exclude', default='', type=str)
-argp.add_argument('-j', '--jobs', default=multiprocessing.cpu_count(), type=int)
-argp.add_argument('-s', '--slowdown', default=1.0, type=float)
-argp.add_argument('-p',
-                  '--sample_percent',
-                  default=100.0,
-                  type=percent_type,
-                  help='Run a random sample with that percentage of tests')
-argp.add_argument(
-    '-t',
-    '--travis',
-    default=False,
-    action='store_const',
-    const=True,
-    help='When set, indicates that the script is running on CI (= not locally).'
-)
-argp.add_argument('--newline_on_success',
-                  default=False,
-                  action='store_const',
-                  const=True)
-argp.add_argument('-l',
-                  '--language',
-                  choices=sorted(_LANGUAGES.keys()),
-                  nargs='+',
-                  required=True)
-argp.add_argument('-S',
-                  '--stop_on_failure',
-                  default=False,
-                  action='store_const',
-                  const=True)
-argp.add_argument('--use_docker',
-                  default=False,
-                  action='store_const',
-                  const=True,
-                  help='Run all the tests under docker. That provides ' +
-                  'additional isolation and prevents the need to install ' +
-                  'language specific prerequisites. Only available on Linux.')
-argp.add_argument(
-    '--allow_flakes',
-    default=False,
-    action='store_const',
-    const=True,
-    help=
-    'Allow flaky tests to show as passing (re-runs failed tests up to five times)'
-)
-argp.add_argument(
-    '--arch',
-    choices=['default', 'x86', 'x64'],
-    default='default',
-    help=
-    'Selects architecture to target. For some platforms "default" is the only supported choice.'
-)
-argp.add_argument(
-    '--compiler',
-    choices=[
-        'default',
-        'gcc4.9',
-        'gcc10.2',
-        'gcc10.2_openssl102',
-        'gcc11',
-        'gcc_musl',
-        'clang4',
-        'clang13',
-        'python2.7',
-        'python3.5',
-        'python3.6',
-        'python3.7',
-        'python3.8',
-        'python3.9',
-        'pypy',
-        'pypy3',
-        'python_alpine',
-        'all_the_cpythons',
-        'electron1.3',
-        'electron1.6',
-        'coreclr',
-        'cmake',
-        'cmake_vs2015',
-        'cmake_vs2017',
-        'cmake_vs2019',
-        'mono',
-    ],
-    default='default',
-    help=
-    'Selects compiler to use. Allowed values depend on the platform and language.'
-)
-argp.add_argument('--iomgr_platform',
-                  choices=['native', 'gevent', 'asyncio'],
-                  default='native',
-                  help='Selects iomgr platform to build on')
-argp.add_argument('--build_only',
-                  default=False,
-                  action='store_const',
-                  const=True,
-                  help='Perform all the build steps but don\'t run any tests.')
-argp.add_argument('--measure_cpu_costs',
-                  default=False,
-                  action='store_const',
-                  const=True,
-                  help='Measure the cpu costs of tests')
-argp.add_argument(
-    '--update_submodules',
-    default=[],
-    nargs='*',
-    help=
-    'Update some submodules before building. If any are updated, also run generate_projects. '
-    +
-    'Submodules are specified as SUBMODULE_NAME:BRANCH; if BRANCH is omitted, master is assumed.'
-)
-argp.add_argument('-a', '--antagonists', default=0, type=int)
-argp.add_argument('-x',
-                  '--xml_report',
-                  default=None,
-                  type=str,
-                  help='Generates a JUnit-compatible XML report')
-argp.add_argument('--report_suite_name',
-                  default='tests',
-                  type=str,
-                  help='Test suite name to use in generated JUnit XML report')
-argp.add_argument(
-    '--report_multi_target',
-    default=False,
-    const=True,
-    action='store_const',
-    help='Generate separate XML report for each test job (Looks better in UIs).'
-)
-argp.add_argument(
-    '--quiet_success',
-    default=False,
-    action='store_const',
-    const=True,
-    help=
-    'Don\'t print anything when a test passes. Passing tests also will not be reported in XML report. '
-    + 'Useful when running many iterations of each test (argument -n).')
-argp.add_argument(
-    '--force_default_poller',
-    default=False,
-    action='store_const',
-    const=True,
-    help='Don\'t try to iterate over many polling strategies when they exist')
-argp.add_argument(
-    '--force_use_pollers',
-    default=None,
-    type=str,
-    help='Only use the specified comma-delimited list of polling engines. '
-    'Example: --force_use_pollers epoll1,poll '
-    ' (This flag has no effect if --force_default_poller flag is also used)')
-argp.add_argument('--max_time',
-                  default=-1,
-                  type=int,
-                  help='Maximum test runtime in seconds')
-argp.add_argument('--bq_result_table',
-                  default='',
-                  type=str,
-                  nargs='?',
-                  help='Upload test results to a specified BQ table.')
-args = argp.parse_args()
-
-flaky_tests = set()
-shortname_to_cpu = {}
-
-if args.force_default_poller:
-    _POLLING_STRATEGIES = {}
-elif args.force_use_pollers:
-    _POLLING_STRATEGIES[platform_string()] = args.force_use_pollers.split(',')
-
-jobset.measure_cpu_costs = args.measure_cpu_costs
-
-# update submodules if necessary
-need_to_regenerate_projects = False
-for spec in args.update_submodules:
-    spec = spec.split(':', 1)
-    if len(spec) == 1:
-        submodule = spec[0]
-        branch = 'master'
-    elif len(spec) == 2:
-        submodule = spec[0]
-        branch = spec[1]
-    cwd = 'third_party/%s' % submodule
-
-    def git(cmd, cwd=cwd):
-        print('in %s: git %s' % (cwd, cmd))
-        run_shell_command('git %s' % cmd, cwd=cwd)
-
-    git('fetch')
-    git('checkout %s' % branch)
-    git('pull origin %s' % branch)
-    if os.path.exists('src/%s/gen_build_yaml.py' % submodule):
-        need_to_regenerate_projects = True
-if need_to_regenerate_projects:
-    if jobset.platform_string() == 'linux':
-        run_shell_command('tools/buildgen/generate_projects.sh')
-    else:
-        print(
-            'WARNING: may need to regenerate projects, but since we are not on')
-        print(
-            '         Linux this step is being skipped. Compilation MAY fail.')
-
-# grab config
-run_config = _CONFIGS[args.config]
-build_config = run_config.build_config
-
-if args.travis:
-    _FORCE_ENVIRON_FOR_WRAPPERS = {'GRPC_TRACE': 'api'}
-
-languages = set(_LANGUAGES[l] for l in args.language)
-for l in languages:
-    l.configure(run_config, args)
-
-language_make_options = []
-if any(language.make_options() for language in languages):
-    if not 'gcov' in args.config and len(languages) != 1:
-        print(
-            'languages with custom make options cannot be built simultaneously with other languages'
-        )
-        sys.exit(1)
-    else:
-        # Combining make options is not clean and just happens to work. It allows C & C++ to build
-        # together, and is only used under gcov. All other configs should build languages individually.
-        language_make_options = list(
-            set([
-                make_option for lang in languages
-                for make_option in lang.make_options()
-            ]))
-
-if args.use_docker:
-    if not args.travis:
-        print('Seen --use_docker flag, will run tests under docker.')
-        print('')
-        print(
-            'IMPORTANT: The changes you are testing need to be locally committed'
-        )
-        print(
-            'because only the committed changes in the current branch will be')
-        print('copied to the docker environment.')
-        time.sleep(5)
-
-    dockerfile_dirs = set([l.dockerfile_dir() for l in languages])
-    if len(dockerfile_dirs) > 1:
-        print('Languages to be tested require running under different docker '
-              'images.')
-        sys.exit(1)
-    else:
-        dockerfile_dir = next(iter(dockerfile_dirs))
-
-    child_argv = [arg for arg in sys.argv if not arg == '--use_docker']
-    run_tests_cmd = 'python3 tools/run_tests/run_tests.py %s' % ' '.join(
-        child_argv[1:])
-
-    env = os.environ.copy()
-    env['DOCKERFILE_DIR'] = dockerfile_dir
-    env['DOCKER_RUN_SCRIPT'] = 'tools/run_tests/dockerize/docker_run.sh'
-    env['DOCKER_RUN_SCRIPT_COMMAND'] = run_tests_cmd
-    # TODO(jtattermusch): is the XML_REPORT env variable any useful?
-    if args.xml_report:
-        env['XML_REPORT'] = args.xml_report
-
-    retcode = subprocess.call(
-        'tools/run_tests/dockerize/build_and_run_docker.sh',
-        shell=True,
-        env=env)
-    _print_debug_info_epilogue(dockerfile_dir=dockerfile_dir)
-    sys.exit(retcode)
-
-_check_arch_option(args.arch)
-
-
-def make_jobspec(cfg, targets, makefile='Makefile'):
-    if platform_string() == 'windows':
-        return [
-            jobset.JobSpec([
-                'cmake', '--build', '.', '--target',
-                '%s' % target, '--config', _MSBUILD_CONFIG[cfg]
-            ],
-                           cwd=os.path.dirname(makefile),
-                           timeout_seconds=None) for target in targets
-        ]
-    else:
-        if targets and makefile.startswith('cmake/build/'):
-            # With cmake, we've passed all the build configuration in the pre-build step already
-            return [
-                jobset.JobSpec(
-                    [os.getenv('MAKE', 'make'), '-j',
-                     '%d' % args.jobs] + targets,
-                    cwd='cmake/build',
-                    timeout_seconds=None)
-            ]
-        if targets:
-            return [
-                jobset.JobSpec(
-                    [
-                        os.getenv('MAKE', 'make'), '-f', makefile, '-j',
-                        '%d' % args.jobs,
-                        'EXTRA_DEFINES=GRPC_TEST_SLOWDOWN_MACHINE_FACTOR=%f' %
-                        args.slowdown,
-                        'CONFIG=%s' % cfg, 'Q='
-                    ] + language_make_options +
-                    ([] if not args.travis else ['JENKINS_BUILD=1']) + targets,
-                    timeout_seconds=None)
-            ]
-        else:
-            return []
-
-
-make_targets = {}
-for l in languages:
-    makefile = l.makefile_name()
-    make_targets[makefile] = make_targets.get(makefile, set()).union(
-        set(l.make_targets()))
-
-
-def build_step_environ(cfg):
-    environ = {'CONFIG': cfg}
-    msbuild_cfg = _MSBUILD_CONFIG.get(cfg)
-    if msbuild_cfg:
-        environ['MSBUILD_CONFIG'] = msbuild_cfg
-    return environ
-
-
-build_steps = list(
-    set(
-        jobset.JobSpec(cmdline,
-                       environ=build_step_environ(build_config),
-                       timeout_seconds=_PRE_BUILD_STEP_TIMEOUT_SECONDS,
-                       flake_retries=2)
-        for l in languages
-        for cmdline in l.pre_build_steps()))
-if make_targets:
-    make_commands = itertools.chain.from_iterable(
-        make_jobspec(build_config, list(targets), makefile)
-        for (makefile, targets) in make_targets.items())
-    build_steps.extend(set(make_commands))
-build_steps.extend(
-    set(
-        jobset.JobSpec(cmdline,
-                       environ=build_step_environ(build_config),
-                       timeout_seconds=None)
-        for l in languages
-        for cmdline in l.build_steps()))
-
-post_tests_steps = list(
-    set(
-        jobset.JobSpec(cmdline, environ=build_step_environ(build_config))
-        for l in languages
-        for cmdline in l.post_tests_steps()))
-runs_per_test = args.runs_per_test
-
-
 def _shut_down_legacy_server(legacy_server_port):
+    """Shut down legacy version of port server."""
     try:
         version = int(
             urllib.request.urlopen('http://localhost:%d/version_number' %
@@ -1712,16 +1299,8 @@
     return num_runs, num_failures
 
 
-# _build_and_run results
-class BuildAndRunError(object):
-
-    BUILD = object()
-    TEST = object()
-    POST_TEST = object()
-
-
 def _has_epollexclusive():
-    binary = 'bins/%s/check_epollexclusive' % args.config
+    binary = 'cmake/build/check_epollexclusive'
     if not os.path.exists(binary):
         return False
     try:
@@ -1734,6 +1313,14 @@
         return False
 
 
+class BuildAndRunError(object):
+    """Represents error type in _build_and_run."""
+
+    BUILD = object()
+    TEST = object()
+    POST_TEST = object()
+
+
 # returns a list of things that failed (or an empty list on success)
 def _build_and_run(check_cancelled,
                    newline_on_success,
@@ -1869,6 +1456,267 @@
     return out
 
 
+# parse command line
+argp = argparse.ArgumentParser(description='Run grpc tests.')
+argp.add_argument('-c',
+                  '--config',
+                  choices=sorted(_CONFIGS.keys()),
+                  default='opt')
+argp.add_argument(
+    '-n',
+    '--runs_per_test',
+    default=1,
+    type=runs_per_test_type,
+    help='A positive integer or "inf". If "inf", all tests will run in an '
+    'infinite loop. Especially useful in combination with "-f"')
+argp.add_argument('-r', '--regex', default='.*', type=str)
+argp.add_argument('--regex_exclude', default='', type=str)
+argp.add_argument('-j', '--jobs', default=multiprocessing.cpu_count(), type=int)
+argp.add_argument('-s', '--slowdown', default=1.0, type=float)
+argp.add_argument('-p',
+                  '--sample_percent',
+                  default=100.0,
+                  type=percent_type,
+                  help='Run a random sample with that percentage of tests')
+argp.add_argument(
+    '-t',
+    '--travis',
+    default=False,
+    action='store_const',
+    const=True,
+    help='When set, indicates that the script is running on CI (= not locally).'
+)
+argp.add_argument('--newline_on_success',
+                  default=False,
+                  action='store_const',
+                  const=True)
+argp.add_argument('-l',
+                  '--language',
+                  choices=sorted(_LANGUAGES.keys()),
+                  nargs='+',
+                  required=True)
+argp.add_argument('-S',
+                  '--stop_on_failure',
+                  default=False,
+                  action='store_const',
+                  const=True)
+argp.add_argument('--use_docker',
+                  default=False,
+                  action='store_const',
+                  const=True,
+                  help='Run all the tests under docker. That provides ' +
+                  'additional isolation and prevents the need to install ' +
+                  'language specific prerequisites. Only available on Linux.')
+argp.add_argument(
+    '--allow_flakes',
+    default=False,
+    action='store_const',
+    const=True,
+    help=
+    'Allow flaky tests to show as passing (re-runs failed tests up to five times)'
+)
+argp.add_argument(
+    '--arch',
+    choices=['default', 'x86', 'x64'],
+    default='default',
+    help=
+    'Selects architecture to target. For some platforms "default" is the only supported choice.'
+)
+argp.add_argument(
+    '--compiler',
+    choices=[
+        'default',
+        'gcc4.9',
+        'gcc10.2',
+        'gcc10.2_openssl102',
+        'gcc11',
+        'gcc_musl',
+        'clang4',
+        'clang13',
+        'python2.7',
+        'python3.5',
+        'python3.6',
+        'python3.7',
+        'python3.8',
+        'python3.9',
+        'pypy',
+        'pypy3',
+        'python_alpine',
+        'all_the_cpythons',
+        'electron1.3',
+        'electron1.6',
+        'coreclr',
+        'cmake',
+        'cmake_vs2015',
+        'cmake_vs2017',
+        'cmake_vs2019',
+        'mono',
+    ],
+    default='default',
+    help=
+    'Selects compiler to use. Allowed values depend on the platform and language.'
+)
+argp.add_argument('--iomgr_platform',
+                  choices=['native', 'gevent', 'asyncio'],
+                  default='native',
+                  help='Selects iomgr platform to build on')
+argp.add_argument('--build_only',
+                  default=False,
+                  action='store_const',
+                  const=True,
+                  help='Perform all the build steps but don\'t run any tests.')
+argp.add_argument('--measure_cpu_costs',
+                  default=False,
+                  action='store_const',
+                  const=True,
+                  help='Measure the cpu costs of tests')
+argp.add_argument('-a', '--antagonists', default=0, type=int)
+argp.add_argument('-x',
+                  '--xml_report',
+                  default=None,
+                  type=str,
+                  help='Generates a JUnit-compatible XML report')
+argp.add_argument('--report_suite_name',
+                  default='tests',
+                  type=str,
+                  help='Test suite name to use in generated JUnit XML report')
+argp.add_argument(
+    '--report_multi_target',
+    default=False,
+    const=True,
+    action='store_const',
+    help='Generate separate XML report for each test job (Looks better in UIs).'
+)
+argp.add_argument(
+    '--quiet_success',
+    default=False,
+    action='store_const',
+    const=True,
+    help=
+    'Don\'t print anything when a test passes. Passing tests also will not be reported in XML report. '
+    + 'Useful when running many iterations of each test (argument -n).')
+argp.add_argument(
+    '--force_default_poller',
+    default=False,
+    action='store_const',
+    const=True,
+    help='Don\'t try to iterate over many polling strategies when they exist')
+argp.add_argument(
+    '--force_use_pollers',
+    default=None,
+    type=str,
+    help='Only use the specified comma-delimited list of polling engines. '
+    'Example: --force_use_pollers epoll1,poll '
+    ' (This flag has no effect if --force_default_poller flag is also used)')
+argp.add_argument('--max_time',
+                  default=-1,
+                  type=int,
+                  help='Maximum test runtime in seconds')
+argp.add_argument('--bq_result_table',
+                  default='',
+                  type=str,
+                  nargs='?',
+                  help='Upload test results to a specified BQ table.')
+args = argp.parse_args()
+
+flaky_tests = set()
+shortname_to_cpu = {}
+
+if args.force_default_poller:
+    _POLLING_STRATEGIES = {}
+elif args.force_use_pollers:
+    _POLLING_STRATEGIES[platform_string()] = args.force_use_pollers.split(',')
+
+jobset.measure_cpu_costs = args.measure_cpu_costs
+
+# grab config
+run_config = _CONFIGS[args.config]
+build_config = run_config.build_config
+
+# TODO(jtattermusch): is this setting applied/being used?
+if args.travis:
+    _FORCE_ENVIRON_FOR_WRAPPERS = {'GRPC_TRACE': 'api'}
+
+languages = set(_LANGUAGES[l] for l in args.language)
+for l in languages:
+    l.configure(run_config, args)
+
+if len(languages) != 1:
+    print('Building multiple languages simultaneously is not supported!')
+    sys.exit(1)
+
+# If --use_docker was used, respawn the run_tests.py script under a docker container
+# instead of continuing.
+if args.use_docker:
+    if not args.travis:
+        print('Seen --use_docker flag, will run tests under docker.')
+        print('')
+        print(
+            'IMPORTANT: The changes you are testing need to be locally committed'
+        )
+        print(
+            'because only the committed changes in the current branch will be')
+        print('copied to the docker environment.')
+        time.sleep(5)
+
+    dockerfile_dirs = set([l.dockerfile_dir() for l in languages])
+    if len(dockerfile_dirs) > 1:
+        print('Languages to be tested require running under different docker '
+              'images.')
+        sys.exit(1)
+    else:
+        dockerfile_dir = next(iter(dockerfile_dirs))
+
+    child_argv = [arg for arg in sys.argv if not arg == '--use_docker']
+    run_tests_cmd = 'python3 tools/run_tests/run_tests.py %s' % ' '.join(
+        child_argv[1:])
+
+    env = os.environ.copy()
+    env['DOCKERFILE_DIR'] = dockerfile_dir
+    env['DOCKER_RUN_SCRIPT'] = 'tools/run_tests/dockerize/docker_run.sh'
+    env['DOCKER_RUN_SCRIPT_COMMAND'] = run_tests_cmd
+
+    retcode = subprocess.call(
+        'tools/run_tests/dockerize/build_and_run_docker.sh',
+        shell=True,
+        env=env)
+    _print_debug_info_epilogue(dockerfile_dir=dockerfile_dir)
+    sys.exit(retcode)
+
+_check_arch_option(args.arch)
+
+# collect pre-build steps (which get retried if they fail, e.g. to avoid
+# flakes on downloading dependencies etc.)
+build_steps = list(
+    set(
+        jobset.JobSpec(cmdline,
+                       environ=_build_step_environ(
+                           build_config, extra_env=l.build_steps_environ()),
+                       timeout_seconds=_PRE_BUILD_STEP_TIMEOUT_SECONDS,
+                       flake_retries=2)
+        for l in languages
+        for cmdline in l.pre_build_steps()))
+
+# collect build steps
+build_steps.extend(
+    set(
+        jobset.JobSpec(cmdline,
+                       environ=_build_step_environ(
+                           build_config, extra_env=l.build_steps_environ()),
+                       timeout_seconds=None)
+        for l in languages
+        for cmdline in l.build_steps()))
+
+# collect post test steps
+post_tests_steps = list(
+    set(
+        jobset.JobSpec(cmdline,
+                       environ=_build_step_environ(
+                           build_config, extra_env=l.build_steps_environ()))
+        for l in languages
+        for cmdline in l.post_tests_steps()))
+runs_per_test = args.runs_per_test
+
 errors = _build_and_run(check_cancelled=lambda: False,
                         newline_on_success=args.newline_on_success,
                         xml_report=args.xml_report,