diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c863c7cbb..e0c68aeff 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,7 @@ include(Catch) set(TEST_SOURCES zm_comms.cpp zm_crypt.cpp + zm_font.cpp zm_utils.cpp) add_executable(tests main.cpp ${TEST_SOURCES}) @@ -31,3 +32,9 @@ target_include_directories(tests ${CMAKE_CURRENT_BINARY_DIR}) catch_discover_tests(tests) + +add_custom_command(TARGET tests + PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/data/ ${CMAKE_CURRENT_BINARY_DIR}/data/ + BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/data/) diff --git a/tests/data/fonts/01_bad_magic.zmfnt b/tests/data/fonts/01_bad_magic.zmfnt new file mode 100644 index 000000000..a73d2b51f Binary files /dev/null and b/tests/data/fonts/01_bad_magic.zmfnt differ diff --git a/tests/data/fonts/02_variant_invalid.zmfnt b/tests/data/fonts/02_variant_invalid.zmfnt new file mode 100644 index 000000000..9cbd0b024 Binary files /dev/null and b/tests/data/fonts/02_variant_invalid.zmfnt differ diff --git a/tests/data/fonts/03_missing_cps.zmfnt b/tests/data/fonts/03_missing_cps.zmfnt new file mode 100644 index 000000000..b66e696ed Binary files /dev/null and b/tests/data/fonts/03_missing_cps.zmfnt differ diff --git a/tests/data/fonts/04_valid.zmfnt b/tests/data/fonts/04_valid.zmfnt new file mode 100644 index 000000000..3cb839931 Binary files /dev/null and b/tests/data/fonts/04_valid.zmfnt differ diff --git a/tests/data/fonts/generate_fonts.py b/tests/data/fonts/generate_fonts.py new file mode 100644 index 000000000..8fe24be8c --- /dev/null +++ b/tests/data/fonts/generate_fonts.py @@ -0,0 +1,54 @@ +#!/bin/python + +import struct + +GOOD_MAGIC = b"ZMFNT\0" +BAD_MAGIC = b"ABCDE\0" +NUM_FONT_SIZES = 4 + + +class FontFile: + def __init__(self, path): + self.path = path + + def write_file_header(self, magic): + with open(self.path, "wb") as f: + f.write(magic) + f.write(struct.pack("BB", 0, 0)) # pad + + def write_bm_header(self, height, width, cp_count, idx): + with open(self.path, "ab") as f: + f.write(struct.pack("HHIII", height, width, cp_count, idx, 0)) + + def write_codepoints(self, value, height, count): + with open(self.path, "ab") as f: + for _ in range(height * count): + f.write(struct.pack("Q", value)) + + +font = FontFile("01_bad_magic.zmfnt") +font.write_file_header(BAD_MAGIC) + +# height, width and number of codepoints out of bounds +font = FontFile("02_variant_invalid.zmfnt") +font.write_file_header(GOOD_MAGIC) +font.write_bm_header(201, 65, 256, 0) + +# mismatch between number of codepoints specified in header and actually stored ones +font = FontFile("03_missing_cps.zmfnt") +font.write_file_header(GOOD_MAGIC) +offs = 0 +for _ in range(NUM_FONT_SIZES): + font.write_bm_header(10, 10, 10, offs) + offs += 10 * 10 +for _ in range(NUM_FONT_SIZES): + font.write_codepoints(1, 10, 9) + +font = FontFile("04_valid.zmfnt") +font.write_file_header(GOOD_MAGIC) +offs = 0 +for i in range(NUM_FONT_SIZES): + font.write_bm_header(10 + i, 10 + i, 10, offs) + offs += 10 * (10 + i) +for i in range(NUM_FONT_SIZES): + font.write_codepoints(i, 10 + i, 10) diff --git a/tests/zm_font.cpp b/tests/zm_font.cpp new file mode 100644 index 000000000..3f4d6010e --- /dev/null +++ b/tests/zm_font.cpp @@ -0,0 +1,156 @@ +/* + * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "catch2/catch.hpp" + +#include "zm_font.h" + +CATCH_REGISTER_ENUM(FontLoadError, + FontLoadError::kOk, + FontLoadError::kFileNotFound, + FontLoadError::kInvalidFile) + +TEST_CASE("FontVariant: construction") { + FontVariant variant; + + SECTION("default construction") { + REQUIRE(variant.GetCharHeight() == 0); + REQUIRE(variant.GetCharWidth() == 0); + } + + SECTION("values in range") { + constexpr uint8 height = 10; + constexpr uint8 width = 10; + std::vector bitmap(FontVariant::kMaxNumCodePoints * height); + + REQUIRE_NOTHROW(variant = FontVariant(height, width, bitmap)); + + REQUIRE(variant.GetCharHeight() == height); + REQUIRE(variant.GetCharWidth() == width); + REQUIRE(variant.GetCodepointsCount() == FontVariant::kMaxNumCodePoints); + } + + SECTION("height out of range") { + constexpr uint8 height = FontVariant::kMaxCharHeight + 1; + constexpr uint8 width = 10; + std::vector bitmap(FontVariant::kMaxNumCodePoints * height); + + REQUIRE_THROWS(variant = FontVariant(height, width, bitmap)); + } + + SECTION("width out of range") { + constexpr uint8 height = 10; + constexpr uint8 width = FontVariant::kMaxCharWidth + 1; + std::vector bitmap(FontVariant::kMaxNumCodePoints * height); + + REQUIRE_THROWS(variant = FontVariant(height, width, bitmap)); + } + + SECTION("bitmap of wrong size") { + constexpr uint8 height = 10; + constexpr uint8 width = 10; + std::vector bitmap(FontVariant::kMaxNumCodePoints * height + 1); + + REQUIRE_THROWS(variant = FontVariant(height, width, bitmap)); + } +} + +TEST_CASE("FontVariant: GetCodepoint") { + constexpr uint8 height = 10; + constexpr uint8 width = 10; + std::vector bitmap(FontVariant::kMaxNumCodePoints * height); + + // fill bitmap for each codepoint alternating with 1 and std::numeric_limits::max() + std::generate(bitmap.begin(), bitmap.end(), + [n = 0, zero = true]() mutable { + if (n == height) { + zero = !zero; + n = 0; + } + n++; + if (zero) { + return static_cast(1); + } else { + return std::numeric_limits::max(); + } + }); + + FontVariant variant(height, width, bitmap); + nonstd::span cp; + + SECTION("in bounds") { + cp = variant.GetCodepoint(0); + REQUIRE(std::all_of(cp.begin(), cp.end(), + [](uint64 l) { return l == 1; }) == true); + + cp = variant.GetCodepoint(1); + REQUIRE(std::all_of(cp.begin(), cp.end(), + [](uint64 l) { return l == std::numeric_limits::max(); }) == true); + } + + SECTION("out-of-bounds: all-zero bitmap") { + cp = variant.GetCodepoint(FontVariant::kMaxNumCodePoints); + REQUIRE(std::all_of(cp.begin(), cp.end(), + [](uint64 l) { return l == 0; }) == true); + } +} + +TEST_CASE("ZmFont: variants not loaded") { + ZmFont font; + + SECTION("returns empty variant") { + FontVariant variant; + REQUIRE_NOTHROW(variant = font.GetFontVariant(0)); + + REQUIRE(variant.GetCharHeight() == 0); + REQUIRE(variant.GetCharWidth() == 0); + REQUIRE(variant.GetCodepoint(0).empty() == true); + } + + SECTION("variant idx out-of-bounds") { + REQUIRE_THROWS(font.GetFontVariant(kNumFontSizes)); + } +} + +TEST_CASE("ZmFont: load font file") { + ZmFont font; + + SECTION("file not found") { + REQUIRE(font.LoadFontFile("does_not_exist.zmfnt") == FontLoadError::kFileNotFound); + } + + SECTION("invalid files") { + REQUIRE(font.LoadFontFile("data/fonts/01_bad_magic.zmfnt") == FontLoadError::kInvalidFile); + REQUIRE(font.LoadFontFile("data/fonts/02_variant_invalid.zmfnt") == FontLoadError::kInvalidFile); + REQUIRE(font.LoadFontFile("data/fonts/03_missing_cps.zmfnt") == FontLoadError::kInvalidFile); + } + + SECTION("valid file") { + REQUIRE(font.LoadFontFile("data/fonts/04_valid.zmfnt") == FontLoadError::kOk); + + uint8 var_idx = GENERATE(range(static_cast(0), kNumFontSizes)); + FontVariant variant = font.GetFontVariant(var_idx); + REQUIRE(variant.GetCharHeight() == 10 + var_idx); + REQUIRE(variant.GetCharWidth() == 10 + var_idx); + + uint8 cp_idx = + GENERATE_COPY(range(static_cast(0), variant.GetCodepointsCount())); + nonstd::span cp = variant.GetCodepoint(cp_idx); + REQUIRE(std::all_of(cp.begin(), cp.end(), + [=](uint64 l) { return l == var_idx; }) == true); + } +}