| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <map> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/rand_util.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "build/build_config.h" |
| #include "content/browser/renderer_host/render_view_host_impl.h" |
| #include "content/browser/renderer_host/render_widget_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/shell/common/shell_switches.h" |
| #include "gpu/command_buffer/service/gpu_switches.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/gfx/image/image.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| static const char kCanvasPageString[] = |
| "<body>" |
| " <canvas id=\"canvas\" width=\"64\" height=\"64\"" |
| " style=\"position:absolute;top:0px;left:0px;width:100%;" |
| " height=100%;margin:0;padding:0;\">" |
| " </canvas>" |
| " <script>" |
| " window.ctx = document.getElementById(\"canvas\").getContext(\"2d\");" |
| " function fillWithColor(color) {" |
| " ctx.fillStyle = color;" |
| " ctx.fillRect(0, 0, 64, 64);" |
| " return color;" |
| " }" |
| " var offset = 150;" |
| " function openNewWindow() {" |
| " window.open(\"/test\", \"\", " |
| " \"top=\"+offset+\",left=\"+offset+\",width=200,height=200\");" |
| " offset += 50;" |
| " return true;" |
| " }" |
| " window.document.title = \"Ready\";" |
| " </script>" |
| "</body>"; |
| } |
| |
| class SnapshotBrowserTest : public ContentBrowserTest { |
| public: |
| SnapshotBrowserTest() {} |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| // Use a smaller browser window to speed up the snapshots. |
| command_line->AppendSwitchASCII(::switches::kContentShellHostWindowSize, |
| "200x200"); |
| } |
| |
| void SetUp() override { |
| // These tests rely on the harness producing pixel output. |
| EnablePixelOutput(); |
| ContentBrowserTest::SetUp(); |
| } |
| |
| content::WebContentsImpl* GetWebContents(Shell* browser) { |
| return static_cast<content::WebContentsImpl*>(browser->web_contents()); |
| } |
| |
| content::RenderWidgetHostImpl* GetRenderWidgetHostImpl(Shell* browser) { |
| return GetWebContents(browser)->GetRenderViewHost()->GetWidget(); |
| } |
| |
| void SetupTestServer() { |
| // Use an embedded test server so we can open multiple windows in |
| // the same renderer process, all referring to the same origin. |
| embedded_test_server()->RegisterRequestHandler(base::BindRepeating( |
| &SnapshotBrowserTest::HandleRequest, base::Unretained(this))); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| ASSERT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/test"))); |
| } |
| |
| std::unique_ptr<net::test_server::HttpResponse> HandleRequest( |
| const net::test_server::HttpRequest& request) { |
| GURL absolute_url = embedded_test_server()->GetURL(request.relative_url); |
| if (absolute_url.path() != "/test") |
| return nullptr; |
| |
| std::unique_ptr<net::test_server::BasicHttpResponse> http_response( |
| new net::test_server::BasicHttpResponse()); |
| http_response->set_code(net::HTTP_OK); |
| http_response->set_content(kCanvasPageString); |
| http_response->set_content_type("text/html"); |
| return http_response; |
| } |
| |
| void WaitForAllWindowsToBeReady() { |
| const std::u16string expected_title = u"Ready"; |
| // The subordinate windows may load asynchronously. Wait for all of |
| // them to execute their script before proceeding. |
| auto browser_list = Shell::windows(); |
| for (Shell* browser : browser_list) { |
| TitleWatcher watcher(GetWebContents(browser), expected_title); |
| const std::u16string& actual_title = watcher.WaitAndGetTitle(); |
| EXPECT_EQ(expected_title, actual_title); |
| } |
| } |
| |
| struct ExpectedColor { |
| ExpectedColor() : r(0), g(0), b(0) {} |
| bool operator==(const ExpectedColor& other) const { |
| return (r == other.r && g == other.g && b == other.b); |
| } |
| uint8_t r; |
| uint8_t g; |
| uint8_t b; |
| }; |
| |
| void PickRandomColor(ExpectedColor* expected) { |
| expected->r = static_cast<uint8_t>(base::RandInt(0, 256)); |
| expected->g = static_cast<uint8_t>(base::RandInt(0, 256)); |
| expected->b = static_cast<uint8_t>(base::RandInt(0, 256)); |
| } |
| |
| struct SerialSnapshot { |
| SerialSnapshot() : host(nullptr) {} |
| |
| raw_ptr<content::RenderWidgetHost> host; |
| ExpectedColor color; |
| }; |
| std::vector<SerialSnapshot> expected_snapshots_; |
| |
| void SyncSnapshotCallback(content::RenderWidgetHostImpl* rwhi, |
| const gfx::Image& image) { |
| for (auto iter = expected_snapshots_.begin(); |
| iter != expected_snapshots_.end(); ++iter) { |
| const SerialSnapshot& expected = *iter; |
| if (expected.host == rwhi) { |
| const SkBitmap* bitmap = image.ToSkBitmap(); |
| SkColor color = bitmap->getColor(1, 1); |
| |
| EXPECT_EQ(static_cast<int>(SkColorGetR(color)), |
| static_cast<int>(expected.color.r)) |
| << "Red channels differed"; |
| EXPECT_EQ(static_cast<int>(SkColorGetG(color)), |
| static_cast<int>(expected.color.g)) |
| << "Green channels differed"; |
| EXPECT_EQ(static_cast<int>(SkColorGetB(color)), |
| static_cast<int>(expected.color.b)) |
| << "Blue channels differed"; |
| |
| expected_snapshots_.erase(iter); |
| break; |
| } |
| } |
| } |
| |
| std::map<content::RenderWidgetHost*, std::vector<ExpectedColor>> |
| expected_async_snapshots_map_; |
| int num_remaining_async_snapshots_ = 0; |
| |
| void AsyncSnapshotCallback(content::RenderWidgetHostImpl* rwhi, |
| const gfx::Image& image) { |
| --num_remaining_async_snapshots_; |
| auto iterator = expected_async_snapshots_map_.find(rwhi); |
| ASSERT_NE(iterator, expected_async_snapshots_map_.end()); |
| std::vector<ExpectedColor>& expected_snapshots = iterator->second; |
| const SkBitmap* bitmap = image.ToSkBitmap(); |
| SkColor color = bitmap->getColor(1, 1); |
| bool found = false; |
| // Find first instance of this color in the list and clear out all |
| // of the entries before that point. If it's not found, report |
| // failure. |
| for (auto iter = expected_snapshots.begin(); |
| iter != expected_snapshots.end(); ++iter) { |
| const ExpectedColor& expected = *iter; |
| if (SkColorGetR(color) == expected.r && |
| SkColorGetG(color) == expected.g && |
| SkColorGetB(color) == expected.b) { |
| // Erase everything up to this color, but not this color |
| // itself, since it might be returned again later on |
| // subsequent snapshot requests. |
| expected_snapshots.erase(expected_snapshots.begin(), iter); |
| found = true; |
| break; |
| } |
| } |
| |
| EXPECT_TRUE(found) << "Did not find color (" |
| << static_cast<int>(SkColorGetR(color)) << ", " |
| << static_cast<int>(SkColorGetG(color)) << ", " |
| << static_cast<int>(SkColorGetB(color)) |
| << ") in expected snapshots for RWH 0x" << rwhi; |
| } |
| }; |
| |
| // Even the single-window test doesn't work on Android yet. It's expected |
| // that the multi-window tests would never work on that platform. |
| #if !BUILDFLAG(IS_ANDROID) |
| |
| #if BUILDFLAG(IS_MAC) |
| // TODO(crbug.com/40854618): This test is flakey on macOS. |
| #define MAYBE_SingleWindowTest DISABLED_SingleWindowTest |
| #else |
| #define MAYBE_SingleWindowTest SingleWindowTest |
| #endif |
| IN_PROC_BROWSER_TEST_F(SnapshotBrowserTest, MAYBE_SingleWindowTest) { |
| SetupTestServer(); |
| |
| content::RenderWidgetHostImpl* rwhi = GetRenderWidgetHostImpl(shell()); |
| |
| for (int i = 0; i < 40; ++i) { |
| SerialSnapshot expected; |
| expected.host = rwhi; |
| PickRandomColor(&expected.color); |
| |
| std::string colorString = base::StringPrintf( |
| "#%02x%02x%02x", expected.color.r, expected.color.g, expected.color.b); |
| std::string script = std::string("fillWithColor(\"") + colorString + "\");"; |
| EXPECT_EQ(colorString, EvalJs(GetWebContents(shell()), script)); |
| |
| expected_snapshots_.push_back(expected); |
| |
| // Get the snapshot from the surface rather than the window. The |
| // on-screen display path is verified by the GPU tests, and it |
| // seems difficult to figure out the colorspace transformation |
| // required to make these color comparisons. |
| rwhi->GetSnapshotFromBrowser( |
| base::BindOnce(&SnapshotBrowserTest::SyncSnapshotCallback, |
| base::Unretained(this), base::Unretained(rwhi)), |
| true); |
| while (expected_snapshots_.size() > 0) { |
| base::RunLoop().RunUntilIdle(); |
| } |
| } |
| } |
| |
| // Timing out either all the time, or infrequently, apparently because |
| // they're too slow, on the following configurations: |
| // Windows Debug |
| // Windows Release (https://crbug.com/1376441) |
| // Linux Chromium OS ASAN LSAN Tests (1) |
| // Linux TSAN Tests |
| // See crbug.com/771119 |
| // TODO(crbug.com/40834774): Fix and enable on Fuchsia. |
| #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_FUCHSIA) || \ |
| (BUILDFLAG(IS_LINUX) && defined(THREAD_SANITIZER)) |
| #define MAYBE_SyncMultiWindowTest DISABLED_SyncMultiWindowTest |
| #else |
| #define MAYBE_SyncMultiWindowTest SyncMultiWindowTest |
| #endif |
| IN_PROC_BROWSER_TEST_F(SnapshotBrowserTest, MAYBE_SyncMultiWindowTest) { |
| SetupTestServer(); |
| |
| for (int i = 0; i < 3; ++i) { |
| EXPECT_EQ(true, EvalJs(GetWebContents(shell()), "openNewWindow()")); |
| } |
| |
| base::RunLoop().RunUntilIdle(); |
| |
| WaitForAllWindowsToBeReady(); |
| |
| auto browser_list = Shell::windows(); |
| EXPECT_EQ(4u, browser_list.size()); |
| |
| for (int i = 0; i < 20; ++i) { |
| for (int j = 0; j < 4; j++) { |
| // Start each iteration by taking a snapshot with a different |
| // browser instance. |
| int browser_index = (i + j) % 4; |
| Shell* browser = browser_list[browser_index]; |
| content::RenderWidgetHostImpl* rwhi = GetRenderWidgetHostImpl(browser); |
| |
| SerialSnapshot expected; |
| expected.host = rwhi; |
| PickRandomColor(&expected.color); |
| |
| std::string colorString = |
| base::StringPrintf("#%02x%02x%02x", expected.color.r, |
| expected.color.g, expected.color.b); |
| std::string script = |
| std::string("fillWithColor(\"") + colorString + "\");"; |
| EXPECT_EQ(colorString, EvalJs(GetWebContents(browser), script)); |
| expected_snapshots_.push_back(expected); |
| // Get the snapshot from the surface rather than the window. The |
| // on-screen display path is verified by the GPU tests, and it |
| // seems difficult to figure out the colorspace transformation |
| // required to make these color comparisons. |
| rwhi->GetSnapshotFromBrowser( |
| base::BindOnce(&SnapshotBrowserTest::SyncSnapshotCallback, |
| base::Unretained(this), base::Unretained(rwhi)), |
| true); |
| } |
| |
| while (expected_snapshots_.size() > 0) { |
| base::RunLoop().RunUntilIdle(); |
| } |
| } |
| } |
| |
| // Timing out either all the time, or infrequently, apparently because |
| // they're too slow, on the following configurations: |
| // Windows Debug |
| // Linux Chromium OS ASAN LSAN Tests (1) |
| // Linux TSAN Tests |
| // See crbug.com/771119 |
| // TODO(crbug.com/40740836): recently crashes flakily on |
| // linux_chromium_asan_rel_ng and linux-rel. |
| // TODO(crbug.com/40834774): Fix and enable on Fuchsia. |
| #if (BUILDFLAG(IS_WIN) && !defined(NDEBUG)) || BUILDFLAG(IS_CHROMEOS) || \ |
| BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_FUCHSIA) |
| #define MAYBE_AsyncMultiWindowTest DISABLED_AsyncMultiWindowTest |
| #else |
| #define MAYBE_AsyncMultiWindowTest AsyncMultiWindowTest |
| #endif |
| IN_PROC_BROWSER_TEST_F(SnapshotBrowserTest, MAYBE_AsyncMultiWindowTest) { |
| SetupTestServer(); |
| |
| for (int i = 0; i < 3; ++i) { |
| EXPECT_EQ(true, EvalJs(GetWebContents(shell()), "openNewWindow()")); |
| } |
| |
| base::RunLoop().RunUntilIdle(); |
| |
| WaitForAllWindowsToBeReady(); |
| |
| auto browser_list = Shell::windows(); |
| EXPECT_EQ(4u, browser_list.size()); |
| |
| // This many pending snapshots per window will be put on the queue |
| // before draining the requests. Anything more than 1 seems to catch |
| // bugs which might otherwise be introduced in LatencyInfo's |
| // propagation of the BROWSER_SNAPSHOT_FRAME_NUMBER_COMPONENT |
| // component type. |
| int divisor = 3; |
| |
| for (int i = 0; i < 10 * divisor; ++i) { |
| for (int j = 0; j < 4; j++) { |
| // Start each iteration by taking a snapshot with a different |
| // browser instance. |
| int browser_index = (i + j) % 4; |
| Shell* browser = browser_list[browser_index]; |
| content::RenderWidgetHostImpl* rwhi = GetRenderWidgetHostImpl(browser); |
| |
| std::vector<ExpectedColor>& expected_snapshots = |
| expected_async_snapshots_map_[rwhi]; |
| |
| // Pick a unique random color. |
| ExpectedColor expected; |
| do { |
| PickRandomColor(&expected); |
| } while (base::Contains(expected_snapshots, expected)); |
| expected_snapshots.push_back(expected); |
| |
| std::string colorString = base::StringPrintf("#%02x%02x%02x", expected.r, |
| expected.g, expected.b); |
| std::string script = |
| std::string("fillWithColor(\"") + colorString + "\");"; |
| EXPECT_EQ(colorString, EvalJs(GetWebContents(browser), script)); |
| // Get the snapshot from the surface rather than the window. The |
| // on-screen display path is verified by the GPU tests, and it |
| // seems difficult to figure out the colorspace transformation |
| // required to make these color comparisons. |
| rwhi->GetSnapshotFromBrowser( |
| base::BindOnce(&SnapshotBrowserTest::AsyncSnapshotCallback, |
| base::Unretained(this), base::Unretained(rwhi)), |
| true); |
| ++num_remaining_async_snapshots_; |
| } |
| |
| // Periodically yield and drain the async snapshot requests. |
| if ((i % divisor) == 0) { |
| bool drained; |
| do { |
| drained = true; |
| for (auto iter = expected_async_snapshots_map_.begin(); |
| iter != expected_async_snapshots_map_.end(); ++iter) { |
| if (iter->second.size() > 1) { |
| drained = false; |
| break; |
| } |
| } |
| if (!drained) { |
| base::RunLoop().RunUntilIdle(); |
| } |
| } while (!drained); |
| } |
| } |
| |
| // At the end of the test, cooperatively wait for all of the snapshot |
| // requests to be drained before exiting. This works around crashes |
| // seen when tearing down the compositor while these requests are in |
| // flight. Likely, user-visible APIs that use this facility are safe |
| // in this regard. |
| while (num_remaining_async_snapshots_ > 0) { |
| base::RunLoop().RunUntilIdle(); |
| } |
| } |
| |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| } // namespace content |