From 17f47cb8af51e09f03061fd5ce191e0c31211b0a Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Thu, 30 May 2019 01:22:45 +0200 Subject: [PATCH 1/8] Implement topological sort of the test cases - This is a necessary step for running interdependent tests with the serial execution policy. - This commit adds also an implementation of an ordered set. This data structure is quite useful in several contexts and is indeed used extensively in implementing the test dependencies. Its implementation is based on an `OrderedDict`, but it provides the complete `Set` and `MutableSet` interfaces. --- reframe/frontend/dependency.py | 99 ++++++++++++++--- reframe/utility/__init__.py | 190 +++++++++++++++++++++++++++++++++ unittests/test_policies.py | 60 ++++++++++- unittests/test_utility.py | 123 +++++++++++++++++++++ 4 files changed, 459 insertions(+), 13 deletions(-) diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependency.py index 4f11bf4ae6..4f950a990d 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependency.py @@ -6,6 +6,7 @@ import itertools import reframe as rfm +import reframe.utility as util from reframe.core.exceptions import DependencyError @@ -50,7 +51,6 @@ def resolve_dep(target, from_map, *args): graph = {} for c in cases: - graph[c] = c.deps cname = c.check.name pname = c.partition.fullname ename = c.environ.name @@ -70,6 +70,8 @@ def resolve_dep(target, from_map, *args): c.deps.append(resolve_dep(c, cases_revmap, tname, pname, te)) + graph[c] = util.OrderedSet(c.deps) + return graph @@ -78,6 +80,19 @@ def print_deps(graph): print(c, '->', deps) +def _reduce_deps(graph): + """Reduce test case graph to a test-only graph.""" + ret = {} + for case, deps in graph.items(): + test_deps = util.OrderedSet(d.check.name for d in deps) + try: + ret[case.check.name] |= test_deps + except KeyError: + ret[case.check.name] = test_deps + + return ret + + def validate_deps(graph): """Validate dependency graph.""" @@ -87,16 +102,7 @@ def validate_deps(graph): # (t0, e1) -> (t1, e1) # (t1, e0) -> (t0, e0) # - # This reduction step will result in a graph description with duplicate - # entries in the adjacency list; this is not a problem, cos they will be - # filtered out during the DFS traversal below. - test_graph = {} - for case, deps in graph.items(): - test_deps = [d.check.name for d in deps] - try: - test_graph[case.check.name] += test_deps - except KeyError: - test_graph[case.check.name] = test_deps + test_graph = _reduce_deps(graph) # Check for cyclic dependencies in the test name graph visited = set() @@ -112,7 +118,8 @@ def validate_deps(graph): while path and path[-1] != parent: path.pop() - adjacent = reversed(test_graph[node]) + #adjacent = reversed(test_graph[node]) + adjacent = test_graph[node] path.append(node) for n in adjacent: if n in path: @@ -126,3 +133,71 @@ def validate_deps(graph): visited.add(node) sources -= visited + + +def _reverse_deps(graph): + ret = {} + for n, deps in graph.items(): + ret.setdefault(n, util.OrderedSet({})) + for d in deps: + try: + ret[d] |= {n} + except KeyError: + ret[d] = util.OrderedSet({n}) + + return ret + + +def toposort(graph): + # NOTES on implementation: + # + # 1. This function assumes a directed acyclic graph. + # 2. The purpose of this function is to topologically sort the test cases, + # not only the tests. However, since we do not allow cycles between + # tests in any case (even if this could be classified a + # pseudo-dependency), we first do a topological sort of the tests and we + # subsequently sort the test cases by partition and by programming + # environment. + # 3. To achieve this 3-step sorting with a single sort operations, we rank + # the test cases by associating them with an integer key based on the + # result of the topological sort of the tests and by choosing an + # arbitrary ordering of the partitions and the programming environment. + + test_graph = _reduce_deps(graph) + revgraph = _reverse_deps(test_graph) + + # We do a BFS traversal from each root + visited = {} + roots = set(t for t, deps in test_graph.items() if not deps) + for r in roots: + unvisited = [r] + visited[r] = util.OrderedSet() + while unvisited: + node = unvisited.pop(0) + adjacent = revgraph[node] + unvisited += [n for n in adjacent if n not in visited] + visited[r].add(node) + + # Combine all individual sequences into a single one + ordered_tests = util.OrderedSet() + for tests in visited.values(): + ordered_tests |= tests + + # Get all partitions and programming environments from test cases + partitions = util.OrderedSet() + environs = util.OrderedSet() + for c in graph.keys(): + partitions.add(c.partition.fullname) + environs.add(c.environ.name) + + # Rank test cases; we first need to calculate the base for the rank number + base = max(len(partitions), len(environs)) + 1 + ranks = {} + for i, test in enumerate(ordered_tests): + for j, part in enumerate(partitions): + for k, env in enumerate(environs): + ranks[test, part, env] = i*base**2 + j*base + k + + return sorted(graph.keys(), + key=lambda x: ranks[x.check.name, + x.partition.fullname, x.environ.name]) diff --git a/reframe/utility/__init__.py b/reframe/utility/__init__.py index 7c85f4a811..614c954019 100644 --- a/reframe/utility/__init__.py +++ b/reframe/utility/__init__.py @@ -1,5 +1,6 @@ import abc import collections +import functools import importlib import importlib.util import itertools @@ -243,6 +244,195 @@ def __missing__(self, key): raise KeyError(str(key)) +@functools.total_ordering +class OrderedSet(collections.abc.MutableSet): + """An ordered set.""" + + def __init__(self, *args): + # We need to allow construction without arguments + if not args: + iterable = [] + elif len(args) == 1: + iterable = args[0] + else: + # We use the exact same error message as for the built-in set + raise TypeError('%s expected at most 1 arguments, got %s' % + type(self).__name__, len(args)) + + if not isinstance(iterable, collections.abc.Iterable): + raise TypeError("'%s' object is not iterable" % + type(iterable).__name__) + + # We implement an ordered set through the keys of an OrderedDict; + # its values are all set to None + self.__data = collections.OrderedDict( + itertools.zip_longest(iterable, [], fillvalue=None) + ) + + def __repr__(self): + vals = self.__data.keys() + if not vals: + return type(self).__name__ + '()' + else: + return '{' + ', '.join(str(v) for v in vals) + '}' + + # Container i/face + def __contains__(self, item): + return item in self.__data + + def __iter__(self): + return iter(self.__data) + + def __len__(self): + return len(self.__data.keys()) + + # Set i/face + # + # Note on the complexity of the operators + # + # In every case below we first construct a set from the internal ordered + # dictionary's keys and then apply the operator. This step's complexity is + # O(len(self.__data.keys())). Since the complexity of the standard set + # operators are at the order of magnitute of the lenghts of the operands + # (ranging from O(min(len(a), len(b))) to O(len(a) + len(b))), this step + # does not change the complexity class; it just changes the constant + # factor. + # + def __eq__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + return set(self.__data.keys()) == other + + def __gt__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + return set(self.__data.keys()) > other + + def __and__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + return set(self.__data.keys()) & other + + def __or__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + return set(self.__data.keys()) | other + + def __sub__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + return set(self.__data.keys()) - other + + def __xor__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + return set(self.__data.keys()) ^ other + + def isdisjoint(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + return set(self.__data.keys()).isdisjoint(other) + + def issubset(self, other): + return self <= other + + def issuperset(self, other): + return self >= other + + def symmetric_difference(self, other): + return self ^ other + + def union(self, *others): + ret = type(self)(self) + for s in others: + ret |= s + + return ret + + def intersection(self, *others): + ret = type(self)(self) + for s in others: + ret &= s + + return ret + + def difference(self, *others): + ret = type(self)(self) + for s in others: + ret -= s + + return ret + + # MutableSet i/face + + def add(self, elem): + self.__data[elem] = None + + def remove(self, elem): + del self.__data[elem] + + def discard(self, elem): + try: + self.remove(elem) + except KeyError: + pass + + def pop(self): + return self.__data.popitem()[0] + + def clear(self): + self.__data.clear() + + def __ior__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + for e in other: + self.add(e) + + return self + + def __iand__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + discard_list = [e for e in self if e not in other] + for e in discard_list: + self.discard(e) + + return self + + def __isub__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + for e in other: + self.discard(e) + + return self + + def __ixor__(self, other): + if not isinstance(other, collections.abc.Set): + return NotImplemented + + discard_list = [e for e in self if e in other] + for e in discard_list: + self.discard(e) + + return self + + # Other functions + def __reversed__(self): + return reversed(self.__data.keys()) + + class SequenceView(collections.abc.Sequence): """A read-only view of a sequence.""" diff --git a/unittests/test_policies.py b/unittests/test_policies.py index 903a606cdd..e5b47b65e6 100644 --- a/unittests/test_policies.py +++ b/unittests/test_policies.py @@ -9,6 +9,7 @@ import reframe.frontend.dependency as dependency import reframe.frontend.executors as executors import reframe.frontend.executors.policies as policies +import reframe.utility as util import reframe.utility.os_ext as os_ext from reframe.core.exceptions import DependencyError, JobNotStartedError from reframe.frontend.loader import RegressionCheckLoader @@ -666,7 +667,6 @@ def test_cyclic_deps(self): t1.depends_on('t4') t2.depends_on('t1') t3.depends_on('t1') - t3.depends_on('t2') t4.depends_on('t2') t4.depends_on('t3') t6.depends_on('t5') @@ -705,3 +705,61 @@ def test_cyclic_deps_by_env(self): @rt.switch_runtime(fixtures.TEST_SITE_CONFIG, 'sys0') def test_validate_deps_empty(self): dependency.validate_deps({}) + + @rt.switch_runtime(fixtures.TEST_SITE_CONFIG, 'sys0') + def test_toposort(self): + # + # t0 +-->t5<--+ + # ^ | | + # | | | + # +-->t1<--+ t6 t7 + # | | ^ + # t2<------t3 | + # ^ ^ | + # | | t8 + # +---t4---+ + # + t0 = self.create_test('t0') + t1 = self.create_test('t1') + t2 = self.create_test('t2') + t3 = self.create_test('t3') + t4 = self.create_test('t4') + t5 = self.create_test('t5') + t6 = self.create_test('t6') + t7 = self.create_test('t7') + t8 = self.create_test('t8') + t1.depends_on('t0') + t2.depends_on('t1') + t3.depends_on('t1') + t3.depends_on('t2') + t4.depends_on('t2') + t4.depends_on('t3') + t6.depends_on('t5') + t7.depends_on('t5') + t8.depends_on('t7') + deps = dependency.build_deps( + executors.generate_testcases([t0, t1, t2, t3, t4, + t5, t6, t7, t8]) + ) + cases = dependency.toposort(deps) + + # Check the order of cases by test + tests = list(util.OrderedSet(c.check.name for c in cases)) + assert (tests == ['t0', 't1', 't2', 't3', 't4', + 't5', 't6', 't7', 't8'] or + tests == ['t5', 't6', 't7', 't8', + 't0', 't1', 't2', 't3', 't4'] or + tests == ['t0', 't1', 't2', 't3', 't4', + 't5', 't7', 't6', 't8'] or + tests == ['t5', 't7', 't6', 't8', + 't0', 't1', 't2', 't3', 't4']) + + expected_order = [] + for t in tests: + for p in ['sys0:p0', 'sys0:p1']: + for e in ['e0', 'e1']: + expected_order.append((t, p, e)) + + cases_order = [(c.check.name, c.partition.fullname, c.environ.name) + for c in cases] + assert cases_order == expected_order diff --git a/unittests/test_utility.py b/unittests/test_utility.py index 9c0b9944a8..e797cb8ecf 100644 --- a/unittests/test_utility.py +++ b/unittests/test_utility.py @@ -1,4 +1,6 @@ import os +import pytest +import random import shutil import sys import tempfile @@ -853,3 +855,124 @@ def test_mapping(self): with self.assertRaises(AttributeError): d.setdefault('c', 3) + + +class TestOrderedSet(unittest.TestCase): + def setUp(self): + # Initialize all tests with the same seed + random.seed(1) + + def test_construction(self): + l = list(range(10)) + random.shuffle(l) + + s = util.OrderedSet(l + l) + assert len(s) == 10 + for i in range(10): + assert i in s + + assert list(s) == l + + def test_construction_empty(self): + s = util.OrderedSet() + assert s == set() + assert set() == s + + def test_str(self): + l = list(range(10)) + random.shuffle(l) + + s = util.OrderedSet(l) + assert str(s) == str(l).replace('[', '{').replace(']', '}') + + s = util.OrderedSet() + assert str(s) == type(s).__name__ + '()' + + def test_construction_error(self): + with pytest.raises(TypeError): + s = util.OrderedSet(2) + + with pytest.raises(TypeError): + s = util.OrderedSet(1, 2, 3) + + def test_operators(self): + s0 = util.OrderedSet(range(10)) + s1 = util.OrderedSet(range(20)) + s2 = util.OrderedSet(range(10, 20)) + + assert s0 == set(range(10)) + assert set(range(10)) == s0 + assert s0 != s1 + assert s1 != s0 + + assert s0 < s1 + assert s0 <= s1 + assert s0 <= s0 + assert s1 > s0 + assert s1 >= s0 + assert s1 >= s1 + + assert s0.issubset(s1) + assert s1.issuperset(s0) + + assert (s0 & s1) == s0 + assert (s0 & s2) == set() + assert (s0 | s2) == s1 + + assert (s1 - s0) == s2 + assert (s2 - s0) == s2 + + assert (s0 ^ s1) == s2 + + assert s0.isdisjoint(s2) + assert not s0.isdisjoint(s1) + assert s0.symmetric_difference(s1) == s2 + + def test_union(self): + l0 = list(range(10)) + l1 = list(range(10, 20)) + l2 = list(range(20, 30)) + random.shuffle(l0) + random.shuffle(l1) + random.shuffle(l2) + + s0 = util.OrderedSet(l0) + s1 = util.OrderedSet(l1) + s2 = util.OrderedSet(l2) + + assert list(s0.union(s1, s2)) == l0 + l1 + l2 + + def test_intersection(self): + l0 = list(range(10, 40)) + l1 = list(range(20, 40)) + l2 = list(range(20, 30)) + random.shuffle(l0) + random.shuffle(l1) + random.shuffle(l2) + + s0 = util.OrderedSet(l0) + s1 = util.OrderedSet(l1) + s2 = util.OrderedSet(l2) + + assert s0.intersection(s1, s2) == s2 + + def test_difference(self): + l0 = list(range(10, 40)) + l1 = list(range(20, 40)) + l2 = list(range(20, 30)) + random.shuffle(l0) + random.shuffle(l1) + random.shuffle(l2) + + s0 = util.OrderedSet(l0) + s1 = util.OrderedSet(l1) + s2 = util.OrderedSet(l2) + + assert s0.difference(s1, s2) == set(range(10, 20)) + + def test_reversed(self): + l = list(range(10)) + random.shuffle(l) + + s = util.OrderedSet(l) + assert list(reversed(s)) == list(reversed(l)) From 7308aaef2ec0a40daa155e48f9b9b19eb34ca89b Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Thu, 6 Jun 2019 00:28:25 +0200 Subject: [PATCH 2/8] WIP: Fix topological sort of test cases --- unittests/test_policies.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/unittests/test_policies.py b/unittests/test_policies.py index e5b47b65e6..8e81520053 100644 --- a/unittests/test_policies.py +++ b/unittests/test_policies.py @@ -742,24 +742,25 @@ def test_toposort(self): t5, t6, t7, t8]) ) cases = dependency.toposort(deps) + print(cases) + cases_order = [] + tests = util.OrderedSet() + visited_tests = set() + for c in cases: + check, part, env = c + cases_order.append((check.name, part.fullname, env.name)) + tests.add(check.name) + visited_tests.add(check.name) - # Check the order of cases by test - tests = list(util.OrderedSet(c.check.name for c in cases)) - assert (tests == ['t0', 't1', 't2', 't3', 't4', - 't5', 't6', 't7', 't8'] or - tests == ['t5', 't6', 't7', 't8', - 't0', 't1', 't2', 't3', 't4'] or - tests == ['t0', 't1', 't2', 't3', 't4', - 't5', 't7', 't6', 't8'] or - tests == ['t5', 't7', 't6', 't8', - 't0', 't1', 't2', 't3', 't4']) + # Assert that all dependencies of c have been visited before + for d in deps[c]: + assert d.check.name in visited_tests + # Check the order of systems and prog. environments expected_order = [] for t in tests: for p in ['sys0:p0', 'sys0:p1']: for e in ['e0', 'e1']: expected_order.append((t, p, e)) - cases_order = [(c.check.name, c.partition.fullname, c.environ.name) - for c in cases] assert cases_order == expected_order From 659ebabe13dfebd9449c7eb50eafe108b47d8ab1 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Thu, 6 Jun 2019 01:08:50 +0200 Subject: [PATCH 3/8] WIP: Fix topological sort of test cases --- reframe/frontend/dependency.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependency.py index 4f950a990d..01ab332a9c 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependency.py @@ -163,19 +163,31 @@ def toposort(graph): # result of the topological sort of the tests and by choosing an # arbitrary ordering of the partitions and the programming environment. - test_graph = _reduce_deps(graph) - revgraph = _reverse_deps(test_graph) + test_deps = _reduce_deps(graph) + rev_deps = _reverse_deps(test_deps) # We do a BFS traversal from each root visited = {} - roots = set(t for t, deps in test_graph.items() if not deps) + roots = set(t for t, deps in test_deps.items() if not deps) for r in roots: - unvisited = [r] + unvisited = util.OrderedSet([r]) visited[r] = util.OrderedSet() while unvisited: - node = unvisited.pop(0) - adjacent = revgraph[node] - unvisited += [n for n in adjacent if n not in visited] + # Next node is one whose all dependencies are visited + node = None + for n in unvisited: + if test_deps[n] <= visited[r]: + node = n + break + + # If node is None, graph has a cycle and this is a bug; this + # function assumes acyclic graphs only + assert node is not None + + unvisited.remove(node) + adjacent = rev_deps[node] + unvisited |= util.OrderedSet( + n for n in adjacent if n not in visited) visited[r].add(node) # Combine all individual sequences into a single one From 746d750f3c3e3b563206f20c060d3be5cd3b6e6b Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Thu, 6 Jun 2019 18:15:58 +0200 Subject: [PATCH 4/8] Fix topological sorting - The algorithm wasn't checking that the next node picked for visiting was already visited. Thus, it was not producing a valid ordering. - Updated unit tests to check any possible ordering of the cases. --- reframe/frontend/dependency.py | 5 +++-- unittests/test_policies.py | 22 ++++++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependency.py index 01ab332a9c..e3918895bf 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependency.py @@ -173,7 +173,7 @@ def toposort(graph): unvisited = util.OrderedSet([r]) visited[r] = util.OrderedSet() while unvisited: - # Next node is one whose all dependencies are visited + # Next node is one whose all dependencies are already visited node = None for n in unvisited: if test_deps[n] <= visited[r]: @@ -187,7 +187,8 @@ def toposort(graph): unvisited.remove(node) adjacent = rev_deps[node] unvisited |= util.OrderedSet( - n for n in adjacent if n not in visited) + n for n in adjacent if n not in visited + ) visited[r].add(node) # Combine all individual sequences into a single one diff --git a/unittests/test_policies.py b/unittests/test_policies.py index 8e81520053..0da4acde96 100644 --- a/unittests/test_policies.py +++ b/unittests/test_policies.py @@ -1,4 +1,5 @@ import collections +import itertools import os import pytest import tempfile @@ -742,7 +743,6 @@ def test_toposort(self): t5, t6, t7, t8]) ) cases = dependency.toposort(deps) - print(cases) cases_order = [] tests = util.OrderedSet() visited_tests = set() @@ -757,10 +757,16 @@ def test_toposort(self): assert d.check.name in visited_tests # Check the order of systems and prog. environments - expected_order = [] - for t in tests: - for p in ['sys0:p0', 'sys0:p1']: - for e in ['e0', 'e1']: - expected_order.append((t, p, e)) - - assert cases_order == expected_order + # We are checking against all possible orderings + valid_orderings = [] + for partitions in itertools.permutations(['sys0:p0', 'sys0:p1']): + for environs in itertools.permutations(['e0', 'e1']): + ordering = [] + for t in tests: + for p in partitions: + for e in environs: + ordering.append((t, p, e)) + + valid_orderings.append(ordering) + + assert cases_order in valid_orderings From 7364b6049b1f1686efd34de52282f08736eea29c Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Thu, 6 Jun 2019 21:52:52 +0200 Subject: [PATCH 5/8] Fix Python 3.7 deprecation warning --- unittests/test_typecheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unittests/test_typecheck.py b/unittests/test_typecheck.py index d024c6afb1..b7f1a98509 100644 --- a/unittests/test_typecheck.py +++ b/unittests/test_typecheck.py @@ -117,7 +117,7 @@ def test_type_names(self): types.Dict[str, types.List[int]].__name__) self.assertEqual('Tuple[int,Set[float],str]', types.Tuple[int, types.Set[float], str].__name__) - self.assertEqual("List[Str[r'\d+']]", + self.assertEqual(r"List[Str[r'\d+']]", types.List[types.Str[r'\d+']].__name__) def test_custom_types(self): From 16b10bef9f5ed0231a504dc1b6681fd79acda503 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Thu, 6 Jun 2019 21:54:17 +0200 Subject: [PATCH 6/8] Fix PEP8 issue --- reframe/frontend/dependency.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependency.py index e3918895bf..a4111236ff 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependency.py @@ -118,7 +118,6 @@ def validate_deps(graph): while path and path[-1] != parent: path.pop() - #adjacent = reversed(test_graph[node]) adjacent = test_graph[node] path.append(node) for n in adjacent: From 6e2ffa0fc2c4a573a85125f5f36c45538d68f0df Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Wed, 12 Jun 2019 12:10:16 +0200 Subject: [PATCH 7/8] Remove unnecessary '.keys()' --- reframe/utility/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reframe/utility/__init__.py b/reframe/utility/__init__.py index 614c954019..caaba49297 100644 --- a/reframe/utility/__init__.py +++ b/reframe/utility/__init__.py @@ -284,7 +284,7 @@ def __iter__(self): return iter(self.__data) def __len__(self): - return len(self.__data.keys()) + return len(self.__data) # Set i/face # From 7b3700a376335448afdcc9350dc840118dda0eef Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Wed, 12 Jun 2019 16:34:57 +0200 Subject: [PATCH 8/8] Add FIXME note about toposort's complexity --- reframe/frontend/dependency.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependency.py index a4111236ff..f888d6500d 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependency.py @@ -173,6 +173,7 @@ def toposort(graph): visited[r] = util.OrderedSet() while unvisited: # Next node is one whose all dependencies are already visited + # FIXME: This makes sorting's complexity O(V^2) node = None for n in unvisited: if test_deps[n] <= visited[r]: