| /* |
| * 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 |