Repository: mkorpela/pabot

Issue:
required a support adding new flag which will wait before trigger next suite
Hi
I am using pabot for robot parallel execution and I have a requirement of executing the suites in parallel but need to some time to between each suite. For example, I have suite1 and suite2 and I need to trigger them with some delay as below

Suite1 and wait $DELAY and Suite2

Could you please look at this and implement? it will be helpful for others also

Discussion:
@joonaskuisma: Hi @kbogineni !  

The `#SLEEP` keyword has been implemented and is just waiting for review and merging into the main branch. If you get a chance, please take a look at #621.  

Any feedback is highly appreciated! 😊  
@kbogineni: @joonaskuisma how the pabot command looks if this is fixed@joonaskuisma: Here is section of updated README.md (You can find it my fork and feature branch also):

By using the command #SLEEP X, where X is an integer in the range [0-3600] (in seconds), you can define a startup delay for each subprocess. #SLEEP affects the next line unless the next line starts a group with {, in which case the delay applies to the entire group. If the next line begins with --test or --suite, the delay is applied to that specific item. Any other occurrences of #SLEEP are ignored.

The following example clarifies the behavior:

`pabot --process 2 --ordering order.txt data_1`

where order.txt is:

````
#SLEEP 1
{
#SLEEP 2
--suite Data 1.suite A
#SLEEP 3
--suite Data 1.suite B
#SLEEP 4
}
#SLEEP 5
#SLEEP 6
--suite Data 1.suite C
#SLEEP 7
--suite Data 1.suite D
#SLEEP 8
```` 

prints something like this:

