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