private @NotNull CompletableFuture<R> execFromEDT(@NotNull CompletableFuture<? extends @NotNull ProgressIndicator> progressFuture,
                                                    @NotNull Semaphore modalityEntered,
                                                    @NotNull Function<ProgressIndicator, R> onThreadCallable) {
    CompletableFuture<R> taskFuture = launchTask(onThreadCallable, progressFuture);
    CompletableFuture<R> resultFuture;

    if (isModal) {
      // Running task with blocking EDT event pumping has the following contract in test mode:
      //   if a random EDT event processed by blockingPI fails with an exception, the event pumping
      //   is stopped and the submitted task fails with respective exception.
      // The task submitted to e.g. POOLED thread might not be able to finish at all because it requires invoke&waits,
      //   but EDT is broken due an exception. Hence, initial task should be completed exceptionally
      CompletableFuture<Void> blockingRunFuture = progressFuture.thenAccept(progressIndicator -> {
        if (progressIndicator instanceof BlockingProgressIndicator) {
          //noinspection deprecation
          ((BlockingProgressIndicator)progressIndicator).startBlocking(modalityEntered::up, taskFuture);
        }
        else {
          Logger.getInstance(ProgressRunner.class).warn("Can't go modal without BlockingProgressIndicator");
          modalityEntered.up();
        }
      });
      blockingRunFuture.exceptionally(throwable -> {
        taskFuture.completeExceptionally(throwable);
        return null;
      });
      // `startBlocking` might throw unrelated exceptions execute the submitted task so potential failure should be handled.
      // Relates to testMode-ish execution: unhandled exceptions on event pumping are `LOG.error`-ed, hence throw exceptions in tests.
      resultFuture = taskFuture.thenCombine(blockingRunFuture, (r, __) -> r);
    }
    else {
      resultFuture = taskFuture;
    }

    if (isSync) {
      // At first here was a blocking resultFuture.get() call,
      // which lead to deadlocks when `BlockingProgressIndicator.startBlocking` already exited,
      // and nobody was dispatching EDT events because the current thread (EDT) was blocked on this future,
      // so instead of blocking, we assert that the future should be done at this point,
      // otherwise, `startBlocking` should continue pumping the events to give cancelled tasks a chance to complete.
      if (!resultFuture.isDone()) {
        throw new IllegalStateException("Result future must be done at this point");
      }
    }
    return resultFuture;
  }
private @NotNull CompletableFuture<R> execFromEDT(@NotNull CompletableFuture<? extends @NotNull ProgressIndicator> progressFuture,
                                                    @NotNull Semaphore modalityEntered,
                                                    @NotNull Function<ProgressIndicator, R> onThreadCallable)