blob: 851750d300813c9b269f8175179890455012fe89 [file] [log] [blame]
// 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.testutil.TestUtils.tmpDirFile;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialModule;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.remote.util.IntegrationTestUtils;
import com.google.devtools.build.lib.remote.util.IntegrationTestUtils.WorkerInstance;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.BlockWaitingModule;
import com.google.devtools.build.lib.runtime.BuildSummaryStatsModule;
import com.google.devtools.build.lib.standalone.StandaloneModule;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.io.InputStream;
import org.junit.After;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Integration tests for chunked remote cache with a combined disk + remote cache.
*
* <p>Verifies that chunks downloaded from the remote cache are properly captured to disk cache, and
* that subsequent builds can serve chunks from disk cache without hitting the remote.
*/
@RunWith(JUnit4.class)
public class ChunkedDiskCacheIntegrationTest extends BuildIntegrationTestCase {
@ClassRule @Rule public static final WorkerInstance worker = IntegrationTestUtils.createWorker();
private static PathFragment getDiskCacheDir() {
return PathFragment.create(tmpDirFile().getAbsolutePath()).getRelative("chunked_disk_cache");
}
@Override
protected void setupOptions() throws Exception {
super.setupOptions();
addOptions(
"--remote_cache=grpc://localhost:" + worker.getPort(),
"--disk_cache=" + getDiskCacheDir(),
"--experimental_remote_cache_chunking");
}
@Override
protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
return super.getRuntimeBuilder()
.addBlazeModule(new RemoteModule())
.addBlazeModule(new BuildSummaryStatsModule())
.addBlazeModule(new BlockWaitingModule());
}
@Override
protected ImmutableList<BlazeModule> getSpawnModules() {
return ImmutableList.<BlazeModule>builder()
.addAll(super.getSpawnModules())
.add(new StandaloneModule())
.add(new CredentialModule())
.build();
}
@After
public void tearDown() throws Exception {
runtimeWrapper.newCommand();
getWorkspace().getFileSystem().getPath(getDiskCacheDir()).deleteTree();
}
private Path getOutputPath(String binRelativePath) {
return getTargetConfiguration().getBinDir().getRoot().getRelative(binRelativePath);
}
private void cleanAndRestartServer() throws Exception {
getOutputBase().getRelative("action_cache").deleteTreesBelow();
createRuntimeWrapper();
}
private byte[] readFileBytes(Path path) throws IOException {
try (InputStream in = path.getInputStream()) {
return ByteStreams.toByteArray(in);
}
}
@Test
public void largeBlob_uploadedAndDownloaded_throughDiskAndRemoteCache() throws Exception {
write(
"BUILD",
"""
genrule(
name = "large_file",
srcs = [],
outs = ["large.bin"],
cmd = "dd if=/dev/zero bs=1M count=3 2>/dev/null | tr '\\\\0' 'D' > $@",
)
""");
// First build: generates the file, uploads chunks to remote + disk cache.
buildTarget("//:large_file");
Path output = getOutputPath("large.bin");
assertThat(output.exists()).isTrue();
byte[] originalContent = readFileBytes(output);
assertThat(originalContent.length).isEqualTo(3 * 1024 * 1024);
// Second build: clean outputs + action cache, rebuild.
// Chunks should be served from disk cache (populated during first build's download capture).
output.delete();
cleanAndRestartServer();
buildTarget("//:large_file");
assertThat(output.exists()).isTrue();
assertThat(readFileBytes(output)).isEqualTo(originalContent);
}
@Test
public void largeBlob_diskCasDeleted_rebuildFromRemote() throws Exception {
write(
"BUILD",
"""
genrule(
name = "large_file",
srcs = [],
outs = ["large.bin"],
cmd = "dd if=/dev/zero bs=1M count=3 2>/dev/null | tr '\\\\0' 'E' > $@",
)
""");
// First build: populates both caches.
buildTarget("//:large_file");
Path output = getOutputPath("large.bin");
byte[] originalContent = readFileBytes(output);
// Delete disk cache CAS entries (simulate cache eviction).
Path diskCasCas = getWorkspace().getFileSystem().getPath(getDiskCacheDir().getRelative("cas"));
if (diskCasCas.exists()) {
diskCasCas.deleteTree();
}
// Clean outputs + action cache, rebuild.
// Should fall back to remote cache since disk CAS is gone.
output.delete();
cleanAndRestartServer();
buildTarget("//:large_file");
assertThat(output.exists()).isTrue();
assertThat(readFileBytes(output)).isEqualTo(originalContent);
}
@Test
public void multipleTargets_withDiskCache_allSucceed() throws Exception {
write(
"BUILD",
"""
genrule(
name = "data_a",
srcs = [],
outs = ["a.bin"],
cmd = "dd if=/dev/zero bs=1M count=3 2>/dev/null | tr '\\\\0' 'F' > $@",
)
genrule(
name = "data_b",
srcs = [],
outs = ["b.bin"],
cmd = "dd if=/dev/zero bs=1M count=4 2>/dev/null | tr '\\\\0' 'G' > $@",
)
genrule(
name = "combined",
srcs = [":a.bin", ":b.bin"],
outs = ["combined.bin"],
cmd = "cat $(SRCS) > $@",
)
""");
buildTarget("//:data_a", "//:data_b", "//:combined");
Path outputA = getOutputPath("a.bin");
Path outputB = getOutputPath("b.bin");
Path outputCombined = getOutputPath("combined.bin");
byte[] contentA = readFileBytes(outputA);
byte[] contentB = readFileBytes(outputB);
byte[] contentCombined = readFileBytes(outputCombined);
assertThat(contentA.length).isEqualTo(3 * 1024 * 1024);
assertThat(contentB.length).isEqualTo(4 * 1024 * 1024);
assertThat(contentCombined.length).isEqualTo(7 * 1024 * 1024);
// Clean and rebuild from cache.
outputA.delete();
outputB.delete();
outputCombined.delete();
cleanAndRestartServer();
buildTarget("//:data_a", "//:data_b", "//:combined");
assertThat(readFileBytes(outputA)).isEqualTo(contentA);
assertThat(readFileBytes(outputB)).isEqualTo(contentB);
assertThat(readFileBytes(outputCombined)).isEqualTo(contentCombined);
}
}