| /* |
| * Copyright 2012 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: jmarantz@google.com (Joshua Marantz) |
| |
| #include "net/instaweb/rewriter/public/debug_filter.h" |
| |
| #include "net/instaweb/rewriter/public/mock_critical_images_finder.h" |
| #include "net/instaweb/rewriter/public/rewrite_driver.h" |
| #include "net/instaweb/rewriter/public/rewrite_options.h" |
| #include "net/instaweb/rewriter/public/rewrite_test_base.h" |
| #include "net/instaweb/rewriter/public/server_context.h" |
| #include "net/instaweb/rewriter/public/support_noscript_filter.h" |
| #include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h" |
| #include "pagespeed/kernel/base/gmock.h" |
| #include "pagespeed/kernel/base/gtest.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/timer.h" |
| #include "pagespeed/kernel/html/disable_test_filter.h" |
| #include "pagespeed/kernel/html/html_parse_test_base.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| |
| using ::testing::HasSubstr; |
| using ::testing::Not; |
| |
| namespace net_instaweb { |
| |
| namespace { |
| |
| const char kScriptFormat[] = "<script src='%s'></script>"; |
| const char kScript[] = "x.js"; |
| |
| class DebugFilterTest : public RewriteTestBase { |
| protected: |
| virtual void SetUp() { |
| RewriteTestBase::SetUp(); |
| options()->EnableFilter(RewriteOptions::kDebug); |
| options()->EnableFilter(RewriteOptions::kExtendCacheScripts); |
| rewrite_driver()->AddFilters(); |
| SetupWriter(); |
| |
| SupportNoscriptFilter tmp(rewrite_driver()); |
| expected_dynamically_disabled_filters_.push_back(tmp.Name()); |
| } |
| |
| void ExtractFlushMessagesFromOutput(StringPiece code_to_erase, |
| StringPieceVector* flush_messages) { |
| // Test we got the flush buffers we expect. |
| // |
| // output_buffer_ now contains something like: |
| // "<token><!--xxx--><token><!--yyy-->" |
| // And we want to convert that into a StringVector with "xxx" and "yyy". |
| // So we rip out "<token>" and "-->" and use "<!--" as a delimiter for |
| // splitting the string. |
| GlobalReplaceSubstring(code_to_erase, "", &output_buffer_); |
| GlobalReplaceSubstring("-->", "", &output_buffer_); |
| SplitStringUsingSubstr(output_buffer_, "<!--", flush_messages); |
| } |
| |
| void ParseAndMaybeFlushTwice(bool do_flush, |
| StringPieceVector* flush_messages) { |
| const char kHtmlToken[] = "<token>"; |
| rewrite_driver()->StartParse(kTestDomain); |
| AdvanceTimeUs(1); |
| rewrite_driver()->ParseText(kHtmlToken); |
| AdvanceTimeUs(10); // 11us elapsed so far. |
| if (do_flush) { |
| rewrite_driver()->Flush(); |
| } |
| AdvanceTimeUs(100); // 111us elapsed so far. |
| rewrite_driver()->ParseText(kHtmlToken); |
| AdvanceTimeUs(1000); // 1111us elapsed so far. |
| if (do_flush) { |
| rewrite_driver()->Flush(); |
| } |
| AdvanceTimeUs(10000); // 11111us elapsed so far. |
| rewrite_driver()->ParseText(kHtmlToken); |
| AdvanceTimeUs(100000); // 111111us elapsed so far. |
| rewrite_driver()->FinishParse(); |
| |
| ExtractFlushMessagesFromOutput(kHtmlToken, flush_messages); |
| } |
| |
| GoogleString OptScriptHtml() { |
| return StringPrintf(kScriptFormat, |
| Encode("", "ce", "0", kScript, "js").c_str()); |
| } |
| |
| void InitiateScriptRewrite() { |
| rewrite_driver()->StartParse(kTestDomain); |
| rewrite_driver()->ParseText(StringPrintf(kScriptFormat, kScript)); |
| } |
| |
| void RewriteScriptToWarmTheCache() { |
| // Cache-extend a simple JS file. Then slow down the metadata-cache |
| // lookup so that the Flush takes non-zero time. |
| SetResponseWithDefaultHeaders("x.js", kContentTypeJavascript, "x=0", 100); |
| |
| // First, rewrite the HTML with no cache delays. |
| InitiateScriptRewrite(); |
| rewrite_driver()->FinishParse(); |
| StringPieceVector flush_messages; |
| ExtractFlushMessagesFromOutput(OptScriptHtml(), &flush_messages); |
| ASSERT_EQ(1, flush_messages.size()); |
| EXPECT_HAS_SUBSTR( |
| DebugFilter::FormatEndDocumentMessage(0, 0, 0, 0, 0, false, StringSet(), |
| ExpectedDisabledFilters()), |
| flush_messages[0]); |
| EXPECT_HAS_SUBSTR("db\tDebug", flush_messages[0]); |
| |
| // Clear the output buffer as the bytes would otherwise accumulate. |
| output_buffer_.clear(); |
| } |
| |
| int64 InjectCacheDelay() { |
| // Now rewrite the image but make the cache take non-zero time so we measure |
| // elapsed time for the Flush. We stay within the deadline. |
| const int64 deadline_us = rewrite_driver()->rewrite_deadline_ms() * |
| Timer::kMsUs; |
| const int64 delay_us = deadline_us / 3; |
| SetCacheDelayUs(delay_us); |
| return delay_us; |
| } |
| |
| const StringVector& ExpectedDisabledFilters() { |
| return expected_dynamically_disabled_filters_; |
| } |
| |
| private: |
| StringVector expected_dynamically_disabled_filters_; |
| }; |
| |
| // Tests a simple flow for a parse with two intervening flushes and delays. |
| // Note that our "HTML" is just "<token>", so that we can easily split the |
| // output and examine each flush-buffer individually. |
| TEST_F(DebugFilterTest, TwoFlushes) { |
| StringPieceVector flush_messages; |
| ParseAndMaybeFlushTwice(true, &flush_messages); |
| |
| // Note that we get no parse-time or flush time in this test. I don't know |
| // how to inject parse-time as we have no mock-time-advancement mechanism in |
| // the parser flow. We'll test that we can count flush-time in the test |
| // below. What we measure in this test is elapsed time, and idle time |
| // in between the flushes. |
| // |
| // There are just two flushes but we get 3 flush messages, to |
| // separately account for the 3 chunks of text before, between, and |
| // after the flushes, plus one EndOfDocument message. |
| ASSERT_EQ(4, flush_messages.size()); |
| EXPECT_EQ(DebugFilter::FormatFlushMessage(11, 0, 0, 11), |
| flush_messages[0]); |
| EXPECT_EQ(DebugFilter::FormatFlushMessage(1111, 0, 0, 1100), |
| flush_messages[1]); |
| EXPECT_EQ(DebugFilter::FormatFlushMessage(111111, 0, 0, 110000), |
| flush_messages[2]); |
| EXPECT_HAS_SUBSTR(DebugFilter::FormatEndDocumentMessage( |
| 111111, 0, 0, 111111, 2, false, StringSet(), |
| ExpectedDisabledFilters()), |
| flush_messages[3]); |
| } |
| |
| // This is the same exact test, except that Flush is not called; despite |
| // the elapsed time between parse chunks. The EndDocument message will |
| // be the same, but there will be no Flush messages; not even one at the |
| // end. |
| TEST_F(DebugFilterTest, ZeroFlushes) { |
| StringPieceVector flush_messages; |
| ParseAndMaybeFlushTwice(false, &flush_messages); |
| |
| // The totals are identical to DebugFilterTest.TwoFlushes, but there are |
| // no Flush messages (not even 1 at the end), and the flush-count is 0 rather |
| // than 2. |
| ASSERT_EQ(1, flush_messages.size()); |
| EXPECT_HAS_SUBSTR(DebugFilter::FormatEndDocumentMessage( |
| 111111, 0, 0, 111111, 0, false, StringSet(), |
| ExpectedDisabledFilters()), |
| flush_messages[0]); |
| } |
| |
| TEST_F(DebugFilterTest, CheckFiltersAndOptions) { |
| StringPieceVector flush_messages; |
| ParseAndMaybeFlushTwice(false, &flush_messages); |
| ASSERT_EQ(1, flush_messages.size()); |
| EXPECT_HAS_SUBSTR("mod_pagespeed on", flush_messages[0]); |
| EXPECT_HAS_SUBSTR("Filters:", flush_messages[0]); |
| EXPECT_HAS_SUBSTR("Options:", flush_messages[0]); |
| } |
| |
| TEST_F(DebugFilterTest, FlushWithDelayedCache) { |
| RewriteScriptToWarmTheCache(); |
| int64 delay_us = InjectCacheDelay(); |
| InitiateScriptRewrite(); |
| |
| // Flush before finishing the parse. The delay is accounted for in the |
| // first Flush, and there will be a second Flush which won't do anything, |
| // followed by the summary data for the rewrite at EndDocument. |
| rewrite_driver()->Flush(); |
| rewrite_driver()->FinishParse(); |
| StringPieceVector flush_messages; |
| ExtractFlushMessagesFromOutput(OptScriptHtml(), &flush_messages); |
| ASSERT_EQ(3, flush_messages.size()); |
| EXPECT_STREQ(DebugFilter::FormatFlushMessage(0, 0, delay_us, 0), |
| flush_messages[0]); |
| EXPECT_STREQ(DebugFilter::FormatFlushMessage(delay_us, 0, 0, 0), |
| flush_messages[1]); |
| EXPECT_HAS_SUBSTR(DebugFilter::FormatEndDocumentMessage( |
| delay_us, 0, delay_us, 0, 1, false, StringSet(), |
| ExpectedDisabledFilters()), |
| flush_messages[2]); |
| } |
| |
| TEST_F(DebugFilterTest, EndWithDelayedCache) { |
| RewriteScriptToWarmTheCache(); |
| int64 delay_us = InjectCacheDelay(); |
| InitiateScriptRewrite(); |
| |
| // Finish the parse immediately, which causes an implicit Flush. However |
| // since there's only one, the report is dropped as everything is in the |
| // EndDocument. |
| rewrite_driver()->FinishParse(); |
| StringPieceVector flush_messages; |
| ExtractFlushMessagesFromOutput(OptScriptHtml(), &flush_messages); |
| ASSERT_EQ(1, flush_messages.size()); |
| EXPECT_HAS_SUBSTR( |
| DebugFilter::FormatEndDocumentMessage( |
| 0, 0, delay_us, 0, 0, false, StringSet(), ExpectedDisabledFilters()), |
| flush_messages[0]); |
| } |
| |
| TEST_F(DebugFilterTest, FlushInStyleTag) { |
| // Verify that flush comments do not get insert in the middle of a literal tag |
| // (style or script) and instead are buffered until the end of that element. |
| const char kStyleStartTag[] = "<style>"; |
| const char kStyleEndTag[] = "</style>"; |
| const char kCss1[] = ".a { color:red; }"; |
| const char kCss2[] = ".b { color:blue; }"; |
| rewrite_driver()->StartParse(kTestDomain); |
| AdvanceTimeUs(1); |
| rewrite_driver()->ParseText(kStyleStartTag); |
| rewrite_driver()->ParseText(kCss1); |
| AdvanceTimeUs(10); // 11us elapsed so far. |
| rewrite_driver()->Flush(); |
| AdvanceTimeUs(10); // 21us elapsed so far. |
| rewrite_driver()->ParseText(kCss2); |
| AdvanceTimeUs(10); // 31us elapsed so far. |
| rewrite_driver()->Flush(); |
| AdvanceTimeUs(10); // 41us elapsed so far. |
| rewrite_driver()->ParseText(kStyleEndTag); |
| AdvanceTimeUs(10); // 51us elapsed so far. |
| rewrite_driver()->FinishParse(); |
| EXPECT_HAS_SUBSTR( |
| StrCat( |
| StrCat("<!--", DebugFilter::FormatFlushMessage(11, 0, 0, 11), "-->"), |
| kStyleStartTag, kCss1, kCss2, kStyleEndTag, |
| StrCat("<!--", DebugFilter::FormatFlushMessage(31, 0, 0, 20), "-->"), |
| StrCat("<!--", DebugFilter::FormatFlushMessage(51, 0, 0, 20), "-->")), |
| output_buffer_); |
| EXPECT_HAS_SUBSTR(StrCat(DebugFilter::FormatEndDocumentMessage( |
| 51, 0, 0, 51, 2, false, StringSet(), |
| ExpectedDisabledFilters()), |
| "-->"), |
| output_buffer_); |
| } |
| |
| class DebugFilterWithCriticalImagesTest : public RewriteTestBase { |
| protected: |
| virtual void SetUp() { |
| RewriteTestBase::SetUp(); |
| options()->EnableFilter(RewriteOptions::kDebug); |
| options()->EnableFilter(RewriteOptions::kExtendCacheScripts); |
| options()->EnableFilter(RewriteOptions::kLazyloadImages); |
| rewrite_driver()->AddFilters(); |
| factory()->set_use_beacon_results_in_filters(true); |
| SetupWriter(); |
| } |
| }; |
| |
| TEST_F(DebugFilterWithCriticalImagesTest, CriticalImageMessage) { |
| MockCriticalImagesFinder* finder = new MockCriticalImagesFinder(statistics()); |
| server_context()->set_critical_images_finder(finder); |
| StringSet* critical_images = new StringSet; |
| GoogleString img_url = StrCat(kTestDomain, "a.jpg"); |
| critical_images->insert(img_url); |
| finder->set_critical_images(critical_images); |
| |
| GoogleString input_html = |
| "<img src=\"a.jpg\">" |
| "<img src=\"b.jpg\">"; |
| |
| ParseUrl(kTestDomain, input_html); |
| EXPECT_HAS_SUBSTR(StrCat("Critical Images:\n\t", img_url), output_buffer_); |
| EXPECT_HAS_SUBSTR_NE(StrCat(kTestDomain, "b.jpg"), output_buffer_); |
| } |
| |
| TEST_F(DebugFilterWithCriticalImagesTest, CriticalImageMessageBlankSrc) { |
| // Make sure we don't crash with null or unparseable img src. |
| MockCriticalImagesFinder* finder = new MockCriticalImagesFinder(statistics()); |
| server_context()->set_critical_images_finder(finder); |
| StringSet* critical_images = new StringSet; |
| finder->set_critical_images(critical_images); |
| |
| GoogleString input_html = "<img src>"; |
| ParseUrl(kTestDomain, input_html); |
| } |
| |
| class DebugFilterNoOtherFiltersTest : public DebugFilterTest { |
| protected: |
| virtual void SetUp() { |
| RewriteTestBase::SetUp(); |
| options()->set_support_noscript_enabled(false); |
| options()->EnableFilter(RewriteOptions::kDebug); |
| } |
| |
| virtual void FinishSetup() { |
| rewrite_driver()->AddFilters(); |
| SetupWriter(); |
| } |
| }; |
| |
| class DisabledFilter : public EmptyHtmlFilter { |
| public: |
| explicit DisabledFilter(const GoogleString& name) : name_(name) {} |
| |
| virtual void DetermineEnabled(GoogleString* disabled_reason) { |
| set_is_enabled(false); |
| } |
| |
| virtual const char* Name() const { return name_.c_str(); } |
| |
| private: |
| GoogleString name_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DisabledFilter); |
| }; |
| |
| TEST_F(DebugFilterNoOtherFiltersTest, NoDisabledFiltersTest) { |
| FinishSetup(); |
| |
| Parse("no_disabled_filters", "<!-- Empty body -->"); |
| EXPECT_HAS_SUBSTR("No filters were disabled", output_buffer_); |
| EXPECT_HAS_SUBSTR_NE("The following filters were disabled:", output_buffer_); |
| } |
| |
| TEST_F(DebugFilterNoOtherFiltersTest, DisabledFilterTest) { |
| DisableTestFilter filter1("disabled_filter_1", false, ""); |
| rewrite_driver()->AddFilter(&filter1); |
| |
| DisableTestFilter filter2("disabled_filter_2", false, "Reasons"); |
| rewrite_driver()->AddFilter(&filter2); |
| |
| DisableTestFilter filter3("disabled_filter_3", false, ""); |
| rewrite_driver()->AddFilter(&filter3); |
| |
| FinishSetup(); |
| |
| Parse("disabled_filters", "<!-- Empty body -->"); |
| EXPECT_HAS_SUBSTR_NE("No filters were disabled", output_buffer_); |
| EXPECT_HAS_SUBSTR( |
| "The following filters were disabled for this request:\n" |
| "\tdisabled_filter_1\n" |
| "\tdisabled_filter_2: Reasons\n" |
| "\tdisabled_filter_3\n", |
| output_buffer_); |
| } |
| |
| } // namespace |
| |
| } // namespace net_instaweb |