普通文本  |  606行  |  21.26 KB

// 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 "base/file_util.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/test_file_util.h"
#include "content/browser/browser_thread_impl.h"
#include "content/browser/byte_stream.h"
#include "content/browser/download/download_create_info.h"
#include "content/browser/download/download_file_impl.h"
#include "content/browser/download/download_request_handle.h"
#include "content/public/browser/download_destination_observer.h"
#include "content/public/browser/download_interrupt_reasons.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/power_save_blocker.h"
#include "content/public/test/mock_download_manager.h"
#include "net/base/file_stream.h"
#include "net/base/mock_file_stream.h"
#include "net/base/net_errors.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::SetArgPointee;
using ::testing::StrictMock;

namespace content {
namespace {

class MockByteStreamReader : public ByteStreamReader {
 public:
  MockByteStreamReader() {}
  ~MockByteStreamReader() {}

  // ByteStream functions
  MOCK_METHOD2(Read, ByteStreamReader::StreamState(
      scoped_refptr<net::IOBuffer>*, size_t*));
  MOCK_CONST_METHOD0(GetStatus, int());
  MOCK_METHOD1(RegisterCallback, void(const base::Closure&));
};

class MockDownloadDestinationObserver : public DownloadDestinationObserver {
 public:
  MOCK_METHOD3(DestinationUpdate, void(int64, int64, const std::string&));
  MOCK_METHOD1(DestinationError, void(DownloadInterruptReason));
  MOCK_METHOD1(DestinationCompleted, void(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_METHOD3(CurrentUpdateStatus,
               void(int64, int64, const std::string&));
};

MATCHER(IsNullCallback, "") { return (arg.is_null()); }

}  // namespace

class DownloadFileTest : public testing::Test {
 public:

  static const char* kTestData1;
  static const char* kTestData2;
  static const char* kTestData3;
  static const char* kDataHash;
  static const uint32 kDummyDownloadId;
  static const int kDummyChildId;
  static const int kDummyRequestId;

  DownloadFileTest() :
      observer_(new StrictMock<MockDownloadDestinationObserver>),
      observer_factory_(observer_.get()),
      input_stream_(NULL),
      bytes_(-1),
      bytes_per_sec_(-1),
      hash_state_("xyzzy"),
      ui_thread_(BrowserThread::UI, &loop_),
      file_thread_(BrowserThread::FILE, &loop_) {
  }

  virtual ~DownloadFileTest() {
  }

  void SetUpdateDownloadInfo(int64 bytes, int64 bytes_per_sec,
                             const std::string& hash_state) {
    bytes_ = bytes;
    bytes_per_sec_ = bytes_per_sec;
    hash_state_ = hash_state;
  }

  void ConfirmUpdateDownloadInfo() {
    observer_->CurrentUpdateStatus(bytes_, bytes_per_sec_, hash_state_);
  }

  virtual void SetUp() {
    EXPECT_CALL(*(observer_.get()), DestinationUpdate(_, _, _))
        .Times(AnyNumber())
        .WillRepeatedly(Invoke(this, &DownloadFileTest::SetUpdateDownloadInfo));
  }

  // Mock calls to this function are forwarded here.
  void RegisterCallback(base::Closure sink_callback) {
    sink_callback_ = sink_callback;
  }

  void SetInterruptReasonCallback(bool* was_called,
                                  DownloadInterruptReason* reason_p,
                                  DownloadInterruptReason reason) {
    *was_called = true;
    *reason_p = reason;
  }

