# Buggy code
def run_parallel(config, venv_dict):
    """here we'll just start parallel sub-processes"""
    live_out = config.option.parallel_live
    disable_spinner = bool(os.environ.get("TOX_PARALLEL_NO_SPINNER") == "1")
    args = [sys.executable, MAIN_FILE] + config.args
    try:
        position = args.index("--")
    except ValueError:
        position = len(args)

    max_parallel = config.option.parallel
    if max_parallel is None:
        max_parallel = len(venv_dict)
    semaphore = Semaphore(max_parallel)
    finished = Event()

    show_progress = (
        not disable_spinner
        and not live_out
        and reporter.verbosity() > reporter.Verbosity.QUIET
    )

    with Spinner(enabled=show_progress) as spinner:

        def run_in_thread(tox_env, os_env, processes):
            output = None
            env_name = tox_env.envconfig.envname
            status = "skipped tests" if config.option.notest else None
            try:
                os_env[str(PARALLEL_ENV_VAR_KEY)] = str(env_name)
                args_sub = list(args)
                if hasattr(tox_env, "package"):
                    args_sub.insert(position, str(tox_env.package))
                    args_sub.insert(position, "--installpkg")
                if tox_env.get_result_json_path():
                    result_json_index = args_sub.index("--result-json")
                    args_sub[result_json_index + 1] = "{}".format(
                        tox_env.get_result_json_path()
                    )
                with tox_env.new_action("parallel {}".format(tox_env.name)) as action:

                    def collect_process(process):
                        processes[tox_env] = (action, process)

                    print_out = not live_out and tox_env.envconfig.parallel_show_output
                    output = action.popen(
                        args=args_sub,
                        env=os_env,
                        redirect=not live_out,
                        capture_err=print_out,
                        callback=collect_process,
                        returnout=print_out,
                    )

            except InvocationError as err:
                status = "parallel child exit code {}".format(err.exit_code)
            finally:
                semaphore.release()
                finished.set()
                tox_env.status = status
                done.add(env_name)
                outcome = spinner.succeed
                if config.option.notest:
                    outcome = spinner.skip
                elif status is not None:
                    outcome = spinner.fail
                outcome(env_name)
                if print_out and output is not None:
                    reporter.verbosity0(output)

        threads = deque()
        processes = {}
        todo_keys = set(venv_dict.keys())
        todo = OrderedDict(
            (n, todo_keys & set(v.envconfig.depends)) for n, v in venv_dict.items()
        )
        done = set()
        try:
            while todo:
                for name, depends in list(todo.items()):
                    if depends - done:
                        # skip if has unfinished dependencies
                        continue
                    del todo[name]
                    venv = venv_dict[name]
                    semaphore.acquire(blocking=True)
                    spinner.add(name)
                    thread = Thread(
                        target=run_in_thread, args=(venv, os.environ.copy(), processes)
                    )
                    thread.daemon = True
                    thread.start()
                    threads.append(thread)
                if todo:
                    # wait until someone finishes and retry queuing jobs
                    finished.wait()
                    finished.clear()
            while threads:
                threads = [
                    thread
                    for thread in threads
                    if not thread.join(0.1) and thread.is_alive()
                ]
        except KeyboardInterrupt:
            reporter.verbosity0(
                "[{}] KeyboardInterrupt parallel - stopping children".format(
                    os.getpid()
                )
            )
            while True:
                # do not allow to interrupt until children interrupt
                try:
                    # putting it inside a thread so it's not interrupted
                    stopper = Thread(
                        target=_stop_child_processes, args=(processes, threads)
                    )
                    stopper.start()
                    stopper.join()
                except KeyboardInterrupt:
                    continue
                raise KeyboardInterrupt


# Traceback (AssertionError)
$ tox -p all
⠸ [3] py36 | py37 | py38ERROR: invocation failed (exit code 1), logfile: /home/churchyard/tmp/totox/.tox/py37/log/py37-5.log
=================================================== log start ===================================================
py37 run-test-pre: PYTHONHASHSEED='1114882869'
py37 run-test: commands[0] | python runner.py
py37 run-test-pre: PYTHONHASHSEED='3136963143'
py37 run-test: commands[0] | python -c 'import sys; print(sys.version)'
3.7.5 (default, Oct 17 2019, 12:09:47)
[GCC 9.2.1 20190827 (Red Hat 9.2.1-1)]
Traceback (most recent call last):
File "runner.py", line 8, in <module>
assert 'py36' in cp.stdout
AssertionError
ERROR: InvocationError for command /home/churchyard/tmp/totox/.tox/py37/bin/python runner.py (exited with code 1)

==================================================== log end ====================================================
✖ FAIL py37 in 1.389 seconds
⠴ [2] py36 | py38ERROR: invocation failed (exit code 1), logfile: /home/churchyard/tmp/totox/.tox/py38/log/py38-5.log
=================================================== log start ===================================================
py38 run-test-pre: PYTHONHASHSEED='3502501543'
py38 run-test: commands[0] | python runner.py
py38 run-test-pre: PYTHONHASHSEED='1953872060'
py38 run-test: commands[0] | python -c 'import sys; print(sys.version)'
3.8.0 (default, Oct 16 2019, 12:47:36)
[GCC 9.2.1 20190827 (Red Hat 9.2.1-1)]
Traceback (most recent call last):
File "runner.py", line 8, in <module>
assert 'py36' in cp.stdout
AssertionError
ERROR: InvocationError for command /home/churchyard/tmp/totox/.tox/py38/bin/python runner.py (exited with code 1)

