blob: 19ba920e8cee34761073afa5986c1b5b76e580b7 [file] [log] [blame]
/*
* 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