unit tests: coverage, settings
This commit is contained in:
@@ -7,5 +7,49 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g -O3")
|
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g -O3")
|
||||||
set(CMAKE_CXX_FLAGS "-fno-threadsafe-statics")
|
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})
|
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()
|
||||||
|
|||||||
9
coverage.sh
Executable file
9
coverage.sh
Executable file
@@ -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"
|
||||||
@@ -5,10 +5,10 @@ namespace wavefront {
|
|||||||
auto start = source.data();
|
auto start = source.data();
|
||||||
// size is the size of buffer, data() + size() is the first available byte after the string.
|
// size is the size of buffer, data() + size() is the first available byte after the string.
|
||||||
auto end = source.data() + source.size() - 1;
|
auto end = source.data() + source.size() - 1;
|
||||||
while (start < end && std::isspace(*start)) {
|
while (start <= end && std::isspace(*start)) {
|
||||||
++start;
|
++start;
|
||||||
}
|
}
|
||||||
while (start < end && (std::isspace(*end) || *end == 0)) {
|
while (start <= end && (std::isspace(*end) || *end == 0)) {
|
||||||
--end;
|
--end;
|
||||||
}
|
}
|
||||||
return std::string_view(start, end - start + 1);
|
return std::string_view(start, end - start + 1);
|
||||||
|
|||||||
6
test.sh
Executable file
6
test.sh
Executable file
@@ -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
|
||||||
20
tests/data/test-scan-1.txt
Normal file
20
tests/data/test-scan-1.txt
Normal file
@@ -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
|
||||||
187
tests/settings_test.cpp
Normal file
187
tests/settings_test.cpp
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <random>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#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<std::size_t> 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<std::ifstream *>(&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<std::ofstream *>(&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<std::ifstream *>(&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<std::ofstream *>(&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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
36
tests/trim_test.cpp
Normal file
36
tests/trim_test.cpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "trim.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using ::testing::StrEq;
|
||||||
|
|
||||||
|
struct TrimCase {
|
||||||
|
const char *source;
|
||||||
|
const char *expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TrimTest : public ::testing::TestWithParam<TrimCase> {
|
||||||
|
};
|
||||||
|
|
||||||
|
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{"", ""}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user