Skip to content

Commit 67c14a9

Browse files
authored
Force unit tests to run before deployments (#4575)
Followup for the analyze/triage chrome incident
1 parent 8c3f0f2 commit 67c14a9

File tree

3 files changed

+50
-18
lines changed

3 files changed

+50
-18
lines changed

src/local/butler/deploy.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from local.butler import common
3030
from local.butler import constants
3131
from local.butler import package
32+
from local.butler import py_unittest
3233
from src.clusterfuzz._internal.base import utils
3334
from src.clusterfuzz._internal.config import local_config
3435
from src.clusterfuzz._internal.system import environment
@@ -457,6 +458,14 @@ def _is_safe_deploy_day():
457458
return day_now_in_ny not in {4, 5, 6} # The days of the week are 0-indexed.
458459

459460

461+
def _enforce_tests_pass():
462+
config = local_config.Config()
463+
if not config.get('project.enforce_tests_before_deploy', False):
464+
return
465+
py_unittest.run_tests(target='core', parallel=True)
466+
py_unittest.run_tests(target='appengine', parallel=True)
467+
468+
460469
def _enforce_safe_day_to_deploy():
461470
"""Checks that is not an unsafe day (Friday, Saturday, or Sunday) to
462471
deploy for chrome ClusterFuzz."""
@@ -521,6 +530,7 @@ def execute(args):
521530
print('gsutil not found in PATH.')
522531
sys.exit(1)
523532

533+
_enforce_tests_pass()
524534
_enforce_safe_day_to_deploy()
525535

526536
# Build templates before deployment.

src/local/butler/package.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from local.butler import appengine
2222
from local.butler import common
2323
from local.butler import constants
24+
from local.butler import py_unittest
2425
from src.clusterfuzz._internal.base import utils
2526

2627
MIN_SUPPORTED_NODEJS_VERSION = 4
@@ -83,6 +84,8 @@ def package(revision,
8384
print('You do not have nodejs, or your nodejs is not at least version 4.')
8485
sys.exit(1)
8586

87+
py_unittest.execute(args={})
88+
8689
common.install_dependencies(platform_name=platform_name)
8790

8891
# This needs to be done before packaging step to let src/appengine/config be

src/local/butler/py_unittest.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(self, *args, **kwargs):
4545
self.slow_tests = []
4646

4747
def startTest(self, test):
48-
self._start_time = time.time()
48+
self._start_time = time.time() # pylint: disable=attribute-defined-outside-init
4949
super().startTest(test)
5050

5151
def addSuccess(self, test):
@@ -126,25 +126,27 @@ def run_one_test_parallel(args):
126126
raise
127127

128128

129-
def run_tests_single_core(args, test_directory, top_level_dir):
129+
def run_tests_single_core(pattern, unsuppress_output, test_directory,
130+
top_level_dir):
130131
"""Run tests (single CPU)."""
131132
suites = unittest.loader.TestLoader().discover(
132-
test_directory, pattern=args.pattern, top_level_dir=top_level_dir)
133+
test_directory, pattern=pattern, top_level_dir=top_level_dir)
133134

134135
# TODO(mbarbella): Re-implement code coverage after migrating to Python 3.
135136
# Verbosity=2 since we want to see real-time test execution with test name
136137
# and result.
137138
result = TrackedTestRunner(
138-
verbosity=2, buffer=not args.unsuppress_output).run(suites)
139+
verbosity=2, buffer=not unsuppress_output).run(suites)
139140

140141
if result.errors or result.failures:
141142
sys.exit(1)
142143

143144

144-
def run_tests_parallel(args, test_directory, top_level_dir):
145+
def run_tests_parallel(pattern, unsuppress_output, test_directory,
146+
top_level_dir):
145147
"""Run tests (multiple CPUs)."""
146148
suites = unittest.loader.TestLoader().discover(
147-
test_directory, pattern=args.pattern, top_level_dir=top_level_dir)
149+
test_directory, pattern=pattern, top_level_dir=top_level_dir)
148150

149151
test_classes = [] # pylint: disable=protected-access
150152
for suite in suites:
@@ -181,7 +183,7 @@ def run_tests_parallel(args, test_directory, top_level_dir):
181183
tests_per_cpu = max(1, len(test_modules) // cpu_count)
182184
for i in range(0, len(test_modules), tests_per_cpu):
183185
group = test_modules[i:i + tests_per_cpu]
184-
test_args.append((group, not args.unsuppress_output))
186+
test_args.append((group, not unsuppress_output))
185187

186188
results = pool.map_async(run_one_test_parallel, test_args)
187189

@@ -215,7 +217,12 @@ def run_tests_parallel(args, test_directory, top_level_dir):
215217
sys.exit(1)
216218

217219

218-
def execute(args):
220+
def run_tests(target=None,
221+
config_dir=None,
222+
verbose=None,
223+
pattern=None,
224+
unsuppress_output=None,
225+
parallel=None):
219226
"""Run Python unit tests. For unittests involved appengine, sys.path needs
220227
certain modification."""
221228
os.environ['PY_UNITTESTS'] = 'True'
@@ -235,7 +242,7 @@ def execute(args):
235242
os.environ['CONFIG_DIR_OVERRIDE'] = os.path.join('.', 'configs', 'test')
236243

237244
top_level_dir = os.path.join('src', 'clusterfuzz', '_internal')
238-
if args.target == 'appengine':
245+
if target == 'appengine':
239246
# Build template files.
240247
appengine.build_templates()
241248

@@ -248,12 +255,12 @@ def execute(args):
248255
sys.path[i] = os.path.abspath(
249256
os.path.join('src', 'appengine', 'third_party'))
250257

251-
elif args.target == 'core':
258+
elif target == 'core':
252259
test_directory = CORE_TEST_DIRECTORY
253260
else:
254261
# Config module tests.
255-
os.environ['CONFIG_DIR_OVERRIDE'] = args.config_dir
256-
test_directory = os.path.join(args.config_dir, 'modules')
262+
os.environ['CONFIG_DIR_OVERRIDE'] = config_dir
263+
test_directory = os.path.join(config_dir, 'modules')
257264
top_level_dir = None
258265

259266
# Modules may use libs from our App Engine directory.
@@ -269,17 +276,29 @@ def execute(args):
269276
# Needed for NDB to work with cloud datastore emulator.
270277
os.environ['DATASTORE_USE_PROJECT_ID_AS_APP_ID'] = 'true'
271278

272-
if args.verbose:
279+
if verbose:
273280
# Force logging to console for this process and child processes.
274281
os.environ['LOG_TO_CONSOLE'] = 'True'
275282
else:
276283
# Disable logging.
277284
logging.disable(logging.CRITICAL)
278285

279-
if args.pattern is None:
280-
args.pattern = '*_test.py'
286+
if pattern is None:
287+
pattern = '*_test.py'
281288

282-
if args.parallel:
283-
run_tests_parallel(args, test_directory, top_level_dir)
289+
if parallel:
290+
run_tests_parallel(pattern, unsuppress_output, test_directory,
291+
top_level_dir)
284292
else:
285-
run_tests_single_core(args, test_directory, top_level_dir)
293+
run_tests_single_core(pattern, unsuppress_output, test_directory,
294+
top_level_dir)
295+
296+
297+
def execute(args):
298+
run_tests(
299+
target=args.target,
300+
config_dir=args.config_dir,
301+
verbose=args.verbose,
302+
pattern=args.pattern,
303+
unsuppress_output=args.unsuppress_output,
304+
parallel=args.parallel)

0 commit comments

Comments
 (0)