From 753a6e0e1d624e70ede1edf320fb544d8ea9661c Mon Sep 17 00:00:00 2001 From: Plamen Dragiyski Date: Sat, 21 Feb 2026 14:15:08 +0200 Subject: [PATCH] unit tests: coverage, settings --- CMakeLists.txt | 46 ++++++++- coverage.sh | 9 ++ src/trim.cpp | 4 +- test.sh | 6 ++ tests/data/test-scan-1.txt | 20 ++++ tests/settings_test.cpp | 187 +++++++++++++++++++++++++++++++++++++ tests/trim_test.cpp | 36 +++++++ 7 files changed, 305 insertions(+), 3 deletions(-) create mode 100755 coverage.sh create mode 100755 test.sh create mode 100644 tests/data/test-scan-1.txt create mode 100644 tests/settings_test.cpp create mode 100644 tests/trim_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb757c..51b6e26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,5 +7,49 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g -O3") set(CMAKE_CXX_FLAGS "-fno-threadsafe-statics") -file(GLOB SRC_FILES src/*.cpp) +option(ENABLE_COVERAGE "Enable coverage instrumentation for tests" OFF) + +file(GLOB SRC_FILES CONFIGURE_DEPENDS src/*.cpp) + +set(APP_SOURCES ${SRC_FILES}) +list(REMOVE_ITEM APP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp) + add_executable(wavefront_parser ${SRC_FILES}) +target_include_directories(wavefront_parser PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +find_package(GTest REQUIRED) + +enable_testing() + +add_executable(wavefront_tests + tests/trim_test.cpp + tests/settings_test.cpp + ${APP_SOURCES} +) +target_link_libraries(wavefront_tests PRIVATE + GTest::gtest + GTest::gmock + GTest::gtest_main +) +target_include_directories(wavefront_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +if(ENABLE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(wavefront_tests PRIVATE --coverage -O0 -g) + target_link_options(wavefront_tests PRIVATE --coverage) +endif() + +include(GoogleTest) +gtest_discover_tests(wavefront_tests) + +find_program(LCOV_EXECUTABLE lcov) +find_program(GENHTML_EXECUTABLE genhtml) +if(LCOV_EXECUTABLE AND GENHTML_EXECUTABLE) + add_custom_target(coverage + COMMAND ${LCOV_EXECUTABLE} --directory "${CMAKE_BINARY_DIR}" --capture --rc geninfo_unexecuted_blocks=1 --demangle-cpp --ignore-errors inconsistent --keep-going --output-file coverage.info + COMMAND ${LCOV_EXECUTABLE} --extract coverage.info "${CMAKE_SOURCE_DIR}/src/*" --output-file coverage.info + COMMAND ${GENHTML_EXECUTABLE} coverage.info --output-directory coverage + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + COMMENT "Generating lcov report at \"${CMAKE_BINARY_DIR}/coverage/index.html\"" + VERBATIM + ) +endif() diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 0000000..4a07265 --- /dev/null +++ b/coverage.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +cmake -S . -B build/Coverage -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON +cmake --build build/Coverage --config Debug +ctest --test-dir build/Coverage --output-on-failure +cmake --build build/Coverage --target coverage + +echo "Coverage report: file://$(pwd)/build/Coverage/coverage/index.html" diff --git a/src/trim.cpp b/src/trim.cpp index 9762dd3..40da42a 100644 --- a/src/trim.cpp +++ b/src/trim.cpp @@ -5,10 +5,10 @@ namespace wavefront { auto start = source.data(); // size is the size of buffer, data() + size() is the first available byte after the string. auto end = source.data() + source.size() - 1; - while (start < end && std::isspace(*start)) { + while (start <= end && std::isspace(*start)) { ++start; } - while (start < end && (std::isspace(*end) || *end == 0)) { + while (start <= end && (std::isspace(*end) || *end == 0)) { --end; } return std::string_view(start, end - start + 1); diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..c706622 --- /dev/null +++ b/test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +cmake -S . -B build/Debug -DCMAKE_BUILD_TYPE=Debug +cmake --build build/Debug --config Debug +ctest --test-dir build/Debug --output-on-failure diff --git a/tests/data/test-scan-1.txt b/tests/data/test-scan-1.txt new file mode 100644 index 0000000..4223d07 --- /dev/null +++ b/tests/data/test-scan-1.txt @@ -0,0 +1,20 @@ +# Command +v 0.1 0.2 0.3 +v 0.2 0.3 0.4 +v 0.3 0.4 0.5 +v 1.1 1.2 1.3 +v 1.2 1.3 1.4 +v 1.3 1.4 1.5 +v 2.1 2.2 2.3 +v 2.2 2.3 2.4 +v 2.3 2.4 2.5 +vn 0.15 0.25 0.35 +vn 0.25 0.35 0.45 +vn 0.35 0.45 0.55 +vt 0.9 0.8 +vt 0.8 0.7 +f 1/1/1 2/2/2 3/3/2 +o test +f 4/3/2 5/2/1 6/1/2 +g test +f 7/2/2 8/3/2 9/1/1 diff --git a/tests/settings_test.cpp b/tests/settings_test.cpp new file mode 100644 index 0000000..163467f --- /dev/null +++ b/tests/settings_test.cpp @@ -0,0 +1,187 @@ +#include +#include +#include +#include +#include + +#include + +#include "settings.hpp" + +namespace { + class ScopedFileCleanup { + public: + explicit ScopedFileCleanup(std::filesystem::path path) : path_(std::move(path)) {} + ScopedFileCleanup(const ScopedFileCleanup &) = delete; + ScopedFileCleanup &operator=(const ScopedFileCleanup &) = delete; + ScopedFileCleanup(ScopedFileCleanup &&) = delete; + ScopedFileCleanup &operator=(ScopedFileCleanup &&) = delete; + ~ScopedFileCleanup() { + if (!path_.empty()) { + std::error_code error; + std::filesystem::remove(path_, error); + } + } + + private: + std::filesystem::path path_; + }; + + std::string random_token(std::size_t length) { + static const char charset[] = + "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + std::string result; + result.reserve(length); + + std::mt19937 engine{std::random_device{}()}; + std::uniform_int_distribution dist(0, sizeof(charset) - 2); + for (std::size_t i = 0; i < length; ++i) { + result.push_back(charset[dist(engine)]); + } + return result; + } + + TEST(SettingsBuilder, DefaultsToStdStreamsAndFlagsOff) { + wavefront::SettingsBuilder builder; + + EXPECT_EQ(&builder.input(), &std::cin); + EXPECT_EQ(&builder.output(), &std::cout); + EXPECT_FALSE(builder.with_normals()); + EXPECT_FALSE(builder.with_texcoords()); + EXPECT_FALSE(builder.use_float64()); + + wavefront::Settings settings = builder.build(); + + EXPECT_EQ(&settings.input(), &std::cin); + EXPECT_EQ(&settings.output(), &std::cout); + EXPECT_FALSE(settings.extract_normals()); + EXPECT_FALSE(settings.extract_texcoords()); + EXPECT_FALSE(settings.use_float64()); + EXPECT_TRUE(settings.selected_objects().empty()); + EXPECT_TRUE(settings.selected_groups().empty()); + } + + TEST(SettingsBuilder, AppliesConfigurationAndSelection) { + std::istringstream input("dummy"); + std::ostringstream output; + + wavefront::SettingsBuilder builder; + builder.input(input) + .output(output) + .with_normals(true) + .with_texcoords(true) + .use_float64(true); + builder.selected_objects().push_back("object_a"); + builder.selected_groups().push_back("group_a"); + + EXPECT_EQ(&builder.input(), &input); + EXPECT_EQ(&builder.output(), &output); + EXPECT_TRUE(builder.with_normals()); + EXPECT_TRUE(builder.with_texcoords()); + EXPECT_TRUE(builder.use_float64()); + + wavefront::Settings settings = builder.build(); + + EXPECT_EQ(&settings.input(), &input); + EXPECT_EQ(&settings.output(), &output); + EXPECT_TRUE(settings.extract_normals()); + EXPECT_TRUE(settings.extract_texcoords()); + EXPECT_TRUE(settings.use_float64()); + ASSERT_EQ(settings.selected_objects().size(), 1u); + EXPECT_EQ(settings.selected_objects()[0], "object_a"); + ASSERT_EQ(settings.selected_groups().size(), 1u); + EXPECT_EQ(settings.selected_groups()[0], "group_a"); + } + + TEST(SettingsBuilder, CanOpenFileFromString) { + const auto temp_dir = std::filesystem::temp_directory_path(); + const auto token = random_token(12); + const auto input_path = temp_dir / std::filesystem::path("dragiyski-wavefront-parser-test-settings-input-" + token + ".txt"); + const auto output_path = temp_dir / std::filesystem::path("dragiyski-wavefront-parser-test-settings-output-" + token + ".txt"); + const ScopedFileCleanup input_cleanup(input_path); + const ScopedFileCleanup output_cleanup(output_path); + + { + std::ofstream seed(input_path); + seed << token; + } + + wavefront::SettingsBuilder builder; + builder.input(input_path.string()) + .output(output_path.string()); + + wavefront::Settings settings = builder.build(); + + EXPECT_NE(&settings.input(), &std::cin); + EXPECT_NE(&settings.output(), &std::cout); + + auto *input_stream = dynamic_cast(&settings.input()); + ASSERT_NE(input_stream, nullptr); + EXPECT_TRUE(input_stream->is_open()); + + std::string input_contents; + *input_stream >> input_contents; + EXPECT_EQ(input_contents, token); + + auto *output_stream = dynamic_cast(&settings.output()); + ASSERT_NE(output_stream, nullptr); + EXPECT_TRUE(output_stream->is_open()); + + *output_stream << token; + output_stream->flush(); + + std::ifstream verify(output_path); + std::string output_contents; + verify >> output_contents; + EXPECT_EQ(output_contents, token); + } + + TEST(SettingsBuilder, CanOpenFileFromMovedStreams) { + const auto temp_dir = std::filesystem::temp_directory_path(); + const auto token = random_token(12); + const auto input_path = temp_dir / std::filesystem::path("dragiyski-wavefront-parser-test-settings-input-move-" + token + ".txt"); + const auto output_path = temp_dir / std::filesystem::path("dragiyski-wavefront-parser-test-settings-output-move-" + token + ".txt"); + const ScopedFileCleanup input_cleanup(input_path); + const ScopedFileCleanup output_cleanup(output_path); + + { + std::ofstream seed(input_path); + seed << token; + } + + std::ifstream input_stream(input_path); + std::ofstream output_stream(output_path); + + wavefront::SettingsBuilder builder; + builder.input(std::move(input_stream)) + .output(std::move(output_stream)); + + wavefront::Settings settings = builder.build(); + + EXPECT_NE(&settings.input(), &std::cin); + EXPECT_NE(&settings.output(), &std::cout); + + auto *input_file = dynamic_cast(&settings.input()); + ASSERT_NE(input_file, nullptr); + EXPECT_TRUE(input_file->is_open()); + + std::string input_contents; + *input_file >> input_contents; + EXPECT_EQ(input_contents, token); + + auto *output_file = dynamic_cast(&settings.output()); + ASSERT_NE(output_file, nullptr); + EXPECT_TRUE(output_file->is_open()); + + *output_file << token; + output_file->flush(); + + std::ifstream verify(output_path); + std::string output_contents; + verify >> output_contents; + EXPECT_EQ(output_contents, token); + } +} + diff --git a/tests/trim_test.cpp b/tests/trim_test.cpp new file mode 100644 index 0000000..562684c --- /dev/null +++ b/tests/trim_test.cpp @@ -0,0 +1,36 @@ +#include + +#include +#include + +#include "trim.hpp" + +namespace { + using ::testing::StrEq; + + struct TrimCase { + const char *source; + const char *expected; + }; + + class TrimTest : public ::testing::TestWithParam { + }; + + TEST_P(TrimTest, ReturnsTrimmedString) { + const auto ¶m = GetParam(); + std::string_view result = wavefront::trim(param.source); + EXPECT_THAT(std::string(result), StrEq(param.expected)); + } + + INSTANTIATE_TEST_SUITE_P( + TrimCases, + TrimTest, + ::testing::Values( + TrimCase{" hello world ", "hello world"}, + TrimCase{"\t\n spaced\t\n", "spaced"}, + TrimCase{"trimmed", "trimmed"}, + TrimCase{" ", ""}, + TrimCase{"", ""} + ) + ); +}