From 6bdc760f726ebfcb8a3369c757edbb66fa372c7a Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Sun, 8 Jun 2025 21:37:29 -0700 Subject: [PATCH 1/8] chore: Schedule --- src/util/schedule.star | 195 +++++++++++++++++++++++++++++++++++ test/util/schedule_test.star | 108 +++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 src/util/schedule.star create mode 100644 test/util/schedule_test.star diff --git a/src/util/schedule.star b/src/util/schedule.star new file mode 100644 index 00000000..cb6a6a06 --- /dev/null +++ b/src/util/schedule.star @@ -0,0 +1,195 @@ +def create(): + __self_ref = [None] + __items_by_id = {} + + def __self(): + return __self_ref[0] + + def add(*items): + for item in items: + _assert_item(item) + + if __items_by_id.get(item.id): + fail("Failed to add item {}: item with the same ID already exists") + + __items_by_id[item.id] = item + + return __self() + + # This function returns the items in the order they should be launched + # based on their dependencies. + # + # It will try to preserve the order in which the items were added, + # only reordering them if necessary to satisfy the dependencies. + # + # If there are any cycles in the dependencies, it will fail. + # If there are any missing dependencies, it will also fail. + def sequence(): + # First we check whether we have all the items + all_dependency_ids = [ + dependency + for item in __items_by_id.values() + for dependency in item.dependencies + ] + + # Now check we have all of them + missing_dependency_ids = [ + id for id in all_dependency_ids if id not in __items_by_id + ] + if missing_dependency_ids: + fail( + "Failed to launch: Missing items {}".format( + ",".join(missing_dependency_ids) + ) + ) + + # Now we have to order the items based on their dependencies + # + # First we start with the default sequence - the order in which the items were added + ordered_items = __items_by_id.values() + num_items = len(ordered_items) + + for index in range(num_items): + item = ordered_items[index] + + # Since we are not allowed any unbound loops, we'll have to resort to somewhat different strategy + # + # We will calculate the lowest index at which this item can be placed + # based on its dependencies. + lowest_desired_index = _lowest_desired_index(item, ordered_items) + + # If the lowest index is lower or equal to the current index, everything is fine and we can continue + if lowest_desired_index <= index: + continue + + # If the lowest index is greater than the current index, we need to swap the item with the item at the lowest index + item_to_swap = ordered_items[lowest_desired_index] + + # 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 + # + # We do this by checking the lowest desired index for the item being swapped in, + # and if it is greater than the current index, we fail + # + # In other words, if the item we want to swap with the current item is dependent on the current item, + # we cannot swap them because we have a cycle + lowest_desired_index_for_item_to_swap = _lowest_desired_index( + item_to_swap, ordered_items + ) + + if lowest_desired_index_for_item_to_swap > index: + fail( + "Cannot create launch sequence: Item {} <-> {}".format( + item.id, item_to_swap.id + ) + ) + + ordered_items[index] = item_to_swap + ordered_items[lowest_desired_index] = item + + return ordered_items + + __self_ref[0] = struct( + add=add, + sequence=sequence, + ) + + return __self() + + +# Launches a scheule by executing each item in the order determined by the schedule. +def launch(plan, schedule): + items = schedule.sequence() + launched = {} + + for item in items: + missing_dependencies = [id for id in item.dependencies if id not in launched] + if missing_dependencies: + fail( + "schedule: Launch error: Missing dependencies {} for item {}".format( + ",".join(missing_dependencies), + item.id, + ) + ) + + launched[item.id] = item.launch(plan, launched) + + return launched + + +def item(id, launch, dependencies=[]): + return _assert_item( + struct( + id=id, + launch=launch, + dependencies=dependencies, + ) + ) + + +def _lowest_desired_index(item, items): + items_without_item = list(items) + items_without_item.remove(item) + + for index in range(len(items)): + previous_items = items_without_item[:index] + previous_ids = [i.id for i in previous_items] + + missing_dependencies = [ + id for id in item.dependencies if id not in previous_ids + ] + + if not missing_dependencies: + return index + + +def _assert_item(item): + type_of_item = type(item) + if type_of_item != "struct": + fail( + "schedule: Expected an item to be a struct, got {} of type {}".format( + item, type_of_item + ) + ) + + if not hasattr(item, "dependencies"): + fail( + "schedule: Expected an item to have a property 'dependencies', got {}".format( + item, type_of_item + ) + ) + + type_of_dependencies = type(item.dependencies) + if type_of_dependencies != "list": + fail( + "schedule: Expected an item to have a 'dependencies' property of type list but 'dependencies' is of type".format( + type_of_dependencies + ) + ) + + has_self_as_dependency = item.id in item.dependencies + if has_self_as_dependency: + fail("schedule: Item {} specifies itself as its dependency".format(item.id)) + + if not hasattr(item, "id"): + fail( + "schedule: Expected an item to have a property 'id', got {}".format( + item, type_of_item + ) + ) + + if not hasattr(item, "launch"): + fail( + "schedule: Expected an item to have a property 'launch', got {}".format( + item, type_of_item + ) + ) + + type_of_launch = type(item.launch) + if type_of_launch != "function": + fail( + "schedule: Expected an item to have a 'launch' function but 'launch' is of type".format( + type_of_launch + ) + ) + + return item diff --git a/test/util/schedule_test.star b/test/util/schedule_test.star new file mode 100644 index 00000000..dfa35179 --- /dev/null +++ b/test/util/schedule_test.star @@ -0,0 +1,108 @@ +_schedule = import_module("/src/util/schedule.star") + + +def _default_launch(): + return None + + +def test_util_schedule_dependency_on_self(plan): + schedule = _schedule.create() + + # We check whether the item() utility function catches this + expect.fails( + lambda: _schedule.item(id="a", launch=_default_launch, dependencies=["a"]), + "schedule: Item a specifies itself as its dependency", + ) + + # And whether the schedule.add() function catches this + expect.fails( + lambda: schedule.add( + struct(id="a", launch=_default_launch, dependencies=["a"]) + ), + "schedule: Item a specifies itself as its dependency", + ) + + +def test_util_schedule_no_dependencies(plan): + schedule = _schedule.create() + + item_a = _schedule.item(id="a", launch=_default_launch) + item_b = _schedule.item(id="b", launch=_default_launch) + + schedule.add(item_b) + schedule.add(item_a) + + expect.eq(schedule.sequence(), [item_b, item_a]) + + +def test_util_schedule_simple_linear_dependencies(plan): + schedule = _schedule.create() + + item_a = _schedule.item(id="a", launch=_default_launch) + item_b = _schedule.item(id="b", launch=_default_launch, dependencies=["a"]) + + schedule.add(item_b) + schedule.add(item_a) + + expect.eq(schedule.sequence(), [item_a, item_b]) + + +def test_util_schedule_simple_simple_cycle_dependencies(plan): + schedule = _schedule.create() + + item_a = _schedule.item(id="a", launch=_default_launch, dependencies=["b"]) + item_b = _schedule.item(id="b", launch=_default_launch, dependencies=["a"]) + + schedule.add(item_b) + schedule.add(item_a) + + expect.fails( + lambda: schedule.sequence(), "Cannot create launch sequence: Item b <-> a" + ) + + +def test_util_schedule_simple_large_cycle_dependencies(plan): + schedule = _schedule.create() + + item_a = _schedule.item(id="a", launch=_default_launch, dependencies=["d"]) + item_b = _schedule.item(id="b", launch=_default_launch, dependencies=["a"]) + item_c = _schedule.item(id="c", launch=_default_launch, dependencies=["b"]) + item_d = _schedule.item(id="d", launch=_default_launch, dependencies=["a"]) + + schedule.add(item_b) + schedule.add(item_a) + schedule.add(item_c) + schedule.add(item_d) + + expect.fails( + lambda: schedule.sequence(), "Cannot create launch sequence: Item b <-> a" + ) + + +def test_util_schedule_simple_branching_dependencies(plan): + schedule = _schedule.create() + + item_a = _schedule.item(id="a", launch=_default_launch) + item_b = _schedule.item(id="b", launch=_default_launch) + item_c1 = _schedule.item(id="c1", launch=_default_launch, dependencies=["b"]) + item_c2 = _schedule.item(id="c2", launch=_default_launch, dependencies=["b"]) + item_c21 = _schedule.item(id="c21", launch=_default_launch, dependencies=["c2"]) + item_c22 = _schedule.item(id="c22", launch=_default_launch, dependencies=["c21"]) + item_c3 = _schedule.item(id="c3", launch=_default_launch, dependencies=["b"]) + item_d = _schedule.item( + id="d", launch=_default_launch, dependencies=["c1", "c22", "c3"] + ) + + schedule.add(item_b) + schedule.add(item_c1) + schedule.add(item_d) + schedule.add(item_c21) + schedule.add(item_c22) + schedule.add(item_a) + schedule.add(item_c3) + schedule.add(item_c2) + + expect.eq( + schedule.sequence(), + [item_b, item_c1, item_c3, item_c2, item_c21, item_a, item_c22, item_d], + ) From 4bde0844af841df80bffd2c60978b70d9ce50155 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 9 Jun 2025 09:26:44 -0700 Subject: [PATCH 2/8] chore: Tests --- src/util/schedule.star | 38 ++++++--- test/util/schedule_test.star | 147 ++++++++++++++++++++++++++++++++++- 2 files changed, 174 insertions(+), 11 deletions(-) diff --git a/src/util/schedule.star b/src/util/schedule.star index cb6a6a06..6ccfcd0e 100644 --- a/src/util/schedule.star +++ b/src/util/schedule.star @@ -111,7 +111,7 @@ def launch(plan, schedule): ) ) - launched[item.id] = item.launch(plan, launched) + launched[item.id] = item.launch(plan=plan, dependencies=launched) return launched @@ -151,6 +151,21 @@ def _assert_item(item): ) ) + if not hasattr(item, "id"): + fail( + "schedule: Expected an item to have a property 'id', got {}".format( + item, type_of_item + ) + ) + + type_of_id = type(item.id) + if type_of_id != "string": + fail( + "schedule: Expected an item to have an 'id' of type string but 'id' is of type {}".format( + type_of_id + ) + ) + if not hasattr(item, "dependencies"): fail( "schedule: Expected an item to have a property 'dependencies', got {}".format( @@ -161,22 +176,25 @@ def _assert_item(item): type_of_dependencies = type(item.dependencies) if type_of_dependencies != "list": fail( - "schedule: Expected an item to have a 'dependencies' property of type list but 'dependencies' is of type".format( + "schedule: Expected an item to have a 'dependencies' property of type list but 'dependencies' is of type {}".format( type_of_dependencies ) ) - has_self_as_dependency = item.id in item.dependencies - if has_self_as_dependency: - fail("schedule: Item {} specifies itself as its dependency".format(item.id)) - - if not hasattr(item, "id"): + mistyped_dependencies = [d for d in item.dependencies if type(d) != "string"] + if mistyped_dependencies: fail( - "schedule: Expected an item to have a property 'id', got {}".format( - item, type_of_item + "schedule: Expected an item to have a 'dependencies' property of type list of strings but 'dependencies' contains {}".format( + ", ".join( + ["{} of type {}".format(d, type(d)) for d in mistyped_dependencies] + ) ) ) + has_self_as_dependency = item.id in item.dependencies + if has_self_as_dependency: + fail("schedule: Item {} specifies itself as its dependency".format(item.id)) + if not hasattr(item, "launch"): fail( "schedule: Expected an item to have a property 'launch', got {}".format( @@ -187,7 +205,7 @@ def _assert_item(item): type_of_launch = type(item.launch) if type_of_launch != "function": fail( - "schedule: Expected an item to have a 'launch' function but 'launch' is of type".format( + "schedule: Expected an item to have a 'launch' property of type function but 'launch' is of type {}".format( type_of_launch ) ) diff --git a/test/util/schedule_test.star b/test/util/schedule_test.star index dfa35179..9d40a8d5 100644 --- a/test/util/schedule_test.star +++ b/test/util/schedule_test.star @@ -1,10 +1,58 @@ _schedule = import_module("/src/util/schedule.star") -def _default_launch(): +def _default_launch(plan, dependencies): return None +def test_util_schedule_dependency_invalid_item(plan): + schedule = _schedule.create() + + # We check for a missing id + expect.fails( + lambda: schedule.add(struct()), + "schedule: Expected an item to have a property 'id'", + ) + + # We check for a mistyped id + expect.fails( + lambda: schedule.add(struct(id=123)), + "schedule: Expected an item to have an 'id' of type string but 'id' is of type int", + ) + + # We check for missing dependencies + expect.fails( + lambda: schedule.add(struct(id="a", launch=_default_launch)), + "schedule: Expected an item to have a property 'dependencies'", + ) + + # We check for mistyped dependencies + expect.fails( + lambda: schedule.add(struct(id="a", launch=_default_launch, dependencies="b")), + "schedule: Expected an item to have a 'dependencies' property of type list but 'dependencies' is of type string", + ) + + # We check for mistyped dependencies + expect.fails( + lambda: schedule.add( + struct(id="a", launch=_default_launch, dependencies=[123, [], {}, False]) + ), + "schedule: Expected an item to have a 'dependencies' property of type list of strings but 'dependencies' contains 123 of type int, \\[\\] of type list, \\{\\} of type dict, False of type bool", + ) + + # We check for missing launch + expect.fails( + lambda: schedule.add(struct(id="a", dependencies=["b"])), + "schedule: Expected an item to have a property 'launch'", + ) + + # We check for mistyped launch + expect.fails( + lambda: schedule.add(struct(id="a", launch=123, dependencies=["b"])), + "schedule: Expected an item to have a 'launch' property of type function but 'launch' is of type int", + ) + + def test_util_schedule_dependency_on_self(plan): schedule = _schedule.create() @@ -106,3 +154,100 @@ def test_util_schedule_simple_branching_dependencies(plan): schedule.sequence(), [item_b, item_c1, item_c3, item_c2, item_c21, item_a, item_c22, item_d], ) + + +def test_util_schedule_launch_empty(plan): + schedule = _schedule.create() + + # Launching an empty schedule should return an empty dict + expect.eq(_schedule.launch(plan, schedule), {}) + + +def test_util_schedule_launch_simple(plan): + schedule = _schedule.create() + + schedule.add( + _schedule.item( + id="a", + launch=lambda plan, dependencies: "a launched with dependencies {}".format( + dependencies + ), + ) + ) + schedule.add( + _schedule.item( + id="b", + launch=lambda plan, dependencies: "b launched with dependencies {}".format( + dependencies + ), + dependencies=["a"], + ) + ) + + expect.eq( + _schedule.launch(plan, schedule), + { + "a": "a launched with dependencies {}", + "b": 'b launched with dependencies {"a": "a launched with dependencies {}"}', + }, + ) + + +def test_util_schedule_launch_branching(plan): + schedule = _schedule.create() + + schedule.add( + _schedule.item( + id="a", + launch=lambda plan, dependencies: "a launched with dependencies {}".format( + dependencies + ), + ) + ) + schedule.add( + _schedule.item( + id="b", + launch=lambda plan, dependencies: "b launched with dependencies {}".format( + dependencies + ), + dependencies=["a"], + ) + ) + schedule.add( + _schedule.item( + id="c1", + launch=lambda plan, dependencies: "c1 launched with dependencies {}".format( + dependencies + ), + dependencies=["b"], + ) + ) + schedule.add( + _schedule.item( + id="c2", + launch=lambda plan, dependencies: "c2 launched with dependencies {}".format( + dependencies + ), + dependencies=["b"], + ) + ) + schedule.add( + _schedule.item( + id="d", + launch=lambda plan, dependencies: "d launched with dependencies {}".format( + dependencies + ), + dependencies=["c1", "c2"], + ) + ) + + expect.eq( + _schedule.launch(plan, schedule), + { + "a": "a launched with dependencies {}", + "b": 'b launched with dependencies {"a": "a launched with dependencies {}"}', + "c1": 'c1 launched with dependencies {"a": "a launched with dependencies {}", "b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}"}', + "c2": 'c2 launched with dependencies {"a": "a launched with dependencies {}", "b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}", "c1": "c1 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\"}"}', + "d": 'd launched with dependencies {"a": "a launched with dependencies {}", "b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}", "c1": "c1 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\"}", "c2": "c2 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\", \\"c1\\": \\"c1 launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\", \\\\\\"b\\\\\\": \\\\\\"b launched with dependencies {\\\\\\\\\\\\\\"a\\\\\\\\\\\\\\": \\\\\\\\\\\\\\"a launched with dependencies {}\\\\\\\\\\\\\\"}\\\\\\"}\\"}"}', + }, + ) From 82d69c09159f6232a95dd640fa4283b786c27a96 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 10 Jun 2025 10:43:48 -0700 Subject: [PATCH 3/8] chore: Only pass explicit dependencies --- src/util/schedule.star | 5 ++++- test/util/schedule_test.star | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/util/schedule.star b/src/util/schedule.star index 6ccfcd0e..356c9be0 100644 --- a/src/util/schedule.star +++ b/src/util/schedule.star @@ -111,7 +111,10 @@ def launch(plan, schedule): ) ) - launched[item.id] = item.launch(plan=plan, dependencies=launched) + # We will always only pass the explicitly defined dependencies + item_dependencies = {id: launched[id] for id in item.dependencies} + + launched[item.id] = item.launch(plan=plan, dependencies=item_dependencies) return launched diff --git a/test/util/schedule_test.star b/test/util/schedule_test.star index 9d40a8d5..60c78a88 100644 --- a/test/util/schedule_test.star +++ b/test/util/schedule_test.star @@ -251,3 +251,39 @@ def test_util_schedule_launch_branching(plan): "d": 'd launched with dependencies {"a": "a launched with dependencies {}", "b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}", "c1": "c1 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\"}", "c2": "c2 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\", \\"c1\\": \\"c1 launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\", \\\\\\"b\\\\\\": \\\\\\"b launched with dependencies {\\\\\\\\\\\\\\"a\\\\\\\\\\\\\\": \\\\\\\\\\\\\\"a launched with dependencies {}\\\\\\\\\\\\\\"}\\\\\\"}\\"}"}', }, ) + + +def test_util_schedule_launch_no_implicit_dependencies(plan): + schedule = _schedule.create() + + schedule.add( + _schedule.item( + id="a", + launch=lambda plan, dependencies: expect.eq(dependencies, {}), + ) + ) + + schedule.add( + _schedule.item( + id="b", + launch=lambda plan, dependencies: expect.eq(dependencies, {"a": None}), + dependencies=["a"], + ) + ) + + schedule.add( + _schedule.item( + id="c", + launch=lambda plan, dependencies: expect.eq(dependencies, {"b": None}), + dependencies=["b"], + ) + ) + + expect.eq( + _schedule.launch(plan, schedule), + { + "a": None, + "b": None, + "c": None, + }, + ) From 8a7be1c8c732b861bbc913e7b3cfc6ef4ba17920 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 10 Jun 2025 10:46:21 -0700 Subject: [PATCH 4/8] fix: Test --- test/util/schedule_test.star | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/util/schedule_test.star b/test/util/schedule_test.star index 60c78a88..4105efbc 100644 --- a/test/util/schedule_test.star +++ b/test/util/schedule_test.star @@ -246,9 +246,9 @@ def test_util_schedule_launch_branching(plan): { "a": "a launched with dependencies {}", "b": 'b launched with dependencies {"a": "a launched with dependencies {}"}', - "c1": 'c1 launched with dependencies {"a": "a launched with dependencies {}", "b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}"}', - "c2": 'c2 launched with dependencies {"a": "a launched with dependencies {}", "b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}", "c1": "c1 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\"}"}', - "d": 'd launched with dependencies {"a": "a launched with dependencies {}", "b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}", "c1": "c1 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\"}", "c2": "c2 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\", \\"c1\\": \\"c1 launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\", \\\\\\"b\\\\\\": \\\\\\"b launched with dependencies {\\\\\\\\\\\\\\"a\\\\\\\\\\\\\\": \\\\\\\\\\\\\\"a launched with dependencies {}\\\\\\\\\\\\\\"}\\\\\\"}\\"}"}', + "c1": 'c1 launched with dependencies {"b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}"}', + "c2": 'c2 launched with dependencies {"b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}", "c1": "c1 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\"}"}', + "d": 'd launched with dependencies {"c1": "c1 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\"}", "c2": "c2 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\", \\"c1\\": \\"c1 launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\", \\\\\\"b\\\\\\": \\\\\\"b launched with dependencies {\\\\\\\\\\\\\\"a\\\\\\\\\\\\\\": \\\\\\\\\\\\\\"a launched with dependencies {}\\\\\\\\\\\\\\"}\\\\\\"}\\"}"}', }, ) From 9ee0684f71b3964dcf90ffe45731f7f1e89b0dcc Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 10 Jun 2025 10:50:45 -0700 Subject: [PATCH 5/8] fix: Test --- test/util/schedule_test.star | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/util/schedule_test.star b/test/util/schedule_test.star index 4105efbc..0ac6d272 100644 --- a/test/util/schedule_test.star +++ b/test/util/schedule_test.star @@ -200,7 +200,7 @@ def test_util_schedule_launch_branching(plan): _schedule.item( id="a", launch=lambda plan, dependencies: "a launched with dependencies {}".format( - dependencies + ",".join(dependencies.keys()) ), ) ) @@ -208,7 +208,7 @@ def test_util_schedule_launch_branching(plan): _schedule.item( id="b", launch=lambda plan, dependencies: "b launched with dependencies {}".format( - dependencies + ",".join(dependencies.keys()) ), dependencies=["a"], ) @@ -217,7 +217,7 @@ def test_util_schedule_launch_branching(plan): _schedule.item( id="c1", launch=lambda plan, dependencies: "c1 launched with dependencies {}".format( - dependencies + ",".join(dependencies.keys()) ), dependencies=["b"], ) @@ -226,7 +226,7 @@ def test_util_schedule_launch_branching(plan): _schedule.item( id="c2", launch=lambda plan, dependencies: "c2 launched with dependencies {}".format( - dependencies + ",".join(dependencies.keys()) ), dependencies=["b"], ) @@ -235,7 +235,7 @@ def test_util_schedule_launch_branching(plan): _schedule.item( id="d", launch=lambda plan, dependencies: "d launched with dependencies {}".format( - dependencies + ",".join(dependencies.keys()) ), dependencies=["c1", "c2"], ) @@ -244,11 +244,11 @@ def test_util_schedule_launch_branching(plan): expect.eq( _schedule.launch(plan, schedule), { - "a": "a launched with dependencies {}", - "b": 'b launched with dependencies {"a": "a launched with dependencies {}"}', - "c1": 'c1 launched with dependencies {"b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}"}', - "c2": 'c2 launched with dependencies {"b": "b launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\"}", "c1": "c1 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\"}"}', - "d": 'd launched with dependencies {"c1": "c1 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\"}", "c2": "c2 launched with dependencies {\\"a\\": \\"a launched with dependencies {}\\", \\"b\\": \\"b launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\"}\\", \\"c1\\": \\"c1 launched with dependencies {\\\\\\"a\\\\\\": \\\\\\"a launched with dependencies {}\\\\\\", \\\\\\"b\\\\\\": \\\\\\"b launched with dependencies {\\\\\\\\\\\\\\"a\\\\\\\\\\\\\\": \\\\\\\\\\\\\\"a launched with dependencies {}\\\\\\\\\\\\\\"}\\\\\\"}\\"}"}', + "a": "a launched with dependencies ", + "b": "b launched with dependencies a", + "c1": "c1 launched with dependencies b", + "c2": "c2 launched with dependencies b", + "d": "d launched with dependencies c1,c2", }, ) From 64332decdeae43b1d4560de44f7c28fa3970d63d Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 10 Jun 2025 11:27:41 -0700 Subject: [PATCH 6/8] chore: Add update functionality --- src/util/schedule.star | 38 ++++++++++++++++++- test/util/schedule_test.star | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/util/schedule.star b/src/util/schedule.star index 356c9be0..0e1f81dd 100644 --- a/src/util/schedule.star +++ b/src/util/schedule.star @@ -10,12 +10,46 @@ def create(): _assert_item(item) if __items_by_id.get(item.id): - fail("Failed to add item {}: item with the same ID already exists") + fail( + "schedule: Failed to add item {}: item with the same ID already exists".format( + item.id + ) + ) __items_by_id[item.id] = item return __self() + def update(id, updater): + if id not in __items_by_id: + fail("schedule: Failed to update item {}: item does not exist".format(id)) + + # We rigorously ensure that all is well because the errors from here would not very readable + type_of_updater = type(updater) + if type_of_updater != "function": + fail( + "schedule: Failed to update item {}: expected 'updater' to be of type function but 'updater' is of type {}".format( + id, type_of_updater + ) + ) + + item = __items_by_id[id] + updated_item = _assert_item(updater(item=item)) + + if updated_item.id != item.id: + fail( + "schedule: Failed to update item {}: updater changed the ID from {} to {}".format( + id, item.id, updated_item.id + ) + ) + + __items_by_id[id] = updated_item + + return __self() + + def items(): + return __items_by_id.values() + # This function returns the items in the order they should be launched # based on their dependencies. # @@ -90,6 +124,8 @@ def create(): __self_ref[0] = struct( add=add, + update=update, + items=items, sequence=sequence, ) diff --git a/test/util/schedule_test.star b/test/util/schedule_test.star index 0ac6d272..fa7b7c5a 100644 --- a/test/util/schedule_test.star +++ b/test/util/schedule_test.star @@ -5,6 +5,10 @@ def _default_launch(plan, dependencies): return None +def _default_updater(item): + return item + + def test_util_schedule_dependency_invalid_item(plan): schedule = _schedule.create() @@ -71,6 +75,73 @@ def test_util_schedule_dependency_on_self(plan): ) +def test_util_schedule_add_duplicate_id(plan): + schedule = _schedule.create() + + item_a = _schedule.item(id="a", launch=_default_launch) + schedule.add(item_a) + + # Adding an item with the same id should fail + expect.fails( + lambda: schedule.add(item_a), + "schedule: Failed to add item a: item with the same ID already exists", + ) + + +def test_util_schedule_update_missing_item(plan): + schedule = _schedule.create() + + expect.fails( + lambda: schedule.update(id="a", updater=_default_updater), + "schedule: Failed to update item a: item does not exist", + ) + + +def test_util_schedule_update_invalid_updater(plan): + schedule = _schedule.create() + + item_a = _schedule.item(id="a", launch=_default_launch) + schedule.add(item_a) + + expect.fails( + lambda: schedule.update(id="a", updater=123), + "schedule: Failed to update item a: expected 'updater' to be of type function but 'updater' is of type int", + ) + + +def test_util_schedule_update_success(plan): + schedule = _schedule.create() + + item_a = _schedule.item(id="a", launch=_default_launch) + item_b = _schedule.item(id="b", launch=_default_launch, dependencies=["a"]) + item_c = _schedule.item(id="c", launch=_default_launch, dependencies=["a"]) + + schedule.add(item_a) + schedule.add(item_b) + schedule.add(item_c) + + expect.eq(schedule.sequence(), [item_a, item_b, item_c]) + + updated_item_b = _schedule.item(id="b", launch=_default_launch, dependencies=["c"]) + schedule.update(id="b", updater=lambda item: updated_item_b) + + expect.eq(schedule.sequence(), [item_a, item_c, updated_item_b]) + + +def test_util_schedule_update_changed_id(plan): + schedule = _schedule.create() + + item_a = _schedule.item(id="a", launch=_default_launch) + schedule.add(item_a) + + expect.fails( + lambda: schedule.update( + id="a", updater=lambda item: _schedule.item(id="b", launch=_default_launch) + ), + "schedule: Failed to update item a: updater changed the ID from a to b", + ) + + def test_util_schedule_no_dependencies(plan): schedule = _schedule.create() From a4d45a12a5b20102258ef52e12dc01ca93935f66 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 10 Jun 2025 11:31:41 -0700 Subject: [PATCH 7/8] chore: Comments --- src/util/schedule.star | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/util/schedule.star b/src/util/schedule.star index 0e1f81dd..9f886e56 100644 --- a/src/util/schedule.star +++ b/src/util/schedule.star @@ -5,6 +5,12 @@ def create(): def __self(): return __self_ref[0] + # Adds an item to the schedule. + # + # This function will fail if the item already exists in the schedule, + # or if the item is not valid. + # + # It will return the schedule itself so that it can be chained. def add(*items): for item in items: _assert_item(item) @@ -20,6 +26,15 @@ def create(): return __self() + # Updates an item in the schedule. + # + # This is useful for inserting launch steps into the schedule + # after the item has been added by changing the dependencies of the item. + # + # This function will fail if the item does not exist in the schedule, + # or if the updater is not a function, or if the updater changes the ID of the item. + # + # It will return the schedule itself so that it can be chained. def update(id, updater): if id not in __items_by_id: fail("schedule: Failed to update item {}: item does not exist".format(id)) @@ -47,6 +62,7 @@ def create(): return __self() + # Returns all the items in the schedule in an unspecified order def items(): return __items_by_id.values() From 0a9f1980f98a75e6ca24d74a5bdd0adfe74871ff Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 10 Jun 2025 11:58:30 -0700 Subject: [PATCH 8/8] fix: Lint --- src/util/schedule.star | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util/schedule.star b/src/util/schedule.star index 9f886e56..47559180 100644 --- a/src/util/schedule.star +++ b/src/util/schedule.star @@ -6,10 +6,10 @@ def create(): return __self_ref[0] # Adds an item to the schedule. - # + # # This function will fail if the item already exists in the schedule, # or if the item is not valid. - # + # # It will return the schedule itself so that it can be chained. def add(*items): for item in items: @@ -27,10 +27,10 @@ def create(): return __self() # Updates an item in the schedule. - # + # # This is useful for inserting launch steps into the schedule # after the item has been added by changing the dependencies of the item. - # + # # This function will fail if the item does not exist in the schedule, # or if the updater is not a function, or if the updater changes the ID of the item. #