Skip to content

Commit 5a46cde

Browse files
author
Vasileios Karakasis
committed
Add API methods for skipping tests
1 parent 1c9b188 commit 5a46cde

File tree

9 files changed

+209
-21
lines changed

9 files changed

+209
-21
lines changed

reframe/core/decorators.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import traceback
2121

2222
import reframe.utility.osext as osext
23-
from reframe.core.exceptions import ReframeSyntaxError, user_frame
23+
from reframe.core.exceptions import (ReframeSyntaxError,
24+
SkipTestError,
25+
user_frame)
2426
from reframe.core.logging import getlogger
2527
from reframe.core.pipeline import RegressionTest
2628
from reframe.utility.versioning import VersionValidator
@@ -54,12 +56,15 @@ def _instantiate_all():
5456

5557
try:
5658
ret.append(_instantiate(cls, args))
59+
except SkipTestError as e:
60+
getlogger().warning(f'skipping test {cls.__name__!r}: {e}')
5761
except Exception:
5862
frame = user_frame(*sys.exc_info())
59-
msg = "skipping test due to errors: %s: " % cls.__name__
60-
msg += "use `-v' for more information\n"
61-
msg += " FILE: %s:%s" % (frame.filename, frame.lineno)
62-
getlogger().warning(msg)
63+
getlogger().warning(
64+
f"skipping test {cls.__name__!r} due to errors: "
65+
f"use `-v' for more information\n"
66+
f" FILE: {frame.filename}:{frame.lineno}"
67+
)
6368
getlogger().verbose(traceback.format_exc())
6469

6570
return ret

reframe/core/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ class DependencyError(ReframeError):
276276
'''Raised when a dependency problem is encountered.'''
277277

278278

