blob: be054182a90bf6113ea3b95a5632c88e64aa9e73 [file] [log] [blame]
/*
* Copyright 2013 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)
// pulkitg@google.com (Pulkit Goyal)
// Unit-tests for CacheHtmlFlow.
#include "pagespeed/automatic/proxy_interface_test_base.h"
#include "base/logging.h"
#include "net/instaweb/http/public/log_record.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/mock_callback.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/public/global_constants.h"
#include "net/instaweb/rewriter/public/blink_util.h"
#include "net/instaweb/rewriter/public/cache_html_info_finder.h"
#include "net/instaweb/rewriter/public/critical_css_filter.h"
#include "net/instaweb/rewriter/public/critical_selector_finder.h"
#include "net/instaweb/rewriter/public/delay_images_filter.h"
#include "net/instaweb/rewriter/public/flush_early_info_finder_test_base.h"
#include "net/instaweb/rewriter/public/js_disable_filter.h"
#include "net/instaweb/rewriter/public/mock_critical_css_finder.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/static_asset_manager.h"
#include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/rewriter/public/url_namer.h"
#include "net/instaweb/util/public/cache_property_store.h"
#include "net/instaweb/util/public/mock_property_page.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/automatic/cache_html_flow.h"
#include "pagespeed/automatic/proxy_fetch.h"
#include "pagespeed/automatic/proxy_interface.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/dynamic_annotations.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_hasher.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/null_message_handler.h"
#include "pagespeed/kernel/base/ref_counted_ptr.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/base/time_util.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/base/wildcard.h"
#include "pagespeed/kernel/cache/delay_cache.h"
#include "pagespeed/kernel/cache/lru_cache.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/http_options.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/http/user_agent_matcher_test_base.h"
#include "pagespeed/kernel/thread/mock_scheduler.h"
#include "pagespeed/kernel/thread/thread_synchronizer.h"
#include "pagespeed/kernel/thread/worker_test_base.h"
namespace net_instaweb {
class AbstractMutex;
class AsyncFetch;
class Statistics;
namespace {
const char kTestUrl[] = "http://test.com/text.html";
const char kMockHashValue[] = "MDAwMD";
const char kCssContent[] = "* { display: none; }";
const char kSampleJpgFile[] = "Sample.jpg";
const char kChromeLinuxUserAgent[] =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.46 Safari/536.5";
const char kWindowsUserAgent[] =
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 "
"Firefox/15.0a1";
const char kBlackListUserAgent[] =
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 Firefox/2.0a1";
const char kWhitespace[] = " ";
const char kHtmlInput[] =
"<html>"
"<head>"
"</head>"
"<body>\n"
"<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<h2 id=\"beforeItems\"> This is before Items </h2>"
"<div class=\"item\">"
"<img src=\"image1\">"
"<img src=\"image2\">"
"</div>"
"<div class=\"item\">"
"<img src=\"image3\">"
"<div class=\"item\">"
"<img src=\"image4\">"
"</div>"
"</div>"
"</div>"
"</body></html>";
const char kHtmlInputWithMinifiableJs[] =
"<html>"
"<head>"
"<script type=\"text/javascript\">var a = \"hello\"; </script>"
"</head>"
"<body>\n"
"<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<h2 id=\"beforeItems\"> This is before Items </h2>"
"<div class=\"item\">"
"<img src=\"image1\">"
"<img src=\"image2\">"
"</div>"
"<div class=\"item\">"
"<img src=\"image3\">"
"<div class=\"item\">"
"<img src=\"image4\">"
"</div>"
"</div>"
"</div>"
"</body></html>";
const char kHtmlInputWithMinifiedJs[] =
"<html>"
"<head>"
"<script data-pagespeed-orig-type=\"text/javascript\" "
"type=\"text/psajs\" orig_index=\"0\">var a=\"hello\";</script>"
"</head>"
"<body>\n"
"<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<h2 id=\"beforeItems\"> This is before Items </h2>"
"<div class=\"item\">"
"<img src=\"image1\">"
"<img src=\"image2\">"
"</div>"
"<div class=\"item\">"
"<img src=\"image3\">"
"<div class=\"item\">"
"<img src=\"image4\">"
"</div>"
"</div>"
"</div>"
"%s<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\"></script>"
"</body></html>";
const char kHtmlInputWithExtraCommentAndNonCacheable[] =
"<html>"
"<head>"
"</head>"
"<body>\n"
"<!-- Hello -->"
"<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<h2 id=\"beforeItems\"> This is before Items </h2>"
"<div class=\"item\">"
"<img src=\"image1\">"
"<img src=\"image2\">"
"</div>"
"<div class=\"item\">"
"<img src=\"image3\">"
"<div class=\"item\">"
"<img src=\"image4\">"
"</div>"
"</div>"
"</div>"
"</body></html>";
const char kHtmlInputWithExtraAttribute[] =
"<html>"
"<head>"
"</head>"
"<body>\n"
"<div id=\"header\" align=\"center\"> This is the header </div>"
"<div id=\"container\" class>"
"<h2 id=\"beforeItems\"> This is before Items </h2>"
"<div class=\"item\">"
"<img src=\"image1\">"
"<img src=\"image2\">"
"</div>"
"<div class=\"item\">"
"<img src=\"image3\">"
"<div class=\"item\">"
"<img src=\"image4\">"
"</div>"
"</div>"
"</div>"
"</body></html>";
const char kHtmlInputWithEmptyVisiblePortions[] =
"<html><body></body></html>";
const char kSmallHtmlInput[] =
"<html><head></head><body>A small test html.</body></html>";
const char kHtmlInputForNoBlink[] =
"<html><head></head><body></body></html>";
const char kBlinkOutputCommon[] =
"<html><head></head><body>"
"<noscript><meta HTTP-EQUIV=\"refresh\" content=\"0;"
"url='%s?PageSpeed=noscript'\" />"
"<style><!--table,div,span,font,p{display:none} --></style>"
"<div style=\"display:block\">Please click "
"<a href=\"%s?PageSpeed=noscript\">here</a> "
"if you are not redirected within a few seconds.</div></noscript>"
"\n<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<!--GooglePanel begin panel-id-1.0-->"
"<!--GooglePanel end panel-id-1.0-->"
"<!--GooglePanel begin panel-id-0.0-->"
"<!--GooglePanel end panel-id-0.0-->"
"<!--GooglePanel begin panel-id-0.1-->"
"<!--GooglePanel end panel-id-0.1-->"
"</div>"
"</body></html>"
"%s<script type=\"text/javascript\" src=\"/psajs/blink.0.js\"></script>"
"<script type=\"text/javascript\">"
"pagespeed.panelLoaderInit();</script>\n"
"<script type=\"text/javascript\">"
"pagespeed.panelLoader.setRequestFromInternalIp();</script>\n";
const char kCookieScript[] =
"<script>pagespeed.panelLoader.loadCookies([\"helo=world; path=/\"]);"
"</script>";
const char kBlinkOutputSuffix[] =
"<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-1.0\":{\"instance_html\":\"<h2 id=\\\"beforeItems\\\"> This is before Items </h2>\",\"xpath\":\"//div[@id=\\\"container\\\"]/h2[1]\"}}\n);</script>" // NOLINT
"<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-0.0\":{\"instance_html\":\"<div class=\\\"item\\\"><img src=\\\"%s\\\"><img src=\\\"image2\\\"></div>\",\"xpath\":\"//div[@id=\\\"container\\\"]/div[2]\"}}\n);</script>" // NOLINT
"<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-0.1\":{\"instance_html\":\"<div class=\\\"item\\\"><img src=\\\"image3\\\"><div class=\\\"item\\\"><img src=\\\"image4\\\"></div></div>\",\"xpath\":\"//div[@id=\\\"container\\\"]/div[3]\"}}\n);</script>" // NOLINT
"<script>pagespeed.panelLoader.bufferNonCriticalData({});</script>"; // NOLINT
const char kBlinkOutputWithCacheablePanelsNoCookiesSuffix[] =
"<script>pagespeed.panelLoader.bufferNonCriticalData();</script>\n"
"</body></html>\n";
const char kBlinkOutputWithCacheablePanelsCookiesSuffix[] =
"</body></html>\n";
const char kFakePngInput[] = "FakePng";
const char kFlushSubresourcesHtmlInput[] =
"<html>"
"<head>"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"1.css\">"
"</head>"
"<body>\n"
"<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<h2 id=\"beforeItems\"> This is before Items </h2>"
"<div class=\"item\">"
"<img src=\"image1\">"
"<img src=\"image2\">"
"</div>"
"<div class=\"item\">"
"<img src=\"image3\">"
"<div class=\"item\">"
"<img src=\"image4\">"
"</div>"
"</div>"
"</body></html>";
const char kNoBlinkUrl[] =
"http://test.com/noblink_text.html?PageSpeed=noscript";
const char kNoScriptTextUrl[] = "http://test.com/text.html?PageSpeed=noscript";
// 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,
WorkerTestBase::SyncPoint* notify,
const RequestContextPtr& request_context)
: ExpectStringAsyncFetch(expect_success, request_context),
notify_(notify) {
}
virtual ~AsyncExpectStringAsyncFetch() {}
virtual void HandleDone(bool success) {
ExpectStringAsyncFetch::HandleDone(success);
notify_->Notify();
}
private:
WorkerTestBase::SyncPoint* notify_;
DISALLOW_COPY_AND_ASSIGN(AsyncExpectStringAsyncFetch);
};
class ProxyInterfaceWithDelayCache : public ProxyInterface {
public:
ProxyInterfaceWithDelayCache(const StringPiece& hostname, int port,
ServerContext* manager, Statistics* stats,
DelayCache* delay_cache,
TestRewriteDriverFactory* factory)
: ProxyInterface(hostname, port, manager, stats),
manager_(manager),
delay_cache_(delay_cache),
key_(""),
factory_(factory) {
}
// Initiates the PropertyCache look up.
virtual ProxyFetchPropertyCallbackCollector* InitiatePropertyCacheLookup(
bool is_resource_fetch,
const GoogleUrl& request_url,
RewriteOptions* options,
AsyncFetch* async_fetch,
const bool requires_blink_cohort) {
GoogleString options_signature_hash;
if (options != NULL) {
manager_->ComputeSignature(options);
options_signature_hash =
manager_->GetRewriteOptionsSignatureHash(options);
}
PropertyCache* pcache = manager_->page_property_cache();
const PropertyCache::Cohort* cohort =
pcache->GetCohort(BlinkUtil::kBlinkCohort);
key_ = factory_->cache_property_store()->CacheKey(
request_url.Spec(),
options_signature_hash,
UserAgentMatcher::DeviceTypeSuffix(UserAgentMatcher::kDesktop),
cohort);
delay_cache_->DelayKey(key_);
return ProxyFetchFactory::InitiatePropertyCacheLookup(
is_resource_fetch, request_url, server_context_, options, async_fetch,
requires_blink_cohort);
}
const GoogleString& key() const { return key_; }
private:
ServerContext* manager_;
DelayCache* delay_cache_;
GoogleString key_;
TestRewriteDriverFactory* factory_;
DISALLOW_COPY_AND_ASSIGN(ProxyInterfaceWithDelayCache);
};
} // namespace
// RequestContext that overrides NewSubordinateLogRecord to return a
// CopyOnWriteLogRecord that copies to a logging_info given at construction
// time.
class TestRequestContext : public RequestContext {
public:
TestRequestContext(ThreadSystem* threads, LoggingInfo* logging_info)
: RequestContext(kDefaultHttpOptionsForTests, threads->NewMutex(), NULL),
logging_info_copy_(logging_info) {}
virtual AbstractLogRecord* NewSubordinateLogRecord(
AbstractMutex* logging_mutex) {
return new CopyOnWriteLogRecord(logging_mutex, logging_info_copy_);
}
private:
LoggingInfo* logging_info_copy_; // Not owned by us.
DISALLOW_COPY_AND_ASSIGN(TestRequestContext);
};
typedef RefCountedPtr<TestRequestContext> TestRequestContextPtr;
// TODO(nikhilmadan): Test cookies, fetch failures, 304 responses etc.
// TODO(nikhilmadan): Test 304 responses etc.
class CacheHtmlFlowTest : public ProxyInterfaceTestBase {
protected:
static const int kHtmlCacheTimeSec = 5000;
CacheHtmlFlowTest() : test_request_context_(TestRequestContextPtr(
new TestRequestContext(server_context()->thread_system(),
&cache_html_logging_info_))) {
ConvertTimeToString(MockTimer::kApr_5_2010_ms, &start_time_string_);
}
// These must be run prior to the calls to 'new CustomRewriteDriverFactory'
// in the constructor initializer above. Thus the calls to Initialize() in
// the base class are too late.
static void SetUpTestCase() {
RewriteOptions::Initialize();
}
static void TearDownTestCase() {
RewriteOptions::Terminate();
}
void InitializeOutputs(RewriteOptions* options) {
blink_output_partial_ = StringPrintf(
kBlinkOutputCommon, kTestUrl, kTestUrl,
GetJsDisableScriptSnippet(options).c_str());
blink_output_ = StrCat(blink_output_partial_, kCookieScript,
StringPrintf(kBlinkOutputSuffix, "image1"));
noblink_output_ = StrCat("<html><head></head><body>",
StringPrintf(kNoScriptRedirectFormatter,
kNoBlinkUrl, kNoBlinkUrl),
"</body></html>");
blink_output_with_cacheable_panels_no_cookies_ =
StrCat(StringPrintf(kBlinkOutputCommon, "http://test.com/flaky.html",
"http://test.com/flaky.html",
GetJsDisableScriptSnippet(options).c_str()),
kBlinkOutputWithCacheablePanelsNoCookiesSuffix);
blink_output_with_cacheable_panels_cookies_ =
StrCat(StringPrintf(kBlinkOutputCommon, "http://test.com/cache.html",
"http://test.com/cache.html",
GetJsDisableScriptSnippet(options).c_str()),
kBlinkOutputWithCacheablePanelsCookiesSuffix);
}
GoogleString GetJsDisableScriptSnippet(RewriteOptions* options) {
if (options->enable_defer_js_experimental()) {
return StrCat("<script type=\"text/javascript\" data-pagespeed-no-defer>",
JsDisableFilter::kEnableJsExperimental,
"</script>");
} else {
return "";
}
}
virtual void SetUp() {
const PropertyCache::Cohort* blink_cohort =
SetupCohort(server_context_->page_property_cache(),
BlinkUtil::kBlinkCohort);
server_context_->set_blink_cohort(blink_cohort);
server_context_->set_enable_property_cache(true);
InitHasher();
ThreadSynchronizer* sync = server_context()->thread_synchronizer();
sync->EnableForPrefix(CacheHtmlFlow::kBackgroundComputationDone);
sync->EnableForPrefix(ProxyFetch::kCollectorFinish);
sync->AllowSloppyTermination(CacheHtmlFlow::kBackgroundComputationDone);
sync->AllowSloppyTermination(ProxyFetch::kCollectorFinish);
flush_early_info_finder_ = new MeaningfulFlushEarlyInfoFinder;
server_context()->set_flush_early_info_finder(flush_early_info_finder_);
options_.reset(server_context()->NewOptions());
options_->EnableFilter(RewriteOptions::kCachePartialHtml);
options_->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
options_->EnableFilter(RewriteOptions::kRewriteJavascriptInline);
options_->set_non_cacheables_for_cache_partial_html(
"class=item,id=beforeItems");
options_->Disallow("*blacklist*");
InitializeOutputs(options_.get());
SetRewriteOptions(options_.get());
server_context()->ComputeSignature(options_.get());
ProxyInterfaceTestBase::SetUp();
ProxyInterface::InitStats(statistics());
proxy_interface_.reset(
new ProxyInterface("localhost", 80, server_context(), statistics()));
server_context()->url_namer()->set_proxy_domain("http://proxy-domain");
server_context()->set_cache_html_info_finder(new CacheHtmlInfoFinder());
SetTimeMs(MockTimer::kApr_5_2010_ms);
SetFetchFailOnUnexpected(false);
response_headers_.SetStatusAndReason(HttpStatus::kOK);
response_headers_.Add(HttpAttributes::kContentType,
kContentTypePng.mime_type());
SetFetchResponse("http://test.com/test.png", response_headers_,
kFakePngInput);
response_headers_.Remove(HttpAttributes::kContentType,
kContentTypePng.mime_type());
response_headers_.SetStatusAndReason(HttpStatus::kNotFound);
response_headers_.Add(HttpAttributes::kContentType,
kContentTypeText.mime_type());
SetFetchResponse("http://test.com/404.html", response_headers_,
kHtmlInput);
response_headers_.SetStatusAndReason(HttpStatus::kOK);
response_headers_.SetDateAndCaching(MockTimer::kApr_5_2010_ms,
1 * Timer::kSecondMs);
response_headers_.ComputeCaching();
SetFetchResponse("http://test.com/plain.html", response_headers_,
kHtmlInput);
SetFetchResponse("http://test.com/blacklist.html", response_headers_,
kHtmlInput);
response_headers_.Replace(HttpAttributes::kContentType,
"text/html; charset=utf-8");
response_headers_.Add(HttpAttributes::kSetCookie, "helo=world; path=/");
SetFetchResponse("http://test.com/text.html", response_headers_,
kHtmlInput);
SetFetchResponse("http://test.com/minifiable_text.html", response_headers_,
kHtmlInputWithMinifiableJs);
SetFetchResponse("http://test.com/smalltest.html", response_headers_,
kSmallHtmlInput);
SetFetchResponse("http://test.com/noblink_text.html", response_headers_,
kHtmlInputForNoBlink);
SetFetchResponse("https://test.com/noblink_text.html", response_headers_,
kHtmlInputForNoBlink);
SetFetchResponse("http://test.com/cache.html", response_headers_,
kHtmlInput);
SetFetchResponse("http://test.com/non_html.html", response_headers_,
kFakePngInput);
SetFetchResponse("http://test.com/ws_text.html", response_headers_,
StrCat(kWhitespace, kHtmlInput));
SetFetchResponse("http://test.com/flush_subresources.html",
response_headers_, kFlushSubresourcesHtmlInput);
SetResponseWithDefaultHeaders(StrCat(kTestDomain, "1.css"), kContentTypeCss,
kCssContent, kHtmlCacheTimeSec * 2);
AddFileToMockFetcher(StrCat(kTestDomain, "image1"), kSampleJpgFile,
kContentTypeJpeg, 100);
}
virtual void InitHasher() {
UseMd5Hasher();
}
virtual RequestContextPtr CreateRequestContext() {
return RequestContextPtr(test_request_context_);
}
void InitializeExperimentSpec() {
options_->set_running_experiment(true);
NullMessageHandler handler;
ASSERT_TRUE(options_->AddExperimentSpec("id=3;percent=100;default",
&handler));
}
void GetDefaultRequestHeaders(RequestHeaders* request_headers) {
// Request from an internal ip.
request_headers->Add(HttpAttributes::kUserAgent, kChromeLinuxUserAgent);
request_headers->Add(HttpAttributes::kAccept, "image/webp");
request_headers->Add(HttpAttributes::kXForwardedFor, "127.0.0.1");
request_headers->Add(HttpAttributes::kXGoogleRequestEventId,
"1345815119391831");
}
void FetchFromProxyWaitForBackground(const StringPiece& url,
bool expect_success,
GoogleString* string_out,
ResponseHeaders* headers_out) {
FetchFromProxy(url, expect_success, string_out, headers_out, true);
}
void FetchFromProxyWaitForBackground(const StringPiece& url,
bool expect_success,
const RequestHeaders& request_headers,
GoogleString* string_out,
ResponseHeaders* headers_out,
GoogleString* user_agent_out,
bool wait_for_background_computation) {
FetchFromProxy(url, expect_success, request_headers, string_out,
headers_out, user_agent_out,
wait_for_background_computation);
}
void VerifyNonCacheHtmlResponse(const ResponseHeaders& response_headers) {
ConstStringStarVector values;
EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kCacheControl,
&values));
EXPECT_EQ(2, values.size());
EXPECT_STREQ("max-age=0", *(values[0]));
EXPECT_STREQ("no-cache", *(values[1]));
}
void VerifyCacheHtmlResponse(const ResponseHeaders& response_headers) {
EXPECT_STREQ("OK", response_headers.reason_phrase());
EXPECT_STREQ(start_time_string_,
response_headers.Lookup1(HttpAttributes::kDate));
ConstStringStarVector v;
EXPECT_STREQ("text/html; charset=utf-8",
response_headers.Lookup1(HttpAttributes::kContentType));
EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kCacheControl, &v));
EXPECT_EQ("max-age=0", *v[0]);
EXPECT_EQ("private", *v[1]);
EXPECT_EQ("no-cache", *v[2]);
}
void FetchFromProxyNoWaitForBackground(const StringPiece& url,
bool expect_success,
GoogleString* string_out,
ResponseHeaders* headers_out) {
FetchFromProxy(url, expect_success, string_out, headers_out, false);
}
void FetchFromProxy(const StringPiece& url,
bool expect_success,
GoogleString* string_out,
ResponseHeaders* headers_out,
bool wait_for_background_computation) {
RequestHeaders request_headers;
GetDefaultRequestHeaders(&request_headers);
FetchFromProxy(url, expect_success, request_headers,
string_out, headers_out, wait_for_background_computation);
}
void FetchFromProxy(const StringPiece& url,
bool expect_success,
const RequestHeaders& request_headers,
GoogleString* string_out,
ResponseHeaders* headers_out,
bool wait_for_background_computation) {
FetchFromProxy(url, expect_success, request_headers,
string_out, headers_out, NULL,
wait_for_background_computation);
}
void FetchFromProxy(const StringPiece& url,
bool expect_success,
const RequestHeaders& request_headers,
GoogleString* string_out,
ResponseHeaders* headers_out,
GoogleString* user_agent_out,
bool wait_for_background_computation) {
FetchFromProxy(url,
expect_success,
request_headers,
string_out,
headers_out,
NULL,
wait_for_background_computation,
true);
}
void FetchFromProxy(const StringPiece& url,
bool expect_success,
const RequestHeaders& request_headers,
GoogleString* string_out,
ResponseHeaders* headers_out,
GoogleString* user_agent_out,
bool wait_for_background_computation,
bool proxy_fetch_property_callback_collector_created) {
FetchFromProxyNoQuiescence(url, expect_success, request_headers,
string_out, headers_out, user_agent_out);
if (proxy_fetch_property_callback_collector_created) {
ThreadSynchronizer* thread_synchronizer =
server_context()->thread_synchronizer();
thread_synchronizer->Wait(ProxyFetch::kCollectorFinish);
}
if (wait_for_background_computation) {
ThreadSynchronizer* sync = server_context()->thread_synchronizer();
sync->Wait(CacheHtmlFlow::kBackgroundComputationDone);
}
}
void FetchFromProxyNoQuiescence(const StringPiece& url,
bool expect_success,
const RequestHeaders& request_headers,
GoogleString* string_out,
ResponseHeaders* headers_out) {
FetchFromProxyNoQuiescence(url, expect_success, request_headers,
string_out, headers_out, NULL);
}
void FetchFromProxyNoQuiescence(const StringPiece& url,
bool expect_success,
const RequestHeaders& request_headers,
GoogleString* string_out,
ResponseHeaders* headers_out,
GoogleString* user_agent_out) {
WorkerTestBase::SyncPoint sync(server_context()->thread_system());
AsyncExpectStringAsyncFetch callback(
expect_success, &sync, rewrite_driver()->request_context());
callback.set_response_headers(headers_out);
callback.request_headers()->CopyFrom(request_headers);
proxy_interface_->Fetch(AbsolutifyUrl(url), message_handler(), &callback);
CHECK(server_context()->thread_synchronizer() != NULL);
sync.Wait();
EXPECT_TRUE(callback.done());
*string_out = callback.buffer();
if (user_agent_out != NULL &&
callback.request_headers()->Lookup1(HttpAttributes::kUserAgent)
!= NULL) {
user_agent_out->assign(
callback.request_headers()->Lookup1(HttpAttributes::kUserAgent));
}
}
void FetchFromProxyWithDelayCache(
const StringPiece& url, bool expect_success,
const RequestHeaders& request_headers,
ProxyInterfaceWithDelayCache* proxy_interface,
GoogleString* string_out,
ResponseHeaders* headers_out) {
WorkerTestBase::SyncPoint sync(server_context()->thread_system());
AsyncExpectStringAsyncFetch callback(
expect_success, &sync, rewrite_driver()->request_context());
callback.set_response_headers(headers_out);
callback.request_headers()->CopyFrom(request_headers);
proxy_interface->Fetch(AbsolutifyUrl(url), message_handler(), &callback);
CHECK(server_context()->thread_synchronizer() != NULL);
delay_cache()->ReleaseKey(proxy_interface->key());
sync.Wait();
EXPECT_TRUE(callback.done());
*string_out = callback.buffer();
ThreadSynchronizer* ts = server_context()->thread_synchronizer();
ts->Wait(CacheHtmlFlow::kBackgroundComputationDone);
mock_scheduler()->AwaitQuiescence();
}
void CheckHeaders(const ResponseHeaders& headers,
const ContentType& expect_type) {
ASSERT_TRUE(headers.has_status_code());
EXPECT_EQ(HttpStatus::kOK, headers.status_code());
EXPECT_STREQ(expect_type.mime_type(),
headers.Lookup1(HttpAttributes::kContentType));
}
// Verifies the fields of CacheHtmlFlow Info proto being logged.
CacheHtmlLoggingInfo* VerifyCacheHtmlLoggingInfo(
int cache_html_request_flow, const char* url) {
CacheHtmlLoggingInfo* cache_html_logging_info =
cache_html_logging_info_.mutable_cache_html_logging_info();
EXPECT_EQ(cache_html_request_flow,
cache_html_logging_info->cache_html_request_flow());
EXPECT_EQ("1345815119391831",
cache_html_logging_info->request_event_id_time_usec());
EXPECT_STREQ(url, cache_html_logging_info->url());
return cache_html_logging_info;
}
CacheHtmlLoggingInfo* VerifyCacheHtmlLoggingInfo(
int cache_html_request_flow, bool html_match, const char* url) {
CacheHtmlLoggingInfo* cache_html_logging_info =
VerifyCacheHtmlLoggingInfo(cache_html_request_flow, url);
EXPECT_EQ(html_match, cache_html_logging_info->html_match());
return cache_html_logging_info;
}
void VerifyBlacklistUserAgent(const ResponseHeaders& response_headers) {
ConstStringStarVector v;
EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kCacheControl, &v));
EXPECT_STREQ("text/plain",
response_headers.Lookup1(HttpAttributes::kContentType));
EXPECT_STREQ("max-age=1", *v[0]);
}
void VerifyFlushSubresourcesResponse(GoogleString text,
bool is_applied_expected) {
// If FlushSubresources Filter is applied then the response has
// rel="subresource".
bool is_applied = false;
const char pattern[] = "rel=\"stylesheet\"";
int pattern_position = text.find(pattern);
if (pattern_position != GoogleString::npos) {
is_applied = true;
}
EXPECT_EQ(is_applied_expected, is_applied);
}
void UnEscapeString(GoogleString* str) {
GlobalReplaceSubstring("__psa_lt;", "<", str);
GlobalReplaceSubstring("__psa_gt;", ">", str);
}
// TODO(nikhilmadan): This is super fragile as RewriteTestBase also has
// an options_ member.
scoped_ptr<RewriteOptions> options_;
GoogleString start_time_string_;
void SetFetchHtmlResponseWithStatus(const char* url,
HttpStatus::Code status) {
ResponseHeaders response_headers;
response_headers.SetStatusAndReason(status);
response_headers.Add(HttpAttributes::kContentType, "text/html");
SetFetchResponse(url, response_headers, kHtmlInput);
}
void CheckStats(int diff_matches, int diff_mismatches,
int smart_diff_matches, int smart_diff_mismatches,
int hits, int misses) {
EXPECT_EQ(diff_matches, TimedValue(CacheHtmlFlow::kNumCacheHtmlMatches));
EXPECT_EQ(diff_mismatches, TimedValue(
CacheHtmlFlow::kNumCacheHtmlMismatches));
EXPECT_EQ(smart_diff_matches, TimedValue(
CacheHtmlFlow::kNumCacheHtmlSmartdiffMatches));
EXPECT_EQ(smart_diff_mismatches, TimedValue(
CacheHtmlFlow::kNumCacheHtmlSmartdiffMismatches));
EXPECT_EQ(hits, TimedValue(CacheHtmlFlow::kNumCacheHtmlHits));
EXPECT_EQ(misses, TimedValue(CacheHtmlFlow::kNumCacheHtmlMisses));
}
void TestCacheHtmlChangeDetection(bool use_smart_diff) {
options_->ClearSignatureForTesting();
options_->set_enable_blink_html_change_detection(true);
server_context()->ComputeSignature(options_.get());
GoogleString text;
ResponseHeaders response_headers;
// Hashes not set. Results in mismatches.
FetchFromProxyWaitForBackground("text.html", true, &text,
&response_headers);
VerifyCacheHtmlLoggingInfo(
CacheHtmlLoggingInfo::CACHE_HTML_MISS_TRIGGERED_REWRITE, false,
"http://test.com/text.html");
// Diff Match: 0, Diff Mismatch: 0,
// Smart Diff Match: 0, Smart Diff Mismatch: 0
// Hits: 0, Misses: 1
CheckStats(0, 0, 0, 0, 0, 1);
ClearStats();
response_headers.Clear();
// Hashes set. No mismatches.
FetchFromProxyWaitForBackground("text.html", true, &text,
&response_headers);
// Diff Match: 1, Diff Mismatch: 0,
// Smart Diff Match: 1, Smart Diff Mismatch: 0
// Hits: 1, Misses: 0
CheckStats(1, 0, 1, 0, 1, 0);
VerifyCacheHtmlResponse(response_headers);
UnEscapeString(&text);
EXPECT_STREQ(blink_output_, text);
VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_HIT, true,
"http://test.com/text.html");
ClearStats();
response_headers.Clear();
// Input with an extra comment. We strip out comments before taking hash,
// so there should be no mismatches.
SetFetchResponse(kTestUrl, response_headers_,
kHtmlInputWithExtraCommentAndNonCacheable);
FetchFromProxyWaitForBackground("text.html", true, &text,
&response_headers);
// Diff Match: 1, Diff Mismatch: 0,
// Smart Diff Match: 1, Smart Diff Mismatch: 0
// Hits: 1, Misses: 0
CheckStats(1, 0, 1, 0, 1, 0);
VerifyCacheHtmlResponse(response_headers);
UnEscapeString(&text);
EXPECT_STREQ(blink_output_, text);
VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_HIT, true,
"http://test.com/text.html");
ClearStats();
response_headers.Clear();
// Input with extra attributes. This should result in a mismatch with
// full-diff but a match with smart-diff.
SetFetchResponse(kTestUrl, response_headers_,
kHtmlInputWithExtraAttribute);
FetchFromProxyWaitForBackground("text.html", true, &text,
&response_headers);
VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_HIT, false,
"http://test.com/text.html");
// Diff Match: 0, Diff Mismatch: 1,
// Smart Diff Match: 1, Smart Diff Mismatch: 0
// Hits: 1, Misses: 0
CheckStats(0, 1, 1, 0, 1, 0);
ClearStats();
// Input with empty visible portions. Diff calculation should not trigger.
SetFetchResponse(kTestUrl, response_headers_,
kHtmlInputWithEmptyVisiblePortions);
FetchFromProxyWaitForBackground("text.html", true, &text,
&response_headers);
// Diff Match: 0, Diff Mismatch: 1,
// Smart Diff Match: 0, Smart Diff Mismatch: 1
// Hits: 1, Misses: 0
CheckStats(0, 1, 0, 1, 1, 0);
}
GoogleString GetImageOnloadScriptBlock() const {
return StrCat("<script data-pagespeed-no-defer type=\"text/javascript\">",
DelayImagesFilter::kImageOnloadJsSnippet,
"</script>");
}
LoggingInfo cache_html_logging_info_;
ResponseHeaders response_headers_;
GoogleString noblink_output_;
GoogleString noblink_output_with_lazy_load_;
GoogleString blink_output_with_lazy_load_;
GoogleString blink_output_partial_;
GoogleString blink_output_;
GoogleString blink_output_with_cacheable_panels_cookies_;
GoogleString blink_output_with_cacheable_panels_no_cookies_;
MeaningfulFlushEarlyInfoFinder* flush_early_info_finder_;
TestRequestContextPtr test_request_context_;
MockCriticalCssFinder* critical_css_finder_;
private:
DISALLOW_COPY_AND_ASSIGN(CacheHtmlFlowTest);
};
TEST_F(CacheHtmlFlowTest, TestCacheHtmlCacheMiss) {
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxyWaitForBackground("minifiable_text.html", true, &text,
&response_headers);
ConstStringStarVector values;
EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kSetCookie, &values));
EXPECT_EQ(1, values.size());
VerifyNonCacheHtmlResponse(response_headers);
VerifyCacheHtmlLoggingInfo(
CacheHtmlLoggingInfo::CACHE_HTML_MISS_TRIGGERED_REWRITE, false,
"http://test.com/minifiable_text.html");
EXPECT_STREQ(StringPrintf(
kHtmlInputWithMinifiedJs, GetJsDisableScriptSnippet(
options_.get()).c_str()), text);
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlCacheMissAndHit) {
GoogleString text;
ResponseHeaders response_headers;
// First request updates the property cache with cached html.
FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
VerifyNonCacheHtmlResponse(response_headers);
EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
VerifyCacheHtmlLoggingInfo(
CacheHtmlLoggingInfo::CACHE_HTML_MISS_TRIGGERED_REWRITE, false,
"http://test.com/text.html");
CheckStats(0, 0, 0, 0, 0, 1);
ClearStats();
// Cache Html hit case.
response_headers.Clear();
FetchFromProxyNoWaitForBackground("text.html", true, &text,
&response_headers);
VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_HIT, false,
"http://test.com/text.html");
CheckStats(0, 0, 0, 0, 1, 0);
ClearStats();
VerifyCacheHtmlResponse(response_headers);
UnEscapeString(&text);
EXPECT_STREQ(blink_output_, text);
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlChangeDetection) {
TestCacheHtmlChangeDetection(false);
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlChangeDetectionWithSmartDiffOn) {
TestCacheHtmlChangeDetection(true);
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlMissExperimentSetCookie) {
options_->ClearSignatureForTesting();
options_->set_experiment_cookie_duration_ms(1000);
SetTimeMs(MockTimer::kApr_5_2010_ms);
InitializeExperimentSpec();
server_context()->ComputeSignature(options_.get());
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
ConstStringStarVector values;
EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kSetCookie, &values));
ASSERT_EQ(2, values.size());
EXPECT_STREQ("PageSpeedExperiment=3", (*(values[1])).substr(0, 21));
GoogleString expires_str;
ConvertTimeToString(MockTimer::kApr_5_2010_ms + 1000, &expires_str);
EXPECT_NE(GoogleString::npos, ((*(values[1])).find(expires_str)));
VerifyNonCacheHtmlResponse(response_headers);
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlHitExperimentSetCookie) {
options_->ClearSignatureForTesting();
InitializeExperimentSpec();
server_context()->ComputeSignature(options_.get());
GoogleString text;
ResponseHeaders response_headers;
// Populate the property cache in first request.
FetchFromProxyWaitForBackground("text.html", true, &text,
&response_headers);
response_headers.Clear();
FetchFromProxyNoWaitForBackground("text.html", true, &text,
&response_headers);
ConstStringStarVector values;
EXPECT_TRUE(response_headers.Lookup(HttpAttributes::kSetCookie, &values));
EXPECT_EQ(1, values.size());
EXPECT_STREQ("PageSpeedExperiment=3", (*(values[0])).substr(0, 21));
VerifyCacheHtmlResponse(response_headers);
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlExperimentCookieHandling) {
options_->ClearSignatureForTesting();
InitializeExperimentSpec();
server_context()->ComputeSignature(options_.get());
GoogleString text;
ResponseHeaders response_headers;
RequestHeaders request_headers;
GetDefaultRequestHeaders(&request_headers);
request_headers.Add(HttpAttributes::kCookie, "PageSpeedExperiment=3");
// Populate the property cache in first request.
FetchFromProxyWaitForBackground("text.html", true, &text,
&response_headers);
response_headers.Clear();
FetchFromProxy("text.html", true, request_headers,
&text, &response_headers, false);
EXPECT_FALSE(response_headers.Has(HttpAttributes::kSetCookie));
VerifyCacheHtmlResponse(response_headers);
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlCacheHitWithInlinePreviewImages) {
const char kInlinePreviewHtmlInput[] =
"<html>"
"<head>"
"</head>"
"<body>\n"
"<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<h2 id=\"beforeItems\"> This is before Items </h2>"
"<div class=\"item1\">"
"<img src=\"image1\">"
"<img src=\"image2\">"
"</div>"
"<div class=\"item\">"
"<img src=\"image3\">"
"<div class=\"item\">"
"<img src=\"image4\">"
"</div>"
"</div>"
"</div>"
"</body></html>";
SetFetchResponse("http://test.com/text.html", response_headers_,
kInlinePreviewHtmlInput);
StringSet* critical_images = new StringSet;
critical_images->insert(StrCat(kTestDomain, "image1"));
SetCriticalImagesInFinder(critical_images);
options_->ClearSignatureForTesting();
options_->EnableFilter(RewriteOptions::kDelayImages);
server_context()->ComputeSignature(options_.get());
GoogleString text;
ResponseHeaders response_headers;
// First request updates the property cache with cached html.
FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
VerifyNonCacheHtmlResponse(response_headers);
EXPECT_EQ(-1, logging_info()->num_html_critical_images());
EXPECT_EQ(-1, logging_info()->num_css_critical_images());
// Cache Html hit case.
response_headers.Clear();
FetchFromProxyNoWaitForBackground("text.html", true, &text,
&response_headers);
VerifyCacheHtmlResponse(response_headers);
UnEscapeString(&text);
const char kBlinkOutputWithInlinePreviewImages[] =
"<html><head></head><body>"
"<noscript><meta HTTP-EQUIV=\"refresh\" content=\"0;"
"url='%s?PageSpeed=noscript'\" />"
"<style><!--table,div,span,font,p{display:none} --></style>"
"<div style=\"display:block\">Please click "
"<a href=\"%s?PageSpeed=noscript\">here</a> "
"if you are not redirected within a few seconds.</div></noscript>"
"\n<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<!--GooglePanel begin panel-id-1.0-->"
"<!--GooglePanel end panel-id-1.0-->"
"<div class=\"item1\">%s" // Inlined Image tag with script.
"%s" // image-onload js snippet.
"<img src=\"image2\">"
"</div>"
"<!--GooglePanel begin panel-id-0.0-->"
"<!--GooglePanel end panel-id-0.0-->"
"</div>"
"</body></html>"
"%s<script type=\"text/javascript\" src=\"/psajs/blink.0.js\"></script>"
"<script type=\"text/javascript\">"
"pagespeed.panelLoaderInit();</script>\n"
"<script type=\"text/javascript\">"
"pagespeed.panelLoader.setRequestFromInternalIp();</script>\n"
"%s" // kCookieScript
"<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-1.0\":{\"instance_html\":\"<h2 id=\\\"beforeItems\\\"> This is before Items </h2>\",\"xpath\":\"//div[@id=\\\"container\\\"]/h2[1]\"}}\n);</script>" // NOLINT
"<script>pagespeed.panelLoader.loadNonCacheableObject({\"panel-id-0.0\":{\"instance_html\":\"<div class=\\\"item\\\"><img src=\\\"image3\\\"><div class=\\\"item\\\"><img src=\\\"image4\\\"></div></div>\",\"xpath\":\"//div[@id=\\\"container\\\"]/div[3]\"}}\n);</script>" // NOLINT
"<script>pagespeed.panelLoader.bufferNonCriticalData({});</script>"; // NOLINT
GoogleString inlined_image_wildcard =
StringPrintf(kBlinkOutputWithInlinePreviewImages, kTestUrl, kTestUrl,
GetImageOnloadScriptBlock().c_str(),
"<img data-pagespeed-high-res-src=\"image1\" "
"src=\"data:image/jpeg;base64*",
GetJsDisableScriptSnippet(options_.get()).c_str(),
kCookieScript);
EXPECT_TRUE(Wildcard(inlined_image_wildcard).Match(text))
<< "Expected:\n" << inlined_image_wildcard << "\n\nGot:\n" << text;
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlOverThreshold) {
options_->ClearSignatureForTesting();
// Content type is more than the limit to buffer in secondary fetch.
int64 size_of_small_html = arraysize(kSmallHtmlInput) - 1;
int64 html_buffer_threshold = size_of_small_html - 1;
options_->ClearSignatureForTesting();
options_->set_blink_max_html_size_rewritable(html_buffer_threshold);
server_context()->ComputeSignature(options_.get());
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxyWaitForBackground(
"smalltest.html", true, &text, &response_headers);
GoogleString SmallHtmlOutput =
StrCat("<html><head></head><body>A small test html.",
GetJsDisableScriptSnippet(options_.get()),
"<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\">"
"</script></body></html>");
EXPECT_STREQ(SmallHtmlOutput, text);
VerifyCacheHtmlLoggingInfo(
CacheHtmlLoggingInfo::FOUND_CONTENT_LENGTH_OVER_THRESHOLD,
"http://test.com/smalltest.html");
// 1 Miss for original plain text,
// 1 Miss for Blink Cohort.
EXPECT_EQ(2, lru_cache()->num_misses());
CheckStats(0, 0, 0, 0, 0, 1);
ClearStats();
text.clear();
response_headers.Clear();
options_->ClearSignatureForTesting();
html_buffer_threshold = size_of_small_html + 1;
options_->set_blink_max_html_size_rewritable(html_buffer_threshold);
server_context()->ComputeSignature(options_.get());
FetchFromProxyWaitForBackground(
"smalltest.html", true, &text, &response_headers);
EXPECT_EQ(2, lru_cache()->num_misses());
CheckStats(0, 0, 0, 0, 0, 1);
ClearStats();
text.clear();
response_headers.Clear();
options_->ClearSignatureForTesting();
FetchFromProxyNoWaitForBackground(
"smalltest.html", true, &text, &response_headers);
CheckStats(0, 0, 0, 0, 1, 0);
EXPECT_EQ(1, lru_cache()->num_misses());
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlHeaderOverThreshold) {
options_->ClearSignatureForTesting();
InitializeExperimentSpec();
int64 size_of_small_html = arraysize(kSmallHtmlInput) - 1;
int64 html_buffer_threshold = size_of_small_html;
options_->ClearSignatureForTesting();
options_->set_blink_max_html_size_rewritable(html_buffer_threshold);
server_context()->ComputeSignature(options_.get());
GoogleString text;
ResponseHeaders response_headers;
// Setting a higher content length to verify if the header's content length
// is checked before rewriting.
response_headers.Add(HttpAttributes::kContentLength,
IntegerToString(size_of_small_html + 1));
response_headers.SetStatusAndReason(HttpStatus::kOK);
response_headers.Add(HttpAttributes::kContentType,
"text/html; charset=utf-8");
SetFetchResponse("http://test.com/smalltest.html", response_headers,
kSmallHtmlInput);
FetchFromProxyWaitForBackground(
"smalltest.html", true, &text, &response_headers);
VerifyCacheHtmlLoggingInfo(
CacheHtmlLoggingInfo::FOUND_CONTENT_LENGTH_OVER_THRESHOLD,
"http://test.com/smalltest.html");
// 1 Miss for original plain text,
// 1 Miss for Blink Cohort.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
}
TEST_F(CacheHtmlFlowTest, Non200StatusCode) {
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxyWaitForBackground("404.html", true, &text, &response_headers);
EXPECT_STREQ(kHtmlInput, text);
EXPECT_STREQ("text/plain",
response_headers.Lookup1(HttpAttributes::kContentType));
VerifyCacheHtmlLoggingInfo(CacheHtmlLoggingInfo::CACHE_HTML_MISS_FETCH_NON_OK,
"http://test.com/404.html");
// 1 Miss for original plain text,
// 1 Miss for Blink Cohort.
EXPECT_EQ(2, lru_cache()->num_misses());
CheckStats(0, 0, 0, 0, 0, 1);
}
TEST_F(CacheHtmlFlowTest, NonHtmlContent) {
// Content type is non html.
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxyNoWaitForBackground(
"plain.html", true, &text, &response_headers);
EXPECT_STREQ(kHtmlInput, text);
EXPECT_STREQ("text/plain",
response_headers.Lookup1(HttpAttributes::kContentType));
VerifyCacheHtmlLoggingInfo(
CacheHtmlLoggingInfo::CACHE_HTML_MISS_FOUND_RESOURCE,
"http://test.com/plain.html");
// 1 Miss for Blink Cohort.
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(1, lru_cache()->num_inserts());
CheckStats(0, 0, 0, 0, 0, 1);
ClearStats();
text.clear();
response_headers.Clear();
FetchFromProxyNoWaitForBackground(
"plain.html", true, &text, &response_headers);
VerifyCacheHtmlLoggingInfo(
CacheHtmlLoggingInfo::CACHE_HTML_MISS_FOUND_RESOURCE,
"http://test.com/plain.html");
// 1 Miss for Blink Cohort.
CheckStats(0, 0, 0, 0, 0, 1);
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_inserts());
// Content type is html but the actual content is non html.
FetchFromProxyNoWaitForBackground(
"non_html.html", true, &text, &response_headers);
FetchFromProxyWaitForBackground(
"non_html.html", true, &text, &response_headers);
VerifyCacheHtmlLoggingInfo(
CacheHtmlLoggingInfo::CACHE_HTML_MISS_FOUND_RESOURCE,
"http://test.com/non_html.html");
CheckStats(0, 0, 0, 0, 0, 3);
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlWithWebp) {
if (RunningOnValgrind()) {
return;
}
rewrite_driver_->server_context()->set_hasher(factory_->mock_hasher());
AddFileToMockFetcher(StrCat(kTestDomain, "image1"), "Puzzle.jpg",
kContentTypeJpeg, 100);
options_->ClearSignatureForTesting();
options_->EnableFilter(RewriteOptions::kConvertJpegToWebp);
server_context()->ComputeSignature(options_.get());
GoogleString text;
ResponseHeaders response_headers;
// First request updates the property cache with cached html.
FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
VerifyNonCacheHtmlResponse(response_headers);
ClearStats();
// Cache Html hit case.
response_headers.Clear();
FetchFromProxyNoWaitForBackground("text.html", true, &text,
&response_headers);
ClearStats();
VerifyCacheHtmlResponse(response_headers);
UnEscapeString(&text);
GoogleString correct_url =
Encode("", RewriteOptions::kImageCompressionId, "0", "image1", "webp");
GoogleString blink_output_with_webp =
StrCat(blink_output_partial_, kCookieScript,
StringPrintf(kBlinkOutputSuffix, correct_url.c_str()));
EXPECT_STREQ(blink_output_with_webp, text);
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlWithHttpsUrl) {
GoogleString text;
ResponseHeaders response_headers;
RequestHeaders request_headers;
GetDefaultRequestHeaders(&request_headers);
FetchFromProxy("https://test.com/noblink_text.html", true, request_headers,
&text, &response_headers, false);
EXPECT_STREQ(
StrCat("<html><head></head><body>",
GetJsDisableScriptSnippet(options_.get()),
"<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\">"
"</script></body></html>"), text);
EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlWithWhitespace) {
GoogleString text;
ResponseHeaders response_headers;
FetchFromProxyWaitForBackground(
"ws_text.html", true, &text, &response_headers);
EXPECT_EQ(0, TimedValue(CacheHtmlFlow::kNumCacheHtmlHits));
EXPECT_EQ(1, TimedValue(CacheHtmlFlow::kNumCacheHtmlMisses));
EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlushSubresources) {
// FlushSubresources is applied when blink is enabled and user agent
// does not support blink.
GoogleString text;
RequestHeaders request_headers;
request_headers.Replace(HttpAttributes::kUserAgent,
"prefetch_link_rel_subresource");
ResponseHeaders response_headers;
FetchFromProxy("http://test.com/flush_subresources.html"
"?PageSpeedFilters=+extend_cache_css,-inline_css", true,
request_headers, &text, &response_headers, NULL, false);
VerifyNonCacheHtmlResponse(response_headers);
EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
// Requesting again.
flush_early_info_finder_->Clear();
response_headers.Clear();
FetchFromProxy("http://test.com/flush_subresources.html"
"?PageSpeedFilters=+extend_cache_css,-inline_css", true,
request_headers, &text, &response_headers, NULL, false);
VerifyFlushSubresourcesResponse(text, true);
EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlowUrlCacheInvalidation) {
GoogleString text;
ResponseHeaders response_headers;
GoogleString htmlOutput = StrCat(
"<html><head></head>"
"<body>\n"
"<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<h2 id=\"beforeItems\"> This is before Items </h2>"
"<div class=\"item\">"
"<img src=\"image1\">"
"<img src=\"image2\">"
"</div>"
"<div class=\"item\">"
"<img src=\"image3\">"
"<div class=\"item\">"
"<img src=\"image4\">"
"</div>"
"</div>"
"</div>",
GetJsDisableScriptSnippet(options_.get()),
"<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\"></script>"
"</body></html>");
FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
EXPECT_STREQ(htmlOutput, text);
// Cache lookup for original plain text and Blink Cohort
// all miss.
// ie., 1 + 1 (Blink Cohort).
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(1, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_deletes());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
response_headers.Clear();
ClearStats();
// Property cache hit.
FetchFromProxyNoWaitForBackground(
"text.html", true, &text, &response_headers);
UnEscapeString(&text);
EXPECT_STREQ(blink_output_, text);
// 1 Miss for original plain text
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_deletes());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
ClearStats();
// Invalidate the cache for some URL other than 'text.html'.
options_->ClearSignatureForTesting();
options_->AddUrlCacheInvalidationEntry(
AbsolutifyUrl("foo.bar"), timer()->NowMs(), true);
server_context()->ComputeSignature(options_.get());
// Property cache hit.
FetchFromProxyNoWaitForBackground(
"text.html", true, &text, &response_headers);
UnEscapeString(&text);
EXPECT_STREQ(blink_output_, text);
// 1 Miss for original plain text
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_deletes());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
ClearStats();
// Invalidate the cache.
options_->ClearSignatureForTesting();
options_->AddUrlCacheInvalidationEntry(
AbsolutifyUrl("text.html"), timer()->NowMs(), true);
server_context()->ComputeSignature(options_.get());
// Property cache hit, but invalidated. Hence treated as a miss and
// passthrough by blink.
FetchFromProxyWaitForBackground("text.html", true, &text, &response_headers);
EXPECT_STREQ(htmlOutput, text);
// 1 Miss for original plain text
EXPECT_EQ(1, lru_cache()->num_misses());
EXPECT_EQ(1, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_deletes());
EXPECT_EQ(1, lru_cache()->num_identical_reinserts()); // identical insert
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlowWithHeadRequest) {
GoogleString text;
ResponseHeaders response_headers;
RequestHeaders request_headers;
request_headers.Add(HttpAttributes::kUserAgent, kChromeLinuxUserAgent);
request_headers.set_method(RequestHeaders::kHead);
FetchFromProxy("text.html", true, request_headers,
&text, &response_headers, false);
EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlowDataMissDelayCache) {
GoogleString text;
ResponseHeaders response_headers;
ProxyInterfaceWithDelayCache* proxy_interface =
new ProxyInterfaceWithDelayCache("localhost", 80,
server_context(), statistics(),
delay_cache(), factory());
proxy_interface_.reset(proxy_interface);
RequestHeaders request_headers;
GetDefaultRequestHeaders(&request_headers);
FetchFromProxyWithDelayCache(
"text.html", true, request_headers, proxy_interface,
&text, &response_headers);
GoogleString htmlOutput = StrCat(
"<html><head></head>"
"<body>\n"
"<div id=\"header\"> This is the header </div>"
"<div id=\"container\" class>"
"<h2 id=\"beforeItems\"> This is before Items </h2>"
"<div class=\"item\">"
"<img src=\"image1\">"
"<img src=\"image2\">"
"</div>"
"<div class=\"item\">"
"<img src=\"image3\">"
"<div class=\"item\">"
"<img src=\"image4\">"
"</div>"
"</div>"
"</div>",
GetJsDisableScriptSnippet(options_.get()),
"<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\"></script>"
"</body></html>");
EXPECT_STREQ(htmlOutput, text);
EXPECT_STREQ("text/html; charset=utf-8",
response_headers.Lookup1(HttpAttributes::kContentType));
// 1 Miss for original plain text,
// 1 miss for BlinkCohort
VerifyNonCacheHtmlResponse(response_headers);
EXPECT_EQ(2, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
}
TEST_F(CacheHtmlFlowTest, TestCacheHtmlFlowWithDifferentUserAgents) {
GoogleString text;
ResponseHeaders response_headers;
RequestHeaders request_headers;
// Blacklisted User Agent.
request_headers.Add(HttpAttributes::kUserAgent, kBlackListUserAgent);
FetchFromProxy("blacklist.html", true, request_headers, &text,
&response_headers, NULL, false, false);
EXPECT_STREQ(kHtmlInput, text);
VerifyBlacklistUserAgent(response_headers);
EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
ClearStats();
// NULL User Agent.
request_headers.Add(HttpAttributes::kUserAgent, NULL);
FetchFromProxy("noblink_text.html", true, request_headers, &text,
&response_headers, false);
EXPECT_STREQ(
StrCat("<html><head>"
"</head><body>",
GetJsDisableScriptSnippet(options_.get()),
"<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\">"
"</script></body></html>"), text);
EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
ClearStats();
// Empty User Agent.
request_headers.Replace(HttpAttributes::kUserAgent, "");
FetchFromProxy("noblink_text.html", true, request_headers, &text,
&response_headers, false);
EXPECT_STREQ(
StrCat("<html><head>"
"</head><body>",
GetJsDisableScriptSnippet(options_.get()),
"<script type=\"text/javascript\" src=\"/psajs/js_defer.0.js\">"
"</script></body></html>"), text);
EXPECT_EQ(0, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
ClearStats();
// Mobile User Agent.
request_headers.Clear();
request_headers.Add(
HttpAttributes::kUserAgent,
UserAgentMatcherTestBase::kIPhone4Safari); // Mobile Request.
request_headers.Add(HttpAttributes::kXForwardedFor, "127.0.0.1");
FetchFromProxy("text.html", true, request_headers, &text, &response_headers,
true);
VerifyNonCacheHtmlResponse(response_headers);
EXPECT_EQ(1, TimedValue(ProxyInterface::kCacheHtmlRequestCount));
ClearStats();
// Hit case.
response_headers.Clear();
FetchFromProxy("text.html", true, request_headers, &text, &response_headers,
false);
VerifyCacheHtmlResponse(response_headers);
UnEscapeString(&text);
EXPECT_STREQ(blink_output_, text);
}
class CacheHtmlPrioritizeCriticalCssTest : public CacheHtmlFlowTest {
public:
virtual void SetUp() {
CacheHtmlFlowTest::SetUp();
SetOptions();
InitializeResponses();
}
virtual void InitHasher() {} // avoid UseMd5Hasher() in base class
void SetOptions() {
// Enable FlushSubresourcesFilter filter.
options_->ClearSignatureForTesting();
options_->EnableFilter(RewriteOptions::kCachePartialHtml);
options_->EnableFilter(RewriteOptions::kPrioritizeCriticalCss);
options_->DisableFilter(RewriteOptions::kRewriteJavascriptExternal);
options_->DisableFilter(RewriteOptions::kRewriteJavascriptInline);
options_->set_non_cacheables_for_cache_partial_html(
"class=item,id=beforeItems");
options_->set_in_place_rewriting_enabled(true);
options_->set_use_selectors_for_critical_css(false);
options_->ComputeSignature();
}
void InitializeResponses() {
// Some weird but valid CSS.
SetResponseWithDefaultHeaders("a.css", kContentTypeCss,
"div,span,*::first-letter { display: block; }"
"p { display: inline; }",
kHtmlCacheTimeSec * 2);
SetResponseWithDefaultHeaders("b.css?x=1&y=2", kContentTypeCss,
"@media screen,print { * { margin: 0px; } }",
kHtmlCacheTimeSec * 2);
}
GoogleString InputHtml() {
return GoogleString(
"<!doctype html PUBLIC \"HTML 4.0.1 Strict>"
"<html><head>"
"<title>Flush Subresources Early example</title>"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"a.css\">"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"b.css?x=1&y=2\">"
"</head>"
"<body>"
"Hello, mod_pagespeed!"
"</body></html>");
}
GoogleString ExpectedHtml(GoogleString full_styles_html) {
GoogleString expected_html = StrCat(
"<!doctype html PUBLIC \"HTML 4.0.1 Strict>"
"<html><head>"
"<title>Flush Subresources Early example</title>",
"<style>div,*::first-letter{display:block}</style>" // from a.css
"<style>@media screen{*{margin:0}}</style>" // from b.css
"</head>");
StrAppend(&expected_html,
"<body>",
StringPrintf(kNoScriptRedirectFormatter,
kNoScriptTextUrl, kNoScriptTextUrl),
"Hello, mod_pagespeed!",
full_styles_html,
"</body></html>");
StrAppend(&expected_html,
GetJsDisableScriptSnippet(options_.get()),
"<script type=\"text/javascript\" src=\"/psajs/blink.0.js\"></script>"
"<script type=\"text/javascript\">"
"pagespeed.panelLoaderInit();</script>\n"
"<script type=\"text/javascript\">"
"pagespeed.panelLoader.setRequestFromInternalIp();</script>\n",
kCookieScript,
"<script>pagespeed.panelLoader.bufferNonCriticalData({});</script>");
return expected_html;
}
GoogleString CssLinkEncodedHref(GoogleString url) {
return StrCat(
"<link rel=\"stylesheet\" type=\"text/css\" href=\"",
Encode("", "cf", kMockHashValue, url, "css"),
"\">");
}
GoogleString url() { return kTestUrl; }
void ValidateCacheHtml(const StringPiece& case_id,
const GoogleString& input_html,
const GoogleString& expected_html) {
SetFetchResponse(url(), response_headers_, input_html);
ResponseHeaders headers;
GoogleString actual_html;
// First request updates the property cache with cached html.
FetchFromProxyWaitForBackground(
url(), true /* expect_success */, &actual_html, &headers);
VerifyNonCacheHtmlResponse(headers);
headers.Clear();
// Fetch the url again (with no wait) and expect a cache html hit.
FetchFromProxyNoWaitForBackground(
url(), true /* expect_success */, &actual_html, &headers);
VerifyCacheHtmlResponse(headers);
UnEscapeString(&actual_html);
EXPECT_EQ(expected_html, actual_html) << "Test id:" << case_id;
}
};
TEST_F(CacheHtmlPrioritizeCriticalCssTest, CacheHtmlWithCriticalCss) {
// Add critical css rules.
MockCriticalCssFinder* critical_css_finder =
new MockCriticalCssFinder(rewrite_driver(), statistics());
server_context()->set_critical_css_finder(critical_css_finder);
critical_css_finder->AddCriticalCss(
"http://test.com/a.css", "div,*::first-letter{display:block}", 100);
critical_css_finder->AddCriticalCss(
"http://test.com/b.css?x=1&y=2", "@media screen{*{margin:0}}", 100);
GoogleString full_styles_html = StrCat(
"<noscript class=\"psa_add_styles\">"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"a.css\">"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"b.css?x=1&y=2\">"
"</noscript>"
"<script data-pagespeed-no-defer type=\"text/javascript\">",
CriticalCssFilter::kAddStylesScript,
"window['pagespeed'] = window['pagespeed'] || {};"
"window['pagespeed']['criticalCss'] = {"
" 'total_critical_inlined_size': 60,"
" 'total_original_external_size': 200,"
" 'total_overhead_size': 60,"
" 'num_replaced_links': 2,"
" 'num_unreplaced_links': 0};"
"</script>");
ValidateCacheHtml(
"critical_css", InputHtml(), ExpectedHtml(full_styles_html));
}
class TestCriticalSelectorFinder : public CriticalSelectorFinder {
public:
TestCriticalSelectorFinder(const PropertyCache::Cohort* cohort,
Statistics* stats)
: CriticalSelectorFinder(cohort, NULL /* nonce_generator */, stats) {}
virtual ~TestCriticalSelectorFinder() {}
virtual int SupportInterval() const { return 1; }
protected:
virtual bool ShouldReplacePriorResult() const { return true; }
private:
DISALLOW_COPY_AND_ASSIGN(TestCriticalSelectorFinder);
};
TEST_F(CacheHtmlPrioritizeCriticalCssTest, CacheHtmlWithCriticalSelectors) {
SetMockHashValue("00000"); // Base64 encodes to kMockHashValue.
server_context()->set_enable_property_cache(true);
PropertyCache* pcache = server_context()->page_property_cache();
const PropertyCache::Cohort* dom_cohort =
SetupCohort(pcache, RewriteDriver::kDomCohort);
const PropertyCache::Cohort* beacon_cohort =
SetupCohort(pcache, RewriteDriver::kBeaconCohort);
server_context()->set_dom_cohort(dom_cohort);
server_context()->set_beacon_cohort(beacon_cohort);
rewrite_driver()->Clear();
rewrite_driver()->set_request_context(
RequestContext::NewTestRequestContext(factory()->thread_system()));
MockPropertyPage* page = NewMockPage(
url(), kMockHashValue /* hash */, UserAgentMatcher::kDesktop);
rewrite_driver()->set_property_page(page);
pcache->Read(page);
options_->ClearSignatureForTesting();
options_->set_use_selectors_for_critical_css(true);
options_->ComputeSignature();
server_context()->set_critical_selector_finder(new TestCriticalSelectorFinder(
server_context()->beacon_cohort(), statistics()));
// Write critical selectors to property cache
StringSet selectors;
selectors.insert("div");
selectors.insert("*");
CriticalSelectorFinder* finder = server_context()->critical_selector_finder();
finder->WriteCriticalSelectorsToPropertyCache(
selectors, "" /* last_nonce */, rewrite_driver());
rewrite_driver()->property_page()->
WriteCohort(server_context()->beacon_cohort());
EXPECT_TRUE(finder->IsCriticalSelector(rewrite_driver(), "div"));
EXPECT_TRUE(finder->IsCriticalSelector(rewrite_driver(), "*"));
GoogleString full_styles_html = StrCat(
"<noscript class=\"psa_add_styles\">",
// URLs are encoded because CSS rewrite is enabled with selectors filter.
CssLinkEncodedHref("a.css"), CssLinkEncodedHref("b.css?x=1&y=2"),
"</noscript>"
"<script data-pagespeed-no-defer type=\"text/javascript\">",
rewrite_driver()->server_context()->static_asset_manager()->GetAsset(
StaticAssetEnum::CRITICAL_CSS_LOADER_JS,
rewrite_driver()->options()),
"pagespeed.CriticalCssLoader.Run();</script>");
ValidateCacheHtml(
"critical_selector", InputHtml(), ExpectedHtml(full_styles_html));
}
} // namespace net_instaweb