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"
- )