Flutter Engine
The Flutter Engine
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.
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)
int main(int argc, char **argv)
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 DEFINE_string(skip, "", "Space-separated list of test cases (regexps) to skip.")
static SkMD5::Digest md5(const SkBitmap &bm)
Definition: CodecTest.cpp:77
std::map< std::string, std::string > GetCompilationModeGoldAndPerfKeyValuePairs()
#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)
Definition: SkJSONWriter.h:114
void endObject()
Definition: SkJSONWriter.h:126
void appendString(const char *value, size_t size)
Definition: SkJSONWriter.h:176
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
char ** argv
Definition: library.h:9
void StringEven(std::string name, CommandLineFlags::StringArray flag)
Definition: TestRunner.cpp:31
void StringNonEmpty(std::string name, CommandLineFlags::StringArray flag)
Definition: TestRunner.cpp:17
void StringAtMostOne(std::string name, CommandLineFlags::StringArray flag)
Definition: TestRunner.cpp:24
bool ShouldRunTestCase(const char *name, CommandLineFlags::StringArray &matchFlag, CommandLineFlags::StringArray &skipFlag)
Definition: TestRunner.cpp:113
void Log(const char *format,...) SK_PRINTF_LIKE(1
Definition: TestRunner.cpp:137
void InitAndLogCmdlineArgs(int argc, char **argv)
Definition: TestRunner.cpp:88
def match(bench, filt)
Definition: benchmark.py:23
Definition: bitmap.py:1
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir path
Definition: switches.h:57
DEF_SWITCHES_START aot vmservice shared library name
Definition: switches.h:32
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
Definition: Draw.h:18
SkString toLowercaseHexString() const
Definition: SkMD5.cpp:116
enum WritePNGAndJSONFilesResult::@445 status