1
0

Compare commits

..

5 Commits

Author SHA1 Message Date
306633d01e refactor parse + unit test 2026-02-21 15:44:33 +02:00
6a3a7541ea test: scan 2026-02-21 14:47:30 +02:00
0f40f89cbb move tests to tests/unit 2026-02-21 14:20:36 +02:00
753a6e0e1d unit tests: coverage, settings 2026-02-21 14:15:08 +02:00
ad55b1109d feature: write indexed data to file. 2026-02-20 22:20:10 +02:00
15 changed files with 877 additions and 370 deletions

View File

@@ -7,5 +7,51 @@ 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/unit/trim_test.cpp
tests/unit/settings_test.cpp
tests/unit/scan_test.cpp
tests/unit/parse_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
View 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"

View File

@@ -88,12 +88,12 @@ namespace wavefront {
} else {
target.push_back(0);
}
std::copy(
source.begin(),
source.end(),
std::back_inserter(target)
);
}
std::copy(
source.begin(),
source.end(),
std::back_inserter(target)
);
},
std::index_sequence<1, 2, 3>{}
);
@@ -227,20 +227,28 @@ namespace wavefront {
std::array<IndexType, 3> triangle_data;
std::size_t triangle_vertex_index = 0;
for (const auto &vertex : triangle) {
std::array<IndexType, VertexDimCount> coordinate_indices;
for (std::size_t i = 0; i < VertexDimCount; ++i) {
const auto &wavefront_index = mapper[i].get_vertex_index(vertex);
const auto &line_index = mapper[i].attribute_line_map.at(wavefront_index);
const auto &coordinate_index = mapper[i].line_coordinate_map.at(line_index);
if constexpr (VertexDimCount > 1) {
std::array<IndexType, VertexDimCount> coordinate_indices;
for (std::size_t i = 0; i < VertexDimCount; ++i) {
const auto &wavefront_index = mapper[i].get_vertex_index(vertex);
const auto &line_index = mapper[i].attribute_line_map.at(wavefront_index);
const auto &coordinate_index = mapper[i].line_coordinate_map.at(line_index);
assert(coordinate_index != 0);
coordinate_indices[i] = coordinate_index;
}
auto iterator = std::lower_bound(vertex_data.begin(), vertex_data.end(), coordinate_indices);
assert(iterator != vertex_data.end());
auto index = std::distance(vertex_data.begin(), iterator);
assert(index != 0);
assert(triangle_vertex_index < 3);
triangle_data[triangle_vertex_index++] = index;
} else {
const auto &wavefront_index = mapper[0].get_vertex_index(vertex);
const auto &line_index = mapper[0].attribute_line_map.at(wavefront_index);
const auto &coordinate_index = mapper[0].line_coordinate_map.at(line_index);
assert(coordinate_index != 0);
coordinate_indices[i] = coordinate_index;
triangle_data[triangle_vertex_index++] = coordinate_index;
}
auto iterator = std::lower_bound(vertex_data.begin(), vertex_data.end(), coordinate_indices);
assert(iterator != vertex_data.end());
auto index = std::distance(vertex_data.begin(), iterator);
assert(index != 0);
assert(triangle_vertex_index < 3);
triangle_data[triangle_vertex_index++] = index;
}
assert(triangle_vertex_index == 3);
triangle_data_set.emplace(triangle_data);

View File

@@ -124,10 +124,10 @@ int main(int argc, char** argv) {
using FloatType = float;
using IndexType = std::uint32_t;
CATCH_AND_RETURN(scan_data, wavefront::scan_error, 1, wavefront::scan(
auto scan_data = wavefront::scan(
settings.input(),
settings.selected_objects(),
settings.selected_groups())
settings.selected_groups()
);
std::cerr << "Scanned " << scan_data.total_lines << " lines" << std::endl;
@@ -177,9 +177,10 @@ int main(int argc, char** argv) {
auto &output = settings.output();
// 0:8
// 0:4: Signature
settings.output().write("\x7F" "3DG", 4);
wavefront::write_number<std::uint32_t>(output, 0);
// 4:8: Version
wavefront::write_number<std::uint32_t>(output, 1);
std::uint32_t flags = 0;
@@ -198,37 +199,27 @@ int main(int argc, char** argv) {
}
}, coordinate_data.texture_coordinates);
std::visit([&](const auto &vertex_data) {
if constexpr (std::ranges::range<std::decay_t<decltype(vertex_data)>>) {
using mapped_type = typename std::decay_t<decltype(vertex_data)>::value_type;
if constexpr (std::ranges::range<mapped_type>) {
flags |= std::tuple_size_v<mapped_type> << 4;
} else {
flags |= 1 << 4;
}
}
}, index_storage.vertices);
// 8:12
// 8:12: flags<0: float_size, 1: has_normals, 2&3: texcoord_ndims (0 = no),
wavefront::write_number<std::uint32_t>(output, flags);
// 12:16
// 12:16: number_list item count
wavefront::write_number<std::uint32_t>(output, number_list.size());
// 16:28
wavefront::write_number<std::uint32_t>(output, std::get<1>(coordinate_storage).size());
// 16:20: 2D storage item count
wavefront::write_number<std::uint32_t>(output, std::get<2>(coordinate_storage).size());
// 20:24: 3D storage item count
wavefront::write_number<std::uint32_t>(output, std::get<3>(coordinate_storage).size());
// 28:36
std::visit([&](const auto &vertex_data){
wavefront::write_number<std::uint32_t>(output, vertex_data.size());
// 24:28: Vertex storage item count
std::visit([&](const auto &vertex_data) {
using value_t = std::decay_t<decltype(vertex_data)>::value_type;
if constexpr (std::tuple_size_v<value_t> > 1) {
wavefront::write_number<std::uint32_t>(output, vertex_data.size());
} else {
wavefront::write_number<std::uint32_t>(output, 0);
}
}, index_storage.vertices);
wavefront::write_number<std::uint32_t>(output, index_storage.triangles.size());
// 36:48
wavefront::write_number<std::uint32_t>(output, 0);
wavefront::write_number<std::uint64_t>(output, 0);
output.write(reinterpret_cast<const char *>(number_list.data()), number_list.size() * sizeof(decltype(number_list)::value_type));
@@ -239,16 +230,6 @@ int main(int argc, char** argv) {
required_padding = (16 - (number_list.size() * sizeof(decltype(number_list)::value_type) % 16)) % 16;
output.write(padding.data(), required_padding);
if (std::get<1>(coordinate_storage).size() > 0) {
const auto &value_list = std::get<1>(coordinate_storage);
using value_t = std::decay_t<decltype(value_list)>::value_type;
output.write(reinterpret_cast<const char *>(value_list.data()), value_list.size() * sizeof(value_t));
required_padding = (16 - (value_list.size() * sizeof(value_t) % 16)) % 16;
if (required_padding > 0) {
output.write(padding.data(), required_padding);
}
}
if (std::get<2>(coordinate_storage).size() > 0) {
const auto &value_list = std::get<2>(coordinate_storage);
using value_t = std::decay_t<decltype(value_list)>::value_type;
@@ -271,10 +252,12 @@ int main(int argc, char** argv) {
std::visit([&](const auto &vertex_data) {
using value_t = std::decay_t<decltype(vertex_data)>::value_type;
output.write(reinterpret_cast<const char *>(vertex_data.data()), vertex_data.size() * sizeof(value_t));
required_padding = (16 - (vertex_data.size() * sizeof(value_t) % 16)) % 16;
if (required_padding > 0) {
output.write(padding.data(), required_padding);
if constexpr (std::tuple_size_v<value_t> > 1) {
output.write(reinterpret_cast<const char *>(vertex_data.data()), vertex_data.size() * sizeof(value_t));
required_padding = (16 - (vertex_data.size() * sizeof(value_t) % 16)) % 16;
if (required_padding > 0) {
output.write(padding.data(), required_padding);
}
}
}, index_storage.vertices);

View File

@@ -55,313 +55,15 @@ namespace wavefront {
wavefront_face_data_result_t parse_face_data(const scan_result &scan_data);
template<typename FloatType>
coordinate_data_t<FloatType> parse_coordinate_data(const scan_result &scan_data, const wavefront_face_data_result_t &face_data) {
using namespace std::string_literals;
coordinate_data_t<FloatType> result;
const auto &position_data = scan_data.category_map.at("v"s);
for (const auto &position_index : face_data.index_position_set) {
std::array<FloatType, 3> position_coordinates;
const auto &position_line_index = position_data.at(position_index);
const auto &position_line = scan_data.line_data.at(position_line_index);
decltype(position_line.find_first_of(' ')) number_begin_pos = 2, number_end_pos;
for (std::size_t position_coordinate_index = 0; position_coordinate_index < 3; position_coordinate_index++) {
if (number_begin_pos >= position_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Line \"{}\" must contain at least {} numbers", "v", 3)
));
}
number_end_pos = std::min(position_line.find_first_of(' ', number_begin_pos), position_line.size());
auto [number_end, conversion_error] = std::from_chars(
position_line.data() + number_begin_pos,
position_line.data() + number_end_pos,
position_coordinates[position_coordinate_index],
std::chars_format::fixed
);
if (conversion_error != std::errc() || position_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Unable to parse \"{}\" floating point value", "v")
));
}
number_begin_pos = std::min(position_line.find_first_not_of(' ', number_end_pos), position_line.size());
}
if (number_begin_pos < position_line.size()) {
if (position_line.find_first_not_of("0123456789.-", number_begin_pos)) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Additional data in \"{}\" line: expected {} only", "v", "<x> <y> <z> [weight]")
));
}
}
result.position_coordinates[position_line_index] = position_coordinates;
}
if (face_data.index_normal_set.size() > 0 && scan_data.category_map.contains("vn"s)) {
const auto &normal_data = scan_data.category_map.at("vn"s);
for (const auto &normal_index : face_data.index_normal_set) {
std::array<FloatType, 3> normal_coordinates;
const auto &normal_line_index = normal_data.at(normal_index);
const auto &normal_line = scan_data.line_data.at(normal_line_index);
decltype(normal_line.find_first_of(' ')) number_begin_pos = 3, number_end_pos;
for (std::size_t normal_coordinate_index = 0; normal_coordinate_index < 3; normal_coordinate_index++) {
if (number_begin_pos >= normal_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Line \"{}\" must contain exactly {} numbers", "vn", 3)
));
}
number_end_pos = std::min(normal_line.find_first_of(' ', number_begin_pos), normal_line.size());
auto [number_end, conversion_error] = std::from_chars(
normal_line.data() + number_begin_pos,
normal_line.data() + number_end_pos,
normal_coordinates[normal_coordinate_index],
std::chars_format::fixed
);
if (conversion_error != std::errc() || normal_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Unable to parse \"{}\" floating point value", "vn")
));
}
number_begin_pos = std::min(normal_line.find_first_not_of(' ', number_end_pos), normal_line.size());
}
if (number_end_pos != normal_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Additional data in \"{}\" line: expected {} only", "vn", "<x> <y> <z>")
));
}
result.normal_coordinates[normal_line_index] = normal_coordinates;
}
}
if (face_data.index_texcoord_set.size() > 0 && scan_data.category_map.contains("vt"s)) {
using insert_texcoord_data_t = std::function<void()>;
const auto &texcoord_data = scan_data.category_map.at("vt"s);
for (const auto &texcoord_index : face_data.index_texcoord_set) {
std::array<FloatType, 3> texcoord_coordinates;
const auto &texcoord_line_index = texcoord_data.at(texcoord_index);
const auto &texcoord_line = scan_data.line_data.at(texcoord_line_index);
decltype(texcoord_line.find_first_of(' ')) number_begin_pos = 3, number_end_pos;
std::size_t texcoord_coordinate_index = 0;
while (number_begin_pos < texcoord_line.size()) {
if (texcoord_coordinate_index >= 3) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\" must contain maximum {} numbers", "vt", 3)
));
}
number_end_pos = std::min(texcoord_line.find_first_of(' ', number_begin_pos), texcoord_line.size());
auto [number_end, conversion_error] = std::from_chars(
texcoord_line.data() + number_begin_pos,
texcoord_line.data() + number_end_pos,
texcoord_coordinates[texcoord_coordinate_index++],
std::chars_format::fixed
);
if (conversion_error != std::errc() || texcoord_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" floating point value", "vt")
));
}
number_begin_pos = std::min(texcoord_line.find_first_not_of(' ', number_end_pos), texcoord_line.size());
}
if (texcoord_coordinate_index == 0) [[unlikely]] {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" line: expected minimum one number", "vt")
));
}
if (result.texture_coordinates.valueless_by_exception() || result.texture_coordinates.index() == 0) [[unlikely]] {
if (texcoord_coordinate_index == 1) {
result.texture_coordinates = std::map<file_line_t, FloatType>{};
} else if (texcoord_coordinate_index == 2) {
result.texture_coordinates = std::map<file_line_t, std::array<FloatType, 2>>{};
} else if (texcoord_coordinate_index == 3) {
result.texture_coordinates = std::map<file_line_t, std::array<FloatType, 3>>{};
} else {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\" must contain maximum {} numbers", "vt", 3)
));
}
}
if (texcoord_coordinate_index == 0) [[unlikely]] {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" line: expected minimum one number", "vt")
));
}
try {
if (texcoord_coordinate_index == 1) {
std::get<1>(result.texture_coordinates)[texcoord_line_index] = texcoord_coordinates[0];
} else if (texcoord_coordinate_index == 2) {
std::get<2>(result.texture_coordinates)[texcoord_line_index] = std::array<FloatType, 2>{texcoord_coordinates[0], texcoord_coordinates[1]};
} else if (texcoord_coordinate_index == 3) {
std::get<3>(result.texture_coordinates)[texcoord_line_index] = texcoord_coordinates;
}
} catch (std::bad_variant_access&) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\": {}D texture coordinates expected, got {}D texture coordinates", "vt", result.texture_coordinates.index(), texcoord_coordinate_index)
));
}
}
}
return result;
}
coordinate_data_t<FloatType> parse_coordinate_data(const scan_result &scan_data, const wavefront_face_data_result_t &face_data);
template<typename FloatType>
std::vector<FloatType> create_number_list(const coordinate_data_t<FloatType> &coordinate_data) {
numset_t<FloatType> number_set;
for (const auto& [line_number, coordinates] : coordinate_data.position_coordinates) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
}
for (const auto& [line_number, coordinates] : coordinate_data.normal_coordinates) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
}
std::visit([&](const auto &texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(texture_coordinate_map)>>) {
for (const auto& [line_number, coordinates] : texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(coordinates)>>) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
} else {
number_set.emplace(coordinates);
}
}
}
}, coordinate_data.texture_coordinates);
number_set.emplace(std::numeric_limits<FloatType>::quiet_NaN());
std::vector<FloatType> number_list;
number_list.reserve(number_set.size());
std::copy(number_set.begin(), number_set.end(), std::back_inserter(number_list));
return number_list;
}
std::vector<FloatType> create_number_list(const coordinate_data_t<FloatType> &coordinate_data);
template<typename FloatType, typename IndexType>
coordinate_data_t<IndexType> create_coordinate_index(const coordinate_data_t<FloatType> &coordinate_data, const std::vector<FloatType> &float_list) {
coordinate_data_t<IndexType> coordinate_index_data;
static const auto float_compare = float_is_equal<FloatType>{};
static const auto float_less = less_with_nan_first_and_nearly_equal<FloatType>{};
for (const auto& [line_number, coordinates] : coordinate_data.position_coordinates) {
std::array<IndexType, coordinates.size()> position_coordinate_indices;
for (decltype(coordinates.size()) dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
position_coordinate_indices[dim] = index;
}
coordinate_index_data.position_coordinates[line_number] = position_coordinate_indices;
}
for (const auto& [line_number, coordinates] : coordinate_data.normal_coordinates) {
std::array<IndexType, coordinates.size()> normal_coordinate_indices;
for (decltype(coordinates.size()) dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
normal_coordinate_indices[dim] = index;
}
coordinate_index_data.normal_coordinates[line_number] = normal_coordinate_indices;
}
if (!coordinate_data.texture_coordinates.valueless_by_exception() && coordinate_data.texture_coordinates.index() > 0) {
std::visit([&](const auto &texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(texture_coordinate_map)>>) {
if constexpr (std::ranges::range<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>) {
coordinate_index_data.texture_coordinates = std::map<
typename std::decay_t<decltype(texture_coordinate_map)>::key_type,
std::array<
IndexType,
std::tuple_size_v<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>
>
>{};
} else {
coordinate_index_data.texture_coordinates = std::map<
typename std::decay_t<decltype(texture_coordinate_map)>::key_type,
IndexType
>{};
}
for (const auto& [line_number, coordinates] : texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(coordinates)>>) {
std::array<IndexType, coordinates.size()> texcoord_coordinate_indices;
for (std::size_t dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
texcoord_coordinate_indices[dim] = index;
}
std::visit([&](auto &target_data) {
if constexpr (std::is_same_v<
std::map<
file_line_t,
std::array<
IndexType,
std::tuple_size_v<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>
>
>,
typename std::decay_t<decltype(target_data)>
>) {
target_data[line_number] = texcoord_coordinate_indices;
}
}, coordinate_index_data.texture_coordinates);
} else {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates, float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates));
auto coordinate = std::distance(float_list.begin(), iterator);
assert(coordinate != 0);
std::visit([&](auto &target_data) {
if constexpr (std::is_same_v<
std::map<file_line_t, IndexType>,
typename std::decay_t<decltype(target_data)>
>) {
target_data[line_number] = coordinate;
}
}, coordinate_index_data.texture_coordinates);
}
}
}
int j = 0;
}, coordinate_data.texture_coordinates);
}
return coordinate_index_data;
}
coordinate_data_t<IndexType> create_coordinate_index(const coordinate_data_t<FloatType> &coordinate_data, const std::vector<FloatType> &float_list);
}
#include "parse.tpp"
#endif // __WAVEFRONT_PARSE_HPP__

