blob: 41fd65fe47869b41ffe488d2896b20195594e0af [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stddef.h>
#include <stdint.h>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/location.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_task_environment.h"
#include "base/test/test_file_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/build_config.h"
#include "components/download/public/common/download_create_info.h"
#include "components/download/public/common/download_destination_observer.h"
#include "components/download/public/common/download_file_impl.h"
#include "components/download/public/common/download_interrupt_reasons.h"
#include "components/download/public/common/mock_input_stream.h"
#include "net/base/net_errors.h"
#include "services/service_manager/public/cpp/connector.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::DoAll;
using ::testing::InSequence;
using ::testing::Return;
using ::testing::Sequence;
using ::testing::SetArgPointee;
using ::testing::StrictMock;
namespace download {
namespace {
// Struct for SourceStream states verification.
struct SourceStreamTestData {
SourceStreamTestData(int64_t offset, int64_t bytes_written, bool finished)
: offset(offset), bytes_written(bytes_written), finished(finished) {}
int64_t offset;
int64_t bytes_written;
bool finished;
};
int64_t GetBuffersLength(const char** buffers, size_t num_buffer) {
int64_t result = 0;
for (size_t i = 0; i < num_buffer; ++i)
result += static_cast<int64_t>(strlen(buffers[i]));
return result;
}
std::string GetHexEncodedHashValue(crypto::SecureHash* hash_state) {
if (!hash_state)
return std::string();
std::vector<char> hash_value(hash_state->GetHashLength());
hash_state->Finish(&hash_value.front(), hash_value.size());
return base::HexEncode(&hash_value.front(), hash_value.size());
}
class MockDownloadDestinationObserver : public DownloadDestinationObserver {
public:
MOCK_METHOD3(DestinationUpdate,
void(int64_t,
int64_t,
const std::vector<DownloadItem::ReceivedSlice>&));
void DestinationError(
DownloadInterruptReason reason,
int64_t bytes_so_far,
std::unique_ptr<crypto::SecureHash> hash_state) override {
MockDestinationError(reason, bytes_so_far,
GetHexEncodedHashValue(hash_state.get()));
}
void DestinationCompleted(
int64_t total_bytes,
std::unique_ptr<crypto::SecureHash> hash_state) override {
MockDestinationCompleted(total_bytes,
GetHexEncodedHashValue(hash_state.get()));
}
MOCK_METHOD3(MockDestinationError,
void(DownloadInterruptReason, int64_t, const std::string&));
MOCK_METHOD2(MockDestinationCompleted, void(int64_t, const std::string&));
// Doesn't override any methods in the base class. Used to make sure
// that the last DestinationUpdate before a Destination{Completed,Error}
// had the right values.
MOCK_METHOD2(CurrentUpdateStatus, void(int64_t, int64_t));
};
enum DownloadFileRenameMethodType { RENAME_AND_UNIQUIFY, RENAME_AND_ANNOTATE };
// This is a test DownloadFileImpl that has no retry delay and, on Posix,
// retries renames failed due to ACCESS_DENIED.
class TestDownloadFileImpl : public DownloadFileImpl {
public:
TestDownloadFileImpl(std::unique_ptr<DownloadSaveInfo> save_info,
const base::FilePath& default_downloads_directory,
std::unique_ptr<InputStream> stream,
uint32_t download_id,
base::WeakPtr<DownloadDestinationObserver> observer)
: DownloadFileImpl(std::move(save_info),
default_downloads_directory,
std::move(stream),
download_id,
observer) {}
protected:
base::TimeDelta GetRetryDelayForFailedRename(int attempt_count) override {
return base::TimeDelta::FromMilliseconds(0);
}
#if !defined(OS_WIN)
// On Posix, we don't encounter transient errors during renames, except
// possibly EAGAIN, which is difficult to replicate reliably. So we resort to
// simulating a transient error using ACCESS_DENIED instead.
bool ShouldRetryFailedRename(DownloadInterruptReason reason) override {
return reason == DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
}
#endif
};
} // namespace
class DownloadFileTest : public testing::Test {
public:
static const char kTestData1[];
static const char kTestData2[];
static const char kTestData3[];
static const char kTestData4[];
static const char kTestData5[];
static const char* kTestData6[];
static const char* kTestData7[];
static const char* kTestData8[];
static const char kDataHash[];
static const char kEmptyHash[];
static const uint32_t kDummyDownloadId;
static const int kDummyChildId;
static const int kDummyRequestId;
DownloadFileTest()
: observer_(new StrictMock<MockDownloadDestinationObserver>),
observer_factory_(observer_.get()),
input_stream_(nullptr),
additional_streams_(
std::vector<StrictMock<MockInputStream>*>{nullptr, nullptr}),
bytes_(-1),
bytes_per_sec_(-1) {}
~DownloadFileTest() override {}
void SetUpdateDownloadInfo(
int64_t bytes,
int64_t bytes_per_sec,
const std::vector<DownloadItem::ReceivedSlice>& received_slices) {
bytes_ = bytes;
bytes_per_sec_ = bytes_per_sec;
}
void ConfirmUpdateDownloadInfo() {
observer_->CurrentUpdateStatus(bytes_, bytes_per_sec_);
}
void SetUp() override {
EXPECT_CALL(*(observer_.get()), DestinationUpdate(_, _, _))
.Times(AnyNumber())
.WillRepeatedly(Invoke(this, &DownloadFileTest::SetUpdateDownloadInfo));
bool result = download_dir_.CreateUniqueTempDir();
CHECK(result);
}
// Mock calls to this function are forwarded here.
void RegisterCallback(
const mojo::SimpleWatcher::ReadyCallback& sink_callback) {
sink_callback_ = sink_callback;
}
void ClearCallback() { sink_callback_.Reset(); }
void SetInterruptReasonCallback(const base::Closure& closure,
DownloadInterruptReason* reason_p,
DownloadInterruptReason reason,
int64_t bytes_wasted) {
*reason_p = reason;
closure.Run();
}
bool CreateDownloadFile(bool calculate_hash) {
return CreateDownloadFile(0, calculate_hash, DownloadItem::ReceivedSlices(),
-1);
}
bool CreateDownloadFile(int length,
bool calculate_hash,
const DownloadItem::ReceivedSlices& received_slices) {
return CreateDownloadFile(length, calculate_hash,
DownloadItem::ReceivedSlices(), -1);
}
bool CreateDownloadFile(int length,
bool calculate_hash,
const DownloadItem::ReceivedSlices& received_slices,
int file_offset) {
// There can be only one.
DCHECK(!download_file_);
input_stream_ = new StrictMock<MockInputStream>();
// TODO: Need to actually create a function that'll set the variables
// based on the inputs from the callback.
EXPECT_CALL(*input_stream_, RegisterDataReadyCallback(_))
.WillOnce(Invoke(this, &DownloadFileTest::RegisterCallback))
.RetiresOnSaturation();
std::unique_ptr<DownloadSaveInfo> save_info(new DownloadSaveInfo());
// Fill the file by repeatedly copying |kTestData1| if |file_offset| is
// positive.
if (file_offset > 0) {
base::CreateTemporaryFileInDir(download_dir_.GetPath(),
&save_info->file_path);
int len = file_offset;
int data_len = strlen(kTestData1);
while (len > 0) {
int bytes_to_write = len > data_len ? data_len : len;
base::AppendToFile(save_info->file_path, kTestData1, bytes_to_write);
len -= bytes_to_write;
}
}
save_info->offset = 0;
save_info->length = length;
save_info->file_offset = file_offset;
download_file_.reset(new TestDownloadFileImpl(
std::move(save_info), download_dir_.GetPath(),
std::unique_ptr<MockInputStream>(input_stream_),
DownloadItem::kInvalidId, observer_factory_.GetWeakPtr()));
EXPECT_CALL(*input_stream_, Read(_, _))
.WillOnce(Return(InputStream::EMPTY))
.RetiresOnSaturation();
base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this);
DownloadInterruptReason result = DOWNLOAD_INTERRUPT_REASON_NONE;
base::RunLoop loop_runner;
download_file_->Initialize(
base::BindRepeating(&DownloadFileTest::SetInterruptReasonCallback,
weak_ptr_factory.GetWeakPtr(),
loop_runner.QuitClosure(), &result),
DownloadFile::CancelRequestCallback(), received_slices, true);
loop_runner.Run();
::testing::Mock::VerifyAndClearExpectations(input_stream_);
return result == DOWNLOAD_INTERRUPT_REASON_NONE;
}
void DestroyDownloadFile(int offset, bool compare_disk_data = true) {
EXPECT_FALSE(download_file_->InProgress());
// Make sure the data has been properly written to disk.
if (compare_disk_data) {
std::string disk_data;
EXPECT_TRUE(
base::ReadFileToString(download_file_->FullPath(), &disk_data));
EXPECT_EQ(expected_data_, disk_data);
}
// Make sure the Browser and File threads outlive the DownloadFile
// to satisfy thread checks inside it.
download_file_.reset();
}
// Setup the stream to append data or write from |offset| to the file.
// Don't actually trigger the callback or do verifications.
void SetupDataAppend(const char** data_chunks,
size_t num_chunks,
MockInputStream* input_stream,
::testing::Sequence s,
int64_t offset = -1) {
DCHECK(input_stream);
size_t current_pos = static_cast<size_t>(offset);
for (size_t i = 0; i < num_chunks; i++) {
const char* source_data = data_chunks[i];
size_t length = strlen(source_data);
scoped_refptr<net::IOBuffer> data =
base::MakeRefCounted<net::IOBuffer>(length);
memcpy(data->data(), source_data, length);
EXPECT_CALL(*input_stream, Read(_, _))
.InSequence(s)
.WillOnce(DoAll(SetArgPointee<0>(data), SetArgPointee<1>(length),
Return(InputStream::HAS_DATA)))
.RetiresOnSaturation();
if (offset < 0) {
// Append data.
expected_data_ += source_data;
continue;
}
// Write from offset. May fill holes with '\0'.
size_t new_len = current_pos + length;
if (new_len > expected_data_.size())
expected_data_.append(new_len - expected_data_.size(), '\0');
expected_data_.replace(current_pos, length, source_data);
current_pos += length;
}
}
void VerifyStreamAndSize() {
::testing::Mock::VerifyAndClearExpectations(input_stream_);
int64_t size;
EXPECT_TRUE(base::GetFileSize(download_file_->FullPath(), &size));
EXPECT_EQ(expected_data_.size(), static_cast<size_t>(size));
}
// TODO(rdsmith): Manage full percentage issues properly.
void AppendDataToFile(const char** data_chunks, size_t num_chunks) {
::testing::Sequence s1;
SetupDataAppend(data_chunks, num_chunks, input_stream_, s1);
EXPECT_CALL(*input_stream_, Read(_, _))
.InSequence(s1)
.WillOnce(Return(InputStream::EMPTY))
.RetiresOnSaturation();
sink_callback_.Run(MOJO_RESULT_OK);
VerifyStreamAndSize();
}
void SetupFinishStream(DownloadInterruptReason interrupt_reason,
MockInputStream* input_stream,
::testing::Sequence s) {
EXPECT_CALL(*input_stream, Read(_, _))
.InSequence(s)
.WillOnce(Return(InputStream::COMPLETE))
.RetiresOnSaturation();
EXPECT_CALL(*input_stream, GetCompletionStatus())
.InSequence(s)
.WillOnce(Return(interrupt_reason))
.RetiresOnSaturation();
EXPECT_CALL(*input_stream, ClearDataReadyCallback()).RetiresOnSaturation();
}
void FinishStream(DownloadInterruptReason interrupt_reason,
bool check_observer,
const std::string& expected_hash) {
::testing::Sequence s1;
SetupFinishStream(interrupt_reason, input_stream_, s1);
sink_callback_.Run(MOJO_RESULT_OK);
VerifyStreamAndSize();
if (check_observer) {
EXPECT_CALL(*(observer_.get()),
MockDestinationCompleted(_, expected_hash));
base::RunLoop().RunUntilIdle();
::testing::Mock::VerifyAndClearExpectations(observer_.get());
EXPECT_CALL(*(observer_.get()), DestinationUpdate(_, _, _))
.Times(AnyNumber())
.WillRepeatedly(
Invoke(this, &DownloadFileTest::SetUpdateDownloadInfo));
}
}
DownloadInterruptReason RenameAndUniquify(const base::FilePath& full_path,
base::FilePath* result_path_p) {
return InvokeRenameMethodAndWaitForCallback(RENAME_AND_UNIQUIFY, full_path,
result_path_p);
}
DownloadInterruptReason RenameAndAnnotate(const base::FilePath& full_path,
base::FilePath* result_path_p) {
return InvokeRenameMethodAndWaitForCallback(RENAME_AND_ANNOTATE, full_path,
result_path_p);
}
void ExpectPermissionError(DownloadInterruptReason err) {
EXPECT_TRUE(err == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR ||
err == DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED)
<< "Interrupt reason = " << err;
}
protected:
void InvokeRenameMethod(
DownloadFileRenameMethodType method,
const base::FilePath& full_path,
const DownloadFile::RenameCompletionCallback& completion_callback) {
switch (method) {
case RENAME_AND_UNIQUIFY:
download_file_->RenameAndUniquify(full_path, completion_callback);
break;
case RENAME_AND_ANNOTATE:
download_file_->RenameAndAnnotate(
full_path, "12345678-ABCD-1234-DCBA-123456789ABC", GURL(), GURL(),
nullptr, completion_callback);
break;
}
}
DownloadInterruptReason InvokeRenameMethodAndWaitForCallback(
DownloadFileRenameMethodType method,
const base::FilePath& full_path,
base::FilePath* result_path_p) {
DownloadInterruptReason result_reason(DOWNLOAD_INTERRUPT_REASON_NONE);
base::FilePath result_path;
base::RunLoop loop_runner;
DownloadFile::RenameCompletionCallback completion_callback =
base::Bind(&DownloadFileTest::SetRenameResult, base::Unretained(this),
loop_runner.QuitClosure(), &result_reason, result_path_p);
InvokeRenameMethod(method, full_path, completion_callback);
loop_runner.Run();
return result_reason;
}
// Prepare a byte stream to write to the file sink.
void PrepareStream(StrictMock<MockInputStream>** stream,
int64_t offset,
bool create_stream,
bool will_finish,
const char** buffers,
size_t num_buffer) {
if (create_stream)
*stream = new StrictMock<MockInputStream>();
// Expectation on MockInputStream for MultipleStreams tests:
// 1. RegisterCallback: Must called twice. One to set the callback, the
// other to release the stream.
// 2. Read: If filled with N buffer, called (N+1) times, where the last Read
// call doesn't read any data but returns STRAM_COMPLETE.
// The stream may terminate in the middle and less Read calls are expected.
// 3. GetStatus: Only called if the stream is completed and last Read call
// returns STREAM_COMPLETE.
Sequence seq;
SetupDataAppend(buffers, num_buffer, *stream, seq, offset);
if (will_finish)
SetupFinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, *stream, seq);
}
void VerifySourceStreamsStates(const SourceStreamTestData& data) {
DCHECK(download_file_->source_streams_.find(data.offset) !=
download_file_->source_streams_.end())
<< "Can't find stream at offset : " << data.offset;
DownloadFileImpl::SourceStream* stream =
download_file_->source_streams_[data.offset].get();
DCHECK(stream);
EXPECT_EQ(data.offset, stream->offset());
EXPECT_EQ(data.bytes_written, stream->bytes_written());
EXPECT_EQ(data.finished, stream->is_finished());
}
size_t source_streams_count() const {
DCHECK(download_file_);
return download_file_->source_streams_.size();
}
int64_t TotalBytesReceived() const {
DCHECK(download_file_);
return download_file_->TotalBytesReceived();
}
std::unique_ptr<StrictMock<MockDownloadDestinationObserver>> observer_;
base::WeakPtrFactory<DownloadDestinationObserver> observer_factory_;
// DownloadFile instance we are testing.
std::unique_ptr<DownloadFileImpl> download_file_;
// Stream for sending data into the download file.
// Owned by download_file_; will be alive for lifetime of download_file_.
StrictMock<MockInputStream>* input_stream_;
// Additional streams to test multiple stream write.
std::vector<StrictMock<MockInputStream>*> additional_streams_;
// Sink callback data for stream.
mojo::SimpleWatcher::ReadyCallback sink_callback_;
base::ScopedTempDir download_dir_;
// Latest update sent to the observer.
int64_t bytes_;
int64_t bytes_per_sec_;
// Keep track of what data should be saved to the disk file.
std::string expected_data_;
private:
void SetRenameResult(const base::Closure& closure,
DownloadInterruptReason* reason_p,
base::FilePath* result_path_p,
DownloadInterruptReason reason,
const base::FilePath& result_path) {
if (reason_p)
*reason_p = reason;
if (result_path_p)
*result_path_p = result_path;
closure.Run();
}
base::test::ScopedTaskEnvironment scoped_task_environment_;
};
// DownloadFile::RenameAndAnnotate and DownloadFile::RenameAndUniquify have a
// considerable amount of functional overlap. In order to re-use test logic, we
// are going to introduce this value parameterized test fixture. It will take a
// DownloadFileRenameMethodType value which can be either of the two rename
// methods.
class DownloadFileTestWithRename
: public DownloadFileTest,
public ::testing::WithParamInterface<DownloadFileRenameMethodType> {
protected:
DownloadInterruptReason InvokeSelectedRenameMethod(
const base::FilePath& full_path,
base::FilePath* result_path_p) {
return InvokeRenameMethodAndWaitForCallback(GetParam(), full_path,
result_path_p);
}
};
// And now instantiate all DownloadFileTestWithRename tests using both
// DownloadFile rename methods. Each test of the form
// DownloadFileTestWithRename.<FooTest> will be instantiated once with
// RenameAndAnnotate as the value parameter and once with RenameAndUniquify as
// the value parameter.
INSTANTIATE_TEST_SUITE_P(DownloadFile,
DownloadFileTestWithRename,
::testing::Values(RENAME_AND_ANNOTATE,
RENAME_AND_UNIQUIFY));
const char DownloadFileTest::kTestData1[] =
"Let's write some data to the file!\n";
const char DownloadFileTest::kTestData2[] = "Writing more data.\n";
const char DownloadFileTest::kTestData3[] = "Final line.";
const char DownloadFileTest::kTestData4[] = "abcdefg";
const char DownloadFileTest::kTestData5[] = "01234";
const char* DownloadFileTest::kTestData6[] = {kTestData1, kTestData2};
const char* DownloadFileTest::kTestData7[] = {kTestData4, kTestData5};
const char* DownloadFileTest::kTestData8[] = {kTestData1, kTestData2,
kTestData4, kTestData5};
const char DownloadFileTest::kDataHash[] =
"CBF68BF10F8003DB86B31343AFAC8C7175BD03FB5FC905650F8C80AF087443A8";
const char DownloadFileTest::kEmptyHash[] =
"E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855";
const uint32_t DownloadFileTest::kDummyDownloadId = 23;
const int DownloadFileTest::kDummyChildId = 3;
const int DownloadFileTest::kDummyRequestId = 67;
// Rename the file before any data is downloaded, after some has, after it all
// has, and after it's closed.
TEST_P(DownloadFileTestWithRename, RenameFileFinal) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
base::FilePath path_1(initial_path.InsertBeforeExtensionASCII("_1"));
base::FilePath path_2(initial_path.InsertBeforeExtensionASCII("_2"));
base::FilePath path_3(initial_path.InsertBeforeExtensionASCII("_3"));
base::FilePath path_4(initial_path.InsertBeforeExtensionASCII("_4"));
base::FilePath output_path;
// Rename the file before downloading any data.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
InvokeSelectedRenameMethod(path_1, &output_path));
base::FilePath renamed_path = download_file_->FullPath();
EXPECT_EQ(path_1, renamed_path);
EXPECT_EQ(path_1, output_path);
// Check the files.
EXPECT_FALSE(base::PathExists(initial_path));
EXPECT_TRUE(base::PathExists(path_1));
// Download the data.
const char* chunks1[] = {kTestData1, kTestData2};
AppendDataToFile(chunks1, 2);
// Rename the file after downloading some data.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
InvokeSelectedRenameMethod(path_2, &output_path));
renamed_path = download_file_->FullPath();
EXPECT_EQ(path_2, renamed_path);
EXPECT_EQ(path_2, output_path);
// Check the files.
EXPECT_FALSE(base::PathExists(path_1));
EXPECT_TRUE(base::PathExists(path_2));
const char* chunks2[] = {kTestData3};
AppendDataToFile(chunks2, 1);
// Rename the file after downloading all the data.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
InvokeSelectedRenameMethod(path_3, &output_path));
renamed_path = download_file_->FullPath();
EXPECT_EQ(path_3, renamed_path);
EXPECT_EQ(path_3, output_path);
// Check the files.
EXPECT_FALSE(base::PathExists(path_2));
EXPECT_TRUE(base::PathExists(path_3));
FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true, kDataHash);
base::RunLoop().RunUntilIdle();
// Rename the file after downloading all the data and closing the file.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
InvokeSelectedRenameMethod(path_4, &output_path));
renamed_path = download_file_->FullPath();
EXPECT_EQ(path_4, renamed_path);
EXPECT_EQ(path_4, output_path);
// Check the files.
EXPECT_FALSE(base::PathExists(path_3));
EXPECT_TRUE(base::PathExists(path_4));
DestroyDownloadFile(0);
}
// Test to make sure the rename overwrites when requested. This is separate from
// the above test because it only applies to RenameAndAnnotate().
// RenameAndUniquify() doesn't overwrite by design.
TEST_F(DownloadFileTest, RenameOverwrites) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
base::FilePath path_1(initial_path.InsertBeforeExtensionASCII("_1"));
ASSERT_FALSE(base::PathExists(path_1));
static const char file_data[] = "xyzzy";
ASSERT_EQ(static_cast<int>(sizeof(file_data)),
base::WriteFile(path_1, file_data, sizeof(file_data)));
ASSERT_TRUE(base::PathExists(path_1));
base::FilePath new_path;
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
RenameAndAnnotate(path_1, &new_path));
EXPECT_EQ(path_1.value(), new_path.value());
std::string file_contents;
ASSERT_TRUE(base::ReadFileToString(new_path, &file_contents));
EXPECT_NE(std::string(file_data), file_contents);
FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true, kEmptyHash);
base::RunLoop().RunUntilIdle();
DestroyDownloadFile(0);
}
// Test to make sure the rename uniquifies if we aren't overwriting
// and there's a file where we're aiming. As above, not a
// DownloadFileTestWithRename test because this only applies to
// RenameAndUniquify().
TEST_F(DownloadFileTest, RenameUniquifies) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
base::FilePath path_1(initial_path.InsertBeforeExtensionASCII("_1"));
base::FilePath path_1_suffixed(path_1.InsertBeforeExtensionASCII(" (1)"));
ASSERT_FALSE(base::PathExists(path_1));
static const char file_data[] = "xyzzy";
ASSERT_EQ(static_cast<int>(sizeof(file_data)),
base::WriteFile(path_1, file_data, sizeof(file_data)));
ASSERT_TRUE(base::PathExists(path_1));
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, RenameAndUniquify(path_1, nullptr));
EXPECT_TRUE(base::PathExists(path_1_suffixed));
FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true, kEmptyHash);
base::RunLoop().RunUntilIdle();
DestroyDownloadFile(0);
}
// Test that RenameAndUniquify doesn't try to uniquify in the case where the
// target filename is the same as the current filename.
TEST_F(DownloadFileTest, RenameRecognizesSelfConflict) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
base::FilePath new_path;
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
RenameAndUniquify(initial_path, &new_path));
EXPECT_TRUE(base::PathExists(initial_path));
FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true, kEmptyHash);
base::RunLoop().RunUntilIdle();
DestroyDownloadFile(0);
EXPECT_EQ(initial_path.value(), new_path.value());
}
// Test to make sure we get the proper error on failure.
TEST_P(DownloadFileTestWithRename, RenameError) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
// Create a subdirectory.
base::FilePath target_dir(
initial_path.DirName().Append(FILE_PATH_LITERAL("TargetDir")));
ASSERT_FALSE(base::DirectoryExists(target_dir));
ASSERT_TRUE(base::CreateDirectory(target_dir));
base::FilePath target_path(target_dir.Append(initial_path.BaseName()));
// Targets
base::FilePath target_path_suffixed(
target_path.InsertBeforeExtensionASCII(" (1)"));
ASSERT_FALSE(base::PathExists(target_path));
ASSERT_FALSE(base::PathExists(target_path_suffixed));
// Make the directory unwritable and try to rename within it.
{
base::FilePermissionRestorer restorer(target_dir);
ASSERT_TRUE(base::MakeFileUnwritable(target_dir));
// Expect nulling out of further processing.
EXPECT_CALL(*input_stream_, ClearDataReadyCallback());
ExpectPermissionError(InvokeSelectedRenameMethod(target_path, nullptr));
EXPECT_FALSE(base::PathExists(target_path_suffixed));
}
FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true, kEmptyHash);
base::RunLoop().RunUntilIdle();
DestroyDownloadFile(0);
}
namespace {
void TestRenameCompletionCallback(const base::Closure& closure,
bool* did_run_callback,
DownloadInterruptReason interrupt_reason,
const base::FilePath& new_path) {
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, interrupt_reason);
*did_run_callback = true;
closure.Run();
}
} // namespace
// Test that the retry logic works. This test assumes that DownloadFileImpl will
// post tasks to the current message loop (acting as the download sequence)
// asynchronously to retry the renames. We will stuff RunLoop::QuitClosures()
// in between the retry tasks to stagger them and then allow the rename to
// succeed.
//
// Note that there is only one queue of tasks to run, and that is in the tests'
// base::MessageLoopCurrent::Get(). Each RunLoop processes that queue until it
// sees a QuitClosure() targeted at itself, at which point it stops processing.
TEST_P(DownloadFileTestWithRename, RenameWithErrorRetry) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
// Create a subdirectory.
base::FilePath target_dir(
initial_path.DirName().Append(FILE_PATH_LITERAL("TargetDir")));
ASSERT_FALSE(base::DirectoryExists(target_dir));
ASSERT_TRUE(base::CreateDirectory(target_dir));
base::FilePath target_path(target_dir.Append(initial_path.BaseName()));
bool did_run_callback = false;
// Each RunLoop can be used the run the MessageLoop until the corresponding
// QuitClosure() is run. This one is used to produce the QuitClosure() that
// will be run when the entire rename operation is complete.
base::RunLoop succeeding_run;
{
// (Scope for the base::File or base::FilePermissionRestorer below.)
#if defined(OS_WIN)
// On Windows we test with an actual transient error, a sharing violation.
// The rename will fail because we are holding the file open for READ. On
// Posix this doesn't cause a failure.
base::File locked_file(initial_path,
base::File::FLAG_OPEN | base::File::FLAG_READ);
ASSERT_TRUE(locked_file.IsValid());
#else
// Simulate a transient failure by revoking write permission for target_dir.
// The TestDownloadFileImpl class treats this error as transient even though
// DownloadFileImpl itself doesn't.
base::FilePermissionRestorer restore_permissions_for(target_dir);
ASSERT_TRUE(base::MakeFileUnwritable(target_dir));
#endif
// The Rename() should fail here and enqueue a retry task without invoking
// the completion callback.
InvokeRenameMethod(
GetParam(), target_path,
base::Bind(&TestRenameCompletionCallback, succeeding_run.QuitClosure(),
&did_run_callback));
EXPECT_FALSE(did_run_callback);
base::RunLoop first_failing_run;
// Queue the QuitClosure() on the MessageLoop now. Any tasks queued by the
// Rename() will be in front of the QuitClosure(). Running the message loop
// now causes the just the first retry task to be run. The rename still
// fails, so another retry task would get queued behind the QuitClosure().
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, first_failing_run.QuitClosure());
first_failing_run.Run();
EXPECT_FALSE(did_run_callback);
// Running another loop should have the same effect as the above as long as
// kMaxRenameRetries is greater than 2.
base::RunLoop second_failing_run;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, second_failing_run.QuitClosure());
second_failing_run.Run();
EXPECT_FALSE(did_run_callback);
}
// This time the QuitClosure from succeeding_run should get executed.
succeeding_run.Run();
EXPECT_TRUE(did_run_callback);
FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true, kEmptyHash);
base::RunLoop().RunUntilIdle();
DestroyDownloadFile(0);
}
// Various tests of the StreamActive method.
TEST_F(DownloadFileTest, StreamEmptySuccess) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
// Test that calling the sink_callback_ on an empty stream shouldn't
// do anything.
AppendDataToFile(nullptr, 0);
// Finish the download this way and make sure we see it on the observer.
FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true, kEmptyHash);
base::RunLoop().RunUntilIdle();
DestroyDownloadFile(0);
}
TEST_F(DownloadFileTest, StreamEmptyError) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
// Finish the download in error and make sure we see it on the
// observer.
EXPECT_CALL(
*(observer_.get()),
MockDestinationError(DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED, 0,
kEmptyHash))
.WillOnce(InvokeWithoutArgs(
this, &DownloadFileTest::ConfirmUpdateDownloadInfo));
// If this next EXPECT_CALL fails flakily, it's probably a real failure.
// We'll be getting a stream of UpdateDownload calls from the timer, and
// the last one may have the correct information even if the failure
// doesn't produce an update, as the timer update may have triggered at the
// same time.
EXPECT_CALL(*(observer_.get()), CurrentUpdateStatus(0, _));
FinishStream(DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED, false,
kEmptyHash);
base::RunLoop().RunUntilIdle();
DestroyDownloadFile(0);
}
TEST_F(DownloadFileTest, StreamNonEmptySuccess) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
const char* chunks1[] = {kTestData1, kTestData2};
::testing::Sequence s1;
SetupDataAppend(chunks1, 2, input_stream_, s1);
SetupFinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, input_stream_, s1);
EXPECT_CALL(*(observer_.get()), MockDestinationCompleted(_, _));
sink_callback_.Run(MOJO_RESULT_OK);
VerifyStreamAndSize();
base::RunLoop().RunUntilIdle();
DestroyDownloadFile(0);
}
TEST_F(DownloadFileTest, StreamNonEmptyError) {
ASSERT_TRUE(CreateDownloadFile(true));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
const char* chunks1[] = {kTestData1, kTestData2};
::testing::Sequence s1;
SetupDataAppend(chunks1, 2, input_stream_, s1);
SetupFinishStream(DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED,
input_stream_, s1);
EXPECT_CALL(*(observer_.get()),
MockDestinationError(
DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED, _, _))
.WillOnce(InvokeWithoutArgs(
this, &DownloadFileTest::ConfirmUpdateDownloadInfo));
// If this next EXPECT_CALL fails flakily, it's probably a real failure.
// We'll be getting a stream of UpdateDownload calls from the timer, and
// the last one may have the correct information even if the failure
// doesn't produce an update, as the timer update may have triggered at the
// same time.
EXPECT_CALL(*(observer_.get()),
CurrentUpdateStatus(strlen(kTestData1) + strlen(kTestData2), _));
sink_callback_.Run(MOJO_RESULT_OK);
base::RunLoop().RunUntilIdle();
VerifyStreamAndSize();
DestroyDownloadFile(0);
}
// Tests that if file content validation succeeds, all the remaining data will
// be writing to the file.
TEST_F(DownloadFileTest, FileContentValidationSuccess) {
int stream_length = strlen(kTestData1) * 2;
ASSERT_TRUE(CreateDownloadFile(
stream_length /* length */, true /* calculate_hash */,
DownloadItem::ReceivedSlices(), strlen(kTestData1) - 1));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
const char* chunks1[] = {kTestData1, kTestData1};
::testing::Sequence s1;
SetupDataAppend(chunks1, 2 /* num_chunks */, input_stream_, s1);
SetupFinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, input_stream_, s1);
EXPECT_CALL(*(observer_.get()), MockDestinationCompleted(_, _));
sink_callback_.Run(MOJO_RESULT_OK);
VerifyStreamAndSize();
base::RunLoop().RunUntilIdle();
DestroyDownloadFile(0);
}
// Tests that if file content validation fails, an error will occur and no data
// will be written.
TEST_F(DownloadFileTest, FileContentValidationFail) {
int file_length = strlen(kTestData2) - 1;
int stream_length = strlen(kTestData1) + strlen(kTestData2);
ASSERT_TRUE(CreateDownloadFile(stream_length /* length */,
true /* calculate_hash */,
DownloadItem::ReceivedSlices(), file_length));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
std::string file_content = std::string(kTestData1, 0, file_length);
expected_data_ = file_content;
VerifyStreamAndSize();
const char* chunks1[] = {kTestData2, kTestData1};
::testing::Sequence s1;
// Only 1 chunk will be read, and it will generate an error after
// failing the validation.
SetupDataAppend(chunks1, 1 /* num_chunks */, input_stream_, s1);
EXPECT_CALL(*input_stream_, ClearDataReadyCallback());
EXPECT_CALL(*(observer_.get()),
MockDestinationError(DOWNLOAD_INTERRUPT_REASON_FILE_HASH_MISMATCH,
file_length, _));
sink_callback_.Run(MOJO_RESULT_OK);
base::RunLoop().RunUntilIdle();
expected_data_ = file_content;
DestroyDownloadFile(0);
}
// Tests for concurrent streams handling, used for parallel download.
//
// Activate both streams at the same time.
TEST_F(DownloadFileTest, MultipleStreamsWrite) {
int64_t stream_0_length = GetBuffersLength(kTestData6, 2);
int64_t stream_1_length = GetBuffersLength(kTestData7, 2);
ASSERT_TRUE(CreateDownloadFile(stream_0_length, true,
DownloadItem::ReceivedSlices()));
PrepareStream(&input_stream_, 0, false, true, kTestData6, 2);
PrepareStream(&additional_streams_[0], stream_0_length, true, true,
kTestData7, 2);
EXPECT_CALL(*additional_streams_[0], RegisterDataReadyCallback(_))
.RetiresOnSaturation();
EXPECT_CALL(*(observer_.get()), MockDestinationCompleted(_, _));
// Activate the streams.
download_file_->AddInputStream(
std::unique_ptr<MockInputStream>(additional_streams_[0]), stream_0_length,
DownloadSaveInfo::kLengthFullContent);
sink_callback_.Run(MOJO_RESULT_OK);
base::RunLoop().RunUntilIdle();
SourceStreamTestData stream_data_0(0, stream_0_length, true);
SourceStreamTestData stream_data_1(stream_0_length, stream_1_length, true);
VerifySourceStreamsStates(stream_data_0);
VerifySourceStreamsStates(stream_data_1);
EXPECT_EQ(stream_0_length + stream_1_length, TotalBytesReceived());
DestroyDownloadFile(0);
}
// 3 streams write to one sink, the second stream has a limited length.
TEST_F(DownloadFileTest, MutipleStreamsLimitedLength) {
int64_t stream_0_length = GetBuffersLength(kTestData6, 2);
// The second stream has a limited length and should be partially written
// to disk. When we prepare the stream, we fill the stream with 2 full buffer.
int64_t stream_1_length = GetBuffersLength(kTestData7, 2) - 1;
// The last stream can't have length limit, it's a half open request, e.g
// "Range:50-".
int64_t stream_2_length = GetBuffersLength(kTestData6, 2);
ASSERT_TRUE(CreateDownloadFile(stream_0_length, true,
DownloadItem::ReceivedSlices()));
PrepareStream(&input_stream_, 0, false, true, kTestData6, 2);
PrepareStream(&additional_streams_[0], stream_0_length, true, false,
kTestData7, 2);
PrepareStream(&additional_streams_[1], stream_0_length + stream_1_length,
true, true, kTestData6, 2);
EXPECT_CALL(*additional_streams_[0], RegisterDataReadyCallback(_))
.Times(1)
.RetiresOnSaturation();
EXPECT_CALL(*additional_streams_[0], ClearDataReadyCallback())
.Times(1)
.RetiresOnSaturation();
EXPECT_CALL(*additional_streams_[1], RegisterDataReadyCallback(_))
.RetiresOnSaturation();
EXPECT_CALL(*(observer_.get()), MockDestinationCompleted(_, _));
// Activate all the streams.
download_file_->AddInputStream(
std::unique_ptr<MockInputStream>(additional_streams_[0]), stream_0_length,
stream_1_length);
download_file_->AddInputStream(
std::unique_ptr<MockInputStream>(additional_streams_[1]),
stream_0_length + stream_1_length, DownloadSaveInfo::kLengthFullContent);
sink_callback_.Run(MOJO_RESULT_OK);
base::RunLoop().RunUntilIdle();
SourceStreamTestData stream_data_0(0, stream_0_length, true);
SourceStreamTestData stream_data_1(stream_0_length, stream_1_length, true);
SourceStreamTestData stream_data_2(stream_0_length + stream_1_length,
stream_2_length, true);
VerifySourceStreamsStates(stream_data_0);
VerifySourceStreamsStates(stream_data_1);
VerifySourceStreamsStates(stream_data_2);
EXPECT_EQ(stream_0_length + stream_1_length + stream_2_length,
TotalBytesReceived());
download_file_->Cancel();
DestroyDownloadFile(0, false);
}
// Activate and deplete one stream, later add the second stream.
TEST_F(DownloadFileTest, MultipleStreamsFirstStreamWriteAllData) {
int64_t stream_0_length = GetBuffersLength(kTestData8, 4);
ASSERT_TRUE(CreateDownloadFile(DownloadSaveInfo::kLengthFullContent, true,
DownloadItem::ReceivedSlices()));
PrepareStream(&input_stream_, 0, false, true, kTestData8, 4);
EXPECT_CALL(*(observer_.get()), MockDestinationCompleted(_, _));
sink_callback_.Run(MOJO_RESULT_OK);
base::RunLoop().RunUntilIdle();
// Add another stream, the file is already closed, so nothing should be
// called.
EXPECT_FALSE(download_file_->InProgress());
additional_streams_[0] = new StrictMock<MockInputStream>();
download_file_->AddInputStream(
std::unique_ptr<MockInputStream>(additional_streams_[0]),
stream_0_length - 1, DownloadSaveInfo::kLengthFullContent);
base::RunLoop().RunUntilIdle();
SourceStreamTestData stream_data_0(0, stream_0_length, true);
VerifySourceStreamsStates(stream_data_0);
EXPECT_EQ(stream_0_length, TotalBytesReceived());
EXPECT_EQ(1u, source_streams_count());
DestroyDownloadFile(0);
}
// While one stream is writing, kick off another stream with an offset that has
// been written by the first one.
TEST_F(DownloadFileTest, SecondStreamStartingOffsetAlreadyWritten) {
int64_t stream_0_length = GetBuffersLength(kTestData6, 2);
ASSERT_TRUE(CreateDownloadFile(stream_0_length, true,
DownloadItem::ReceivedSlices()));
Sequence seq;
SetupDataAppend(kTestData6, 2, input_stream_, seq, 0);
EXPECT_CALL(*input_stream_, Read(_, _))
.InSequence(seq)
.WillOnce(Return(InputStream::EMPTY))
.RetiresOnSaturation();
sink_callback_.Run(MOJO_RESULT_OK);
base::RunLoop().RunUntilIdle();
additional_streams_[0] = new StrictMock<MockInputStream>();
EXPECT_CALL(*additional_streams_[0], RegisterDataReadyCallback(_))
.WillRepeatedly(Invoke(this, &DownloadFileTest::RegisterCallback))
.RetiresOnSaturation();
EXPECT_CALL(*additional_streams_[0], ClearDataReadyCallback())
.WillRepeatedly(Invoke(this, &DownloadFileTest::ClearCallback))
.RetiresOnSaturation();
EXPECT_CALL(*additional_streams_[0], Read(_, _))
.WillOnce(Return(InputStream::EMPTY))
.RetiresOnSaturation();
download_file_->AddInputStream(
std::unique_ptr<MockInputStream>(additional_streams_[0]),
strlen(kTestData1), DownloadSaveInfo::kLengthFullContent);
// The stream should get terminated and reset the callback.
EXPECT_TRUE(sink_callback_.is_null());
download_file_->Cancel();
DestroyDownloadFile(0, false);
}
} // namespace download