blob: 2a4a14b8e39738d3f9cebb5eec3d8b05a3cb23fa [file] [log] [blame]
/*
* Copyright 2023 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* This program runs all GMs registered via macros such as DEF_GM, and for each GM, it saves the
* resulting SkBitmap as a .png file to disk, along with a .json file with the hash of the pixels.
*/
#include "gm/gm.h"
#include "gm/surface_manager/SurfaceManager.h"
#include "gm/vias/Draw.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkColorType.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkStream.h"
#include "include/core/SkSurface.h"
#include "include/encode/SkPngEncoder.h"
#include "include/private/base/SkAssert.h"
#include "include/private/base/SkDebug.h"
#include "src/core/SkMD5.h"
#include "src/utils/SkJSONWriter.h"
#include "src/utils/SkOSPath.h"
#include "tools/HashAndEncode.h"
#include <ctime>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
// TODO(lovisolo): Add flag --skip.
// TODO(lovisolo): Add flag --omitDigestIfHashInFile (provides the known hashes file).
// When running under Bazel and overriding the output directory, you might encounter errors such
// as "No such file or directory" and "Read-only file system". The former can happen when running
// on RBE because the passed in output dir might not exist on the remote worker, whereas the latter
// can happen when running locally in sandboxed mode, which is the default strategy when running
// outside of RBE. One possible workaround is to run the test as a local subprocess, which can be
// done by passing flag --strategy=TestRunner=local to Bazel.
//
// Reference: https://bazel.build/docs/user-manual#execution-strategy.
static DEFINE_string(outputDir,
"",
"Directory where to write any output .png and .json files. "
"Optional when running under Bazel "
"(e.g. \"bazel test //path/to:test\") as it defaults to "
"$TEST_UNDECLARED_OUTPUTS_DIR.");
// We named this flag --surfaceConfig rather than --config to avoid confusion with the --config
// Bazel flag.
static DEFINE_string(surfaceConfig,
"",
"Name of the Surface configuration to use (e.g. \"8888\"). This determines "
"how we construct the SkSurface from which we get the SkCanvas that GMs will "
"draw on. See file //gm/surface_manager/SurfaceManager.h for details.");
static DEFINE_string(via,
"direct", // Equivalent to running DM without a via.
"Name of the \"via\" to use (e.g. \"picture_serialization\"). Optional.");
// Takes a SkBitmap and writes the resulting PNG and MD5 hash into the given files. Returns an
// empty string on success, or an error message in the case of failures.
static std::string write_png_and_json_files(std::string name,
std::map<std::string, std::string> gmGoldKeys,
std::map<std::string, std::string> surfaceGoldKeys,
const SkBitmap& bitmap,
const char* pngPath,
const char* jsonPath) {
HashAndEncode hashAndEncode(bitmap);
// Compute MD5 hash.
SkMD5 hash;
hashAndEncode.feedHash(&hash);
SkMD5::Digest digest = hash.finish();
SkString md5 = digest.toLowercaseHexString();
// Write PNG file.
SkFILEWStream pngFile(pngPath);
bool result = hashAndEncode.encodePNG(&pngFile,
md5.c_str(),
/* key= */ CommandLineFlags::StringArray(),
/* properties= */ CommandLineFlags::StringArray());
if (!result) {
return "Error encoding or writing PNG to " + std::string(pngPath);
}
// Validate GM-related Gold keys.
if (gmGoldKeys.find("name") == gmGoldKeys.end()) {
SK_ABORT("gmGoldKeys does not contain key \"name\"");
}
if (gmGoldKeys.find("source_type") == gmGoldKeys.end()) {
SK_ABORT("gmGoldKeys does not contain key \"source_type\"");
}
// Validate surface-related Gold keys.
if (surfaceGoldKeys.find("surface_config") == surfaceGoldKeys.end()) {
SK_ABORT("surfaceGoldKeys does not contain key \"surface_config\"");
}
// Gather all Gold keys.
std::map<std::string, std::string> keys = {
{"build_system", "bazel"},
};
keys.merge(surfaceGoldKeys);
keys.merge(gmGoldKeys);
// Write JSON file with MD5 hash and Gold key-value pairs.
SkFILEWStream jsonFile(jsonPath);
SkJSONWriter jsonWriter(&jsonFile, SkJSONWriter::Mode::kPretty);
jsonWriter.beginObject(); // Root object.
jsonWriter.appendString("md5", md5);
jsonWriter.beginObject("keys"); // "keys" dictionary.
for (auto const& [param, value] : keys) {
jsonWriter.appendString(param.c_str(), SkString(value));
}
jsonWriter.endObject(); // "keys" dictionary.
jsonWriter.endObject(); // Root object.
return "";
}
static std::string now() {
std::time_t t = std::time(nullptr);
std::tm* now = std::gmtime(&t);
std::ostringstream oss;
oss << std::put_time(now, "%Y-%m-%d %H:%M:%S UTC");
return oss.str();
}
static std::string draw_result_to_string(skiagm::DrawResult result) {
switch (result) {
case skiagm::DrawResult::kOk:
return "Ok";
case skiagm::DrawResult::kFail:
return "Fail";
case skiagm::DrawResult::kSkip:
return "Skip";
default:
SkUNREACHABLE;
}
}
static int gNumSuccessfulGMs = 0;
static int gNumFailedGMs = 0;
static int gNumSkippedGMs = 0;
// Runs a GM under the given surface config, and saves its output PNG file (and accompanying JSON
// file with metadata) to the given output directory.
void run_gm(std::unique_ptr<skiagm::GM> gm, std::string config, std::string outputDir) {
SkDebugf("[%s] GM: %s\n", now().c_str(), gm->getName().c_str());
// Create surface and canvas.
std::unique_ptr<SurfaceManager> surfaceManager =
SurfaceManager::FromConfig(config, gm->getISize().width(), gm->getISize().height());
if (surfaceManager == nullptr) {
SK_ABORT("Unknown --surfaceConfig flag value: %s.", config.c_str());
}
// Set up GPU.
SkDebugf("[%s] Setting up GPU...\n", now().c_str());
SkString msg;
skiagm::DrawResult result = gm->gpuSetup(surfaceManager->getSurface()->getCanvas(), &msg);
// Draw GM into canvas if GPU setup was successful.
SkBitmap bitmap;
if (result == skiagm::DrawResult::kOk) {
GMOutput output;
std::string viaName = FLAGS_via.size() == 0 ? "" : (FLAGS_via[0]);
SkDebugf("[%s] Drawing GM via \"%s\"...\n", now().c_str(), viaName.c_str());
output = draw(gm.get(), surfaceManager->getSurface().get(), viaName);
result = output.result;
msg = SkString(output.msg.c_str());
bitmap = output.bitmap;
}
// Keep track of results. We will exit with a non-zero exit code in the case of failures.
switch (result) {
case skiagm::DrawResult::kOk:
// We don't increment numSuccessfulGMs just yet. We still need to successfully save
// its output bitmap to disk.
SkDebugf("[%s] Flushing surface...\n", now().c_str());
surfaceManager->flush();
break;
case skiagm::DrawResult::kFail:
gNumFailedGMs++;
break;
case skiagm::DrawResult::kSkip:
gNumSkippedGMs++;
break;
default:
SkUNREACHABLE;
}
// Report GM result and optional message.
SkDebugf("[%s] Result: %s\n", now().c_str(), draw_result_to_string(result).c_str());
if (!msg.isEmpty()) {
SkDebugf("[%s] Message: \"%s\"\n", now().c_str(), msg.c_str());
}
// Save PNG and JSON file with MD5 hash to disk if the GM was successful.
if (result == skiagm::DrawResult::kOk) {
std::string name = std::string(gm->getName().c_str());
SkString pngPath = SkOSPath::Join(outputDir.c_str(), (name + ".png").c_str());
SkString jsonPath = SkOSPath::Join(outputDir.c_str(), (name + ".json").c_str());
std::string pngAndJSONResult = write_png_and_json_files(gm->getName().c_str(),
gm->getGoldKeys(),
surfaceManager->getGoldKeys(),
bitmap,
pngPath.c_str(),
jsonPath.c_str());
if (pngAndJSONResult != "") {
SkDebugf("[%s] %s\n", now().c_str(), pngAndJSONResult.c_str());
gNumFailedGMs++;
} else {
gNumSuccessfulGMs++;
SkDebugf("[%s] PNG file written to: %s\n", now().c_str(), pngPath.c_str());
SkDebugf("[%s] JSON file written to: %s\n", now().c_str(), jsonPath.c_str());
}
}
}
int main(int argc, char** argv) {
#ifdef SK_BUILD_FOR_ANDROID
extern bool gSkDebugToStdOut; // If true, sends SkDebugf to stdout as well.
gSkDebugToStdOut = true;
#endif
// Print command-line for debugging purposes.
if (argc < 2) {
SkDebugf("GM runner invoked with no arguments.\n");
} else {
SkDebugf("GM runner invoked with arguments:");
for (int i = 1; i < argc; i++) {
SkDebugf(" %s", argv[i]);
}
SkDebugf("\n");
}
// When running under Bazel (e.g. "bazel test //path/to:test"), we'll store output files in
// $TEST_UNDECLARED_OUTPUTS_DIR unless overridden via the --outputDir flag.
//
// See https://bazel.build/reference/test-encyclopedia#initial-conditions.
std::string testUndeclaredOutputsDir;
if (char* envVar = std::getenv("TEST_UNDECLARED_OUTPUTS_DIR")) {
testUndeclaredOutputsDir = envVar;
}
bool isBazelTest = !testUndeclaredOutputsDir.empty();
// Parse and validate flags.
CommandLineFlags::Parse(argc, argv);
if (!isBazelTest && FLAGS_outputDir.isEmpty()) {
SK_ABORT("Flag --outputDir cannot be empty.");
}
if (FLAGS_outputDir.size() > 1) {
SK_ABORT("Flag --outputDir takes one single value, got %d.", FLAGS_outputDir.size());
}
if (FLAGS_surfaceConfig.isEmpty()) {
SK_ABORT("Flag --surfaceConfig cannot be empty.");
}
if (FLAGS_surfaceConfig.size() > 1) {
SK_ABORT("Flag --surfaceConfig takes one single value, got %d.",
FLAGS_surfaceConfig.size());
}
if (FLAGS_via.size() > 1) {
SK_ABORT("Flag --via takes at most one value, got %d.", FLAGS_via.size());
}
std::string outputDir =
FLAGS_outputDir.isEmpty() ? testUndeclaredOutputsDir : FLAGS_outputDir[0];
std::string config = FLAGS_surfaceConfig[0];
// Execute all GM registerer functions, then run all registered GMs.
for (const skiagm::GMRegistererFn& f : skiagm::GMRegistererFnRegistry::Range()) {
std::string errorMsg = f();
if (errorMsg != "") {
SK_ABORT("Error while gathering GMs: %s", errorMsg.c_str());
}
}
for (const skiagm::GMFactory& f : skiagm::GMRegistry::Range()) {
run_gm(f(), config, outputDir);
}
// TODO(lovisolo): If running under Bazel, print command to display output files.
SkDebugf(gNumFailedGMs > 0 ? "FAIL\n" : "PASS\n");
SkDebugf("%d successful GMs (images written to %s).\n", gNumSuccessfulGMs, outputDir.c_str());
SkDebugf("%d failed GMs.\n", gNumFailedGMs);
SkDebugf("%d skipped GMs.\n", gNumSkippedGMs);
return gNumFailedGMs > 0 ? 1 : 0;
}