1/* 2 * Copyright 2011 Google Inc. 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#include "include/core/SkBitmap.h" 8#include "include/core/SkData.h" 9#include "include/core/SkImageEncoder.h" 10#include "include/core/SkPixelRef.h" 11#include "include/core/SkStream.h" 12#include "include/private/SkTDArray.h" 13#include "src/core/SkOSFile.h" 14#include "src/core/SkTSearch.h" 15#include "src/utils/SkOSPath.h" 16#include "tools/skdiff/skdiff.h" 17#include "tools/skdiff/skdiff_html.h" 18#include "tools/skdiff/skdiff_utils.h" 19 20#include <stdlib.h> 21 22/** 23 * skdiff 24 * 25 * Given three directory names, expects to find identically-named files in 26 * each of the first two; the first are treated as a set of baseline, 27 * the second a set of variant images, and a diff image is written into the 28 * third directory for each pair. 29 * Creates an index.html in the current third directory to compare each 30 * pair that does not match exactly. 31 * Recursively descends directories, unless run with --norecurse. 32 * 33 * Returns zero exit code if all images match across baseDir and comparisonDir. 34 */ 35 36typedef SkTArray<SkString> StringArray; 37typedef StringArray FileArray; 38 39static void add_unique_basename(StringArray* array, const SkString& filename) { 40 // trim off dirs 41 const char* src = filename.c_str(); 42 const char* trimmed = strrchr(src, SkOSPath::SEPARATOR); 43 if (trimmed) { 44 trimmed += 1; // skip the separator 45 } else { 46 trimmed = src; 47 } 48 const char* end = strrchr(trimmed, '.'); 49 if (!end) { 50 end = trimmed + strlen(trimmed); 51 } 52 SkString result(trimmed, end - trimmed); 53 54 // only add unique entries 55 for (int i = 0; i < array->count(); ++i) { 56 if (array->at(i) == result) { 57 return; 58 } 59 } 60 array->push_back(std::move(result)); 61} 62 63struct DiffSummary { 64 DiffSummary () 65 : fNumMatches(0) 66 , fNumMismatches(0) 67 , fMaxMismatchV(0) 68 , fMaxMismatchPercent(0) { } 69 70 uint32_t fNumMatches; 71 uint32_t fNumMismatches; 72 uint32_t fMaxMismatchV; 73 float fMaxMismatchPercent; 74 75 FileArray fResultsOfType[DiffRecord::kResultCount]; 76 FileArray fStatusOfType[DiffResource::kStatusCount][DiffResource::kStatusCount]; 77 78 StringArray fFailedBaseNames[DiffRecord::kResultCount]; 79 80 void printContents(const FileArray& fileArray, 81 const char* baseStatus, const char* comparisonStatus, 82 bool listFilenames) { 83 int n = fileArray.count(); 84 printf("%d file pairs %s in baseDir and %s in comparisonDir", 85 n, baseStatus, comparisonStatus); 86 if (listFilenames) { 87 printf(": "); 88 for (int i = 0; i < n; ++i) { 89 printf("%s ", fileArray[i].c_str()); 90 } 91 } 92 printf("\n"); 93 } 94 95 void printStatus(bool listFilenames, 96 bool failOnStatusType[DiffResource::kStatusCount] 97 [DiffResource::kStatusCount]) { 98 typedef DiffResource::Status Status; 99 100 for (int base = 0; base < DiffResource::kStatusCount; ++base) { 101 Status baseStatus = static_cast<Status>(base); 102 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { 103 Status comparisonStatus = static_cast<Status>(comparison); 104 const FileArray& fileArray = fStatusOfType[base][comparison]; 105 if (fileArray.count() > 0) { 106 if (failOnStatusType[base][comparison]) { 107 printf(" [*] "); 108 } else { 109 printf(" [_] "); 110 } 111 printContents(fileArray, 112 DiffResource::getStatusDescription(baseStatus), 113 DiffResource::getStatusDescription(comparisonStatus), 114 listFilenames); 115 } 116 } 117 } 118 } 119 120 // Print a line about the contents of this FileArray to stdout. 121 void printContents(const FileArray& fileArray, const char* headerText, bool listFilenames) { 122 int n = fileArray.count(); 123 printf("%d file pairs %s", n, headerText); 124 if (listFilenames) { 125 printf(": "); 126 for (int i = 0; i < n; ++i) { 127 printf("%s ", fileArray[i].c_str()); 128 } 129 } 130 printf("\n"); 131 } 132 133 void print(bool listFilenames, bool failOnResultType[DiffRecord::kResultCount], 134 bool failOnStatusType[DiffResource::kStatusCount] 135 [DiffResource::kStatusCount]) { 136 printf("\ncompared %d file pairs:\n", fNumMatches + fNumMismatches); 137 for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) { 138 DiffRecord::Result result = static_cast<DiffRecord::Result>(resultInt); 139 if (failOnResultType[result]) { 140 printf("[*] "); 141 } else { 142 printf("[_] "); 143 } 144 printContents(fResultsOfType[result], DiffRecord::getResultDescription(result), 145 listFilenames); 146 if (DiffRecord::kCouldNotCompare_Result == result) { 147 printStatus(listFilenames, failOnStatusType); 148 } 149 } 150 printf("(results marked with [*] will cause nonzero return value)\n"); 151 printf("\nnumber of mismatching file pairs: %d\n", fNumMismatches); 152 if (fNumMismatches > 0) { 153 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV); 154 printf("Largest area mismatch was %.2f%% of pixels\n",fMaxMismatchPercent); 155 } 156 } 157 158 void printfFailingBaseNames(const char separator[]) { 159 for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) { 160 const StringArray& array = fFailedBaseNames[resultInt]; 161 if (array.count()) { 162 printf("%s [%d]%s", DiffRecord::ResultNames[resultInt], array.count(), separator); 163 for (int j = 0; j < array.count(); ++j) { 164 printf("%s%s", array[j].c_str(), separator); 165 } 166 printf("\n"); 167 } 168 } 169 } 170 171 void add (const DiffRecord& drp) { 172 uint32_t mismatchValue; 173 174 if (drp.fBase.fFilename.equals(drp.fComparison.fFilename)) { 175 fResultsOfType[drp.fResult].push_back(drp.fBase.fFilename); 176 } else { 177 SkString blame("("); 178 blame.append(drp.fBase.fFilename); 179 blame.append(", "); 180 blame.append(drp.fComparison.fFilename); 181 blame.append(")"); 182 fResultsOfType[drp.fResult].push_back(std::move(blame)); 183 } 184 switch (drp.fResult) { 185 case DiffRecord::kEqualBits_Result: 186 fNumMatches++; 187 break; 188 case DiffRecord::kEqualPixels_Result: 189 fNumMatches++; 190 break; 191 case DiffRecord::kDifferentSizes_Result: 192 fNumMismatches++; 193 break; 194 case DiffRecord::kDifferentPixels_Result: 195 fNumMismatches++; 196 if (drp.fFractionDifference * 100 > fMaxMismatchPercent) { 197 fMaxMismatchPercent = drp.fFractionDifference * 100; 198 } 199 mismatchValue = MAX3(drp.fMaxMismatchR, drp.fMaxMismatchG, 200 drp.fMaxMismatchB); 201 if (mismatchValue > fMaxMismatchV) { 202 fMaxMismatchV = mismatchValue; 203 } 204 break; 205 case DiffRecord::kCouldNotCompare_Result: 206 fNumMismatches++; 207 fStatusOfType[drp.fBase.fStatus][drp.fComparison.fStatus].push_back( 208 drp.fBase.fFilename); 209 break; 210 case DiffRecord::kUnknown_Result: 211 SkDEBUGFAIL("adding uncategorized DiffRecord"); 212 break; 213 default: 214 SkDEBUGFAIL("adding DiffRecord with unhandled fResult value"); 215 break; 216 } 217 218 switch (drp.fResult) { 219 case DiffRecord::kEqualBits_Result: 220 case DiffRecord::kEqualPixels_Result: 221 break; 222 default: 223 add_unique_basename(&fFailedBaseNames[drp.fResult], drp.fBase.fFilename); 224 break; 225 } 226 } 227}; 228 229/// Returns true if string contains any of these substrings. 230static bool string_contains_any_of(const SkString& string, 231 const StringArray& substrings) { 232 for (int i = 0; i < substrings.count(); i++) { 233 if (string.contains(substrings[i].c_str())) { 234 return true; 235 } 236 } 237 return false; 238} 239 240/// Internal (potentially recursive) implementation of get_file_list. 241static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir, 242 const StringArray& matchSubstrings, 243 const StringArray& nomatchSubstrings, 244 bool recurseIntoSubdirs, FileArray *files) { 245 bool isSubDirEmpty = subDir.isEmpty(); 246 SkString dir(rootDir); 247 if (!isSubDirEmpty) { 248 dir.append(PATH_DIV_STR); 249 dir.append(subDir); 250 } 251 252 // Iterate over files (not directories) within dir. 253 SkOSFile::Iter fileIterator(dir.c_str()); 254 SkString fileName; 255 while (fileIterator.next(&fileName, false)) { 256 if (fileName.startsWith(".")) { 257 continue; 258 } 259 SkString pathRelativeToRootDir(subDir); 260 if (!isSubDirEmpty) { 261 pathRelativeToRootDir.append(PATH_DIV_STR); 262 } 263 pathRelativeToRootDir.append(fileName); 264 if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) && 265 !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) { 266 files->push_back(std::move(pathRelativeToRootDir)); 267 } 268 } 269 270 // Recurse into any non-ignored subdirectories. 271 if (recurseIntoSubdirs) { 272 SkOSFile::Iter dirIterator(dir.c_str()); 273 SkString dirName; 274 while (dirIterator.next(&dirName, true)) { 275 if (dirName.startsWith(".")) { 276 continue; 277 } 278 SkString pathRelativeToRootDir(subDir); 279 if (!isSubDirEmpty) { 280 pathRelativeToRootDir.append(PATH_DIV_STR); 281 } 282 pathRelativeToRootDir.append(dirName); 283 if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) { 284 get_file_list_subdir(rootDir, pathRelativeToRootDir, 285 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, 286 files); 287 } 288 } 289 } 290} 291 292/// Iterate over dir and get all files whose filename: 293/// - matches any of the substrings in matchSubstrings, but... 294/// - DOES NOT match any of the substrings in nomatchSubstrings 295/// - DOES NOT start with a dot (.) 296/// Adds the matching files to the list in *files. 297static void get_file_list(const SkString& dir, 298 const StringArray& matchSubstrings, 299 const StringArray& nomatchSubstrings, 300 bool recurseIntoSubdirs, FileArray *files) { 301 get_file_list_subdir(dir, SkString(""), 302 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, 303 files); 304} 305 306/// Comparison routines for qsort, sort by file names. 307static int compare_file_name_metrics(SkString *lhs, SkString *rhs) { 308 return strcmp(lhs->c_str(), rhs->c_str()); 309} 310 311class AutoReleasePixels { 312public: 313 AutoReleasePixels(DiffRecord* drp) 314 : fDrp(drp) { 315 SkASSERT(drp != nullptr); 316 } 317 ~AutoReleasePixels() { 318 fDrp->fBase.fBitmap.setPixelRef(nullptr, 0, 0); 319 fDrp->fComparison.fBitmap.setPixelRef(nullptr, 0, 0); 320 fDrp->fDifference.fBitmap.setPixelRef(nullptr, 0, 0); 321 fDrp->fWhite.fBitmap.setPixelRef(nullptr, 0, 0); 322 } 323 324private: 325 DiffRecord* fDrp; 326}; 327 328static void get_bounds(DiffResource& resource, const char* name) { 329 if (resource.fBitmap.empty() && !DiffResource::isStatusFailed(resource.fStatus)) { 330 sk_sp<SkData> fileBits(read_file(resource.fFullPath.c_str())); 331 if (fileBits) { 332 get_bitmap(fileBits, resource, true, true); 333 } else { 334 SkDebugf("WARNING: couldn't read %s file <%s>\n", name, resource.fFullPath.c_str()); 335 resource.fStatus = DiffResource::kCouldNotRead_Status; 336 } 337 } 338} 339 340static void get_bounds(DiffRecord& drp) { 341 get_bounds(drp.fBase, "base"); 342 get_bounds(drp.fComparison, "comparison"); 343} 344 345#ifdef SK_OS_WIN 346#define ANSI_COLOR_RED "" 347#define ANSI_COLOR_GREEN "" 348#define ANSI_COLOR_YELLOW "" 349#define ANSI_COLOR_RESET "" 350#else 351#define ANSI_COLOR_RED "\x1b[31m" 352#define ANSI_COLOR_GREEN "\x1b[32m" 353#define ANSI_COLOR_YELLOW "\x1b[33m" 354#define ANSI_COLOR_RESET "\x1b[0m" 355#endif 356 357#define VERBOSE_STATUS(status,color,filename) if (verbose) printf( "[ " color " %10s " ANSI_COLOR_RESET " ] %s\n", status, filename.c_str()) 358 359/// Creates difference images, returns the number that have a 0 metric. 360/// If outputDir.isEmpty(), don't write out diff files. 361static void create_diff_images (DiffMetricProc dmp, 362 const int colorThreshold, 363 bool ignoreColorSpace, 364 RecordArray* differences, 365 const SkString& baseDir, 366 const SkString& comparisonDir, 367 const SkString& outputDir, 368 const StringArray& matchSubstrings, 369 const StringArray& nomatchSubstrings, 370 bool recurseIntoSubdirs, 371 bool getBounds, 372 bool verbose, 373 DiffSummary* summary) { 374 SkASSERT(!baseDir.isEmpty()); 375 SkASSERT(!comparisonDir.isEmpty()); 376 377 FileArray baseFiles; 378 FileArray comparisonFiles; 379 380 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles); 381 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, 382 &comparisonFiles); 383 384 if (!baseFiles.empty()) { 385 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString), 386 SkCastForQSort(compare_file_name_metrics)); 387 } 388 if (!comparisonFiles.empty()) { 389 qsort(comparisonFiles.begin(), comparisonFiles.count(), sizeof(SkString), 390 SkCastForQSort(compare_file_name_metrics)); 391 } 392 393 if (!outputDir.isEmpty()) { 394 sk_mkdir(outputDir.c_str()); 395 } 396 397 int i = 0; 398 int j = 0; 399 400 while (i < baseFiles.count() && 401 j < comparisonFiles.count()) { 402 403 SkString basePath(baseDir); 404 SkString comparisonPath(comparisonDir); 405 406 DiffRecord drp; 407 int v = strcmp(baseFiles[i].c_str(), comparisonFiles[j].c_str()); 408 409 if (v < 0) { 410 // in baseDir, but not in comparisonDir 411 drp.fResult = DiffRecord::kCouldNotCompare_Result; 412 413 basePath.append(baseFiles[i]); 414 comparisonPath.append(baseFiles[i]); 415 416 drp.fBase.fFilename = baseFiles[i]; 417 drp.fBase.fFullPath = basePath; 418 drp.fBase.fStatus = DiffResource::kExists_Status; 419 420 drp.fComparison.fFilename = baseFiles[i]; 421 drp.fComparison.fFullPath = comparisonPath; 422 drp.fComparison.fStatus = DiffResource::kDoesNotExist_Status; 423 424 VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]); 425 426 ++i; 427 } else if (v > 0) { 428 // in comparisonDir, but not in baseDir 429 drp.fResult = DiffRecord::kCouldNotCompare_Result; 430 431 basePath.append(comparisonFiles[j]); 432 comparisonPath.append(comparisonFiles[j]); 433 434 drp.fBase.fFilename = comparisonFiles[j]; 435 drp.fBase.fFullPath = basePath; 436 drp.fBase.fStatus = DiffResource::kDoesNotExist_Status; 437 438 drp.fComparison.fFilename = comparisonFiles[j]; 439 drp.fComparison.fFullPath = comparisonPath; 440 drp.fComparison.fStatus = DiffResource::kExists_Status; 441 442 VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]); 443 444 ++j; 445 } else { 446 // Found the same filename in both baseDir and comparisonDir. 447 SkASSERT(DiffRecord::kUnknown_Result == drp.fResult); 448 449 basePath.append(baseFiles[i]); 450 comparisonPath.append(comparisonFiles[j]); 451 452 drp.fBase.fFilename = baseFiles[i]; 453 drp.fBase.fFullPath = basePath; 454 drp.fBase.fStatus = DiffResource::kExists_Status; 455 456 drp.fComparison.fFilename = comparisonFiles[j]; 457 drp.fComparison.fFullPath = comparisonPath; 458 drp.fComparison.fStatus = DiffResource::kExists_Status; 459 460 sk_sp<SkData> baseFileBits(read_file(drp.fBase.fFullPath.c_str())); 461 if (baseFileBits) { 462 drp.fBase.fStatus = DiffResource::kRead_Status; 463 } 464 sk_sp<SkData> comparisonFileBits(read_file(drp.fComparison.fFullPath.c_str())); 465 if (comparisonFileBits) { 466 drp.fComparison.fStatus = DiffResource::kRead_Status; 467 } 468 if (nullptr == baseFileBits || nullptr == comparisonFileBits) { 469 if (nullptr == baseFileBits) { 470 drp.fBase.fStatus = DiffResource::kCouldNotRead_Status; 471 VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]); 472 } 473 if (nullptr == comparisonFileBits) { 474 drp.fComparison.fStatus = DiffResource::kCouldNotRead_Status; 475 VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]); 476 } 477 drp.fResult = DiffRecord::kCouldNotCompare_Result; 478 479 } else if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) { 480 drp.fResult = DiffRecord::kEqualBits_Result; 481 VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]); 482 } else { 483 AutoReleasePixels arp(&drp); 484 get_bitmap(baseFileBits, drp.fBase, false, ignoreColorSpace); 485 get_bitmap(comparisonFileBits, drp.fComparison, false, ignoreColorSpace); 486 VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]); 487 if (DiffResource::kDecoded_Status == drp.fBase.fStatus && 488 DiffResource::kDecoded_Status == drp.fComparison.fStatus) { 489 create_and_write_diff_image(&drp, dmp, colorThreshold, 490 outputDir, drp.fBase.fFilename); 491 } else { 492 drp.fResult = DiffRecord::kCouldNotCompare_Result; 493 } 494 } 495 496 ++i; 497 ++j; 498 } 499 500 if (getBounds) { 501 get_bounds(drp); 502 } 503 SkASSERT(DiffRecord::kUnknown_Result != drp.fResult); 504 summary->add(drp); 505 differences->push_back(std::move(drp)); 506 } 507 508 for (; i < baseFiles.count(); ++i) { 509 // files only in baseDir 510 DiffRecord drp; 511 drp.fBase.fFilename = baseFiles[i]; 512 drp.fBase.fFullPath = baseDir; 513 drp.fBase.fFullPath.append(drp.fBase.fFilename); 514 drp.fBase.fStatus = DiffResource::kExists_Status; 515 516 drp.fComparison.fFilename = baseFiles[i]; 517 drp.fComparison.fFullPath = comparisonDir; 518 drp.fComparison.fFullPath.append(drp.fComparison.fFilename); 519 drp.fComparison.fStatus = DiffResource::kDoesNotExist_Status; 520 521 drp.fResult = DiffRecord::kCouldNotCompare_Result; 522 if (getBounds) { 523 get_bounds(drp); 524 } 525 summary->add(drp); 526 differences->push_back(std::move(drp)); 527 } 528 529 for (; j < comparisonFiles.count(); ++j) { 530 // files only in comparisonDir 531 DiffRecord drp; 532 drp.fBase.fFilename = comparisonFiles[j]; 533 drp.fBase.fFullPath = baseDir; 534 drp.fBase.fFullPath.append(drp.fBase.fFilename); 535 drp.fBase.fStatus = DiffResource::kDoesNotExist_Status; 536 537 drp.fComparison.fFilename = comparisonFiles[j]; 538 drp.fComparison.fFullPath = comparisonDir; 539 drp.fComparison.fFullPath.append(drp.fComparison.fFilename); 540 drp.fComparison.fStatus = DiffResource::kExists_Status; 541 542 drp.fResult = DiffRecord::kCouldNotCompare_Result; 543 if (getBounds) { 544 get_bounds(drp); 545 } 546 summary->add(drp); 547 differences->push_back(std::move(drp)); 548 } 549} 550 551static void usage (char * argv0) { 552 SkDebugf("Skia baseline image diff tool\n"); 553 SkDebugf("\n" 554"Usage: \n" 555" %s <baseDir> <comparisonDir> [outputDir] \n", argv0); 556 SkDebugf( 557"\nArguments:" 558"\n --failonresult <result>: After comparing all file pairs, exit with nonzero" 559"\n return code (number of file pairs yielding this" 560"\n result) if any file pairs yielded this result." 561"\n This flag may be repeated, in which case the" 562"\n return code will be the number of fail pairs" 563"\n yielding ANY of these results." 564"\n --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return" 565"\n code if any file pairs yielded this status." 566"\n --help: display this info" 567"\n --listfilenames: list all filenames for each result type in stdout" 568"\n --match <substring>: compare files whose filenames contain this substring;" 569"\n if unspecified, compare ALL files." 570"\n this flag may be repeated." 571"\n --nocolorspace: Ignore color space of images." 572"\n --nodiffs: don't write out image diffs or index.html, just generate" 573"\n report on stdout" 574"\n --nomatch <substring>: regardless of --match, DO NOT compare files whose" 575"\n filenames contain this substring." 576"\n this flag may be repeated." 577"\n --noprintdirs: do not print the directories used." 578"\n --norecurse: do not recurse into subdirectories." 579"\n --sortbymaxmismatch: sort by worst color channel mismatch;" 580"\n break ties with -sortbymismatch" 581"\n --sortbymismatch: sort by average color channel mismatch" 582"\n --threshold <n>: only report differences > n (per color channel) [default 0]" 583"\n --weighted: sort by # pixels different weighted by color difference" 584"\n" 585"\n baseDir: directory to read baseline images from." 586"\n comparisonDir: directory to read comparison images from" 587"\n outputDir: directory to write difference images and index.html to;" 588"\n defaults to comparisonDir" 589"\n" 590"\nIf no sort is specified, it will sort by fraction of pixels mismatching." 591"\n"); 592} 593 594const int kNoError = 0; 595const int kGenericError = -1; 596 597int main(int argc, char** argv) { 598 DiffMetricProc diffProc = compute_diff_pmcolor; 599 int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>; 600 601 // Maximum error tolerated in any one color channel in any one pixel before 602 // a difference is reported. 603 int colorThreshold = 0; 604 SkString baseDir; 605 SkString comparisonDir; 606 SkString outputDir; 607 608 StringArray matchSubstrings; 609 StringArray nomatchSubstrings; 610 611 bool generateDiffs = true; 612 bool listFilenames = false; 613 bool printDirNames = true; 614 bool recurseIntoSubdirs = true; 615 bool verbose = false; 616 bool listFailingBase = false; 617 bool ignoreColorSpace = false; 618 619 RecordArray differences; 620 DiffSummary summary; 621 622 bool failOnResultType[DiffRecord::kResultCount]; 623 for (int i = 0; i < DiffRecord::kResultCount; i++) { 624 failOnResultType[i] = false; 625 } 626 627 bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount]; 628 for (int base = 0; base < DiffResource::kStatusCount; ++base) { 629 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { 630 failOnStatusType[base][comparison] = false; 631 } 632 } 633 634 int numUnflaggedArguments = 0; 635 for (int i = 1; i < argc; i++) { 636 if (!strcmp(argv[i], "--failonresult")) { 637 if (argc == ++i) { 638 SkDebugf("failonresult expects one argument.\n"); 639 continue; 640 } 641 DiffRecord::Result type = DiffRecord::getResultByName(argv[i]); 642 if (type != DiffRecord::kResultCount) { 643 failOnResultType[type] = true; 644 } else { 645 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]); 646 } 647 continue; 648 } 649 if (!strcmp(argv[i], "--failonstatus")) { 650 if (argc == ++i) { 651 SkDebugf("failonstatus missing base status.\n"); 652 continue; 653 } 654 bool baseStatuses[DiffResource::kStatusCount]; 655 if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) { 656 SkDebugf("unrecognized base status <%s>\n", argv[i]); 657 } 658 659 if (argc == ++i) { 660 SkDebugf("failonstatus missing comparison status.\n"); 661 continue; 662 } 663 bool comparisonStatuses[DiffResource::kStatusCount]; 664 if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) { 665 SkDebugf("unrecognized comarison status <%s>\n", argv[i]); 666 } 667 668 for (int base = 0; base < DiffResource::kStatusCount; ++base) { 669 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { 670 failOnStatusType[base][comparison] |= 671 baseStatuses[base] && comparisonStatuses[comparison]; 672 } 673 } 674 continue; 675 } 676 if (!strcmp(argv[i], "--help")) { 677 usage(argv[0]); 678 return kNoError; 679 } 680 if (!strcmp(argv[i], "--listfilenames")) { 681 listFilenames = true; 682 continue; 683 } 684 if (!strcmp(argv[i], "--verbose")) { 685 verbose = true; 686 continue; 687 } 688 if (!strcmp(argv[i], "--match")) { 689 matchSubstrings.emplace_back(argv[++i]); 690 continue; 691 } 692 if (!strcmp(argv[i], "--nocolorspace")) { 693 ignoreColorSpace = true; 694 continue; 695 } 696 if (!strcmp(argv[i], "--nodiffs")) { 697 generateDiffs = false; 698 continue; 699 } 700 if (!strcmp(argv[i], "--nomatch")) { 701 nomatchSubstrings.emplace_back(argv[++i]); 702 continue; 703 } 704 if (!strcmp(argv[i], "--noprintdirs")) { 705 printDirNames = false; 706 continue; 707 } 708 if (!strcmp(argv[i], "--norecurse")) { 709 recurseIntoSubdirs = false; 710 continue; 711 } 712 if (!strcmp(argv[i], "--sortbymaxmismatch")) { 713 sortProc = compare<CompareDiffMaxMismatches>; 714 continue; 715 } 716 if (!strcmp(argv[i], "--sortbymismatch")) { 717 sortProc = compare<CompareDiffMeanMismatches>; 718 continue; 719 } 720 if (!strcmp(argv[i], "--threshold")) { 721 colorThreshold = atoi(argv[++i]); 722 continue; 723 } 724 if (!strcmp(argv[i], "--weighted")) { 725 sortProc = compare<CompareDiffWeighted>; 726 continue; 727 } 728 if (argv[i][0] != '-') { 729 switch (numUnflaggedArguments++) { 730 case 0: 731 baseDir.set(argv[i]); 732 continue; 733 case 1: 734 comparisonDir.set(argv[i]); 735 continue; 736 case 2: 737 outputDir.set(argv[i]); 738 continue; 739 default: 740 SkDebugf("extra unflagged argument <%s>\n", argv[i]); 741 usage(argv[0]); 742 return kGenericError; 743 } 744 } 745 if (!strcmp(argv[i], "--listFailingBase")) { 746 listFailingBase = true; 747 continue; 748 } 749 750 SkDebugf("Unrecognized argument <%s>\n", argv[i]); 751 usage(argv[0]); 752 return kGenericError; 753 } 754 755 if (numUnflaggedArguments == 2) { 756 outputDir = comparisonDir; 757 } else if (numUnflaggedArguments != 3) { 758 usage(argv[0]); 759 return kGenericError; 760 } 761 762 if (!baseDir.endsWith(PATH_DIV_STR)) { 763 baseDir.append(PATH_DIV_STR); 764 } 765 if (printDirNames) { 766 printf("baseDir is [%s]\n", baseDir.c_str()); 767 } 768 769 if (!comparisonDir.endsWith(PATH_DIV_STR)) { 770 comparisonDir.append(PATH_DIV_STR); 771 } 772 if (printDirNames) { 773 printf("comparisonDir is [%s]\n", comparisonDir.c_str()); 774 } 775 776 if (!outputDir.endsWith(PATH_DIV_STR)) { 777 outputDir.append(PATH_DIV_STR); 778 } 779 if (generateDiffs) { 780 if (printDirNames) { 781 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str()); 782 } 783 } else { 784 if (printDirNames) { 785 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str()); 786 } 787 outputDir.set(""); 788 } 789 790 // If no matchSubstrings were specified, match ALL strings 791 // (except for whatever nomatchSubstrings were specified, if any). 792 if (matchSubstrings.empty()) { 793 matchSubstrings.emplace_back(""); 794 } 795 796 create_diff_images(diffProc, colorThreshold, ignoreColorSpace, &differences, 797 baseDir, comparisonDir, outputDir, 798 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs, 799 verbose, &summary); 800 summary.print(listFilenames, failOnResultType, failOnStatusType); 801 802 if (listFailingBase) { 803 summary.printfFailingBaseNames("\n"); 804 } 805 806 if (differences.count()) { 807 qsort(differences.begin(), differences.count(), sizeof(DiffRecord), sortProc); 808 } 809 810 if (generateDiffs) { 811 print_diff_page(summary.fNumMatches, colorThreshold, differences, 812 baseDir, comparisonDir, outputDir); 813 } 814 815 int num_failing_results = 0; 816 for (int i = 0; i < DiffRecord::kResultCount; i++) { 817 if (failOnResultType[i]) { 818 num_failing_results += summary.fResultsOfType[i].count(); 819 } 820 } 821 if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) { 822 for (int base = 0; base < DiffResource::kStatusCount; ++base) { 823 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { 824 if (failOnStatusType[base][comparison]) { 825 num_failing_results += summary.fStatusOfType[base][comparison].count(); 826 } 827 } 828 } 829 } 830 831 // On Linux (and maybe other platforms too), any results outside of the 832 // range [0...255] are wrapped (mod 256). Do the conversion ourselves, to 833 // make sure that we only return 0 when there were no failures. 834 return (num_failing_results > 255) ? 255 : num_failing_results; 835} 836