blob: 2a47760b36b7de9a53f55fe7488085501d87dce0 [file] [log] [blame]
// Copyright 2019 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.bazel.repository.downloader;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.bazel.repository.downloader.DownloaderTestUtils.sendLines;
import static com.google.devtools.build.lib.bazel.repository.downloader.HttpParser.readHttpRequest;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.authandtls.StaticCredentials;
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
import com.google.devtools.build.lib.vfs.Path;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.stubbing.Answer;
/** Tests for {@link HttpDownloader} */
@RunWith(JUnit4.class)
public class HttpDownloaderTest {
@Rule public final TemporaryFolder workingDir = new TemporaryFolder();
@Rule public final Timeout timeout = new Timeout(30, SECONDS);
private final RepositoryCache repositoryCache = mock(RepositoryCache.class);
private final HttpDownloader httpDownloader = new HttpDownloader();
private final DownloadManager downloadManager =
new DownloadManager(repositoryCache, httpDownloader);
private final ExecutorService executor = Executors.newFixedThreadPool(2);
private final ExtendedEventHandler eventHandler = mock(ExtendedEventHandler.class);
private final JavaIoFileSystem fs;
public HttpDownloaderTest() {
fs = new JavaIoFileSystem(DigestHashFunction.SHA256);
// Scale timeouts down to make tests fast.
httpDownloader.setTimeoutScaling(0.1f);
}
@After
public void after() {
executor.shutdown();
}
@Test
public void downloadFrom1UrlOk() throws IOException, InterruptedException {
try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
try (Socket socket = server.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"Content-Length: 5",
"",
"hello");
}
return null;
});
Path resultingFile =
downloadManager.download(
Collections.singletonList(
new URL(String.format("http://localhost:%d/foo", server.getLocalPort()))),
Collections.emptyMap(),
Collections.emptyMap(),
Optional.empty(),
"testCanonicalId",
Optional.empty(),
fs.getPath(workingDir.newFile().getAbsolutePath()),
eventHandler,
Collections.emptyMap(),
"testRepo");
assertThat(new String(readFile(resultingFile), UTF_8)).isEqualTo("hello");
}
}
@Test
public void downloadFrom2UrlsFirstOk() throws IOException, InterruptedException {
try (ServerSocket server1 = new ServerSocket(0, 1, InetAddress.getByName(null));
ServerSocket server2 = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
while (!executor.isShutdown()) {
try (Socket socket = server1.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"",
"content1");
}
}
return null;
});
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError2 =
executor.submit(
() -> {
while (!executor.isShutdown()) {
try (Socket socket = server2.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"",
"content2");
}
}
return null;
});
final List<URL> urls = new ArrayList<>(2);
urls.add(new URL(String.format("http://localhost:%d/foo", server1.getLocalPort())));
urls.add(new URL(String.format("http://localhost:%d/foo", server2.getLocalPort())));
Path resultingFile =
downloadManager.download(
urls,
Collections.emptyMap(),
Collections.emptyMap(),
Optional.empty(),
"testCanonicalId",
Optional.empty(),
fs.getPath(workingDir.newFile().getAbsolutePath()),
eventHandler,
Collections.emptyMap(),
"testRepo");
assertThat(new String(readFile(resultingFile), UTF_8)).isEqualTo("content1");
}
}
@Ignore("b/182150157")
@Test
public void downloadFrom2UrlsFirstSocketTimeoutOnBodyReadSecondOk()
throws IOException, InterruptedException {
try (ServerSocket server1 = new ServerSocket(0, 1, InetAddress.getByName(null));
ServerSocket server2 = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
Socket socket = server1.accept();
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"",
"content1");
// Never close the socket to cause SocketTimeoutException during body read on client
// side.
return null;
});
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError2 =
executor.submit(
() -> {
while (!executor.isShutdown()) {
try (Socket socket = server2.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"",
"content2");
}
}
return null;
});
final List<URL> urls = new ArrayList<>(2);
urls.add(new URL(String.format("http://localhost:%d/foo", server1.getLocalPort())));
urls.add(new URL(String.format("http://localhost:%d/foo", server2.getLocalPort())));
Path resultingFile =
downloadManager.download(
urls,
Collections.emptyMap(),
Collections.emptyMap(),
Optional.empty(),
"testCanonicalId",
Optional.empty(),
fs.getPath(workingDir.newFile().getAbsolutePath()),
eventHandler,
Collections.emptyMap(),
"testRepo");
assertThat(new String(readFile(resultingFile), UTF_8)).isEqualTo("content2");
}
}
@Ignore("b/182150157")
@Test
public void downloadFrom2UrlsBothSocketTimeoutDuringBodyRead()
throws IOException, InterruptedException {
try (ServerSocket server1 = new ServerSocket(0, 1, InetAddress.getByName(null));
ServerSocket server2 = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
Socket socket = server1.accept();
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"",
"content1");
// Never close the socket to cause SocketTimeoutException during body read on client
// side.
return null;
});
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError2 =
executor.submit(
() -> {
Socket socket = server1.accept();
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"",
"content2");
// Never close the socket to cause SocketTimeoutException during body read on client
// side.
return null;
});
final List<URL> urls = new ArrayList<>(2);
urls.add(new URL(String.format("http://localhost:%d/foo", server1.getLocalPort())));
urls.add(new URL(String.format("http://localhost:%d/foo", server2.getLocalPort())));
Path outputFile = fs.getPath(workingDir.newFile().getAbsolutePath());
try {
downloadManager.download(
urls,
Collections.emptyMap(),
Collections.emptyMap(),
Optional.empty(),
"testCanonicalId",
Optional.empty(),
outputFile,
eventHandler,
Collections.emptyMap(),
"testRepo");
fail("Should have thrown");
} catch (IOException expected) {
assertThat(expected.getSuppressed()).hasLength(2);
for (Throwable suppressed : expected.getSuppressed()) {
assertThat(suppressed).isInstanceOf(IOException.class);
assertThat(suppressed).hasCauseThat().isInstanceOf(SocketTimeoutException.class);
}
}
}
}
private static byte[] readFile(Path path) throws IOException {
final byte[] data = new byte[(int) path.getFileSize()];
try (DataInputStream stream = new DataInputStream(path.getInputStream())) {
stream.readFully(data);
}
return data;
}
@Test
public void downloadOneUrl_ok() throws IOException, InterruptedException {
try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
try (Socket socket = server.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"Content-Length: 5",
"",
"hello");
}
return null;
});
Path destination = fs.getPath(workingDir.newFile().getAbsolutePath());
httpDownloader.download(
Collections.singletonList(
new URL(String.format("http://localhost:%d/foo", server.getLocalPort()))),
Collections.emptyMap(),
StaticCredentials.EMPTY,
Optional.empty(),
"testCanonicalId",
destination,
eventHandler,
Collections.emptyMap(),
Optional.empty());
assertThat(new String(readFile(destination), UTF_8)).isEqualTo("hello");
}
}
@Test
public void downloadOneUrl_notFound() throws IOException, InterruptedException {
try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
try (Socket socket = server.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 404 Not Found",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"Content-Length: 5",
"",
"");
}
return null;
});
assertThrows(
IOException.class,
() ->
httpDownloader.download(
Collections.singletonList(
new URL(String.format("http://localhost:%d/foo", server.getLocalPort()))),
Collections.emptyMap(),
StaticCredentials.EMPTY,
Optional.empty(),
"testCanonicalId",
fs.getPath(workingDir.newFile().getAbsolutePath()),
eventHandler,
Collections.emptyMap(),
Optional.empty()));
}
}
@Test
public void downloadTwoUrls_firstNotFoundAndSecondOk() throws IOException, InterruptedException {
try (ServerSocket server1 = new ServerSocket(0, 1, InetAddress.getByName(null));
ServerSocket server2 = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
try (Socket socket = server1.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 404 Not Found",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"Content-Length: 5",
"",
"");
}
return null;
});
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError2 =
executor.submit(
() -> {
while (!executor.isShutdown()) {
try (Socket socket = server2.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"",
"content2");
}
}
return null;
});
final List<URL> urls = new ArrayList<>(2);
urls.add(new URL(String.format("http://localhost:%d/foo", server1.getLocalPort())));
urls.add(new URL(String.format("http://localhost:%d/foo", server2.getLocalPort())));
Path destination = fs.getPath(workingDir.newFile().getAbsolutePath());
httpDownloader.download(
urls,
Collections.emptyMap(),
StaticCredentials.EMPTY,
Optional.empty(),
"testCanonicalId",
destination,
eventHandler,
Collections.emptyMap(),
Optional.empty());
assertThat(new String(readFile(destination), UTF_8)).isEqualTo("content2");
}
}
@Test
public void downloadAndReadOneUrl_ok() throws IOException, InterruptedException {
try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
try (Socket socket = server.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"Content-Length: 5",
"",
"hello");
}
return null;
});
assertThat(
new String(
httpDownloader.downloadAndReadOneUrl(
new URL(String.format("http://localhost:%d/foo", server.getLocalPort())),
StaticCredentials.EMPTY,
Optional.empty(),
eventHandler,
Collections.emptyMap()),
UTF_8))
.isEqualTo("hello");
}
}
@Test
public void downloadAndReadOneUrl_notFound() throws IOException, InterruptedException {
try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
try (Socket socket = server.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 404 Not Found",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"Content-Length: 5",
"",
"");
}
return null;
});
assertThrows(
FileNotFoundException.class,
() ->
httpDownloader.downloadAndReadOneUrl(
new URL(String.format("http://localhost:%d/foo", server.getLocalPort())),
StaticCredentials.EMPTY,
Optional.empty(),
eventHandler,
Collections.emptyMap()));
}
}
@Test
public void downloadAndReadOneUrl_checksumProvided()
throws IOException, Checksum.InvalidChecksumException, InterruptedException {
try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
try (Socket socket = server.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"Content-Length: 5",
"",
"hello");
}
return null;
});
assertThat(
new String(
httpDownloader.downloadAndReadOneUrl(
new URL(String.format("http://localhost:%d/foo", server.getLocalPort())),
StaticCredentials.EMPTY,
Optional.of(
Checksum.fromString(
RepositoryCache.KeyType.SHA256,
Hashing.sha256().hashString("hello", UTF_8).toString())),
eventHandler,
ImmutableMap.of()),
UTF_8))
.isEqualTo("hello");
}
}
@Test
public void downloadAndReadOneUrl_checksumMismatch() throws IOException {
try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getByName(null))) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
executor.submit(
() -> {
try (Socket socket = server.accept()) {
readHttpRequest(socket.getInputStream());
sendLines(
socket,
"HTTP/1.1 200 OK",
"Date: Fri, 31 Dec 1999 23:59:59 GMT",
"Connection: close",
"Content-Type: text/plain",
"Content-Length: 9",
"",
"malicious");
}
return null;
});
var e =
assertThrows(
UnrecoverableHttpException.class,
() ->
httpDownloader.downloadAndReadOneUrl(
new URL(String.format("http://localhost:%d/foo", server.getLocalPort())),
StaticCredentials.EMPTY,
Optional.of(
Checksum.fromString(
RepositoryCache.KeyType.SHA256,
Hashing.sha256().hashUnencodedChars("hello").toString())),
eventHandler,
ImmutableMap.of()));
assertThat(e).hasMessageThat().contains("Checksum was");
}
}
@Test
public void download_contentLengthMismatch_propagateErrorIfNotRetry() throws Exception {
Downloader downloader = mock(Downloader.class);
DownloadManager downloadManager = new DownloadManager(repositoryCache, downloader);
// do not retry
downloadManager.setRetries(0);
AtomicInteger times = new AtomicInteger(0);
byte[] data = "content".getBytes(UTF_8);
doAnswer(
(Answer<Void>)
invocationOnMock -> {
times.getAndIncrement();
throw new ContentLengthMismatchException(0, data.length);
})
.when(downloader)
.download(any(), any(), any(), any(), any(), any(), any(), any(), any());
assertThrows(
ContentLengthMismatchException.class,
() ->
downloadManager.download(
ImmutableList.of(new URL("http://localhost")),
Collections.emptyMap(),
ImmutableMap.of(),
Optional.empty(),
"testCanonicalId",
Optional.empty(),
fs.getPath(workingDir.newFile().getAbsolutePath()),
eventHandler,
ImmutableMap.of(),
"testRepo"));
assertThat(times.get()).isEqualTo(1);
}
@Test
public void download_contentLengthMismatch_retries() throws Exception {
Downloader downloader = mock(Downloader.class);
int retires = 5;
DownloadManager downloadManager = new DownloadManager(repositoryCache, downloader);
downloadManager.setRetries(retires);
AtomicInteger times = new AtomicInteger(0);
byte[] data = "content".getBytes(UTF_8);
doAnswer(
(Answer<Void>)
invocationOnMock -> {
if (times.getAndIncrement() < 3) {
throw new ContentLengthMismatchException(0, data.length);
}
Path output = invocationOnMock.getArgument(5, Path.class);
try (OutputStream outputStream = output.getOutputStream()) {
ByteStreams.copy(new ByteArrayInputStream(data), outputStream);
}
return null;
})
.when(downloader)
.download(any(), any(), any(), any(), any(), any(), any(), any(), any());
Path result =
downloadManager.download(
ImmutableList.of(new URL("http://localhost")),
ImmutableMap.of(),
ImmutableMap.of(),
Optional.empty(),
"testCanonicalId",
Optional.empty(),
fs.getPath(workingDir.newFile().getAbsolutePath()),
eventHandler,
ImmutableMap.of(),
"testRepo");
assertThat(times.get()).isEqualTo(4);
String content = new String(ByteStreams.toByteArray(result.getInputStream()), UTF_8);
assertThat(content).isEqualTo("content");
}
}