Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
BazelGMTestRunner.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2023 Google LLC
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 *
7 * This program runs all GMs registered via macros such as DEF_GM, and for each GM, it saves the
8 * resulting SkBitmap as a .png file to disk, along with a .json file with the hash of the pixels.
9 */
10
11#include "gm/gm.h"
22#include "src/core/SkMD5.h"
24#include "src/utils/SkOSPath.h"
25#include "tools/HashAndEncode.h"
30
31#include <algorithm>
32#include <ctime>
33#include <filesystem>
34#include <fstream>
35#include <iomanip>
36#include <iostream>
37#include <regex>
38#include <set>
39#include <sstream>
40#include <string>
41
42// TODO(lovisolo): Add flag --omitDigestIfHashInFile (provides the known hashes file).
43
44static DEFINE_string(skip, "", "Space-separated list of test cases (regexps) to skip.");
46 match,
47 "",
48 "Space-separated list of test cases (regexps) to run. Will run all tests if omitted.");
49
50// When running under Bazel and overriding the output directory, you might encounter errors such
51// as "No such file or directory" and "Read-only file system". The former can happen when running
52// on RBE because the passed in output dir might not exist on the remote worker, whereas the latter
53// can happen when running locally in sandboxed mode, which is the default strategy when running
54// outside of RBE. One possible workaround is to run the test as a local subprocess, which can be
55// done by passing flag --strategy=TestRunner=local to Bazel.
56//
57// Reference: https://bazel.build/docs/user-manual#execution-strategy.
58static DEFINE_string(outputDir,
59 "",
60 "Directory where to write any output .png and .json files. "
61 "Optional when running under Bazel "
62 "(e.g. \"bazel test //path/to:test\") as it defaults to "
63 "$TEST_UNDECLARED_OUTPUTS_DIR.");
64
65static DEFINE_string(knownDigestsFile,
66 "",
67 "Plaintext file with one MD5 hash per line. This test runner will omit from "
68 "the output directory any images with an MD5 hash in this file.");
69
70static DEFINE_string(key, "", "Space-separated key/value pairs common to all traces.");
71
72// We named this flag --surfaceConfig rather than --config to avoid confusion with the --config
73// Bazel flag.
75 surfaceConfig,
76 "",
77 "Name of the Surface configuration to use (e.g. \"8888\"). This determines "
78 "how we construct the SkSurface from which we get the SkCanvas that GMs will "
79 "draw on. See file //tools/testrunners/common/surface_manager/SurfaceManager.h for "
80 "details.");
81
83 cpuName,
84 "",
85 "Contents of the \"cpu_or_gpu_value\" dimension for CPU-bound traces (e.g. \"AVX512\").");
86
88 gpuName,
89 "",
90 "Contents of the \"cpu_or_gpu_value\" dimension for GPU-bound traces (e.g. \"RTX3060\").");
91
92static DEFINE_string(via,
93 "direct", // Equivalent to running DM without a via.
94 "Name of the \"via\" to use (e.g. \"picture_serialization\"). Optional.");
95
96// Set in //bazel/devicesrc but only consumed by adb_test_runner.go. We cannot use the
97// DEFINE_string macro because the flag name includes dashes.
98[[maybe_unused]] static bool unused =
99 SkFlagInfo::CreateStringFlag("device-specific-bazel-config",
100 nullptr,
102 nullptr,
103 "Ignored by this test runner.",
104 nullptr);
105
106// Return type for function write_png_and_json_files().
109 std::string errorMsg = "";
110 std::string skippedDigest = "";
111};
112
113// Takes a SkBitmap and writes the resulting PNG and MD5 hash into the given files.
115 std::string name,
116 std::map<std::string, std::string> commonKeys,
117 std::map<std::string, std::string> gmGoldKeys,
118 std::map<std::string, std::string> surfaceGoldKeys,
119 const SkBitmap& bitmap,
120 const char* pngPath,
121 const char* jsonPath,
122 std::set<std::string> knownDigests) {
123 HashAndEncode hashAndEncode(bitmap);
124
125 // Compute MD5 hash.
126 SkMD5 hash;
127 hashAndEncode.feedHash(&hash);
128 SkMD5::Digest digest = hash.finish();
130
131 // Skip this digest if it's known.
132 if (knownDigests.find(md5.c_str()) != knownDigests.end()) {
133 return {
135 .skippedDigest = md5.c_str(),
136 };
137 }
138
139 // Write PNG file.
140 SkFILEWStream pngFile(pngPath);
141 bool result = hashAndEncode.encodePNG(&pngFile,
142 md5.c_str(),
144 /* properties= */ CommandLineFlags::StringArray());
145 if (!result) {
146 return {
148 .errorMsg = "Error encoding or writing PNG to " + std::string(pngPath),
149 };
150 }
151
152 // Validate GM-related Gold keys.
153 if (gmGoldKeys.find("name") == gmGoldKeys.end()) {
154 SK_ABORT("gmGoldKeys does not contain key \"name\"");
155 }
156 if (gmGoldKeys.find("source_type") == gmGoldKeys.end()) {
157 SK_ABORT("gmGoldKeys does not contain key \"source_type\"");
158 }
159
160 // Validate surface-related Gold keys.
161 if (surfaceGoldKeys.find("surface_config") == surfaceGoldKeys.end()) {
162 SK_ABORT("surfaceGoldKeys does not contain key \"surface_config\"");
163 }
164
165 // Gather all Gold keys.
166 std::map<std::string, std::string> keys = {
167 {"build_system", "bazel"},
168 };
170 keys.merge(commonKeys);
171 keys.merge(surfaceGoldKeys);
172 keys.merge(gmGoldKeys);
173
174 // Write JSON file with MD5 hash and Gold key-value pairs.
175 SkFILEWStream jsonFile(jsonPath);
176 SkJSONWriter jsonWriter(&jsonFile, SkJSONWriter::Mode::kPretty);
177 jsonWriter.beginObject(); // Root object.
178 jsonWriter.appendString("md5", md5);
179 jsonWriter.beginObject("keys"); // "keys" dictionary.
180 for (auto const& [param, value] : keys) {
181 jsonWriter.appendString(param.c_str(), SkString(value));
182 }
183 jsonWriter.endObject(); // "keys" dictionary.
184 jsonWriter.endObject(); // Root object.
185
186 return {.status = WritePNGAndJSONFilesResult::kSuccess};
187}
188
190 switch (result) {
192 return "Ok";
194 return "Fail";
196 return "Skip";
197 default:
199 }
200}
201
202static int gNumSuccessfulGMs = 0;
203static int gNumFailedGMs = 0;
204static int gNumSkippedGMs = 0;
205
207
208// Runs a GM under the given surface config, and saves its output PNG file (and accompanying JSON
209// file with metadata) to the given output directory.
210void run_gm(std::unique_ptr<skiagm::GM> gm,
211 std::string config,
212 std::map<std::string, std::string> keyValuePairs,
213 std::string cpuName,
214 std::string gpuName,
215 std::string outputDir,
216 std::set<std::string> knownDigests) {
217 TestRunner::Log("GM: %s", gm->getName().c_str());
218
219 // Create surface and canvas.
220 std::unique_ptr<SurfaceManager> surfaceManager = SurfaceManager::FromConfig(
221 config, SurfaceOptions{gm->getISize().width(), gm->getISize().height()});
222 if (surfaceManager == nullptr) {
223 SK_ABORT("Unknown --surfaceConfig flag value: %s.", config.c_str());
224 }
225
226 // Print warning about missing cpu_or_gpu key if necessary.
227 if ((surfaceManager->isCpuOrGpuBound() == SurfaceManager::CpuOrGpu::kCPU && cpuName == "" &&
230 "\tWarning: The surface is CPU-bound, but flag --cpuName was not provided. "
231 "Gold traces will omit keys \"cpu_or_gpu\" and \"cpu_or_gpu_value\".");
233 }
234 if ((surfaceManager->isCpuOrGpuBound() == SurfaceManager::CpuOrGpu::kGPU && gpuName == "" &&
237 "\tWarning: The surface is GPU-bound, but flag --gpuName was not provided. "
238 "Gold traces will omit keys \"cpu_or_gpu\" and \"cpu_or_gpu_value\".");
240 }
241
242 // Set up GPU.
243 TestRunner::Log("\tSetting up GPU...");
244 SkString msg;
245 skiagm::DrawResult result = gm->gpuSetup(surfaceManager->getSurface()->getCanvas(), &msg);
246
247 // Draw GM into canvas if GPU setup was successful.
250 GMOutput output;
251 std::string viaName = FLAGS_via.size() == 0 ? "" : (FLAGS_via[0]);
252 TestRunner::Log("\tDrawing GM via \"%s\"...", viaName.c_str());
253 output = draw(gm.get(), surfaceManager->getSurface().get(), viaName);
254 result = output.result;
255 msg = SkString(output.msg.c_str());
256 bitmap = output.bitmap;
257 }
258
259 // Keep track of results. We will exit with a non-zero exit code in the case of failures.
260 switch (result) {
262 // We don't increment numSuccessfulGMs just yet. We still need to successfully save
263 // its output bitmap to disk.
264 TestRunner::Log("\tFlushing surface...");
265 surfaceManager->flush();
266 break;
269 break;
272 break;
273 default:
275 }
276
277 // Report GM result and optional message.
278 TestRunner::Log("\tResult: %s", draw_result_to_string(result).c_str());
279 if (!msg.isEmpty()) {
280 TestRunner::Log("\tMessage: \"%s\"", msg.c_str());
281 }
282
283 // Save PNG and JSON file with MD5 hash to disk if the GM was successful.
285 std::string name = std::string(gm->getName().c_str());
286 SkString pngPath = SkOSPath::Join(outputDir.c_str(), (name + ".png").c_str());
287 SkString jsonPath = SkOSPath::Join(outputDir.c_str(), (name + ".json").c_str());
288
289 WritePNGAndJSONFilesResult pngAndJSONResult =
290 write_png_and_json_files(gm->getName().c_str(),
291 keyValuePairs,
292 gm->getGoldKeys(),
293 surfaceManager->getGoldKeyValuePairs(cpuName, gpuName),
294 bitmap,
295 pngPath.c_str(),
296 jsonPath.c_str(),
297 knownDigests);
298 if (pngAndJSONResult.status == WritePNGAndJSONFilesResult::kError) {
299 TestRunner::Log("\tERROR: %s", pngAndJSONResult.errorMsg.c_str());
301 } else if (pngAndJSONResult.status == WritePNGAndJSONFilesResult::kSkippedKnownDigest) {
302 TestRunner::Log("\tSkipping known digest: %s", pngAndJSONResult.skippedDigest.c_str());
303 } else {
305 TestRunner::Log("\tPNG file written to: %s", pngPath.c_str());
306 TestRunner::Log("\tJSON file written to: %s", jsonPath.c_str());
307 }
308 }
309}
310
311// Reads a plaintext file with "known digests" (i.e. digests that are known positives or negatives
312// in Gold) and returns the digests (MD5 hashes) as a set of strings.
313std::set<std::string> read_known_digests_file(std::string path) {
314 std::set<std::string> hashes;
315 std::regex md5HashRegex("^[a-fA-F0-9]{32}$");
316 std::ifstream f(path);
317 std::string line;
318 for (int lineNum = 1; std::getline(f, line); lineNum++) {
319 // Trim left and right (https://stackoverflow.com/a/217605).
320 auto isSpace = [](unsigned char c) { return !std::isspace(c); };
321 std::string md5 = line;
322 md5.erase(md5.begin(), std::find_if(md5.begin(), md5.end(), isSpace));
323 md5.erase(std::find_if(md5.rbegin(), md5.rend(), isSpace).base(), md5.end());
324
325 if (md5 == "") continue;
326
327 if (!std::regex_match(md5, md5HashRegex)) {
328 SK_ABORT(
329 "File '%s' passed via --knownDigestsFile contains an invalid entry on line "
330 "%d: '%s'",
331 path.c_str(),
332 lineNum,
333 line.c_str());
334 }
335 hashes.insert(md5);
336 }
337 return hashes;
338}
339
340int main(int argc, char** argv) {
342
343 // When running under Bazel (e.g. "bazel test //path/to:test"), we'll store output files in
344 // $TEST_UNDECLARED_OUTPUTS_DIR unless overridden via the --outputDir flag.
345 //
346 // See https://bazel.build/reference/test-encyclopedia#initial-conditions.
347 std::string testUndeclaredOutputsDir;
348 if (char* envVar = std::getenv("TEST_UNDECLARED_OUTPUTS_DIR")) {
349 testUndeclaredOutputsDir = envVar;
350 }
351 bool isBazelTest = !testUndeclaredOutputsDir.empty();
352
353 // Parse and validate flags.
355 if (!isBazelTest) {
356 TestRunner::FlagValidators::StringNonEmpty("--outputDir", FLAGS_outputDir);
357 }
358 TestRunner::FlagValidators::StringAtMostOne("--outputDir", FLAGS_outputDir);
359 TestRunner::FlagValidators::StringAtMostOne("--knownDigestsFile", FLAGS_knownDigestsFile);
360 TestRunner::FlagValidators::StringEven("--key", FLAGS_key);
361 TestRunner::FlagValidators::StringNonEmpty("--surfaceConfig", FLAGS_surfaceConfig);
362 TestRunner::FlagValidators::StringAtMostOne("--surfaceConfig", FLAGS_surfaceConfig);
363 TestRunner::FlagValidators::StringAtMostOne("--cpuName", FLAGS_cpuName);
364 TestRunner::FlagValidators::StringAtMostOne("--gpuName", FLAGS_gpuName);
366
367 std::string outputDir =
368 FLAGS_outputDir.isEmpty() ? testUndeclaredOutputsDir : FLAGS_outputDir[0];
369
370 auto knownDigests = std::set<std::string>();
371 if (!FLAGS_knownDigestsFile.isEmpty()) {
372 knownDigests = read_known_digests_file(FLAGS_knownDigestsFile[0]);
374 "Read %zu known digests from: %s", knownDigests.size(), FLAGS_knownDigestsFile[0]);
375 }
376
377 std::map<std::string, std::string> keyValuePairs;
378 for (int i = 1; i < FLAGS_key.size(); i += 2) {
379 keyValuePairs[FLAGS_key[i - 1]] = FLAGS_key[i];
380 }
381 std::string config = FLAGS_surfaceConfig[0];
382 std::string cpuName = FLAGS_cpuName.isEmpty() ? "" : FLAGS_cpuName[0];
383 std::string gpuName = FLAGS_gpuName.isEmpty() ? "" : FLAGS_gpuName[0];
384
385 // Execute all GM registerer functions, then run all registered GMs.
387 std::string errorMsg = f();
388 if (errorMsg != "") {
389 SK_ABORT("Error while gathering GMs: %s", errorMsg.c_str());
390 }
391 }
393 std::unique_ptr<skiagm::GM> gm = f();
394
395 if (!TestRunner::ShouldRunTestCase(gm->getName().c_str(), FLAGS_match, FLAGS_skip)) {
396 TestRunner::Log("Skipping %s", gm->getName().c_str());
398 continue;
399 }
400
401 run_gm(std::move(gm), config, keyValuePairs, cpuName, gpuName, outputDir, knownDigests);
402 }
403
404 // TODO(lovisolo): If running under Bazel, print command to display output files.
405
406 TestRunner::Log(gNumFailedGMs > 0 ? "FAIL" : "PASS");
408 "%d successful GMs (images written to %s).", gNumSuccessfulGMs, outputDir.c_str());
409 TestRunner::Log("%d failed GMs.", gNumFailedGMs);
410 TestRunner::Log("%d skipped GMs.", gNumSkippedGMs);
411 return gNumFailedGMs > 0 ? 1 : 0;
412}
void run_gm(std::unique_ptr< skiagm::GM > gm, std::string config, std::map< std::string, std::string > keyValuePairs, std::string cpuName, std::string gpuName, std::string outputDir, std::set< std::string > knownDigests)
static int gNumSkippedGMs
static int gNumSuccessfulGMs
std::set< std::string > read_known_digests_file(std::string path)
static bool unused
static bool gMissingCpuOrGpuWarningLogged
static WritePNGAndJSONFilesResult write_png_and_json_files(std::string name, std::map< std::string, std::string > commonKeys, std::map< std::string, std::string > gmGoldKeys, std::map< std::string, std::string > surfaceGoldKeys, const SkBitmap &bitmap, const char *pngPath, const char *jsonPath, std::set< std::string > knownDigests)
static std::string draw_result_to_string(skiagm::DrawResult result)
static int gNumFailedGMs
static SkMD5::Digest md5(const SkBitmap &bm)
Definition CodecTest.cpp:77
#define DEFINE_string(name, defaultValue, helpString)
std::map< std::string, std::string > GetCompilationModeGoldAndPerfKeyValuePairs()
static bool match(const char *needle, const char *haystack)
Definition DM.cpp:1132
#define SkUNREACHABLE
Definition SkAssert.h:135
#define SK_ABORT(message,...)
Definition SkAssert.h:70
static bool skip(SkStream *stream, size_t amount)
static uint32_t hash(const SkShaderBase::GradientInfo &v)
static void draw(SkCanvas *canvas, SkRect &target, int x, int y)
Definition aaclip.cpp:27
static void Parse(int argc, const char *const *argv)
bool encodePNG(SkWStream *, const char *md5, CommandLineFlags::StringArray key, CommandLineFlags::StringArray properties) const
void feedHash(SkWStream *) const
static bool CreateStringFlag(const char *name, const char *shortName, CommandLineFlags::StringArray *pStrings, const char *defaultValue, const char *helpString, const char *extendedHelpString)
void beginObject(const char *name=nullptr, bool multiline=true)
void appendString(const char *value, size_t size)
Definition SkMD5.h:19
static SkString Join(const char *rootPath, const char *relativePath)
Definition SkOSPath.cpp:14
bool isEmpty() const
Definition SkString.h:130
const char * c_str() const
Definition SkString.h:133
static std::unique_ptr< SurfaceManager > FromConfig(std::string config, SurfaceOptions surfaceOptions)
uint8_t value
GAsyncResult * result
const char * name
Definition fuchsia.cc:50
char ** argv
Definition library.h:9
void StringEven(std::string name, CommandLineFlags::StringArray flag)
void StringNonEmpty(std::string name, CommandLineFlags::StringArray flag)
void StringAtMostOne(std::string name, CommandLineFlags::StringArray flag)
bool ShouldRunTestCase(const char *name, CommandLineFlags::StringArray &matchFlag, CommandLineFlags::StringArray &skipFlag)
void Log(const char *format,...) SK_PRINTF_LIKE(1
void InitAndLogCmdlineArgs(int argc, char **argv)
Definition main.py:1
std::function< std::unique_ptr< skiagm::GM >()> GMFactory
Definition gm.h:239
std::function< std::string()> GMRegistererFn
Definition gm.h:254
DrawResult
Definition gm.h:104
int32_t width
SkString toLowercaseHexString() const
Definition SkMD5.cpp:116
enum WritePNGAndJSONFilesResult::@451 status