blob: 1178d55c4cb378def2fe418f7b3b72f416d80865 [file] [log] [blame]
/*
* Copyright 2011 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: sligocki@google.com (Shawn Ligocki)
#include "net/instaweb/rewriter/public/css_rewrite_test_base.h"
#include "base/logging.h"
#include "net/instaweb/rewriter/public/css_url_extractor.h"
#include "net/instaweb/rewriter/public/resource_namer.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/hasher.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/base/wildcard.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/response_headers.h"
namespace net_instaweb {
CssRewriteTestBase::~CssRewriteTestBase() {}
// Check that inline CSS gets rewritten correctly.
bool CssRewriteTestBase::ValidateRewriteInlineCss(
StringPiece id, StringPiece css_input, StringPiece expected_css_output,
int flags) {
static const char prefix[] =
"<head>\n"
" <title>Example style outline</title>\n"
" <!-- Style starts here -->\n"
" <style type='text/css'>";
static const char suffix1[] = "</style>";
static const char suffix2[] =
"\n"
" <!-- Style ends here -->\n"
"</head>";
GoogleString html_url = StrCat(kTestDomain, id, ".html");
CheckFlags(flags);
GoogleString html_input = StrCat(prefix, css_input, suffix1, suffix2);
GoogleString html_output = StrCat(prefix, expected_css_output, suffix1,
DebugMessage(html_url), suffix2);
return ValidateWithStats(id, html_input, html_output,
css_input, expected_css_output, flags);
}
void CssRewriteTestBase::ResetStats() {
num_blocks_rewritten_->Clear();
num_fallback_rewrites_->Clear();
num_parse_failures_->Clear();
num_rewrites_dropped_->Clear();
total_bytes_saved_->Clear();
total_original_bytes_->Clear();
num_uses_->Clear();
num_flatten_imports_charset_mismatch_->Clear();
num_flatten_imports_invalid_url_->Clear();
num_flatten_imports_limit_exceeded_->Clear();
num_flatten_imports_minify_failed_->Clear();
num_flatten_imports_recursion_->Clear();
num_flatten_imports_complex_queries_->Clear();
}
bool CssRewriteTestBase::ValidateWithStats(
StringPiece id,
StringPiece html_input, StringPiece expected_html_output,
StringPiece css_input, StringPiece expected_css_output,
int flags) {
ResetStats();
// Rewrite
bool success = ValidateExpected(id, html_input, expected_html_output);
// Check stats
if (success && !FlagSet(flags, kNoStatCheck)) {
if (FlagSet(flags, kExpectSuccess)) {
EXPECT_EQ(1, num_blocks_rewritten_->Get()) << css_input;
EXPECT_EQ(0, num_fallback_rewrites_->Get()) << css_input;
EXPECT_EQ(0, num_parse_failures_->Get()) << css_input;
EXPECT_EQ(0, num_rewrites_dropped_->Get()) << css_input;
EXPECT_EQ(static_cast<int>(css_input.size()) -
static_cast<int>(expected_css_output.size()),
total_bytes_saved_->Get()) << css_input;
EXPECT_EQ(css_input.size(), total_original_bytes_->Get()) << css_input;
EXPECT_EQ(1, num_uses_->Get()) << css_input;
} else if (FlagSet(flags, kExpectCached)) {
EXPECT_EQ(0, num_blocks_rewritten_->Get()) << css_input;
EXPECT_EQ(0, num_fallback_rewrites_->Get()) << css_input;
EXPECT_EQ(0, num_parse_failures_->Get()) << css_input;
EXPECT_EQ(0, total_bytes_saved_->Get()) << css_input;
EXPECT_EQ(0, total_original_bytes_->Get()) << css_input;
EXPECT_EQ(1, num_uses_->Get()) << css_input; // The only non-zero value.
} else if (FlagSet(flags, kExpectNoChange)) {
EXPECT_EQ(0, num_blocks_rewritten_->Get()) << css_input;
EXPECT_EQ(0, num_fallback_rewrites_->Get()) << css_input;
EXPECT_EQ(0, num_parse_failures_->Get()) << css_input;
// TODO(sligocki): Test num_rewrites_dropped_. Currently a couple tests
// have kExpectNoChange, but fail at a different place in the code, so
// they do not trigger the num_rewrites_dropped_ variable.
// EXPECT_EQ(1, num_rewrites_dropped_->Get()) << css_input;
EXPECT_EQ(0, total_bytes_saved_->Get()) << css_input;
EXPECT_EQ(0, total_original_bytes_->Get()) << css_input;
EXPECT_EQ(0, num_uses_->Get()) << css_input;
} else if (FlagSet(flags, kExpectFallback)) {
EXPECT_EQ(0, num_blocks_rewritten_->Get()) << css_input;
EXPECT_EQ(1, num_fallback_rewrites_->Get()) << css_input;
EXPECT_EQ(1, num_parse_failures_->Get()) << css_input;
EXPECT_EQ(0, num_rewrites_dropped_->Get()) << css_input;
EXPECT_EQ(0, total_bytes_saved_->Get()) << css_input;
EXPECT_EQ(0, total_original_bytes_->Get()) << css_input;
EXPECT_EQ(1, num_uses_->Get()) << css_input;
} else {
CHECK(FlagSet(flags, kExpectFailure));
EXPECT_EQ(0, num_blocks_rewritten_->Get()) << css_input;
EXPECT_EQ(0, num_fallback_rewrites_->Get()) << css_input;
EXPECT_EQ(1, num_parse_failures_->Get()) << css_input;
EXPECT_EQ(0, num_rewrites_dropped_->Get()) << css_input;
EXPECT_EQ(0, total_bytes_saved_->Get()) << css_input;
EXPECT_EQ(0, total_original_bytes_->Get()) << css_input;
EXPECT_EQ(0, num_uses_->Get()) << css_input;
}
}
// Check each of the import flattening statistics. Since each of these
// is controlled individually they are not gated by kNoStatCheck above,
// although if the results were fetched from the cache the flattener
// doesn't count these as new errors so skip this check in that case.
if (!FlagSet(flags, kExpectCached)) {
EXPECT_EQ(FlagSet(flags, kFlattenImportsCharsetMismatch) ? 1 : 0,
num_flatten_imports_charset_mismatch_->Get()) << css_input;
EXPECT_EQ(FlagSet(flags, kFlattenImportsInvalidUrl) ? 1 : 0,
num_flatten_imports_invalid_url_->Get()) << css_input;
EXPECT_EQ(FlagSet(flags, kFlattenImportsLimitExceeded) ? 1 : 0,
num_flatten_imports_limit_exceeded_->Get()) << css_input;
EXPECT_EQ(FlagSet(flags, kFlattenImportsMinifyFailed) ? 1 : 0,
num_flatten_imports_minify_failed_->Get()) << css_input;
EXPECT_EQ(FlagSet(flags, kFlattenImportsRecursion) ? 1 : 0,
num_flatten_imports_recursion_->Get()) << css_input;
EXPECT_EQ(FlagSet(flags, kFlattenImportsComplexQueries) ? 1 : 0,
num_flatten_imports_complex_queries_->Get()) << css_input;
}
// TODO(sligocki): This success value does not reflect failures in the
// stats checks. Perhaps it should.
return success;
}
void CssRewriteTestBase::GetNamerForCss(StringPiece leaf_name,
StringPiece expected_css_output,
ResourceNamer* namer) {
namer->set_id(RewriteOptions::kCssFilterId);
namer->set_hash(hasher()->Hash(expected_css_output));
namer->set_ext("css");
namer->set_name(leaf_name);
}
GoogleString CssRewriteTestBase::ExpectedUrlForNamer(
const ResourceNamer& namer) {
return Encode("", namer.id(), namer.hash(), namer.name(), namer.ext());
}
GoogleString CssRewriteTestBase::ExpectedUrlForCss(
StringPiece id, StringPiece expected_css_output) {
ResourceNamer namer;
GetNamerForCss(StrCat(id, ".css"), expected_css_output, &namer);
return ExpectedUrlForNamer(namer);
}
GoogleString CssRewriteTestBase::MakeHtmlWithExternalCssLink(
StringPiece css_url, int flags, bool insert_debug_message) {
GoogleString link_extras("");
if (FlagSet(flags, kLinkCharsetIsUTF8)) {
link_extras = " charset='utf-8'";
}
if (FlagSet(flags, kLinkScreenMedia) && FlagSet(flags, kLinkPrintMedia)) {
StrAppend(&link_extras, " media='screen,print'");
} else if (FlagSet(flags, kLinkScreenMedia)) {
StrAppend(&link_extras, " media='screen'");
} else if (FlagSet(flags, kLinkPrintMedia)) {
StrAppend(&link_extras, " media='print'");
}
GoogleString meta_tag("");
if (FlagSet(flags, kMetaCharsetUTF8)) {
StrAppend(&meta_tag, " <meta charset=\"utf-8\">");
}
if (FlagSet(flags, kMetaCharsetISO88591)) {
StrAppend(&meta_tag, " <meta charset=ISO-8859-1>");
}
if (FlagSet(flags, kMetaHttpEquiv)) {
StrAppend(&meta_tag,
" <meta http-equiv=\"Content-Type\" "
"content=\"text/html; charset=UTF-8\">");
}
if (FlagSet(flags, kMetaHttpEquivUnquoted)) {
// Same as the previous one but content's value isn't quoted!
StrAppend(&meta_tag,
" <meta http-equiv=\"Content-Type\" "
"content=text/html; charset=ISO-8859-1>");
}
// This helper method is used to produce both input and expected_output HTML.
// For input HTML we do not want to insert a debug message.
// For expected_output HTML we do.
GoogleString debug_message;
if (insert_debug_message) {
debug_message = DebugMessage(css_url);
}
return StringPrintf("<head>\n"
" <title>Example style outline</title>\n"
"%s"
" <!-- Style starts here -->\n"
" <link rel='stylesheet' type='text/css' href='%.*s'%s>"
"%s\n"
" <!-- Style ends here -->\n"
"</head>",
meta_tag.c_str(),
static_cast<int>(css_url.size()), css_url.data(),
link_extras.c_str(), debug_message.c_str());
}
GoogleString CssRewriteTestBase::MakeIndentedCssWithImage(
StringPiece image_url) {
return StrCat("body {\n"
" background-image: url(", image_url, ");\n"
"}\n");
}
GoogleString CssRewriteTestBase::MakeMinifiedCssWithImage(
StringPiece image_url) {
return StrCat("body{background-image:url(", image_url, ")}");
}
GoogleString CssRewriteTestBase::ExtractCssBackgroundImage(
StringPiece in_css) {
const char css_template[] = "*{background-image:url(*)}*";
GoogleString image_url;
if (!Wildcard(css_template).Match(in_css)) {
return image_url;
}
StringVector extracted_urls;
CssUrlExtractor url_extractor;
url_extractor.ExtractUrl(in_css, &extracted_urls);
// Although the CssUrlExtractor returns a StringVector, we expect only one
// url in the input string.
if (extracted_urls.size() == 1) {
image_url = extracted_urls[0];
}
return image_url;
}
// Check that external CSS gets rewritten correctly.
void CssRewriteTestBase::ValidateRewriteExternalCssUrl(
StringPiece id, StringPiece css_url,
StringPiece css_input, StringPiece expected_css_output, int flags) {
CheckFlags(flags);
// Set input file.
if (!FlagSet(flags, kNoClearFetcher)) {
ClearFetcherResponses();
}
SetResponseWithDefaultHeaders(css_url, kContentTypeCss, css_input, 300);
GoogleString html_input = MakeHtmlWithExternalCssLink(css_url, flags, false);
// Do we expect the URL to be rewritten?
bool rewrite_url = (FlagSet(flags, kExpectSuccess) ||
FlagSet(flags, kExpectCached) ||
FlagSet(flags, kExpectFallback));
GoogleString expected_new_url;
if (rewrite_url) {
ResourceNamer namer;
GoogleUrl css_gurl(css_url);
GetNamerForCss(css_gurl.LeafWithQuery(), expected_css_output, &namer);
expected_new_url = Encode(css_gurl.AllExceptLeaf(), namer.id(),
namer.hash(), namer.name(), namer.ext());
} else {
css_url.CopyToString(&expected_new_url);
}
GoogleString expected_html_output =
MakeHtmlWithExternalCssLink(expected_new_url, flags, true);
ValidateWithStats(id, html_input, expected_html_output,
css_input, expected_css_output, flags);
if (rewrite_url) {
// Check the new output resource.
GoogleString actual_output;
// TODO(sligocki): This will only work with mock_hasher.
ResponseHeaders headers_out;
EXPECT_TRUE(FetchResourceUrl(expected_new_url, &actual_output,
&headers_out)) << css_url;
EXPECT_EQ(expected_css_output, actual_output) << css_url;
// Non-fallback CSS should have very long caching headers
if (!FlagSet(flags, kExpectFallback)) {
EXPECT_TRUE(headers_out.IsProxyCacheable());
EXPECT_LE(Timer::kYearMs, headers_out.cache_ttl_ms());
}
// Serve from new context.
if (!FlagSet(flags, kNoOtherContexts)) {
ServeResourceFromManyContexts(expected_new_url, expected_css_output);
}
}
}
// Helper to test for how we handle trailing junk
void CssRewriteTestBase::TestCorruptUrl(const char* new_suffix) {
DebugWithMessage("");
const char kInput[] = " div { } ";
const char kOutput[] = "div{}";
// Compute normal version
ValidateRewriteExternalCss("rep", kInput, kOutput, kExpectSuccess);
// Fetch with messed up extension
GoogleString css_url = ExpectedUrlForCss("rep", kOutput);
ASSERT_TRUE(StringCaseEndsWith(css_url, ".css"));
GoogleString munged_url =
ChangeSuffix(css_url, false /*replace*/, ".css", new_suffix);
GoogleString output;
EXPECT_TRUE(FetchResourceUrl(StrCat(kTestDomain, munged_url), &output));
// Now see that output is correct
ValidateRewriteExternalCss(
"rep", kInput, kOutput,
kExpectSuccess | kNoClearFetcher | kNoStatCheck);
}
} // namespace net_instaweb