``````` 
2025-02-15 19:15:00.408321 [0] [ID:1] SLEEPING 6 SECONDS BEFORE STARTING Data 1.suite C
2025-02-15 19:15:00.408321 [1] [ID:0] SLEEPING 1 SECONDS BEFORE STARTING Group_Data 1.suite A_Data 1.suite B
2025-02-15 19:15:01.409389 [PID:52008] [1] [ID:0] EXECUTING Group_Data 1.suite A_Data 1.suite B
2025-02-15 19:15:06.409024 [PID:1528] [0] [ID:1] EXECUTING Data 1.suite C
2025-02-15 19:15:09.257564 [PID:52008] [1] [ID:0] PASSED Group_Data 1.suite A_Data 1.suite B in 7.8 seconds
2025-02-15 19:15:09.259067 [1] [ID:2] SLEEPING 7 SECONDS BEFORE STARTING Data 1.suite D
2025-02-15 19:15:09.647342 [PID:1528] [0] [ID:1] PASSED Data 1.suite C in 3.2 seconds
2025-02-15 19:15:16.260432 [PID:48156] [1] [ID:2] EXECUTING Data 1.suite D
2025-02-15 19:15:18.696420 [PID:48156] [1] [ID:2] PASSED Data 1.suite D in 2.4 seconds@joonaskuisma: Hi @kbogineni !

What you think about this solution? (See my previous comment)@kbogineni: It looks good to me but is there any other solution to add it as command line arguments@mkorpela: Done in 4.2.0

Solution patch:
diff --git a/README.md b/README.md
index 34b8e5cb..263457df 100644
--- a/README.md
+++ b/README.md
@@ -63,9 +63,10 @@ pabot [--verbose|--testlevelsplit|--command .. --end-command|
         --processtimeout num|
         --shard i/n|
         --artifacts extensions|--artifactsinsubfolders|
-        --resourcefile file|--argumentfile[num] file|--suitesfrom file|--ordering file
-        --chunk
-        --pabotprerunmodifier modifier
+        --resourcefile file|--argumentfile[num] file|--suitesfrom file|--ordering file|
+        --chunk|
+        --pabotprerunmodifier modifier|
+        --no-rebot|
         --help|--version]
       [robot options] [path ...]
 
@@ -153,11 +154,16 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
   pabot subprocesses. Depending on the intended use, this may be desirable as well as more efficient. Can be used, for 
   example, to modify the list of tests to be performed.
 
- --help             
- Print usage instructions.
+--no-rebot    
+  If specified, the tests will execute as usual, but Rebot will not be called to merge the logs. This option is designed 
+  for scenarios where Rebot should be run later due to large log files, ensuring better memory and resource availability. 
+  Subprocess results are stored in the pabot_results folder.
+
+--help             
+  Print usage instructions.
  
- --version                
- Print version information.
+--version                
+  Print version information.
 
 Example usages:
 
@@ -259,6 +265,50 @@ There different possibilities to influence the execution:
 --test robotTest.1 Scalar.Test With Arguments and Return Values
 --test robotTest.3 Dictionary.Test with Dictionaries as Arguments
 --test robotTest.3 Dictionary.Test with FOR loops and Dictionaries #DEPENDS robotTest.1 Scalar.Test Case with Return Values
+```
+
+  * By using the command `#SLEEP X`, where `X` is an integer in the range [0-3600] (in seconds), you can 
+  define a startup delay for each subprocess. `#SLEEP` affects the next line unless the next line starts a 
+  group with `{`, in which case the delay applies to the entire group. If the next line begins with `--test` 
+  or `--suite`, the delay is applied to that specific item. Any other occurrences of `#SLEEP` are ignored.
+
+The following example clarifies the behavior:
+
+```sh
+pabot --process 2 --ordering order.txt data_1
+```
+
+where order.txt is:
+
+```
+#SLEEP 1
+{
+#SLEEP 2
+--suite Data 1.suite A
+#SLEEP 3
+--suite Data 1.suite B
+#SLEEP 4
+}
+#SLEEP 5
+#SLEEP 6
+--suite Data 1.suite C
+#SLEEP 7
+--suite Data 1.suite D
+#SLEEP 8
+```
+
+prints something like this:
+
+```
+2025-02-15 19:15:00.408321 [0] [ID:1] SLEEPING 6 SECONDS BEFORE STARTING Data 1.suite C
+2025-02-15 19:15:00.408321 [1] [ID:0] SLEEPING 1 SECONDS BEFORE STARTING Group_Data 1.suite A_Data 1.suite B
+2025-02-15 19:15:01.409389 [PID:52008] [1] [ID:0] EXECUTING Group_Data 1.suite A_Data 1.suite B
+2025-02-15 19:15:06.409024 [PID:1528] [0] [ID:1] EXECUTING Data 1.suite C
+2025-02-15 19:15:09.257564 [PID:52008] [1] [ID:0] PASSED Group_Data 1.suite A_Data 1.suite B in 7.8 seconds
+2025-02-15 19:15:09.259067 [1] [ID:2] SLEEPING 7 SECONDS BEFORE STARTING Data 1.suite D
+2025-02-15 19:15:09.647342 [PID:1528] [0] [ID:1] PASSED Data 1.suite C in 3.2 seconds
+2025-02-15 19:15:16.260432 [PID:48156] [1] [ID:2] EXECUTING Data 1.suite D
+2025-02-15 19:15:18.696420 [PID:48156] [1] [ID:2] PASSED Data 1.suite D in 2.4 seconds
 ```
 
 ### Programmatic use
diff --git a/src/pabot/arguments.py b/src/pabot/arguments.py
index 95259c15..64733769 100644
--- a/src/pabot/arguments.py
+++ b/src/pabot/arguments.py
@@ -16,6 +16,7 @@
     SuiteItem,
     TestItem,
     WaitItem,
+    SleepItem,
 )
 
 ARGSMATCHER = re.compile(r"--argumentfile(\d+)")
@@ -91,6 +92,7 @@ def _parse_pabot_args(args):  # type: (List[str]) -> Tuple[List[str], Dict[str,
         "shardindex": 0,
         "shardcount": 1,
         "chunk": False,
