blob: 51a9138bb034f74b66a55b71ddb8c92af67822e2 [file]
// Copyright 2026 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;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.remote.util.Futures.getFromFuture;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import build.bazel.remote.execution.v2.Digest;
import build.bazel.remote.execution.v2.RequestMetadata;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Futures;
import com.google.devtools.build.lib.actions.AbstractAction;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionInputMap;
import com.google.devtools.build.lib.actions.ActionInputPrefetcher;
import com.google.devtools.build.lib.actions.ActionKeyContext;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.actions.ArtifactRoot.RootType;
import com.google.devtools.build.lib.actions.DiscoveredModulesPruner;
import com.google.devtools.build.lib.actions.FileArtifactValue;
import com.google.devtools.build.lib.actions.FileStatusWithMetadata;
import com.google.devtools.build.lib.actions.ThreadStateReceiver;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil.FakeInputMetadataHandlerBase;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil.NullAction;
import com.google.devtools.build.lib.actions.util.DummyExecutor;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.events.StoredEventHandler;
import com.google.devtools.build.lib.exec.FileWriteStrategy;
import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext;
import com.google.devtools.build.lib.remote.disk.DiskCacheClient;
import com.google.devtools.build.lib.remote.options.RemoteOutputsMode;
import com.google.devtools.build.lib.remote.util.DigestUtil;
import com.google.devtools.build.lib.remote.util.InMemoryCacheClient;
import com.google.devtools.build.lib.testutil.Scratch;
import com.google.devtools.build.lib.util.DeterministicWriter;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.SyscallCache;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import java.io.IOException;
import java.time.Duration;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link RemoteFileWriteStrategy}. */
@RunWith(JUnit4.class)
public final class RemoteFileWriteStrategyTest {
private static final DigestUtil DIGEST_UTIL =
new DigestUtil(SyscallCache.NO_CACHE, DigestHashFunction.SHA256);
private static final String CONTENT = "hello";
private static final Digest CONTENT_DIGEST = DIGEST_UTIL.compute(CONTENT.getBytes(UTF_8));
private final FileSystem fileSystem = new InMemoryFileSystem(DigestHashFunction.SHA256);
private final Scratch scratch = new Scratch(fileSystem);
private final InMemoryCacheClient cacheClient = spy(new InMemoryCacheClient());
private final StoredEventHandler eventHandler = new StoredEventHandler();
private Path execRoot;
private RemoteActionFileSystem actionFileSystem;
private AbstractAction action;
private Artifact output;
@Before
public void setUp() throws IOException {
execRoot = scratch.dir("/execroot");
var outputRoot = ArtifactRoot.asDerivedRoot(execRoot, RootType.OUTPUT, "bazel-out");
outputRoot.getRoot().asPath().createDirectory();
output =
ActionsTestUtil.createArtifactWithRootRelativePath(
outputRoot, PathFragment.create("pkg/file"));
action = new NullAction(output);
actionFileSystem =
new RemoteActionFileSystem(
fileSystem,
execRoot.asFragment(),
outputRoot.getExecPathString(),
new ActionInputMap(0),
mock(RemoteActionInputFetcher.class));
actionFileSystem.createDirectoryAndParents(output.getPath().getParentDirectory().asFragment());
}
@Test
public void remotable_storesContentsRemotely() throws Exception {
var strategy = createStrategy(RemoteOutputsMode.MINIMAL, cacheClient);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
assertStoredRemotely(cacheClient);
var metadata = getRemoteMetadata();
assertThat(metadata.isRemote()).isTrue();
assertThat(metadata.getDigest()).isEqualTo(DigestUtil.toBinaryDigest(CONTENT_DIGEST));
assertThat(metadata.getSize()).isEqualTo(CONTENT.length());
assertThat(output.getPath().exists()).isFalse();
assertThat(eventHandler.getEvents()).isEmpty();
}
@Test
public void alreadyStoredRemotely_skipsUpload() throws Exception {
var prepopulatedCacheClient =
spy(new InMemoryCacheClient(ImmutableMap.of(CONTENT_DIGEST, CONTENT.getBytes(UTF_8))));
var strategy = createStrategy(RemoteOutputsMode.MINIMAL, prepopulatedCacheClient);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
verify(prepopulatedCacheClient, never()).uploadBlobImpl(any(), any(), any());
assertThat(getRemoteMetadata().isRemote()).isTrue();
assertThat(output.getPath().exists()).isFalse();
}
@Test
public void nonRemotable_writesLocally() throws Exception {
var strategy = createStrategy(RemoteOutputsMode.MINIMAL, cacheClient);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ false);
assertWrittenLocally();
assertNotStoredRemotely(cacheClient);
}
@Test
public void outputRequestedForDownload_writesLocally() throws Exception {
var strategy = createStrategy(RemoteOutputsMode.ALL, cacheClient);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
assertWrittenLocally();
assertNotStoredRemotely(cacheClient);
}
@Test
public void remoteUploadDisabled_missingRemotely_writesLocally() throws Exception {
var strategy =
createStrategy(RemoteOutputsMode.MINIMAL, cacheClient, /* remoteUploadEnabled= */ false);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
assertWrittenLocally();
assertNotStoredRemotely(cacheClient);
assertThat(eventHandler.getEvents()).isEmpty();
}
@Test
public void remoteUploadDisabled_alreadyStoredRemotely_recordsRemoteOutput() throws Exception {
var prepopulatedCacheClient =
spy(new InMemoryCacheClient(ImmutableMap.of(CONTENT_DIGEST, CONTENT.getBytes(UTF_8))));
var strategy =
createStrategy(
RemoteOutputsMode.MINIMAL, prepopulatedCacheClient, /* remoteUploadEnabled= */ false);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
verify(prepopulatedCacheClient, never()).uploadBlobImpl(any(), any(), any());
var metadata = getRemoteMetadata();
assertThat(metadata.isRemote()).isTrue();
assertThat(metadata.getDigest()).isEqualTo(DigestUtil.toBinaryDigest(CONTENT_DIGEST));
assertThat(output.getPath().exists()).isFalse();
assertThat(eventHandler.getEvents()).isEmpty();
}
@Test
public void noActionFileSystem_writesLocally() throws Exception {
var strategy = createStrategy(RemoteOutputsMode.MINIMAL, cacheClient);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(/* actionFileSystem= */ null),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
assertWrittenLocally();
assertNotStoredRemotely(cacheClient);
}
@Test
public void diskCacheOnly_storesContentsInDiskCache() throws Exception {
var diskCacheClient = createDiskCacheClient();
var strategy =
createStrategy(
RemoteOutputsMode.MINIMAL,
/* cacheClient= */ null,
diskCacheClient,
/* remoteUploadEnabled= */ false);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
assertStoredInDiskCache(diskCacheClient);
var metadata = getRemoteMetadata();
assertThat(metadata.isRemote()).isTrue();
assertThat(metadata.getDigest()).isEqualTo(DigestUtil.toBinaryDigest(CONTENT_DIGEST));
assertThat(output.getPath().exists()).isFalse();
assertThat(eventHandler.getEvents()).isEmpty();
}
@Test
public void remoteUploadDisabled_missingRemotely_storesContentsInDiskCache() throws Exception {
var diskCacheClient = createDiskCacheClient();
var strategy =
createStrategy(
RemoteOutputsMode.MINIMAL,
cacheClient,
diskCacheClient,
/* remoteUploadEnabled= */ false);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
assertStoredInDiskCache(diskCacheClient);
assertNotStoredRemotely(cacheClient);
assertThat(getRemoteMetadata().isRemote()).isTrue();
assertThat(output.getPath().exists()).isFalse();
assertThat(eventHandler.getEvents()).isEmpty();
}
@Test
public void alreadyStoredRemotely_storesContentsInDiskCache() throws Exception {
var diskCacheClient = createDiskCacheClient();
var prepopulatedCacheClient =
spy(new InMemoryCacheClient(ImmutableMap.of(CONTENT_DIGEST, CONTENT.getBytes(UTF_8))));
var strategy =
createStrategy(
RemoteOutputsMode.MINIMAL,
prepopulatedCacheClient,
diskCacheClient,
/* remoteUploadEnabled= */ true);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
verify(prepopulatedCacheClient, never()).uploadBlobImpl(any(), any(), any());
assertStoredInDiskCache(diskCacheClient);
assertThat(getRemoteMetadata().isRemote()).isTrue();
assertThat(output.getPath().exists()).isFalse();
}
@Test
public void remotable_storesContentsInBothCaches() throws Exception {
var diskCacheClient = createDiskCacheClient();
var strategy =
createStrategy(
RemoteOutputsMode.MINIMAL,
cacheClient,
diskCacheClient,
/* remoteUploadEnabled= */ true);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
assertStoredRemotely(cacheClient);
assertStoredInDiskCache(diskCacheClient);
assertThat(getRemoteMetadata().isRemote()).isTrue();
assertThat(output.getPath().exists()).isFalse();
}
@Test
public void uploadFails_writesLocallyAndWarns() throws Exception {
doReturn(Futures.immediateFailedFuture(new IOException("upload failed")))
.when(cacheClient)
.uploadBlobImpl(any(), any(), any());
var strategy = createStrategy(RemoteOutputsMode.MINIMAL, cacheClient);
var unused =
strategy.writeOutputToFile(
action,
createActionExecutionContext(actionFileSystem),
writer(CONTENT),
/* makeExecutable= */ false,
/* isRemotable= */ true);
assertWrittenLocally();
var warning = eventHandler.getEvents().stream().findFirst().orElseThrow();
assertThat(warning.getKind()).isEqualTo(EventKind.WARNING);
assertThat(warning.getMessage()).contains("Remote Cache:");
assertThat(warning.getMessage()).contains("upload failed");
}
private RemoteFileWriteStrategy createStrategy(
RemoteOutputsMode outputsMode, InMemoryCacheClient cacheClient) {
return createStrategy(outputsMode, cacheClient, /* remoteUploadEnabled= */ true);
}
private RemoteFileWriteStrategy createStrategy(
RemoteOutputsMode outputsMode, InMemoryCacheClient cacheClient, boolean remoteUploadEnabled) {
return createStrategy(
outputsMode, cacheClient, /* diskCacheClient= */ null, remoteUploadEnabled);
}
private RemoteFileWriteStrategy createStrategy(
RemoteOutputsMode outputsMode,
@Nullable InMemoryCacheClient cacheClient,
@Nullable DiskCacheClient diskCacheClient,
boolean remoteUploadEnabled) {
return new RemoteFileWriteStrategy(
new FileWriteStrategy(),
new CombinedCache(
cacheClient,
diskCacheClient,
/* symlinkTemplate= */ null,
DIGEST_UTIL,
/* chunkingFunction= */ null,
new ChunkLocationMap()),
new RemoteOutputChecker("build", outputsMode, ImmutableList.of()),
DIGEST_UTIL,
"build-request-id",
"command-id",
Duration.ofHours(1),
remoteUploadEnabled,
/* verboseFailures= */ false);
}
private DiskCacheClient createDiskCacheClient() throws IOException {
return new DiskCacheClient(
scratch.dir("/disk_cache"), DIGEST_UTIL, /* checkActionResultIntegrity= */ false);
}
private ActionExecutionContext createActionExecutionContext(
@Nullable FileSystem actionFileSystem) {
var metadataHandler = new FakeInputMetadataHandlerBase();
return new ActionExecutionContext(
new DummyExecutor(fileSystem, execRoot),
/* inputMetadataProvider= */ metadataHandler,
ActionInputPrefetcher.NONE,
new ActionKeyContext(),
/* outputMetadataStore= */ metadataHandler,
/* rewindingEnabled= */ false,
ActionExecutionContext.LostInputsCheck.NONE,
/* fileOutErr= */ null,
eventHandler,
/* clientEnv= */ ImmutableMap.of(),
actionFileSystem,
DiscoveredModulesPruner.DEFAULT,
SyscallCache.NO_CACHE,
ThreadStateReceiver.NULL_INSTANCE);
}
private static DeterministicWriter writer(String content) {
return out -> out.write(content.getBytes(UTF_8));
}
private FileArtifactValue getRemoteMetadata() throws IOException {
var status = actionFileSystem.getPath(output.getPath().asFragment()).stat();
assertThat(status).isInstanceOf(FileStatusWithMetadata.class);
return ((FileStatusWithMetadata) status).getMetadata();
}
private void assertWrittenLocally() throws IOException {
assertThat(FileSystemUtils.readContent(output.getPath(), UTF_8)).isEqualTo(CONTENT);
}
private static void assertStoredRemotely(InMemoryCacheClient cacheClient) throws Exception {
assertThat(
getFromFuture(
cacheClient.findMissingDigests(
RemoteActionExecutionContext.create(RequestMetadata.getDefaultInstance()),
ImmutableList.of(CONTENT_DIGEST))))
.isEmpty();
}
private static void assertStoredInDiskCache(DiskCacheClient diskCacheClient) throws Exception {
assertThat(diskCacheClient.toPath(CONTENT_DIGEST, Store.CAS).exists()).isTrue();
}
private static void assertNotStoredRemotely(InMemoryCacheClient cacheClient) throws Exception {
verify(cacheClient, never()).uploadBlobImpl(any(), any(), any());
assertThat(
getFromFuture(
cacheClient.findMissingDigests(
RemoteActionExecutionContext.create(RequestMetadata.getDefaultInstance()),
ImmutableList.of(CONTENT_DIGEST))))
.containsExactly(CONTENT_DIGEST);
}
}