/* * Copyright 2016 Nu-book Inc. * Copyright 2019 Axel Waggershauser */ // SPDX-License-Identifier: Apache-2.0 #include "BlackboxTestRunner.h" #include "ByteArray.h" #include "ImageLoader.h" #include "ReadBarcode.h" #include "Utf.h" #include "ZXAlgorithms.h" #include "StdPrint.h" #include "Version.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef __GNUC__ // turns out that libstdc++ is depending on TBB without even linking against it, so this fails to link :-/ #undef __cpp_lib_execution #endif #ifdef __cpp_lib_execution #include #else #include #endif namespace ZXing::Test { namespace { struct PureTag {} pure; struct TestCase { struct TC { std::string name = {}; int minPassCount = 0; // The number of images which must decode for the test to pass. int maxMisreads = 0; // Maximum number of successfully read images with the wrong contents. std::set notDetectedFiles = {}; std::map misReadFiles = {}; }; TC tc[2] = {}; int rotation = 0; // The rotation in degrees clockwise to use for this test. TestCase(int mntf, int mnts, int mmf, int mms, int r) : tc{{"fast", mntf, mmf}, {"slow", mnts, mms}}, rotation(r) {} TestCase(int mntf, int mnts, int r) : TestCase(mntf, mnts, 0, 0, r) {} TestCase(int mntp, int mmp, PureTag) : tc{{"pure", mntp, mmp}} {} }; struct FalsePositiveTestCase { int maxAllowed; // Maximum number of images which can fail due to successfully reading the wrong contents int rotation; // The rotation in degrees clockwise to use for this test. }; } // Helper for `compareResult()` - map `key` to Barcode property, converting value to std::string static std::string getBarcodeValue(const Barcode& barcode, const std::string& key) { if (key == "format") return ToString(barcode.format()); if (key == "contentType") return ToString(barcode.contentType()); if (key == "ecLevel") return barcode.ecLevel(); if (key == "orientation") return std::to_string(barcode.orientation()); if (key == "symbologyIdentifier") return barcode.symbologyIdentifier(); if (key == "sequenceSize") return std::to_string(barcode.sequenceSize()); if (key == "sequenceIndex") return std::to_string(barcode.sequenceIndex()); if (key == "sequenceId") return barcode.sequenceId(); if (key == "isLastInSequence") return barcode.isLastInSequence() ? "true" : "false"; if (key == "isPartOfSequence") return barcode.isPartOfSequence() ? "true" : "false"; if (key == "isMirrored") return barcode.isMirrored() ? "true" : "false"; if (key == "isInverted") return barcode.isInverted() ? "true" : "false"; if (key == "readerInit") return barcode.readerInit() ? "true" : "false"; return std::format("***Unknown key '{}'***", key); } // Read ".result.txt" file contents `expected` with lines "key=value" and compare to `actual` static bool compareResult(const Barcode& barcode, const std::string& expected, std::string& actual) { bool ret = true; actual.clear(); actual.reserve(expected.size()); std::stringstream expectedLines(expected); std::string expectedLine; while (std::getline(expectedLines, expectedLine)) { if (expectedLine.empty() || expectedLine[0] == '#') continue; auto equals = expectedLine.find('='); if (equals == std::string::npos) { actual += "***Bad format, missing equals***\n"; return false; } std::string key = expectedLine.substr(0, equals); std::string expectedValue = expectedLine.substr(equals + 1); std::string actualValue = getBarcodeValue(barcode, key); if (actualValue != expectedValue) { ret = false; actualValue += " ***Mismatch***"; } actual += StrCat(key, '=', actualValue, '\n'); } return ret; } static std::string checkResult(const fs::path& imgPath, BarcodeFormat expectedFormat, const Barcode& barcode) { if (expectedFormat != barcode.format() && expectedFormat != barcode.symbology()) // allow matching either format or symbology return std::format("Format mismatch: expected '{}' but got '{}'", ToString(expectedFormat), ToString(barcode.format())); auto readFile = [imgPath](const char* ending) { std::ifstream ifs(fs::path(imgPath).replace_extension(ending), std::ios::binary); return ifs ? std::optional(std::string(std::istreambuf_iterator(ifs), std::istreambuf_iterator())) : std::nullopt; }; if (auto expected = readFile(".result.txt")) { std::string actual; if (!compareResult(barcode, *expected, actual)) return std::format("Result mismatch: expected\n{} but got\n{}", *expected, actual); } if (auto expected = readFile(".txt")) { expected = EscapeNonGraphical(*expected); auto utf8Result = barcode.text(TextMode::Escaped); return utf8Result != *expected ? std::format("Content mismatch: expected '{}' but got '{}'", *expected, utf8Result) : ""; } if (auto expected = readFile(".bin")) { ByteArray binaryExpected(*expected); return barcode.bytes() != binaryExpected ? std::format("Content mismatch: expected '{}' but got '{}'", ToHex(binaryExpected), ToHex(barcode.bytes())) : ""; } return "Error reading file"; } static int failed = 0; static int extra = 0; static int totalImageLoadTime = 0; int timeSince(std::chrono::steady_clock::time_point startTime) { auto duration = std::chrono::steady_clock::now() - startTime; return narrow_cast(std::chrono::duration_cast(duration).count()); } // pre-load images into cache, so the disc io time does not end up in the timing measurement void preloadImageCache(const std::vector& imgPaths) { auto startTime = std::chrono::steady_clock::now(); ImageLoader::clearCache(); for (const auto& imgPath : imgPaths) ImageLoader::load(imgPath); totalImageLoadTime += timeSince(startTime); } static std::string printPositiveTestStats(int imageCount, const TestCase::TC& tc) { int passCount = imageCount - Size(tc.misReadFiles) - Size(tc.notDetectedFiles); std::print(" | {}: {:3} of {:3}, misread {} of {}", tc.name, passCount, tc.minPassCount, Size(tc.misReadFiles), tc.maxMisreads); std::string failures; if (passCount < tc.minPassCount && !tc.notDetectedFiles.empty()) { failures += std::format(" Not detected ({}):", tc.name); for (const auto& f : tc.notDetectedFiles) failures += std::format(" {}", f.filename().string()); failures += "\n"; failed += tc.minPassCount - passCount; } extra += std::max(0, passCount - tc.minPassCount); if (passCount > tc.minPassCount) failures += std::format(" Unexpected detections ({}): {}\n", tc.name, passCount - tc.minPassCount); if (Size(tc.misReadFiles) > tc.maxMisreads) { failures += std::format(" Read error ({}):", tc.name); for (const auto& [path, error] : tc.misReadFiles) failures += std::format(" {}: {}\n", path.filename().string(), error); failed += Size(tc.misReadFiles) - tc.maxMisreads; } return failures; } static std::vector getImagesInDirectory(const fs::path& directory) { std::vector result; for (const auto& entry : fs::directory_iterator(directory)) if (fs::is_regular_file(entry.status()) && Contains({".webp", ".png", ".jpg", ".pgm", ".gif"}, entry.path().extension())) result.push_back(entry.path()); preloadImageCache(result); return result; } static void doRunTests(const fs::path& directory, BarcodeFormat format, int totalTests, const std::vector& tests, ReaderOptions opts) { auto imgPaths = getImagesInDirectory(directory); auto folderName = directory.stem(); if (Size(imgPaths) != totalTests) std::println("TEST {} => Expected number of tests: {}, got: {} => FAILED", folderName.string(), totalTests, imgPaths.size()); for (auto& test : tests) { std::print("{:20} @ {:3}, {:3}", folderName.string(), test.rotation, Size(imgPaths)); std::vector times; std::string failures; for (auto tc : test.tc) { if (tc.name.empty()) break; auto startTime = std::chrono::steady_clock::now(); opts.tryDownscale(tc.name == "slow_"); opts.downscaleFactor(2); opts.downscaleThreshold(180); opts.tryHarder(tc.name == "slow"); opts.tryRotate(tc.name == "slow"); opts.tryInvert(tc.name == "slow"); opts.isPure(tc.name == "pure"); if (opts.isPure()) opts.binarizer(Binarizer::FixedThreshold); #ifndef PRINT_DEBUG #ifdef __cpp_lib_execution std::vector barcodes(imgPaths.size()); std::transform(std::execution::par, imgPaths.begin(), imgPaths.end(), barcodes.begin(), [&](const fs::path& imgPath) { return ReadBarcode(ImageLoader::load(imgPath).rotated(test.rotation), opts); }); for (size_t i = 0; i < imgPaths.size(); ++i) { const auto& imgPath = imgPaths[i]; const auto& barcode = barcodes[i]; #else auto futures = std::vector>>{}; for (const auto& imgPath : imgPaths) { futures.push_back(std::make_pair(imgPath, std::async(std::launch::async, [&](const fs::path& path) { return ReadBarcode(ImageLoader::load(path).rotated(test.rotation), opts); }, imgPath))); } for (auto& [imgPath, fut] : futures) { auto barcode = fut.get(); #endif // __cpp_lib_execution #else for (const auto& imgPath : imgPaths) { auto barcode = ReadBarcode(ImageLoader::load(imgPath).rotated(test.rotation), opts); #endif if (barcode.isValid()) { auto error = checkResult(imgPath, format, barcode); if (!error.empty()) tc.misReadFiles[imgPath] = error; } else { tc.notDetectedFiles.insert(imgPath); } } times.push_back(timeSince(startTime)); failures += printPositiveTestStats(Size(imgPaths), tc); } std::println(" | time: {:3} vs {:3} ms", times.front(), times.back()); if (!failures.empty()) std::println("\n{}", failures); } } static Barcode readMultiple(const std::vector& imgPaths, BarcodeFormat format) { Barcodes allBarcodes; for (const auto& imgPath : imgPaths) { auto barcodes = ReadBarcodes(ImageLoader::load(imgPath), ReaderOptions().formats(format).tryDownscale(false)); allBarcodes.insert(allBarcodes.end(), barcodes.begin(), barcodes.end()); } return MergeStructuredAppendSequence(allBarcodes); } static void doRunStructuredAppendTest(const fs::path& directory, BarcodeFormat format, int totalTests, const std::vector& tests) { auto imgPaths = getImagesInDirectory(directory); auto folderName = directory.stem(); std::map> imageGroups; for (const auto& imgPath : imgPaths) { std::string fn = imgPath.filename().string(); auto p = fn.find_last_of('-'); imageGroups[imgPath.parent_path() / fn.substr(0, p)].push_back(imgPath); } if (Size(imageGroups) != totalTests) std::println("TEST {} => Expected number of tests: {}, got: {} => FAILED", folderName.string(), totalTests, imageGroups.size()); for (auto& test : tests) { std::print("{:20} @ {:3}, {:3}", folderName.string(), test.rotation, Size(imgPaths)); auto tc = test.tc[0]; auto startTime = std::chrono::steady_clock::now(); for (const auto& [testPath, testImgPaths] : imageGroups) { auto barcode = readMultiple(testImgPaths, format); if (barcode.isValid()) { auto error = checkResult(testPath, format, barcode); if (!error.empty()) tc.misReadFiles[testPath] = error; } else { tc.notDetectedFiles.insert(testPath); } } auto failures = printPositiveTestStats(Size(imageGroups), tc); std::println(" | time: {:3} ms", timeSince(startTime)); if (!failures.empty()) std::println("\n{}", failures); } } int runBlackBoxTests(const fs::path& testPathPrefix, const std::set& includedTests) { auto hasTest = [&includedTests](const fs::path& dir) { auto stem = dir.stem().string(); return includedTests.empty() || Contains(includedTests, stem) || Contains(includedTests, stem.substr(0, stem.size() - 2)); }; auto runTests = [&](std::string_view directory, BarcodeFormat format, int total, const std::vector& tests, const ReaderOptions& opts = ReaderOptions()) { if (hasTest(directory) && format & (BarcodeFormat::AllReadable | BarcodeFormat::None)) doRunTests(testPathPrefix / directory, format, total, tests, opts); }; auto runStructuredAppendTest = [&](std::string_view directory, BarcodeFormat format, int total, const std::vector& tests) { if (hasTest(directory) && format & (BarcodeFormat::AllReadable | BarcodeFormat::None)) doRunStructuredAppendTest(testPathPrefix / directory, format, total, tests); }; try { auto startTime = std::chrono::steady_clock::now(); using enum BarcodeFormat; // clang-format off // Expected failures: // abc-inverted.png (fast) - fast does not try inverted // az-thick.png (pure) runTests("aztec-1", Aztec, 32, { { 31, 32, 0 }, { 31, 32, 90 }, { 31, 32, 180 }, { 31, 32, 270 }, { 30, 0, pure }, }); runTests("aztec-2", Aztec, 16, { { 16, 16, 0 }, { 16, 16, 90 }, { 16, 16, 180 }, { 16, 16, 270 }, }); runTests("datamatrix-1", DataMatrix, 30, { { 30, 30, 0 }, { 0, 28, 90 }, { 0, 28, 180 }, { 0, 28, 270 }, { 28, 0, pure }, }); runTests("datamatrix-2", DataMatrix, 13, { { 13, 13, 0 }, { 0, 13, 90 }, { 0, 13, 180 }, { 0, 13, 270 }, }); runTests("datamatrix-3", DataMatrix, 28, { { 26, 27, 0 }, { 0, 27, 90 }, { 0, 27, 180 }, { 0, 27, 270 }, }); runTests("datamatrix-4", DataMatrix, 21, { { 21, 21, 0 }, { 0, 21, 90 }, { 0, 21, 180 }, { 0, 21, 270 }, { 19, 0, pure }, }); runTests("dxfilmedge-1", DXFilmEdge, 3, { { 1, 3, 0 }, { 0, 3, 180 }, }); runTests("codabar-1", Codabar, 11, { { 11, 11, 0 }, { 11, 11, 180 }, }); runTests("codabar-2", Codabar, 4, { { 3, 4, 0 }, { 3, 4, 180 }, }); runTests("code39-1", Code39, 6, { { 6, 6, 0 }, { 6, 6, 180 }, }); runTests("code39-2", Code39Ext, 3, { { 3, 3, 0 }, { 3, 3, 180 }, }); runTests("code39-3", Code39, 5, { { 5, 5, 0 }, { 5, 5, 180 }, }); runTests("code93-1", Code93, 3, { { 3, 3, 0 }, { 3, 3, 180 }, }); runTests("code128-1", Code128, 6, { { 6, 6, 0 }, { 6, 6, 180 }, }); runTests("code128-2", Code128, 14, { { 13, 14, 0 }, { 13, 14, 180 }, }); runTests("code128-3", Code128, 2, { { 2, 2, 0 }, { 2, 2, 180 }, }); runTests("ean8-1", EAN8, 9, { { 9, 9, 0 }, { 9, 9, 180 }, { 8, 0, pure }, }); runTests("ean13-1", EAN13, 16, { { 10, 14, 0 }, { 11, 14, 180 }, }); runTests("ean13-2", EAN13, 16, { { 1, 6, 0 }, { 1, 6, 180 }, }); runTests("ean13-3", EAN13, 8, { { 8, 8, 0 }, { 8, 8, 180 }, }); runTests("ean13-4", EAN13, 20, { { 8, 12, 0 }, { 7, 12, 180 }, }); runTests("ean13-extension-1", EAN13, 5, { { 4, 5, 0 }, { 4, 5, 180 }, }, ReaderOptions().eanAddOnSymbol(EanAddOnSymbol::Require)); runTests("itf-1", ITF, 14, { { 13, 14, 0 }, { 13, 14, 180 }, }); runTests("itf-2", ITF, 6, { { 6, 6, 0 }, { 6, 6, 180 }, }); runTests("telepen-1", Telepen, 4, { { 4, 4, 0 }, { 4, 4, 180 }, }); runTests("upca-1", UPCA, 5, { { 4, 5, 0 }, { 4, 5, 180 }, }, ReaderOptions().formats(UPCA)); runTests("upca-2", UPCA, 26, { { 10, 16, 0 }, { 12, 16, 180 }, }, ReaderOptions().formats(UPCA)); runTests("upca-3", UPCA, 6, { { 3, 6, 0 }, { 3, 6, 180 }, }, ReaderOptions().formats(UPCA)); runTests("upca-4", UPCA, 13, { { 5, 9, 0 }, { 6, 9, 180 }, }, ReaderOptions().formats(UPCA)); runTests("upca-5", UPCA, 20, { { 7, 9, 0 }, { 7, 9, 180 }, }, ReaderOptions().formats(UPCA)); runTests("upca-extension-1", UPCA, 6, { { 4, 5, 0 }, { 4, 5, 180 }, }, ReaderOptions().eanAddOnSymbol(EanAddOnSymbol::Require).formats(UPCA)); runTests("upce-1", UPCE, 3, { { 3, 3, 0 }, { 3, 3, 180 }, { 3, 0, pure }, }); runTests("upce-2", UPCE, 15, { { 9, 10, 0 }, { 9, 10, 180 }, }); runTests("upce-3", UPCE, 11, { { 5, 7, 0 }, { 5, 7, 180 }, }); runTests("rss14-1", DataBarOmni, 13, { { 11, 11, 0 }, { 11, 11, 180 }, }); runTests("rss14-2", DataBarStk, 7, { { 5, 6, 0 }, { 6, 6, 180 }, }); runTests("rssexpanded-1", DataBarExp, 37, { { 37, 37, 0 }, { 37, 37, 180 }, { 37, 0, pure }, }); runTests("rssexpanded-2", DataBarExp, 7, { { 5, 7, 0 }, { 5, 7, 180 }, }); runTests("rssexpanded-3", DataBarExp, 118, { { 118, 118, 0 }, { 118, 118, 180 }, { 118, 0, pure }, }); runTests("rssexpandedstacked-1", DataBarExpStk, 62, { { 52, 62, 0 }, { 52, 62, 180 }, { 57, 0, pure }, }); runTests("rssexpandedstacked-2", DataBarExpStk, 2, { { 2, 2, 0 }, { 2, 2, 180 }, }); runTests("databarltd-1", DataBarLtd, 2, { { 2, 2, 0 }, { 2, 2, 180 }, { 1, 0, pure }, }); runTests("maxicode-1", MaxiCode, 9, { { 9, 9, 0 }, }); runTests("maxicode-2", MaxiCode, 4, { { 0, 0, 0 }, }); runTests("qrcode-1", QRCode, 8, { { 8, 8, 0 }, { 8, 8, 90 }, { 8, 8, 180 }, { 8, 8, 270 }, }); runTests("qrcode-2", QRCode, 54, { { 48, 51, 0 }, { 49, 52, 90 }, { 48, 51, 180 }, { 48, 51, 270 }, { 23, 1, pure }, // the misread is the 'outer' symbol in 16.png }); runTests("qrcode-3", QRCode, 18, { { 18, 18, 0 }, { 18, 18, 90 }, { 18, 18, 180 }, { 18, 18, 270 }, }); runTests("qrcode-4", QRCode, 24, { { 15, 15, 0 }, { 15, 15, 90 }, { 15, 15, 180 }, { 15, 15, 270 }, }); runTests("qrcode-5", QRCode, 16, { { 16, 16, 0 }, { 16, 16, 90 }, { 16, 16, 180 }, { 16, 16, 270 }, { 4, 0, pure }, }); runTests("qrcode-6", QRCode, 15, { { 15, 15, 0 }, { 15, 15, 90 }, { 15, 15, 180 }, { 15, 15, 270 }, }); runStructuredAppendTest("qrcode-7", QRCode, 1, { { 1, 1, 0 }, }); runTests("microqrcode-1", MicroQRCode, 16, { { 15, 15, 0 }, { 14, 14, 90 }, { 14, 14, 180 }, // ughs: 1 result is platform/compiler dependent (e.g. -march=core2 vs. haswell) { 15, 15, 270 }, { 9, 0, pure }, }); runTests("rmqrcode-1", RMQRCode, 3, { { 2, 3, 0 }, { 2, 3, 90 }, { 2, 3, 180 }, { 2, 3, 270 }, { 2, 2, pure }, }); runTests("pdf417-1", PDF417, 17, { { 16, 17, 0 }, { 1, 17, 90 }, { 16, 17, 180 }, { 1, 17, 270 }, { 16, 0, pure }, }); runTests("pdf417-2", PDF417, 25, { { 25, 25, 0 }, { 0, 25, 90 }, { 25, 25, 180 }, { 0, 25, 270 }, }); runTests("pdf417-3", PDF417, 16, { { 16, 16, 0 }, { 0, 16, 90 }, { 16, 16, 180 }, { 0, 16, 270 }, { 7, 0, pure }, }); runStructuredAppendTest("pdf417-4", PDF417, 3, { { 3, 3, 0 }, }); runTests("micropdf417-1", MicroPDF417, 5, { { 4, 4, 0 }, { 0, 4, 90 }, { 4, 4, 180 }, { 0, 4, 270 }, { 1, 0, pure }, }); runTests("falsepositives-1", None, 21, { { 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 90 }, { 0, 0, 0, 0, 180 }, { 0, 0, 0, 0, 270 }, { 0, 0, pure }, }); runTests("falsepositives-2", None, 25, { { 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 90 }, { 0, 0, 0, 0, 180 }, { 0, 0, 0, 0, 270 }, { 0, 0, pure }, }); // clang-format on int totalTime = timeSince(startTime); int decodeTime = totalTime - totalImageLoadTime; std::println("load time: {} ms.", totalImageLoadTime); std::println("decode time: {} ms.", decodeTime); std::println("total time: {} ms.", totalTime); if (failed) std::println("WARNING: {} tests failed.", failed); if (extra) std::println("INFO: {} tests succeeded unexpectedly.", extra); return failed; } catch (const std::exception& e) { std::println("{}", e.what()); } catch (...) { std::println("Internal error"); } return -1; } } // ZXing::Test