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