# Buggy code
def run(self):
    """
    Execute the batch run
    """
    args = [
        [],
        self.opts["fun"],
        self.opts["arg"],
        self.opts["timeout"],
        "list",
    ]
    bnum = self.get_bnum()
    # No targets to run
    if not self.minions:
        return
    to_run = copy.deepcopy(self.minions)
    active = []
    ret = {}
    iters = []
    # wait the specified time before decide a job is actually done
    bwait = self.opts.get("batch_wait", 0)
    wait = []

    if self.options:
        show_jid = self.options.show_jid
        show_verbose = self.options.verbose
    else:
        show_jid = False
        show_verbose = False

    # the minion tracker keeps track of responses and iterators
    # - it removes finished iterators from iters[]
    # - if a previously detected minion does not respond, its
    #   added with an empty answer to ret{} once the timeout is reached
    # - unresponsive minions are removed from active[] to make
    #   sure that the main while loop finishes even with unresp minions
    minion_tracker = {}

    if not self.quiet:
        # We already know some minions didn't respond to the ping, so inform
        # the user we won't be attempting to run a job on them
        for down_minion in self.down_minions:
            salt.utils.stringutils.print_cli(
                "Minion {} did not respond. No job will be sent.".format(down_minion)
            )

    # Iterate while we still have things to execute
    while len(ret) < len(self.minions):
        next_ = []
        if bwait and wait:
            self.__update_wait(wait)
        if len(to_run) <= bnum - len(wait) and not active:
            # last bit of them, add them all to next iterator
            while to_run:
                next_.append(to_run.pop())
        else:
            for i in range(bnum - len(active) - len(wait)):
                if to_run:
                    minion_id = to_run.pop()
                    if isinstance(minion_id, dict):
                        next_.append(next(iter(minion_id)))
                    else:
                        next_.append(minion_id)

        active += next_
        args[0] = next_

        if next_:
            if not self.quiet:
                salt.utils.stringutils.print_cli(
                    "\nExecuting run on {}\n".format(sorted(next_))
                )
            # create a new iterator for this batch of minions
            return_value = self.opts.get("return", self.opts.get("ret", ""))
            new_iter = self.local.cmd_iter_no_block(
                *args,
                raw=self.opts.get("raw", False),
                ret=return_value,
                show_jid=show_jid,
                verbose=show_verbose,
                gather_job_timeout=self.opts["gather_job_timeout"],
                **self.eauth,
            )
            # add it to our iterators and to the minion_tracker
            iters.append(new_iter)
            minion_tracker[new_iter] = {}
            # every iterator added is 'active' and has its set of minions
            minion_tracker[new_iter]["minions"] = next_
            minion_tracker[new_iter]["active"] = True

        else:
            time.sleep(0.02)
        parts = {}

        # see if we found more minions
        for ping_ret in self.ping_gen:
            if ping_ret is None:
                break
            m = next(iter(ping_ret.keys()))
            if m not in self.minions:
                self.minions.append(m)
                to_run.append(m)

        for queue in iters:
            try:
                # Gather returns until we get to the bottom
                ncnt = 0
                while True:
                    part = next(queue)
                    if part is None:
                        time.sleep(0.01)
                        ncnt += 1
                        if ncnt > 5:
                            break
                        continue
                    if self.opts.get("raw"):
                        parts.update({part["data"]["id"]: part})
                        if part["data"]["id"] in minion_tracker[queue]["minions"]:
                            minion_tracker[queue]["minions"].remove(part["data"]["id"])
                        else:
                            salt.utils.stringutils.print_cli(
                                "minion {} was already deleted from tracker, probably a duplicate key".format(
                                    part["id"]
                                )
                            )
                    else:
                        parts.update(part)
                        for id in part:
                            if id in minion_tracker[queue]["minions"]:
                                minion_tracker[queue]["minions"].remove(id)
                            else:
                                salt.utils.stringutils.print_cli(
                                    "minion {} was already deleted from tracker, probably a duplicate key".format(
                                        id
                                    )
                                )
            except StopIteration:
                # if a iterator is done:
                # - set it to inactive
                # - add minions that have not responded to parts{}

                # check if the tracker contains the iterator
                if queue in minion_tracker:
                    minion_tracker[queue]["active"] = False

                    # add all minions that belong to this iterator and
                    # that have not responded to parts{} with an empty response
                    for minion in minion_tracker[queue]["minions"]:
                        if minion not in parts:
                            parts[minion] = {}
                            parts[minion]["ret"] = {}

        for minion, data in parts.items():
            if minion in active:
                active.remove(minion)
                if bwait:
                    wait.append(datetime.now() + timedelta(seconds=bwait))
            # Munge retcode into return data
            failhard = False
            if (
                "retcode" in data
                and isinstance(data["ret"], dict)
                and "retcode" not in data["ret"]
            ):
                data["ret"]["retcode"] = data["retcode"]
                if self.opts.get("failhard") and data["ret"]["retcode"] > 0:
                    failhard = True
            else:
                if self.opts.get("failhard") and data["retcode"] > 0:
                    failhard = True

            if self.opts.get("raw"):
                ret[minion] = data
                yield data
            else:
                ret[minion] = data["ret"]
                yield {minion: data["ret"]}
            if not self.quiet:
                ret[minion] = data["ret"]
                data[minion] = data.pop("ret")
                if "out" in data:
                    out = data.pop("out")
                else:
                    out = None
                salt.output.display_output(data, out, self.opts)
            if failhard:
                log.error(
                    "Minion %s returned with non-zero exit code. "
                    "Batch run stopped due to failhard",
                    minion,
                )
                raise StopIteration

        # remove inactive iterators from the iters list
        for queue in minion_tracker:
            # only remove inactive queues
            if not minion_tracker[queue]["active"] and queue in iters:
                iters.remove(queue)
                # also remove the iterator's minions from the active list
                for minion in minion_tracker[queue]["minions"]:
                    if minion in active:
                        active.remove(minion)
                        if bwait:
                            wait.append(datetime.now() + timedelta(seconds=bwait))
    self.local.destroy()


