Skip to content

Commit 8cc8d13

Browse files
chore: Schedule
1 parent 989a632 commit 8cc8d13

File tree

2 files changed

+303
-0
lines changed

2 files changed

+303
-0
lines changed

src/util/schedule.star

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
def create():
2+
__self_ref = [None]
3+
__items_by_id = {}
4+
5+
def __self():
6+
return __self_ref[0]
7+
8+
def add(*items):
9+
for item in items:
10+
_assert_item(item)
11+
12+
if __items_by_id.get(item.id):
13+
fail("Failed to add item {}: item with the same ID already exists")
14+
15+
__items_by_id[item.id] = item
16+
17+
return __self()
18+
19+
# This function returns the items in the order they should be launched
20+
# based on their dependencies.
21+
#
22+
# It will try to preserve the order in which the items were added,
23+
# only reordering them if necessary to satisfy the dependencies.
24+
#
25+
# If there are any cycles in the dependencies, it will fail.
26+
# If there are any missing dependencies, it will also fail.
27+
def sequence():
28+
# First we check whether we have all the items
29+
all_dependency_ids = [
30+
dependency
31+
for item in __items_by_id.values()
32+
for dependency in item.dependencies
33+
]
34+
35+
# Now check we have all of them
36+
missing_dependency_ids = [
37+
id for id in all_dependency_ids if id not in __items_by_id
38+
]
39+
if missing_dependency_ids:
40+
fail(
41+
"Failed to launch: Missing items {}".format(
42+
",".join(missing_dependency_ids)
43+
)
44+
)
45+
46+
# Now we have to order the items based on their dependencies
47+
#
48+
# First we start with the default sequence - the order in which the items were added
49+
ordered_items = __items_by_id.values()
50+
num_items = len(ordered_items)
51+
52+
for index in range(num_items):
53+
item = ordered_items[index]
54+
55+
# Since we are not allowed any unbound loops, we'll have to resort to somewhat different strategy
56+
#
57+
# We will calculate the lowest index at which this item can be placed
58+
# based on its dependencies.
59+
lowest_desired_index = _lowest_desired_index(item, ordered_items)
60+
61+
# If the lowest index is lower or equal to the current index, everything is fine and we can continue
62+
if lowest_desired_index <= index:
63+
continue
64+
65+
# If the lowest index is greater than the current index, we need to swap the item with the item at the lowest index
66+
item_to_swap = ordered_items[lowest_desired_index]
67+
68+
# We cannot just swap thew though - we also need to check that the item being swapped in is not dependent on the item being swapped out
69+
#
70+
# We do this by checking the lowest desired index for the item being swapped in,
71+
# and if it is greater than the current index, we fail
72+
#
73+
# In other words, if the item we want to swap with the current item is dependent on the current item,
74+
# we cannot swap them because we have a cycle
75+
lowest_desired_index_for_item_to_swap = _lowest_desired_index(
76+
item_to_swap, ordered_items
77+
)
78+
79+
if lowest_desired_index_for_item_to_swap > index:
80+
fail(
81+
"Cannot create launch sequence: Item {} <-> {}".format(
82+
item.id, item_to_swap.id
83+
)
84+
)
85+
86+
ordered_items[index] = item_to_swap
87+
ordered_items[lowest_desired_index] = item
88+
89+
return ordered_items
90+
91+
__self_ref[0] = struct(
92+
add=add,
93+
sequence=sequence,
94+
)
95+
96+
return __self()
97+
98+
99+
# Launches a scheule by executing each item in the order determined by the schedule.
100+
def launch(plan, schedule):
101+
items = schedule.sequence()
102+
launched = {}
103+
104+
for item in items:
105+
missing_dependencies = [id for id in item.dependencies if id not in launched]
106+
if missing_dependencies:
107+
fail(
108+
"schedule: Launch error: Missing dependencies {} for item {}".format(
109+
",".join(missing_dependencies),
110+
item.id,
111+
)
112+
)
113+
114+
launched[item.id] = item.launch(plan, launched)
115+
116+
return launched
117+
118+
119+
def item(id, launch, dependencies=[]):
120+
return _assert_item(
121+
struct(
122+
id=id,
123+
launch=launch,
124+
dependencies=dependencies,
125+
)
126+
)
127+
128+
129+
def _lowest_desired_index(item, items):
130+
items_without_item = list(items)
131+
items_without_item.remove(item)
132+
133+
for index in range(len(items)):
134+
previous_items = items_without_item[:index]
135+
previous_ids = [i.id for i in previous_items]
136+
137+
missing_dependencies = [
138+
id for id in item.dependencies if id not in previous_ids
139+
]
140+
141+
if not missing_dependencies:
142+
return index
143+
144+
145+
def _assert_item(item):
146+
type_of_item = type(item)
147+
if type_of_item != "struct":
148+
fail(
149+
"schedule: Expected an item to be a struct, got {} of type {}".format(
150+
item, type_of_item
151+
)
152+
)
153+
154+
if not hasattr(item, "dependencies"):
155+
fail(
156+
"schedule: Expected an item to have a property 'dependencies', got {}".format(
157+
item, type_of_item
158+
)
159+
)
160+
161+
type_of_dependencies = type(item.dependencies)
162+
if type_of_dependencies != "list":
163+
fail(
164+
"schedule: Expected an item to have a 'dependencies' property of type list but 'dependencies' is of type".format(
165+
type_of_dependencies
166+
)
167+
)
168+
169+
has_self_as_dependency = item.id in item.dependencies
170+
if has_self_as_dependency:
171+
fail("schedule: Item {} specifies itself as its dependency".format(item.id))
172+
173+
if not hasattr(item, "id"):
174+
fail(
175+
"schedule: Expected an item to have a property 'id', got {}".format(
176+
item, type_of_item
177+
)
178+
)
179+
180+
if not hasattr(item, "launch"):
181+
fail(
182+
"schedule: Expected an item to have a property 'launch', got {}".format(
183+
item, type_of_item
184+
)
185+
)
186+
187+
type_of_launch = type(item.launch)
188+
if type_of_launch != "function":
189+
fail(
190+
"schedule: Expected an item to have a 'launch' function but 'launch' is of type".format(
191+
type_of_launch
192+
)
193+
)
194+
195+
return item

