| // Copyright (c) 2013 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 "base/command_line.h" |
| #include "base/file_util.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_paths.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/content_browser_test.h" |
| #include "content/test/content_browser_test_utils.h" |
| #include "gpu/config/gpu_test_config.h" |
| #include "net/base/net_util.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/gfx/codec/png_codec.h" |
| #include "ui/gfx/size.h" |
| #include "ui/gl/gl_switches.h" |
| #include "ui/snapshot/snapshot.h" |
| |
| namespace { |
| |
| enum ReferenceImageOption { |
| kReferenceImageLocal, |
| kReferenceImageCheckedIn, |
| kReferenceImageNone // Only check a few key pixels. |
| }; |
| |
| struct ReferencePixel { |
| int x, y; |
| unsigned char r, g, b; |
| }; |
| |
| // Command line flag for overriding the default location for putting generated |
| // test images that do not match references. |
| const char kGeneratedDir[] = "generated-dir"; |
| // Command line flag for overriding the default location for reference images. |
| const char kReferenceDir[] = "reference-dir"; |
| // Command line flag for Chromium build revision. |
| const char kBuildRevision[] = "build-revision"; |
| |
| // Reads and decodes a PNG image to a bitmap. Returns true on success. The PNG |
| // should have been encoded using |gfx::PNGCodec::Encode|. |
| bool ReadPNGFile(const base::FilePath& file_path, SkBitmap* bitmap) { |
| DCHECK(bitmap); |
| base::FilePath abs_path(base::MakeAbsoluteFilePath(file_path)); |
| if (abs_path.empty()) |
| return false; |
| |
| std::string png_data; |
| return base::ReadFileToString(abs_path, &png_data) && |
| gfx::PNGCodec::Decode(reinterpret_cast<unsigned char*>(&png_data[0]), |
| png_data.length(), |
| bitmap); |
| } |
| |
| // Encodes a bitmap into a PNG and write to disk. Returns true on success. The |
| // parent directory does not have to exist. |
| bool WritePNGFile(const SkBitmap& bitmap, const base::FilePath& file_path) { |
| std::vector<unsigned char> png_data; |
| if (gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, true, &png_data) && |
| file_util::CreateDirectory(file_path.DirName())) { |
| int bytes_written = file_util::WriteFile( |
| file_path, reinterpret_cast<char*>(&png_data[0]), png_data.size()); |
| if (bytes_written == static_cast<int>(png_data.size())) |
| return true; |
| } |
| return false; |
| } |
| |
| // Write an empty file, whose name indicates the chrome revision when the ref |
| // image was generated. |
| bool WriteREVFile(const base::FilePath& file_path) { |
| if (file_util::CreateDirectory(file_path.DirName())) { |
| char one_byte = 0; |
| int bytes_written = file_util::WriteFile(file_path, &one_byte, 1); |
| if (bytes_written == 1) |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace anonymous |
| |
| namespace content { |
| |
| // Test fixture for GPU image comparison tests. |
| // TODO(kkania): Document how to add to/modify these tests. |
| class GpuPixelBrowserTest : public ContentBrowserTest { |
| public: |
| GpuPixelBrowserTest() |
| : ref_img_revision_(0), |
| ref_img_revision_no_older_than_(0), |
| ref_img_option_(kReferenceImageNone) { |
| } |
| |
| virtual void SetUp() { |
| // We expect real pixel output for these tests. |
| UseRealGLContexts(); |
| |
| ContentBrowserTest::SetUp(); |
| } |
| |
| virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { |
| command_line->AppendSwitchASCII(switches::kTestGLLib, |
| "libllvmpipe.so"); |
| } |
| |
| virtual void SetUpInProcessBrowserTestFixture() OVERRIDE { |
| ContentBrowserTest::SetUpInProcessBrowserTestFixture(); |
| |
| CommandLine* command_line = CommandLine::ForCurrentProcess(); |
| if (command_line->HasSwitch(switches::kUseGpuInTests)) |
| ref_img_option_ = kReferenceImageLocal; |
| |
| if (command_line->HasSwitch(kBuildRevision)) |
| build_revision_ = command_line->GetSwitchValueASCII(kBuildRevision); |
| |
| ASSERT_TRUE(PathService::Get(DIR_TEST_DATA, &test_data_dir_)); |
| test_data_dir_ = test_data_dir_.AppendASCII("gpu"); |
| |
| if (command_line->HasSwitch(kGeneratedDir)) |
| generated_img_dir_ = command_line->GetSwitchValuePath(kGeneratedDir); |
| else |
| generated_img_dir_ = test_data_dir_.AppendASCII("generated"); |
| |
| switch (ref_img_option_) { |
| case kReferenceImageLocal: |
| if (command_line->HasSwitch(kReferenceDir)) |
| ref_img_dir_ = command_line->GetSwitchValuePath(kReferenceDir); |
| else |
| ref_img_dir_ = test_data_dir_.AppendASCII("gpu_reference"); |
| break; |
| case kReferenceImageCheckedIn: |
| ref_img_dir_ = test_data_dir_.AppendASCII("llvmpipe_reference"); |
| break; |
| default: |
| break; |
| } |
| |
| test_name_ = testing::UnitTest::GetInstance()->current_test_info()->name(); |
| const char* test_status_prefixes[] = { |
| "DISABLED_", "FLAKY_", "FAILS_", "MANUAL_"}; |
| for (size_t i = 0; i < arraysize(test_status_prefixes); ++i) { |
| ReplaceFirstSubstringAfterOffset( |
| &test_name_, 0, test_status_prefixes[i], std::string()); |
| } |
| } |
| |
| // If the existing ref image was saved from an revision older than the |
| // ref_img_update_revision, refresh the ref image. |
| void RunPixelTest(const gfx::Size& tab_container_size, |
| const base::FilePath& url, |
| int64 ref_img_update_revision, |
| const ReferencePixel* ref_pixels, |
| size_t ref_pixel_count) { |
| if (ref_img_option_ == kReferenceImageLocal) { |
| ref_img_revision_no_older_than_ = ref_img_update_revision; |
| ObtainLocalRefImageRevision(); |
| } |
| |
| DOMMessageQueue message_queue; |
| NavigateToURL(shell(), net::FilePathToFileURL(url)); |
| |
| std::string message; |
| // Wait for notification that page is loaded. |
| ASSERT_TRUE(message_queue.WaitForMessage(&message)); |
| EXPECT_STREQ("\"SUCCESS\"", message.c_str()) << message; |
| |
| SkBitmap bitmap; |
| ASSERT_TRUE(TabSnapShotToImage(&bitmap, tab_container_size)); |
| bool same_pixels = true; |
| if (ref_img_option_ == kReferenceImageNone && ref_pixels && ref_pixel_count) |
| same_pixels = ComparePixels(bitmap, ref_pixels, ref_pixel_count); |
| else |
| same_pixels = CompareImages(bitmap); |
| EXPECT_TRUE(same_pixels); |
| } |
| |
| const base::FilePath& test_data_dir() const { |
| return test_data_dir_; |
| } |
| |
| private: |
| base::FilePath test_data_dir_; |
| base::FilePath generated_img_dir_; |
| base::FilePath ref_img_dir_; |
| int64 ref_img_revision_; |
| std::string build_revision_; |
| // The name of the test, with any special prefixes dropped. |
| std::string test_name_; |
| |
| // Any local ref image generated from older revision is ignored. |
| int64 ref_img_revision_no_older_than_; |
| |
| // Whether use locally generated ref images, or checked in ref images, or |
| // simply check a few key pixels. |
| ReferenceImageOption ref_img_option_; |
| |
| // Compares the generated bitmap with the appropriate reference image on disk. |
| // Returns true iff the images were the same. |
| // |
| // If no valid reference image exists, save the generated bitmap to the disk. |
| // The image format is: |
| // <test_name>_<revision>.png |
| // E.g., |
| // WebGLTeapot_19762.png |
| // The number is the chromium revision that generated the image. |
| // |
| // On failure or on ref image generation, the image and diff image will be |
| // written to disk. The formats are: |
| // FAIL_<ref_image_name>, DIFF_<ref_image_name> |
| // E.g., |
| // FAIL_WebGLTeapot_19762.png, DIFF_WebGLTeapot_19762.png |
| bool CompareImages(const SkBitmap& gen_bmp) { |
| SkBitmap ref_bmp_on_disk; |
| |
| base::FilePath img_path = ref_img_dir_.AppendASCII(test_name_ + ".png"); |
| bool found_ref_img = ReadPNGFile(img_path, &ref_bmp_on_disk); |
| |
| if (!found_ref_img && ref_img_option_ == kReferenceImageCheckedIn) { |
| LOG(ERROR) << "Couldn't find reference image: " |
| << img_path.value(); |
| // No image to compare to, exit early. |
| return false; |
| } |
| |
| const SkBitmap* ref_bmp; |
| bool save_gen = false; |
| bool save_diff = true; |
| bool rt = true; |
| |
| if ((ref_img_revision_ <= 0 && ref_img_option_ == kReferenceImageLocal) || |
| !found_ref_img) { |
| base::FilePath rev_path = ref_img_dir_.AppendASCII( |
| test_name_ + "_" + build_revision_ + ".rev"); |
| if (!WritePNGFile(gen_bmp, img_path)) { |
| LOG(ERROR) << "Can't save generated image to: " |
| << img_path.value() |
| << " as future reference."; |
| rt = false; |
| } else { |
| LOG(INFO) << "Saved reference image to: " |
| << img_path.value(); |
| } |
| if (rt) { |
| if (!WriteREVFile(rev_path)) { |
| LOG(ERROR) << "Can't save revision file to: " |
| << rev_path.value(); |
| rt = false; |
| base::DeleteFile(img_path, false); |
| } else { |
| LOG(INFO) << "Saved revision file to: " |
| << rev_path.value(); |
| } |
| } |
| if (ref_img_revision_ > 0) { |
| LOG(ERROR) << "Can't read the local ref image: " |
| << img_path.value() |
| << ", reset it."; |
| rt = false; |
| } |
| // If we re-generate the ref image, we save the gen and diff images so |
| // the ref image can be uploaded to the server and be viewed later. |
| save_gen = true; |
| save_diff = true; |
| ref_bmp = &gen_bmp; |
| } else { |
| ref_bmp = &ref_bmp_on_disk; |
| } |
| |
| SkBitmap diff_bmp; |
| if (ref_bmp->width() != gen_bmp.width() || |
| ref_bmp->height() != gen_bmp.height()) { |
| LOG(ERROR) |
| << "Dimensions do not match (Expected) vs (Actual):" |
| << "(" << ref_bmp->width() << "x" << ref_bmp->height() |
| << ") vs. " |
| << "(" << gen_bmp.width() << "x" << gen_bmp.height() << ")"; |
| if (ref_img_option_ == kReferenceImageLocal) |
| save_gen = true; |
| rt = false; |
| } else { |
| // Compare pixels and create a simple diff image. |
| int diff_pixels_count = 0; |
| diff_bmp.setConfig(SkBitmap::kARGB_8888_Config, |
| gen_bmp.width(), gen_bmp.height()); |
| diff_bmp.allocPixels(); |
| diff_bmp.eraseColor(SK_ColorWHITE); |
| SkAutoLockPixels lock_bmp(gen_bmp); |
| SkAutoLockPixels lock_ref_bmp(*ref_bmp); |
| SkAutoLockPixels lock_diff_bmp(diff_bmp); |
| // The reference images were saved with no alpha channel. Use the mask to |
| // set alpha to 0. |
| uint32_t kAlphaMask = 0x00FFFFFF; |
| for (int x = 0; x < gen_bmp.width(); ++x) { |
| for (int y = 0; y < gen_bmp.height(); ++y) { |
| if ((*gen_bmp.getAddr32(x, y) & kAlphaMask) != |
| (*ref_bmp->getAddr32(x, y) & kAlphaMask)) { |
| ++diff_pixels_count; |
| *diff_bmp.getAddr32(x, y) = 192 << 16; // red |
| } |
| } |
| } |
| if (diff_pixels_count > 0) { |
| LOG(ERROR) << diff_pixels_count |
| << " pixels do not match."; |
| if (ref_img_option_ == kReferenceImageLocal) { |
| save_gen = true; |
| save_diff = true; |
| } |
| rt = false; |
| } |
| } |
| |
| std::string ref_img_filename = img_path.BaseName().MaybeAsASCII(); |
| if (save_gen) { |
| base::FilePath img_fail_path = generated_img_dir_.AppendASCII( |
| "FAIL_" + ref_img_filename); |
| if (!WritePNGFile(gen_bmp, img_fail_path)) { |
| LOG(ERROR) << "Can't save generated image to: " |
| << img_fail_path.value(); |
| } else { |
| LOG(INFO) << "Saved generated image to: " |
| << img_fail_path.value(); |
| } |
| } |
| if (save_diff) { |
| base::FilePath img_diff_path = generated_img_dir_.AppendASCII( |
| "DIFF_" + ref_img_filename); |
| if (!WritePNGFile(diff_bmp, img_diff_path)) { |
| LOG(ERROR) << "Can't save generated diff image to: " |
| << img_diff_path.value(); |
| } else { |
| LOG(INFO) << "Saved difference image to: " |
| << img_diff_path.value(); |
| } |
| } |
| return rt; |
| } |
| |
| bool ComparePixels(const SkBitmap& gen_bmp, |
| const ReferencePixel* ref_pixels, |
| size_t ref_pixel_count) { |
| SkAutoLockPixels lock_bmp(gen_bmp); |
| |
| for (size_t i = 0; i < ref_pixel_count; ++i) { |
| int x = ref_pixels[i].x; |
| int y = ref_pixels[i].y; |
| unsigned char r = ref_pixels[i].r; |
| unsigned char g = ref_pixels[i].g; |
| unsigned char b = ref_pixels[i].b; |
| |
| DCHECK(x >= 0 && x < gen_bmp.width() && y >= 0 && y < gen_bmp.height()); |
| |
| unsigned char* rgba = reinterpret_cast<unsigned char*>( |
| gen_bmp.getAddr32(x, y)); |
| DCHECK(rgba); |
| if (rgba[0] != b || rgba[1] != g || rgba[2] != r) { |
| std::string error_message = base::StringPrintf( |
| "pixel(%d,%d) expects [%u,%u,%u], but gets [%u,%u,%u] instead", |
| x, y, r, g, b, rgba[0], rgba[1], rgba[2]); |
| LOG(ERROR) << error_message.c_str(); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Take snapshot of the tab, encode it as PNG, and save to a SkBitmap. |
| bool TabSnapShotToImage(SkBitmap* bitmap, const gfx::Size& size) { |
| CHECK(bitmap); |
| std::vector<unsigned char> png; |
| |
| gfx::Rect snapshot_bounds(size); |
| RenderViewHost* view_host = shell()->web_contents()->GetRenderViewHost(); |
| if (!ui::GrabViewSnapshot(view_host->GetView()->GetNativeView(), |
| &png, snapshot_bounds)) { |
| LOG(ERROR) << "ui::GrabViewSnapShot() failed"; |
| return false; |
| } |
| |
| if (!gfx::PNGCodec::Decode(reinterpret_cast<unsigned char*>(&*png.begin()), |
| png.size(), bitmap)) { |
| LOG(ERROR) << "Decode PNG to a SkBitmap failed"; |
| return false; |
| } |
| return true; |
| } |
| |
| // If no valid local revision file is located, the ref_img_revision_ is 0. |
| void ObtainLocalRefImageRevision() { |
| base::FilePath filter; |
| filter = filter.AppendASCII(test_name_ + "_*.rev"); |
| base::FileEnumerator locator(ref_img_dir_, |
| false, // non recursive |
| base::FileEnumerator::FILES, |
| filter.value()); |
| int64 max_revision = 0; |
| std::vector<base::FilePath> outdated_revs; |
| for (base::FilePath full_path = locator.Next(); |
| !full_path.empty(); |
| full_path = locator.Next()) { |
| std::string filename = |
| full_path.BaseName().RemoveExtension().MaybeAsASCII(); |
| std::string revision_string = |
| filename.substr(test_name_.length() + 1); |
| int64 revision = 0; |
| bool converted = base::StringToInt64(revision_string, &revision); |
| if (!converted) |
| continue; |
| if (revision < ref_img_revision_no_older_than_ || |
| revision < max_revision) { |
| outdated_revs.push_back(full_path); |
| continue; |
| } |
| max_revision = revision; |
| } |
| ref_img_revision_ = max_revision; |
| for (size_t i = 0; i < outdated_revs.size(); ++i) |
| base::DeleteFile(outdated_revs[i], false); |
| } |
| |
| DISALLOW_COPY_AND_ASSIGN(GpuPixelBrowserTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(GpuPixelBrowserTest, MANUAL_WebGLGreenTriangle) { |
| // If test baseline needs to be updated after a given revision, update the |
| // following number. If no revision requirement, then 0. |
| const int64 ref_img_revision_update = 123489; |
| |
| const ReferencePixel ref_pixels[] = { |
| // x, y, r, g, b |
| {50, 100, 0, 0, 0}, |
| {100, 100, 0, 255, 0}, |
| {150, 100, 0, 0, 0}, |
| {50, 150, 0, 255, 0}, |
| {100, 150, 0, 255, 0}, |
| {150, 150, 0, 255, 0} |
| }; |
| const size_t ref_pixel_count = sizeof(ref_pixels) / sizeof(ReferencePixel); |
| |
| gfx::Size container_size(400, 300); |
| base::FilePath url = |
| test_data_dir().AppendASCII("pixel_webgl.html"); |
| RunPixelTest(container_size, url, ref_img_revision_update, |
| ref_pixels, ref_pixel_count); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(GpuPixelBrowserTest, MANUAL_CSS3DBlueBox) { |
| // If test baseline needs to be updated after a given revision, update the |
| // following number. If no revision requirement, then 0. |
| const int64 ref_img_revision_update = 226592; |
| |
| const ReferencePixel ref_pixels[] = { |
| // x, y, r, g, b |
| {70, 50, 0, 0, 255}, |
| {150, 50, 0, 0, 0}, |
| {70, 90, 0, 0, 255}, |
| {150, 90, 0, 0, 255}, |
| {70, 125, 0, 0, 255}, |
| {150, 125, 0, 0, 0} |
| }; |
| const size_t ref_pixel_count = sizeof(ref_pixels) / sizeof(ReferencePixel); |
| |
| gfx::Size container_size(400, 300); |
| base::FilePath url = |
| test_data_dir().AppendASCII("pixel_css3d.html"); |
| RunPixelTest(container_size, url, ref_img_revision_update, |
| ref_pixels, ref_pixel_count); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(GpuPixelBrowserTest, MANUAL_Canvas2DRedBoxHD) { |
| // If test baseline needs to be updated after a given revision, update the |
| // following number. If no revision requirement, then 0. |
| const int64 ref_img_revision_update = 224170; |
| |
| const ReferencePixel ref_pixels[] = { |
| // x, y, r, g, b |
| {40, 100, 0, 0, 0}, |
| {60, 100, 127, 0, 0}, |
| {140, 100, 127, 0, 0}, |
| {160, 100, 0, 0, 0} |
| }; |
| const size_t ref_pixel_count = sizeof(ref_pixels) / sizeof(ReferencePixel); |
| |
| gfx::Size container_size(400, 300); |
| base::FilePath url = |
| test_data_dir().AppendASCII("pixel_canvas2d.html"); |
| RunPixelTest(container_size, url, ref_img_revision_update, |
| ref_pixels, ref_pixel_count); |
| } |
| |
| class GpuPixelTestCanvas2DSD : public GpuPixelBrowserTest { |
| public: |
| virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { |
| GpuPixelBrowserTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitch(switches::kDisableAccelerated2dCanvas); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(GpuPixelTestCanvas2DSD, MANUAL_Canvas2DRedBoxSD) { |
| // If test baseline needs to be updated after a given revision, update the |
| // following number. If no revision requirement, then 0. |
| const int64 ref_img_revision_update = 224170; |
| |
| const ReferencePixel ref_pixels[] = { |
| // x, y, r, g, b |
| {40, 100, 0, 0, 0}, |
| {60, 100, 127, 0, 0}, |
| {140, 100, 127, 0, 0}, |
| {160, 100, 0, 0, 0} |
| }; |
| const size_t ref_pixel_count = sizeof(ref_pixels) / sizeof(ReferencePixel); |
| |
| gfx::Size container_size(400, 300); |
| base::FilePath url = |
| test_data_dir().AppendASCII("pixel_canvas2d.html"); |
| RunPixelTest(container_size, url, ref_img_revision_update, |
| ref_pixels, ref_pixel_count); |
| } |
| |
| class GpuPixelTestBrowserPlugin : public GpuPixelBrowserTest { |
| public: |
| virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { |
| GpuPixelBrowserTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitch(switches::kEnableBrowserPluginForAllViewTypes); |
| } |
| }; |
| |
| // TODO(fsamuel): re-enable as MANUAL_BrowserPluginBlueBox: crbug.com/166165 |
| IN_PROC_BROWSER_TEST_F(GpuPixelTestBrowserPlugin, |
| DISABLED_BrowserPluginBlueBox) { |
| // If test baseline needs to be updated after a given revision, update the |
| // following number. If no revision requirement, then 0. |
| const int64 ref_img_revision_update = 209445; |
| |
| const ReferencePixel ref_pixels[] = { |
| // x, y, r, g, b |
| {70, 50, 0, 0, 255}, |
| {150, 50, 0, 0, 0}, |
| {70, 90, 0, 0, 255}, |
| {150, 90, 0, 0, 255}, |
| {70, 125, 0, 0, 255}, |
| {150, 125, 0, 0, 0} |
| }; |
| const size_t ref_pixel_count = sizeof(ref_pixels) / sizeof(ReferencePixel); |
| |
| gfx::Size container_size(400, 300); |
| base::FilePath url = |
| test_data_dir().AppendASCII("pixel_browser_plugin.html"); |
| RunPixelTest(container_size, url, ref_img_revision_update, |
| ref_pixels, ref_pixel_count); |
| } |
| |
| } // namespace content |
| |