  virtual bool CreateDownloadFile(int offset, bool calculate_hash) {
    // There can be only one.
    DCHECK(!download_file_.get());

    input_stream_ = new StrictMock<MockByteStreamReader>();

    // TODO: Need to actually create a function that'll set the variables
    // based on the inputs from the callback.
    EXPECT_CALL(*input_stream_, RegisterCallback(_))
        .WillOnce(Invoke(this, &DownloadFileTest::RegisterCallback))
        .RetiresOnSaturation();

    scoped_ptr<DownloadSaveInfo> save_info(new DownloadSaveInfo());
    download_file_.reset(
        new DownloadFileImpl(save_info.Pass(),
                             base::FilePath(),
                             GURL(),  // Source
                             GURL(),  // Referrer
                             calculate_hash,
                             scoped_ptr<ByteStreamReader>(input_stream_),
                             net::BoundNetLog(),
                             scoped_ptr<PowerSaveBlocker>().Pass(),
                             observer_factory_.GetWeakPtr()));
    download_file_->SetClientGuid(
        "12345678-ABCD-1234-DCBA-123456789ABC");

    EXPECT_CALL(*input_stream_, Read(_, _))
        .WillOnce(Return(ByteStreamReader::STREAM_EMPTY))
        .RetiresOnSaturation();

    base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this);
    bool called = false;
    DownloadInterruptReason result;
    download_file_->Initialize(base::Bind(
        &DownloadFileTest::SetInterruptReasonCallback,
        weak_ptr_factory.GetWeakPtr(), &called, &result));
    loop_.RunUntilIdle();
    EXPECT_TRUE(called);

    ::testing::Mock::VerifyAndClearExpectations(input_stream_);
    return result == DOWNLOAD_INTERRUPT_REASON_NONE;
  }

  virtual void DestroyDownloadFile(int offset) {
    EXPECT_FALSE(download_file_->InProgress());

    // Make sure the data has been properly written to disk.
    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 do be a data append; don't actually trigger
  // the callback or do verifications.
  void SetupDataAppend(const char **data_chunks, size_t num_chunks,
                       ::testing::Sequence s) {
    DCHECK(input_stream_);
    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 = new 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(ByteStreamReader::STREAM_HAS_DATA)))
          .RetiresOnSaturation();
      expected_data_ += source_data;
    }
  }

