Index: content/browser/renderer_host/offscreen_canvas_provider_impl_unittest.cc |
diff --git a/content/browser/renderer_host/offscreen_canvas_provider_impl_unittest.cc b/content/browser/renderer_host/offscreen_canvas_provider_impl_unittest.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..06e1f5a51daaadabba38aa729a4ea075ca243b05 |
--- /dev/null |
+++ b/content/browser/renderer_host/offscreen_canvas_provider_impl_unittest.cc |
@@ -0,0 +1,347 @@ |
+// 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 |