319
src/parse.tpp Normal file
View File

@@ -0,0 +1,319 @@
#ifndef __WAVEFRONT_PARSE_TPP__
#define __WAVEFRONT_PARSE_TPP__
#include <algorithm>
#include <limits>
#include <ranges>
namespace wavefront {
template<typename FloatType>
coordinate_data_t<FloatType> parse_coordinate_data(const scan_result &scan_data, const wavefront_face_data_result_t &face_data) {
using namespace std::string_literals;
coordinate_data_t<FloatType> result;
const auto &position_data = scan_data.category_map.at("v"s);
for (const auto &position_index : face_data.index_position_set) {
std::array<FloatType, 3> position_coordinates;
const auto &position_line_index = position_data.at(position_index);
const auto &position_line = scan_data.line_data.at(position_line_index);
decltype(position_line.find_first_of(' ')) number_begin_pos = 2, number_end_pos;
for (std::size_t position_coordinate_index = 0; position_coordinate_index < 3; position_coordinate_index++) {
if (number_begin_pos >= position_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Line \"{}\" must contain at least {} numbers", "v", 3)
));
}
number_end_pos = std::min(position_line.find_first_of(' ', number_begin_pos), position_line.size());
auto [number_end, conversion_error] = std::from_chars(
position_line.data() + number_begin_pos,
position_line.data() + number_end_pos,
position_coordinates[position_coordinate_index],
std::chars_format::fixed
);
if (conversion_error != std::errc() || position_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Unable to parse \"{}\" floating point value", "v")
));
}
number_begin_pos = std::min(position_line.find_first_not_of(' ', number_end_pos), position_line.size());
}
if (number_begin_pos < position_line.size()) {
if (position_line.find_first_not_of("0123456789.-", number_begin_pos)) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Additional data in \"{}\" line: expected {} only", "v", "<x> <y> <z> [weight]")
));
}
}
result.position_coordinates[position_line_index] = position_coordinates;
}
if (face_data.index_normal_set.size() > 0 && scan_data.category_map.contains("vn"s)) {
const auto &normal_data = scan_data.category_map.at("vn"s);
for (const auto &normal_index : face_data.index_normal_set) {
std::array<FloatType, 3> normal_coordinates;
const auto &normal_line_index = normal_data.at(normal_index);
const auto &normal_line = scan_data.line_data.at(normal_line_index);
decltype(normal_line.find_first_of(' ')) number_begin_pos = 3, number_end_pos;
for (std::size_t normal_coordinate_index = 0; normal_coordinate_index < 3; normal_coordinate_index++) {
if (number_begin_pos >= normal_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Line \"{}\" must contain exactly {} numbers", "vn", 3)
));
}
number_end_pos = std::min(normal_line.find_first_of(' ', number_begin_pos), normal_line.size());
auto [number_end, conversion_error] = std::from_chars(
normal_line.data() + number_begin_pos,
normal_line.data() + number_end_pos,
normal_coordinates[normal_coordinate_index],
std::chars_format::fixed
);
if (conversion_error != std::errc() || normal_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Unable to parse \"{}\" floating point value", "vn")
));
}
number_begin_pos = std::min(normal_line.find_first_not_of(' ', number_end_pos), normal_line.size());
}
if (number_end_pos != normal_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Additional data in \"{}\" line: expected {} only", "vn", "<x> <y> <z>")
));
}
result.normal_coordinates[normal_line_index] = normal_coordinates;
}
}
if (face_data.index_texcoord_set.size() > 0 && scan_data.category_map.contains("vt"s)) {
using insert_texcoord_data_t = std::function<void()>;
const auto &texcoord_data = scan_data.category_map.at("vt"s);
for (const auto &texcoord_index : face_data.index_texcoord_set) {
std::array<FloatType, 3> texcoord_coordinates;
const auto &texcoord_line_index = texcoord_data.at(texcoord_index);
const auto &texcoord_line = scan_data.line_data.at(texcoord_line_index);
decltype(texcoord_line.find_first_of(' ')) number_begin_pos = 3, number_end_pos;
std::size_t texcoord_coordinate_index = 0;
while (number_begin_pos < texcoord_line.size()) {
if (texcoord_coordinate_index >= 3) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\" must contain maximum {} numbers", "vt", 3)
));
}
number_end_pos = std::min(texcoord_line.find_first_of(' ', number_begin_pos), texcoord_line.size());
auto [number_end, conversion_error] = std::from_chars(
texcoord_line.data() + number_begin_pos,
texcoord_line.data() + number_end_pos,
texcoord_coordinates[texcoord_coordinate_index++],
std::chars_format::fixed
);
if (conversion_error != std::errc() || texcoord_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" floating point value", "vt")
));
}
number_begin_pos = std::min(texcoord_line.find_first_not_of(' ', number_end_pos), texcoord_line.size());
}
if (texcoord_coordinate_index == 0) [[unlikely]] {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" line: expected minimum one number", "vt")
));
}
if (result.texture_coordinates.valueless_by_exception() || result.texture_coordinates.index() == 0) [[unlikely]] {
if (texcoord_coordinate_index == 1) {
result.texture_coordinates = std::map<file_line_t, FloatType>{};
} else if (texcoord_coordinate_index == 2) {
result.texture_coordinates = std::map<file_line_t, std::array<FloatType, 2>>{};
} else if (texcoord_coordinate_index == 3) {
result.texture_coordinates = std::map<file_line_t, std::array<FloatType, 3>>{};
} else {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\" must contain maximum {} numbers", "vt", 3)
));
}
}
if (texcoord_coordinate_index == 0) [[unlikely]] {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" line: expected minimum one number", "vt")
));
}
try {
if (texcoord_coordinate_index == 1) {
std::get<1>(result.texture_coordinates)[texcoord_line_index] = texcoord_coordinates[0];
} else if (texcoord_coordinate_index == 2) {
std::get<2>(result.texture_coordinates)[texcoord_line_index] = std::array<FloatType, 2>{texcoord_coordinates[0], texcoord_coordinates[1]};
} else if (texcoord_coordinate_index == 3) {
std::get<3>(result.texture_coordinates)[texcoord_line_index] = texcoord_coordinates;
}
} catch (std::bad_variant_access&) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\": {}D texture coordinates expected, got {}D texture coordinates", "vt", result.texture_coordinates.index(), texcoord_coordinate_index)
));
}
}
}
return result;
}
template<typename FloatType>
std::vector<FloatType> create_number_list(const coordinate_data_t<FloatType> &coordinate_data) {
numset_t<FloatType> number_set;
for (const auto& [line_number, coordinates] : coordinate_data.position_coordinates) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
}
for (const auto& [line_number, coordinates] : coordinate_data.normal_coordinates) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
}
std::visit([&](const auto &texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(texture_coordinate_map)>>) {
for (const auto& [line_number, coordinates] : texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(coordinates)>>) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
} else {
number_set.emplace(coordinates);
}
}
}
}, coordinate_data.texture_coordinates);
number_set.emplace(std::numeric_limits<FloatType>::quiet_NaN());
std::vector<FloatType> number_list;
number_list.reserve(number_set.size());
std::copy(number_set.begin(), number_set.end(), std::back_inserter(number_list));
return number_list;
}
template<typename FloatType, typename IndexType>
coordinate_data_t<IndexType> create_coordinate_index(const coordinate_data_t<FloatType> &coordinate_data, const std::vector<FloatType> &float_list) {
coordinate_data_t<IndexType> coordinate_index_data;
static const auto float_compare = float_is_equal<FloatType>{};
static const auto float_less = less_with_nan_first_and_nearly_equal<FloatType>{};
for (const auto& [line_number, coordinates] : coordinate_data.position_coordinates) {
std::array<IndexType, coordinates.size()> position_coordinate_indices;
for (decltype(coordinates.size()) dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
position_coordinate_indices[dim] = index;
}
coordinate_index_data.position_coordinates[line_number] = position_coordinate_indices;
}
for (const auto& [line_number, coordinates] : coordinate_data.normal_coordinates) {
std::array<IndexType, coordinates.size()> normal_coordinate_indices;
for (decltype(coordinates.size()) dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
normal_coordinate_indices[dim] = index;
}
coordinate_index_data.normal_coordinates[line_number] = normal_coordinate_indices;
}
if (!coordinate_data.texture_coordinates.valueless_by_exception() && coordinate_data.texture_coordinates.index() > 0) {
std::visit([&](const auto &texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(texture_coordinate_map)>>) {
if constexpr (std::ranges::range<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>) {
coordinate_index_data.texture_coordinates = std::map<
typename std::decay_t<decltype(texture_coordinate_map)>::key_type,
std::array<
IndexType,
std::tuple_size_v<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>
>
>{};
} else {
coordinate_index_data.texture_coordinates = std::map<
typename std::decay_t<decltype(texture_coordinate_map)>::key_type,
IndexType
>{};
}
for (const auto& [line_number, coordinates] : texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(coordinates)>>) {
std::array<IndexType, coordinates.size()> texcoord_coordinate_indices;
for (std::size_t dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
texcoord_coordinate_indices[dim] = index;
}
std::visit([&](auto &target_data) {
if constexpr (std::is_same_v<
std::map<
file_line_t,
std::array<
IndexType,
std::tuple_size_v<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>
>
>,
typename std::decay_t<decltype(target_data)>
>) {
target_data[line_number] = texcoord_coordinate_indices;
}
}, coordinate_index_data.texture_coordinates);
} else {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates, float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates));
auto coordinate = std::distance(float_list.begin(), iterator);
assert(coordinate != 0);
std::visit([&](auto &target_data) {
if constexpr (std::is_same_v<
std::map<file_line_t, IndexType>,
typename std::decay_t<decltype(target_data)>
>) {
target_data[line_number] = coordinate;
}
}, coordinate_index_data.texture_coordinates);
}
}
}
int j = 0;
}, coordinate_data.texture_coordinates);
}
return coordinate_index_data;
}
}
#endif // __WAVEFRONT_PARSE_TPP__

