From 6376c1ae306136cda23631ef61ccd65cfe3290c0 Mon Sep 17 00:00:00 2001 From: Daniel Scharrer <~@ds.me> Date: Mon, 30 Dec 2024 19:29:43 +0100 Subject: [PATCH] Add a simple unit test framework --- CHANGELOG | 1 + CMakeLists.txt | 73 +++++++++++++++++++++++++ README.md | 7 ++- cmake/FilterList.cmake | 4 ++ src/util/test.cpp | 120 +++++++++++++++++++++++++++++++++++++++++ src/util/test.hpp | 81 ++++++++++++++++++++++++++++ 6 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 src/util/test.cpp create mode 100644 src/util/test.hpp diff --git a/CHANGELOG b/CHANGELOG index 0241869..23a837d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ innoextract 1.10 (TBD) - Added support for Inno Setup 6.3.x installers - Added support for a modified Inno Setup 5.3.10 variant + - Added unit tests innoextract 1.9 (2020-08-09) - Added preliminary support for Inno Setup 6.1.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ef455e..2dbf2d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,11 @@ option(DEVELOPER "Use build settings suitable for developers" OFF) option(CONTINUOUS_INTEGRATION "Use build settings suitable for CI" OFF) # Components +set(default_BUILD_TESTS OFF) +if(CONTINUOUS_INTEGRATION OR DEVELOPER) + set(default_BUILD_TESTS ON) +endif() +suboption(BUILD_TESTS "Build tests" BOOL ${default_BUILD_TESTS}) option(USE_ARC4 "Build ARC4 decryption support" ON) # Optional dependencies @@ -83,6 +88,15 @@ else() set(OPTIONAL_DEPENDENCY) endif() +# Test configuration +set(RUN_TARGET CACHE STRING "Wrapper to run built targets") +mark_as_advanced(RUN_TARGET) +set(default_RUN_TESTS OFF) +if((DEVELOPER OR CONTINUOUS_INTEGRATION) AND (NOT CMAKE_CROSSCOMPILING OR NOT RUN_TARGET STREQUAL "")) + set(default_RUN_TESTS ON) +endif() +suboption(RUN_TESTS "Run tests as part of the default build target" BOOL ${default_RUN_TESTS}) + # Install destinations if(CMAKE_VERSION VERSION_LESS 2.8.5) set(CMAKE_INSTALL_DATAROOTDIR "share" CACHE @@ -441,6 +455,7 @@ set(INNOEXTRACT_SOURCES src/util/storedenum.hpp src/util/time.hpp src/util/time.cpp + src/util/test.hpp src/util/types.hpp src/util/unique_ptr.hpp src/util/windows.hpp @@ -448,7 +463,15 @@ set(INNOEXTRACT_SOURCES ) +set(UNITTEST_SOURCES + + src/util/test.hpp + src/util/test.cpp + +) + filter_list(INNOEXTRACT_SOURCES ALL_INNOEXTRACT_SOURCES) +filter_list(UNITTEST_SOURCES ALL_UNITTEST_SOURCES) create_source_groups(ALL_INNOEXTRACT_SOURCES) @@ -481,6 +504,44 @@ install(TARGETS innoextract RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES ${MAN_FILE} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 OPTIONAL) +# Test target + +if(BUILD_TESTS) + + enable_testing() + + set(run_tests) + if(RUN_TESTS) + set(run_tests ALL) + endif() + + add_executable(unittest ${UNITTEST_SOURCES}) + target_link_libraries(unittest ${LIBRARIES}) + target_compile_definitions(unittest PRIVATE INNOEXTRACT_BUILD_TESTS) + + set(unittest_binary "$") + + add_test(NAME "unit tests" + COMMAND ${RUN_TARGET} "${unittest_binary}" + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}" + ) + + add_custom_command( + OUTPUT "${PROJECT_BINARY_DIR}/unittest.check" + COMMAND ${RUN_TARGET} "${unittest_binary}" + COMMAND ${CMAKE_COMMAND} -E touch "${PROJECT_BINARY_DIR}/unittest.check" + DEPENDS unittest + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}" + COMMENT "Running unit tests" VERBATIM + ) + + add_custom_target(check ${run_tests} + DEPENDS "${PROJECT_BINARY_DIR}/unittest.check" + ) + +endif() + + # Additional targets. add_style_check_target(style "${ALL_INNOEXTRACT_SOURCES}" innoextract) @@ -525,10 +586,22 @@ print_configuration("Charset conversion" message("") if(DEVELOPER) + file(READ "README.md" readme) parse_version_file("VERSION" "VERSION") string(REPLACE "${VERSION_2}" "" readme_without_version "${readme}") if(readme_without_version STREQUAL readme) message(WARNING "Could not find '${VERSION_2}' in README.md.") endif() + + foreach(file IN LISTS ALL_INNOEXTRACT_SOURCES) + file(READ "${file}" source) + if(source MATCHES ".*INNOEXTRACT_TEST.*") + list(FIND ALL_UNITTEST_SOURCES ${file} result) + if(result EQUAL -1) + message(WARNING "Could not find '${file}' in UNITTEST_SOURCES") + endif() + endif() + endforeach() + endif() diff --git a/README.md b/README.md index 6cae1f5..b5e1e36 100644 --- a/README.md +++ b/README.md @@ -68,13 +68,16 @@ The default build settings are tuned for users - if you plan to make changes to | `DEVELOPER` | `OFF` | Enable build options suitable for developers⁵. | `FASTLINK` | `OFF`⁶ | Optimize for link speed. | `USE_LTO` | `ON`² | Use link-time code generation. +| `BUILD_TESTS` | `OFF`⁶ | Build unit tests that can be run using `make check` +| `RUN_TESTS` | `OFF`⁷ | Automatically run tests +| `RUN_TARGET` | (none) | Wrapper to run binaries produced in the build process 1. The builtin charset conversion only supports Windows-1252 and UTF-16LE. This is normally enough for filenames, but custom message strings (which can be included in filenames) may use arbitrary encodings. 2. Enabled automatically if `CMAKE_BUILD_TYPE` is set to `Debug`. 3. Under Windows, the default is `ON`. 4. Default is `ON` if `USE_STATIC_LIBS` is enabled. -5. Currently this and enables `DEBUG` and `FASTLINK` for faster incremental builds and improved debug output, unless those options have been explicitly specified by the user. +5. Currently this and enables `DEBUG`, `BUILD_TESTS`, `RUN_TESTS` and `FASTLINK` for faster incremental builds and improved debug output, unless those options have been explicitly specified by the user. 6. Enabled automatically if `DEVELOPER` is enabled. -7. Disabled automatically if `SET_OPTIMIZATION_FLAGS` is disabled or `FASTLINK` is enabled. +7. Enabled automatically if `DEVELOPER` is enabled unless cross-compiling without `RUN_TARGET` set Install options: diff --git a/cmake/FilterList.cmake b/cmake/FilterList.cmake index 5535359..156300c 100644 --- a/cmake/FilterList.cmake +++ b/cmake/FilterList.cmake @@ -131,6 +131,10 @@ function(filter_list LIST_NAME) message(FATAL_ERROR "bad filter_list syntax: unexpected end, expected }") endif() + if(last_item) + list(APPEND filtered ${last_item}) + endif() + list(SORT filtered) list(REMOVE_DUPLICATES filtered) set(${LIST_NAME} ${filtered} PARENT_SCOPE) diff --git a/src/util/test.cpp b/src/util/test.cpp new file mode 100644 index 0000000..8a21167 --- /dev/null +++ b/src/util/test.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2024 Daniel Scharrer + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the author(s) be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include "util/test.hpp" + +#include "util/windows.hpp" + +#include "configure.hpp" + +#if INNOEXTRACT_HAVE_ISATTY +#include +#endif + +namespace { + +bool test_verbose = false; +bool test_progress = false; +int test_failed = 0; + +} // anonymous namewspace + +Testsuite * Testsuite::tests = NULL; + +Testsuite::Testsuite(const char * suitename) : name(suitename) { + next = tests; + tests = this; +} + +int Testsuite::run_all() { + + int count = 0; + for(Testsuite * test = tests; test; test = test->next) { + count++; + } + int len = 2; + int r = count; + while(r >= 10) { + len++; + r /= 10; + } + + int i = 0; + for(Testsuite * test = tests; test; test = test->next) { + i++; + if(test_verbose || test_progress) { + std::printf("%*d/%d [%s]", len, i, count, test->name); + if(test_verbose) { + std::printf("\n"); + } + } + try { + test->run(); + } catch(...) { + test_failed++; + if(test_progress) { + std::printf("\r\x1b[K"); + } + std::fprintf(stderr, "%s: EXCEPTION\n", test->name); + } + } + + if(test_progress) { + std::printf("\r\x1b[K"); + } + if(test_failed == 0) { + std::printf("all %d test suites ok\n", count); + } + + return test_failed > 0 ? 1 : 0; +} + +void Testsuite::test(const char * testcase, bool ok) { + if(!ok) { + test_failed++; + } + if(test_progress) { + std::printf("\r\x1b[K"); + } + if(!ok || test_verbose) { + std::fprintf(ok ? stdout : stderr, "%s.%s: %s\n", name, testcase, ok ? "ok" : "FAILED"); + } +} + +int main(int argc, const char * argv[]) { + + if((argc > 1 && std::strcmp(argv[1], "--verbose") == 0) || \ + (argc > 1 && argv[1][0] == '-' && argv[1][1] != '-' && std::strchr(argv[1], 'v')) || \ + (std::getenv("VERBOSE") && std::strcmp(std::getenv("VERBOSE"), "0") != 0)) { + test_verbose = true; + } else { + #if defined(_WIN32) || INNOEXTRACT_HAVE_ISATTY + test_progress = isatty(1) && isatty(2); + #endif + if(test_progress) { + char * term = std::getenv("TERM"); + if(!term || !std::strcmp(term, "dumb")) { + test_progress = false; // Terminal does not support escape sequences + } + } + } + + return Testsuite::run_all(); +} diff --git a/src/util/test.hpp b/src/util/test.hpp new file mode 100644 index 0000000..0f39fe7 --- /dev/null +++ b/src/util/test.hpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 Daniel Scharrer + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the author(s) be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/*! + * \file + * + * Test utility functions. + */ +#ifndef INNOEXTRACT_UTIL_TEST_HPP +#define INNOEXTRACT_UTIL_TEST_HPP + +#ifdef INNOEXTRACT_BUILD_TESTS + +#include +#include +#include +#include + +static const char * testdata = "The dhole (pronounced \"dole\") is also known as the Asiatic wild dog," + " red dog, and whistling dog. It is about the size of a German shepherd but" + " looks more like a long-legged fox. This highly elusive and skilled jumper" + " is classified with wolves, coyotes, jackals, and foxes in the taxonomic" + " family Canidae."; +static const size_t testlen = std::strlen(testdata); + +struct Testsuite { + + Testsuite(const char * suitename); + + static int run_all(); + + void test(const char * testcase, bool ok); + + inline void test_equals(const char * testcase, const void * a, const void * b, size_t count) { + test(testcase, std::memcmp(a, b, count) == 0); + } + + virtual void run() = 0; + +private: + + static Testsuite * tests; + Testsuite * next; + +protected: + + const char * name; + +}; + +#define INNOEXTRACT_TEST(Name, ...) \ + struct Name ## _test : public Testsuite { \ + Name ## _test() : Testsuite(# Name) { } \ + void run(); \ + } test_ ## Name; \ + void Name ## _test::run() { __VA_ARGS__ } + +#else + +#define INNOEXTRACT_TEST(Name, ...) + +#endif // INNOEXTRACT_BUILD_TESTS + +#endif // INNOEXTRACT_UTIL_TEST_HPP