blob: a0a9683e2cd292d40d4ae446a4fa52ba3882f33c [file] [log] [blame]
// Copyright 2019 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 "ui/base/test/skia_gold_pixel_diff.h"
#include "build/build_config.h"
#if defined(OS_WIN)
#include <windows.h>
#endif
#include "third_party/skia/include/core/SkBitmap.h"
#include "base/command_line.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/image/image.h"
#include "ui/snapshot/snapshot.h"
const char* kSkiaGoldInstance = "chrome";
#if defined(OS_WIN)
const wchar_t* kSkiaGoldCtl = L"tools/skia_goldctl/win/goldctl.exe";
#elif defined(OS_MACOSX)
const char* kSkiaGoldCtl = "tools/skia_goldctl/mac/goldctl";
#else
const char* kSkiaGoldCtl = "tools/skia_goldctl/linux/goldctl";
#endif
const char* kBuildRevisionKey = "build-revision";
// The switch keys for tryjob.
const char* kIssueKey = "issue";
const char* kPatchSetKey = "patchset";
const char* kJobIdKey = "jobid";
const char* kNoLuciAuth = "no-luci-auth";
SkiaGoldPixelDiff::SkiaGoldPixelDiff() = default;
SkiaGoldPixelDiff::~SkiaGoldPixelDiff() = default;
base::FilePath GetAbsoluteSrcRelativePath(base::FilePath::StringType path) {
base::FilePath root_path;
base::PathService::Get(base::BasePathKey::DIR_SOURCE_ROOT, &root_path);
return base::MakeAbsoluteFilePath(root_path.Append(path));
}
// Append args after program.
// The base::Commandline.AppendArg append the arg at
// the end which doesn't work for us.
void AppendArgsJustAfterProgram(base::CommandLine& cmd,
base::CommandLine::StringVector args) {
base::CommandLine::StringVector& argv =
const_cast<base::CommandLine::StringVector&>(cmd.argv());
int args_size = args.size();
argv.resize(argv.size() + args_size);
for (int i = argv.size() - args_size; i > 1; --i) {
argv[i + args_size - 1] = argv[i - 1];
}
argv.insert(argv.begin() + 1, args.begin(), args.end());
}
// Fill in test environment to the keys_file. The format is json.
// We need the system information to determine whether a new screenshot
// is good or not. All the information that can affect the output of pixels
// should be filled in. Eg: operating system, graphics card, processor
// architecture, screen resolution, etc.
bool FillInTestEnvironment(const base::FilePath& keys_file) {
std::string system = "unknown";
std::string processor = "unknown";
#if defined(OS_WIN)
system = "windows";
SYSTEM_INFO system_info;
GetSystemInfo(&system_info);
switch (system_info.wProcessorArchitecture) {
case PROCESSOR_ARCHITECTURE_INTEL:
processor = "x86";
break;
case PROCESSOR_ARCHITECTURE_AMD64:
processor = "x86_64";
break;
case PROCESSOR_ARCHITECTURE_IA64:
processor = "ia_64";
break;
case PROCESSOR_ARCHITECTURE_ARM:
processor = "arm";
break;
}
#else
LOG(WARNING) << "Other OS not implemented.";
#endif
base::Value::DictStorage ds;
ds["system"] = std::make_unique<base::Value>(system);
ds["processor"] = std::make_unique<base::Value>(processor);
base::Value root(std::move(ds));
std::string content;
base::JSONWriter::Write(root, &content);
base::ScopedAllowBlockingForTesting allow_blocking;
base::File file(keys_file, base::File::Flags::FLAG_CREATE_ALWAYS |
base::File::Flags::FLAG_WRITE);
int ret_code = file.Write(0, content.c_str(), content.size());
file.Close();
if (ret_code <= 0) {
LOG(ERROR) << "Writing the keys file to temporary file failed."
<< "File path:" << keys_file.AsUTF8Unsafe()
<< ". Return code: " << ret_code;
return false;
}
return true;
}
int SkiaGoldPixelDiff::LaunchProcess(const base::CommandLine& cmdline) const {
base::Process sub_process =
base::LaunchProcess(cmdline, base::LaunchOptionsForTest());
int exit_code = 0;
if (!sub_process.WaitForExit(&exit_code)) {
ADD_FAILURE() << "Failed to wait for process.";
// Return a non zero code indicating an error.
return 1;
}
return exit_code;
}
void SkiaGoldPixelDiff::InitSkiaGold() {
base::ScopedAllowBlockingForTesting allow_blocking;
base::CommandLine cmd(GetAbsoluteSrcRelativePath(kSkiaGoldCtl));
cmd.AppendSwitchPath("work-dir", working_dir_);
if (luci_auth_) {
cmd.AppendArg("--luci");
}
AppendArgsJustAfterProgram(cmd, {FILE_PATH_LITERAL("auth")});
base::CommandLine::StringType cmd_str = cmd.GetCommandLineString();
LOG(INFO) << "Skia Gold Auth Commandline: " << cmd_str;
int exit_code = LaunchProcess(cmd);
ASSERT_EQ(exit_code, 0);
base::FilePath json_temp_file =
working_dir_.Append(FILE_PATH_LITERAL("keys_file.txt"));
FillInTestEnvironment(json_temp_file);
base::FilePath failure_temp_file =
working_dir_.Append(FILE_PATH_LITERAL("failure.log"));
cmd = base::CommandLine(GetAbsoluteSrcRelativePath(kSkiaGoldCtl));
cmd.AppendSwitchASCII("instance", kSkiaGoldInstance);
cmd.AppendSwitchPath("work-dir", working_dir_);
cmd.AppendSwitchPath("keys-file", json_temp_file);
cmd.AppendSwitchPath("failure-file", failure_temp_file);
cmd.AppendSwitch("passfail");
cmd.AppendSwitchASCII("commit", build_revision_);
if (issue_.length()) {
cmd.AppendSwitchASCII("issue", issue_);
cmd.AppendSwitchASCII("patchset", patchset_);
cmd.AppendSwitchASCII("jobid", job_id_);
}
AppendArgsJustAfterProgram(
cmd, {FILE_PATH_LITERAL("imgtest"), FILE_PATH_LITERAL("init")});
cmd_str = cmd.GetCommandLineString();
LOG(INFO) << "Skia Gold imgtest init Commandline: " << cmd_str;
exit_code = LaunchProcess(cmd);
ASSERT_EQ(exit_code, 0);
}
void SkiaGoldPixelDiff::Init(const std::string& screenshot_prefix) {
auto* cmd_line = base::CommandLine::ForCurrentProcess();
ASSERT_TRUE(cmd_line->HasSwitch(kBuildRevisionKey))
<< "Missing switch " << kBuildRevisionKey;
ASSERT_TRUE(
cmd_line->HasSwitch(kIssueKey) && cmd_line->HasSwitch(kPatchSetKey) &&
cmd_line->HasSwitch(kJobIdKey) ||
!cmd_line->HasSwitch(kIssueKey) && !cmd_line->HasSwitch(kPatchSetKey) &&
!cmd_line->HasSwitch(kJobIdKey))
<< "Missing switch. If it's running for tryjob, you should pass --"
<< kIssueKey << " --" << kPatchSetKey << " --" << kJobIdKey
<< ". Otherwise, do not pass any one of them.";
build_revision_ = cmd_line->GetSwitchValueASCII(kBuildRevisionKey);
if (cmd_line->HasSwitch(kIssueKey)) {
issue_ = cmd_line->GetSwitchValueASCII(kIssueKey);
patchset_ = cmd_line->GetSwitchValueASCII(kPatchSetKey);
job_id_ = cmd_line->GetSwitchValueASCII(kJobIdKey);
}
if (cmd_line->HasSwitch(kNoLuciAuth)) {
luci_auth_ = false;
}
initialized_ = true;
prefix_ = screenshot_prefix;
base::CreateNewTempDirectory(FILE_PATH_LITERAL("SkiaGoldTemp"),
&working_dir_);
InitSkiaGold();
}
bool SkiaGoldPixelDiff::UploadToSkiaGoldServer(
const base::FilePath& local_file_path,
const std::string& remote_golden_image_name) const {
base::ScopedAllowBlockingForTesting allow_blocking;
base::CommandLine cmd(GetAbsoluteSrcRelativePath(kSkiaGoldCtl));
cmd.AppendSwitchASCII("test-name", remote_golden_image_name);
cmd.AppendSwitchASCII("add-test-key", "source_type:gtest-pixeltests");
cmd.AppendSwitchPath("png-file", local_file_path);
cmd.AppendSwitchPath("work-dir", working_dir_);
AppendArgsJustAfterProgram(
cmd, {FILE_PATH_LITERAL("imgtest"), FILE_PATH_LITERAL("add")});
base::CommandLine::StringType cmd_str = cmd.GetCommandLineString();
LOG(INFO) << "Skia Gold Commandline: " << cmd_str;
int exit_code = LaunchProcess(cmd);
return exit_code == 0;
}
bool SkiaGoldPixelDiff::CompareScreenshot(const std::string& screenshot_name,
const SkBitmap& bitmap) const {
DCHECK(Initialized()) << "Initialize the class before using this method.";
std::vector<unsigned char> output;
bool ret = gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, true, &output);
if (!ret) {
LOG(ERROR) << "Encoding SkBitmap to PNG format failed.";
return false;
}
// The golden image name should be unique on GCS. And also the name
// should be valid across all systems.
std::string name = prefix_ + "_" + screenshot_name;
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath temporary_path =
working_dir_.Append(base::FilePath::FromUTF8Unsafe(name + ".png"));
base::File file(temporary_path, base::File::Flags::FLAG_CREATE_ALWAYS |
base::File::Flags::FLAG_WRITE);
int ret_code = file.Write(0, (char*)output.data(), output.size());
file.Close();
if (ret_code <= 0) {
LOG(ERROR) << "Writing the PNG image to temporary file failed."
<< "File path:" << temporary_path.AsUTF8Unsafe()
<< ". Return code: " << ret_code;
return false;
}
return UploadToSkiaGoldServer(temporary_path, name);
}