Skip to content

Commit 30fa94a

Browse files
author
Vasileios Karakasis
authored
Merge pull request #1875 from vkarak/feat/skip-tests
[feat] Add test API methods for skipping tests
2 parents 4d21a13 + 80bf74f commit 30fa94a

File tree

9 files changed

+213
-23
lines changed

9 files changed

+213
-23
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
@@ -288,6 +288,10 @@ class DependencyError(ReframeError):
288288
'''Raised when a dependency problem is encountered.'''
289289

290290

291+
class SkipTestError(ReframeError):
292+
'''Raised when a test needs to be skipped.'''
293+
294+
291295
def user_frame(exc_type, exc_value, tb):
292296
'''Return a user frame from the exception's traceback.
293297

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
@@ -1843,6 +1843,26 @@ def getdep(self, target, environ=None, part=None):
18431843
raise DependencyError(f'could not resolve dependency to ({target!r}, '
18441844
f'{part!r}, {environ!r})')
18451845

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

reframe/frontend/executors/__init__.py

Lines changed: 31 additions & 7 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.'''
@@ -400,7 +422,6 @@ def stats(self):
400422
return self._stats
401423

402424
def runall(self, testcases, restored_cases=None):
403-
abort_reason = None
404425
num_checks = len({tc.check.name for tc in testcases})
405426
self._printer.separator('short double line',
406427
'Running %d check(s)' % num_checks)
@@ -415,18 +436,21 @@ def runall(self, testcases, restored_cases=None):
415436
# Print the summary line
416437
num_failures = len(self._stats.failed())
417438
num_completed = len(self._stats.completed())
439+
num_skipped = len(self._stats.skipped())
418440
num_tasks = len(self._stats.tasks())
419-
if num_failures > 0 or num_completed < num_tasks:
441+
if num_failures > 0 or num_completed + num_skipped < num_tasks:
420442
status = 'FAILED'
421443
else:
422444
status = 'PASSED'
423445

424-
total_run = len(testcases) - num_tasks + num_completed
446+
total_run = len(testcases)
447+
total_completed = len(self._stats.completed(0))
448+
total_skipped = len(self._stats.skipped(0))
425449
self._printer.status(
426450
status,
427-
f'Ran {num_completed}/{total_run}'
451+
f'Ran {total_completed}/{total_run}'
428452
f' test case(s) from {num_checks} check(s) '
429-
f'({num_failures} failure(s))',
453+
f'({num_failures} failure(s), {total_skipped} skipped)',
430454
just='center'
431455
)
432456
self._printer.timestamp('Finished on', 'short double line')

reframe/frontend/executors/policies.py

Lines changed: 45 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,17 @@ 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+
107+
# We raise the SkipTestError here and catch it immediately in
108+
# order for `skip()` to get the correct exception context.
109+
try:
110+
raise SkipTestError('skipped due to skipped dependencies')
111+
except SkipTestError as e:
112+
task.skip()
113+
raise TaskExit from e
114+
103115
partname = task.testcase.partition.fullname
104116
task.setup(task.testcase.partition,
105117
task.testcase.environ,
@@ -132,12 +144,10 @@ def runcase(self, case):
132144

133145
self._retired_tasks.append(task)
134146
task.finalize()
135-
136147
except TaskExit:
137148
return
138149
except ABORT_REASONS as e:
139150
task.abort(e)
140-
141151
raise
142152
except BaseException:
143153
task.fail(sys.exc_info())
@@ -151,6 +161,10 @@ def on_task_run(self, task):
151161
def on_task_exit(self, task):
152162
pass
153163

164+
def on_task_skip(self, task):
165+
msg = str(task.exc_info[1])
166+
self.printer.status('SKIP', msg, just='right')
167+
154168
def on_task_failure(self, task):
155169
self._num_failed_tasks += 1
156170
timings = task.pipeline_timings(['compile_complete',
@@ -247,6 +261,8 @@ def _remove_from_running(self, task):
247261
getlogger().debug2('Task was not running')
248262
pass
249263

264+
# FIXME: The following functions are very similar and they are also reused
265+
# in the serial policy; we should refactor them
250266
def deps_failed(self, task):
251267
# NOTE: Restored dependencies are not in the task_index
252268
return any(self._task_index[c].failed
@@ -257,6 +273,11 @@ def deps_succeeded(self, task):
257273
return all(self._task_index[c].succeeded
258274
for c in task.testcase.deps if c in self._task_index)
259275

276+
def deps_skipped(self, task):
277+
# NOTE: Restored dependencies are not in the task_index
278+
return any(self._task_index[c].skipped
279+
for c in task.testcase.deps if c in self._task_index)
280+
260281
def on_task_setup(self, task):
261282
partname = task.check.current_partition.fullname
262283
self._ready_tasks[partname].append(task)
@@ -265,6 +286,17 @@ def on_task_run(self, task):
265286
partname = task.check.current_partition.fullname
266287
self._running_tasks[partname].append(task)
267288

289+
def on_task_skip(self, task):
290+
# Remove the task from the running list if it was skipped after the
291+
# run phase
292+
if task.check.current_partition:
293+
partname = task.check.current_partition.fullname
294+
if task.failed_stage in ('run_complete', 'run_wait'):
295+
self._running_tasks[partname].remove(task)
296+
297+
msg = str(task.exc_info[1])
298+
self.printer.status('SKIP', msg, just='right')
299+
268300
def on_task_failure(self, task):
269301
if task.aborted:
270302
return
@@ -308,7 +340,13 @@ def on_task_exit(self, task):
308340
self._completed_tasks.append(task)
309341

310342
def _setup_task(self, task):
311-
if self.deps_succeeded(task):
343+
if self.deps_skipped(task):
344+
try:
345+
raise SkipTestError('skipped due to skipped dependencies')
346+
except SkipTestError as e:
347+
task.skip()
348+
return False
349+
elif self.deps_succeeded(task):
312350
try:
313351
task.setup(task.testcase.partition,
314352
task.testcase.environ,
@@ -346,7 +384,7 @@ def runcase(self, case):
346384
try:
347385
partname = partition.fullname
348386
if not self._setup_task(task):
349-
if not task.failed:
387+
if not task.skipped and not task.failed:
350388
self.printer.status(
351389
'DEP', '%s on %s using %s' %
352390
(check.name, partname, environ.name),
@@ -371,7 +409,7 @@ def runcase(self, case):
371409
else:
372410
self.printer.status('HOLD', task.check.info(), just='right')
373411
except TaskExit:
374-
if not task.failed:
412+
if not task.failed or not task.skipped:
375413
with contextlib.suppress(TaskExit):
376414
self._reschedule(task)
377415

@@ -380,7 +418,6 @@ def runcase(self, case):
380418
# If abort was caused due to failure elsewhere, abort current
381419
# task as well
382420
task.abort(e)
383-
384421
self._failall(e)
385422
raise
386423

@@ -416,7 +453,8 @@ def split_jobs(tasks):
416453
def _setup_all(self):
417454
still_waiting = []
418455
for task in self._waiting_tasks:
419-
if not self._setup_task(task) and not task.failed:
456+
if (not self._setup_task(task) and
457+
not task.failed and not task.skipped):
420458
still_waiting.append(task)
421459

422460
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)