blob: 694091f869b47c17ac585e1999a784c5887904e5 [file] [log] [blame]
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Author: morlovich@google.com (Maksim Orlovich)
#include "net/instaweb/rewriter/public/css_summarizer_base.h"
#include "net/instaweb/rewriter/public/css_minify.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 "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/string_writer.h"
#include "pagespeed/kernel/html/html_name.h"
#include "pagespeed/kernel/html/html_node.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/semantic_type.h"
namespace net_instaweb {
namespace {
const char kExpectedResult[] =
"OK/*{display:|OK/div{displa/rel=stylesheet|ParseOrCloseStyleTagError//"
"rel=stylesheet|ParseOrCloseStyleTagError//rel=stylesheet|"
"ParseOrCloseStyleTagError//rel=stylesheet|FetchError//rel=stylesheet|"
"ResourceError/|";
// Extracts first 10 characters of minified form of every stylesheet.
class MinifyExcerptFilter : public CssSummarizerBase {
public:
explicit MinifyExcerptFilter(RewriteDriver* driver)
: CssSummarizerBase(driver),
render_summaries_in_place_(false),
will_not_render_summaries_in_place_(false),
include_base_(false) {}
virtual const char* Name() const { return "Minify10"; }
virtual const char* id() const { return "csr"; }
virtual bool MustSummarize(HtmlElement* element) const {
return (!element->FindAttribute(HtmlName::kDataPagespeedNoDefer));
}
virtual void Summarize(Css::Stylesheet* stylesheet,
GoogleString* out) const {
StringWriter write_out(out);
CssMinify::Stylesheet(*stylesheet, &write_out, driver()->message_handler());
if (out->length() > 10) {
out->resize(10);
}
}
GoogleString EncodeState(SummaryState state) {
switch (state) {
case kSummaryOk:
return "OK";
case kSummaryStillPending:
return "Pending";
case kSummaryCssParseError:
return "ParseOrCloseStyleTagError";
case kSummaryResourceCreationFailed:
return "ResourceError";
case kSummaryInputUnavailable:
return "FetchError";
case kSummarySlotRemoved:
return "SlotRemoved";
}
}
virtual void RenderSummary(int pos,
HtmlElement* element,
HtmlCharactersNode* char_node,
bool* is_element_deleted) {
if (!render_summaries_in_place_) {
return;
}
const SummaryInfo& summary = GetSummaryForStyle(pos);
if (char_node != NULL) {
*char_node->mutable_contents() = summary.data;
} else {
// Replace link with style. Note: real one should also keep media,
// test code does not have to.
HtmlElement* style_element = driver()->NewElement(NULL, HtmlName::kStyle);
driver()->InsertNodeBeforeNode(element, style_element);
HtmlCharactersNode* content =
driver()->NewCharactersNode(style_element, summary.data);
driver()->AppendChild(style_element, content);
EXPECT_TRUE(driver()->DeleteNode(element));
*is_element_deleted = true;
}
}
virtual void WillNotRenderSummary(int pos,
HtmlElement* element,
HtmlCharactersNode* char_node,
bool* is_element_deleted) {
// Note that these should not normally mutate the DOM, we only
// get away with this because the tests we use this in don't really do
// any flushing.
if (!will_not_render_summaries_in_place_) {
return;
}
const SummaryInfo& sum = GetSummaryForStyle(pos);
GoogleString annotation = StrCat("WillNotRender:", IntegerToString(pos),
" --- ", EncodeState(sum.state));
driver()->InsertNodeBeforeNode(
element, driver()->NewCommentNode(NULL, annotation));
}
virtual void SummariesDone() {
result_.clear();
for (int i = 0; i < NumStyles(); ++i) {
const SummaryInfo& sum = GetSummaryForStyle(i);
StrAppend(&result_, EncodeState(sum.state), "/", sum.data,
(sum.is_inside_noscript ? "/noscr" : ""),
(sum.rel.empty() ? "" : StrCat("/rel=", sum.rel)),
(include_base_ ? StrCat("/base=", sum.base) : ""),
"|");
}
InsertNodeAtBodyEnd(driver()->NewCommentNode(NULL, result_));
}
const GoogleString& result() { return result_; }
// Whether we should note the RenderSummary calls in place.
void set_render_summaries_in_place(bool x) {
render_summaries_in_place_ = x;
}
// Whether we should note the WillNotRenderSummary calls in place.
void set_will_not_render_summaries_in_place(bool x) {
will_not_render_summaries_in_place_ = x;
}
// Whether we should include the base URL in the output string we compute.
void set_include_base(bool x) {
include_base_ = x;
}
private:
GoogleString result_;
bool render_summaries_in_place_;
bool will_not_render_summaries_in_place_;
bool include_base_;
};
class CssSummarizerBaseTest : public RewriteTestBase {
public:
CssSummarizerBaseTest()
: head_(StrCat("<html>\n",
"<style>* {display: none; }</style>",
CssLinkHref("a.css"), // ok
CssLinkHref("b.css"), // parse error
CssLinkHref("c.css"), // parse error due to bad URL
CssLinkHref("close_style_tag.css"), // closing style tag
CssLinkHref("404.css"), // fetch error
CssLinkHref("http://evil.com/d.css"))) { }
virtual ~CssSummarizerBaseTest() { }
protected:
virtual void SetUp() {
RewriteTestBase::SetUp();
rewrite_driver()->AddFilters();
filter_ = new MinifyExcerptFilter(rewrite_driver());
rewrite_driver()->AppendOwnedPreRenderFilter(filter_);
server_context()->ComputeSignature(options());
// Valid CSS.
SetResponseWithDefaultHeaders("a.css", kContentTypeCss,
"div { display: block; }", 100);
// Parse error.
SetResponseWithDefaultHeaders("b.css", kContentTypeCss,
"div { ", 100);
SetResponseWithDefaultHeaders(
"c.css", kContentTypeCss,
".z{background-image:url(\"</style>\");", 100);
// Contents that include a closing style tag.
SetResponseWithDefaultHeaders("close_style_tag.css",
kContentTypeCss,
".x </style> {color: white }", 100);
// Permit testing a 404.
SetFetchFailOnUnexpected(false);
// An inline div? evil indeed.
SetResponseWithDefaultHeaders("http://evil.com/d.css", kContentTypeCss,
"div { display: inline; }", 100);
}
void StartTest(StringPiece name, StringPiece pre_comment) {
SetupWriter();
GoogleString url = StrCat(kTestDomain, name);
rewrite_driver()->StartParse(url);
rewrite_driver()->ParseText(head_);
rewrite_driver()->ParseText(pre_comment);
}
const GoogleString FinishTest(
StringPiece pre_comment, StringPiece post_comment) {
const GoogleString expected_html = StrCat(
head_, pre_comment, "<!--", kExpectedResult, "-->", post_comment);
rewrite_driver()->ParseText(post_comment);
rewrite_driver()->FinishParse();
return expected_html;
}
const GoogleString FullTest(
StringPiece name, StringPiece pre_comment, StringPiece post_comment) {
StartTest(name, pre_comment);
return FinishTest(pre_comment, post_comment);
}
const GoogleString FlushTest(
StringPiece name, StringPiece pre_flush,
StringPiece pre_comment, StringPiece post_comment) {
StartTest(name, pre_flush);
rewrite_driver()->Flush();
rewrite_driver()->ParseText(pre_comment);
GoogleString full_pre_comment = StrCat(pre_flush, pre_comment);
return FinishTest(full_pre_comment, post_comment);
}
void VerifyUnauthNotRendered(StringPiece summary_comment) {
FullTest("will_not_render", "", "");
EXPECT_STREQ(
StrCat("<html>\n"
"<style>* {display: none; }</style>",
CssLinkHref("a.css"),
StrCat("<!--WillNotRender:2 --- ParseOrCloseStyleTagError-->",
CssLinkHref("b.css"),
"<!--WillNotRender:3 --- ParseOrCloseStyleTagError-->",
CssLinkHref("c.css")),
StrCat("<!--WillNotRender:4 --- ParseOrCloseStyleTagError-->",
CssLinkHref("close_style_tag.css"),
"<!--WillNotRender:5 --- FetchError-->",
CssLinkHref("404.css")),
StrCat("<!--WillNotRender:6 --- ResourceError-->",
CssLinkHref("http://evil.com/d.css")),
summary_comment,
StrCat("<!--", kExpectedResult, "-->")),
output_buffer_);
}
MinifyExcerptFilter* filter_; // owned by the driver;
const GoogleString head_;
private:
DISALLOW_COPY_AND_ASSIGN(CssSummarizerBaseTest);
};
TEST_F(CssSummarizerBaseTest, BasicOperation) {
GoogleString expected =
FullTest("basic", "<body> <p>some content</p> ", "</body></html>");
EXPECT_STREQ(expected, output_buffer_);
EXPECT_STREQ(kExpectedResult, filter_->result());
// Re-test to make sure we behave OK with the result cached.
expected = FullTest("basic", "<body> <p>some content</p> ", "</body></html>");
EXPECT_STREQ(expected, output_buffer_);
EXPECT_STREQ(kExpectedResult, filter_->result());
}
TEST_F(CssSummarizerBaseTest, RenderSummary) {
filter_->set_render_summaries_in_place(true);
Parse("link", StrCat(CssLinkHref("a.css"),
"<style>* { background: blue; }</style>"));
EXPECT_STREQ("<html>\n<style>div{displa</style><style>*{backgrou</style>"
"<!--OK/div{displa/rel=stylesheet|"
"OK/*{backgrou|--></html>", output_buffer_);
}
TEST_F(CssSummarizerBaseTest, WillNotRenderSummary) {
filter_->set_will_not_render_summaries_in_place(true);
VerifyUnauthNotRendered(/* summary_comment= */ "");
}
TEST_F(CssSummarizerBaseTest, WillNotRenderSummaryWithUnauthEnabled) {
filter_->set_will_not_render_summaries_in_place(true);
options()->ClearSignatureForTesting();
options()->AddInlineUnauthorizedResourceType(semantic_type::kStylesheet);
server_context()->ComputeSignature(options());
VerifyUnauthNotRendered(/* summary_comment= */ "");
}
TEST_F(CssSummarizerBaseTest, WillNotRenderSummaryWithDebug) {
filter_->set_will_not_render_summaries_in_place(true);
options()->ClearSignatureForTesting();
options()->EnableFilter(RewriteOptions::kDebug);
server_context()->ComputeSignature(options());
const char kDebugSummary[] =
"<!--Summary computation status for Minify10\n"
"Resource 0 http://test.com/will_not_render:2: Computed OK\n"
"Resource 1 http://test.com/a.css: Computed OK\n"
"Resource 2 http://test.com/b.css: "
"Unrecoverable CSS parse error or resource contains closing style tag\n"
"Resource 3 http://test.com/c.css: "
"Unrecoverable CSS parse error or resource contains closing style tag\n"
"Resource 4 http://test.com/close_style_tag.css: "
"Unrecoverable CSS parse error or resource contains closing style tag\n"
"Resource 5 http://test.com/404.css: "
"Fetch failed or resource not publicly cacheable\n"
"Resource 6 http://evil.com/d.css: Cannot create resource: either its "
"domain is unauthorized and InlineUnauthorizedResources is not enabled, "
"or it cannot be fetched (check the server logs)\n"
"-->";
VerifyUnauthNotRendered(kDebugSummary);
}
TEST_F(CssSummarizerBaseTest, WillNotRenderSummaryWait) {
filter_->set_will_not_render_summaries_in_place(true);
SetupWaitFetcher();
Parse("link", CssLinkHref("a.css"));
EXPECT_STREQ(StrCat("<html>\n",
"<!--WillNotRender:0 --- Pending-->",
CssLinkHref("a.css"),
"</html>"),
output_buffer_);
CallFetcherCallbacks();
}
TEST_F(CssSummarizerBaseTest, Base) {
filter_->set_include_base(true);
GoogleString css =
StrCat(CssLinkHref("a.css"), "<style>*{display:block;}</style>");
Parse("base", css);
EXPECT_STREQ(
StrCat("<html>\n", css,
StrCat("<!--OK/div{displa/rel=stylesheet/base=",
kTestDomain, "a.css"),
StrCat("|OK/*{display:/base=", kTestDomain, "base.html|-->"),
"</html>"),
output_buffer_);
}
TEST_F(CssSummarizerBaseTest, AlternateHandling) {
// CssSummarizerBase itself handles alternate stylesheets, just keeps
// the rel around inside the SummaryInfo
Parse("alternate", "<link rel=\"stylesheet alternate\" href=\"a.css\">");
EXPECT_STREQ("OK/div{displa/rel=stylesheet alternate|", filter_->result());
}
TEST_F(CssSummarizerBaseTest, NoScriptHandling) {
Parse("ns", StrCat(CssLinkHref("a.css"),
"<noscript>", CssLinkHref("a.css"), "</noscript>"));
EXPECT_STREQ("OK/div{displa/rel=stylesheet|"
"OK/div{displa/noscr/rel=stylesheet|",
filter_->result());
}
TEST_F(CssSummarizerBaseTest, IgnoreNonSummarizable) {
filter_->set_render_summaries_in_place(true);
Parse("non-summarizable",
"<style>* { background: blue; }</style>"
"<style data-pagespeed-no-defer>div {display:none;}</style>"
"<style scoped>p {display:none;}</style>"
"<link rel=stylesheet href='b.css' data-pagespeed-no-defer>"
"<link rel=stylesheet href='a.css'>");
EXPECT_STREQ("<html>\n"
"<style>*{backgrou</style>"
"<style data-pagespeed-no-defer>div {display:none;}</style>"
"<style scoped>p {display:none;}</style>"
"<link rel=stylesheet href='b.css' data-pagespeed-no-defer>"
"<style>div{displa</style>"
"<!--OK/*{backgrou|OK/div{displa/rel=stylesheet|--></html>",
output_buffer_);
}
class CssSummarizerBaseWithCombinerFilterTest : public CssSummarizerBaseTest {
protected:
virtual void SetUp() {
options()->EnableFilter(RewriteOptions::kCombineCss);
CssSummarizerBaseTest::SetUp();
SetHtmlMimetype(); // no <link />, just <link>
}
};
TEST_F(CssSummarizerBaseWithCombinerFilterTest, Interaction) {
SetResponseWithDefaultHeaders("a2.css", kContentTypeCss,
"span { display: inline; }", 100);
GoogleString combined_url = Encode("", "cc", "0",
MultiUrl("a.css", "a2.css"), "css");
Parse("with_combine", StrCat(CssLinkHref("a.css"), CssLinkHref("a2.css")));
EXPECT_EQ(StrCat("<html>\n", CssLinkHref(combined_url),
"<!--OK/div{displa/rel=stylesheet|"
"SlotRemoved//rel=stylesheet|--></html>"),
output_buffer_);
}
TEST_F(CssSummarizerBaseWithCombinerFilterTest, InteractionWithFlush) {
// Make sure that SummariesDone is called once only, at the actual end of the
// document, and not for every flush window.
SetResponseWithDefaultHeaders("a2.css", kContentTypeCss,
"span { display: inline; }", 100);
GoogleString combined_url = Encode("", "cc", "0",
MultiUrl("a.css", "a2.css"), "css");
GoogleString css = StrCat(CssLinkHref("a.css"), CssLinkHref("a2.css"));
SetupWriter();
html_parse()->StartParse(StrCat(kTestDomain, "example.html"));
html_parse()->ParseText(css);
html_parse()->Flush();
html_parse()->ParseText(css);
html_parse()->FinishParse();
// Should only see the comment once, since SummariesDone is supposed to be
// called only at document end.
EXPECT_EQ(StrCat(CssLinkHref(combined_url), CssLinkHref(combined_url),
StrCat("<!--",
"OK/div{displa/rel=stylesheet|",
"SlotRemoved//rel=stylesheet|",
"OK/div{displa/rel=stylesheet|",
"SlotRemoved//rel=stylesheet|",
"-->")),
output_buffer_);
}
TEST_F(CssSummarizerBaseWithCombinerFilterTest, BaseAcrossPaths) {
// Make sure base is updated if a previous filter moves a resource across
// directories.
filter_->set_include_base(true);
SetResponseWithDefaultHeaders("b/a2.css", kContentTypeCss,
"span { display: inline; }", 100);
GoogleString combined_url = "b,_a2.css+a.css.pagespeed.cc.0.css";
Parse("base_accross_paths",
StrCat(CssLinkHref("b/a2.css"), CssLinkHref("a.css")));
EXPECT_EQ(StrCat(
"<html>\n", CssLinkHref(combined_url),
"<!--OK/span{displ/rel=stylesheet/base=", kTestDomain, combined_url,
"|SlotRemoved//rel=stylesheet/base=", kTestDomain, "a.css"
"|--></html>"),
output_buffer_);
}
} // namespace
} // namespace net_instaweb