blob: a49696db98b9aa9e02dad6ec8ef03ab93a5207b6 [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: jmarantz@google.com (Joshua Marantz)
// Base-class & helper classes for testing RewriteContext and its
// interaction with various subsystems.
#include "net/instaweb/rewriter/public/rewrite_context_test_base.h"
#include "base/logging.h"
#include "net/instaweb/http/public/http_cache.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/rewriter/cached_result.pb.h"
#include "net/instaweb/rewriter/public/output_resource.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_result.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/stl_util.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/thread/mock_scheduler.h"
namespace net_instaweb {
const char TrimWhitespaceRewriter::kFilterId[] = "tw";
const char TrimWhitespaceSyncFilter::kFilterId[] = "ts";
const char UpperCaseRewriter::kFilterId[] = "uc";
const char NestedFilter::kFilterId[] = "nf";
const char CombiningFilter::kFilterId[] = "cr";
// This is needed to prevent link error due to EXPECT_EQ on this field in
// RewriteContextTest::TrimFetchHashFailedShortTtl.
const int64 RewriteContextTestBase::kLowOriginTtlMs;
TrimWhitespaceRewriter::~TrimWhitespaceRewriter() {
}
bool TrimWhitespaceRewriter::RewriteText(const StringPiece& url,
const StringPiece& in,
GoogleString* out,
ServerContext* server_context) {
LOG(INFO) << "Trimming whitespace.";
++num_rewrites_;
TrimWhitespace(in, out);
return in != *out;
}
HtmlElement::Attribute* TrimWhitespaceRewriter::FindResourceAttribute(
HtmlElement* element) {
if (element->keyword() == HtmlName::kLink) {
return element->FindAttribute(HtmlName::kHref);
}
return NULL;
}
TrimWhitespaceSyncFilter::~TrimWhitespaceSyncFilter() {
}
void TrimWhitespaceSyncFilter::StartElementImpl(HtmlElement* element) {
if (element->keyword() == HtmlName::kLink) {
HtmlElement::Attribute* href = element->FindAttribute(HtmlName::kHref);
if (href != NULL) {
GoogleUrl gurl(driver()->google_url(), href->DecodedValueOrNull());
href->SetValue(StrCat(gurl.Spec(), ".pagespeed.ts.0.css"));
}
}
}
UpperCaseRewriter::~UpperCaseRewriter() {
}
NestedFilter::~NestedFilter() {
}
NestedFilter::Context::~Context() {
STLDeleteElements(&strings_);
}
void NestedFilter::Context::RewriteSingle(
const ResourcePtr& input, const OutputResourcePtr& output) {
++filter_->num_top_rewrites_;
// Assume that this file just has nested CSS URLs one per line,
// which we will rewrite.
StringPieceVector pieces;
SplitStringPieceToVector(input->ExtractUncompressedContents(), "\n", &pieces,
true);
GoogleUrl base(input->url());
if (base.IsWebValid()) {
// Add a new nested multi-slot context.
for (int i = 0, n = pieces.size(); i < n; ++i) {
GoogleUrl url(base, pieces[i]);
if (url.IsWebValid()) {
bool unused;
ResourcePtr resource(Driver()->CreateInputResource(url, &unused));
if (resource.get() != NULL) {
ResourceSlotPtr slot(new NestedSlot(resource));
RewriteContext* nested_context =
filter_->upper_filter()->MakeNestedRewriteContext(this, slot);
AddNestedContext(nested_context);
nested_slots_.push_back(slot);
// Test chaining of a 2nd rewrite on the same slot, if asked.
if (chain_) {
RewriteContext* nested_context2 =
filter_->upper_filter()->MakeNestedRewriteContext(this,
slot);
AddNestedContext(nested_context2);
}
}
}
}
// TODO(jmarantz): start this automatically. This will be easier
// to do once the states are kept more explicitly via a refactor.
StartNestedTasks();
}
}
void NestedFilter::Context::Harvest() {
RewriteResult result = kRewriteFailed;
GoogleString new_content;
if (filter_->check_nested_rewrite_result_) {
for (int i = 0, n = nested_slots_.size(); i < n; ++i) {
EXPECT_EQ(filter_->expected_nested_rewrite_result(),
nested_slots_[i]->was_optimized());
}
}
CHECK_EQ(1, num_slots());
for (int i = 0, n = num_nested(); i < n; ++i) {
CHECK_EQ(1, nested(i)->num_slots());
ResourceSlotPtr slot(nested(i)->slot(0));
ResourcePtr resource(slot->resource());
StrAppend(&new_content, resource->url(), "\n");
}
// Warning: this uses input's content-type for simplicity, but real
// filters should not do that --- see comments in
// CacheExtender::RewriteLoadedResource as to why.
if (Driver()->Write(ResourceVector(1, slot(0)->resource()),
new_content,
slot(0)->resource()->type(),
slot(0)->resource()->charset(),
output(0).get())) {
result = kRewriteOk;
}
RewriteDone(result, 0);
}
void NestedFilter::StartElementImpl(HtmlElement* element) {
HtmlElement::Attribute* attr = element->FindAttribute(HtmlName::kHref);
if (attr != NULL) {
bool unused;
ResourcePtr resource = CreateInputResource(attr->DecodedValueOrNull(),
&unused);
if (resource.get() != NULL) {
ResourceSlotPtr slot(driver()->GetSlot(resource, element, attr));
// This 'new' is paired with a delete in RewriteContext::FinishFetch()
Context* context = new Context(driver(), this, chain_);
context->AddSlot(slot);
driver()->InitiateRewrite(context);
}
}
}
CombiningFilter::CombiningFilter(RewriteDriver* driver,
MockScheduler* scheduler,
int64 rewrite_delay_ms)
: RewriteFilter(driver),
scheduler_(scheduler),
num_rewrites_(0),
num_render_(0),
num_will_not_render_(0),
num_cancel_(0),
rewrite_delay_ms_(rewrite_delay_ms),
rewrite_block_on_(NULL),
rewrite_signal_on_(NULL),
on_the_fly_(false),
optimization_only_(true),
disable_successors_(false) {
ClearStats();
}
CombiningFilter::~CombiningFilter() {
}
CombiningFilter::Context::Context(RewriteDriver* driver,
CombiningFilter* filter,
MockScheduler* scheduler)
: RewriteContext(driver, NULL, NULL),
combiner_(driver, filter),
scheduler_(scheduler),
time_at_start_of_rewrite_us_(scheduler_->timer()->NowUs()),
filter_(filter) {
combiner_.set_prefix(filter_->prefix_);
}
bool CombiningFilter::Context::Partition(OutputPartitions* partitions,
OutputResourceVector* outputs) {
MessageHandler* handler = Driver()->message_handler();
CachedResult* partition = partitions->add_partition();
for (int i = 0, n = num_slots(); i < n; ++i) {
if (!slot(i)->resource()->IsSafeToRewrite(rewrite_uncacheable()) ||
!combiner_.AddResourceNoFetch(slot(i)->resource(), handler).value) {
return false;
}
// This should be called after checking IsSafeToRewrite, since
// AddInputInfoToPartition requires the resource to be loaded()
slot(i)->resource()->AddInputInfoToPartition(
Resource::kIncludeInputHash, i, partition);
}
OutputResourcePtr combination(combiner_.MakeOutput());
// MakeOutput can fail if for example there is only one input resource.
if (combination.get() == NULL) {
return false;
}
// ResourceCombiner provides us with a pre-populated CachedResult,
// so we need to copy it over to our CachedResult. This is
// less efficient than having ResourceCombiner work with our
// cached_result directly but this allows code-sharing as we
// transition to the async flow.
combination->UpdateCachedResultPreservingInputInfo(partition);
DisableRemovedSlots(partition);
outputs->push_back(combination);
return true;
}
void CombiningFilter::Context::Rewrite(int partition_index,
CachedResult* partition,
const OutputResourcePtr& output) {
if (filter_->rewrite_signal_on_ != NULL) {
filter_->rewrite_signal_on_->Notify();
}
if (filter_->rewrite_block_on_ != NULL) {
filter_->rewrite_block_on_->Wait();
}
if (filter_->rewrite_delay_ms() == 0) {
DoRewrite(partition_index, partition, output);
} else {
int64 wakeup_us = time_at_start_of_rewrite_us_ +
1000 * filter_->rewrite_delay_ms();
Function* closure = MakeFunction(
this, &Context::DoRewrite, partition_index, partition, output);
scheduler_->AddAlarmAtUs(wakeup_us, closure);
}
}
void CombiningFilter::Context::DoRewrite(int partition_index,
CachedResult* partition,
OutputResourcePtr output) {
++filter_->num_rewrites_;
// resource_combiner.cc takes calls WriteCombination as part
// of Combine. But if we are being called on behalf of a
// fetch then the resource still needs to be written.
RewriteResult result = kRewriteOk;
if (!output->IsWritten()) {
ResourceVector resources;
for (int i = 0, n = num_slots(); i < n; ++i) {
ResourcePtr resource(slot(i)->resource());
resources.push_back(resource);
}
if (!combiner_.Write(resources, output)) {
result = kRewriteFailed;
}
}
RewriteDone(result, partition_index);
}
void CombiningFilter::Context::Render() {
++filter_->num_render_;
// Slot 0 will be replaced by the combined resource as part of
// rewrite_context.cc. But we still need to delete slots 1-N.
for (int p = 0, np = num_output_partitions(); p < np; ++p) {
DisableRemovedSlots(output_partition(p));
}
}
void CombiningFilter::Context::WillNotRender() {
++filter_->num_will_not_render_;
}
void CombiningFilter::Context::Cancel() {
++filter_->num_cancel_;
}
void CombiningFilter::Context::DisableRemovedSlots(CachedResult* partition) {
if (filter_->disable_successors_) {
slot(0)->set_disable_further_processing(true);
}
for (int i = 1; i < partition->input_size(); ++i) {
int slot_index = partition->input(i).index();
slot(slot_index)->RequestDeleteElement();
}
}
void CombiningFilter::StartElementImpl(HtmlElement* element) {
if (element->keyword() == HtmlName::kLink) {
HtmlElement::Attribute* href = element->FindAttribute(HtmlName::kHref);
if (href != NULL) {
bool unused;
ResourcePtr resource(CreateInputResource(href->DecodedValueOrNull(),
&unused));
if (resource.get() != NULL) {
if (context_.get() == NULL) {
context_.reset(new Context(driver(), this, scheduler_));
}
context_->AddElement(element, href, resource);
}
}
}
}
const int64 RewriteContextTestBase::kRewriteDeadlineMs;
RewriteContextTestBase::~RewriteContextTestBase() {
}
void RewriteContextTestBase::SetUp() {
trim_filter_ = NULL;
other_trim_filter_ = NULL;
combining_filter_ = NULL;
nested_filter_ = NULL;
// The default deadline set in RewriteDriver is dependent on whether
// the system was compiled for debug, or is being run under valgrind.
// However, the unit-tests here use mock-time so we want to set the
// deadline explicitly.
options()->set_rewrite_deadline_ms(kRewriteDeadlineMs);
other_options()->set_rewrite_deadline_ms(kRewriteDeadlineMs);
RewriteTestBase::SetUp();
EXPECT_EQ(kRewriteDeadlineMs, rewrite_driver()->rewrite_deadline_ms());
EXPECT_EQ(kRewriteDeadlineMs, other_rewrite_driver()->rewrite_deadline_ms());
}
void RewriteContextTestBase::TearDown() {
rewrite_driver()->WaitForShutDown();
RewriteTestBase::TearDown();
}
void RewriteContextTestBase::InitResourcesToDomain(const char* domain) {
ResponseHeaders default_css_header;
SetDefaultLongCacheHeaders(&kContentTypeCss, &default_css_header);
int64 now_ms = http_cache()->timer()->NowMs();
default_css_header.SetDateAndCaching(now_ms, kOriginTtlMs);
default_css_header.ComputeCaching();
// trimmable
SetFetchResponse(StrCat(domain, "a.css"), default_css_header, " a ");
// not trimmable
SetFetchResponse(StrCat(domain, "b.css"), default_css_header, "b");
SetFetchResponse(StrCat(domain, "c.css"), default_css_header,
"a.css\nb.css\n");
// not trimmable, low ttl.
ResponseHeaders low_ttl_css_header;
SetDefaultLongCacheHeaders(&kContentTypeCss, &low_ttl_css_header);
low_ttl_css_header.SetDateAndCaching(now_ms, kLowOriginTtlMs);
low_ttl_css_header.ComputeCaching();
low_ttl_css_header.Add(HttpAttributes::kContentType, "text/css");
SetFetchResponse(StrCat(domain, "d.css"), low_ttl_css_header, "d");
// trimmable, low ttl.
SetFetchResponse(StrCat(domain, "e.css"), low_ttl_css_header, " e ");
// trimmable, with charset.
ResponseHeaders encoded_css_header;
server_context()->SetDefaultLongCacheHeaders(
&kContentTypeCss, "koi8-r", StringPiece(), &encoded_css_header);
SetFetchResponse(StrCat(domain, "a_ru.css"), encoded_css_header,
" a = \xc1 ");
// trimmable, private
ResponseHeaders private_css_header;
private_css_header.set_major_version(1);
private_css_header.set_minor_version(1);
private_css_header.SetStatusAndReason(HttpStatus::kOK);
private_css_header.SetDateAndCaching(now_ms, kOriginTtlMs, ",private");
private_css_header.Add(HttpAttributes::kContentType, "text/css");
private_css_header.ComputeCaching();
SetFetchResponse(StrCat(domain, "a_private.css"),
private_css_header,
" a ");
// trimmable, no-cache
ResponseHeaders no_cache_css_header;
no_cache_css_header.set_major_version(1);
no_cache_css_header.set_minor_version(1);
no_cache_css_header.SetStatusAndReason(HttpStatus::kOK);
no_cache_css_header.SetDateAndCaching(now_ms, 0, ",no-cache");
no_cache_css_header.Add(HttpAttributes::kContentType, "text/css");
no_cache_css_header.ComputeCaching();
SetFetchResponse(StrCat(domain, "a_no_cache.css"),
no_cache_css_header,
" a ");
// trimmable, no-transform
ResponseHeaders no_transform_css_header;
no_transform_css_header.set_major_version(1);
no_transform_css_header.set_minor_version(1);
no_transform_css_header.SetStatusAndReason(HttpStatus::kOK);
no_transform_css_header.SetDateAndCaching(now_ms, kOriginTtlMs,
",no-transform");
no_transform_css_header.Add(HttpAttributes::kContentType, "text/css");
no_transform_css_header.ComputeCaching();
SetFetchResponse(StrCat(domain, "a_no_transform.css"),
no_transform_css_header,
" a ");
// trimmable, no-cache, no-store
ResponseHeaders no_store_css_header;
no_store_css_header.set_major_version(1);
no_store_css_header.set_minor_version(1);
no_store_css_header.SetStatusAndReason(HttpStatus::kOK);
no_store_css_header.SetDateAndCaching(now_ms, 0, ",no-cache,no-store");
no_store_css_header.Add(HttpAttributes::kContentType, "text/css");
no_store_css_header.ComputeCaching();
SetFetchResponse(StrCat(domain, "a_no_store.css"),
no_store_css_header,
" a ");
}
void RewriteContextTestBase::InitUpperFilter(OutputResourceKind kind,
RewriteDriver* rewrite_driver) {
UpperCaseRewriter* rewriter;
rewrite_driver->AppendRewriteFilter(
UpperCaseRewriter::MakeFilter(kind, rewrite_driver, &rewriter));
}
void RewriteContextTestBase::InitCombiningFilter(int64 rewrite_delay_ms) {
RewriteDriver* driver = rewrite_driver();
combining_filter_ = new CombiningFilter(driver, mock_scheduler(),
rewrite_delay_ms);
driver->AppendRewriteFilter(combining_filter_);
driver->AddFilters();
}
void RewriteContextTestBase::InitNestedFilter(
bool expected_nested_rewrite_result) {
RewriteDriver* driver = rewrite_driver();
// Note that we only register this instance for rewrites, not HTML
// handling, so that uppercasing doesn't end up messing things up before
// NestedFilter gets to them.
UpperCaseRewriter* upper_rewriter;
SimpleTextFilter* upper_filter =
UpperCaseRewriter::MakeFilter(kOnTheFlyResource, driver,
&upper_rewriter);
AddFetchOnlyRewriteFilter(upper_filter);
nested_filter_ = new NestedFilter(driver, upper_filter, upper_rewriter,
expected_nested_rewrite_result);
driver->AppendRewriteFilter(nested_filter_);
driver->AddFilters();
}
void RewriteContextTestBase::InitTrimFilters(OutputResourceKind kind) {
trim_filter_ = new TrimWhitespaceRewriter(kind);
rewrite_driver()->AppendRewriteFilter(
new SimpleTextFilter(trim_filter_, rewrite_driver()));
rewrite_driver()->AddFilters();
other_trim_filter_ = new TrimWhitespaceRewriter(kind);
other_rewrite_driver()->AppendRewriteFilter(
new SimpleTextFilter(other_trim_filter_, other_rewrite_driver()));
other_rewrite_driver()->AddFilters();
}
void RewriteContextTestBase::ClearStats() {
RewriteTestBase::ClearStats();
if (trim_filter_ != NULL) {
trim_filter_->ClearStats();
}
if (other_trim_filter_ != NULL) {
other_trim_filter_->ClearStats();
}
if (combining_filter_ != NULL) {
combining_filter_->ClearStats();
}
if (nested_filter_ != NULL) {
nested_filter_->ClearStats();
}
}
} // namespace net_instaweb