+        "no-rebot": False,
     }
     # Explicitly define argument types for validation
     flag_args = {
@@ -100,6 +102,7 @@ def _parse_pabot_args(args):  # type: (List[str]) -> Tuple[List[str], Dict[str,
         "pabotlib",
         "artifactsinsubfolders",
         "chunk",
+        "no-rebot"
     }
     value_args = {
         "hive": str,
@@ -217,6 +220,8 @@ def parse_execution_item_line(text):  # type: (str) -> ExecutionItem
     if text.startswith("DYNAMICTEST"):
         suite, test = text[12:].split(" :: ")
         return DynamicTestItem(test, suite)
+    if text.startswith("#SLEEP "):
+        return SleepItem(text[7:])
     if text == "#WAIT":
         return WaitItem()
     if text == "{":
diff --git a/src/pabot/execution_items.py b/src/pabot/execution_items.py
index 1df9b77b..f6280821 100644
--- a/src/pabot/execution_items.py
+++ b/src/pabot/execution_items.py
@@ -12,6 +12,7 @@ class ExecutionItem(object):
     isWait = False
     type = None  # type: str
     name = None  # type: str
+    sleep = 0  # type: int
 
     def top_name(self):
         # type: () -> str
@@ -29,6 +30,14 @@ def line(self):
         # type: () -> str
         return ""
 
+    def set_sleep(self, sleep_time):
+        # type: (int) -> None
+        self.sleep = sleep_time
+
+    def get_sleep(self):
+        # type: () -> int
+        return self.sleep
+
     def modify_options_for_executor(self, options):
         options[self.type] = self.name
 
@@ -82,6 +91,8 @@ def add(self, item):
             )
         if len(self._items) > 0:
             self.name += "_"
+        if self.get_sleep() < item.get_sleep():     # TODO: check!
+            self.set_sleep(item.get_sleep())
         self.name += item.name
         self._element_type = item.type
         self._items.append(item)
@@ -274,6 +285,23 @@ def line(self):
         return self.name
 
 
+class SleepItem(ExecutionItem):
+    type = "sleep"
+
+    def __init__(self, time):
+        try:
+            assert 3600 >= int(time) >= 0  # 1 h max.
+            self.name = time
+            self.sleep = int(time)
+        except ValueError:
+            raise ValueError("#SLEEP value %s is not integer" % time)
+        except AssertionError:
+            raise ValueError("#SLEEP value %s is not in between 0 and 3600" % time)
+
+    def line(self):
+        return "#SLEEP " + self.name
+
+
 class GroupStartItem(ExecutionItem):
     type = "group"
 
diff --git a/src/pabot/pabot.py b/src/pabot/pabot.py
index 3b04f673..d360f7dd 100644
--- a/src/pabot/pabot.py
+++ b/src/pabot/pabot.py
@@ -41,6 +41,7 @@
 import time
 import traceback
 import uuid
+import copy
 from collections import namedtuple
 from contextlib import closing
 from glob import glob
@@ -78,6 +79,7 @@
     SuiteItems,
     TestItem,
     RunnableItem,
+    SleepItem,
 )
 from .result_merger import merge
 
@@ -262,6 +264,7 @@ def execute_and_wait_with(item):
                 item.index,
                 item.execution_item.type != "test",
                 process_timeout=item.timeout,