  void VerifyStreamAndSize() {
    ::testing::Mock::VerifyAndClearExpectations(input_stream_);
    int64 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, s1);
    EXPECT_CALL(*input_stream_, Read(_, _))
        .InSequence(s1)
        .WillOnce(Return(ByteStreamReader::STREAM_EMPTY))
        .RetiresOnSaturation();
    sink_callback_.Run();
    VerifyStreamAndSize();
  }

  void SetupFinishStream(DownloadInterruptReason interrupt_reason,
                       ::testing::Sequence s) {
    EXPECT_CALL(*input_stream_, Read(_, _))
        .InSequence(s)
        .WillOnce(Return(ByteStreamReader::STREAM_COMPLETE))
        .RetiresOnSaturation();
    EXPECT_CALL(*input_stream_, GetStatus())
        .InSequence(s)
        .WillOnce(Return(interrupt_reason))
        .RetiresOnSaturation();
    EXPECT_CALL(*input_stream_, RegisterCallback(_))
        .RetiresOnSaturation();
  }

  void FinishStream(DownloadInterruptReason interrupt_reason,
                    bool check_observer) {
    ::testing::Sequence s1;
    SetupFinishStream(interrupt_reason, s1);
    sink_callback_.Run();
    VerifyStreamAndSize();
    if (check_observer) {
      EXPECT_CALL(*(observer_.get()), DestinationCompleted(_));
      loop_.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) {
    base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this);
    DownloadInterruptReason result_reason(DOWNLOAD_INTERRUPT_REASON_NONE);
    bool callback_was_called(false);
    base::FilePath result_path;

    download_file_->RenameAndUniquify(
        full_path, base::Bind(&DownloadFileTest::SetRenameResult,
                              weak_ptr_factory.GetWeakPtr(),
                              &callback_was_called,
                              &result_reason, result_path_p));
    loop_.RunUntilIdle();

    EXPECT_TRUE(callback_was_called);
    return result_reason;
  }

  DownloadInterruptReason RenameAndAnnotate(
      const base::FilePath& full_path,
      base::FilePath* result_path_p) {
    base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this);
    DownloadInterruptReason result_reason(DOWNLOAD_INTERRUPT_REASON_NONE);
    bool callback_was_called(false);
    base::FilePath result_path;

    download_file_->RenameAndAnnotate(
        full_path, base::Bind(&DownloadFileTest::SetRenameResult,
                              weak_ptr_factory.GetWeakPtr(),
                              &callback_was_called,
                              &result_reason, result_path_p));
    loop_.RunUntilIdle();

    EXPECT_TRUE(callback_was_called);
    return result_reason;
  }

 protected:
  scoped_ptr<StrictMock<MockDownloadDestinationObserver> > observer_;
  base::WeakPtrFactory<DownloadDestinationObserver> observer_factory_;

  // DownloadFile instance we are testing.
  scoped_ptr<DownloadFile> download_file_;

  // Stream for sending data into the download file.
  // Owned by download_file_; will be alive for lifetime of download_file_.
  StrictMock<MockByteStreamReader>* input_stream_;

  // Sink callback data for stream.
  base::Closure sink_callback_;

  // Latest update sent to the observer.
  int64 bytes_;
  int64 bytes_per_sec_;
  std::string hash_state_;

  base::MessageLoop loop_;

 private:
  void SetRenameResult(bool* called_p,
                       DownloadInterruptReason* reason_p,
                       base::FilePath* result_path_p,
                       DownloadInterruptReason reason,
                       const base::FilePath& result_path) {
    if (called_p)
      *called_p = true;
    if (reason_p)
      *reason_p = reason;
    if (result_path_p)
      *result_path_p = result_path;
  }

  // UI thread.
  BrowserThreadImpl ui_thread_;
  // File thread to satisfy debug checks in DownloadFile.
  BrowserThreadImpl file_thread_;

  // Keep track of what data should be saved to the disk file.
  std::string expected_data_;
};

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::kDataHash =
    "CBF68BF10F8003DB86B31343AFAC8C7175BD03FB5FC905650F8C80AF087443A8";

const uint32 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_F(DownloadFileTest, RenameFileFinal) {
  ASSERT_TRUE(CreateDownloadFile(0, 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 path_5(initial_path.InsertBeforeExtensionASCII("_5"));
  base::FilePath output_path;

  // Rename the file before downloading any data.
  EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
            RenameAndUniquify(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,
            RenameAndUniquify(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,
            RenameAndUniquify(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));

  // Should not be able to get the hash until the file is closed.
  std::string hash;
  EXPECT_FALSE(download_file_->GetHash(&hash));
  FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true);
  loop_.RunUntilIdle();

  // Rename the file after downloading all the data and closing the file.
  EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
            RenameAndUniquify(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));

  // Check the hash.
  EXPECT_TRUE(download_file_->GetHash(&hash));
  EXPECT_EQ(kDataHash, base::HexEncode(hash.data(), hash.size()));

  // Check that a rename with overwrite to an existing file succeeds.
  std::string file_contents;
  ASSERT_FALSE(base::PathExists(path_5));
  static const char file_data[] = "xyzzy";
  ASSERT_EQ(static_cast<int>(sizeof(file_data) - 1),
            file_util::WriteFile(path_5, file_data, sizeof(file_data) - 1));
  ASSERT_TRUE(base::PathExists(path_5));
  EXPECT_TRUE(base::ReadFileToString(path_5, &file_contents));
  EXPECT_EQ(std::string(file_data), file_contents);

  EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
            RenameAndAnnotate(path_5, &output_path));
  EXPECT_EQ(path_5, output_path);

  file_contents = "";
  EXPECT_TRUE(base::ReadFileToString(path_5, &file_contents));
  EXPECT_NE(std::string(file_data), file_contents);

  DestroyDownloadFile(0);
}