# Traceback (RuntimeError)
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/salt/client/__init__.py", line 1046, in get_cli_returns
raise StopIteration()
StopIteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "/usr/local/bin/run_tests", line 141, in <module>
main(args)
File "/usr/local/bin/run_tests", line 120, in main
salt_master_run(args.target, "state.apply", ["unittests"],
File "/usr/lib/python3/dist-packages/salt_helper.py", line 156, in salt_master_run
for data in client.get_cli_returns(job["jid"], job["minions"],
RuntimeError: generator raised StopIteration

# Fixed code
def run(self):
    """
    Execute the batch run
    """
    args = [
        [],
        self.opts["fun"],
        self.opts["arg"],
        self.opts["timeout"],
        "list",
    ]
    bnum = self.get_bnum()
    # No targets to run
    if not self.minions:
        return
    to_run = copy.deepcopy(self.minions)
    active = []
    ret = {}
    iters = []
    # wait the specified time before decide a job is actually done
    bwait = self.opts.get("batch_wait", 0)
    wait = []

    if self.options:
        show_jid = self.options.show_jid
        show_verbose = self.options.verbose
    else:
        show_jid = False
        show_verbose = False

    # the minion tracker keeps track of responses and iterators
    # - it removes finished iterators from iters[]
    # - if a previously detected minion does not respond, its
    #   added with an empty answer to ret{} once the timeout is reached
    # - unresponsive minions are removed from active[] to make
    #   sure that the main while loop finishes even with unresp minions
    minion_tracker = {}

    if not self.quiet:
        # We already know some minions didn't respond to the ping, so inform
        # the user we won't be attempting to run a job on them
        for down_minion in self.down_minions:
            salt.utils.stringutils.print_cli(
                "Minion {} did not respond. No job will be sent.".format(down_minion)
            )

    # Iterate while we still have things to execute
    while len(ret) < len(self.minions):
        next_ = []
        if bwait and wait:
            self.__update_wait(wait)
        if len(to_run) <= bnum - len(wait) and not active:
            # last bit of them, add them all to next iterator
            while to_run:
                next_.append(to_run.pop())
        else:
            for i in range(bnum - len(active) - len(wait)):
                if to_run:
                    minion_id = to_run.pop()
                    if isinstance(minion_id, dict):
                        next_.append(next(iter(minion_id)))
                    else:
                        next_.append(minion_id)

        active += next_
        args[0] = next_

        if next_:
            if not self.quiet:
                salt.utils.stringutils.print_cli(
                    "\nExecuting run on {}\n".format(sorted(next_))
                )
            # create a new iterator for this batch of minions
            return_value = self.opts.get("return", self.opts.get("ret", ""))
            new_iter = self.local.cmd_iter_no_block(
                *args,
                raw=self.opts.get("raw", False),
                ret=return_value,
                show_jid=show_jid,
                verbose=show_verbose,
                gather_job_timeout=self.opts["gather_job_timeout"],
                **self.eauth,
            )
            # add it to our iterators and to the minion_tracker
            iters.append(new_iter)
            minion_tracker[new_iter] = {}
            # every iterator added is 'active' and has its set of minions
            minion_tracker[new_iter]["minions"] = next_
            minion_tracker[new_iter]["active"] = True

        else:
            time.sleep(0.02)
        parts = {}

        # see if we found more minions
        for ping_ret in self.ping_gen:
            if ping_ret is None:
                break
            m = next(iter(ping_ret.keys()))
            if m not in self.minions:
                self.minions.append(m)
                to_run.append(m)

        for queue in iters:
            try:
                # Gather returns until we get to the bottom
                ncnt = 0
                while True:
                    part = next(queue)
                    if part is None:
                        time.sleep(0.01)
                        ncnt += 1
                        if ncnt > 5:
                            break
                        continue
                    if self.opts.get("raw"):
                        parts.update({part["data"]["id"]: part})
                        if part["data"]["id"] in minion_tracker[queue]["minions"]:
                            minion_tracker[queue]["minions"].remove(part["data"]["id"])
                        else:
                            salt.utils.stringutils.print_cli(
                                "minion {} was already deleted from tracker, probably a duplicate key".format(
                                    part["id"]
                                )
                            )
                    else:
                        parts.update(part)
                        for id in part:
                            if id in minion_tracker[queue]["minions"]:
                                minion_tracker[queue]["minions"].remove(id)
                            else:
                                salt.utils.stringutils.print_cli(
                                    "minion {} was already deleted from tracker, probably a duplicate key".format(
                                        id
                                    )
                                )
            except StopIteration:
                # if a iterator is done:
                # - set it to inactive
                # - add minions that have not responded to parts{}

                # check if the tracker contains the iterator
                if queue in minion_tracker:
                    minion_tracker[queue]["active"] = False

                    # add all minions that belong to this iterator and
                    # that have not responded to parts{} with an empty response
                    for minion in minion_tracker[queue]["minions"]:
                        if minion not in parts:
                            parts[minion] = {}
                            parts[minion]["ret"] = {}

        for minion, data in parts.items():
            if minion in active:
                active.remove(minion)
                if bwait:
                    wait.append(datetime.now() + timedelta(seconds=bwait))
            # Munge retcode into return data
            failhard = False
            if (
                "retcode" in data
                and isinstance(data["ret"], dict)
                and "retcode" not in data["ret"]
            ):
                data["ret"]["retcode"] = data["retcode"]
                if self.opts.get("failhard") and data["ret"]["retcode"] > 0:
                    failhard = True
            else:
                if self.opts.get("failhard") and data["retcode"] > 0:
                    failhard = True

            if self.opts.get("raw"):
                ret[minion] = data
                yield data
            else:
                ret[minion] = data["ret"]
                yield {minion: data["ret"]}
            if not self.quiet:
                ret[minion] = data["ret"]
                data[minion] = data.pop("ret")
                if "out" in data:
                    out = data.pop("out")
                else:
                    out = None
                salt.output.display_output(data, out, self.opts)
            if failhard:
                log.error(
                    "Minion %s returned with non-zero exit code. "
                    "Batch run stopped due to failhard",
                    minion,
                )
                return

        # remove inactive iterators from the iters list
        for queue in minion_tracker:
            # only remove inactive queues
            if not minion_tracker[queue]["active"] and queue in iters:
                iters.remove(queue)
                # also remove the iterator's minions from the active list
                for minion in minion_tracker[queue]["minions"]:
                    if minion in active:
                        active.remove(minion)
                        if bwait:
                            wait.append(datetime.now() + timedelta(seconds=bwait))
    self.local.destroy()