test/util/schedule_test.star

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
_schedule = import_module("/src/util/schedule.star")
2+
3+
4+
def _default_launch():
5+
return None
6+
7+
8+
def test_util_schedule_dependency_on_self(plan):
9+
schedule = _schedule.create()
10+
11+
# We check whether the item() utility function catches this
12+
expect.fails(
13+
lambda: _schedule.item(id="a", launch=_default_launch, dependencies=["a"]),
14+
"schedule: Item a specifies itself as its dependency",
15+
)
16+
17+
# And whether the schedule.add() function catches this
18+
expect.fails(
19+
lambda: schedule.add(
20+
struct(id="a", launch=_default_launch, dependencies=["a"])
21+
),
22+
"schedule: Item a specifies itself as its dependency",
23+
)
24+
25+
26+
def test_util_schedule_no_dependencies(plan):
27+
schedule = _schedule.create()
28+
29+
item_a = _schedule.item(id="a", launch=_default_launch)
30+
item_b = _schedule.item(id="b", launch=_default_launch)
31+
32+
schedule.add(item_b)
33+
schedule.add(item_a)
34+
35+
expect.eq(schedule.sequence(), [item_b, item_a])
36+
37+
38+
def test_util_schedule_simple_linear_dependencies(plan):
39+
schedule = _schedule.create()
40+
41+
item_a = _schedule.item(id="a", launch=_default_launch)
42+
item_b = _schedule.item(id="b", launch=_default_launch, dependencies=["a"])
43+
44+
schedule.add(item_b)
45+
schedule.add(item_a)
46+
47+
expect.eq(schedule.sequence(), [item_a, item_b])
48+
49+
50+
def test_util_schedule_simple_simple_cycle_dependencies(plan):
51+
schedule = _schedule.create()
52+
53+
item_a = _schedule.item(id="a", launch=_default_launch, dependencies=["b"])
54+
item_b = _schedule.item(id="b", launch=_default_launch, dependencies=["a"])
55+
56+
schedule.add(item_b)
57+
schedule.add(item_a)
58+
59+
expect.fails(
60+
lambda: schedule.sequence(), "Cannot create launch sequence: Item b <-> a"
61+
)
62+
63+
64+
def test_util_schedule_simple_large_cycle_dependencies(plan):
65+
schedule = _schedule.create()
66+
67+
item_a = _schedule.item(id="a", launch=_default_launch, dependencies=["d"])
68+
item_b = _schedule.item(id="b", launch=_default_launch, dependencies=["a"])
69+
item_c = _schedule.item(id="c", launch=_default_launch, dependencies=["b"])
70+
item_d = _schedule.item(id="d", launch=_default_launch, dependencies=["a"])
71+
72+
schedule.add(item_b)
73+
schedule.add(item_a)
74+
schedule.add(item_c)
75+
schedule.add(item_d)
76+
77+
expect.fails(
78+
lambda: schedule.sequence(), "Cannot create launch sequence: Item b <-> a"
79+
)
80+
81+
82+
def test_util_schedule_simple_branching_dependencies(plan):
83+
schedule = _schedule.create()
84+
85+
item_a = _schedule.item(id="a", launch=_default_launch)
86+
item_b = _schedule.item(id="b", launch=_default_launch)
87+
item_c1 = _schedule.item(id="c1", launch=_default_launch, dependencies=["b"])
88+
item_c2 = _schedule.item(id="c2", launch=_default_launch, dependencies=["b"])
89+
item_c21 = _schedule.item(id="c21", launch=_default_launch, dependencies=["c2"])
90+
item_c22 = _schedule.item(id="c22", launch=_default_launch, dependencies=["c21"])
91+
item_c3 = _schedule.item(id="c3", launch=_default_launch, dependencies=["b"])
92+
item_d = _schedule.item(
93+
id="d", launch=_default_launch, dependencies=["c1", "c22", "c3"]
94+
)
95+
96+
schedule.add(item_b)
97+
schedule.add(item_c1)
98+
schedule.add(item_d)
99+
schedule.add(item_c21)
100+
schedule.add(item_c22)
101+
schedule.add(item_a)
102+
schedule.add(item_c3)
103+
schedule.add(item_c2)
104+
105+
expect.eq(
106+
schedule.sequence(),
107+
[item_b, item_c1, item_c3, item_c2, item_c21, item_a, item_c22, item_d],
108+
)

0 commit comments

Comments
 (0)