Skip to content

Commit c31e961

Browse files
authored
Merge pull request #3732 from Flamefire/ignore_test_failure
Add option to ignore failing test step
2 parents 76b6ab1 + 2a76373 commit c31e961

File tree

6 files changed

+81
-3
lines changed

6 files changed

+81
-3
lines changed

easybuild/base/testing.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import pprint
3636
import re
3737
import sys
38+
from contextlib import contextmanager
3839

3940
try:
4041
from cStringIO import StringIO # Python 2
@@ -185,6 +186,26 @@ def get_stderr(self):
185186
"""Return output captured from stderr until now."""
186187
return sys.stderr.getvalue()
187188

189+
@contextmanager
190+
def mocked_stdout_stderr(self, mock_stdout=True, mock_stderr=True):
191+
"""Context manager to mock stdout and stderr"""
192+
if mock_stdout:
193+
self.mock_stdout(True)
194+
if mock_stderr:
195+
self.mock_stderr(True)
196+
try:
197+
if mock_stdout and mock_stderr:
198+
yield sys.stdout, sys.stderr
199+
elif mock_stdout:
200+
yield sys.stdout
201+
else:
202+
yield sys.stderr
203+
finally:
204+
if mock_stdout:
205+
self.mock_stdout(False)
206+
if mock_stderr:
207+
self.mock_stderr(False)
208+
188209
def tearDown(self):
189210
"""Cleanup after running a test."""
190211
self.mock_stdout(False)

easybuild/framework/easyblock.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ def __init__(self, ec):
258258
if group_name is not None:
259259
self.group = use_group(group_name)
260260

261+
self.ignore_test_failure = build_option('ignore_test_failure')
262+
261263
# generate build/install directories
262264
self.gen_builddir()
263265
self.gen_installdir()
@@ -1814,6 +1816,18 @@ def remove_module_file(self):
18141816
self.log.info("Removing existing module file %s", self.mod_filepath)
18151817
remove_file(self.mod_filepath)
18161818

1819+
def report_test_failure(self, msg_or_error):
1820+
"""
1821+
Report a failing test either via an exception or warning depending on ignore-test-failure
1822+
1823+
:param msg_or_error: failure description (string value or an EasyBuildError instance)
1824+
"""
1825+
if self.ignore_test_failure:
1826+
print_warning("Test failure ignored: " + str(msg_or_error), log=self.log)
1827+
else:
1828+
exception = msg_or_error if isinstance(msg_or_error, EasyBuildError) else EasyBuildError(msg_or_error)
1829+
raise exception
1830+
18171831
#
18181832
# STEP FUNCTIONS
18191833
#
@@ -2229,6 +2243,13 @@ def test_step(self):
22292243

22302244
return out
22312245

