| // Copyright 2015 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 java.nio.charset.StandardCharsets.UTF_8; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.collect.Maps; | |
| import com.google.devtools.build.lib.actions.Root; | |
| import com.google.devtools.build.lib.remote.CasServiceGrpc.CasServiceImplBase; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.ActionResult; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.BlobChunk; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.CasDownloadBlobRequest; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.CasDownloadReply; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.CasLookupReply; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.CasLookupRequest; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.CasStatus; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.CasUploadBlobReply; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.CasUploadBlobRequest; | |
| import com.google.devtools.build.lib.remote.RemoteProtocol.ContentDigest; | |
| import com.google.devtools.build.lib.testutil.Scratch; | |
| import com.google.devtools.build.lib.util.Preconditions; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.common.options.Options; | |
| import com.google.protobuf.ByteString; | |
| import io.grpc.ManagedChannel; | |
| import io.grpc.Server; | |
| import io.grpc.inprocess.InProcessChannelBuilder; | |
| import io.grpc.inprocess.InProcessServerBuilder; | |
| import io.grpc.stub.StreamObserver; | |
| import java.util.concurrent.ConcurrentMap; | |
| import org.junit.After; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| import org.mockito.MockitoAnnotations; | |
| /** Tests for {@link GrpcActionCache}. */ | |
| @RunWith(JUnit4.class) | |
| public class GrpcActionCacheTest { | |
| private final FakeRemoteCacheService fakeRemoteCacheService = new FakeRemoteCacheService(); | |
| private final Server server = | |
| InProcessServerBuilder.forName(getClass().getSimpleName()) | |
| .directExecutor() | |
| .addService(fakeRemoteCacheService) | |
| .build(); | |
| private final ManagedChannel channel = | |
| InProcessChannelBuilder.forName(getClass().getSimpleName()).directExecutor().build(); | |
| private Scratch scratch; | |
| private Root rootDir; | |
| @Before | |
| public final void setUp() throws Exception { | |
| MockitoAnnotations.initMocks(this); | |
| scratch = new Scratch(); | |
| rootDir = Root.asDerivedRoot(scratch.dir("/exec/root")); | |
| server.start(); | |
| } | |
| @After | |
| public void tearDown() { | |
| server.shutdownNow(); | |
| channel.shutdownNow(); | |
| } | |
| @Test | |
| public void testDownloadEmptyBlobs() throws Exception { | |
| GrpcActionCache client = new GrpcActionCache(channel, Options.getDefaults(RemoteOptions.class)); | |
| ContentDigest fooDigest = fakeRemoteCacheService.put("foo".getBytes(UTF_8)); | |
| ContentDigest emptyDigest = ContentDigests.computeDigest(new byte[0]); | |
| ImmutableList<byte[]> results = | |
| client.downloadBlobs(ImmutableList.<ContentDigest>of(emptyDigest, fooDigest, emptyDigest)); | |
| // Will not query the server for empty blobs. | |
| assertThat(new String(results.get(0), UTF_8)).isEmpty(); | |
| assertThat(new String(results.get(1), UTF_8)).isEqualTo("foo"); | |
| assertThat(new String(results.get(2), UTF_8)).isEmpty(); | |
| // Will not call the server at all. | |
| assertThat(new String(client.downloadBlob(emptyDigest), UTF_8)).isEmpty(); | |
| } | |
| @Test | |
| public void testDownloadBlobs() throws Exception { | |
| GrpcActionCache client = new GrpcActionCache(channel, Options.getDefaults(RemoteOptions.class)); | |
| ContentDigest fooDigest = fakeRemoteCacheService.put("foo".getBytes(UTF_8)); | |
| ContentDigest barDigest = fakeRemoteCacheService.put("bar".getBytes(UTF_8)); | |
| ImmutableList<byte[]> results = | |
| client.downloadBlobs(ImmutableList.<ContentDigest>of(fooDigest, barDigest)); | |
| assertThat(new String(results.get(0), UTF_8)).isEqualTo("foo"); | |
| assertThat(new String(results.get(1), UTF_8)).isEqualTo("bar"); | |
| } | |
| @Test | |
| public void testDownloadBlobsBatchChunk() throws Exception { | |
| RemoteOptions options = Options.getDefaults(RemoteOptions.class); | |
| options.grpcMaxBatchInputs = 10; | |
| options.grpcMaxChunkSizeBytes = 2; | |
| options.grpcMaxBatchSizeBytes = 10; | |
| options.grpcTimeoutSeconds = 10; | |
| GrpcActionCache client = new GrpcActionCache(channel, options); | |
| ContentDigest fooDigest = fakeRemoteCacheService.put("fooooooo".getBytes(UTF_8)); | |
| ContentDigest barDigest = fakeRemoteCacheService.put("baaaar".getBytes(UTF_8)); | |
| ContentDigest s1Digest = fakeRemoteCacheService.put("1".getBytes(UTF_8)); | |
| ContentDigest s2Digest = fakeRemoteCacheService.put("2".getBytes(UTF_8)); | |
| ContentDigest s3Digest = fakeRemoteCacheService.put("3".getBytes(UTF_8)); | |
| ImmutableList<byte[]> results = | |
| client.downloadBlobs( | |
| ImmutableList.<ContentDigest>of(fooDigest, barDigest, s1Digest, s2Digest, s3Digest)); | |
| assertThat(new String(results.get(0), UTF_8)).isEqualTo("fooooooo"); | |
| assertThat(new String(results.get(1), UTF_8)).isEqualTo("baaaar"); | |
| assertThat(new String(results.get(2), UTF_8)).isEqualTo("1"); | |
| assertThat(new String(results.get(3), UTF_8)).isEqualTo("2"); | |
| assertThat(new String(results.get(4), UTF_8)).isEqualTo("3"); | |
| } | |
| @Test | |
| public void testUploadBlobs() throws Exception { | |
| GrpcActionCache client = new GrpcActionCache(channel, Options.getDefaults(RemoteOptions.class)); | |
| byte[] foo = "foo".getBytes(UTF_8); | |
| byte[] bar = "bar".getBytes(UTF_8); | |
| ContentDigest fooDigest = ContentDigests.computeDigest(foo); | |
| ContentDigest barDigest = ContentDigests.computeDigest(bar); | |
| ImmutableList<ContentDigest> digests = client.uploadBlobs(ImmutableList.<byte[]>of(foo, bar)); | |
| assertThat(digests).containsExactly(fooDigest, barDigest); | |
| assertThat(fakeRemoteCacheService.get(fooDigest)).isEqualTo(foo); | |
| assertThat(fakeRemoteCacheService.get(barDigest)).isEqualTo(bar); | |
| } | |
| @Test | |
| public void testUploadBlobsBatchChunk() throws Exception { | |
| RemoteOptions options = Options.getDefaults(RemoteOptions.class); | |
| options.grpcMaxBatchInputs = 10; | |
| options.grpcMaxChunkSizeBytes = 2; | |
| options.grpcMaxBatchSizeBytes = 10; | |
| options.grpcTimeoutSeconds = 10; | |
| GrpcActionCache client = new GrpcActionCache(channel, options); | |
| byte[] foo = "fooooooo".getBytes(UTF_8); | |
| byte[] bar = "baaaar".getBytes(UTF_8); | |
| byte[] s1 = "1".getBytes(UTF_8); | |
| byte[] s2 = "2".getBytes(UTF_8); | |
| byte[] s3 = "3".getBytes(UTF_8); | |
| ContentDigest fooDigest = ContentDigests.computeDigest(foo); | |
| ContentDigest barDigest = ContentDigests.computeDigest(bar); | |
| ContentDigest s1Digest = ContentDigests.computeDigest(s1); | |
| ContentDigest s2Digest = ContentDigests.computeDigest(s2); | |
| ContentDigest s3Digest = ContentDigests.computeDigest(s3); | |
| ImmutableList<ContentDigest> digests = | |
| client.uploadBlobs(ImmutableList.<byte[]>of(foo, bar, s1, s2, s3)); | |
| assertThat(digests).containsExactly(fooDigest, barDigest, s1Digest, s2Digest, s3Digest); | |
| assertThat(fakeRemoteCacheService.get(fooDigest)).isEqualTo(foo); | |
| assertThat(fakeRemoteCacheService.get(barDigest)).isEqualTo(bar); | |
| assertThat(fakeRemoteCacheService.get(s1Digest)).isEqualTo(s1); | |
| assertThat(fakeRemoteCacheService.get(s2Digest)).isEqualTo(s2); | |
| assertThat(fakeRemoteCacheService.get(s3Digest)).isEqualTo(s3); | |
| } | |
| @Test | |
| public void testUploadAllResults() throws Exception { | |
| GrpcActionCache client = new GrpcActionCache(channel, Options.getDefaults(RemoteOptions.class)); | |
| byte[] foo = "foo".getBytes(UTF_8); | |
| byte[] bar = "bar".getBytes(UTF_8); | |
| Path fooFile = scratch.file("/exec/root/a/foo", foo); | |
| Path emptyFile = scratch.file("/exec/root/b/empty"); | |
| Path barFile = scratch.file("/exec/root/a/bar", bar); | |
| ContentDigest fooDigest = ContentDigests.computeDigest(fooFile); | |
| ContentDigest barDigest = ContentDigests.computeDigest(barFile); | |
| ContentDigest emptyDigest = ContentDigests.computeDigest(new byte[0]); | |
| ActionResult.Builder result = ActionResult.newBuilder(); | |
| client.uploadAllResults( | |
| rootDir.getPath(), ImmutableList.<Path>of(fooFile, emptyFile, barFile), result); | |
| assertThat(fakeRemoteCacheService.get(fooDigest)).isEqualTo(foo); | |
| assertThat(fakeRemoteCacheService.get(barDigest)).isEqualTo(bar); | |
| ActionResult.Builder expectedResult = ActionResult.newBuilder(); | |
| expectedResult | |
| .addOutputBuilder() | |
| .setPath("a/foo") | |
| .getFileMetadataBuilder() | |
| .setDigest(fooDigest); | |
| expectedResult | |
| .addOutputBuilder() | |
| .setPath("b/empty") | |
| .getFileMetadataBuilder() | |
| .setDigest(emptyDigest); | |
| expectedResult | |
| .addOutputBuilder() | |
| .setPath("a/bar") | |
| .getFileMetadataBuilder() | |
| .setDigest(barDigest); | |
| assertThat(result.build()).isEqualTo(expectedResult.build()); | |
| } | |
| @Test | |
| public void testDownloadAllResults() throws Exception { | |
| GrpcActionCache client = new GrpcActionCache(channel, Options.getDefaults(RemoteOptions.class)); | |
| ContentDigest fooDigest = fakeRemoteCacheService.put("foo".getBytes(UTF_8)); | |
| ContentDigest barDigest = fakeRemoteCacheService.put("bar".getBytes(UTF_8)); | |
| ContentDigest emptyDigest = ContentDigests.computeDigest(new byte[0]); | |
| ActionResult.Builder result = ActionResult.newBuilder(); | |
| result.addOutputBuilder().setPath("a/foo").getFileMetadataBuilder().setDigest(fooDigest); | |
| result.addOutputBuilder().setPath("b/empty").getFileMetadataBuilder().setDigest(emptyDigest); | |
| result.addOutputBuilder().setPath("a/bar").getFileMetadataBuilder().setDigest(barDigest); | |
| client.downloadAllResults(result.build(), rootDir.getPath()); | |
| Path fooFile = rootDir.getPath().getRelative("a/foo"); | |
| Path emptyFile = rootDir.getPath().getRelative("b/empty"); | |
| Path barFile = rootDir.getPath().getRelative("a/bar"); | |
| assertThat(ContentDigests.computeDigest(fooFile)).isEqualTo(fooDigest); | |
| assertThat(ContentDigests.computeDigest(emptyFile)).isEqualTo(emptyDigest); | |
| assertThat(ContentDigests.computeDigest(barFile)).isEqualTo(barDigest); | |
| } | |
| private static class FakeRemoteCacheService extends CasServiceImplBase { | |
| private final ConcurrentMap<String, byte[]> cache = Maps.newConcurrentMap(); | |
| public ContentDigest put(byte[] blob) { | |
| ContentDigest digest = ContentDigests.computeDigest(blob); | |
| cache.put(ContentDigests.toHexString(digest), blob); | |
| return digest; | |
| } | |
| public byte[] get(ContentDigest digest) { | |
| return cache.get(ContentDigests.toHexString(digest)); | |
| } | |
| public void clear() { | |
| cache.clear(); | |
| } | |
| @Override | |
| public void lookup(CasLookupRequest request, StreamObserver<CasLookupReply> observer) { | |
| CasLookupReply.Builder reply = CasLookupReply.newBuilder(); | |
| CasStatus.Builder status = reply.getStatusBuilder(); | |
| for (ContentDigest digest : request.getDigestList()) { | |
| if (get(digest) == null) { | |
| status.addMissingDigest(digest); | |
| } | |
| } | |
| status.setSucceeded(true); | |
| observer.onNext(reply.build()); | |
| observer.onCompleted(); | |
| } | |
| @Override | |
| public void downloadBlob( | |
| CasDownloadBlobRequest request, StreamObserver<CasDownloadReply> observer) { | |
| CasDownloadReply.Builder reply = CasDownloadReply.newBuilder(); | |
| CasStatus.Builder status = reply.getStatusBuilder(); | |
| boolean success = true; | |
| for (ContentDigest digest : request.getDigestList()) { | |
| if (get(digest) == null) { | |
| status.addMissingDigest(digest); | |
| success = false; | |
| } | |
| } | |
| if (!success) { | |
| status.setError(CasStatus.ErrorCode.MISSING_DIGEST); | |
| status.setSucceeded(false); | |
| observer.onNext(reply.build()); | |
| observer.onCompleted(); | |
| return; | |
| } | |
| // We change the order on purpose, to test for blobs out of order: | |
| for (ContentDigest digest : Lists.reverse(request.getDigestList())) { | |
| observer.onNext( | |
| CasDownloadReply.newBuilder() | |
| .setStatus(CasStatus.newBuilder().setSucceeded(true)) | |
| .setData( | |
| BlobChunk.newBuilder() | |
| .setDigest(digest) | |
| .setData(ByteString.copyFrom(get(digest)))) | |
| .build()); | |
| } | |
| observer.onCompleted(); | |
| } | |
| @Override | |
| public StreamObserver<CasUploadBlobRequest> uploadBlob( | |
| final StreamObserver<CasUploadBlobReply> responseObserver) { | |
| return new StreamObserver<CasUploadBlobRequest>() { | |
| byte[] blob = null; | |
| ContentDigest digest = null; | |
| long offset = 0; | |
| @Override | |
| public void onNext(CasUploadBlobRequest request) { | |
| BlobChunk chunk = request.getData(); | |
| try { | |
| if (chunk.hasDigest()) { | |
| // Check if the previous chunk was really done. | |
| Preconditions.checkArgument( | |
| digest == null || offset == 0, | |
| "Missing input chunk for digest %s", | |
| digest == null ? "" : ContentDigests.toString(digest)); | |
| digest = chunk.getDigest(); | |
| blob = new byte[(int) digest.getSizeBytes()]; | |
| } | |
| Preconditions.checkArgument(digest != null, "First chunk contains no digest"); | |
| Preconditions.checkArgument( | |
| offset == chunk.getOffset(), | |
| "Missing input chunk for digest %s", | |
| ContentDigests.toString(digest)); | |
| if (digest.getSizeBytes() > 0) { | |
| chunk.getData().copyTo(blob, (int) offset); | |
| offset = (offset + chunk.getData().size()) % digest.getSizeBytes(); | |
| } | |
| if (offset == 0) { | |
| ContentDigest uploadedDigest = put(blob); | |
| Preconditions.checkArgument( | |
| uploadedDigest.equals(digest), | |
| "Digest mismatch: client sent %s, server computed %s", | |
| ContentDigests.toString(digest), | |
| ContentDigests.toString(uploadedDigest)); | |
| } | |
| } catch (Exception e) { | |
| CasUploadBlobReply.Builder reply = CasUploadBlobReply.newBuilder(); | |
| reply | |
| .getStatusBuilder() | |
| .setSucceeded(false) | |
| .setError( | |
| e instanceof IllegalArgumentException | |
| ? CasStatus.ErrorCode.INVALID_ARGUMENT | |
| : CasStatus.ErrorCode.UNKNOWN) | |
| .setErrorDetail(e.toString()); | |
| responseObserver.onNext(reply.build()); | |
| } | |
| } | |
| @Override | |
| public void onError(Throwable t) {} | |
| @Override | |
| public void onCompleted() { | |
| responseObserver.onCompleted(); | |
| } | |
| }; | |
| } | |
| } | |
| } |