blob: f6b102152fc80c7fc9f8544bd8306bb1b1fd8fec [file] [log] [blame]
/*
* Copyright 2011 Google Inc.
*
* 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.
*/
// Author: mmohabey@google.com (Megha Mohabey)
#include "pagespeed/automatic/proxy_interface_test_base.h"
#include <cstddef>
#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/http/public/mock_callback.h"
#include "net/instaweb/http/public/mock_url_fetcher.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/rewriter/public/mock_critical_images_finder.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/util/public/cache_property_store.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/automatic/proxy_fetch.h"
#include "pagespeed/automatic/proxy_interface.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/cache/delay_cache.h"
#include "pagespeed/kernel/cache/lru_cache.h"
#include "pagespeed/kernel/html/html_element.h"
#include "pagespeed/kernel/html/html_node.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/http/user_agent_matcher.h"
#include "pagespeed/kernel/thread/mock_scheduler.h"
#include "pagespeed/kernel/thread/queued_worker_pool.h"
#include "pagespeed/kernel/thread/thread_synchronizer.h"
#include "pagespeed/kernel/thread/worker_test_base.h"
namespace net_instaweb {
namespace {
// Like ExpectStringAsyncFetch but for asynchronous invocation -- it lets
// one specify a WorkerTestBase::SyncPoint to help block until completion.
class AsyncExpectStringAsyncFetch : public ExpectStringAsyncFetch {
public:
AsyncExpectStringAsyncFetch(bool expect_success,
bool log_flush,
GoogleString* buffer,
ResponseHeaders* response_headers,
bool* done_value,
WorkerTestBase::SyncPoint* notify,
ThreadSynchronizer* sync,
const RequestContextPtr& request_context)
: ExpectStringAsyncFetch(expect_success, request_context),
buffer_(buffer),
done_value_(done_value),
notify_(notify),
sync_(sync),
log_flush_(log_flush) {
buffer->clear();
response_headers->Clear();
*done_value = false;
set_response_headers(response_headers);
}
virtual ~AsyncExpectStringAsyncFetch() {}
virtual void HandleHeadersComplete() {
// Make sure we have cleaned the headers in ProxyInterface.
EXPECT_FALSE(
request_headers()->Has(HttpAttributes::kAcceptEncoding));
sync_->Wait(ProxyFetch::kHeadersSetupRaceWait);
response_headers()->Add("HeadersComplete", "1"); // Dirties caching info.
sync_->Signal(ProxyFetch::kHeadersSetupRaceFlush);
}
virtual void HandleDone(bool success) {
*buffer_ = buffer();
*done_value_ = success;
ExpectStringAsyncFetch::HandleDone(success);
WorkerTestBase::SyncPoint* notify = notify_;
delete this;
notify->Notify();
}
virtual bool HandleFlush(MessageHandler* handler) {
if (log_flush_) {
HandleWrite("|Flush|", handler);
}
return true;
}
private:
GoogleString* buffer_;
bool* done_value_;
WorkerTestBase::SyncPoint* notify_;
ThreadSynchronizer* sync_;
bool log_flush_;
DISALLOW_COPY_AND_ASSIGN(AsyncExpectStringAsyncFetch);
};
} // namespace
// ProxyUrlNamer.
const char ProxyUrlNamer::kProxyHost[] = "proxy_host.com";
bool ProxyUrlNamer::Decode(const GoogleUrl& gurl,
const RewriteOptions* rewrite_options,
GoogleUrl* domain,
GoogleString* decoded) const {
if (gurl.Host() != kProxyHost) {
return false;
}
StringPieceVector path_vector;
SplitStringPieceToVector(gurl.PathAndLeaf(), "/", &path_vector, false);
if (path_vector.size() < 3) {
return false;
}
if (domain != NULL) {
domain->Reset(StrCat("http://", path_vector[1]));
}
// [0] is "" because PathAndLeaf returns a string with a leading slash
*decoded = StrCat(gurl.Scheme(), ":/");
for (size_t i = 2, n = path_vector.size(); i < n; ++i) {
StrAppend(decoded, "/", path_vector[i]);
}
return true;
}
// MockFilter.
void MockFilter::StartDocument() {
num_elements_ = 0;
PropertyCache* page_cache =
driver_->server_context()->page_property_cache();
const PropertyCache::Cohort* cohort =
page_cache->GetCohort(RewriteDriver::kDomCohort);
PropertyPage* page = driver_->property_page();
if (page != NULL) {
num_elements_property_ = page->GetProperty(cohort, "num_elements");
} else {
num_elements_property_ = NULL;
}
}
void MockFilter::StartElement(HtmlElement* element) {
if (num_elements_ == 0) {
// Before the start of the first element, print out the number
// of elements that we expect based on the cache.
GoogleString comment = " ";
PropertyCache* page_cache =
driver_->server_context()->page_property_cache();
if ((num_elements_property_ != NULL) &&
num_elements_property_->has_value()) {
StrAppend(&comment, num_elements_property_->value(),
" elements ",
page_cache->IsStable(num_elements_property_)
? "stable " : "unstable ");
}
HtmlNode* node = driver_->NewCommentNode(element->parent(), comment);
driver_->InsertNodeBeforeCurrent(node);
}
++num_elements_;
}
void MockFilter::EndDocument() {
// We query IsBrowserCacheable for the HTML file only to ensure that
// the test will crash if ComputeCaching() was never called.
//
// All these HTML responses are Cache-Control: private.
EXPECT_TRUE(driver_->response_headers()->IsBrowserCacheable());
PropertyPage* page = driver_->property_page();
if (page != NULL) {
page->UpdateValue(
driver_->server_context()->dom_cohort(),
"num_elements",
IntegerToString(num_elements_));
num_elements_property_ = NULL;
}
}
// ProxyInterfaceTestBase.
ProxyInterfaceTestBase::ProxyInterfaceTestBase()
: callback_done_value_(false),
mock_critical_images_finder_(
new MockCriticalImagesFinder(statistics())) {}
void ProxyInterfaceTestBase::TestHeadersSetupRace() {
mock_url_fetcher()->SetResponseFailure(AbsolutifyUrl(kPageUrl));
TestPropertyCache(kPageUrl, true, true, false);
}
void ProxyInterfaceTestBase::SetUp() {
RewriteTestBase::SetUp();
ThreadSynchronizer* sync = server_context()->thread_synchronizer();
sync->EnableForPrefix(ProxyFetch::kCollectorFinish);
sync->AllowSloppyTermination(ProxyFetch::kCollectorFinish);
ProxyInterface::InitStats(statistics());
proxy_interface_.reset(
new ProxyInterface("localhost", 80, server_context(), statistics()));
server_context()->set_critical_images_finder(
mock_critical_images_finder_);
}
void ProxyInterfaceTestBase::TearDown() {
// Make sure all the jobs are over before we check for leaks ---
// someone might still be trying to clean themselves up.
mock_scheduler()->AwaitQuiescence();
EXPECT_EQ(0, server_context()->num_active_rewrite_drivers());
RewriteTestBase::TearDown();
}
void ProxyInterfaceTestBase::SetCriticalImagesInFinder(
StringSet* critical_images) {
mock_critical_images_finder_->set_critical_images(critical_images);
}
void ProxyInterfaceTestBase::SetCssCriticalImagesInFinder(
StringSet* css_critical_images) {
mock_critical_images_finder_->set_css_critical_images(css_critical_images);
}
void ProxyInterfaceTestBase::FetchFromProxy(
const StringPiece& url,
const RequestHeaders& request_headers,
bool expect_success,
GoogleString* string_out,
ResponseHeaders* headers_out,
bool proxy_fetch_property_callback_collector_created) {
FetchFromProxyNoWait(url, request_headers, expect_success,
false /* log_flush*/, headers_out);
WaitForFetch(proxy_fetch_property_callback_collector_created);
*string_out = callback_buffer_;
}
// Initiates a fetch using the proxy interface, and waits for it to
// complete.
void ProxyInterfaceTestBase::FetchFromProxy(
const StringPiece& url,
const RequestHeaders& request_headers,
bool expect_success,
GoogleString* string_out,
ResponseHeaders* headers_out) {
FetchFromProxy(url, request_headers, expect_success, string_out,
headers_out, true);
}
// TODO(jmarantz): eliminate this interface as it's annoying to have
// the function overload just to save an empty RequestHeaders arg.
void ProxyInterfaceTestBase::FetchFromProxy(const StringPiece& url,
bool expect_success,
GoogleString* string_out,
ResponseHeaders* headers_out) {
RequestHeaders request_headers;
FetchFromProxy(url, request_headers, expect_success, string_out,
headers_out);
}
void ProxyInterfaceTestBase::FetchFromProxyLoggingFlushes(
const StringPiece& url, bool expect_success, GoogleString* string_out) {
RequestHeaders request_headers;
ResponseHeaders response_headers;
FetchFromProxyNoWait(url, request_headers, expect_success,
true /* log_flush*/, &response_headers);
WaitForFetch(true);
*string_out = callback_buffer_;
}
// Initiates a fetch using the proxy interface, without waiting for it to
// complete. The usage model here is to delay callbacks and/or fetches
// to control their order of delivery, then call WaitForFetch.
void ProxyInterfaceTestBase::FetchFromProxyNoWait(
const StringPiece& url,
const RequestHeaders& request_headers,
bool expect_success,
bool log_flush,
ResponseHeaders* headers_out) {
sync_.reset(new WorkerTestBase::SyncPoint(
server_context()->thread_system()));
AsyncFetch* fetch = new AsyncExpectStringAsyncFetch(
expect_success, log_flush, &callback_buffer_,
&callback_response_headers_, &callback_done_value_, sync_.get(),
server_context()->thread_synchronizer(),
rewrite_driver()->request_context());
fetch->set_response_headers(headers_out);
fetch->request_headers()->CopyFrom(request_headers);
proxy_interface_->Fetch(AbsolutifyUrl(url), message_handler(), fetch);
}
// This must be called after FetchFromProxyNoWait, once all of the required
// resources (fetches, cache lookups) have been released.
void ProxyInterfaceTestBase::WaitForFetch(
bool proxy_fetch_property_callback_collector_created) {
sync_->Wait();
mock_scheduler()->AwaitQuiescence();
if (proxy_fetch_property_callback_collector_created) {
ThreadSynchronizer* thread_synchronizer =
server_context()->thread_synchronizer();
thread_synchronizer->Wait(ProxyFetch::kCollectorFinish);
}
}
// Tests a single flow through the property-cache, optionally delaying or
// threading property-cache lookups, and using the ThreadSynchronizer to
// tease out race conditions.
//
// delay_pcache indicates that we will suspend the PropertyCache lookup
// until after the FetchFromProxy call. This is used in the
// PropCacheNoWritesIfNonHtmlDelayedCache below, which tests the flow where
// we have already detached the ProxyFetchPropertyCallbackCollector before
// Done() is called.
//
// thread_pcache forces the property-cache to issue the lookup
// callback in a different thread. This lets us reproduce a
// potential race conditions where a context switch in
// ProxyFetchPropertyCallbackCollector::Done() would lead to a
// double-deletion of the collector object.
void ProxyInterfaceTestBase::TestPropertyCache(const StringPiece& url,
bool delay_pcache,
bool thread_pcache,
bool expect_success) {
RequestHeaders request_headers;
ResponseHeaders response_headers;
GoogleString output;
TestPropertyCacheWithHeadersAndOutput(
url, delay_pcache, thread_pcache, expect_success, true, true, false,
request_headers, &response_headers, &output);
}
void ProxyInterfaceTestBase::TestPropertyCacheWithHeadersAndOutput(
const StringPiece& url, bool delay_pcache, bool thread_pcache,
bool expect_success, bool check_stats, bool add_create_filter_callback,
bool expect_detach_before_pcache, const RequestHeaders& request_headers,
ResponseHeaders* response_headers, GoogleString* output) {
scoped_ptr<QueuedWorkerPool> pool;
QueuedWorkerPool::Sequence* sequence = NULL;
GoogleString delay_pcache_key, delay_http_cache_key;
if (delay_pcache || thread_pcache) {
PropertyCache* pcache = page_property_cache();
const PropertyCache::Cohort* cohort =
pcache->GetCohort(RewriteDriver::kDomCohort);
delay_http_cache_key = AbsolutifyUrl(url);
delay_pcache_key = factory()->cache_property_store()->CacheKey(
delay_http_cache_key,
"",
UserAgentMatcher::DeviceTypeSuffix(UserAgentMatcher::kDesktop),
cohort);
delay_cache()->DelayKey(delay_pcache_key);
if (thread_pcache) {
delay_cache()->DelayKey(delay_http_cache_key);
pool.reset(new QueuedWorkerPool(
1, "pcache", server_context()->thread_system()));
sequence = pool->NewSequence();
}
}
CreateFilterCallback create_filter_callback;
if (add_create_filter_callback) {
factory()->AddCreateFilterCallback(&create_filter_callback);
}
if (thread_pcache) {
FetchFromProxyNoWait(url, request_headers, expect_success,
false /* don't log flushes*/, response_headers);
delay_cache()->ReleaseKeyInSequence(delay_pcache_key, sequence);
// Now release the HTTPCache lookup, which allows the mock-fetch
// to stream the bytes in the ProxyFetch and call HandleDone().
// Note that we release this key in mainline, so that call
// sequence happens directly from ReleaseKey.
delay_cache()->ReleaseKey(delay_http_cache_key);
WaitForFetch(true);
*output = callback_buffer_;
pool->ShutDown();
} else {
FetchFromProxyNoWait(url, request_headers, expect_success, false,
response_headers);
if (expect_detach_before_pcache) {
WaitForFetch(false);
}
if (delay_pcache) {
delay_cache()->ReleaseKey(delay_pcache_key);
}
if (!expect_detach_before_pcache) {
WaitForFetch(false);
}
ThreadSynchronizer* thread_synchronizer =
server_context()->thread_synchronizer();
thread_synchronizer->Wait(ProxyFetch::kCollectorFinish);
*output = callback_buffer_;
}
if (check_stats) {
EXPECT_EQ(1, lru_cache()->num_inserts()); // http-cache
// We expect 2 misses. 1 for http-cache and 1 for prop-cache which
// correspond to each different device type in
// UserAgentMatcher::DeviceType.
EXPECT_EQ(2, lru_cache()->num_misses());
}
}
} // namespace net_instaweb