blob: 06e1f5a51daaadabba38aa729a4ea075ca243b05 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/offscreen_canvas_provider_impl.h"
#include <algorithm>
#include <utility>
#include <vector>
#include "base/memory/ptr_util.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "cc/ipc/mojo_compositor_frame_sink.mojom.h"
#include "cc/output/compositor_frame.h"
#include "content/browser/compositor/test/no_transport_image_transport_factory.h"
#include "content/browser/renderer_host/offscreen_canvas_surface_impl.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/WebKit/public/platform/modules/offscreencanvas/offscreen_canvas_surface.mojom.h"
#if !defined(OS_ANDROID)
#include "content/browser/compositor/image_transport_factory.h"
#endif
using testing::ElementsAre;
using testing::IsEmpty;
namespace content {
namespace {
constexpr uint32_t kRendererClientId = 3;
constexpr cc::FrameSinkId kFrameSinkParent(kRendererClientId, 1);
constexpr cc::FrameSinkId kFrameSinkA(kRendererClientId, 3);
constexpr cc::FrameSinkId kFrameSinkB(kRendererClientId, 4);
// Stub OffscreenCanvasSurfaceClient that stores the latest SurfaceInfo.
class StubOffscreenCanvasSurfaceClient
: public blink::mojom::OffscreenCanvasSurfaceClient {
public:
StubOffscreenCanvasSurfaceClient() : binding_(this) {}
~StubOffscreenCanvasSurfaceClient() override {}
blink::mojom::OffscreenCanvasSurfaceClientPtr GetInterfacePtr() {
return binding_.CreateInterfacePtrAndBind();
}
const cc::SurfaceInfo& GetLastSurfaceInfo() const {
return last_surface_info_;
}
private:
// blink::mojom::OffscreenCanvasSurfaceClient:
void OnSurfaceCreated(const cc::SurfaceInfo& surface_info) override {
last_surface_info_ = surface_info;
}
mojo::Binding<blink::mojom::OffscreenCanvasSurfaceClient> binding_;
cc::SurfaceInfo last_surface_info_;
DISALLOW_COPY_AND_ASSIGN(StubOffscreenCanvasSurfaceClient);
};
// Stub MojoCompositorFrameSinkClient that does nothing.
class StubCompositorFrameSinkClient
: public cc::mojom::MojoCompositorFrameSinkClient {
public:
StubCompositorFrameSinkClient() : binding_(this) {}
~StubCompositorFrameSinkClient() override {}
cc::mojom::MojoCompositorFrameSinkClientPtr GetInterfacePtr() {
return binding_.CreateInterfacePtrAndBind();
}
private:
// cc::mojom::MojoCompositorFrameSinkClient:
void DidReceiveCompositorFrameAck(
const cc::ReturnedResourceArray& resources) override {}
void OnBeginFrame(const cc::BeginFrameArgs& begin_frame_args) override {}
void ReclaimResources(const cc::ReturnedResourceArray& resources) override {}
mojo::Binding<cc::mojom::MojoCompositorFrameSinkClient> binding_;
DISALLOW_COPY_AND_ASSIGN(StubCompositorFrameSinkClient);
};
// Create a CompositorFrame suitable to send over IPC.
cc::CompositorFrame MakeCompositorFrame() {
cc::CompositorFrame frame;
frame.metadata.begin_frame_ack.source_id =
cc::BeginFrameArgs::kManualSourceId;
frame.metadata.begin_frame_ack.sequence_number =
cc::BeginFrameArgs::kStartingFrameNumber;
frame.metadata.device_scale_factor = 1.0f;
auto render_pass = cc::RenderPass::Create();
render_pass->id = 1;
render_pass->output_rect = gfx::Rect(100, 100);
frame.render_pass_list.push_back(std::move(render_pass));
return frame;
}
// Creates a closure that sets |error_variable| true when run.
base::Closure ConnectionErrorClosure(bool* error_variable) {
return base::Bind([](bool* error_variable) { *error_variable = true; },
error_variable);
}
} // namespace
class OffscreenCanvasProviderImplTest : public testing::Test {
public:
OffscreenCanvasProviderImpl* provider() { return provider_.get(); }
// Gets the OffscreenCanvasSurfaceImpl for |frame_sink_id| or null if it
// it doesn't exist.
OffscreenCanvasSurfaceImpl* GetOffscreenCanvasSurface(
const cc::FrameSinkId& frame_sink_id) {
auto iter = provider_->canvas_map_.find(frame_sink_id);
if (iter == provider_->canvas_map_.end())
return nullptr;
return iter->second.get();
}
// Gets list of FrameSinkId for all offscreen canvases.
std::vector<cc::FrameSinkId> GetAllCanvases() {
std::vector<cc::FrameSinkId> frame_sink_ids;
for (auto& map_entry : provider_->canvas_map_)
frame_sink_ids.push_back(map_entry.second->frame_sink_id());
std::sort(frame_sink_ids.begin(), frame_sink_ids.end());
return frame_sink_ids;
}
void DeleteOffscreenCanvasProviderImpl() { provider_.reset(); }
void RunUntilIdle() { base::RunLoop().RunUntilIdle(); }
protected:
void SetUp() override {
#if !defined(OS_ANDROID)
ImageTransportFactory::InitializeForUnitTests(
std::unique_ptr<ImageTransportFactory>(
new NoTransportImageTransportFactory));
ImageTransportFactory::GetInstance()
->GetFrameSinkManagerHost()
->ConnectToFrameSinkManager();
#endif
provider_ =
base::MakeUnique<OffscreenCanvasProviderImpl>(kRendererClientId);
}
void TearDown() override {
provider_.reset();
#if !defined(OS_ANDROID)
ImageTransportFactory::Terminate();
#endif
}
private:
base::MessageLoop message_loop_;
std::unique_ptr<OffscreenCanvasProviderImpl> provider_;
};
// Mimics the workflow of OffscreenCanvas.commit() on renderer process.
TEST_F(OffscreenCanvasProviderImplTest,
SingleHTMLCanvasElementTransferToOffscreen) {
// Mimic connection from the renderer main thread to browser.
StubOffscreenCanvasSurfaceClient surface_client;
blink::mojom::OffscreenCanvasSurfacePtr surface;
provider()->CreateOffscreenCanvasSurface(kFrameSinkParent, kFrameSinkA,
surface_client.GetInterfacePtr(),
mojo::MakeRequest(&surface));
OffscreenCanvasSurfaceImpl* surface_impl =
GetOffscreenCanvasSurface(kFrameSinkA);
// There should be a single OffscreenCanvasSurfaceImpl and it should have the
// provided FrameSinkId and parent FrameSinkId.
EXPECT_EQ(kFrameSinkA, surface_impl->frame_sink_id());
EXPECT_EQ(kFrameSinkParent, surface_impl->parent_frame_sink_id());
EXPECT_THAT(GetAllCanvases(), ElementsAre(kFrameSinkA));
// Mimic connection from the renderer main or worker thread to browser.
cc::mojom::MojoCompositorFrameSinkPtr compositor_frame_sink;
StubCompositorFrameSinkClient compositor_frame_sink_client;
provider()->CreateCompositorFrameSink(
kFrameSinkA, compositor_frame_sink_client.GetInterfacePtr(),
mojo::MakeRequest(&compositor_frame_sink));
// Renderer submits a CompositorFrame with |local_id|.
const cc::LocalSurfaceId local_id(1, base::UnguessableToken::Create());
compositor_frame_sink->SubmitCompositorFrame(local_id, MakeCompositorFrame());
RunUntilIdle();
// OffscreenCanvasSurfaceImpl in browser should have LocalSurfaceId that was
// submitted with the CompositorFrame.
EXPECT_EQ(local_id, surface_impl->local_surface_id());
// OffscreenCanvasSurfaceClient in the renderer should get the new SurfaceId
// including the |local_id|.
const auto& surface_info = surface_client.GetLastSurfaceInfo();
EXPECT_EQ(kFrameSinkA, surface_info.id().frame_sink_id());
EXPECT_EQ(local_id, surface_info.id().local_surface_id());
}
// Check that renderer closing the mojom::OffscreenCanvasSurface connection
// destroys the OffscreenCanvasSurfaceImpl in browser.
TEST_F(OffscreenCanvasProviderImplTest, ClientClosesConnection) {
StubOffscreenCanvasSurfaceClient surface_client;
blink::mojom::OffscreenCanvasSurfacePtr surface;
provider()->CreateOffscreenCanvasSurface(kFrameSinkParent, kFrameSinkA,
surface_client.GetInterfacePtr(),
mojo::MakeRequest(&surface));
RunUntilIdle();
EXPECT_THAT(GetAllCanvases(), ElementsAre(kFrameSinkA));
// Mimic closing the connection from the renderer.
surface.reset();
RunUntilIdle();
// The renderer closing the connection should destroy the
// OffscreenCanvasSurfaceImpl.
EXPECT_THAT(GetAllCanvases(), IsEmpty());
}
// Check that destroying OffscreenCanvasProviderImpl closes connection to
// renderer.
TEST_F(OffscreenCanvasProviderImplTest, ProviderClosesConnections) {
StubOffscreenCanvasSurfaceClient surface_client;
blink::mojom::OffscreenCanvasSurfacePtr surface;
provider()->CreateOffscreenCanvasSurface(kFrameSinkParent, kFrameSinkA,
surface_client.GetInterfacePtr(),
mojo::MakeRequest(&surface));
// Observe connection errors on |surface|.
bool connection_error = false;
surface.set_connection_error_handler(
ConnectionErrorClosure(&connection_error));
RunUntilIdle();
// There should be a OffscreenCanvasSurfaceImpl and |surface| should be bound.
EXPECT_THAT(GetAllCanvases(), ElementsAre(kFrameSinkA));
EXPECT_TRUE(surface.is_bound());
EXPECT_FALSE(connection_error);
// Delete OffscreenCanvasProviderImpl before client disconnects.
DeleteOffscreenCanvasProviderImpl();
RunUntilIdle();
// This should destroy the OffscreenCanvasSurfaceImpl and close the connection
// to |surface| triggering a connection error.
EXPECT_TRUE(connection_error);
}
// Check that connecting MojoCompositorFrameSink without first making a
// OffscreenCanvasSurface connection fails.
TEST_F(OffscreenCanvasProviderImplTest, ClientConnectionWrongOrder) {
// Mimic connection from the renderer main or worker thread.
cc::mojom::MojoCompositorFrameSinkPtr compositor_frame_sink;
StubCompositorFrameSinkClient compositor_frame_sink_client;
// Try to connect MojoCompositorFrameSink without first making
// OffscreenCanvasSurface connection. This should fail.
provider()->CreateCompositorFrameSink(
kFrameSinkA, compositor_frame_sink_client.GetInterfacePtr(),
mojo::MakeRequest(&compositor_frame_sink));
// Observe connection errors on |compositor_frame_sink|.
bool connection_error = false;
compositor_frame_sink.set_connection_error_handler(
ConnectionErrorClosure(&connection_error));
RunUntilIdle();
// The connection for |compositor_frame_sink| will have failed and triggered a
// connection error.
EXPECT_TRUE(connection_error);
}
// Check that trying to create an OffscreenCanvasSurfaceImpl with a client id
// that doesn't match the renderer fails.
TEST_F(OffscreenCanvasProviderImplTest, InvalidClientId) {
const cc::FrameSinkId invalid_frame_sink_id(4, 3);
EXPECT_NE(kRendererClientId, invalid_frame_sink_id.client_id());
StubOffscreenCanvasSurfaceClient surface_client;
blink::mojom::OffscreenCanvasSurfacePtr surface;
provider()->CreateOffscreenCanvasSurface(
kFrameSinkParent, invalid_frame_sink_id, surface_client.GetInterfacePtr(),
mojo::MakeRequest(&surface));
// Observe connection errors on |surface|.
bool connection_error = false;
surface.set_connection_error_handler(
ConnectionErrorClosure(&connection_error));
RunUntilIdle();
// No OffscreenCanvasSurfaceImpl should have been created.
EXPECT_THAT(GetAllCanvases(), IsEmpty());
// The connection for |surface| will have failed and triggered a connection
// error.
EXPECT_TRUE(connection_error);
}
// Mimic renderer with two offscreen canvases.
TEST_F(OffscreenCanvasProviderImplTest,
MultiHTMLCanvasElementTransferToOffscreen) {
StubOffscreenCanvasSurfaceClient surface_client_a;
blink::mojom::OffscreenCanvasSurfacePtr surface_a;
provider()->CreateOffscreenCanvasSurface(kFrameSinkParent, kFrameSinkA,
surface_client_a.GetInterfacePtr(),
mojo::MakeRequest(&surface_a));
StubOffscreenCanvasSurfaceClient surface_client_b;
blink::mojom::OffscreenCanvasSurfacePtr surface_b;
provider()->CreateOffscreenCanvasSurface(kFrameSinkParent, kFrameSinkB,
surface_client_b.GetInterfacePtr(),
mojo::MakeRequest(&surface_b));
RunUntilIdle();
// There should be two OffscreenCanvasSurfaceImpls created.
EXPECT_THAT(GetAllCanvases(), ElementsAre(kFrameSinkA, kFrameSinkB));
// Mimic closing first connection from the renderer.
surface_a.reset();
RunUntilIdle();
EXPECT_THAT(GetAllCanvases(), ElementsAre(kFrameSinkB));
// Mimic closing second connection from the renderer.
surface_b.reset();
RunUntilIdle();
EXPECT_THAT(GetAllCanvases(), IsEmpty());
}
} // namespace content