+                sleep_before_start=item.sleep_before_start
             )
         outputxml_preprocessing(
             item.options, outs_dir, name, item.verbose, _make_id(), caller_id
@@ -320,8 +323,9 @@ def _try_execute_and_wait(
     my_index=-1,
     show_stdout_on_failure=False,
     process_timeout=None,
+    sleep_before_start=0
 ):
-    # type: (List[str], str, str, bool, int, str, int, bool, Optional[int]) -> None
+    # type: (List[str], str, str, bool, int, str, int, bool, Optional[int], int) -> None
     plib = None
     is_ignored = False
     if _pabotlib_in_use():
@@ -339,6 +343,7 @@ def _try_execute_and_wait(
                     my_index,
                     outs_dir,
                     process_timeout,
+                    sleep_before_start
                 )
     except:
         _write(traceback.format_exc())
@@ -521,8 +526,16 @@ def _run(
     item_index,
     outs_dir,
     process_timeout,
+    sleep_before_start,
 ):
-    # type: (List[str], IO[Any], IO[Any], str, bool, int, int, str, Optional[int]) -> Tuple[Union[subprocess.Popen[bytes], subprocess.Popen], Tuple[int, float]]
+    # type: (List[str], IO[Any], IO[Any], str, bool, int, int, str, Optional[int], int) -> Tuple[Union[subprocess.Popen[bytes], subprocess.Popen], Tuple[int, float]]
+    timestamp = datetime.datetime.now()
+    if sleep_before_start > 0:
+        _write(
+            "%s [%s] [ID:%s] SLEEPING %s SECONDS BEFORE STARTING %s"
+            % (timestamp, pool_id, item_index, sleep_before_start, item_name),
+        )
+        time.sleep(sleep_before_start)
     timestamp = datetime.datetime.now()
     cmd = " ".join(command)
     if PY2:
@@ -757,6 +770,7 @@ def _group_by_groups(tokens):
                     "Ordering: Group can not contain a group. Encoutered '{'"
                 )
             group = GroupItem()
+            group.set_sleep(token.get_sleep())
             result.append(group)
             continue
         if isinstance(token, GroupEndItem):
@@ -935,6 +949,13 @@ def solve_suite_names(outs_dir, datasources, options, pabot_args):
             )
             execution_item_lines = [parse_execution_item_line(l) for l in lines[4:]]
             if corrupted or h != file_h or file_hash != hash_of_file or pabot_args.get("pabotprerunmodifier"):