// Test to make sure the rename uniquifies if we aren't overwriting
// and there's a file where we're aiming.
TEST_F(DownloadFileTest, RenameUniquifies) {
  ASSERT_TRUE(CreateDownloadFile(0, 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)),
            file_util::WriteFile(path_1, file_data, sizeof(file_data)));
  ASSERT_TRUE(base::PathExists(path_1));

  EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, RenameAndUniquify(path_1, NULL));
  EXPECT_TRUE(base::PathExists(path_1_suffixed));

  FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true);
  loop_.RunUntilIdle();
  DestroyDownloadFile(0);
}

// Test to make sure we get the proper error on failure.
TEST_F(DownloadFileTest, RenameError) {
  ASSERT_TRUE(CreateDownloadFile(0, true));
  base::FilePath initial_path(download_file_->FullPath());

  // Create a subdirectory.
  base::FilePath tempdir(
      initial_path.DirName().Append(FILE_PATH_LITERAL("tempdir")));
  ASSERT_TRUE(base::CreateDirectory(tempdir));
  base::FilePath target_path(tempdir.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.
  {
    file_util::PermissionRestorer restorer(tempdir);
    ASSERT_TRUE(file_util::MakeFileUnwritable(tempdir));

    // Expect nulling out of further processing.
    EXPECT_CALL(*input_stream_, RegisterCallback(IsNullCallback()));
    EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
              RenameAndAnnotate(target_path, NULL));
    EXPECT_FALSE(base::PathExists(target_path_suffixed));
  }

  FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true);
  loop_.RunUntilIdle();
  DestroyDownloadFile(0);
}

// Various tests of the StreamActive method.
TEST_F(DownloadFileTest, StreamEmptySuccess) {
  ASSERT_TRUE(CreateDownloadFile(0, 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(NULL, 0);

  // Finish the download this way and make sure we see it on the
  // observer.
  EXPECT_CALL(*(observer_.get()), DestinationCompleted(_));
  FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, false);
  loop_.RunUntilIdle();

  DestroyDownloadFile(0);
}

TEST_F(DownloadFileTest, StreamEmptyError) {
  ASSERT_TRUE(CreateDownloadFile(0, 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()),
              DestinationError(
                  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(0, _, _));

  FinishStream(DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED, false);

  loop_.RunUntilIdle();

  DestroyDownloadFile(0);
}

TEST_F(DownloadFileTest, StreamNonEmptySuccess) {
  ASSERT_TRUE(CreateDownloadFile(0, 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, s1);
  SetupFinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, s1);
  EXPECT_CALL(*(observer_.get()), DestinationCompleted(_));
  sink_callback_.Run();
  VerifyStreamAndSize();
  loop_.RunUntilIdle();
  DestroyDownloadFile(0);
}

TEST_F(DownloadFileTest, StreamNonEmptyError) {
  ASSERT_TRUE(CreateDownloadFile(0, 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, s1);
  SetupFinishStream(DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED, s1);

  EXPECT_CALL(*(observer_.get()),
              DestinationError(
                  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();
  loop_.RunUntilIdle();
  VerifyStreamAndSize();
  DestroyDownloadFile(0);
}

// Send some data, wait 3/4s of a second, run the message loop, and
// confirm the values the observer received are correct.
TEST_F(DownloadFileTest, ConfirmUpdate) {
  CreateDownloadFile(0, true);

  const char* chunks1[] = { kTestData1, kTestData2 };
  AppendDataToFile(chunks1, 2);

  // Run the message loops for 750ms and check for results.
  loop_.PostDelayedTask(FROM_HERE,
                        base::MessageLoop::QuitClosure(),
                        base::TimeDelta::FromMilliseconds(750));
  loop_.Run();

  EXPECT_EQ(static_cast<int64>(strlen(kTestData1) + strlen(kTestData2)),
            bytes_);
  EXPECT_EQ(download_file_->GetHashState(), hash_state_);

  FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true);
  DestroyDownloadFile(0);
}

}  // namespace content