From 0f0723509c9e91b5d43241b09e225a192872e823 Mon Sep 17 00:00:00 2001 From: Michel Kraemer Date: Sun, 16 Jan 2022 09:24:09 +0100 Subject: [PATCH] Use Worker API to download files in parallel Refers to #138 --- .../gradle/tasks/download/Download.java | 12 +-- .../gradle/tasks/download/DownloadAction.java | 78 +++++++++++++----- .../tasks/download/DownloadExtension.java | 4 +- .../internal/DefaultWorkerExecutorHelper.java | 79 +++++++++++++++++++ .../gradle/tasks/download/internal/Job.java | 15 ++++ .../internal/LegacyWorkerExecutorHelper.java | 51 ++++++++++++ .../internal/WorkerExecutorHelper.java | 45 +++++++++++ .../tasks/download/AuthenticationTest.java | 13 ++- .../tasks/download/ContentLengthTest.java | 7 +- .../tasks/download/DownloadExtensionTest.java | 4 +- .../gradle/tasks/download/OfflineTest.java | 7 +- .../gradle/tasks/download/RedirectTest.java | 12 ++- .../gradle/tasks/download/RetryTest.java | 10 +-- .../gradle/tasks/download/SslTest.java | 7 +- .../gradle/tasks/download/TestBase.java | 15 ++++ .../gradle/tasks/download/TimeoutTest.java | 10 +-- 16 files changed, 317 insertions(+), 52 deletions(-) create mode 100644 src/main/java/de/undercouch/gradle/tasks/download/internal/DefaultWorkerExecutorHelper.java create mode 100644 src/main/java/de/undercouch/gradle/tasks/download/internal/Job.java create mode 100644 src/main/java/de/undercouch/gradle/tasks/download/internal/LegacyWorkerExecutorHelper.java create mode 100644 src/main/java/de/undercouch/gradle/tasks/download/internal/WorkerExecutorHelper.java diff --git a/src/main/java/de/undercouch/gradle/tasks/download/Download.java b/src/main/java/de/undercouch/gradle/tasks/download/Download.java index 5938914b..529022f7 100644 --- a/src/main/java/de/undercouch/gradle/tasks/download/Download.java +++ b/src/main/java/de/undercouch/gradle/tasks/download/Download.java @@ -78,12 +78,12 @@ public Download() { */ @TaskAction public void download() throws IOException { - action.execute(); - - // handle 'upToDate' - if (action.isUpToDate()) { - getState().setDidWork(false); - } + action.execute().thenRun(() -> { + // handle 'upToDate' + if (action.isUpToDate()) { + getState().setDidWork(false); + } + }); } /** diff --git a/src/main/java/de/undercouch/gradle/tasks/download/DownloadAction.java b/src/main/java/de/undercouch/gradle/tasks/download/DownloadAction.java index 866fb0b1..c3e67da5 100644 --- a/src/main/java/de/undercouch/gradle/tasks/download/DownloadAction.java +++ b/src/main/java/de/undercouch/gradle/tasks/download/DownloadAction.java @@ -17,6 +17,7 @@ import de.undercouch.gradle.tasks.download.internal.CachingHttpClientFactory; import de.undercouch.gradle.tasks.download.internal.HttpClientFactory; import de.undercouch.gradle.tasks.download.internal.ProgressLoggerWrapper; +import de.undercouch.gradle.tasks.download.internal.WorkerExecutorHelper; import groovy.json.JsonOutput; import groovy.json.JsonSlurper; import groovy.lang.Closure; @@ -44,10 +45,12 @@ import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.UncheckedIOException; import org.gradle.api.file.Directory; import org.gradle.api.file.ProjectLayout; import org.gradle.api.file.RegularFile; import org.gradle.api.logging.Logger; +import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Provider; import org.gradle.util.GradleVersion; @@ -71,6 +74,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; @@ -92,6 +96,7 @@ public class DownloadAction implements DownloadSpec { private final ProjectLayout projectLayout; private final Logger logger; private final Object servicesOwner; + private final ObjectFactory objectFactory; private final boolean isOffline; private final List sourceObjects = new ArrayList<>(1); private List cachedSources; @@ -139,15 +144,18 @@ public DownloadAction(Project project, @Nullable Task task) { } else { this.servicesOwner = project; } + this.objectFactory = project.getObjects(); this.isOffline = project.getGradle().getStartParameter().isOffline(); this.downloadTaskDir = new File(project.getBuildDir(), "download-task"); } /** * Starts downloading + * @return a {@link CompletableFuture} that completes once the download + * has finished * @throws IOException if the file could not downloaded */ - public void execute() throws IOException { + public CompletableFuture execute() throws IOException { if (GradleVersion.current().compareTo(HARD_MIN_GRADLE_VERSION) < 0 && !quiet) { throw new IllegalStateException("gradle-download-task requires " + "Gradle 5.x or higher"); @@ -181,7 +189,7 @@ public void execute() throws IOException { //make sure build dir exists dest.mkdirs(); } - + if (sources.size() > 1 && !dest.isDirectory()) { if (!dest.exists()) { // create directory automatically @@ -191,18 +199,58 @@ public void execute() throws IOException { + "the destination has to be a directory."); } } - + + WorkerExecutorHelper workerExecutor = WorkerExecutorHelper.newInstance(objectFactory); + CachingHttpClientFactory clientFactory = new CachingHttpClientFactory(); - try { - for (URL src : sources) { - execute(src, clientFactory); + CompletableFuture[] futures = new CompletableFuture[sources.size()]; + for (int i = 0; i < sources.size(); i++) { + URL src = sources.get(i); + + // create progress logger + ProgressLoggerWrapper progressLogger = new ProgressLoggerWrapper(logger); + if (!quiet) { + try { + progressLogger.init(servicesOwner, src.toString()); + } catch (Exception e) { + // unable to get progress logger + logger.error("Unable to get progress logger. Download " + + "progress will not be displayed."); + } } - } finally { - clientFactory.close(); + + // submit download job for asynchronous execution + CompletableFuture f = new CompletableFuture<>(); + futures[i] = f; + workerExecutor.submit(() -> { + try { + execute(src, clientFactory, progressLogger); + f.complete(null); + } catch (Throwable t) { + f.completeExceptionally(t); + throw t; + } + }); } + + // wait for all downloads to finish (necessary if we're on an old + // Gradle version (< 5.6) without Worker API) + if (workerExecutor.needsAwait()) { + workerExecutor.await(); + } + + return CompletableFuture.allOf(futures).whenComplete((v, t) -> { + // always close HTTP client factory + try { + clientFactory.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } - private void execute(URL src, HttpClientFactory clientFactory) throws IOException { + private void execute(URL src, HttpClientFactory clientFactory, + ProgressLoggerWrapper progressLogger) throws IOException { final File destFile = makeDestFile(src); if (!overwrite && destFile.exists()) { if (!quiet) { @@ -229,18 +277,6 @@ private void execute(URL src, HttpClientFactory clientFactory) throws IOExceptio final long timestamp = onlyIfModified && destFile.exists() ? destFile.lastModified() : 0; - // create progress logger - ProgressLoggerWrapper progressLogger = new ProgressLoggerWrapper(logger); - if (!quiet) { - try { - progressLogger.init(servicesOwner, src.toString()); - } catch (Exception e) { - // unable to get progress logger - logger.error("Unable to get progress logger. Download " - + "progress will not be displayed."); - } - } - if ("file".equals(src.getProtocol())) { executeFileProtocol(src, timestamp, destFile, progressLogger); } else { diff --git a/src/main/java/de/undercouch/gradle/tasks/download/DownloadExtension.java b/src/main/java/de/undercouch/gradle/tasks/download/DownloadExtension.java index 7deb1e6e..f3e1d12d 100644 --- a/src/main/java/de/undercouch/gradle/tasks/download/DownloadExtension.java +++ b/src/main/java/de/undercouch/gradle/tasks/download/DownloadExtension.java @@ -41,8 +41,8 @@ public DownloadExtension(Project project) { public DownloadExtension configure(Closure cl) { DownloadAction da = ConfigureUtil.configure(cl, new DownloadAction(project)); try { - da.execute(); - } catch (IOException e) { + da.execute().get(); + } catch (Exception e) { String message = e.getMessage(); if (message == null) { message = "Could not download file"; diff --git a/src/main/java/de/undercouch/gradle/tasks/download/internal/DefaultWorkerExecutorHelper.java b/src/main/java/de/undercouch/gradle/tasks/download/internal/DefaultWorkerExecutorHelper.java new file mode 100644 index 00000000..cac907e6 --- /dev/null +++ b/src/main/java/de/undercouch/gradle/tasks/download/internal/DefaultWorkerExecutorHelper.java @@ -0,0 +1,79 @@ +package de.undercouch.gradle.tasks.download.internal; + +import org.gradle.api.UncheckedIOException; +import org.gradle.api.provider.Property; +import org.gradle.workers.WorkAction; +import org.gradle.workers.WorkParameters; +import org.gradle.workers.WorkQueue; +import org.gradle.workers.WorkerExecutor; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Default implementation of {@link WorkerExecutorHelper} that executes + * {@link Job}s asynchronously using the Gradle Worker API + * @author Michel Kraemer + */ +@SuppressWarnings("UnstableApiUsage") +public class DefaultWorkerExecutorHelper extends WorkerExecutorHelper { + /** + * A unique ID for jobs. Used to access jobs in {@link #jobs} + */ + private static final AtomicInteger UNIQUE_ID = new AtomicInteger(); + + /** + * A maps of jobs submitted to this executor + */ + private static final Map jobs = new ConcurrentHashMap<>(); + + private final WorkerExecutor workerExecutor; + private final WorkQueue workQueue; + + /** + * Constructs a new executor + * @param workerExecutor the Gradle Worker API executor + */ + @Inject + public DefaultWorkerExecutorHelper(WorkerExecutor workerExecutor) { + this.workerExecutor = workerExecutor; + this.workQueue = workerExecutor.noIsolation(); + } + + @Override + public void submit(Job job) { + int id = UNIQUE_ID.getAndIncrement(); + jobs.put(id, job); + workQueue.submit(DefaultWorkAction.class, parameters -> + parameters.getID().set(id)); + } + + @Override + public void await() { + workerExecutor.await(); + } + + @Override + public boolean needsAwait() { + return false; + } + + public interface DefaultWorkParameters extends WorkParameters { + Property getID(); + } + + public static abstract class DefaultWorkAction implements WorkAction { + @Override + public void execute() { + Job job = jobs.remove(getParameters().getID().get()); + try { + job.run(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } +} diff --git a/src/main/java/de/undercouch/gradle/tasks/download/internal/Job.java b/src/main/java/de/undercouch/gradle/tasks/download/internal/Job.java new file mode 100644 index 00000000..0166c791 --- /dev/null +++ b/src/main/java/de/undercouch/gradle/tasks/download/internal/Job.java @@ -0,0 +1,15 @@ +package de.undercouch.gradle.tasks.download.internal; + +import java.io.IOException; + +/** + * An asynchronous job executed by a {@link WorkerExecutorHelper} + * @author Michel Kraemer + */ +public interface Job { + /** + * Execute the job + * @throws IOException if the job failed + */ + void run() throws IOException; +} diff --git a/src/main/java/de/undercouch/gradle/tasks/download/internal/LegacyWorkerExecutorHelper.java b/src/main/java/de/undercouch/gradle/tasks/download/internal/LegacyWorkerExecutorHelper.java new file mode 100644 index 00000000..b288409b --- /dev/null +++ b/src/main/java/de/undercouch/gradle/tasks/download/internal/LegacyWorkerExecutorHelper.java @@ -0,0 +1,51 @@ +package de.undercouch.gradle.tasks.download.internal; + +import java.io.IOException; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * Executes jobs asynchronously with an {@link ExecutorService} on Gradle + * versions where the Worker API is not available + * @author Michel Kraemer + */ +public class LegacyWorkerExecutorHelper extends WorkerExecutorHelper { + private final ExecutorService executorService = Executors.newWorkStealingPool(); + private final Queue> futures = new ConcurrentLinkedQueue<>(); + + @Override + public void submit(Job job) { + CompletableFuture f = new CompletableFuture<>(); + futures.add(f); + executorService.submit(() -> { + try { + job.run(); + f.complete(null); + futures.remove(f); + } catch (IOException e) { + f.completeExceptionally(e); + } + }); + } + + @Override + public void await() { + Future f; + while ((f = futures.poll()) != null) { + try { + f.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public boolean needsAwait() { + return true; + } +} diff --git a/src/main/java/de/undercouch/gradle/tasks/download/internal/WorkerExecutorHelper.java b/src/main/java/de/undercouch/gradle/tasks/download/internal/WorkerExecutorHelper.java new file mode 100644 index 00000000..4b0947e3 --- /dev/null +++ b/src/main/java/de/undercouch/gradle/tasks/download/internal/WorkerExecutorHelper.java @@ -0,0 +1,45 @@ +package de.undercouch.gradle.tasks.download.internal; + +import org.gradle.api.model.ObjectFactory; +import org.gradle.util.GradleVersion; + +/** + * Executes jobs asynchronously. Either uses the Gradle Worker API (if + * available) or falls back to a legacy implementation using an + * {@link java.util.concurrent.ExecutorService}. + * @author Michel Kraemer + */ +public abstract class WorkerExecutorHelper { + /** + * Creates a new instance of the {@link WorkerExecutorHelper} depending + * on the Gradle version + * @param objectFactory creates Gradle model objects + * @return the helper + */ + public static WorkerExecutorHelper newInstance(ObjectFactory objectFactory) { + if (GradleVersion.current().getBaseVersion().compareTo(GradleVersion.version("5.6")) >= 0) { + return objectFactory.newInstance(DefaultWorkerExecutorHelper.class); + } + return new LegacyWorkerExecutorHelper(); + } + + /** + * Execute a job asynchronously + * @param job the job to execute + */ + public abstract void submit(Job job); + + /** + * Wait for all jobs of the current build operation to complete + */ + public abstract void await(); + + /** + * Returns {@code true} if {@link #await()} MUST be called at the end of + * the task. This mostly applies to Gradle versions that don't have a + * Worker API and therefore cannot let the task continue to run in parallel + * to others. + * @return {@code true} if {@link #await()} must be called + */ + public abstract boolean needsAwait(); +} diff --git a/src/test/java/de/undercouch/gradle/tasks/download/AuthenticationTest.java b/src/test/java/de/undercouch/gradle/tasks/download/AuthenticationTest.java index 6d510ffc..ae68da14 100644 --- a/src/test/java/de/undercouch/gradle/tasks/download/AuthenticationTest.java +++ b/src/test/java/de/undercouch/gradle/tasks/download/AuthenticationTest.java @@ -16,7 +16,9 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.digest.DigestUtils; -import org.gradle.api.UncheckedIOException; +import org.apache.hc.client5.http.ClientProtocolException; +import org.assertj.core.api.Assertions; +import org.gradle.workers.WorkerExecutionException; import org.junit.jupiter.api.Test; import javax.servlet.http.HttpServletResponse; @@ -59,7 +61,10 @@ public void noAuthorization() throws Exception { File dst = newTempFile(); t.dest(dst); - assertThatThrownBy(() -> execute(t)).isInstanceOf(UncheckedIOException.class); + Assertions.setMaxStackTraceElementsDisplayed(1000); + assertThatThrownBy(() -> execute(t)) + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(ClientProtocolException.class); } /** @@ -90,7 +95,9 @@ public void invalidCredentials() throws Exception { t.username(wrongUser); t.password(wrongPass); - assertThatThrownBy(() -> execute(t)).isInstanceOf(UncheckedIOException.class); + assertThatThrownBy(() -> execute(t)) + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(ClientProtocolException.class); } /** diff --git a/src/test/java/de/undercouch/gradle/tasks/download/ContentLengthTest.java b/src/test/java/de/undercouch/gradle/tasks/download/ContentLengthTest.java index 72ab3daa..d582f0de 100644 --- a/src/test/java/de/undercouch/gradle/tasks/download/ContentLengthTest.java +++ b/src/test/java/de/undercouch/gradle/tasks/download/ContentLengthTest.java @@ -14,7 +14,8 @@ package de.undercouch.gradle.tasks.download; -import org.gradle.api.UncheckedIOException; +import org.apache.hc.core5.http.ConnectionClosedException; +import org.gradle.workers.WorkerExecutionException; import org.junit.jupiter.api.Test; import java.io.File; @@ -96,6 +97,8 @@ public void tooLargeContentLength() throws Exception { t.src(wireMock.url(testFileName)); File dst = newTempFile(); t.dest(dst); - assertThatThrownBy(() -> execute(t)).isInstanceOf(UncheckedIOException.class); + assertThatThrownBy(() -> execute(t)) + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(ConnectionClosedException.class); } } diff --git a/src/test/java/de/undercouch/gradle/tasks/download/DownloadExtensionTest.java b/src/test/java/de/undercouch/gradle/tasks/download/DownloadExtensionTest.java index 32cebe82..0a335fa8 100644 --- a/src/test/java/de/undercouch/gradle/tasks/download/DownloadExtensionTest.java +++ b/src/test/java/de/undercouch/gradle/tasks/download/DownloadExtensionTest.java @@ -15,6 +15,7 @@ package de.undercouch.gradle.tasks.download; import groovy.lang.Closure; +import org.apache.hc.client5.http.ClientProtocolException; import org.gradle.api.Project; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -98,6 +99,7 @@ public void downloadSingleFileError() throws Exception { String src = wireMock.url("foobar.txt"); File dst = newTempFile(); assertThatThrownBy(() -> doDownload(t.getProject(), src, dst)) - .isInstanceOf(IllegalStateException.class); + .isInstanceOf(IllegalStateException.class) + .hasRootCauseInstanceOf(ClientProtocolException.class); } } diff --git a/src/test/java/de/undercouch/gradle/tasks/download/OfflineTest.java b/src/test/java/de/undercouch/gradle/tasks/download/OfflineTest.java index 6e81e3f3..992b3580 100644 --- a/src/test/java/de/undercouch/gradle/tasks/download/OfflineTest.java +++ b/src/test/java/de/undercouch/gradle/tasks/download/OfflineTest.java @@ -14,7 +14,8 @@ package de.undercouch.gradle.tasks.download; -import org.gradle.api.UncheckedIOException; +import org.apache.hc.client5.http.ClientProtocolException; +import org.gradle.workers.WorkerExecutionException; import org.junit.jupiter.api.Test; import java.io.File; @@ -57,7 +58,9 @@ public void offlineFail() { t.src(wireMock.baseUrl()); File dst = new File(folder.toFile(), "offlineFail"); t.dest(dst); - assertThatThrownBy(() -> execute(t)).isInstanceOf(UncheckedIOException.class); + assertThatThrownBy(() -> execute(t)) + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(ClientProtocolException.class); } /** diff --git a/src/test/java/de/undercouch/gradle/tasks/download/RedirectTest.java b/src/test/java/de/undercouch/gradle/tasks/download/RedirectTest.java index a7c6cced..5f7fd00c 100644 --- a/src/test/java/de/undercouch/gradle/tasks/download/RedirectTest.java +++ b/src/test/java/de/undercouch/gradle/tasks/download/RedirectTest.java @@ -22,7 +22,9 @@ import com.github.tomakehurst.wiremock.http.Response; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.github.tomakehurst.wiremock.matching.UrlPattern; -import org.gradle.api.UncheckedIOException; +import org.apache.hc.client5.http.CircularRedirectException; +import org.apache.hc.client5.http.RedirectException; +import org.gradle.workers.WorkerExecutionException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -162,7 +164,9 @@ public void circularRedirect() throws Exception { t.src(wireMock.url(REDIRECT)); File dst = newTempFile(); t.dest(dst); - assertThatThrownBy(() -> execute(t)).isInstanceOf(UncheckedIOException.class); + assertThatThrownBy(() -> execute(t)) + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(CircularRedirectException.class); } /** @@ -182,6 +186,8 @@ public void tooManyRedirects() throws Exception { t.src(redirectWireMock.url(REDIRECT) + "?r=52"); File dst = newTempFile(); t.dest(dst); - assertThatThrownBy(() -> execute(t)).isInstanceOf(UncheckedIOException.class); + assertThatThrownBy(() -> execute(t)) + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(RedirectException.class); } } diff --git a/src/test/java/de/undercouch/gradle/tasks/download/RetryTest.java b/src/test/java/de/undercouch/gradle/tasks/download/RetryTest.java index bf78072b..6ed2e492 100644 --- a/src/test/java/de/undercouch/gradle/tasks/download/RetryTest.java +++ b/src/test/java/de/undercouch/gradle/tasks/download/RetryTest.java @@ -16,7 +16,7 @@ import com.github.tomakehurst.wiremock.http.Fault; import org.apache.hc.core5.http.NoHttpResponseException; -import org.gradle.api.UncheckedIOException; +import org.gradle.workers.WorkerExecutionException; import org.junit.jupiter.api.Test; import java.io.File; @@ -65,8 +65,8 @@ public void retryDefault() throws Exception { File dst = newTempFile(); t.dest(dst); assertThatThrownBy(() -> execute(t)) - .isInstanceOf(UncheckedIOException.class) - .hasCauseInstanceOf(NoHttpResponseException.class); + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(NoHttpResponseException.class); verify(1, getRequestedFor(urlEqualTo("/" + TEST_FILE_NAME))); } @@ -211,8 +211,8 @@ public void retryTwo() throws Exception { File dst = newTempFile(); t.dest(dst); assertThatThrownBy(() -> execute(t)) - .isInstanceOf(UncheckedIOException.class) - .hasCauseInstanceOf(NoHttpResponseException.class); + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(NoHttpResponseException.class); verify(3, getRequestedFor(urlEqualTo("/" + TEST_FILE_NAME))); } diff --git a/src/test/java/de/undercouch/gradle/tasks/download/SslTest.java b/src/test/java/de/undercouch/gradle/tasks/download/SslTest.java index b5729e11..b3faf15f 100644 --- a/src/test/java/de/undercouch/gradle/tasks/download/SslTest.java +++ b/src/test/java/de/undercouch/gradle/tasks/download/SslTest.java @@ -15,12 +15,13 @@ package de.undercouch.gradle.tasks.download; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; -import org.gradle.api.UncheckedIOException; +import org.gradle.workers.WorkerExecutionException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import java.io.File; import java.nio.charset.StandardCharsets; +import java.security.cert.CertPathBuilderException; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; @@ -79,6 +80,8 @@ public void unknownCertificate() throws Exception { File dst = newTempFile(); t.dest(dst); assertThat(t.isAcceptAnyCertificate()).isFalse(); - assertThatThrownBy(() -> execute(t)).isInstanceOf(UncheckedIOException.class); + assertThatThrownBy(() -> execute(t)) + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(CertPathBuilderException.class); } } diff --git a/src/test/java/de/undercouch/gradle/tasks/download/TestBase.java b/src/test/java/de/undercouch/gradle/tasks/download/TestBase.java index 127b0d1e..0fd2e00e 100644 --- a/src/test/java/de/undercouch/gradle/tasks/download/TestBase.java +++ b/src/test/java/de/undercouch/gradle/tasks/download/TestBase.java @@ -14,9 +14,14 @@ package de.undercouch.gradle.tasks.download; +import de.undercouch.gradle.tasks.download.internal.WorkerExecutorHelper; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.internal.operations.BuildOperationCategory; +import org.gradle.internal.operations.BuildOperationDescriptor; +import org.gradle.internal.operations.BuildOperationState; +import org.gradle.internal.operations.CurrentBuildOperationRef; import org.gradle.testfixtures.ProjectBuilder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; @@ -89,6 +94,15 @@ protected Download makeProjectAndTask(@Nullable Action projectConfigura Map taskParams = new HashMap<>(); taskParams.put("type", Download.class); + + // start parent build operation + BuildOperationState op = new BuildOperationState( + BuildOperationDescriptor.displayName("") + .metadata(BuildOperationCategory.TASK) + .build(), 0); + op.setRunning(true); + CurrentBuildOperationRef.instance().set(op); + return (Download)project.task(taskParams, "downloadFile"); } @@ -97,5 +111,6 @@ protected void execute(Task t) { for (Action a : actions) { a.execute(t); } + WorkerExecutorHelper.newInstance(t.getProject().getObjects()).await(); } } diff --git a/src/test/java/de/undercouch/gradle/tasks/download/TimeoutTest.java b/src/test/java/de/undercouch/gradle/tasks/download/TimeoutTest.java index d514c6f1..8e546651 100644 --- a/src/test/java/de/undercouch/gradle/tasks/download/TimeoutTest.java +++ b/src/test/java/de/undercouch/gradle/tasks/download/TimeoutTest.java @@ -15,7 +15,7 @@ package de.undercouch.gradle.tasks.download; import org.apache.hc.client5.http.ConnectTimeoutException; -import org.gradle.api.UncheckedIOException; +import org.gradle.workers.WorkerExecutionException; import org.junit.jupiter.api.Test; import java.io.File; @@ -55,8 +55,8 @@ public void readTimeout() throws Exception { File dst = newTempFile(); t.dest(dst); assertThatThrownBy(() -> execute(t)) - .isInstanceOf(UncheckedIOException.class) - .hasCauseInstanceOf(SocketTimeoutException.class); + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(SocketTimeoutException.class); } /** @@ -73,8 +73,8 @@ public void connectTimeout() throws Exception { t.dest(dst); long start = System.currentTimeMillis(); assertThatThrownBy(() -> execute(t)) - .isInstanceOf(UncheckedIOException.class) - .hasCauseInstanceOf(ConnectTimeoutException.class); + .isInstanceOf(WorkerExecutionException.class) + .hasRootCauseInstanceOf(ConnectTimeoutException.class); long end = System.currentTimeMillis(); assertThat(end).isCloseTo(start, offset(TIMEOUT_MS * 2L)); }