+                if file_h[0] != h[0] and file_h[2] == h[2]:
+                    suite_names = _levelsplit(
+                        generate_suite_names_with_builder(outs_dir, datasources, options),
+                        pabot_args,
+                    )
+                    store_suite_names(h, suite_names)
+                    return suite_names
                 return _regenerate(
                     file_h,
                     h,
@@ -1735,6 +1756,7 @@ def __init__(
         self.hive = hive
         self.processes = processes
         self.timeout = timeout
+        self.sleep_before_start = execution_item.get_sleep()
 
     @property
     def index(self):
@@ -2002,6 +2024,14 @@ def main_program(args):
                 opts_for_run,
                 pabot_args,
             )
+        if pabot_args["no-rebot"]:
+            _write((
+                "All tests were executed, but the --no-rebot argument was given, "
+                "so the results were not compiled, and no summary was generated. "
+                f"All results have been saved in the {os.path.join(os.path.curdir, 'pabot_results')} folder."
+            ))
+            _write("===================================================")
+            return 0 if not _ABNORMAL_EXIT_HAPPENED else 252
         result_code = _report_results(
             outs_dir,
             pabot_args,
@@ -2043,6 +2073,8 @@ def _parse_ordering(filename):  # type: (str) -> List[ExecutionItem]
             ]
     except FileNotFoundError:
         raise DataError("Error: File '%s' not found." % filename)
+    except ValueError as e:
+        raise DataError("Error in ordering file: %s: %s" % (filename, e))
     except:
         raise DataError("Error parsing ordering file '%s'" % filename)
 
@@ -2051,7 +2083,8 @@ def _group_suites(outs_dir, datasources, options, pabot_args):
     suite_names = solve_suite_names(outs_dir, datasources, options, pabot_args)
     _verify_depends(suite_names)
     ordering_arg = _parse_ordering(pabot_args.get("ordering")) if (pabot_args.get("ordering")) is not None else None
-    ordered_suites = _preserve_order(suite_names, ordering_arg)
+    ordering_arg_with_sleep = _set_sleep_times(ordering_arg)
+    ordered_suites = _preserve_order(suite_names, ordering_arg_with_sleep)
     shard_suites = solve_shard_suites(ordered_suites, pabot_args)
     grouped_suites = (
         _chunked_suite_names(shard_suites, pabot_args["processes"])
@@ -2062,6 +2095,29 @@ def _group_suites(outs_dir, datasources, options, pabot_args):
     return grouped_by_depend
 
 
+def _set_sleep_times(ordering_arg):
+    # type: (List[ExecutionItem]) -> List[ExecutionItem]
+    set_sleep_value = 0
+    in_group = False
+    output = copy.deepcopy(ordering_arg)
+    if output is not None:
+        if len(output) >= 2:
+            for i in range(len(output) - 1):
+                if isinstance(output[i], SleepItem):
+                    set_sleep_value = output[i].get_sleep()
+                else:
+                    set_sleep_value = 0
+                if isinstance(output[i], GroupStartItem):
+                    in_group = True
+                if isinstance(output[i], GroupEndItem):
+                    in_group = False
+                if isinstance(output[i + 1], GroupStartItem) and set_sleep_value > 0:
+                    output[i + 1].set_sleep(set_sleep_value)
+                if isinstance(output[i + 1], RunnableItem) and set_sleep_value > 0 and not in_group:
+                    output[i + 1].set_sleep(set_sleep_value)
+    return output
+
+
 def _chunked_suite_names(suite_names, processes):
     q, r = divmod(len(suite_names), processes)
     result = []


Test patch:
diff --git a/tests/test_ordering.py b/tests/test_ordering.py
index e02e416a..89fde654 100644
--- a/tests/test_ordering.py
+++ b/tests/test_ordering.py
@@ -192,3 +192,73 @@ def test_longnames_in_tests(self):
             self.assertIn(b"PASSED", stdout, stderr)
             self.assertNotIn(b"FAILED", stdout, stderr)
             self.assertEqual(stdout.count(b"PASSED"), 2)
+
+
+class PabotOrderingSleepTest(unittest.TestCase):
+    def setUp(self):
+        self.tmpdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.tmpdir)
+
+    def _run_tests_with(self, testfile, orderfile):
+        robot_file = open("{}/test.robot".format(self.tmpdir), "w")
+        robot_file.write(textwrap.dedent(testfile))
+        robot_file.close()
+        with open("{}/order.dat".format(self.tmpdir), "w") as f:
+            f.write(textwrap.dedent(orderfile))
+        process = subprocess.Popen(
+            [
+                sys.executable,
+                "-m" "pabot.pabot",
+                "--testlevelsplit",
+                "--ordering",
+                "{}/order.dat".format(self.tmpdir),
+                "{}/test.robot".format(self.tmpdir),
+            ],
+            cwd=self.tmpdir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        return process.communicate()
+
+    def test_sleep_test_cases_and_group(self):
+        stdout, stderr = self._run_tests_with(
+            """
+        *** Test Cases ***
+        Test Case A
+           Log  Hello!
+
+        Test Case B
+           Log  Hello!
+
+        Test Case C
+           Log  Hello!
+
+        Test Case D
+            Log  Hello!
+        """,
+            """
+        #SLEEP 1
+        {
+        #SLEEP 4
+        --test Test.Test Case A
+        #SLEEP 4
+        --test Test.Test Case B
+        #SLEEP 4
+        }
+        #SLEEP 4
+        #SLEEP 3
+        --test Test.Test Case C
+        #SLEEP 2
+        --test Test.Test Case D
+        #SLEEP 4
+        """,
+        )
+        self.assertIn(b"PASSED", stdout, stderr)
+        self.assertNotIn(b"FAILED", stdout, stderr)
+        self.assertEqual(stdout.count(b"PASSED"), 3)
+        self.assertIn(b"SLEEPING 1 SECONDS BEFORE STARTING Group_Test.Test Case A_Test.Test Case B", stdout, stderr)
+        self.assertIn(b"SLEEPING 3 SECONDS BEFORE STARTING Test.Test Case C", stdout, stderr)
+        self.assertIn(b"SLEEPING 2 SECONDS BEFORE STARTING Test.Test Case D", stdout, stderr)
+        self.assertNotIn(b"SLEEPING 4", stdout, stderr)
diff --git a/tests/test_suite_structure.py b/tests/test_suite_structure.py
new file mode 100644
index 00000000..b8620e09
--- /dev/null
+++ b/tests/test_suite_structure.py
@@ -0,0 +1,232 @@
+import unittest
+import tempfile
+import textwrap
+import shutil
+import subprocess
+import sys
+import os
+from test_pabotprerunmodifier import get_tmpdir_name
+
+
+class SuiteStructureTests(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.tmpdir = tempfile.mkdtemp()
+        cls.tmpdir_name = get_tmpdir_name(os.path.basename(cls.tmpdir))
+
+        # robot case files
+        cls.robot_file_path_1 = f'{cls.tmpdir}/dir_1/suite_1.robot'
+        cls.robot_file_path_2 = f'{cls.tmpdir}/dir_1/dir_2/suite_2.robot'
+        cls.robot_file_path_3 = f'{cls.tmpdir}/dir_1/dir_2/dir_3/suite_3.robot'
+        cls.robot_file_path_4 = f'{cls.tmpdir}/dir_1/dir_2/dir_3/dir_4/suite_4.robot'
+
+        cls.robot_dir_1 = os.path.dirname(cls.robot_file_path_1)
+        cls.robot_dir_2 = os.path.dirname(cls.robot_file_path_2)
+        cls.robot_dir_3 = os.path.dirname(cls.robot_file_path_3)
+        cls.robot_dir_4 = os.path.dirname(cls.robot_file_path_4)
+
+
+        os.makedirs(os.path.dirname(cls.robot_file_path_4), exist_ok=True)
+        with open(cls.robot_file_path_1, 'w') as robot_file:
+            robot_file.write(
+                textwrap.dedent("""
+*** Test Cases ***
+Testing 1 1
+   Log  hello
+"""))
+            
+        with open(cls.robot_file_path_2, 'w') as robot_file:
+            robot_file.write(
+                textwrap.dedent("""
+*** Test Cases ***
+Testing 2 1
+   Log  hello
+"""))
+            
+        with open(cls.robot_file_path_3, 'w') as robot_file:
+            robot_file.write(
+                textwrap.dedent("""
+*** Test Cases ***
+Testing 3 1
+   Log  hello
+"""))
+            
+        with open(cls.robot_file_path_4, 'w') as robot_file:
+            robot_file.write(
+                textwrap.dedent("""
+*** Test Cases ***
+Testing 4 1
+   [Tags]  tag
+   Log  hello
+"""))
+            
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.tmpdir)
+
+
+    def tearDown(self):
+        """ Deletes .pabotsuitenames ja output.xml files between tests. """
+        for filename in [".pabotsuitenames", "output.xml"]:
+            file_path = os.path.join(self.tmpdir, filename)
+            if os.path.exists(file_path):
+                os.remove(file_path)
+
+
+    def test_run_root_then_suite_4_then_dir_1_then_dir_4(self):
+        process_root_1 = subprocess.Popen(
+            [
+                sys.executable,
+                "-m", "pabot.pabot",
+                self.tmpdir
+            ],
+            cwd=self.tmpdir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, stderr = process_root_1.communicate()
+        self.assertIn(b'4 tests, 4 passed, 0 failed, 0 skipped.', stdout)
+        self.assertEqual(b"", stderr)
+        self.assertIn(f'PASSED {self.tmpdir_name}.Dir 1.Suite 1'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED {self.tmpdir_name}.Dir 1.Dir 2.Suite 2'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED {self.tmpdir_name}.Dir 1.Dir 2.Dir 3.Suite 3'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED {self.tmpdir_name}.Dir 1.Dir 2.Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
+
+        process_suite_4 = subprocess.Popen(
+            [
+                sys.executable,
+                "-m", "pabot.pabot",
+                self.robot_file_path_4
+            ],
+            cwd=self.tmpdir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, stderr = process_suite_4.communicate()
+        self.assertIn(b'1 tests, 1 passed, 0 failed, 0 skipped.', stdout)
+        self.assertEqual(b"", stderr)
+        self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
+        self.assertNotIn(f'Suite 2'.encode('utf-8'), stdout)
+        self.assertNotIn(f'Suite 3'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Suite 4'.encode('utf-8'), stdout)
+
+        process_dir_1 = subprocess.Popen(
+            [
+                sys.executable,
+                "-m", "pabot.pabot",
+                self.robot_dir_1
+            ],
+            cwd=self.tmpdir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, stderr = process_dir_1.communicate()
+        self.assertIn(b'4 tests, 4 passed, 0 failed, 0 skipped.', stdout)
+        self.assertEqual(b"", stderr)
+        self.assertIn(f'PASSED Dir 1.Suite 1'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 1.Dir 2.Suite 2'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 1.Dir 2.Dir 3.Suite 3'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 1.Dir 2.Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
+
+        process_dir_4 = subprocess.Popen(
+            [
+                sys.executable,
+                "-m", "pabot.pabot",
+                self.robot_dir_4
+            ],
+            cwd=self.tmpdir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, stderr = process_dir_4.communicate()
+        self.assertIn(b'1 tests, 1 passed, 0 failed, 0 skipped.', stdout)
+        self.assertEqual(b"", stderr)
+        self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
+        self.assertNotIn(f'Suite 2'.encode('utf-8'), stdout)
+        self.assertNotIn(f'Suite 3'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 4.Suite 4'.encode('utf-8'), stdout)
+
+
+    
+    def test_run_dir_2_then_dir_3(self):
+        process_dir_2 = subprocess.Popen(
+            [
+                sys.executable,
+                "-m", "pabot.pabot",
+                self.robot_dir_2
+            ],
+            cwd=self.tmpdir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, stderr = process_dir_2.communicate()
+        self.assertIn(b'3 tests, 3 passed, 0 failed, 0 skipped.', stdout)
+        self.assertEqual(b"", stderr)
+        self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 2.Suite 2'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 2.Dir 3.Suite 3'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 2.Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
+
+        process_dir_3 = subprocess.Popen(
+            [
+                sys.executable,
+                "-m", "pabot.pabot",
+                self.robot_dir_3
+            ],
+            cwd=self.tmpdir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, stderr = process_dir_3.communicate()
+        self.assertIn(b'2 tests, 2 passed, 0 failed, 0 skipped.', stdout)
+        self.assertEqual(b"", stderr)
+        self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
+        self.assertNotIn(f'Suite 2'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 3.Suite 3'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
+
+    
+    def test_run_dir_3_then_dir_2(self):
+        process_dir_3 = subprocess.Popen(
+            [
+                sys.executable,
+                "-m", "pabot.pabot",
+                self.robot_dir_3
+            ],
+            cwd=self.tmpdir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, stderr = process_dir_3.communicate()
+        self.assertIn(b'2 tests, 2 passed, 0 failed, 0 skipped.', stdout)
+        self.assertEqual(b"", stderr)
+        self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
+        self.assertNotIn(f'Suite 2'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 3.Suite 3'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
+
+        process_dir_2 = subprocess.Popen(
+            [
+                sys.executable,
+                "-m", "pabot.pabot",
+                self.robot_dir_2
+            ],
+            cwd=self.tmpdir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, stderr = process_dir_2.communicate()
+        self.assertIn(b'3 tests, 3 passed, 0 failed, 0 skipped.', stdout)
+        self.assertEqual(b"", stderr)
+        self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 2.Suite 2'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 2.Dir 3.Suite 3'.encode('utf-8'), stdout)
+        self.assertIn(f'PASSED Dir 2.Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)