2246+
def _test_step(self):
2247+
"""Run the test_step and handles failures"""
2248+
try:
2249+
self.test_step()
2250+
except EasyBuildError as err:
2251+
self.report_test_failure(err)
2252+
22322253
def stage_install_step(self):
22332254
"""
22342255
Install in a stage directory before actual installation.
@@ -3367,10 +3388,12 @@ def run_step(self, step, step_methods):
33673388
run_hook(step, self.hooks, pre_step_hook=True, args=[self])
33683389

33693390
for step_method in step_methods:
3370-
self.log.info("Running method %s part of step %s" % (extract_method_name(step_method), step))
3391+
# Remove leading underscore from e.g. "_test_step"
3392+
method_name = extract_method_name(step_method).lstrip('_')
3393+
self.log.info("Running method %s part of step %s", method_name, step)
33713394

33723395
if self.dry_run:
3373-
self.dry_run_msg("[%s method]", step_method(self).__name__)
3396+
self.dry_run_msg("[%s method]", method_name)
33743397

33753398
# if an known possible error occurs, just report it and continue
33763399
try:
@@ -3443,7 +3466,7 @@ def install_step_spec(initial):
34433466
prepare_step_spec = (PREPARE_STEP, 'preparing', [lambda x: x.prepare_step], False)
34443467
configure_step_spec = (CONFIGURE_STEP, 'configuring', [lambda x: x.configure_step], True)
34453468
build_step_spec = (BUILD_STEP, 'building', [lambda x: x.build_step], True)
3446-
test_step_spec = (TEST_STEP, 'testing', [lambda x: x.test_step], True)
3469+
test_step_spec = (TEST_STEP, 'testing', [lambda x: x._test_step], True)
34473470
extensions_step_spec = (EXTENSIONS_STEP, 'taking care of extensions', [lambda x: x.extensions_step], False)
34483471

34493472
# part 1: pre-iteration + first iteration

easybuild/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from easybuild.framework.easyconfig.tools import det_easyconfig_paths, dump_env_script, get_paths_for
5656
from easybuild.framework.easyconfig.tools import parse_easyconfigs, review_pr, run_contrib_checks, skip_available
5757
from easybuild.framework.easyconfig.tweak import obtain_ec_for, tweak
58+
from easybuild.tools.build_log import print_warning
5859
from easybuild.tools.config import find_last_log, get_repository, get_repositorypath, build_option
5960
from easybuild.tools.containers.common import containerize
6061
from easybuild.tools.docs import list_software
@@ -304,6 +305,14 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
304305
init_session_state.update({'module_list': modlist})
305306
_log.debug("Initial session state: %s" % init_session_state)
306307

308+
if options.skip_test_step:
309+
if options.ignore_test_failure:
310+
raise EasyBuildError("Found both ignore-test-failure and skip-test-step enabled. "
311+
"Please use only one of them.")
312+
else:
313+
print_warning("Will not run the test step as requested via skip-test-step. "
314+
"Consider using ignore-test-failure instead and verify the results afterwards")
315+
307316
# determine easybuild-easyconfigs package install path
308317
easyconfigs_pkg_paths = get_paths_for(subdir=EASYCONFIGS_PKG_SUBDIR)
309318
if not easyconfigs_pkg_paths:

easybuild/tools/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
248248
'ignore_checksums',
249249
'ignore_index',
250250
'ignore_locks',
251+
'ignore_test_failure',
251252
'install_latest_eb_release',
252253
'logtostdout',
253254
'minimal_toolchains',

easybuild/tools/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ def override_options(self):
418418
"\"client[A-z0-9]*.example.com': ['Authorization: Basic token']\".",
419419
None, 'append', None, {'metavar': '[URLPAT::][HEADER:]FILE|FIELD'}),
420420
'ignore-checksums': ("Ignore failing checksum verification", None, 'store_true', False),
421+
'ignore-test-failure': ("Ignore a failing test step", None, 'store_true', False),
421422
'ignore-osdeps': ("Ignore any listed OS dependencies", None, 'store_true', False),
422423
'install-latest-eb-release': ("Install latest known version of easybuild", None, 'store_true', False),
423424
'lib-lib64-symlink': ("Automatically create symlinks for lib/ pointing to lib64/ if the former is missing",

test/framework/options.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,29 @@ def test_skip_test_step(self):
394394
found = re.search(test_run_msg, outtxt)
395395
self.assertFalse(found, "Test execution command is NOT present, outtxt: %s" % outtxt)
396396

397+
def test_ignore_test_failure(self):
398+
"""Test ignore failing tests (--ignore-test-failure)."""
399+
400+
topdir = os.path.abspath(os.path.dirname(__file__))
401+
# This EC uses a `runtest` command which does not exist and hence will make the test step fail
402+
toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0-test.eb')
403+
404+
args = [toy_ec, '--ignore-test-failure', '--force']
405+
406+
with self.mocked_stdout_stderr() as (_, stderr):
407+
outtxt = self.eb_main(args, do_build=True)
408+
409+
msg = 'Test failure ignored'
410+
self.assertTrue(re.search(msg, outtxt),
411+
"Ignored test failure message in log should be found, outtxt: %s" % outtxt)
412+
self.assertTrue(re.search(msg, stderr.getvalue()),
413+
"Ignored test failure message in stderr should be found, stderr: %s" % stderr.getvalue())
414+
415+
# Passing skip and ignore options is disallowed
416+
args.append('--skip-test-step')
417+
error_pattern = 'Found both ignore-test-failure and skip-test-step enabled'
418+
self.assertErrorRegex(EasyBuildError, error_pattern, self.eb_main, args, do_build=True, raise_error=True)
419+
397420
def test_job(self):
398421
"""Test submitting build as a job."""
399422

0 commit comments

Comments
 (0)