Skip to content

Commit 12b7110

Browse files
author
Vasileios Karakasis
authored
Merge pull request #797 from vkarak/feat/test-graph-validation
[feat] Validate test case graph
2 parents edc8ecf + aa064ab commit 12b7110

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

reframe/frontend/dependency.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44

55
import collections
6+
import itertools
67

78
import reframe as rfm
89
from reframe.core.exceptions import DependencyError
@@ -79,3 +80,49 @@ def print_deps(graph):
7980

8081
def validate_deps(graph):
8182
"""Validate dependency graph."""
83+
84+
# Reduce test case graph to a test name only graph; this disallows
85+
# pseudo-dependencies as follows:
86+
#
87+
# (t0, e1) -> (t1, e1)
88+
# (t1, e0) -> (t0, e0)
89+
#
90+
# This reduction step will result in a graph description with duplicate
91+
# entries in the adjacency list; this is not a problem, cos they will be
92+
# filtered out during the DFS traversal below.
93+
test_graph = {}
94+
for case, deps in graph.items():
95+
test_deps = [d.check.name for d in deps]
96+
try:
97+
test_graph[case.check.name] += test_deps
98+
except KeyError:
99+
test_graph[case.check.name] = test_deps
100+
101+
# Check for cyclic dependencies in the test name graph
102+
visited = set()
103+
sources = set(test_graph.keys())
104+
path = []
105+
106+
# Since graph may comprise multiple not connected subgraphs, we search for
107+
# cycles starting from all possible sources
108+
while sources:
109+
unvisited = [(sources.pop(), None)]
110+
while unvisited:
111+
node, parent = unvisited.pop()
112+
while path and path[-1] != parent:
113+
path.pop()
114+
115+
adjacent = reversed(test_graph[node])
116+
path.append(node)
117+
for n in adjacent:
118+
if n in path:
119+
cycle_str = '->'.join(path + [n])
120+
raise DependencyError(
121+
'found cyclic dependency between tests: ' + cycle_str)
122+
123+
if n not in visited:
124+
unvisited.append((n, node))
125+
126+
visited.add(node)
127+
128+
sources -= visited

unittests/test_policies.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ def test_build_deps(self):
489489

490490
# Build dependencies and continue testing
491491
deps = dependency.build_deps(cases)
492+
dependency.validate_deps(deps)
492493

493494
# Check DEPEND_FULLY dependencies
494495
assert num_deps(deps, 'Test1_fully') == 8
@@ -587,3 +588,120 @@ def test_build_deps_unknown_source_env(self):
587588
# is not executed for eX
588589
deps = dependency.build_deps(executors.generate_testcases(checks))
589590
assert num_deps(deps, 'Test1_default') == 4
591+
592+
@rt.switch_runtime(fixtures.TEST_SITE_CONFIG, 'sys0')
593+
def test_build_deps_empty(self):
594+
assert {} == dependency.build_deps([])
595+
596+
def create_test(self, name):
597+
test = rfm.RegressionTest()
598+
test.name = name
599+
test.valid_systems = ['*']
600+
test.valid_prog_environs = ['*']
601+
test.executable = 'echo'
602+
test.executable_opts = [name]
603+
return test
604+
605+
@rt.switch_runtime(fixtures.TEST_SITE_CONFIG, 'sys0')
606+
def test_valid_deps(self):
607+
#
608+
# t0 +-->t5<--+
609+
# ^ | |
610+
# | | |
611+
# +-->t1<--+ t6 t7
612+
# | | ^
613+
# t2<------t3 |
614+
# ^ ^ |
615+
# | | t8
616+
# +---t4---+
617+
#
618+
t0 = self.create_test('t0')
619+
t1 = self.create_test('t1')
620+
t2 = self.create_test('t2')
621+
t3 = self.create_test('t3')
622+
t4 = self.create_test('t4')
623+
t5 = self.create_test('t5')
624+
t6 = self.create_test('t6')
625+
t7 = self.create_test('t7')
626+
t8 = self.create_test('t8')
627+
t1.depends_on('t0')
628+
t2.depends_on('t1')
629+
t3.depends_on('t1')
630+
t3.depends_on('t2')
631+
t4.depends_on('t2')
632+
t4.depends_on('t3')
633+
t6.depends_on('t5')
634+
t7.depends_on('t5')
635+
t8.depends_on('t7')
636+
dependency.validate_deps(
637+
dependency.build_deps(
638+
executors.generate_testcases([t0, t1, t2, t3, t4,
639+
t5, t6, t7, t8])
640+
)
641+
)
642+
643+
@rt.switch_runtime(fixtures.TEST_SITE_CONFIG, 'sys0')
644+
def test_cyclic_deps(self):
645+
#
646+
# t0 +-->t5<--+
647+
# ^ | |
648+
# | | |
649+
# +-->t1<--+ t6 t7
650+
# | | | ^
651+
# t2 | t3 |
652+
# ^ | ^ |
653+
# | v | t8
654+
# +---t4---+
655+
#
656+
t0 = self.create_test('t0')
657+
t1 = self.create_test('t1')
658+
t2 = self.create_test('t2')
659+
t3 = self.create_test('t3')
660+
t4 = self.create_test('t4')
661+
t5 = self.create_test('t5')
662+
t6 = self.create_test('t6')
663+
t7 = self.create_test('t7')
664+
t8 = self.create_test('t8')
665+
t1.depends_on('t0')
666+
t1.depends_on('t4')
667+
t2.depends_on('t1')
668+
t3.depends_on('t1')
669+
t3.depends_on('t2')
670+
t4.depends_on('t2')
671+
t4.depends_on('t3')
672+
t6.depends_on('t5')
673+
t7.depends_on('t5')
674+
t8.depends_on('t7')
675+
deps = dependency.build_deps(
676+
executors.generate_testcases([t0, t1, t2, t3, t4,
677+
t5, t6, t7, t8])
678+
)
679+
680+
with pytest.raises(DependencyError) as exc_info:
681+
dependency.validate_deps(deps)
682+
683+
assert ('t4->t2->t1->t4' in str(exc_info.value) or
684+
't2->t1->t4->t2' in str(exc_info.value) or
685+
't1->t4->t2->t1' in str(exc_info.value) or
686+
't1->t4->t3->t1' in str(exc_info.value) or
687+
't4->t3->t1->t4' in str(exc_info.value) or
688+
't3->t1->t4->t3' in str(exc_info.value))
689+
690+
@rt.switch_runtime(fixtures.TEST_SITE_CONFIG, 'sys0')
691+
def test_cyclic_deps_by_env(self):
692+
t0 = self.create_test('t0')
693+
t1 = self.create_test('t1')
694+
t1.depends_on('t0', rfm.DEPEND_EXACT, {'e0': ['e0']})
695+
t0.depends_on('t1', rfm.DEPEND_EXACT, {'e1': ['e1']})
696+
deps = dependency.build_deps(
697+
executors.generate_testcases([t0, t1])
698+
)
699+
with pytest.raises(DependencyError) as exc_info:
700+
dependency.validate_deps(deps)
701+
702+
assert ('t1->t0->t1' in str(exc_info.value) or
703+
't0->t1->t0' in str(exc_info.value))
704+
705+
@rt.switch_runtime(fixtures.TEST_SITE_CONFIG, 'sys0')
706+
def test_validate_deps_empty(self):
707+
dependency.validate_deps({})

0 commit comments

Comments
 (0)