From b043c55f731c8dd3a91b7e0be9559f83c20e76ac Mon Sep 17 00:00:00 2001 From: "P.V.Anita" Date: Mon, 6 Dec 2021 13:59:04 +0100 Subject: [PATCH] modified: gumby/scenario.py Remove scenario language for_loop feature for_loops code has been removed from scenario.py. for_loops documentation and testscript have been removed. --- docs/advanced_scenario_concepts.rst | 129 ----------- gumby/scenario.py | 79 +------ .../tests/scenario_language/test_for_loop.py | 213 ------------------ 3 files changed, 5 insertions(+), 416 deletions(-) delete mode 100644 gumby/tests/scenario_language/test_for_loop.py diff --git a/docs/advanced_scenario_concepts.rst b/docs/advanced_scenario_concepts.rst index aacaf1ea2..3fef3d606 100644 --- a/docs/advanced_scenario_concepts.rst +++ b/docs/advanced_scenario_concepts.rst @@ -5,7 +5,6 @@ Advanced Scenario Language Concepts The document is meant to exemplify some of the more advanced features of the scenario language. Currently, these features refer to: - Support for ``variables`` -- Support for ``for`` loops They will be exemplified and presented in further detail in the sections to follow: @@ -106,131 +105,3 @@ In this example, we will have 20 active peers, 19 of which are querying each oth The variable introduces a cleaner scenario. Moreover, if further changes are required to the key, one can simply change the value once, when the ``key`` is set. Previously, if the key needed to be changed, one would have to manually go through each of its occurrences and make the required modification. -For Loops ---------- - -Often in scenario files, it might be useful to have the same operation executed multiple times by a peer, or have a single operation executed once on multiple peers. Without ``for`` loops both of these use cases are possible, but would demand quite a considerable amount of coding effort, usually requiring that one repeatedly copy-pastes the same line multiple times, making the code messy and difficult to alter when changes are required. - -This is the reason why ``for`` loops have been added to scenario files, as they facilitate the writing and maintenance effort of such experiments. - -The current implementation of ``for`` loops supports the execution of a single experiment callback. The callback can be passed the control variable as a parameter, and similarly, the control variable can be used as the peerspec of the callback. The general structure of the ``for`` loop is the following: - -``@ for in to call []* []* [{}]`` - -The following is an explanation of the elements of the above command line: - -- ```` refers to the time into the experiment when the for loop should be executed, and implicitly its associated experiment callback -- ```` refers to an alias assigned to the control variable, by which it can be referenced in the future -- ```` refers to the initial value that the ```` will take. This bound is inclusive, and need not be lower than the ```` -- ```` refers to the final value of the ````. This bound is inclusive, and need not be higher than the ````. -- ```` refers the experiment callback -- and refer to the unnamed and named parameters that the callback takes. The programmer can use the ```` as a parameter to the ````. -- ```` defines which peers should execute the ``for`` loop. - -The Control Variable -~~~~~~~~~~~~~~~~~~~~ - -The control variable can be seen as a regular variable which is visible only during the for loop. It is referenced using the same construct as normal variables: - -``$`` - -The control variable can be used as a parameter of the experiment callback. It should be mentioned however that **if there already is a declared variable with the same name as the control variable, the ``for`` loop will still work, but the variable will take precedence over the control variable when it is used as a parameter**. - -The Iteration Bounds -~~~~~~~~~~~~~~~~~~~~ - -The ```` and ```` are specified as integers, and are both inclusive. There is no mandatory relationship between the two bounds: they can be equal, or one of them can be greater than the other. - -Intuitively, if the bounds are equal, then the ``for`` loop will iterate once, and the control variable will be equal to the bounds. If, however, the `` < ``, the control variable will move in increments of 1 towards the ````, starting from the ````. If the `` > ``, the control variable will move in decrements of 1 towards the ````, again starting from the ````. - -The Peerspec -~~~~~~~~~~~~ - -Peerspec is short for *peer specification*, and generally speaking, it allows one to specify which peers should execute a callback (or on the opposite, which peers shouldn't execute it). **The ``for`` loop still allows a peerspec to be used, however, its functionality is limited**. The peerspec can only contain one element, which can be one of the following: - -- A literal which identifies a peer by its ID. In this case, the chosen peer will execute all the ``for`` loop's iterations -- The control variable. In this case, each iteration will select a peer to execute the operation, granted there is a peer with an ID that is the same as the control variable at that iteration - -As such, it is currently not possible to specify multiple literals, specify which peers should *not* execute the iterations, or specify a mixture of the aforementioned entities, together with the control variable in the peerspec of a ``for`` loop. - -Examples -~~~~~~~~ - -Let us take a closer look at some examples, which demonstrate how ``for`` loops can be used, and what are some of their limitations. - -The following is a simple example which shows a ``for`` loop where the control variable moves in increments of 1 from 1 to 10. Each peer executing the scenario will run the associated experiment callback (``do_some_work``) 10 times, since no peerspec is used to select which peers should it: - -``@0:25 for i in 1 to 10 call do_some_work`` - -The following ``for`` loop is exactly the same bar the fact that the control variable will move in decrements of 1 from 10 to 1: - -``@0:25 for i in 10 to 1 call do_some_work`` - -It should be mentioned that if we want we can make one, or both of the bounds negative - the ``for`` should still work as expected -: - -``@0:25 for i in -5 to 1 call do_some_work`` - -If we wish we can also make the bounds equal. In such a situation, there would only be one iteration, where the control variable takes on the value of the bounds: - -``@0:25 for i in 1 to 1 call do_some_work`` - -It is possible to use the control variable as an unnamed parameter, named parameter, or both, to the ``for`` loop's associated experiment callback. If we imagine that our ``do_some_work`` function has the following new definition: ``def do_some_work(self, a, b=None, c=None)``, then we could, easily use the ``for`` loop's control variable as one or more parameters to this method. Let us take a look at a possible combination: - -``@0:25 for i in 1 to 10 call do_some_work $i b=100 c=$i`` - -In this example, parameters ``a`` and ``c`` will be assigned the value of ``i``, and ``b`` will be assigned the constant ``100``. - -For loops can also have an associated peerspec. Currently, it is only possible to use either a literal or the control variable within it. An example using a literal might look like this: - -``@0:25 for i in 1 to 10 call do_some_work {1}`` - -Here, the peer with ID ``1`` will execute the ``do_some_work`` experiment callback 10 times, while any other peer will ignore the ``for`` loop. Using the control variable instead would look like this: - -``@0:25 for i in 1 to 10 call do_some_work {$i}`` - -In this case, each of the ``for`` loop iterations will each be executed by a different peer as identified by the control variable in the given iterations. - -Special Use Cases -~~~~~~~~~~~~~~~~~ - -A known special case is when the control variable's name is the same as a variable's. **The ``for`` loop should still work, but the code's behavior might be slightly different if the control variable is used as a parameter**. Let us look at an example describing this case: - -.. code-block:: none - - @! set i foo - - ... - - @10:00 for i in 1 to 100 call my_function $i {$i} - - -It might not be immediately obvious what the behavior of this ``for`` loop will be, so let us take a closer look. Initially, a variable named ``i`` is declared, and is assigned the value ``foo``. Later on in the code, a ``for`` loop is defined, having a control variable with the same name as a regular variable: ``i``. The ``for`` loop will call ``my_function`` 100 times, and it will pass ``i`` as a parameter. **In this case, the variable will take precedence over the control variable, hence, the value passed to ``my_function`` will be ``foo``**. **The control variable will take precedence over the variable in the peerspec, thus, it will filter out peers as described above**. - -Invalid Use Cases -~~~~~~~~~~~~~~~~~ - -The following examples will not work due invalid syntax: - -- ``@0:25 for i in 1 to 10 call my_function {$}`` - the peerspec may not contain the ``$`` without a variable name -- ``@0:25 for i 1 to 10 call my_function`` - invalid ``for`` loop structure -- ``@0:25 for i in 1 10 call my_function`` - invalid ``for`` loop structure -- ``@0:25 for i in 1 to call my_function`` - invalid ``for`` loop structure -- ``@0:25 for i in to 10 call my_function`` - invalid ``for`` loop structure - -The following will not work due to other issues: - -- Usage of a (non-control) variable in the peerspec: - -.. code-block:: none - - @! set j 1 - ... - @10:00 for i in 1 to 100 call my_function $i {$j} - -- Peer negation in the ``for`` loop's peerspec: - -``@10:00 for i in 1 to 100 call my_function $i {!3}`` - -- Multiple peer IDs in the ``for`` loop's peerspec: - -``@10:00 for i in 1 to 100 call my_function $i {1,2,3,4}`` diff --git a/gumby/scenario.py b/gumby/scenario.py index 454348973..c6c49e906 100644 --- a/gumby/scenario.py +++ b/gumby/scenario.py @@ -145,47 +145,6 @@ def _parse_arguments(self, args): return unnamed_args, named_args - def _parse_for_loop(self, loop): - """ - Parse a for loop, checking for its syntactical and lexical correctness and returning its functional components - - :param loop: a string containing the for loop. The string should have the following structure: - 'for in to call - '. The value of the lower_bound needn't actually be lower than the higher - bound. - :return: return a tuple containing the following: the callable, the callable's argument line (unparsed), - the lower_bound, the higher_bound, an offset (either 1 of -1) indicating the for's index in/decrements, - the control variable (including the '$' character at the beginning) - """ - parts = loop.split(' ', 7) - - # Less than 7 tokens means that we had a malformed 'for' - if len(parts) < 7: - raise Exception() - - offset = 1 - control_var, lo_bound, hi_bound, callable_function = parts[::2] - args = parts[7] if len(parts) == 8 else '' - - # Check if the other tokens are correct, so as to avoid ambiguity - if ['in', 'to', 'call'] != [x.lower() for x in parts[1:6:2]]: - raise Exception() - - # Convert the bound strings to integers - lo_bound = int(lo_bound) - hi_bound = int(hi_bound) - - # control_var - control_var = '$' + control_var - - # The lower and upper bounds are considered inclusive, hence the additional in/decrements - if lo_bound > hi_bound: - offset = -1 - hi_bound -= 1 - else: - hi_bound += 1 - - return callable_function, args, lo_bound, hi_bound, offset, control_var def _parse_scenario_line(self, filename, line_number, line, peerspec): """ @@ -221,40 +180,12 @@ def _parse_scenario_line(self, filename, line_number, line, peerspec): commands = [] - if callable == 'for': - # If our current command is a 'for' loop, then we further parse the line - callable, args, lo_bound, hi_bound, offset, control_var = self._parse_for_loop(args) - unnamed_args, named_args = self._parse_arguments(args) - - # get the indexes and keys of the control variable in the (un)named variables - control_index_args = [idx for idx in range(len(unnamed_args)) if unnamed_args[idx] == control_var] - control_index_named_args = [key for key in named_args if named_args[key] == control_var] + if self._re_substitution.match(peerspec): + # We have a substitution variable in the peerspec, which should be illegal in this branch + raise Exception() - for i in range(lo_bound, hi_bound, offset): - if not peerspec or (peerspec == control_var and i == self._peernumber) or \ - (peerspec == str(self._peernumber)): - - # Replace any arguments represented by the loop's control variable with its current value - str_i = str(i) - for idx in control_index_args: - unnamed_args[idx] = str_i - for key in control_index_named_args: - named_args[key] = str_i - - # We need to copy the parameter list and dictionary since there's reference issues otherwise - commands.append((begin, filename, line_number, callable, unnamed_args[:], dict(named_args))) - else: - # TODO: a regex should be added to swap in the value of a variable used in the peerspec; - # one can check if the variable exists, and identifies this peer in _parse_for_this_peer - # the aforementioned function can also be the place where the variable is swapped - # if the command is a for, then the variable should be swapped only if it's not the loop's - # control variable - if self._re_substitution.match(peerspec): - # We have a substitution variable in the peerspec, which should be illegal in this branch - raise Exception() - - unnamed_args, named_args = self._parse_arguments(args) - commands = [(begin, filename, line_number, callable, unnamed_args, named_args)] + unnamed_args, named_args = self._parse_arguments(args) + commands = [(begin, filename, line_number, callable, unnamed_args, named_args)] return commands diff --git a/gumby/tests/scenario_language/test_for_loop.py b/gumby/tests/scenario_language/test_for_loop.py deleted file mode 100644 index 61216225f..000000000 --- a/gumby/tests/scenario_language/test_for_loop.py +++ /dev/null @@ -1,213 +0,0 @@ -import unittest - -from gumby.scenario import ScenarioRunner - - -class TestForLoop(unittest.TestCase): - """ - Test class which evaluates the correctness of the scenario language for loop implementation - """ - - def setUp(self): - super(TestForLoop, self).setUp() - self.scenario_parser = ScenarioRunner() - self.scenario_parser._peernumber = 1 - self.callables = { - 'echo_values': self.echo_values, - 'store': self.store - } - self.local_storage = {} - - def set_variable(self, name, value): - """ - Sets a scenario variable. - - :param name: the name of the variable - :param value: the value of the variable - """ - self.scenario_parser.user_defined_vars[name] = str(value) - - def echo_values(self, val, vval=None, cst=None): - """ - This method will return a list containing four elements: the literal 'Proof' and the method's three parameters. - - :param val: an unnamed parameter which can have any arbitrary value - :param vval: a named parameter which can have any arbitrary value - :param cst: a named parameter which can have any arbitrary value - :return: a list containing four elements: the literal 'Proof' and the method's three parameters - """ - return ["Proof", val, vval, cst] - - def store(self, key, value): - """ - Store a value in a test's local storage. - - :param key: the key under which the value is stored - :param value: the value - :return: None - """ - self.local_storage[key] = value - - def execution_wrapper(self, command_line, peerspec="", line_number=1, test_file="test.file"): - """ - Wraps the execution of a scenario command line. This method mimics what a scenario runner will actually do - with a command line. - - :param command_line: the command line which will be executed - :param peerspec: the command line's peer specification - :param line_number: the line number - :param test_file: the file name from which the command line stems - :return: a list containing the returned values (as lists) of the executed command. There may be multiple - values returned since the command line may be unwrapped in multiple commands - """ - unwrapped_lines = self.scenario_parser._parse_scenario_line(test_file, line_number, command_line, peerspec) - - if not unwrapped_lines: - return [] - - returned_values = [] - - for _, _, _, clb, args, kwargs in unwrapped_lines: - returned_values.append(self.callables[clb](*args, **kwargs)) - - return returned_values - - def test_correct_for_loop_increasing(self): - """ - Test a for loop with unit increments. - """ - ground_truth = [] - for i in [str(x) for x in range(1, 11)]: - ground_truth.append(['Proof', i, i, 'should_be_constant']) - - returned_values = self.execution_wrapper("@! for i in 1 to 10 call echo_values $i vval=$i " - "cst=should_be_constant") - - self.assertEqual(returned_values, ground_truth, 'The returned results is not as expected') - - def test_correct_for_loop_decreasing(self): - """ - Test a for loop with unit decrements. - """ - ground_truth = [] - for i in [str(x) for x in range(10, 0, -1)]: - ground_truth.append(['Proof', i, i, 'should_be_constant']) - - returned_values = self.execution_wrapper("@! for i in 10 to 1 call echo_values $i vval=$i " - "cst=should_be_constant") - - self.assertEqual(returned_values, ground_truth, 'The returned results is not as expected') - - def test_for_loop_variable_replacement(self): - """ - Test the correctness of variable replacement - """ - self.execution_wrapper("@! for i in 1 to 5 call store $i constant_value") - - self.assertEqual([str(x) for x in range(1, 6)], sorted(self.local_storage.keys()), - "The key set must be equal to ['1', '2', ..., '5'] when sorted") - - def test_for_loop_peerspec_included(self): - """ - Test that a peerspec is able to correctly identify if the current node should execute an iteration. - """ - self.execution_wrapper("@! for i in 1 to 5 call store some_key $i", peerspec="$i") - - self.assertTrue(len(self.local_storage) == 1 and "some_key" in self.local_storage - and self.local_storage["some_key"] == '1', "The locally stored entry should be the ID " - "of this peer (i.e. should be '1')") - - def test_for_loop_peerspec_excluded(self): - """ - Test that a peerspec is able to correctly identify if the current node should not execute an iteration. - """ - self.execution_wrapper("@! for i in 10 to 5 call store some_key $i", peerspec="$i") - - self.assertFalse(self.local_storage, "The local storage should be empty.") - - def test_for_loop_peerspec_static_included(self): - """ - Test that a peerspec is able to correctly identify if the current node should execute an iteration when it's - static and refers the peer's ID. - """ - self.execution_wrapper("@! for i in 1 to 5 call store $i some_value", peerspec="1") - - self.assertEqual([str(x) for x in range(1, 6)], sorted(self.local_storage.keys()), - "The key set must be equal to ['1', '2', ..., '5'] when sorted") - - def test_for_loop_peerspec_static_excluded(self): - """ - Test that a peerspec is able to correctly identify if the current node should execute an iteration when it's - static and does not refer the peer's ID. - """ - self.execution_wrapper("@! for i in 1 to 5 call store $i some_value", peerspec="2") - - self.assertFalse(self.local_storage, "The local storage should be empty") - - def test_for_loop_with_variables(self): - """ - Test that a for loop which uses variables, that conflict in name with the control variable, the still works - but the values swapped will be that of the variable. - """ - self.set_variable("i", "constant_key_value") - self.execution_wrapper("@! for i in 1 to 5 call store $i some_value") - - self.assertTrue(len(self.local_storage) == 1 and self.local_storage["constant_key_value"] == "some_value", - "There should be only one key - value pair in local storage: constant_key_value -> some_value") - - def test_for_loop_erroneous_peerspec(self): - """ - Test the for loop when there is an erroneously specified peerspec. - """ - self.set_variable('i', '1') - self.assertFalse( - self.execution_wrapper("@! for my_var in 1 to 10 call echo_values 'Should not work - $i'", peerspec="$i"), - "The command line should not return anything") - - self.assertFalse( - self.execution_wrapper("@! for my_var in 1 to 10 call echo_values 'Should not work - $i'", peerspec="$j"), - "The command line should not return anything") - - self.assertFalse( - self.execution_wrapper("@! for my_var in 1 to 10 call echo_values 'Should not work - $i'", peerspec="$"), - "The command line should not return anything") - - def test_wrong_for_loop_syntax(self): - """ - Test the for loop when its syntax is wrong. These should silently fail, however, in real scenarios, they will - output their errors to the event logger. - """ - self.assertFalse( - self.execution_wrapper("@! for my_var 1 to 10 call echo_values 'Will not work'"), - "This command line should silently fail, and return nothing") - - self.assertFalse( - self.execution_wrapper("@! for my_var in 1 10 call echo_values 'Will not work'"), - "This command line should silently fail, and return nothing") - - self.assertFalse( - self.execution_wrapper("@! for my_var in 1 to call echo_values 'Will not work'"), - "This command line should silently fail, and return nothing") - - self.assertFalse( - self.execution_wrapper("@! for my_var in to 10 call echo_values 'Will not work'"), - "This command line should silently fail, and return nothing") - - def test_for_loop_peerspec_exclusion(self): - """ - Test the for loop when it features peer exclusions. These are not supported yet, thus nothing should happen. - """ - self.assertFalse( - self.execution_wrapper("@! for i in 1 to 10 call echo_values 'Will not work'", peerspec="!3") - ) - - def test_peerspec_variable_outside_for(self): - """ - Test the usage of a variable inside a peerspec outside of a for. This should be illegal, and should fail - silently, even if the variable is set. - """ - self.set_variable("i", "1") - self.assertFalse( - self.execution_wrapper("@! echo_values 'Will not work'", peerspec="$i"), - "The command should fail silently, and should not return anything" - )