blob: 38bad1bdf0723e66e43b65b40edd5d141f63a3a3 [file] [log] [blame]
/*
* Copyright 2015 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/render_blocking_html_computation.h"
#include "base/logging.h"
#include "net/instaweb/http/public/wait_url_async_fetcher.h"
#include "net/instaweb/rewriter/public/common_filter.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 "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "pagespeed/kernel/base/abstract_mutex.h"
#include "pagespeed/kernel/base/condvar.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/thread_annotations.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/html/html_element.h"
#include "pagespeed/kernel/html/html_node.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/thread/mock_scheduler.h"
namespace net_instaweb {
namespace {
// TODO(morlovich): Duped from CommonFilterTest
class CountingFilter : public CommonFilter {
public:
explicit CountingFilter(RewriteDriver* driver) : CommonFilter(driver),
start_doc_calls_(0),
start_element_calls_(0),
end_element_calls_(0) {}
virtual void StartDocumentImpl() { ++start_doc_calls_; }
virtual void StartElementImpl(HtmlElement* element) {
++start_element_calls_;
}
virtual void EndElementImpl(HtmlElement* element) { ++end_element_calls_; }
virtual const char* Name() const { return "CountingFilter"; }
int start_doc_calls_;
int start_element_calls_;
int end_element_calls_;
};
const char kPage[] = "page.html";
const char kContent[] = "<a><b><c></c></b></a>";
class RenderBlockingHtmlComputationTest : public RewriteTestBase {
public:
void OnlyCallFetcherCallbacks() {
// Normal CallFetcherCallbacks() tries to WaitForCompletion and do all
// sorts of other similar stuff that makes it unusable during parsing,
// so we work with the wait fetcher directly.
factory()->wait_url_async_fetcher()->CallCallbacks();
}
protected:
friend class CountingClientFilter;
friend class CountingComputation;
RenderBlockingHtmlComputationTest()
: done_(false), result_(false), start_doc_calls_(0),
start_element_calls_(0), end_element_calls_(0) {}
virtual void SetUp() {
RewriteTestBase::SetUp();
options()->ComputeSignature();
mutex_.reset(server_context()->thread_system()->NewMutex());
cond_.reset(mutex_->NewCondvar());
SetResponseWithDefaultHeaders(kPage, kContentTypeHtml, kContent, 100);
// Permit RenderBlockingHtmlComputation to make resources if we're not also
// running a document through a driver.
SetBaseUrlForFetch(kTestDomain);
}
void Done(bool result, int start_doc_calls,
int start_element_calls, int end_element_calls) {
ScopedMutex hold(mutex_.get());
done_ = true;
result_ = result;
start_doc_calls_ = start_doc_calls;
start_element_calls_ = start_element_calls;
end_element_calls_ = end_element_calls;
cond_->Signal();
}
// Returns success/failure.
bool WaitForDone() {
ScopedMutex hold(mutex_.get());
while (!done_) {
cond_->Wait();
}
done_ = false; // Reset for further runs.
return result_;
}
bool done() const {
ScopedMutex hold(mutex_.get());
return done_;
}
bool result() const {
ScopedMutex hold(mutex_.get());
return result_;
}
int start_doc_calls() const {
ScopedMutex hold(mutex_.get());
return start_doc_calls_;
}
int start_element_calls() const {
ScopedMutex hold(mutex_.get());
return start_element_calls_;
}
int end_element_calls() const {
ScopedMutex hold(mutex_.get());
return end_element_calls_;
}
bool done_ GUARDED_BY(mutex_);
bool result_ GUARDED_BY(mutex_);
int start_doc_calls_ GUARDED_BY(mutex_);
int start_element_calls_ GUARDED_BY(mutex_);
int end_element_calls_ GUARDED_BY(mutex_);
scoped_ptr<ThreadSystem::CondvarCapableMutex> mutex_;
scoped_ptr<ThreadSystem::Condvar> cond_;
};
class CountingComputation : public RenderBlockingHtmlComputation {
public:
CountingComputation(RenderBlockingHtmlComputationTest* fixture,
RewriteDriver* parent_driver)
: RenderBlockingHtmlComputation(parent_driver),
filter_(NULL) , fixture_(fixture) {}
protected:
virtual void SetupFilters(RewriteDriver* child_driver) {
filter_ = new CountingFilter(child_driver);
child_driver->AppendOwnedPreRenderFilter(filter_);
}
virtual void Done(bool success) {
if (success) {
CHECK(filter_ != NULL);
fixture_->Done(true,
filter_->start_doc_calls_,
filter_->start_element_calls_,
filter_->end_element_calls_);
} else {
fixture_->Done(false, 0, 0, 0);
}
}
private:
// Owned indirectly by base class.
CountingFilter* filter_;
RenderBlockingHtmlComputationTest* fixture_;
};
TEST_F(RenderBlockingHtmlComputationTest, ErrorPaths) {
SetFetchFailOnUnexpected(false);
scoped_ptr<CountingComputation> background_computation(
new CountingComputation(this, rewrite_driver()));
background_computation.release()->Compute("fekrfkek://wkewkl");
EXPECT_FALSE(WaitForDone());
background_computation.reset(
new CountingComputation(this, rewrite_driver()));
background_computation.release()->Compute(StrCat(kPage, "404"));
EXPECT_FALSE(WaitForDone());
EXPECT_EQ(0, start_doc_calls_);
EXPECT_EQ(0, start_element_calls_);
EXPECT_EQ(0, end_element_calls_);
}
TEST_F(RenderBlockingHtmlComputationTest, BasicOperation) {
// Makes sure we can run a basic computation through in a simplest case.
scoped_ptr<CountingComputation> background_computation(
new CountingComputation(this, rewrite_driver()));
background_computation.release()->Compute(AbsolutifyUrl(kPage));
EXPECT_TRUE(WaitForDone());
EXPECT_EQ(1, start_doc_calls_);
EXPECT_EQ(3, start_element_calls_);
EXPECT_EQ(3, end_element_calls_);
}
class CountingClientFilter : public CommonFilter {
public:
CountingClientFilter(RewriteDriver* driver,
RenderBlockingHtmlComputationTest* fixture)
: CommonFilter(driver), first_flush_window_(true), fixture_(fixture) {}
virtual void StartDocumentImpl() {
first_flush_window_ = true;
// In real case this would be conditional on something like entry in
// pcache missing.
scoped_ptr<CountingComputation> background_computation(
new CountingComputation(fixture_, driver()));
background_computation.release()->Compute(driver()->url());
}
virtual void RenderDone() {
// Only care about the first one.
if (!first_flush_window_) {
return;
}
EXPECT_TRUE(fixture_->done());
EXPECT_TRUE(fixture_->result());
// Test computer saved results to fixture --- real one would likely save
// it to here.
GoogleString stats = StringPrintf("docs=%d, open=%d, close=%d",
fixture_->start_doc_calls(),
fixture_->start_element_calls(),
fixture_->end_element_calls());
InsertNodeAtBodyEnd(driver()->NewCommentNode(NULL, stats));
first_flush_window_ = false;
}
virtual void StartElementImpl(HtmlElement* element) {}
virtual void EndElementImpl(HtmlElement* element) {}
virtual const char* Name() const { return "CountingClientFilter"; }
private:
bool first_flush_window_;
RenderBlockingHtmlComputationTest* fixture_;
DISALLOW_COPY_AND_ASSIGN(CountingClientFilter);
};
TEST_F(RenderBlockingHtmlComputationTest, WithFilter) {
// A bit more like the expected usage scenario, with a filter invoking
// RenderBlockingHtmlComputation as a background computation.
scoped_ptr<CountingClientFilter> parent_filter(
new CountingClientFilter(rewrite_driver(), this));
rewrite_driver()->AddOwnedEarlyPreRenderFilter(parent_filter.release());
ValidateExpected("page", // it appends .html itself.
"<html>will use fetched content.</html>",
"<html>will use fetched content."
"<!--docs=1, open=3, close=3--></html>");
}
TEST_F(RenderBlockingHtmlComputationTest, WithFilterAndWaiting) {
// Actually run this asynchronously --- also checking that we actually
// do wait for the fetch to happen.
SetupWaitFetcher();
scoped_ptr<CountingClientFilter> parent_filter(
new CountingClientFilter(rewrite_driver(), this));
rewrite_driver()->AddOwnedEarlyPreRenderFilter(parent_filter.release());
int64 start_us = timer()->NowUs();
mock_scheduler()->AddAlarmAtUs(
start_us + 50 * Timer::kMsUs,
MakeFunction(
static_cast<RenderBlockingHtmlComputationTest*>(this),
&RenderBlockingHtmlComputationTest::OnlyCallFetcherCallbacks));
ValidateExpected("page", // it appends .html itself.
"<html>will use fetched content.</html>",
"<html>will use fetched content."
"<!--docs=1, open=3, close=3--></html>");
EXPECT_EQ(50 * Timer::kMsUs, timer()->NowUs() - start_us);
}
} // namespace
} // namespace net_instaweb