279+
class SkipTestError(ReframeError):
280+
'''Raised when a test needs to be skipped.'''
281+
282+
279283
def user_frame(exc_type, exc_value, tb):
280284
'''Return a user frame from the exception's traceback.
281285

reframe/core/pipeline.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
from reframe.core.containers import ContainerPlatformField
3838
from reframe.core.deferrable import _DeferredExpression
3939
from reframe.core.exceptions import (BuildError, DependencyError,
40-
PipelineError, SanityError,
41-
PerformanceError)
40+
PerformanceError, PipelineError,
41+
SanityError, SkipTestError)
4242
from reframe.core.meta import RegressionTestMeta
4343
from reframe.core.schedulers import Job
4444
from reframe.core.warnings import user_deprecation_warning
@@ -1841,6 +1841,26 @@ def getdep(self, target, environ=None, part=None):
18411841
raise DependencyError(f'could not resolve dependency to ({target!r}, '
18421842
f'{part!r}, {environ!r})')
18431843

1844+
def skip(self, msg=None):
1845+
'''Skip test.
1846+
1847+
:arg msg: A message explaining why the test was skipped.
1848+
1849+
.. versionadded:: 3.5.1
1850+
'''
1851+
raise SkipTestError(msg)
1852+
1853+
def skip_if(self, cond, msg=None):
1854+
'''Skip test if condition is true.
1855+
1856+
:arg cond: The condition to check for skipping the test.
1857+
:arg msg: A message explaining why the test was skipped.
1858+
1859+
.. versionadded:: 3.5.1
1860+
'''
1861+
if cond:
1862+
self.skip(msg)
1863+
18441864
def __str__(self):
18451865
return "%s(name='%s', prefix='%s')" % (type(self).__name__,
18461866
self.name, self.prefix)

reframe/frontend/executors/__init__.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
JobNotStartedError,
2020
FailureLimitError,
2121
ForceExitError,
22+
SkipTestError,
2223
TaskExit)
2324
from reframe.core.schedulers.local import LocalJobScheduler
2425
from reframe.frontend.printer import PrettyPrinter
@@ -131,6 +132,7 @@ def __init__(self, case, listeners=[]):
131132
self._current_stage = 'startup'
132133
self._exc_info = (None, None, None)
133134
self._listeners = list(listeners)
135+
self._skipped = False
134136

135137
# Reference count for dependent tests; safe to cleanup the test only
136138
# if it is zero
@@ -212,7 +214,8 @@ def exc_info(self):
212214

213215
@property
214216
def failed(self):
215-
return self._failed_stage is not None and not self._aborted
217+
return (self._failed_stage is not None and
218+
not self._aborted and not self._skipped)
216219

217220
@property
218221
def failed_stage(self):
@@ -230,6 +233,10 @@ def completed(self):
230233
def aborted(self):
231234
return self._aborted
232235

236+
@property
237+
def skipped(self):
238+
return self._skipped
239+
233240
def _notify_listeners(self, callback_name):
234241
for l in self._listeners:
235242
callback = getattr(l, callback_name)
@@ -260,7 +267,12 @@ def __exit__(this, exc_type, exc_value, traceback):
260267
logger.debug(f'Entering stage: {self._current_stage}')
261268
with update_timestamps():
262269
return fn(*args, **kwargs)
263-
270+
except SkipTestError as e:
271+
if not self.succeeded:
272+
# Only skip a test if it hasn't finished yet;
273+
# This practically ignores skipping during the cleanup phase
274+
self.skip()
275+
raise TaskExit from e
264276
except ABORT_REASONS:
265277
self.fail()
266278
raise
@@ -321,6 +333,12 @@ def fail(self, exc_info=None):
321333
self._exc_info = exc_info or sys.exc_info()
322334
self._notify_listeners('on_task_failure')
323335

336+
def skip(self, exc_info=None):
337+
self._skipped = True
338+
self._failed_stage = self._current_stage
339+
self._exc_info = exc_info or sys.exc_info()
340+
self._notify_listeners('on_task_skip')
341+
324342
def abort(self, cause=None):
325343
if self.failed or self._aborted:
326344
return
@@ -355,6 +373,10 @@ def on_task_run(self, task):
355373
def on_task_exit(self, task):
356374
'''Called whenever a RegressionTask finishes.'''
357375

376+
@abc.abstractmethod
377+
def on_task_skip(self, task):
378+
'''Called whenever a RegressionTask is skipped.'''
379+
358380
@abc.abstractmethod
359381
def on_task_failure(self, task):
360382
'''Called when a regression test has failed.'''
@@ -415,18 +437,21 @@ def runall(self, testcases, restored_cases=None):
415437
# Print the summary line
416438
num_failures = len(self._stats.failed())
417439
num_completed = len(self._stats.completed())
440+
num_skipped = len(self._stats.skipped())
418441
num_tasks = len(self._stats.tasks())
419-
if num_failures > 0 or num_completed < num_tasks:
442+
if num_failures > 0 or num_completed + num_skipped < num_tasks:
420443
status = 'FAILED'
421444
else:
422445
status = 'PASSED'
423446

424447
total_run = len(testcases) - num_tasks + num_completed
448+
total_completed = len(self._stats.completed(0))
449+
total_skipped = len(self._stats.skipped(0))
425450
self._printer.status(
426451
status,
427-
f'Ran {num_completed}/{total_run}'
452+
f'Ran {total_completed}/{total_run}'
428453
f' test case(s) from {num_checks} check(s) '
429-
f'({num_failures} failure(s))',
454+
f'({num_failures} failure(s), {total_skipped} skipped)',
430455
just='center'
431456
)
432457
self._printer.timestamp('Finished on', 'short double line')

reframe/frontend/executors/policies.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import time
1111

1212
from reframe.core.exceptions import (FailureLimitError,
13+
SkipTestError,
1314
TaskDependencyError,
1415
TaskExit)
1516
from reframe.core.logging import getlogger
@@ -100,6 +101,14 @@ def runcase(self, case):
100101
for c in case.deps if c in self._task_index):
101102
raise TaskDependencyError('dependencies failed')
102103

104+
if any(self._task_index[c].skipped
105+
for c in case.deps if c in self._task_index):
106+
try:
107+
raise SkipTestError('skipped due to skipped dependencies')
108+
except SkipTestError as e:
109+
task.skip()
110+
raise TaskExit from e
111+
103112
partname = task.testcase.partition.fullname
104113
task.setup(task.testcase.partition,
105114
task.testcase.environ,
@@ -132,12 +141,10 @@ def runcase(self, case):
132141

133142
self._retired_tasks.append(task)
134143
task.finalize()
135-
136144
except TaskExit:
137145
return
138146
except ABORT_REASONS as e:
139147
task.abort(e)
140-
141148
raise
142149
except BaseException:
143150
task.fail(sys.exc_info())
@@ -151,6 +158,10 @@ def on_task_run(self, task):
151158
def on_task_exit(self, task):
152159
pass
153160

161+
def on_task_skip(self, task):
162+
msg = str(task.exc_info[1])
163+
self.printer.status('SKIP', msg, just='right')
164+
154165
def on_task_failure(self, task):
155166
self._num_failed_tasks += 1
156167
timings = task.pipeline_timings(['compile_complete',
@@ -247,6 +258,8 @@ def _remove_from_running(self, task):
247258
getlogger().debug2('Task was not running')
248259
pass
249260

261+
# FIXME: The following functions are very similar and they are also reused
262+
# in the serial policy; we should refactor them
250263
def deps_failed(self, task):
251264
# NOTE: Restored dependencies are not in the task_index
252265
return any(self._task_index[c].failed
@@ -257,6 +270,11 @@ def deps_succeeded(self, task):
257270
return all(self._task_index[c].succeeded
258271
for c in task.testcase.deps if c in self._task_index)
259272

273+
def deps_skipped(self, task):
274+
# NOTE: Restored dependencies are not in the task_index
275+
return any(self._task_index[c].skipped
276+
for c in task.testcase.deps if c in self._task_index)
277+
260278
def on_task_setup(self, task):
261279
partname = task.check.current_partition.fullname
262280
self._ready_tasks[partname].append(task)
@@ -265,6 +283,17 @@ def on_task_run(self, task):
265283
partname = task.check.current_partition.fullname
266284
self._running_tasks[partname].append(task)
267285

286+
def on_task_skip(self, task):
287+
# Remove the task from the running list if it was skipped after the
288+
# run phase
289+
if task.check.current_partition:
290+
partname = task.check.current_partition.fullname
291+
if task.failed_stage in ('run_complete', 'run_wait'):
292+
self._running_tasks[partname].remove(task)
293+
294+
msg = str(task.exc_info[1])
295+
self.printer.status('SKIP', msg, just='right')
296+
268297
def on_task_failure(self, task):
269298
if task.aborted:
270299
return
@@ -308,7 +337,13 @@ def on_task_exit(self, task):
308337
self._completed_tasks.append(task)
309338

310339
def _setup_task(self, task):
311-
if self.deps_succeeded(task):
340+
if self.deps_skipped(task):
341+
try:
342+
raise SkipTestError('skipped due to skipped dependencies')
343+
except SkipTestError as e:
344+
task.skip()
345+
return False
346+
elif self.deps_succeeded(task):
312347
try:
313348
task.setup(task.testcase.partition,
314349
task.testcase.environ,
@@ -346,7 +381,7 @@ def runcase(self, case):
346381
try:
347382
partname = partition.fullname
348383
if not self._setup_task(task):
349-
if not task.failed:
384+
if not task.skipped and not task.failed:
350385
self.printer.status(
351386
'DEP', '%s on %s using %s' %
352387
(check.name, partname, environ.name),
@@ -371,7 +406,7 @@ def runcase(self, case):
371406
else:
372407
self.printer.status('HOLD', task.check.info(), just='right')
373408
except TaskExit:
374-
if not task.failed:
409+
if not task.failed or not task.skipped:
375410
with contextlib.suppress(TaskExit):
376411
self._reschedule(task)
377412

@@ -380,7 +415,6 @@ def runcase(self, case):
380415
# If abort was caused due to failure elsewhere, abort current
381416
# task as well
382417
task.abort(e)
383-
384418
self._failall(e)
385419
raise
386420

@@ -416,7 +450,8 @@ def split_jobs(tasks):
416450
def _setup_all(self):
417451
still_waiting = []
418452
for task in self._waiting_tasks:
419-
if not self._setup_task(task) and not task.failed:
453+
if (not self._setup_task(task) and
454+
not task.failed and not task.skipped):
420455
still_waiting.append(task)
421456

422457
self._waiting_tasks[:] = still_waiting

reframe/frontend/statistics.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ def tasks(self, run=-1):
3535
def failed(self, run=-1):
3636
return [t for t in self.tasks(run) if t.failed]
3737

38+
def skipped(self, run=-1):
39+
return [t for t in self.tasks(run) if t.skipped]
40+
3841
def aborted(self, run=-1):
3942
return [t for t in self.tasks(run) if t.aborted]
4043

@@ -83,6 +86,7 @@ def json(self, force=False):
8386
testcases = []
8487
num_failures = 0
8588
num_aborted = 0
89+
num_skipped = 0
8690
for t in run:
8791
check = t.check
8892
partition = check.current_partition
@@ -158,6 +162,9 @@ def json(self, force=False):
158162
'traceback': t.exc_info[2]
159163
}
160164
entry['fail_severe'] = errors.is_severe(*t.exc_info)
165+
elif t.skipped:
166+
entry['result'] = 'skipped'
167+
num_skipped += 1
161168
else:
162169
entry['result'] = 'success'
163170
entry['outputdir'] = check.outputdir
@@ -183,6 +190,7 @@ def json(self, force=False):
183190
'num_cases': len(run),
184191
'num_failures': num_failures,
185192
'num_aborted': num_aborted,
193+
'num_skipped': num_skipped,
186194
'runid': runid,
187195
'testcases': testcases
188196
})

unittests/resources/checks/hellocheck.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,14 @@ def __init__(self):
3232
self.valid_prog_environs = ['*']
3333
self.sourcepath = 'hello.c'
3434
self.sanity_patterns = sn.assert_not_found(r'(?i)error', self.stdout)
35+
36+
37+
@rfm.simple_test
38+
class SkipTest(rfm.RunOnlyRegressionTest):
39+
'''Test to be always skipped'''
40+
valid_systems = ['*']
41+
valid_prog_environs = ['*']
42+
sanity_patterns = sn.assert_true(1)
43+
44+
def __init__(self):
45+
self.skip_if(True, 'unsupported')

unittests/test_cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,8 @@ def test_maxfail_option(run_reframe):
734734
)
735735
assert 'Traceback' not in stdout
736736
assert 'Traceback' not in stderr
737-
assert 'Ran 2/2 test case(s) from 2 check(s) (0 failure(s))' in stdout
737+
assert ('Ran 2/2 test case(s) from 2 check(s) '
738+
'(0 failure(s), 0 skipped)') in stdout
738739
assert returncode == 0
739740

740741

0 commit comments

Comments
 (0)