From 0f812eb5e561cc5415d0c9931675e58dc37a5850 Mon Sep 17 00:00:00 2001 From: Chi Wang Date: Tue, 6 Jul 2021 01:14:43 -0700 Subject: [PATCH] Remote: Display download progress when actions are downloading outputs from remote cache. Normally, when executing action with remote cache/execution, the UI only display the "remote"/"remote-cache" strategy: ``` [500 / 1000] 500 actions, 3 running [Sched] Executing genrule //:test-1; Executing genrule //:test-2; 2s remote Executing genrule //:test-3; 3s remote ... ``` However, it doesn't tell users what is happening under the hood. #13555 fixed the confusion which the UI display the action is scheduling while it is actually downloading the outputs. With this change, Bazel will display the downloads if action is downloading outputs. e.g. ``` [500 / 1000] 500 actions, 3 running [Sched] Executing genrule //:test-1; 1s remote Executing genrule //:test-2; Downloading 2.out, 20.1 KiB / 100 KiB; 2s remote Executing genrule //:test-3; 3s remote ... ``` Add a generic `ActionProgressEvent` which can be reported within action execution to display detailed execution progress for that action. Closes #13557. PiperOrigin-RevId: 383224334 --- .../lib/actions/ActionProgressEvent.java | 39 ++++++ .../build/lib/exec/AbstractSpawnStrategy.java | 1 - .../com/google/devtools/build/lib/exec/BUILD | 1 + .../build/lib/exec/SpawnProgressEvent.java | 43 ++++++ .../build/lib/remote/RemoteCache.java | 127 +++++++++++++++++- .../lib/remote/RemoteExecutionService.java | 3 +- .../remote/common/ProgressStatusListener.java | 29 ++++ .../devtools/build/lib/remote/util/Utils.java | 28 ++++ .../build/lib/runtime/UiEventHandler.java | 8 ++ .../build/lib/runtime/UiStateTracker.java | 78 ++++++++++- .../build/lib/remote/GrpcCacheClientTest.java | 35 ++++- .../build/lib/remote/RemoteCacheTests.java | 116 +++++++++++++--- .../lib/remote/RemoteSpawnCacheTest.java | 6 +- .../lib/remote/RemoteSpawnRunnerTest.java | 15 ++- .../build/lib/remote/util/UtilsTest.java | 39 ++++++ .../build/lib/runtime/UiStateTrackerTest.java | 87 ++++++++++++ 16 files changed, 619 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/google/devtools/build/lib/actions/ActionProgressEvent.java create mode 100644 src/main/java/com/google/devtools/build/lib/exec/SpawnProgressEvent.java create mode 100644 src/main/java/com/google/devtools/build/lib/remote/common/ProgressStatusListener.java create mode 100644 src/test/java/com/google/devtools/build/lib/remote/util/UtilsTest.java diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionProgressEvent.java b/src/main/java/com/google/devtools/build/lib/actions/ActionProgressEvent.java new file mode 100644 index 00000000000000..72e450284ecc73 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/actions/ActionProgressEvent.java @@ -0,0 +1,39 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.actions; + +import com.google.auto.value.AutoValue; +import com.google.devtools.build.lib.events.ExtendedEventHandler.ProgressLike; + +/** Notifications for the progress of an in-flight action. */ +@AutoValue +public abstract class ActionProgressEvent implements ProgressLike { + + public static ActionProgressEvent create( + ActionExecutionMetadata action, String progressId, String progress, boolean finished) { + return new AutoValue_ActionProgressEvent(action, progressId, progress, finished); + } + + /** Gets the metadata associated with the action being scheduled. */ + public abstract ActionExecutionMetadata action(); + + /** The id that uniquely determines the progress among all progress events within an action. */ + public abstract String progressId(); + + /** Human readable description of the progress. */ + public abstract String progress(); + + /** Whether the download progress reported about is finished already. */ + public abstract boolean finished(); +} diff --git a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java index 390ca1e126fcf8..a56b83940c09b8 100644 --- a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java @@ -328,7 +328,6 @@ public void report(ProgressStatus progress) { return; } - // TODO(ulfjack): We should report more details to the UI. ExtendedEventHandler eventHandler = actionExecutionContext.getEventHandler(); progress.postTo(eventHandler, action); } diff --git a/src/main/java/com/google/devtools/build/lib/exec/BUILD b/src/main/java/com/google/devtools/build/lib/exec/BUILD index 62e9e8c7e92d2c..50abdb52a2747c 100644 --- a/src/main/java/com/google/devtools/build/lib/exec/BUILD +++ b/src/main/java/com/google/devtools/build/lib/exec/BUILD @@ -269,6 +269,7 @@ java_library( srcs = [ "SpawnCheckingCacheEvent.java", "SpawnExecutingEvent.java", + "SpawnProgressEvent.java", "SpawnRunner.java", "SpawnSchedulingEvent.java", ], diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnProgressEvent.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnProgressEvent.java new file mode 100644 index 00000000000000..9ddde03a1e1497 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnProgressEvent.java @@ -0,0 +1,43 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.exec; + +import com.google.auto.value.AutoValue; +import com.google.devtools.build.lib.actions.ActionExecutionMetadata; +import com.google.devtools.build.lib.actions.ActionProgressEvent; +import com.google.devtools.build.lib.events.ExtendedEventHandler; +import com.google.devtools.build.lib.exec.SpawnRunner.ProgressStatus; + +/** The {@link SpawnRunner} is making some progress. */ +@AutoValue +public abstract class SpawnProgressEvent implements ProgressStatus { + + public static SpawnProgressEvent create(String resourceId, String progress, boolean finished) { + return new AutoValue_SpawnProgressEvent(resourceId, progress, finished); + } + + /** The id that uniquely determines the progress among all progress events for this spawn. */ + abstract String progressId(); + + /** Human readable description of the progress. */ + abstract String progress(); + + /** Whether the progress reported about is finished already. */ + abstract boolean finished(); + + @Override + public void postTo(ExtendedEventHandler eventHandler, ActionExecutionMetadata action) { + eventHandler.post(ActionProgressEvent.create(action, progressId(), progress(), finished())); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java index 2ac802907e6fbb..d978ad6250dea9 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java @@ -15,6 +15,8 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static com.google.devtools.build.lib.remote.common.ProgressStatusListener.NO_ACTION; +import static com.google.devtools.build.lib.remote.util.Utils.bytesCountToDisplayString; import static com.google.devtools.build.lib.remote.util.Utils.getFromFuture; import build.bazel.remote.execution.v2.Action; @@ -50,6 +52,7 @@ import com.google.devtools.build.lib.actions.UserExecException; import com.google.devtools.build.lib.actions.cache.MetadataInjector; import com.google.devtools.build.lib.concurrent.ThreadSafety; +import com.google.devtools.build.lib.exec.SpawnProgressEvent; import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionContext; import com.google.devtools.build.lib.profiler.Profiler; import com.google.devtools.build.lib.profiler.SilentCloseable; @@ -58,6 +61,7 @@ import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.SymlinkMetadata; import com.google.devtools.build.lib.remote.common.LazyFileOutputStream; import com.google.devtools.build.lib.remote.common.OutputDigestMismatchException; +import com.google.devtools.build.lib.remote.common.ProgressStatusListener; import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext; import com.google.devtools.build.lib.remote.common.RemoteActionFileArtifactValue; import com.google.devtools.build.lib.remote.common.RemoteCacheClient; @@ -91,6 +95,9 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -314,6 +321,50 @@ private static Path toTmpDownloadPath(Path actualPath) { return actualPath.getParentDirectory().getRelative(actualPath.getBaseName() + ".tmp"); } + static class DownloadProgressReporter { + private static final Pattern PATTERN = Pattern.compile("^bazel-out/[^/]+/[^/]+/"); + private final ProgressStatusListener listener; + private final String id; + private final String file; + private final String totalSize; + private final AtomicLong downloadedBytes = new AtomicLong(0); + + DownloadProgressReporter(ProgressStatusListener listener, String file, long totalSize) { + this.listener = listener; + this.id = file; + this.totalSize = bytesCountToDisplayString(totalSize); + + Matcher matcher = PATTERN.matcher(file); + this.file = matcher.replaceFirst(""); + } + + void started() { + reportProgress(false, false); + } + + void downloadedBytes(int count) { + downloadedBytes.addAndGet(count); + reportProgress(true, false); + } + + void finished() { + reportProgress(true, true); + } + + private void reportProgress(boolean includeBytes, boolean finished) { + String progress; + if (includeBytes) { + progress = + String.format( + "Downloading %s, %s / %s", + file, bytesCountToDisplayString(downloadedBytes.get()), totalSize); + } else { + progress = String.format("Downloading %s", file); + } + listener.onProgressStatus(SpawnProgressEvent.create(id, progress, finished)); + } + } + /** * Download the output files and directory trees of a remotely executed action to the local * machine, as well stdin / stdout to the given files. @@ -330,7 +381,8 @@ public void download( RemotePathResolver remotePathResolver, ActionResult result, FileOutErr origOutErr, - OutputFilesLocker outputFilesLocker) + OutputFilesLocker outputFilesLocker, + ProgressStatusListener progressStatusListener) throws ExecException, IOException, InterruptedException { ActionResultMetadata metadata = parseActionResultMetadata(context, remotePathResolver, result); @@ -347,7 +399,11 @@ public void download( context, remotePathResolver.localPathToOutputPath(file.path()), toTmpDownloadPath(file.path()), - file.digest()); + file.digest(), + new DownloadProgressReporter( + progressStatusListener, + remotePathResolver.localPathToOutputPath(file.path()), + file.digest().getSizeBytes())); return Futures.transform(download, (d) -> file, directExecutor()); } catch (IOException e) { return Futures.immediateFailedFuture(e); @@ -499,10 +555,14 @@ private void createSymlinks(Iterable symlinks) throws IOExcepti } public ListenableFuture downloadFile( - RemoteActionExecutionContext context, String outputPath, Path localPath, Digest digest) + RemoteActionExecutionContext context, + String outputPath, + Path localPath, + Digest digest, + DownloadProgressReporter reporter) throws IOException { SettableFuture outerF = SettableFuture.create(); - ListenableFuture f = downloadFile(context, localPath, digest); + ListenableFuture f = downloadFile(context, localPath, digest, reporter); Futures.addCallback( f, new FutureCallback() { @@ -529,6 +589,16 @@ public void onFailure(Throwable throwable) { /** Downloads a file (that is not a directory). The content is fetched from the digest. */ public ListenableFuture downloadFile( RemoteActionExecutionContext context, Path path, Digest digest) throws IOException { + return downloadFile(context, path, digest, new DownloadProgressReporter(NO_ACTION, "", 0)); + } + + /** Downloads a file (that is not a directory). The content is fetched from the digest. */ + public ListenableFuture downloadFile( + RemoteActionExecutionContext context, + Path path, + Digest digest, + DownloadProgressReporter reporter) + throws IOException { Preconditions.checkNotNull(path.getParentDirectory()).createDirectoryAndParents(); if (digest.getSizeBytes() == 0) { // Handle empty file locally. @@ -549,7 +619,9 @@ public ListenableFuture downloadFile( return COMPLETED_SUCCESS; } - OutputStream out = new LazyFileOutputStream(path); + reporter.started(); + OutputStream out = new ReportingOutputStream(new LazyFileOutputStream(path), reporter); + SettableFuture outerF = SettableFuture.create(); ListenableFuture f = cacheProtocol.downloadBlob(context, digest, out); Futures.addCallback( @@ -560,6 +632,7 @@ public void onSuccess(Void result) { try { out.close(); outerF.set(null); + reporter.finished(); } catch (IOException e) { outerF.setException(e); } catch (RuntimeException e) { @@ -572,6 +645,7 @@ public void onSuccess(Void result) { public void onFailure(Throwable t) { try { out.close(); + reporter.finished(); } catch (IOException e) { if (t != e) { t.addSuppressed(e); @@ -1100,6 +1174,49 @@ private static FailureDetail createFailureDetail(String message, Code detailedCo .build(); } + /** + * An {@link OutputStream} that reports all the write operations with {@link + * DownloadProgressReporter}. + */ + private static class ReportingOutputStream extends OutputStream { + + private final OutputStream out; + private final DownloadProgressReporter reporter; + + ReportingOutputStream(OutputStream out, DownloadProgressReporter reporter) { + this.out = out; + this.reporter = reporter; + } + + @Override + public void write(byte[] b) throws IOException { + out.write(b); + reporter.downloadedBytes(b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + reporter.downloadedBytes(len); + } + + @Override + public void write(int b) throws IOException { + out.write(b); + reporter.downloadedBytes(1); + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void close() throws IOException { + out.close(); + } + } + /** In-memory representation of action result metadata. */ static class ActionResultMetadata { diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java index 6196a7ceb889cc..22e708610a91a4 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java @@ -386,7 +386,8 @@ public InMemoryOutput downloadOutputs(RemoteAction action, RemoteActionResult re remotePathResolver, result.actionResult, action.spawnExecutionContext.getFileOutErr(), - action.spawnExecutionContext::lockOutputFiles); + action.spawnExecutionContext::lockOutputFiles, + action.spawnExecutionContext::report); } else { PathFragment inMemoryOutputPath = getInMemoryOutputPath(action.spawn); inMemoryOutput = diff --git a/src/main/java/com/google/devtools/build/lib/remote/common/ProgressStatusListener.java b/src/main/java/com/google/devtools/build/lib/remote/common/ProgressStatusListener.java new file mode 100644 index 00000000000000..df5a8beae54758 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/remote/common/ProgressStatusListener.java @@ -0,0 +1,29 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.remote.common; + +import com.google.devtools.build.lib.exec.SpawnRunner.ProgressStatus; + +/** An interface that is used to receive {@link ProgressStatus} updates during spawn execution. */ +@FunctionalInterface +public interface ProgressStatusListener { + + void onProgressStatus(ProgressStatus progress); + + /** A {@link ProgressStatusListener} that does nothing. */ + ProgressStatusListener NO_ACTION = + progress -> { + // Intentionally left empty + }; +} diff --git a/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java b/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java index 1a2d8093196f55..acacffd1759e5b 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java +++ b/src/main/java/com/google/devtools/build/lib/remote/util/Utils.java @@ -22,6 +22,7 @@ import com.google.common.base.Ascii; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.AsyncCallable; import com.google.common.util.concurrent.FluentFuture; @@ -61,6 +62,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.text.DecimalFormat; import java.util.Arrays; import java.util.Collection; import java.util.Locale; @@ -509,4 +511,30 @@ public static V refreshIfUnauthenticated( throw new AssertionError(e); } } + + private static final ImmutableList UNITS = ImmutableList.of("KiB", "MiB", "GiB", "TiB"); + + /** + * Converts the number of bytes to a human readable string, e.g. 1024 -> 1 KiB. + * + *