==================================================== log end ====================================================
✖ FAIL py38 in 1.536 seconds
⠇ [1] py36ERROR: invocation failed (exit code 1), logfile: /home/churchyard/tmp/totox/.tox/py36/log/py36-5.log
=================================================== log start ===================================================
py36 run-test-pre: PYTHONHASHSEED='664963699'
py36 run-test: commands[0] | python runner.py
py36 recreate: /home/churchyard/tmp/totox/inner/.tox/py36
py36 run-test-pre: PYTHONHASHSEED='4138548204'
py36 run-test: commands[0] | python -c 'import sys; print(sys.version)'
3.6.9 (default, Jul  3 2019, 15:08:07)
[GCC 9.1.1 20190503 (Red Hat 9.1.1-1)]
Traceback (most recent call last):
File "runner.py", line 9, in <module>
assert 'py37' in cp.stdout
AssertionError
ERROR: InvocationError for command /home/churchyard/tmp/totox/.tox/py36/bin/python runner.py (exited with code 1)

==================================================== log end ====================================================
✖ FAIL py36 in 4.851 seconds
____________________________________________________ summary ____________________________________________________
ERROR:   py36: parallel child exit code 1
ERROR:   py37: parallel child exit code 1
ERROR:   py38: parallel child exit code 1

# Fixed code
def run_parallel(config, venv_dict):
    """here we'll just start parallel sub-processes"""
    live_out = config.option.parallel_live
    disable_spinner = bool(os.environ.get("TOX_PARALLEL_NO_SPINNER") == "1")
    args = [sys.executable, MAIN_FILE] + config.args
    try:
        position = args.index("--")
    except ValueError:
        position = len(args)

    max_parallel = config.option.parallel
    if max_parallel is None:
        max_parallel = len(venv_dict)
    semaphore = Semaphore(max_parallel)
    finished = Event()

    show_progress = (
        not disable_spinner
        and not live_out
        and reporter.verbosity() > reporter.Verbosity.QUIET
    )

    with Spinner(enabled=show_progress) as spinner:

        def run_in_thread(tox_env, os_env, processes):
            output = None
            env_name = tox_env.envconfig.envname
            status = "skipped tests" if config.option.notest else None
            try:
                os_env[str(PARALLEL_ENV_VAR_KEY_PRIVATE)] = str(env_name)
                os_env[str(PARALLEL_ENV_VAR_KEY_PUBLIC)] = str(env_name)
                args_sub = list(args)
                if hasattr(tox_env, "package"):
                    args_sub.insert(position, str(tox_env.package))
                    args_sub.insert(position, "--installpkg")
                if tox_env.get_result_json_path():
                    result_json_index = args_sub.index("--result-json")
                    args_sub[result_json_index + 1] = "{}".format(
                        tox_env.get_result_json_path()
                    )
                with tox_env.new_action("parallel {}".format(tox_env.name)) as action:

                    def collect_process(process):
                        processes[tox_env] = (action, process)

                    print_out = not live_out and tox_env.envconfig.parallel_show_output
                    output = action.popen(
                        args=args_sub,
                        env=os_env,
                        redirect=not live_out,
                        capture_err=print_out,
                        callback=collect_process,
                        returnout=print_out,
                    )

            except InvocationError as err:
                status = "parallel child exit code {}".format(err.exit_code)
            finally:
                semaphore.release()
                finished.set()
                tox_env.status = status
                done.add(env_name)
                outcome = spinner.succeed
                if config.option.notest:
                    outcome = spinner.skip
                elif status is not None:
                    outcome = spinner.fail
                outcome(env_name)
                if print_out and output is not None:
                    reporter.verbosity0(output)

        threads = deque()
        processes = {}
        todo_keys = set(venv_dict.keys())
        todo = OrderedDict(
            (n, todo_keys & set(v.envconfig.depends)) for n, v in venv_dict.items()
        )
        done = set()
        try:
            while todo:
                for name, depends in list(todo.items()):
                    if depends - done:
                        # skip if has unfinished dependencies
                        continue
                    del todo[name]
                    venv = venv_dict[name]
                    semaphore.acquire(blocking=True)
                    spinner.add(name)
                    thread = Thread(
                        target=run_in_thread, args=(venv, os.environ.copy(), processes)
                    )
                    thread.daemon = True
                    thread.start()
                    threads.append(thread)
                if todo:
                    # wait until someone finishes and retry queuing jobs
                    finished.wait()
                    finished.clear()
            while threads:
                threads = [
                    thread
                    for thread in threads
                    if not thread.join(0.1) and thread.is_alive()
                ]
        except KeyboardInterrupt:
            reporter.verbosity0(
                "[{}] KeyboardInterrupt parallel - stopping children".format(
                    os.getpid()
                )
            )
            while True:
                # do not allow to interrupt until children interrupt
                try:
                    # putting it inside a thread so it's not interrupted
                    stopper = Thread(
                        target=_stop_child_processes, args=(processes, threads)
                    )
                    stopper.start()
                    stopper.join()
                except KeyboardInterrupt:
                    continue
                raise KeyboardInterrupt