View File

@@ -11,8 +11,6 @@
#include "scan.hpp"
namespace wavefront {
scan_error::scan_error(const std::string &message) : std::runtime_error(message) {}
scan_result::scan_result() :
total_lines(0),
line_data(),

View File

@@ -10,11 +10,6 @@
#include "settings.hpp"
namespace wavefront {
class scan_error : public std::runtime_error {
public:
explicit scan_error(const std::string &message);
};
struct scan_result {
std::size_t total_lines;
std::map<std::size_t, std::string> line_data;

View File

@@ -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);

6
test.sh Executable file
View 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

View File

@@ -0,0 +1,26 @@
# 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
o
f 8/1/1 5/2/2 3/3/2
g
f 2/1/1 6/2/2 4/3/2
h this line is skipped

129
tests/unit/parse_test.cpp Normal file
View File

@@ -0,0 +1,129 @@
#include <variant>
#include <gtest/gtest.h>
#include "parse.hpp"
#include "scan.hpp"
namespace {
TEST(ParseFaceData, BuildsTrianglesAndIndexSets) {
wavefront::scan_result scan_result;
scan_result.total_lines = 26;
scan_result.line_data = {
{17u, "f 1/1/1 2/2/2 3/3/2"},
{19u, "f 4/3/2 5/2/1 6/1/2"},
{21u, "f 7/2/2 8/3/2 9/1/1"},
{23u, "f 8/1/1 5/2/2 3/3/2"},
{25u, "f 2/1/1 6/2/2 4/3/2"}
};
scan_result.category_map = {
{"f", {17u, 19u, 21u, 23u, 25u}}
};
const auto face_data = wavefront::parse_face_data(scan_result);
EXPECT_EQ(face_data.triangle_list.size(), 5u);
EXPECT_EQ(face_data.index_position_set.size(), 9u);
EXPECT_TRUE(face_data.index_position_set.contains(1));
EXPECT_TRUE(face_data.index_position_set.contains(9));
EXPECT_EQ(face_data.index_normal_set.size(), 2u);
EXPECT_TRUE(face_data.index_normal_set.contains(1));
EXPECT_TRUE(face_data.index_normal_set.contains(2));
EXPECT_EQ(face_data.index_texcoord_set.size(), 3u);
EXPECT_TRUE(face_data.index_texcoord_set.contains(1));
EXPECT_TRUE(face_data.index_texcoord_set.contains(3));
const auto &first_triangle = face_data.triangle_list.at(0);
EXPECT_EQ(first_triangle[0].face_line_number, 17u);
EXPECT_EQ(first_triangle[0].position_index, 1);
EXPECT_EQ(first_triangle[0].texcoord_index, 1);
EXPECT_EQ(first_triangle[0].normal_index, 1);
}
TEST(ParseCoordinateData, MapsCoordinatesWithoutParsingFaces) {
wavefront::scan_result scan_result;
scan_result.total_lines = 26;
scan_result.line_data = {
{3u, "v 0.1 0.2 0.3"},
{4u, "v 0.2 0.3 0.4"},
{5u, "v 0.3 0.4 0.5"},
{6u, "v 1.1 1.2 1.3"},
{7u, "v 1.2 1.3 1.4"},
{8u, "v 1.3 1.4 1.5"},
{9u, "v 2.1 2.2 2.3"},
{10u, "v 2.2 2.3 2.4"},
{11u, "v 2.3 2.4 2.5"},
{12u, "vn 0.15 0.25 0.35"},
{13u, "vn 0.25 0.35 0.45"},
{14u, "vn 0.35 0.45 0.55"},
{15u, "vt 0.9 0.8"},
{16u, "vt 0.8 0.7"},
{17u, "f 1/1/1 2/2/2 3/3/2"},
{19u, "f 4/3/2 5/2/1 6/1/2"},
{21u, "f 7/2/2 8/3/2 9/1/1"},
{23u, "f 8/1/1 5/2/2 3/3/2"},
{25u, "f 2/1/1 6/2/2 4/3/2"}
};
scan_result.category_map = {
{"v", {0u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u}},
{"vn", {0u, 12u, 13u, 14u}},
{"vt", {0u, 15u, 16u}},
{"f", {17u, 19u, 21u, 23u, 25u}}
};
wavefront::wavefront_face_data_result_t face_data;
face_data.index_position_set = {1, 2, 3, 4, 5, 6, 7, 8, 9};
face_data.index_normal_set = {1, 2, 3};
face_data.index_texcoord_set = {1, 2};
face_data.triangle_list = {
{{{17u, 1, 1, 1}, {17u, 2, 2, 2}, {17u, 3, 3, 2}}},
{{{19u, 4, 3, 2}, {19u, 5, 2, 1}, {19u, 6, 1, 2}}},
{{{21u, 7, 2, 2}, {21u, 8, 3, 2}, {21u, 9, 1, 1}}},
{{{23u, 8, 1, 1}, {23u, 5, 2, 2}, {23u, 3, 3, 2}}},
{{{25u, 2, 1, 1}, {25u, 6, 2, 2}, {25u, 4, 3, 2}}}
};
const auto coordinate_data = wavefront::parse_coordinate_data<float>(scan_result, face_data);
ASSERT_EQ(coordinate_data.position_coordinates.size(), 9u);
const auto &pos_line_2 = coordinate_data.position_coordinates.at(3u);
EXPECT_FLOAT_EQ(pos_line_2[0], 0.1f);
EXPECT_FLOAT_EQ(pos_line_2[1], 0.2f);
EXPECT_FLOAT_EQ(pos_line_2[2], 0.3f);
const auto &pos_line_5 = coordinate_data.position_coordinates.at(6u);
EXPECT_FLOAT_EQ(pos_line_5[0], 1.1f);
EXPECT_FLOAT_EQ(pos_line_5[1], 1.2f);
EXPECT_FLOAT_EQ(pos_line_5[2], 1.3f);
const auto &pos_line_10 = coordinate_data.position_coordinates.at(11u);
EXPECT_FLOAT_EQ(pos_line_10[0], 2.3f);
EXPECT_FLOAT_EQ(pos_line_10[1], 2.4f);
EXPECT_FLOAT_EQ(pos_line_10[2], 2.5f);
ASSERT_EQ(coordinate_data.normal_coordinates.size(), 3u);
const auto &normal_line_11 = coordinate_data.normal_coordinates.at(12u);
EXPECT_FLOAT_EQ(normal_line_11[0], 0.15f);
EXPECT_FLOAT_EQ(normal_line_11[1], 0.25f);
EXPECT_FLOAT_EQ(normal_line_11[2], 0.35f);
const auto &normal_line_13 = coordinate_data.normal_coordinates.at(14u);
EXPECT_FLOAT_EQ(normal_line_13[0], 0.35f);
EXPECT_FLOAT_EQ(normal_line_13[1], 0.45f);
EXPECT_FLOAT_EQ(normal_line_13[2], 0.55f);
ASSERT_EQ(coordinate_data.texture_coordinates.index(), 2u);
const auto &texcoords = std::get<2>(coordinate_data.texture_coordinates);
ASSERT_EQ(texcoords.size(), 2u);
const auto &tex_line_14 = texcoords.at(15u);
EXPECT_FLOAT_EQ(tex_line_14[0], 0.9f);
EXPECT_FLOAT_EQ(tex_line_14[1], 0.8f);
const auto &tex_line_15 = texcoords.at(16u);
EXPECT_FLOAT_EQ(tex_line_15[0], 0.8f);
EXPECT_FLOAT_EQ(tex_line_15[1], 0.7f);
}
}

64
tests/unit/scan_test.cpp Normal file
View File

@@ -0,0 +1,64 @@
#include <filesystem>
#include <fstream>
#include <gtest/gtest.h>
#include "scan.hpp"
namespace {
std::filesystem::path test_data_path(const std::string &filename) {
const auto here = std::filesystem::path(__FILE__).parent_path();
return here.parent_path() / "data" / filename;
}
TEST(Scan, ReadsAllSupportedTypes) {
std::ifstream input(test_data_path("test-scan-1.txt"));
ASSERT_TRUE(input.is_open());
const auto result = wavefront::scan(input);
EXPECT_EQ(result.total_lines, 26u);
EXPECT_EQ(result.line_data.size(), 21u);
EXPECT_TRUE(result.category_map.contains("v"));
EXPECT_TRUE(result.category_map.contains("vn"));
EXPECT_TRUE(result.category_map.contains("vt"));
EXPECT_TRUE(result.category_map.contains("f"));
EXPECT_EQ(result.category_map.at("v").size(), 10u);
EXPECT_EQ(result.category_map.at("vn").size(), 4u);
EXPECT_EQ(result.category_map.at("vt").size(), 3u);
EXPECT_EQ(result.category_map.at("f").size(), 5u);
}
TEST(Scan, FiltersByObject) {
std::ifstream input(test_data_path("test-scan-1.txt"));
ASSERT_TRUE(input.is_open());
const auto result = wavefront::scan(input, {"test"});
EXPECT_EQ(result.category_map.at("f").size(), 4u);
EXPECT_EQ(result.category_map.at("f")[0], 19u);
EXPECT_EQ(result.category_map.at("f")[1], 21u);
}
TEST(Scan, FiltersByGroup) {
std::ifstream input(test_data_path("test-scan-1.txt"));
ASSERT_TRUE(input.is_open());
const auto result = wavefront::scan(input, {}, {"test"});
EXPECT_EQ(result.category_map.at("f").size(), 3u);
EXPECT_EQ(result.category_map.at("f")[0], 21u);
}
TEST(Scan, FiltersByObjectAndGroup) {
std::ifstream input(test_data_path("test-scan-1.txt"));
ASSERT_TRUE(input.is_open());
const auto result = wavefront::scan(input, {"test"}, {"test"});
EXPECT_EQ(result.category_map.at("f").size(), 3u);
EXPECT_EQ(result.category_map.at("f")[0], 21u);
}
}

View File

@@ -0,0 +1,186 @@
#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/unit/trim_test.cpp Normal file
View 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 &param = 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{"", ""}
)
);
}