Negative numbers are not allowed. + */ + public static String bytesCountToDisplayString(long bytes) { + Preconditions.checkArgument(bytes >= 0); + + if (bytes < 1024) { + return bytes + " B"; + } + + int unitIndex = 0; + long value = bytes; + while ((unitIndex + 1) < UNITS.size() && value >= (1 << 20)) { + value >>= 10; + unitIndex++; + } + + // Format as single digit decimal number, but skipping the trailing .0. + DecimalFormat fmt = new DecimalFormat("0.#"); + return String.format("%s %s", fmt.format(value / 1024.0), UNITS.get(unitIndex)); + } } diff --git a/src/main/java/com/google/devtools/build/lib/runtime/UiEventHandler.java b/src/main/java/com/google/devtools/build/lib/runtime/UiEventHandler.java index cb525751896d52..1c56e04ed90ca8 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/UiEventHandler.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/UiEventHandler.java @@ -20,6 +20,7 @@ import com.google.common.primitives.Bytes; import com.google.common.util.concurrent.Uninterruptibles; import com.google.devtools.build.lib.actions.ActionCompletionEvent; +import com.google.devtools.build.lib.actions.ActionProgressEvent; import com.google.devtools.build.lib.actions.ActionScanningCompletedEvent; import com.google.devtools.build.lib.actions.ActionStartedEvent; import com.google.devtools.build.lib.actions.CachingActionEvent; @@ -693,6 +694,13 @@ public void runningAction(RunningActionEvent event) { refresh(); } + @Subscribe + @AllowConcurrentEvents + public void actionProgress(ActionProgressEvent event) { + stateTracker.actionProgress(event); + refresh(); + } + @Subscribe @AllowConcurrentEvents public void actionCompletion(ActionScanningCompletedEvent event) { diff --git a/src/main/java/com/google/devtools/build/lib/runtime/UiStateTracker.java b/src/main/java/com/google/devtools/build/lib/runtime/UiStateTracker.java index 4fa69856d9bac8..c709eb36316840 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/UiStateTracker.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/UiStateTracker.java @@ -25,6 +25,7 @@ import com.google.devtools.build.lib.actions.Action; import com.google.devtools.build.lib.actions.ActionCompletionEvent; import com.google.devtools.build.lib.actions.ActionExecutionMetadata; +import com.google.devtools.build.lib.actions.ActionProgressEvent; import com.google.devtools.build.lib.actions.ActionScanningCompletedEvent; import com.google.devtools.build.lib.actions.ActionStartedEvent; import com.google.devtools.build.lib.actions.Artifact; @@ -58,6 +59,7 @@ import java.util.Comparator; import java.util.Deque; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.PriorityQueue; @@ -215,6 +217,19 @@ private static final class ActionState { */ int runningStrategiesBitmap = 0; + private static class ProgressState { + final String id; + final long nanoStartTime; + ActionProgressEvent latestEvent; + + private ProgressState(String id, long nanoStartTime) { + this.id = id; + this.nanoStartTime = nanoStartTime; + } + } + + private final LinkedHashMap runningProgresses = new LinkedHashMap<>(); + /** Starts tracking the state of an action. */ ActionState(ActionExecutionMetadata action, long nanoStartTime) { this.action = action; @@ -304,6 +319,20 @@ synchronized void setRunning(String strategy, long nanoChangeTime) { nanoStartTime = nanoChangeTime; } + /** Handles the progress event for the action. */ + synchronized void onProgressEvent(ActionProgressEvent event, long nanoChangeTime) { + String id = event.progressId(); + if (event.finished()) { + // a progress is finished, clean it up + runningProgresses.remove(id); + return; + } + + ProgressState state = + runningProgresses.computeIfAbsent(id, key -> new ProgressState(key, nanoChangeTime)); + state.latestEvent = event; + } + /** Generates a human-readable description of this action's state. */ synchronized String describe() { if (runningStrategiesBitmap != 0) { @@ -539,6 +568,13 @@ void runningAction(RunningActionEvent event) { getActionState(action, actionId, now).setRunning(event.getStrategy(), now); } + void actionProgress(ActionProgressEvent event) { + ActionExecutionMetadata action = event.action(); + Artifact actionId = event.action().getPrimaryOutput(); + long now = clock.nanoTime(); + getActionState(action, actionId, now).onProgressEvent(event, now); + } + void actionCompletion(ActionScanningCompletedEvent event) { Action action = event.getAction(); Artifact actionId = action.getPrimaryOutput(); @@ -668,6 +704,29 @@ private String describeTestGroup( return message.append(allReported ? "]" : postfix).toString(); } + private String describeActionProgress(ActionState action, int desiredWidth) { + if (action.runningProgresses.isEmpty()) { + return ""; + } + + ActionState.ProgressState state = + action.runningProgresses.entrySet().iterator().next().getValue(); + ActionProgressEvent event = state.latestEvent; + String message = event.progress(); + if (message.isEmpty()) { + message = state.id; + } + + message = "; " + message; + + if (desiredWidth <= 0 || message.length() <= desiredWidth) { + return message; + } + + message = message.substring(0, desiredWidth - ELLIPSIS.length()) + ELLIPSIS; + return message; + } + // Describe an action by a string of the desired length; if describing that action includes // describing other actions, add those to the to set of actions to skip in further samples of // actions. @@ -721,9 +780,24 @@ private String describeAction( message = action.prettyPrint(); } - if (desiredWidth <= 0) { - return prefix + message + postfix; + String progress = describeActionProgress(actionState, 0); + + if (desiredWidth <= 0 + || (prefix.length() + message.length() + progress.length() + postfix.length()) + <= desiredWidth) { + return prefix + message + progress + postfix; } + + // We have to shorten the progress to fit into the line. + int remainingWidthForProgress = + desiredWidth - prefix.length() - message.length() - postfix.length(); + int minWidthForProgress = 7; // "; " + at least two character + "..." + if (remainingWidthForProgress >= minWidthForProgress) { + progress = describeActionProgress(actionState, remainingWidthForProgress); + return prefix + message + progress + postfix; + } + + // We have to skip the progress to fit into the line. if (prefix.length() + message.length() + postfix.length() <= desiredWidth) { return prefix + message + postfix; } diff --git a/src/test/java/com/google/devtools/build/lib/remote/GrpcCacheClientTest.java b/src/test/java/com/google/devtools/build/lib/remote/GrpcCacheClientTest.java index 3c59a8418fb11f..ef159dbd189496 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/GrpcCacheClientTest.java +++ b/src/test/java/com/google/devtools/build/lib/remote/GrpcCacheClientTest.java @@ -391,7 +391,12 @@ public void testDownloadAllResults() throws Exception { result.addOutputFilesBuilder().setPath("b/empty").setDigest(emptyDigest); result.addOutputFilesBuilder().setPath("a/bar").setDigest(barDigest).setIsExecutable(true); remoteCache.download( - context, remotePathResolver, result.build(), null, /* outputFilesLocker= */ () -> {}); + context, + remotePathResolver, + result.build(), + null, + /* outputFilesLocker= */ () -> {}, + progress -> {}); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("a/foo"))).isEqualTo(fooDigest); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("b/empty"))).isEqualTo(emptyDigest); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("a/bar"))).isEqualTo(barDigest); @@ -416,7 +421,12 @@ public void testDownloadAllResultsForSiblingLayoutAndRelativeToInputRoot() throw result.addOutputFilesBuilder().setPath("main/b/empty").setDigest(emptyDigest); result.addOutputFilesBuilder().setPath("main/a/bar").setDigest(barDigest).setIsExecutable(true); remoteCache.download( - context, remotePathResolver, result.build(), null, /* outputFilesLocker= */ () -> {}); + context, + remotePathResolver, + result.build(), + null, + /* outputFilesLocker= */ () -> {}, + progress -> {}); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("a/foo"))).isEqualTo(fooDigest); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("b/empty"))).isEqualTo(emptyDigest); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("a/bar"))).isEqualTo(barDigest); @@ -453,7 +463,12 @@ public void testDownloadDirectory() throws Exception { result.addOutputFilesBuilder().setPath("a/foo").setDigest(fooDigest); result.addOutputDirectoriesBuilder().setPath("a/bar").setTreeDigest(barTreeDigest); remoteCache.download( - context, remotePathResolver, result.build(), null, /* outputFilesLocker= */ () -> {}); + context, + remotePathResolver, + result.build(), + null, + /* outputFilesLocker= */ () -> {}, + progress -> {}); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("a/foo"))).isEqualTo(fooDigest); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("a/bar/qux"))).isEqualTo(quxDigest); @@ -475,7 +490,12 @@ public void testDownloadDirectoryEmpty() throws Exception { ActionResult.Builder result = ActionResult.newBuilder(); result.addOutputDirectoriesBuilder().setPath("a/bar").setTreeDigest(barTreeDigest); remoteCache.download( - context, remotePathResolver, result.build(), null, /* outputFilesLocker= */ () -> {}); + context, + remotePathResolver, + result.build(), + null, + /* outputFilesLocker= */ () -> {}, + progress -> {}); assertThat(execRoot.getRelative("a/bar").isDirectory()).isTrue(); } @@ -518,7 +538,12 @@ public void testDownloadDirectoryNested() throws Exception { result.addOutputFilesBuilder().setPath("a/foo").setDigest(fooDigest); result.addOutputDirectoriesBuilder().setPath("a/bar").setTreeDigest(barTreeDigest); remoteCache.download( - context, remotePathResolver, result.build(), null, /* outputFilesLocker= */ () -> {}); + context, + remotePathResolver, + result.build(), + null, + /* outputFilesLocker= */ () -> {}, + progress -> {}); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("a/foo"))).isEqualTo(fooDigest); assertThat(DIGEST_UTIL.compute(execRoot.getRelative("a/bar/wobble/qux"))).isEqualTo(quxDigest); diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTests.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTests.java index e4551c7d969b23..9efeb0ff5b2584 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTests.java +++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTests.java @@ -56,6 +56,7 @@ import com.google.devtools.build.lib.clock.JavaClock; import com.google.devtools.build.lib.remote.RemoteCache.OutputFilesLocker; import com.google.devtools.build.lib.remote.RemoteCache.UploadManifest; +import com.google.devtools.build.lib.remote.common.ProgressStatusListener; import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext; import com.google.devtools.build.lib.remote.common.RemoteCacheClient.ActionKey; import com.google.devtools.build.lib.remote.common.RemotePathResolver; @@ -102,6 +103,7 @@ public class RemoteCacheTests { @Mock private OutputFilesLocker outputFilesLocker; + private final ProgressStatusListener progressStatusListener = progress -> {}; private RemoteActionExecutionContext context; private RemotePathResolver remotePathResolver; @@ -614,7 +616,13 @@ public void downloadRelativeFileSymlink() throws Exception { ActionResult.Builder result = ActionResult.newBuilder(); result.addOutputFileSymlinksBuilder().setPath("a/b/link").setTarget("../../foo"); // Doesn't check for dangling links, hence download succeeds. - cache.download(context, remotePathResolver, result.build(), null, outputFilesLocker); + cache.download( + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener); Path path = execRoot.getRelative("a/b/link"); assertThat(path.isSymbolicLink()).isTrue(); assertThat(path.readSymbolicLink()).isEqualTo(PathFragment.create("../../foo")); @@ -627,7 +635,13 @@ public void downloadRelativeDirectorySymlink() throws Exception { ActionResult.Builder result = ActionResult.newBuilder(); result.addOutputDirectorySymlinksBuilder().setPath("a/b/link").setTarget("foo"); // Doesn't check for dangling links, hence download succeeds. - cache.download(context, remotePathResolver, result.build(), null, outputFilesLocker); + cache.download( + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener); Path path = execRoot.getRelative("a/b/link"); assertThat(path.isSymbolicLink()).isTrue(); assertThat(path.readSymbolicLink()).isEqualTo(PathFragment.create("foo")); @@ -647,7 +661,13 @@ public void downloadRelativeSymlinkInDirectory() throws Exception { ActionResult.Builder result = ActionResult.newBuilder(); result.addOutputDirectoriesBuilder().setPath("dir").setTreeDigest(treeDigest); // Doesn't check for dangling links, hence download succeeds. - cache.download(context, remotePathResolver, result.build(), null, outputFilesLocker); + cache.download( + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener); Path path = execRoot.getRelative("dir/link"); assertThat(path.isSymbolicLink()).isTrue(); assertThat(path.readSymbolicLink()).isEqualTo(PathFragment.create("../foo")); @@ -664,7 +684,12 @@ public void downloadAbsoluteDirectorySymlinkError() throws Exception { IOException.class, () -> cache.download( - context, remotePathResolver, result.build(), null, outputFilesLocker)); + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener)); assertThat(expected).hasMessageThat().contains("/abs/link"); assertThat(expected).hasMessageThat().contains("absolute path"); verify(outputFilesLocker).lock(); @@ -680,7 +705,12 @@ public void downloadAbsoluteFileSymlinkError() throws Exception { IOException.class, () -> cache.download( - context, remotePathResolver, result.build(), null, outputFilesLocker)); + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener)); assertThat(expected).hasMessageThat().contains("/abs/link"); assertThat(expected).hasMessageThat().contains("absolute path"); verify(outputFilesLocker).lock(); @@ -703,7 +733,12 @@ public void downloadAbsoluteSymlinkInDirectoryError() throws Exception { IOException.class, () -> cache.download( - context, remotePathResolver, result.build(), null, outputFilesLocker)); + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener)); assertThat(expected.getSuppressed()).isEmpty(); assertThat(expected).hasMessageThat().contains("dir/link"); assertThat(expected).hasMessageThat().contains("/foo"); @@ -727,7 +762,14 @@ public void downloadFailureMaintainsDirectories() throws Exception { result.addOutputFiles(OutputFile.newBuilder().setPath("otherfile").setDigest(otherFileDigest)); assertThrows( BulkTransferException.class, - () -> cache.download(context, remotePathResolver, result.build(), null, outputFilesLocker)); + () -> + cache.download( + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener)); assertThat(cache.getNumFailedDownloads()).isEqualTo(1); assertThat(execRoot.getRelative("outputdir").exists()).isTrue(); assertThat(execRoot.getRelative("outputdir/outputfile").exists()).isFalse(); @@ -765,7 +807,8 @@ public void onErrorWaitForRemainingDownloadsToComplete() throws Exception { remotePathResolver, result, new FileOutErr(stdout, stderr), - outputFilesLocker)); + outputFilesLocker, + progressStatusListener)); assertThat(downloadException.getSuppressed()).hasLength(1); assertThat(cache.getNumSuccessfulDownloads()).isEqualTo(2); assertThat(cache.getNumFailedDownloads()).isEqualTo(1); @@ -801,7 +844,8 @@ public void downloadWithMultipleErrorsAddsThemAsSuppressed() throws Exception { remotePathResolver, result, new FileOutErr(stdout, stderr), - outputFilesLocker)); + outputFilesLocker, + progressStatusListener)); assertThat(e.getSuppressed()).hasLength(2); assertThat(e.getSuppressed()[0]).isInstanceOf(IOException.class); @@ -837,7 +881,8 @@ public void downloadWithDuplicateIOErrorsDoesNotSuppress() throws Exception { remotePathResolver, result, new FileOutErr(stdout, stderr), - outputFilesLocker)); + outputFilesLocker, + progressStatusListener)); for (Throwable t : downloadException.getSuppressed()) { assertThat(t).isInstanceOf(IOException.class); @@ -873,7 +918,8 @@ public void downloadWithDuplicateInterruptionsDoesNotSuppress() throws Exception remotePathResolver, result, new FileOutErr(stdout, stderr), - outputFilesLocker)); + outputFilesLocker, + progressStatusListener)); assertThat(e.getSuppressed()).isEmpty(); assertThat(Throwables.getRootCause(e)).hasMessageThat().isEqualTo("reused interruption"); @@ -902,7 +948,8 @@ public void testDownloadWithStdoutStderrOnSuccess() throws Exception { .setStderrDigest(digestStderr) .build(); - cache.download(context, remotePathResolver, result, spyOutErr, outputFilesLocker); + cache.download( + context, remotePathResolver, result, spyOutErr, outputFilesLocker, progressStatusListener); verify(spyOutErr, Mockito.times(2)).childOutErr(); verify(spyChildOutErr).clearOut(); @@ -945,7 +992,14 @@ public void testDownloadWithStdoutStderrOnFailure() throws Exception { .build(); assertThrows( BulkTransferException.class, - () -> cache.download(context, remotePathResolver, result, spyOutErr, outputFilesLocker)); + () -> + cache.download( + context, + remotePathResolver, + result, + spyOutErr, + outputFilesLocker, + progressStatusListener)); verify(spyOutErr, Mockito.times(2)).childOutErr(); verify(spyChildOutErr).clearOut(); verify(spyChildOutErr).clearErr(); @@ -982,7 +1036,13 @@ public void testDownloadClashes() throws Exception { // act - remoteCache.download(context, remotePathResolver, r, new FileOutErr(), outputFilesLocker); + remoteCache.download( + context, + remotePathResolver, + r, + new FileOutErr(), + outputFilesLocker, + progressStatusListener); // assert @@ -1365,7 +1425,12 @@ public void testDownloadDirectory() throws Exception { // act RemoteCache remoteCache = newRemoteCache(cas); remoteCache.download( - context, remotePathResolver, result.build(), null, /* outputFilesLocker= */ () -> {}); + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener); // assert assertThat(digestUtil.compute(execRoot.getRelative("a/foo"))).isEqualTo(fooDigest); @@ -1390,7 +1455,12 @@ public void testDownloadEmptyDirectory() throws Exception { // act RemoteCache remoteCache = newRemoteCache(map); remoteCache.download( - context, remotePathResolver, result.build(), null, /* outputFilesLocker= */ () -> {}); + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener); // assert assertThat(execRoot.getRelative("a/bar").isDirectory()).isTrue(); @@ -1435,7 +1505,12 @@ public void testDownloadNestedDirectory() throws Exception { // act RemoteCache remoteCache = newRemoteCache(map); remoteCache.download( - context, remotePathResolver, result.build(), null, /* outputFilesLocker= */ () -> {}); + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener); // assert assertThat(digestUtil.compute(execRoot.getRelative("a/foo"))).isEqualTo(fooDigest); @@ -1486,7 +1561,12 @@ public void testDownloadDirectoryWithSameHash() throws Exception { // act RemoteCache remoteCache = newRemoteCache(map); remoteCache.download( - context, remotePathResolver, result.build(), null, /* outputFilesLocker= */ () -> {}); + context, + remotePathResolver, + result.build(), + null, + outputFilesLocker, + progressStatusListener); // assert assertThat(digestUtil.compute(execRoot.getRelative("a/bar/foo/file"))).isEqualTo(fileDigest); diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnCacheTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnCacheTest.java index 21cd123cca591d..200948c332a21b 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnCacheTest.java +++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnCacheTest.java @@ -274,13 +274,13 @@ public Void answer(InvocationOnMock invocation) { } }) .when(remoteCache) - .download(any(), any(), eq(actionResult), eq(outErr), any()); + .download(any(), any(), eq(actionResult), eq(outErr), any(), any()); CacheHandle entry = cache.lookup(simpleSpawn, simplePolicy); assertThat(entry.hasResult()).isTrue(); SpawnResult result = entry.getResult(); // All other methods on RemoteActionCache have side effects, so we verify all of them. - verify(remoteCache).download(any(), any(), eq(actionResult), eq(outErr), any()); + verify(remoteCache).download(any(), any(), eq(actionResult), eq(outErr), any(), any()); verify(remoteCache, never()) .upload( any(RemoteActionExecutionContext.class), @@ -644,7 +644,7 @@ public ActionResult answer(InvocationOnMock invocation) { }); doThrow(new CacheNotFoundException(digest)) .when(remoteCache) - .download(any(), any(), eq(actionResult), eq(outErr), any()); + .download(any(), any(), eq(actionResult), eq(outErr), any(), any()); CacheHandle entry = cache.lookup(simpleSpawn, simplePolicy); assertThat(entry.hasResult()).isFalse(); diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java index 0f1536dcce3c0a..35b3996658d258 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java +++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerTest.java @@ -380,6 +380,7 @@ public void treatFailedCachedActionAsCacheMiss_local() throws Exception { any(RemotePathResolver.class), any(ActionResult.class), eq(outErr), + any(), any()); } @@ -710,6 +711,7 @@ public void testNonHumanReadableServerLogsNotSaved() throws Exception { any(RemotePathResolver.class), eq(result), any(FileOutErr.class), + any(), any()); verify(cache, never()) .downloadFile(any(RemoteActionExecutionContext.class), any(Path.class), any(Digest.class)); @@ -750,6 +752,7 @@ public void testServerLogsNotSavedForSuccessfulAction() throws Exception { any(RemotePathResolver.class), eq(result), any(FileOutErr.class), + any(), any()); verify(cache, never()) .downloadFile(any(RemoteActionExecutionContext.class), any(Path.class), any(Digest.class)); @@ -776,6 +779,7 @@ public void cacheDownloadFailureTriggersRemoteExecution() throws Exception { any(RemotePathResolver.class), eq(cachedResult), any(FileOutErr.class), + any(), any()); ActionResult execResult = ActionResult.newBuilder().setExitCode(31).build(); ExecuteResponse succeeded = ExecuteResponse.newBuilder().setResult(execResult).build(); @@ -791,6 +795,7 @@ public void cacheDownloadFailureTriggersRemoteExecution() throws Exception { any(RemotePathResolver.class), eq(execResult), any(FileOutErr.class), + any(), any()); Spawn spawn = newSimpleSpawn(); @@ -840,6 +845,7 @@ public void resultsDownloadFailureTriggersRemoteExecutionWithSkipCacheLookup() t any(RemotePathResolver.class), eq(cachedResult), any(FileOutErr.class), + any(), any()); doNothing() .when(cache) @@ -848,6 +854,7 @@ public void resultsDownloadFailureTriggersRemoteExecutionWithSkipCacheLookup() t any(RemotePathResolver.class), eq(execResult), any(FileOutErr.class), + any(), any()); Spawn spawn = newSimpleSpawn(); @@ -917,6 +924,7 @@ public void testRemoteExecutionTimeout() throws Exception { any(RemotePathResolver.class), eq(cachedResult), any(FileOutErr.class), + any(), any()); } @@ -967,6 +975,7 @@ public void testRemoteExecutionTimeoutDoesNotTriggerFallback() throws Exception any(RemotePathResolver.class), eq(cachedResult), any(FileOutErr.class), + any(), any()); verify(localRunner, never()).exec(eq(spawn), eq(policy)); } @@ -1012,6 +1021,7 @@ public void testRemoteExecutionCommandFailureDoesNotTriggerFallback() throws Exc any(RemotePathResolver.class), eq(cachedResult), any(FileOutErr.class), + any(), any()); verify(localRunner, never()).exec(eq(spawn), eq(policy)); } @@ -1178,6 +1188,7 @@ public void testDownloadMinimalOnCacheHit() throws Exception { any(RemotePathResolver.class), any(ActionResult.class), eq(outErr), + any(), any()); } @@ -1224,6 +1235,7 @@ public void testDownloadMinimalOnCacheMiss() throws Exception { any(RemotePathResolver.class), any(ActionResult.class), eq(outErr), + any(), any()); } @@ -1276,6 +1288,7 @@ public void testDownloadMinimalIoError() throws Exception { any(RemotePathResolver.class), any(ActionResult.class), eq(outErr), + any(), any()); } @@ -1307,7 +1320,7 @@ public void testDownloadTopLevel() throws Exception { assertThat(result.status()).isEqualTo(Status.SUCCESS); // assert - verify(cache).download(any(), any(), eq(succeededAction), eq(outErr), any()); + verify(cache).download(any(), any(), eq(succeededAction), eq(outErr), any(), any()); verify(cache, never()) .downloadMinimal( any(), any(), eq(succeededAction), anyCollection(), any(), any(), any(), any()); diff --git a/src/test/java/com/google/devtools/build/lib/remote/util/UtilsTest.java b/src/test/java/com/google/devtools/build/lib/remote/util/UtilsTest.java new file mode 100644 index 00000000000000..7077baba9209d3 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/remote/util/UtilsTest.java @@ -0,0 +1,39 @@ +// Copyright 2021 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.remote.util; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.remote.util.Utils.bytesCountToDisplayString; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link Utils}. */ +@RunWith(JUnit4.class) +public class UtilsTest { + @Test + public void bytesCountToDisplayString_works() { + assertThat(bytesCountToDisplayString(1000)).isEqualTo("1000 B"); + assertThat(bytesCountToDisplayString(1 << 10)).isEqualTo("1 KiB"); + assertThat(bytesCountToDisplayString((1 << 10) + (1 << 10) / 10)).isEqualTo("1.1 KiB"); + assertThat(bytesCountToDisplayString(1 << 20)).isEqualTo("1 MiB"); + assertThat(bytesCountToDisplayString((1 << 20) + (1 << 20) / 10)).isEqualTo("1.1 MiB"); + assertThat(bytesCountToDisplayString(1 << 30)).isEqualTo("1 GiB"); + assertThat(bytesCountToDisplayString((1 << 30) + (1 << 30) / 10)).isEqualTo("1.1 GiB"); + assertThat(bytesCountToDisplayString(1L << 40)).isEqualTo("1 TiB"); + assertThat(bytesCountToDisplayString((1L << 40) + (1L << 40) / 10)).isEqualTo("1.1 TiB"); + assertThat(bytesCountToDisplayString(1L << 50)).isEqualTo("1024 TiB"); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/runtime/UiStateTrackerTest.java b/src/test/java/com/google/devtools/build/lib/runtime/UiStateTrackerTest.java index 4eca3a71505017..197ac415e72114 100644 --- a/src/test/java/com/google/devtools/build/lib/runtime/UiStateTrackerTest.java +++ b/src/test/java/com/google/devtools/build/lib/runtime/UiStateTrackerTest.java @@ -29,6 +29,7 @@ import com.google.devtools.build.lib.actions.ActionCompletionEvent; import com.google.devtools.build.lib.actions.ActionLookupData; import com.google.devtools.build.lib.actions.ActionOwner; +import com.google.devtools.build.lib.actions.ActionProgressEvent; import com.google.devtools.build.lib.actions.ActionStartedEvent; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.ArtifactRoot; @@ -583,6 +584,92 @@ public void testActionStrategyVisible() throws Exception { .isTrue(); } + private Action createDummyAction(String progressMessage) { + String primaryOutput = "some/path/to/a/file"; + Path path = outputBase.getRelative(PathFragment.create(primaryOutput)); + Artifact artifact = + ActionsTestUtil.createArtifact(ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)), path); + Action action = mockAction(progressMessage, primaryOutput); + when(action.getOwner()).thenReturn(mock(ActionOwner.class)); + when(action.getPrimaryOutput()).thenReturn(artifact); + return action; + } + + @Test + public void actionProgress_visible() throws Exception { + // arrange + ManualClock clock = new ManualClock(); + Action action = createDummyAction("Some random action"); + UiStateTracker stateTracker = new UiStateTracker(clock, /* targetWidth= */ 70); + stateTracker.actionProgress( + ActionProgressEvent.create(action, "action-id", "action progress", false)); + LoggingTerminalWriter terminalWriter = new LoggingTerminalWriter(/*discardHighlight=*/ true); + + // act + stateTracker.writeProgressBar(terminalWriter); + + // assert + String output = terminalWriter.getTranscript(); + assertThat(output).contains("action progress"); + } + + @Test + public void actionProgress_withTooSmallWidth_progressSkipped() throws Exception { + // arrange + ManualClock clock = new ManualClock(); + Action action = createDummyAction("Some random action"); + UiStateTracker stateTracker = new UiStateTracker(clock, /* targetWidth= */ 30); + stateTracker.actionProgress( + ActionProgressEvent.create(action, "action-id", "action progress", false)); + LoggingTerminalWriter terminalWriter = new LoggingTerminalWriter(/*discardHighlight=*/ true); + + // act + stateTracker.writeProgressBar(terminalWriter); + + // assert + String output = terminalWriter.getTranscript(); + assertThat(output).doesNotContain("action progress"); + } + + @Test + public void actionProgress_withSmallWidth_progressShortened() throws Exception { + // arrange + ManualClock clock = new ManualClock(); + Action action = createDummyAction("Some random action"); + UiStateTracker stateTracker = new UiStateTracker(clock, /* targetWidth= */ 50); + stateTracker.actionProgress( + ActionProgressEvent.create(action, "action-id", "action progress", false)); + LoggingTerminalWriter terminalWriter = new LoggingTerminalWriter(/*discardHighlight=*/ true); + + // act + stateTracker.writeProgressBar(terminalWriter); + + // assert + String output = terminalWriter.getTranscript(); + assertThat(output).contains("action pro..."); + } + + @Test + public void actionProgress_multipleProgress_displayInOrder() throws Exception { + // arrange + ManualClock clock = new ManualClock(); + Action action = createDummyAction("Some random action"); + UiStateTracker stateTracker = new UiStateTracker(clock, /* targetWidth= */ 70); + stateTracker.actionProgress( + ActionProgressEvent.create(action, "action-id1", "action progress 1", false)); + stateTracker.actionProgress( + ActionProgressEvent.create(action, "action-id2", "action progress 2", false)); + LoggingTerminalWriter terminalWriter = new LoggingTerminalWriter(/*discardHighlight=*/ true); + + // act + stateTracker.writeProgressBar(terminalWriter); + + // assert + String output = terminalWriter.getTranscript(); + assertThat(output).contains("action progress 1"); + assertThat(output).doesNotContain("action progress 2"); + } + @Test public void testMultipleActionStrategiesVisibleForDynamicScheduling() throws Exception { String strategy1 = "strategy1";