// 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 <stack> #include <string> #include <utility> #include "base/bind.h" #include "base/bind_helpers.h" #include "base/callback.h" #include "base/compiler_specific.h" #include "base/pickle.h" #include "base/synchronization/waitable_event.h" #include "base/threading/thread.h" #include "net/base/io_buffer.h" #include "net/base/net_errors.h" #include "net/http/http_response_headers.h" #include "testing/gtest/include/gtest/gtest.h" #include "webkit/browser/appcache/appcache_response.h" #include "webkit/browser/appcache/mock_appcache_service.h" using net::IOBuffer; using net::WrappedIOBuffer; namespace appcache { static const int kNumBlocks = 4; static const int kBlockSize = 1024; static const int kNoSuchResponseId = 123; class AppCacheResponseTest : public testing::Test { public: // Test Harness ------------------------------------------------------------- // Helper class used to verify test results class MockStorageDelegate : public AppCacheStorage::Delegate { public: explicit MockStorageDelegate(AppCacheResponseTest* test) : loaded_info_id_(0), test_(test) { } virtual void OnResponseInfoLoaded(AppCacheResponseInfo* info, int64 response_id) OVERRIDE { loaded_info_ = info; loaded_info_id_ = response_id; test_->ScheduleNextTask(); } scoped_refptr<AppCacheResponseInfo> loaded_info_; int64 loaded_info_id_; AppCacheResponseTest* test_; }; // Helper callback to run a test on our io_thread. The io_thread is spun up // once and reused for all tests. template <class Method> void MethodWrapper(Method method) { SetUpTest(); (this->*method)(); } static void SetUpTestCase() { io_thread_.reset(new base::Thread("AppCacheResponseTest Thread")); base::Thread::Options options(base::MessageLoop::TYPE_IO, 0); io_thread_->StartWithOptions(options); } static void TearDownTestCase() { io_thread_.reset(NULL); } AppCacheResponseTest() {} template <class Method> void RunTestOnIOThread(Method method) { test_finished_event_ .reset(new base::WaitableEvent(false, false)); io_thread_->message_loop()->PostTask( FROM_HERE, base::Bind(&AppCacheResponseTest::MethodWrapper<Method>, base::Unretained(this), method)); test_finished_event_->Wait(); } void SetUpTest() { DCHECK(base::MessageLoop::current() == io_thread_->message_loop()); DCHECK(task_stack_.empty()); storage_delegate_.reset(new MockStorageDelegate(this)); service_.reset(new MockAppCacheService()); expected_read_result_ = 0; expected_write_result_ = 0; written_response_id_ = 0; should_delete_reader_in_completion_callback_ = false; should_delete_writer_in_completion_callback_ = false; reader_deletion_count_down_ = 0; writer_deletion_count_down_ = 0; read_callback_was_called_ = false; write_callback_was_called_ = false; } void TearDownTest() { DCHECK(base::MessageLoop::current() == io_thread_->message_loop()); while (!task_stack_.empty()) task_stack_.pop(); reader_.reset(); read_buffer_ = NULL; read_info_buffer_ = NULL; writer_.reset(); write_buffer_ = NULL; write_info_buffer_ = NULL; storage_delegate_.reset(); service_.reset(); } void TestFinished() { // We unwind the stack prior to finishing up to let stack // based objects get deleted. DCHECK(base::MessageLoop::current() == io_thread_->message_loop()); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(&AppCacheResponseTest::TestFinishedUnwound, base::Unretained(this))); } void TestFinishedUnwound() { TearDownTest(); test_finished_event_->Signal(); } void PushNextTask(const base::Closure& task) { task_stack_.push(std::pair<base::Closure, bool>(task, false)); } void PushNextTaskAsImmediate(const base::Closure& task) { task_stack_.push(std::pair<base::Closure, bool>(task, true)); } void ScheduleNextTask() { DCHECK(base::MessageLoop::current() == io_thread_->message_loop()); if (task_stack_.empty()) { TestFinished(); return; } base::Closure task = task_stack_.top().first; bool immediate = task_stack_.top().second; task_stack_.pop(); if (immediate) task.Run(); else base::MessageLoop::current()->PostTask(FROM_HERE, task); } // Wrappers to call AppCacheResponseReader/Writer Read and Write methods void WriteBasicResponse() { static const char kHttpHeaders[] = "HTTP/1.0 200 OK\0Content-Length: 5\0\0"; static const char* kHttpBody = "Hello"; scoped_refptr<IOBuffer> body(new WrappedIOBuffer(kHttpBody)); std::string raw_headers(kHttpHeaders, arraysize(kHttpHeaders)); WriteResponse( MakeHttpResponseInfo(raw_headers), body.get(), strlen(kHttpBody)); } int basic_response_size() { return 5; } // should match kHttpBody above void WriteResponse(net::HttpResponseInfo* head, IOBuffer* body, int body_len) { DCHECK(body); scoped_refptr<IOBuffer> body_ref(body); PushNextTask(base::Bind(&AppCacheResponseTest::WriteResponseBody, base::Unretained(this), body_ref, body_len)); WriteResponseHead(head); } void WriteResponseHead(net::HttpResponseInfo* head) { EXPECT_FALSE(writer_->IsWritePending()); expected_write_result_ = GetHttpResponseInfoSize(head); write_info_buffer_ = new HttpResponseInfoIOBuffer(head); writer_->WriteInfo(write_info_buffer_.get(), base::Bind(&AppCacheResponseTest::OnWriteInfoComplete, base::Unretained(this))); } void WriteResponseBody(scoped_refptr<IOBuffer> io_buffer, int buf_len) { EXPECT_FALSE(writer_->IsWritePending()); write_buffer_ = io_buffer; expected_write_result_ = buf_len; writer_->WriteData(write_buffer_.get(), buf_len, base::Bind(&AppCacheResponseTest::OnWriteComplete, base::Unretained(this))); } void ReadResponseBody(scoped_refptr<IOBuffer> io_buffer, int buf_len) { EXPECT_FALSE(reader_->IsReadPending()); read_buffer_ = io_buffer; expected_read_result_ = buf_len; reader_->ReadData(read_buffer_.get(), buf_len, base::Bind(&AppCacheResponseTest::OnReadComplete, base::Unretained(this))); } // AppCacheResponseReader / Writer completion callbacks void OnWriteInfoComplete(int result) { EXPECT_FALSE(writer_->IsWritePending()); EXPECT_EQ(expected_write_result_, result); ScheduleNextTask(); } void OnWriteComplete(int result) { EXPECT_FALSE(writer_->IsWritePending()); write_callback_was_called_ = true; EXPECT_EQ(expected_write_result_, result); if (should_delete_writer_in_completion_callback_ && --writer_deletion_count_down_ == 0) { writer_.reset(); } ScheduleNextTask(); } void OnReadInfoComplete(int result) { EXPECT_FALSE(reader_->IsReadPending()); EXPECT_EQ(expected_read_result_, result); ScheduleNextTask(); } void OnReadComplete(int result) { EXPECT_FALSE(reader_->IsReadPending()); read_callback_was_called_ = true; EXPECT_EQ(expected_read_result_, result); if (should_delete_reader_in_completion_callback_ && --reader_deletion_count_down_ == 0) { reader_.reset(); } ScheduleNextTask(); } // Helpers to work with HttpResponseInfo objects net::HttpResponseInfo* MakeHttpResponseInfo(const std::string& raw_headers) { net::HttpResponseInfo* info = new net::HttpResponseInfo; info->request_time = base::Time::Now(); info->response_time = base::Time::Now(); info->was_cached = false; info->headers = new net::HttpResponseHeaders(raw_headers); return info; } int GetHttpResponseInfoSize(const net::HttpResponseInfo* info) { Pickle pickle; return PickleHttpResonseInfo(&pickle, info); } bool CompareHttpResponseInfos(const net::HttpResponseInfo* info1, const net::HttpResponseInfo* info2) { Pickle pickle1; Pickle pickle2; PickleHttpResonseInfo(&pickle1, info1); PickleHttpResonseInfo(&pickle2, info2); return (pickle1.size() == pickle2.size()) && (0 == memcmp(pickle1.data(), pickle2.data(), pickle1.size())); } int PickleHttpResonseInfo(Pickle* pickle, const net::HttpResponseInfo* info) { const bool kSkipTransientHeaders = true; const bool kTruncated = false; info->Persist(pickle, kSkipTransientHeaders, kTruncated); return pickle->size(); } // Helpers to fill and verify blocks of memory with a value void FillData(char value, char* data, int data_len) { memset(data, value, data_len); } bool CheckData(char value, const char* data, int data_len) { for (int i = 0; i < data_len; ++i, ++data) { if (*data != value) return false; } return true; } // Individual Tests --------------------------------------------------------- // Most of the individual tests involve multiple async steps. Each test // is delineated with a section header. // ReadNonExistentResponse ------------------------------------------- void ReadNonExistentResponse() { // 1. Attempt to ReadInfo // 2. Attempt to ReadData reader_.reset(service_->storage()->CreateResponseReader( GURL(), 0, kNoSuchResponseId)); // Push tasks in reverse order PushNextTask(base::Bind(&AppCacheResponseTest::ReadNonExistentData, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::ReadNonExistentInfo, base::Unretained(this))); ScheduleNextTask(); } void ReadNonExistentInfo() { EXPECT_FALSE(reader_->IsReadPending()); read_info_buffer_ = new HttpResponseInfoIOBuffer(); reader_->ReadInfo(read_info_buffer_.get(), base::Bind(&AppCacheResponseTest::OnReadInfoComplete, base::Unretained(this))); EXPECT_TRUE(reader_->IsReadPending()); expected_read_result_ = net::ERR_CACHE_MISS; } void ReadNonExistentData() { EXPECT_FALSE(reader_->IsReadPending()); read_buffer_ = new IOBuffer(kBlockSize); reader_->ReadData(read_buffer_.get(), kBlockSize, base::Bind(&AppCacheResponseTest::OnReadComplete, base::Unretained(this))); EXPECT_TRUE(reader_->IsReadPending()); expected_read_result_ = net::ERR_CACHE_MISS; } // LoadResponseInfo_Miss ---------------------------------------------------- void LoadResponseInfo_Miss() { PushNextTask(base::Bind(&AppCacheResponseTest::LoadResponseInfo_Miss_Verify, base::Unretained(this))); service_->storage()->LoadResponseInfo(GURL(), 0, kNoSuchResponseId, storage_delegate_.get()); } void LoadResponseInfo_Miss_Verify() { EXPECT_EQ(kNoSuchResponseId, storage_delegate_->loaded_info_id_); EXPECT_TRUE(!storage_delegate_->loaded_info_.get()); TestFinished(); } // LoadResponseInfo_Hit ---------------------------------------------------- void LoadResponseInfo_Hit() { // This tests involves multiple async steps. // 1. Write a response headers and body to storage // a. headers // b. body // 2. Use LoadResponseInfo to read the response headers back out PushNextTask(base::Bind(&AppCacheResponseTest::LoadResponseInfo_Hit_Step2, base::Unretained(this))); writer_.reset(service_->storage()->CreateResponseWriter(GURL(), 0)); written_response_id_ = writer_->response_id(); WriteBasicResponse(); } void LoadResponseInfo_Hit_Step2() { writer_.reset(); PushNextTask(base::Bind(&AppCacheResponseTest::LoadResponseInfo_Hit_Verify, base::Unretained(this))); service_->storage()->LoadResponseInfo(GURL(), 0, written_response_id_, storage_delegate_.get()); } void LoadResponseInfo_Hit_Verify() { EXPECT_EQ(written_response_id_, storage_delegate_->loaded_info_id_); EXPECT_TRUE(storage_delegate_->loaded_info_.get()); EXPECT_TRUE(CompareHttpResponseInfos( write_info_buffer_->http_info.get(), storage_delegate_->loaded_info_->http_response_info())); EXPECT_EQ(basic_response_size(), storage_delegate_->loaded_info_->response_data_size()); TestFinished(); } // AmountWritten ---------------------------------------------------- void AmountWritten() { static const char kHttpHeaders[] = "HTTP/1.0 200 OK\0\0"; std::string raw_headers(kHttpHeaders, arraysize(kHttpHeaders)); net::HttpResponseInfo* head = MakeHttpResponseInfo(raw_headers); int expected_amount_written = GetHttpResponseInfoSize(head) + kNumBlocks * kBlockSize; // Push tasks in reverse order. PushNextTask(base::Bind(&AppCacheResponseTest::Verify_AmountWritten, base::Unretained(this), expected_amount_written)); for (int i = 0; i < kNumBlocks; ++i) { PushNextTask(base::Bind(&AppCacheResponseTest::WriteOneBlock, base::Unretained(this), kNumBlocks - i)); } PushNextTask(base::Bind(&AppCacheResponseTest::WriteResponseHead, base::Unretained(this), head)); writer_.reset(service_->storage()->CreateResponseWriter(GURL(), 0)); written_response_id_ = writer_->response_id(); ScheduleNextTask(); } void Verify_AmountWritten(int expected_amount_written) { EXPECT_EQ(expected_amount_written, writer_->amount_written()); TestFinished(); } // WriteThenVariouslyReadResponse ------------------------------------------- void WriteThenVariouslyReadResponse() { // This tests involves multiple async steps. // 1. First, write a large body using multiple writes, we don't bother // with a response head for this test. // 2. Read the entire body, using multiple reads // 3. Read the entire body, using one read. // 4. Attempt to read beyond the EOF. // 5. Read just a range. // 6. Attempt to read beyond EOF of a range. // Push tasks in reverse order PushNextTask(base::Bind(&AppCacheResponseTest::ReadRangeFullyBeyondEOF, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::ReadRangePartiallyBeyondEOF, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::ReadPastEOF, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::ReadRange, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::ReadPastEOF, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::ReadAllAtOnce, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::ReadInBlocks, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::WriteOutBlocks, base::Unretained(this))); // Get them going. ScheduleNextTask(); } void WriteOutBlocks() { writer_.reset(service_->storage()->CreateResponseWriter(GURL(), 0)); written_response_id_ = writer_->response_id(); for (int i = 0; i < kNumBlocks; ++i) { PushNextTask(base::Bind(&AppCacheResponseTest::WriteOneBlock, base::Unretained(this), kNumBlocks - i)); } ScheduleNextTask(); } void WriteOneBlock(int block_number) { scoped_refptr<IOBuffer> io_buffer( new IOBuffer(kBlockSize)); FillData(block_number, io_buffer->data(), kBlockSize); WriteResponseBody(io_buffer, kBlockSize); } void ReadInBlocks() { writer_.reset(); reader_.reset(service_->storage()->CreateResponseReader( GURL(), 0, written_response_id_)); for (int i = 0; i < kNumBlocks; ++i) { PushNextTask(base::Bind(&AppCacheResponseTest::ReadOneBlock, base::Unretained(this), kNumBlocks - i)); } ScheduleNextTask(); } void ReadOneBlock(int block_number) { PushNextTask(base::Bind(&AppCacheResponseTest::VerifyOneBlock, base::Unretained(this), block_number)); ReadResponseBody(new IOBuffer(kBlockSize), kBlockSize); } void VerifyOneBlock(int block_number) { EXPECT_TRUE(CheckData(block_number, read_buffer_->data(), kBlockSize)); ScheduleNextTask(); } void ReadAllAtOnce() { PushNextTask(base::Bind(&AppCacheResponseTest::VerifyAllAtOnce, base::Unretained(this))); reader_.reset(service_->storage()->CreateResponseReader( GURL(), 0, written_response_id_)); int big_size = kNumBlocks * kBlockSize; ReadResponseBody(new IOBuffer(big_size), big_size); } void VerifyAllAtOnce() { char* p = read_buffer_->data(); for (int i = 0; i < kNumBlocks; ++i, p += kBlockSize) EXPECT_TRUE(CheckData(i + 1, p, kBlockSize)); ScheduleNextTask(); } void ReadPastEOF() { EXPECT_FALSE(reader_->IsReadPending()); read_buffer_ = new IOBuffer(kBlockSize); expected_read_result_ = 0; reader_->ReadData(read_buffer_.get(), kBlockSize, base::Bind(&AppCacheResponseTest::OnReadComplete, base::Unretained(this))); } void ReadRange() { PushNextTask(base::Bind(&AppCacheResponseTest::VerifyRange, base::Unretained(this))); reader_.reset(service_->storage()->CreateResponseReader( GURL(), 0, written_response_id_)); reader_->SetReadRange(kBlockSize, kBlockSize); ReadResponseBody(new IOBuffer(kBlockSize), kBlockSize); } void VerifyRange() { EXPECT_TRUE(CheckData(2, read_buffer_->data(), kBlockSize)); ScheduleNextTask(); // ReadPastEOF is scheduled next } void ReadRangePartiallyBeyondEOF() { PushNextTask(base::Bind(&AppCacheResponseTest::VerifyRangeBeyondEOF, base::Unretained(this))); reader_.reset(service_->storage()->CreateResponseReader( GURL(), 0, written_response_id_)); reader_->SetReadRange(kBlockSize, kNumBlocks * kBlockSize); ReadResponseBody(new IOBuffer(kNumBlocks * kBlockSize), kNumBlocks * kBlockSize); expected_read_result_ = (kNumBlocks - 1) * kBlockSize; } void VerifyRangeBeyondEOF() { // Just verify the first 1k VerifyRange(); } void ReadRangeFullyBeyondEOF() { reader_.reset(service_->storage()->CreateResponseReader( GURL(), 0, written_response_id_)); reader_->SetReadRange((kNumBlocks * kBlockSize) + 1, kBlockSize); ReadResponseBody(new IOBuffer(kBlockSize), kBlockSize); expected_read_result_ = 0; } // IOChaining ------------------------------------------- void IOChaining() { // 1. Write several blocks out initiating the subsequent write // from within the completion callback of the previous write. // 2. Read and verify several blocks in similarly chaining reads. // Push tasks in reverse order PushNextTaskAsImmediate( base::Bind(&AppCacheResponseTest::ReadInBlocksImmediately, base::Unretained(this))); PushNextTaskAsImmediate( base::Bind(&AppCacheResponseTest::WriteOutBlocksImmediately, base::Unretained(this))); // Get them going. ScheduleNextTask(); } void WriteOutBlocksImmediately() { writer_.reset(service_->storage()->CreateResponseWriter(GURL(), 0)); written_response_id_ = writer_->response_id(); for (int i = 0; i < kNumBlocks; ++i) { PushNextTaskAsImmediate( base::Bind(&AppCacheResponseTest::WriteOneBlock, base::Unretained(this), kNumBlocks - i)); } ScheduleNextTask(); } void ReadInBlocksImmediately() { writer_.reset(); reader_.reset(service_->storage()->CreateResponseReader( GURL(), 0, written_response_id_)); for (int i = 0; i < kNumBlocks; ++i) { PushNextTaskAsImmediate( base::Bind(&AppCacheResponseTest::ReadOneBlockImmediately, base::Unretained(this), kNumBlocks - i)); } ScheduleNextTask(); } void ReadOneBlockImmediately(int block_number) { PushNextTaskAsImmediate(base::Bind(&AppCacheResponseTest::VerifyOneBlock, base::Unretained(this), block_number)); ReadResponseBody(new IOBuffer(kBlockSize), kBlockSize); } // DeleteWithinCallbacks ------------------------------------------- void DeleteWithinCallbacks() { // 1. Write out a few blocks normally, and upon // completion of the last write, delete the writer. // 2. Read in a few blocks normally, and upon completion // of the last read, delete the reader. should_delete_reader_in_completion_callback_ = true; reader_deletion_count_down_ = kNumBlocks; should_delete_writer_in_completion_callback_ = true; writer_deletion_count_down_ = kNumBlocks; PushNextTask(base::Bind(&AppCacheResponseTest::ReadInBlocks, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::WriteOutBlocks, base::Unretained(this))); ScheduleNextTask(); } // DeleteWithIOPending ------------------------------------------- void DeleteWithIOPending() { // 1. Write a few blocks normally. // 2. Start a write, delete with it pending. // 3. Start a read, delete with it pending. PushNextTask(base::Bind(&AppCacheResponseTest::ReadThenDelete, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::WriteThenDelete, base::Unretained(this))); PushNextTask(base::Bind(&AppCacheResponseTest::WriteOutBlocks, base::Unretained(this))); ScheduleNextTask(); } void WriteThenDelete() { write_callback_was_called_ = false; WriteOneBlock(5); EXPECT_TRUE(writer_->IsWritePending()); writer_.reset(); ScheduleNextTask(); } void ReadThenDelete() { read_callback_was_called_ = false; reader_.reset(service_->storage()->CreateResponseReader( GURL(), 0, written_response_id_)); ReadResponseBody(new IOBuffer(kBlockSize), kBlockSize); EXPECT_TRUE(reader_->IsReadPending()); reader_.reset(); // Wait a moment to verify no callbacks. base::MessageLoop::current()->PostDelayedTask( FROM_HERE, base::Bind(&AppCacheResponseTest::VerifyNoCallbacks, base::Unretained(this)), base::TimeDelta::FromMilliseconds(10)); } void VerifyNoCallbacks() { EXPECT_TRUE(!write_callback_was_called_); EXPECT_TRUE(!read_callback_was_called_); TestFinished(); } // Data members scoped_ptr<base::WaitableEvent> test_finished_event_; scoped_ptr<MockStorageDelegate> storage_delegate_; scoped_ptr<MockAppCacheService> service_; std::stack<std::pair<base::Closure, bool> > task_stack_; scoped_ptr<AppCacheResponseReader> reader_; scoped_refptr<HttpResponseInfoIOBuffer> read_info_buffer_; scoped_refptr<IOBuffer> read_buffer_; int expected_read_result_; bool should_delete_reader_in_completion_callback_; int reader_deletion_count_down_; bool read_callback_was_called_; int64 written_response_id_; scoped_ptr<AppCacheResponseWriter> writer_; scoped_refptr<HttpResponseInfoIOBuffer> write_info_buffer_; scoped_refptr<IOBuffer> write_buffer_; int expected_write_result_; bool should_delete_writer_in_completion_callback_; int writer_deletion_count_down_; bool write_callback_was_called_; static scoped_ptr<base::Thread> io_thread_; }; // static scoped_ptr<base::Thread> AppCacheResponseTest::io_thread_; TEST_F(AppCacheResponseTest, ReadNonExistentResponse) { RunTestOnIOThread(&AppCacheResponseTest::ReadNonExistentResponse); } TEST_F(AppCacheResponseTest, LoadResponseInfo_Miss) { RunTestOnIOThread(&AppCacheResponseTest::LoadResponseInfo_Miss); } TEST_F(AppCacheResponseTest, LoadResponseInfo_Hit) { RunTestOnIOThread(&AppCacheResponseTest::LoadResponseInfo_Hit); } TEST_F(AppCacheResponseTest, AmountWritten) { RunTestOnIOThread(&AppCacheResponseTest::AmountWritten); } TEST_F(AppCacheResponseTest, WriteThenVariouslyReadResponse) { RunTestOnIOThread(&AppCacheResponseTest::WriteThenVariouslyReadResponse); } TEST_F(AppCacheResponseTest, IOChaining) { RunTestOnIOThread(&AppCacheResponseTest::IOChaining); } TEST_F(AppCacheResponseTest, DeleteWithinCallbacks) { RunTestOnIOThread(&AppCacheResponseTest::DeleteWithinCallbacks); } TEST_F(AppCacheResponseTest, DeleteWithIOPending) { RunTestOnIOThread(&AppCacheResponseTest::DeleteWithIOPending); } } // namespace appcache