diff --git a/.clang-format b/.clang-format index 906e38a..19a9d20 100644 --- a/.clang-format +++ b/.clang-format @@ -40,7 +40,7 @@ NamespaceIndentation: All PenaltyBreakComment: 20 PenaltyExcessCharacter: 5 PointerAlignment: Middle -SortUsingDeclarations: false +SortUsingDeclarations: true SpaceAfterTemplateKeyword: false SpaceBeforeCpp11BracedList: true SpacesBeforeTrailingComments: 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96dd959..abaed99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,8 +35,7 @@ jobs: run: ctest -V # - name: Test with Coverage - # working-directory: build - # run: cmake --build ${BUILD_DIR} --target os_test_coverage + # run: cmake --build build --target Wavefront_coverage lint: runs-on: ubuntu-latest diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..81e048e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "configurations": [ + // https://stackoverflow.com/a/76447033 + { + "name": "(ctest) Launch", + "type": "cppdbg", + "cwd": "${cmake.testWorkingDirectory}", + "request": "launch", + "program": "${cmake.testProgram}", + "args": [ + "${cmake.testArgs}" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..26ceda5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "cmake.ctest.testSuiteDelimiter": "\\.", + "cmake.ctest.testExplorerIntegrationEnabled": true +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 34bff4c..3763ca7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,10 @@ project(Wavefront option(WAVEFRONT_BUILD_DOCS "Builds the Wavefront documentation" ON) option(WAVEFRONT_BUILD_TESTS "Builds the Wavefront tests" ON) +option(WAVEFRONT_TEST_COVERAGE "Generate gcov target for calculating code coverage" ON) +option(WAVEFRONT_TEST_COVERAGE_CAN_FAIL "Tests can fail for coverage" ON) +option(WAVEFRONT_TEST_COVERAGE_DARK "Use dark theme for html output" ON) + # Only if this is the top level project (not included with add_subdirectory) if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) # Use -std=c++xx instead of -std=g++xx @@ -17,6 +21,19 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) set_property(GLOBAL PROPERTY USE_FOLDERS ON) if(WAVEFRONT_BUILD_TESTS) + set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) + if(WAVEFRONT_TEST_COVERAGE AND CMAKE_COMPILER_IS_GNUCXX) + include(CodeCoverage) + append_coverage_compiler_flags() + if(WAVEFRONT_TEST_COVERAGE_CAN_FAIL) + set(GCOVR_ADDITIONAL_ARGS ${GCOVR_ADDITIONAL_ARGS} --html-high-threshold 100 --html-medium-threshold 90 --fail-under-line 100 --fail-under-branch 100) + endif() + if(WAVEFRONT_TEST_COVERAGE_DARK) + set(GCOVR_ADDITIONAL_ARGS ${GCOVR_ADDITIONAL_ARGS} --html-theme github.dark-green) + else() + set(GCOVR_ADDITIONAL_ARGS ${GCOVR_ADDITIONAL_ARGS} --html-theme github.green) + endif() + endif() # Testing only available for top level projects. It calls enable_testing # which must be in the main CMakeLists. include(CTest) @@ -71,3 +88,14 @@ install(FILES if((CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME OR MODERN_CMAKE_BUILD_TESTING) AND WAVEFRONT_BUILD_TESTS) add_subdirectory(tests) endif() + +if(WAVEFRONT_TEST_COVERAGE AND CMAKE_COMPILER_IS_GNUCXX) + setup_target_for_coverage_gcovr_html( + NAME ${PROJECT_NAME}_coverage + EXECUTABLE ctest + EXECUTABLE_ARGS --output-on-failure + DEPENDENCIES ${PROJECT_NAME} + BASE_DIRECTORY . + EXCLUDE ${PROJECT_SOURCE_DIR}/tests ${PROJECT_BINARY_DIR}/* + ) +endif() diff --git a/Makefile b/Makefile index 976ee0d..775f11a 100644 --- a/Makefile +++ b/Makefile @@ -24,4 +24,12 @@ test: build test-ci: build cd build && GTEST_COLOR=1 ctest -V -.PHONY: setup build install lint format test test-ci +test-cov: test coverage + +coverage: + cmake --build build --target Wavefront_coverage + +show_coverage: + gio open build/Wavefront_coverage/index.html + +.PHONY: setup build install lint format test test-ci test-cov coverage diff --git a/cmake/CodeCoverage.cmake b/cmake/CodeCoverage.cmake new file mode 100644 index 0000000..530b5df --- /dev/null +++ b/cmake/CodeCoverage.cmake @@ -0,0 +1,837 @@ +# Copyright (c) 2012 - 2017, Lars Bilke +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# CHANGES: +# +# 2012-01-31, Lars Bilke +# - Enable Code Coverage +# +# 2013-09-17, Joakim Söderberg +# - Added support for Clang. +# - Some additional usage instructions. +# +# 2016-02-03, Lars Bilke +# - Refactored functions to use named parameters +# +# 2017-06-02, Lars Bilke +# - Merged with modified version from github.com/ufz/ogs +# +# 2019-05-06, Anatolii Kurotych +# - Remove unnecessary --coverage flag +# +# 2019-12-13, FeRD (Frank Dana) +# - Deprecate COVERAGE_LCOVR_EXCLUDES and COVERAGE_GCOVR_EXCLUDES lists in favor +# of tool-agnostic COVERAGE_EXCLUDES variable, or EXCLUDE setup arguments. +# - CMake 3.4+: All excludes can be specified relative to BASE_DIRECTORY +# - All setup functions: accept BASE_DIRECTORY, EXCLUDE list +# - Set lcov basedir with -b argument +# - Add automatic --demangle-cpp in lcovr, if 'c++filt' is available (can be +# overridden with NO_DEMANGLE option in setup_target_for_coverage_lcovr().) +# - Delete output dir, .info file on 'make clean' +# - Remove Python detection, since version mismatches will break gcovr +# - Minor cleanup (lowercase function names, update examples...) +# +# 2019-12-19, FeRD (Frank Dana) +# - Rename Lcov outputs, make filtered file canonical, fix cleanup for targets +# +# 2020-01-19, Bob Apthorpe +# - Added gfortran support +# +# 2020-02-17, FeRD (Frank Dana) +# - Make all add_custom_target()s VERBATIM to auto-escape wildcard characters +# in EXCLUDEs, and remove manual escaping from gcovr targets +# +# 2021-01-19, Robin Mueller +# - Add CODE_COVERAGE_VERBOSE option which will allow to print out commands which are run +# - Added the option for users to set the GCOVR_ADDITIONAL_ARGS variable to supply additional +# flags to the gcovr command +# +# 2020-05-04, Mihchael Davis +# - Add -fprofile-abs-path to make gcno files contain absolute paths +# - Fix BASE_DIRECTORY not working when defined +# - Change BYPRODUCT from folder to index.html to stop ninja from complaining about double defines +# +# 2021-05-10, Martin Stump +# - Check if the generator is multi-config before warning about non-Debug builds +# +# 2022-02-22, Marko Wehle +# - Change gcovr output from -o for --xml and --html output respectively. +# This will allow for Multiple Output Formats at the same time by making use of GCOVR_ADDITIONAL_ARGS, e.g. GCOVR_ADDITIONAL_ARGS "--txt". +# +# 2022-09-28, Sebastian Mueller +# - fix append_coverage_compiler_flags_to_target to correctly add flags +# - replace "-fprofile-arcs -ftest-coverage" with "--coverage" (equivalent) +# +# USAGE: +# +# 1. Copy this file into your cmake modules path. +# +# 2. Add the following line to your CMakeLists.txt (best inside an if-condition +# using a CMake option() to enable it just optionally): +# include(CodeCoverage) +# +# 3. Append necessary compiler flags for all supported source files: +# append_coverage_compiler_flags() +# Or for specific target: +# append_coverage_compiler_flags_to_target(YOUR_TARGET_NAME) +# +# 3.a (OPTIONAL) Set appropriate optimization flags, e.g. -O0, -O1 or -Og +# +# 4. If you need to exclude additional directories from the report, specify them +# using full paths in the COVERAGE_EXCLUDES variable before calling +# setup_target_for_coverage_*(). +# Example: +# set(COVERAGE_EXCLUDES +# '${PROJECT_SOURCE_DIR}/src/dir1/*' +# '/path/to/my/src/dir2/*') +# Or, use the EXCLUDE argument to setup_target_for_coverage_*(). +# Example: +# setup_target_for_coverage_lcov( +# NAME coverage +# EXECUTABLE testrunner +# EXCLUDE "${PROJECT_SOURCE_DIR}/src/dir1/*" "/path/to/my/src/dir2/*") +# +# 4.a NOTE: With CMake 3.4+, COVERAGE_EXCLUDES or EXCLUDE can also be set +# relative to the BASE_DIRECTORY (default: PROJECT_SOURCE_DIR) +# Example: +# set(COVERAGE_EXCLUDES "dir1/*") +# setup_target_for_coverage_gcovr_html( +# NAME coverage +# EXECUTABLE testrunner +# BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src" +# EXCLUDE "dir2/*") +# +# 5. Use the functions described below to create a custom make target which +# runs your test executable and produces a code coverage report. +# +# 6. Build a Debug build: +# cmake -DCMAKE_BUILD_TYPE=Debug .. +# make +# make my_coverage_target +# + +include(CMakeParseArguments) + +option(CODE_COVERAGE_VERBOSE "Verbose information" FALSE) + +# Check prereqs +find_program( GCOV_PATH gcov ) +find_program( LCOV_PATH NAMES lcov lcov.bat lcov.exe lcov.perl) +find_program( FASTCOV_PATH NAMES fastcov fastcov.py ) +find_program( GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat ) +find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test) +find_program( CPPFILT_PATH NAMES c++filt ) + +if(NOT GCOV_PATH) + message(FATAL_ERROR "gcov not found! Aborting...") +endif() # NOT GCOV_PATH + +# Check supported compiler (Clang, GNU and Flang) +get_property(LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) +foreach(LANG ${LANGUAGES}) + if("${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang") + if("${CMAKE_${LANG}_COMPILER_VERSION}" VERSION_LESS 3) + message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...") + endif() + elseif(NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "GNU" + AND NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(LLVM)?[Ff]lang") + message(FATAL_ERROR "Compiler is not GNU or Flang! Aborting...") + endif() +endforeach() + +set(COVERAGE_COMPILER_FLAGS "-g --coverage" + CACHE INTERNAL "") + +if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") + include(CheckCXXCompilerFlag) + check_cxx_compiler_flag(-fprofile-abs-path HAVE_cxx_fprofile_abs_path) + if(HAVE_cxx_fprofile_abs_path) + set(COVERAGE_CXX_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path") + endif() +endif() +if(CMAKE_C_COMPILER_ID MATCHES "(GNU|Clang)") + include(CheckCCompilerFlag) + check_c_compiler_flag(-fprofile-abs-path HAVE_c_fprofile_abs_path) + if(HAVE_c_fprofile_abs_path) + set(COVERAGE_C_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path") + endif() +endif() + +set(CMAKE_Fortran_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the Fortran compiler during coverage builds." + FORCE ) +set(CMAKE_CXX_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C++ compiler during coverage builds." + FORCE ) +set(CMAKE_C_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C compiler during coverage builds." + FORCE ) +set(CMAKE_EXE_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used for linking binaries during coverage builds." + FORCE ) +set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used by the shared libraries linker during coverage builds." + FORCE ) +mark_as_advanced( + CMAKE_Fortran_FLAGS_COVERAGE + CMAKE_CXX_FLAGS_COVERAGE + CMAKE_C_FLAGS_COVERAGE + CMAKE_EXE_LINKER_FLAGS_COVERAGE + CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) + +get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG)) + message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading") +endif() # NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG) + +if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") + link_libraries(gcov) +endif() + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_lcov( +# NAME testrunner_coverage # New target name +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES testrunner # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# NO_DEMANGLE # Don't demangle C++ symbols +# # even if c++filt is found +# ) +function(setup_target_for_coverage_lcov) + + set(options NO_DEMANGLE SONARQUBE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES LCOV_ARGS GENHTML_ARGS) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT LCOV_PATH) + message(FATAL_ERROR "lcov not found! Aborting...") + endif() # NOT LCOV_PATH + + if(NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() # NOT GENHTML_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(LCOV_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_LCOV_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND LCOV_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES LCOV_EXCLUDES) + + # Conditional arguments + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) + set(GENHTML_EXTRA_ARGS "--demangle-cpp") + endif() + + # Setting up commands which will be run to generate coverage data. + # Cleanup lcov + set(LCOV_CLEAN_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -directory . + -b ${BASEDIR} --zerocounters + ) + # Create baseline to make sure untouched files show up in the report + set(LCOV_BASELINE_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -c -i -d . -b + ${BASEDIR} -o ${Coverage_NAME}.base + ) + # Run tests + set(LCOV_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Capturing lcov counters and generating report + set(LCOV_CAPTURE_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --directory . -b + ${BASEDIR} --capture --output-file ${Coverage_NAME}.capture + ) + # add baseline counters + set(LCOV_BASELINE_COUNT_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -a ${Coverage_NAME}.base + -a ${Coverage_NAME}.capture --output-file ${Coverage_NAME}.total + ) + # filter collected data to final coverage report + set(LCOV_FILTER_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --remove + ${Coverage_NAME}.total ${LCOV_EXCLUDES} --output-file ${Coverage_NAME}.info + ) + # Generate HTML output + set(LCOV_GEN_HTML_CMD + ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} -o + ${Coverage_NAME} ${Coverage_NAME}.info + ) + if(${Coverage_SONARQUBE}) + # Generate SonarQube output + set(GCOVR_XML_CMD + ${GCOVR_PATH} --sonarqube ${Coverage_NAME}_sonarqube.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + set(GCOVR_XML_CMD_COMMAND + COMMAND ${GCOVR_XML_CMD} + ) + set(GCOVR_XML_CMD_BYPRODUCTS ${Coverage_NAME}_sonarqube.xml) + set(GCOVR_XML_CMD_COMMENT COMMENT "SonarQube code coverage info report saved in ${Coverage_NAME}_sonarqube.xml.") + endif() + + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + message(STATUS "Command to clean up lcov: ") + string(REPLACE ";" " " LCOV_CLEAN_CMD_SPACED "${LCOV_CLEAN_CMD}") + message(STATUS "${LCOV_CLEAN_CMD_SPACED}") + + message(STATUS "Command to create baseline: ") + string(REPLACE ";" " " LCOV_BASELINE_CMD_SPACED "${LCOV_BASELINE_CMD}") + message(STATUS "${LCOV_BASELINE_CMD_SPACED}") + + message(STATUS "Command to run the tests: ") + string(REPLACE ";" " " LCOV_EXEC_TESTS_CMD_SPACED "${LCOV_EXEC_TESTS_CMD}") + message(STATUS "${LCOV_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to capture counters and generate report: ") + string(REPLACE ";" " " LCOV_CAPTURE_CMD_SPACED "${LCOV_CAPTURE_CMD}") + message(STATUS "${LCOV_CAPTURE_CMD_SPACED}") + + message(STATUS "Command to add baseline counters: ") + string(REPLACE ";" " " LCOV_BASELINE_COUNT_CMD_SPACED "${LCOV_BASELINE_COUNT_CMD}") + message(STATUS "${LCOV_BASELINE_COUNT_CMD_SPACED}") + + message(STATUS "Command to filter collected data: ") + string(REPLACE ";" " " LCOV_FILTER_CMD_SPACED "${LCOV_FILTER_CMD}") + message(STATUS "${LCOV_FILTER_CMD_SPACED}") + + message(STATUS "Command to generate lcov HTML output: ") + string(REPLACE ";" " " LCOV_GEN_HTML_CMD_SPACED "${LCOV_GEN_HTML_CMD}") + message(STATUS "${LCOV_GEN_HTML_CMD_SPACED}") + + if(${Coverage_SONARQUBE}) + message(STATUS "Command to generate SonarQube XML output: ") + string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}") + message(STATUS "${GCOVR_XML_CMD_SPACED}") + endif() + endif() + + # Setup target + add_custom_target(${Coverage_NAME} + COMMAND ${LCOV_CLEAN_CMD} + COMMAND ${LCOV_BASELINE_CMD} + COMMAND ${LCOV_EXEC_TESTS_CMD} + COMMAND ${LCOV_CAPTURE_CMD} + COMMAND ${LCOV_BASELINE_COUNT_CMD} + COMMAND ${LCOV_FILTER_CMD} + COMMAND ${LCOV_GEN_HTML_CMD} + ${GCOVR_XML_CMD_COMMAND} + + # Set output files as GENERATED (will be removed on 'make clean') + BYPRODUCTS + ${Coverage_NAME}.base + ${Coverage_NAME}.capture + ${Coverage_NAME}.total + ${Coverage_NAME}.info + ${GCOVR_XML_CMD_BYPRODUCTS} + ${Coverage_NAME}/index.html + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." + ) + + # Show where to find the lcov info report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info." + ${GCOVR_XML_CMD_COMMENT} + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) + +endfunction() # setup_target_for_coverage_lcov + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_gcovr_xml( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# ) +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the +# GCVOR command. +function(setup_target_for_coverage_gcovr_xml) + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data + # Run tests + set(GCOVR_XML_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Running gcovr + set(GCOVR_XML_CMD + ${GCOVR_PATH} --xml ${Coverage_NAME}.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + + message(STATUS "Command to run tests: ") + string(REPLACE ";" " " GCOVR_XML_EXEC_TESTS_CMD_SPACED "${GCOVR_XML_EXEC_TESTS_CMD}") + message(STATUS "${GCOVR_XML_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to generate gcovr XML coverage data: ") + string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}") + message(STATUS "${GCOVR_XML_CMD_SPACED}") + endif() + + add_custom_target(${Coverage_NAME} + COMMAND ${GCOVR_XML_EXEC_TESTS_CMD} + COMMAND ${GCOVR_XML_CMD} + + BYPRODUCTS ${Coverage_NAME}.xml + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce Cobertura code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml." + ) +endfunction() # setup_target_for_coverage_gcovr_xml + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_gcovr_html( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# ) +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the +# GCVOR command. +function(setup_target_for_coverage_gcovr_html) + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data + # Run tests + set(GCOVR_HTML_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Create folder + set(GCOVR_HTML_FOLDER_CMD + ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME} + ) + # Running gcovr + set(GCOVR_HTML_CMD + ${GCOVR_PATH} --txt --html ${Coverage_NAME}/index.html --html-details --lcov ${Coverage_NAME}.lcov -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + + message(STATUS "Command to run tests: ") + string(REPLACE ";" " " GCOVR_HTML_EXEC_TESTS_CMD_SPACED "${GCOVR_HTML_EXEC_TESTS_CMD}") + message(STATUS "${GCOVR_HTML_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to create a folder: ") + string(REPLACE ";" " " GCOVR_HTML_FOLDER_CMD_SPACED "${GCOVR_HTML_FOLDER_CMD}") + message(STATUS "${GCOVR_HTML_FOLDER_CMD_SPACED}") + + message(STATUS "Command to generate gcovr HTML coverage data: ") + string(REPLACE ";" " " GCOVR_HTML_CMD_SPACED "${GCOVR_HTML_CMD}") + message(STATUS "${GCOVR_HTML_CMD_SPACED}") + endif() + + add_custom_target(${Coverage_NAME} + COMMAND ${GCOVR_HTML_EXEC_TESTS_CMD} + COMMAND ${GCOVR_HTML_FOLDER_CMD} + COMMAND ${GCOVR_HTML_CMD} + + BYPRODUCTS ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html # report directory + ${Coverage_NAME}.lcov + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce HTML code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND true; + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) + +endfunction() # setup_target_for_coverage_gcovr_html + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_gcovr_txt( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# ) +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the +# GCVOR command. +function(setup_target_for_coverage_gcovr_txt) + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data + # Run tests + set(GCOVR_TXT_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Running gcovr + set(GCOVR_TXT_CMD + ${GCOVR_PATH} --txt -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + + message(STATUS "Command to run tests: ") + string(REPLACE ";" " " GCOVR_TXT_EXEC_TESTS_CMD_SPACED "${GCOVR_TXT_EXEC_TESTS_CMD}") + message(STATUS "${GCOVR_TXT_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to generate gcovr TXT coverage data: ") + string(REPLACE ";" " " GCOVR_TXT_CMD_SPACED "${GCOVR_TXT_CMD}") + message(STATUS "${GCOVR_TXT_CMD_SPACED}") + endif() + + add_custom_target(${Coverage_NAME} + COMMAND ${GCOVR_TXT_EXEC_TESTS_CMD} + COMMAND ${GCOVR_TXT_CMD} + + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce TXT code coverage report." + ) + +endfunction() # setup_target_for_coverage_gcovr_txt + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_fastcov( +# NAME testrunner_coverage # New target name +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES testrunner # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/" "src/dir2/" # Patterns to exclude. +# NO_DEMANGLE # Don't demangle C++ symbols +# # even if c++filt is found +# SKIP_HTML # Don't create html report +# POST_CMD perl -i -pe s!${PROJECT_SOURCE_DIR}/!!g ctest_coverage.json # E.g. for stripping source dir from file paths +# ) +function(setup_target_for_coverage_fastcov) + + set(options NO_DEMANGLE SKIP_HTML) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES FASTCOV_ARGS GENHTML_ARGS POST_CMD) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT FASTCOV_PATH) + message(FATAL_ERROR "fastcov not found! Aborting...") + endif() + + if(NOT Coverage_SKIP_HTML AND NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (Patterns, not paths, for fastcov) + set(FASTCOV_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_FASTCOV_EXCLUDES}) + list(APPEND FASTCOV_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES FASTCOV_EXCLUDES) + + # Conditional arguments + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) + set(GENHTML_EXTRA_ARGS "--demangle-cpp") + endif() + + # Set up commands which will be run to generate coverage data + set(FASTCOV_EXEC_TESTS_CMD ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}) + + set(FASTCOV_CAPTURE_CMD ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} + --search-directory ${BASEDIR} + --process-gcno + --output ${Coverage_NAME}.json + --exclude ${FASTCOV_EXCLUDES} + ) + + set(FASTCOV_CONVERT_CMD ${FASTCOV_PATH} + -C ${Coverage_NAME}.json --lcov --output ${Coverage_NAME}.info + ) + + if(Coverage_SKIP_HTML) + set(FASTCOV_HTML_CMD ";") + else() + set(FASTCOV_HTML_CMD ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} + -o ${Coverage_NAME} ${Coverage_NAME}.info + ) + endif() + + set(FASTCOV_POST_CMD ";") + if(Coverage_POST_CMD) + set(FASTCOV_POST_CMD ${Coverage_POST_CMD}) + endif() + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Code coverage commands for target ${Coverage_NAME} (fastcov):") + + message(" Running tests:") + string(REPLACE ";" " " FASTCOV_EXEC_TESTS_CMD_SPACED "${FASTCOV_EXEC_TESTS_CMD}") + message(" ${FASTCOV_EXEC_TESTS_CMD_SPACED}") + + message(" Capturing fastcov counters and generating report:") + string(REPLACE ";" " " FASTCOV_CAPTURE_CMD_SPACED "${FASTCOV_CAPTURE_CMD}") + message(" ${FASTCOV_CAPTURE_CMD_SPACED}") + + message(" Converting fastcov .json to lcov .info:") + string(REPLACE ";" " " FASTCOV_CONVERT_CMD_SPACED "${FASTCOV_CONVERT_CMD}") + message(" ${FASTCOV_CONVERT_CMD_SPACED}") + + if(NOT Coverage_SKIP_HTML) + message(" Generating HTML report: ") + string(REPLACE ";" " " FASTCOV_HTML_CMD_SPACED "${FASTCOV_HTML_CMD}") + message(" ${FASTCOV_HTML_CMD_SPACED}") + endif() + if(Coverage_POST_CMD) + message(" Running post command: ") + string(REPLACE ";" " " FASTCOV_POST_CMD_SPACED "${FASTCOV_POST_CMD}") + message(" ${FASTCOV_POST_CMD_SPACED}") + endif() + endif() + + # Setup target + add_custom_target(${Coverage_NAME} + + # Cleanup fastcov + COMMAND ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} + --search-directory ${BASEDIR} + --zerocounters + + COMMAND ${FASTCOV_EXEC_TESTS_CMD} + COMMAND ${FASTCOV_CAPTURE_CMD} + COMMAND ${FASTCOV_CONVERT_CMD} + COMMAND ${FASTCOV_HTML_CMD} + COMMAND ${FASTCOV_POST_CMD} + + # Set output files as GENERATED (will be removed on 'make clean') + BYPRODUCTS + ${Coverage_NAME}.info + ${Coverage_NAME}.json + ${Coverage_NAME}/index.html # report directory + + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Resetting code coverage counters to zero. Processing code coverage counters and generating report." + ) + + set(INFO_MSG "fastcov code coverage info report saved in ${Coverage_NAME}.info and ${Coverage_NAME}.json.") + if(NOT Coverage_SKIP_HTML) + string(APPEND INFO_MSG " Open ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html in your browser to view the coverage report.") + endif() + # Show where to find the fastcov info report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo ${INFO_MSG} + ) + +endfunction() # setup_target_for_coverage_fastcov + +function(append_coverage_compiler_flags) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}") +endfunction() # append_coverage_compiler_flags + +# Setup coverage for specific library +function(append_coverage_compiler_flags_to_target name) + separate_arguments(_flag_list NATIVE_COMMAND "${COVERAGE_COMPILER_FLAGS}") + target_compile_options(${name} PRIVATE ${_flag_list}) + if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") + target_link_libraries(${name} PRIVATE gcov) + endif() +endfunction() diff --git a/include/WavefrontParser.hpp b/include/wavefront/Parser.hpp similarity index 70% rename from include/WavefrontParser.hpp rename to include/wavefront/Parser.hpp index 1ee7bad..51cdb5c 100644 --- a/include/WavefrontParser.hpp +++ b/include/wavefront/Parser.hpp @@ -19,7 +19,28 @@ namespace wavefront { * * @return a vector of strings */ - vector splitString(const string & str, char delim, size_t maxcount = -1); + vector splitString(const string & str, char delim, int maxcount = -1); + + /** + * Split a string based on whitespace. + * + * Sequential whitespace will be combined int a single delimiter. + * + * @param str the string to split + * @param maxcount the maximum number of splits + * + * @return a vector of strings + */ + vector splitStringSpace(const string & str, int maxcount = -1); + + /** + * @brief Return str with any leading or trailing whitespace removed + * + * Whitespace is defined by std::isspace + * + * @param str string to trim + */ + void trimString(string & str); /** * Parser for Wavefront formatted .obj and .mtl files. @@ -52,7 +73,16 @@ namespace wavefront { iterator() : parser(nullptr) {} - iterator(Parser * parser) : parser(parser) {} + iterator(Parser * parser) : parser(parser) { + if (parser) { + if (parser->hasNext()) { + parser->read(token); + } + else { + this->parser = nullptr; + } + } + } reference operator*() const { return token; @@ -90,6 +120,7 @@ namespace wavefront { private: istream & is; + string line; public: /** @@ -106,6 +137,14 @@ namespace wavefront { */ explicit operator bool() const; + /** + * @brief Are there more tokens to read + * + * @return true the next call to read will return a token + * @return false the next call to read will throw an exception + */ + bool hasNext(); + /** * Read the next token into token. * @@ -125,6 +164,9 @@ namespace wavefront { * * @return an iterator to the end of all tokens */ - iterator end(); + static iterator end(); + + private: + void findNext(); }; } diff --git a/include/Wavefront.hpp b/include/wavefront/Wavefront.hpp similarity index 62% rename from include/Wavefront.hpp rename to include/wavefront/Wavefront.hpp index e0d15ec..cbc77fa 100644 --- a/include/Wavefront.hpp +++ b/include/wavefront/Wavefront.hpp @@ -1,21 +1,28 @@ #pragma once +#include #include +#include #include #include #include #include namespace wavefront { - using std::string; - using std::vector; - using std::shared_ptr; using glm::vec2; using glm::vec3; + using std::istream; + using std::shared_ptr; + using std::string; + using std::vector; + namespace fs = std::filesystem; struct Mesh { + using Ptr = shared_ptr; + using ConstPtr = const shared_ptr; + string name; - size_t matId; + int matId; vector vertices; vector texcoords; vector normals; @@ -31,6 +38,9 @@ namespace wavefront { }; struct Material { + using Ptr = shared_ptr; + using ConstPtr = const shared_ptr; + string name; float specExp; float alpha; @@ -40,7 +50,10 @@ namespace wavefront { string texAlbedo; string texNormal; string texSpecular; + Material(); + static vector fromFile(const fs::path & path); + static vector fromStream(istream & is); }; class ModelLoadException : public std::runtime_error { @@ -49,11 +62,13 @@ namespace wavefront { }; struct Model { - vector> objects; - vector> materials; + using Ptr = shared_ptr; + using ConstPtr = const shared_ptr; + + vector objects; + vector materials; Model(); - ~Model(); Model(const Model & other) = default; @@ -63,10 +78,7 @@ namespace wavefront { Model & operator=(Model && other) = default; - void clear(); - - void loadMaterialsFrom(const string & path); - - void loadModelFrom(const string & path); + static Ptr fromFile(const fs::path & path); + static Ptr fromStream(istream & is, const fs::path & basePath = ""); }; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 02f1766..38c4e2b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,11 +1,8 @@ set(TARGET Wavefront) -set(HEADER_LIST Wavefront.hpp WavefrontParser.hpp) -list(TRANSFORM HEADER_LIST PREPEND "${${PROJECT_NAME}_SOURCE_DIR}/include/") - -set(SOURCE_LIST Wavefront.cpp WavefrontParser.cpp) -list(TRANSFORM SOURCE_LIST PREPEND "${${PROJECT_NAME}_SOURCE_DIR}/src/") +file(GLOB_RECURSE HEADER_LIST "${${PROJECT_NAME}_SOURCE_DIR}/include/*.hpp") +file(GLOB_RECURSE SOURCE_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) add_library(${TARGET} ${SOURCE_LIST} ${HEADER_LIST}) diff --git a/src/Parser.cpp b/src/Parser.cpp new file mode 100644 index 0000000..716d464 --- /dev/null +++ b/src/Parser.cpp @@ -0,0 +1,147 @@ +#include "wavefront/Parser.hpp" + +#include +#include + +namespace wavefront { + using std::isspace; + using std::stringstream; + + // See https://stackoverflow.com/a/217605 + + // Trim from the start (in place) + static inline void ltrim(string & s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + } + + // Trim from the end (in place) + static inline void rtrim(string & s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), + s.end()); + } + + void trimString(string & str) { + ltrim(str); + rtrim(str); + } + + vector splitString(const string & str, char delim, int maxcount) { + vector parts; + + size_t start = 0, end = 0; + while (end < str.size() && parts.size() != maxcount) { + if (str[end] == delim) { + parts.push_back(str.substr(start, end - start)); + start = ++end; + } + else + ++end; + } + + if (parts.empty()) + parts.push_back(str); + + else if (start <= str.size()) + parts.push_back(str.substr(start)); + + return parts; + } + + vector splitStringSpace(const string & str, int maxcount) { + vector parts; + + size_t start = 0, end = 0; + while (end < str.size() && parts.size() != maxcount) { + if (isspace(str[end])) { + if (end > start) + parts.push_back(str.substr(start, end - start)); + start = ++end; + } + else + ++end; + } + + // Stopped for end of string + if (start < end) + parts.push_back(str.substr(start, end - start)); + + // Stopped for max count + if (end < str.size()) { + string remainder = str.substr(end, str.size() - end); + trimString(remainder); + if (!remainder.empty()) + parts.push_back(remainder); + } + + return parts; + } +} + +namespace wavefront { + vector Parser::Token::params() const { + return splitStringSpace(value); + } +} + +namespace wavefront { + Parser::Parser(istream & is) : is(is) {} + + Parser::operator bool() const { + return is.operator bool(); + } + + bool Parser::hasNext() { + if (line.empty()) { + findNext(); + } + return !line.empty(); + } + + void Parser::read(Parser::Token & token) { + // hasNext will populate this->line + if (hasNext()) { + auto split = line.find_first_of(' '); + if (split == string::npos) { + token.key = line; + token.value.clear(); + } + else { + token.key = line.substr(0, split); + token.value = line.substr(split + 1); + } + line.clear(); + } + } + + Parser::iterator Parser::begin() { + return iterator(this); + } + + Parser::iterator Parser::end() { + return iterator(); + } + + void Parser::findNext() { + if (!is) { + line.clear(); + return; + } + + for (; std::getline(is, line);) { + // Remove comment if any exists + auto split = line.find_first_of('#'); + if (split != string::npos) { + line.erase(line.begin() + split, line.end()); + } + + trimString(line); + + if (!line.empty()) + break; + } + } +} diff --git a/src/Wavefront.cpp b/src/Wavefront.cpp index 187bb69..dbbdfe5 100644 --- a/src/Wavefront.cpp +++ b/src/Wavefront.cpp @@ -1,64 +1,54 @@ -#include "Wavefront.hpp" +#include "wavefront/Wavefront.hpp" #include #include #include -#include "WavefrontParser.hpp" +#include "wavefront/Parser.hpp" namespace wavefront { - Mesh::Mesh() : matId(0) {} + Mesh::Mesh() : matId(-1) {} size_t Mesh::size() const { return vertices.size(); } } -namespace wavefront { - Material::Material() : specExp(1.0), alpha(1.0) {} -} - namespace wavefront { using std::ifstream; using std::make_shared; - Model::Model() {} - - Model::~Model() { - clear(); - } - - void Model::clear() { - objects.clear(); - materials.clear(); - } + Material::Material() : specExp(1.0), alpha(1.0) {} - void Model::loadMaterialsFrom(const string & path) { + vector Material::fromFile(const fs::path & path) { ifstream is(path); if (!is.is_open()) { - throw ModelLoadException("Failed to open file " + path); + throw MaterialLoadException("Failed to open file " + path.string()); } + return fromStream(is); + } + + vector Material::fromStream(istream & is) { Parser parser(is); - shared_ptr material = nullptr; + vector materials; + Material::Ptr material = nullptr; for (auto & token : parser) { + if (token.key == "newmtl") { + material = make_shared(); + material->name = token.value; + materials.push_back(material); + } + else if (!material) { + throw MaterialLoadException("Got a token (" + token.key + ") before starting a material (newmtl)"); + } switch (token.key[0]) { - case 'n': { // newmtl - if (token.key != "newmtl") - break; - material = make_shared(); - material->name = token.value; - materials.push_back(material); - } break; case 'K': { if (token.key.size() < 2) break; switch (token.key[1]) { case 'a': { // Ka - if (!material) - throw MaterialLoadException( - "Got a Color ambient (Ka) before starign an material (newmtl)"); auto params = token.params(); if (params.size() != material->colAmbient.length()) throw MaterialLoadException( @@ -67,9 +57,6 @@ namespace wavefront { material->colAmbient[i] = std::stof(params[i]); } break; case 'd': { // Kd - if (!material) - throw MaterialLoadException( - "Got a Color diffuse (Kd) before starign an material (newmtl)"); auto params = token.params(); if (params.size() != material->colDiffuse.length()) throw MaterialLoadException( @@ -78,9 +65,6 @@ namespace wavefront { material->colDiffuse[i] = std::stof(params[i]); } break; case 's': { // Ks - if (!material) - throw MaterialLoadException( - "Got a Color specular (Ks) before starign an material (newmtl)"); auto params = token.params(); if (params.size() != material->colSpecular.length()) throw MaterialLoadException( @@ -130,124 +114,136 @@ namespace wavefront { break; } } + + return materials; } +} - void Model::loadModelFrom(const string & path) { +namespace wavefront { + using std::ifstream; + using std::make_shared; + + Model::Model() {} + + Model::Ptr Model::fromFile(const fs::path & path) { ifstream is(path); if (!is.is_open()) { - throw ModelLoadException("Failed to open file " + path); + throw ModelLoadException("Failed to open file " + path.string()); } - Parser parser(is); + return fromStream(is, path.parent_path()); + } - vector av; - vector avt; - vector avn; + Model::Ptr Model::fromStream(istream & is, const fs::path & basePath) { + Model::Ptr model = make_shared(); + if (model) { + Parser parser(is); - shared_ptr mesh = nullptr; + vector av; + vector avt; + vector avn; - for (auto & token : parser) { - switch (token.key[0]) { - case 'o': { + shared_ptr mesh = nullptr; + + for (auto & token : parser) { + if (token.key == "o") { mesh = make_shared(); mesh->name = token.value; - objects.push_back(mesh); - } break; - case 'v': { - if (token.key.size() == 1) { // v - auto & v = av.emplace_back(); + model->objects.push_back(mesh); + } + else if (!mesh) { + throw ModelLoadException("Got a token (" + token.key + ") before starting an object (o)"); + } + switch (token.key[0]) { + case 'v': { + if (token.key.size() == 1) { // v + auto & v = av.emplace_back(); + auto params = token.params(); + if (params.size() != v.length()) + throw ModelLoadException( + "Vertex (v) must have 3 values"); + for (int i = 0; i < v.length(); i++) // + v[i] = std::stof(params[i]); + } + else { // token.key.size() > 1 + switch (token.key[1]) { + case 't': { // vt + auto & vt = avt.emplace_back(); + auto params = token.params(); + if (params.size() != vt.length()) + throw ModelLoadException( + "TexCoord (vt) must have 2 values"); + for (int i = 0; i < vt.length(); i++) + vt[i] = std::stof(params[i]); + } break; + case 'n': { // vn + auto & vn = avn.emplace_back(); + auto params = token.params(); + if (params.size() != vn.length()) + throw ModelLoadException( + "Normal (vn) must have 3 values"); + for (int i = 0; i < vn.length(); i++) + vn[i] = std::stof(params[i]); + } break; + default: + break; + } + } + } break; + case 'f': { auto params = token.params(); - if (params.size() != v.length()) - throw ModelLoadException( - "Vertex (v) must have 3 values"); - for (int i = 0; i < v.length(); i++) // - v[i] = std::stof(params[i]); - } - else { // token.key.size() > 1 - switch (token.key[1]) { - case 't': { // vt - auto & vt = avt.emplace_back(); - auto params = token.params(); - if (params.size() != vt.length()) - throw ModelLoadException( - "TexCoord (vt) must have 2 values"); - for (int i = 0; i < vt.length(); i++) - vt[i] = std::stof(params[i]); - } break; - case 'n': { // vn - auto & vn = avn.emplace_back(); - auto params = token.params(); - if (params.size() != vn.length()) - throw ModelLoadException( - "Normal (vn) must have 3 values"); - for (int i = 0; i < vn.length(); i++) - vn[i] = std::stof(params[i]); - } break; - default: - break; + if (params.size() != 3) + throw ModelLoadException("Face (f) must have 3 components"); + for (int i = 0; i < 3; i++) { + auto subParams = splitString(params[i], '/'); + if (subParams.size() != 3) + throw ModelLoadException( + "Face (c) component must have 3 values"); + size_t iv = std::stoul(subParams[0]); + iv -= 1; // index at 1 + if (iv >= av.size()) + throw ModelLoadException("Vertex out of bounds"); + mesh->vertices.push_back(av[iv]); + + size_t it = std::stoul(subParams[1]); + it -= 1; // index at 1 + if (it >= avt.size()) + throw ModelLoadException("TexCoord out of bounds"); + mesh->texcoords.push_back(avt[it]); + + size_t in = std::stoul(subParams[2]); + in -= 1; // index at 1 + if (in >= avn.size()) + throw ModelLoadException("Normal out of bounds"); + mesh->normals.push_back(avn[in]); } - } - } break; - case 'f': { - if (!mesh) - throw ModelLoadException( - "Got a face (f) before starting an object (o)"); - auto params = token.params(); - if (params.size() != 3) - throw ModelLoadException("Face (f) must have 3 components"); - for (int i = 0; i < 3; i++) { - auto subParams = splitString(params[i], '/'); - if (subParams.size() != 3) - throw ModelLoadException( - "Face (c) component must have 3 values"); - size_t iv = std::stoul(subParams[0]); - iv -= 1; // index at 1 - if (iv >= av.size()) - throw ModelLoadException("Vertex out of bounds"); - mesh->vertices.push_back(av[iv]); - - size_t it = std::stoul(subParams[1]); - it -= 1; // index at 1 - if (it >= avt.size()) - throw ModelLoadException("TexCoord out of bounds"); - mesh->texcoords.push_back(avt[it]); - - size_t in = std::stoul(subParams[2]); - in -= 1; // index at 1 - if (in >= avn.size()) - throw ModelLoadException("Normal out of bounds"); - mesh->normals.push_back(avn[in]); - } - } break; - case 'm': { // mtllib - if (token.key != "mtllib") - break; - auto lastSlash = path.find_last_of('/'); - string mtlPath = path.substr(0, lastSlash + 1); - mtlPath += token.value; - loadMaterialsFrom(mtlPath); - } break; - case 'u': { // usemtl - if (token.key != "usemtl") - break; - if (!mesh) - throw ModelLoadException( - "Got a (usemtl) before starting an object (o)"); - bool found = false; - for (int i = 0; i < materials.size(); i++) { - if (materials[i]->name == token.value) { - mesh->matId = i; - found = true; + } break; + case 'm': { // mtllib + if (token.key != "mtllib") break; + auto materials = Material::fromFile(basePath / token.value); + model->materials.insert(model->materials.end(), materials.begin(), materials.end()); + } break; + case 'u': { // usemtl + if (token.key != "usemtl") + break; + bool found = false; + for (int i = 0; i < model->materials.size(); i++) { + if (model->materials[i]->name == token.value) { + mesh->matId = i; + found = true; + break; + } } - } - if (!found) { - throw ModelLoadException("Unknown material name " - + token.value); - } - } break; - default: - break; + if (!found) { + throw ModelLoadException("Unknown material name " + + token.value); + } + } break; + default: + break; + } } } + return model; } } diff --git a/src/WavefrontParser.cpp b/src/WavefrontParser.cpp deleted file mode 100644 index 2d3b764..0000000 --- a/src/WavefrontParser.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "WavefrontParser.hpp" - -#include - -namespace wavefront { - using std::stringstream; - - vector splitString(const string & str, char delim, size_t maxcount) { - vector parts; - - size_t start = 0, end = 0; - while (end < str.size() && (maxcount-- > 0)) { - while (end < str.size()) { - if (str[end] == delim) { - parts.push_back(str.substr(start, end - start)); - start = ++end; - break; - } - else - ++end; - } - } - - if (parts.empty()) - parts.push_back(str); - - else if (start <= str.size()) - parts.push_back(str.substr(start)); - - return parts; - } -} - -namespace wavefront { - vector Parser::Token::params() const { - return splitString(value, ' '); - } - - Parser::Parser(istream & is) : is(is) {} - - Parser::operator bool() const { - return is.operator bool(); - } - - void Parser::read(Parser::Token & token) { - string line; - for (; std::getline(is, line);) { - if (line.empty() || line[0] == '#') - continue; - - auto split = line.find_first_of(' '); - if (split == string::npos) { - token.key = line; - token.value.clear(); - } - else { - token.key = line.substr(0, split); - token.value = line.substr(split + 1); - } - break; - } - } - - Parser::iterator Parser::begin() { - return iterator(this); - } - - Parser::iterator Parser::end() { - return iterator(); - } -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a7759e3..06ff0ee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,16 +1,25 @@ -set(TARGET tests) - enable_testing() +include(GoogleTest) -add_executable(${TARGET} - parser_tests.cpp -) -target_link_libraries(${TARGET} PRIVATE gtest gtest_main Wavefront) +file(GLOB_RECURSE SOURCE_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) +file(GLOB_RECURSE HEADER_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.hpp) -add_test( - NAME ${TARGET} - COMMAND $ -) +set(ALL_TEST_TARGETS "") +foreach(TEST_SOURCE ${SOURCE_LIST}) + cmake_path(GET TEST_SOURCE STEM test_name) + if(${test_name} MATCHES "_tests$") + list(APPEND ALL_TEST_TARGETS ${test_name}) + message("Generate test ${test_name}") + + add_executable(${test_name} ${TEST_SOURCE} ${HEADER_LIST}) + target_link_libraries(${test_name} PRIVATE gtest gtest_main Wavefront) + + # add_test( + # NAME ${test_name} + # COMMAND $ + # ) + + gtest_discover_tests(${test_name}) + endif() +endforeach() -include(GoogleTest) -gtest_discover_tests(${TARGET}) diff --git a/tests/parser_iter_tests.cpp b/tests/parser_iter_tests.cpp new file mode 100644 index 0000000..45b54a9 --- /dev/null +++ b/tests/parser_iter_tests.cpp @@ -0,0 +1,44 @@ +#include + +#include +using namespace std; + +#include +using namespace wavefront; + +TEST(ParserIterTest, NullIter) { + EXPECT_EQ(Parser::iterator(), Parser::iterator()); + EXPECT_EQ(Parser::iterator(), Parser::iterator(nullptr)); +} + +TEST(ParserIterTest, Begin) { + istringstream is("one\ntwo"); + auto parser = wavefront::Parser(is); + auto iter = parser.begin(); + auto token = *iter; + EXPECT_EQ("one", token.key); + ++iter; + token = *iter; + EXPECT_EQ("two", token.key); + EXPECT_EQ(Parser::end(), ++iter); +} + +TEST(ParserIterTest, Emtpy) { + istringstream is(""); + auto parser = wavefront::Parser(is); + EXPECT_FALSE(parser.hasNext()); + auto iter = parser.begin(); + EXPECT_TRUE(iter == Parser::end()); +} + +TEST(ParserIterTest, End) { + EXPECT_EQ(Parser::iterator(), Parser::end()); +} + +TEST(ParserIterTest, Equal) { + EXPECT_TRUE(Parser::end() == Parser::end()); +} + +TEST(ParserIterTest, NotEqual) { + EXPECT_FALSE(Parser::end() != Parser::end()); +} diff --git a/tests/parser_tests.cpp b/tests/parser_tests.cpp index 5e26324..a5c504f 100644 --- a/tests/parser_tests.cpp +++ b/tests/parser_tests.cpp @@ -3,75 +3,96 @@ #include using namespace std; -#include +#include -TEST(SplitStringTest, EmptyString) { - auto split = wavefront::splitString("", ','); - EXPECT_EQ(1, split.size()); - EXPECT_EQ("", split[0]); +TEST(ParserTest, TwoLines) { + istringstream is("one\ntwo"); + auto parser = wavefront::Parser(is); + wavefront::Parser::Token token; + parser.read(token); + EXPECT_EQ("one", token.key); + parser.read(token); + EXPECT_EQ("two", token.key); + EXPECT_FALSE(parser.hasNext()); } -TEST(SplitStringTest, NoDelimiter) { - auto split = wavefront::splitString("foo", ','); - EXPECT_EQ(1, split.size()); - EXPECT_EQ("foo", split[0]); +TEST(ParserTest, Space) { + istringstream is("one two"); + auto parser = wavefront::Parser(is); + wavefront::Parser::Token token; + parser.read(token); + EXPECT_EQ("one", token.key); + auto p = token.params(); + ASSERT_EQ(1, p.size()); + EXPECT_EQ("two", p[0]); + EXPECT_FALSE(parser.hasNext()); } -TEST(SplitStringTest, SingleDelimiter) { - auto split = wavefront::splitString("foo,bar", ','); - EXPECT_EQ(2, split.size()); - EXPECT_EQ("foo", split[0]); - EXPECT_EQ("bar", split[1]); +TEST(ParserTest, MultipleSpaces) { + istringstream is("one two"); + auto parser = wavefront::Parser(is); + wavefront::Parser::Token token; + parser.read(token); + EXPECT_EQ("one", token.key); + auto p = token.params(); + ASSERT_EQ(1, p.size()); + EXPECT_EQ("two", p[0]); + EXPECT_FALSE(parser.hasNext()); } -TEST(SplitStringTest, EmptyElements) { - auto split = wavefront::splitString(",", ','); - EXPECT_EQ(2, split.size()); - EXPECT_EQ("", split[0]); - EXPECT_EQ("", split[1]); +TEST(ParserTest, TrailingSpace) { + istringstream is("one two "); + auto parser = wavefront::Parser(is); + wavefront::Parser::Token token; + parser.read(token); + EXPECT_EQ("one", token.key); + auto p = token.params(); + ASSERT_EQ(1, p.size()); + EXPECT_EQ("two", p[0]); + EXPECT_FALSE(parser.hasNext()); } -TEST(SplitStringTest, StartEmptyElements) { - auto split = wavefront::splitString(",bar", ','); - EXPECT_EQ(2, split.size()); - EXPECT_EQ("", split[0]); - EXPECT_EQ("bar", split[1]); +TEST(ParserTest, Comment) { + istringstream is("#one"); + auto parser = wavefront::Parser(is); + wavefront::Parser::Token token; + parser.read(token); + EXPECT_EQ("", token.key); + EXPECT_FALSE(parser.hasNext()); } -TEST(SplitStringTest, EndEmptyElements) { - auto split = wavefront::splitString("foo,", ','); - EXPECT_EQ(2, split.size()); - EXPECT_EQ("foo", split[0]); - EXPECT_EQ("", split[1]); +TEST(ParserTest, TrailingComment) { + istringstream is("one # two"); + auto parser = wavefront::Parser(is); + wavefront::Parser::Token token; + parser.read(token); + EXPECT_EQ("one", token.key); + EXPECT_FALSE(parser.hasNext()); } -TEST(SplitStringTest, MultipleEmptyElements) { - auto split = wavefront::splitString("foo,,bar", ','); - EXPECT_EQ(3, split.size()); - EXPECT_EQ("foo", split[0]); - EXPECT_EQ("", split[1]); - EXPECT_EQ("bar", split[2]); +TEST(ParserTest, HasNext) { + istringstream is("one\ntwo"); + auto parser = wavefront::Parser(is); + wavefront::Parser::Token token; + ASSERT_TRUE(parser.hasNext()); + parser.read(token); + EXPECT_EQ("one", token.key); + ASSERT_TRUE(parser.hasNext()); + parser.read(token); + EXPECT_EQ("two", token.key); + EXPECT_FALSE(parser.hasNext()); } -TEST(SplitStringTest, StartMultipleEmptyElements) { - auto split = wavefront::splitString(",,bar", ','); - EXPECT_EQ(3, split.size()); - EXPECT_EQ("", split[0]); - EXPECT_EQ("", split[1]); - EXPECT_EQ("bar", split[2]); +TEST(ParserTest, EOFHasNext) { + istringstream is(""); + is.get(); + ASSERT_FALSE(is); + auto parser = wavefront::Parser(is); + ASSERT_FALSE(parser.hasNext()); } -TEST(SplitStringTest, EndMultipleEmptyElements) { - auto split = wavefront::splitString("foo,,", ','); - EXPECT_EQ(3, split.size()); - EXPECT_EQ("foo", split[0]); - EXPECT_EQ("", split[1]); - EXPECT_EQ("", split[2]); -} - -TEST(SplitStringTest, MaxCount) { - auto split = wavefront::splitString("a,b,c", ',', 1); - EXPECT_EQ(2, split.size()); - EXPECT_EQ("a", split[0]); - EXPECT_EQ("b,c", split[1]); +TEST(ParserTest, EmptyStream) { + istringstream is(""); + auto parser = wavefront::Parser(is); + EXPECT_EQ(parser.begin(), parser.end()); } diff --git a/tests/split_string_space_tests.cpp b/tests/split_string_space_tests.cpp new file mode 100644 index 0000000..6ef8193 --- /dev/null +++ b/tests/split_string_space_tests.cpp @@ -0,0 +1,67 @@ +#include + +#include +using namespace std; + +#include + +TEST(SplitStringSpaceTest, EmptyString) { + auto split = wavefront::splitStringSpace(""); + ASSERT_EQ(0, split.size()); +} + +TEST(SplitStringSpaceTest, NoDelimiter) { + auto split = wavefront::splitStringSpace("foo"); + ASSERT_EQ(1, split.size()); + EXPECT_EQ("foo", split[0]); +} + +TEST(SplitStringSpaceTest, SingleDelimiter) { + auto split = wavefront::splitStringSpace("foo bar"); + ASSERT_EQ(2, split.size()); + EXPECT_EQ("foo", split[0]); + EXPECT_EQ("bar", split[1]); +} + +TEST(SplitStringSpaceTest, EmptyElements) { + auto split = wavefront::splitStringSpace(" "); + ASSERT_EQ(0, split.size()); +} + +TEST(SplitStringSpaceTest, StartEmptyElements) { + auto split = wavefront::splitStringSpace(" bar"); + ASSERT_EQ(1, split.size()); + EXPECT_EQ("bar", split[0]); +} + +TEST(SplitStringSpaceTest, EndEmptyElements) { + auto split = wavefront::splitStringSpace("foo "); + ASSERT_EQ(1, split.size()); + EXPECT_EQ("foo", split[0]); +} + +TEST(SplitStringSpaceTest, MultipleEmptyElements) { + auto split = wavefront::splitStringSpace("foo bar"); + ASSERT_EQ(2, split.size()); + EXPECT_EQ("foo", split[0]); + EXPECT_EQ("bar", split[1]); +} + +TEST(SplitStringSpaceTest, StartMultipleEmptyElements) { + auto split = wavefront::splitStringSpace(" bar"); + ASSERT_EQ(1, split.size()); + EXPECT_EQ("bar", split[0]); +} + +TEST(SplitStringSpaceTest, EndMultipleEmptyElements) { + auto split = wavefront::splitStringSpace("foo "); + ASSERT_EQ(1, split.size()); + EXPECT_EQ("foo", split[0]); +} + +TEST(SplitStringSpaceTest, MaxCount) { + auto split = wavefront::splitStringSpace("a b c", 1); + ASSERT_EQ(2, split.size()); + EXPECT_EQ("a", split[0]); + EXPECT_EQ("b c", split[1]); +} diff --git a/tests/split_string_tests.cpp b/tests/split_string_tests.cpp new file mode 100644 index 0000000..43010af --- /dev/null +++ b/tests/split_string_tests.cpp @@ -0,0 +1,88 @@ +#include + +#include +using namespace std; + +#include +#include + +TEST(StreamTest, DoesItEnd) { + std::stringstream s("a"); + EXPECT_TRUE(s); + EXPECT_EQ('a', s.get()); + EXPECT_TRUE(s); + EXPECT_EQ(EOF, s.peek()); + EXPECT_EQ(EOF, s.get()); + EXPECT_FALSE(s); +} + +TEST(SplitStringTest, EmptyString) { + auto split = wavefront::splitString("", ','); + ASSERT_EQ(1, split.size()); + EXPECT_EQ("", split[0]); +} + +TEST(SplitStringTest, NoDelimiter) { + auto split = wavefront::splitString("foo", ','); + ASSERT_EQ(1, split.size()); + EXPECT_EQ("foo", split[0]); +} + +TEST(SplitStringTest, SingleDelimiter) { + auto split = wavefront::splitString("foo,bar", ','); + ASSERT_EQ(2, split.size()); + EXPECT_EQ("foo", split[0]); + EXPECT_EQ("bar", split[1]); +} + +TEST(SplitStringTest, EmptyElements) { + auto split = wavefront::splitString(",", ','); + ASSERT_EQ(2, split.size()); + EXPECT_EQ("", split[0]); + EXPECT_EQ("", split[1]); +} + +TEST(SplitStringTest, StartEmptyElements) { + auto split = wavefront::splitString(",bar", ','); + ASSERT_EQ(2, split.size()); + EXPECT_EQ("", split[0]); + EXPECT_EQ("bar", split[1]); +} + +TEST(SplitStringTest, EndEmptyElements) { + auto split = wavefront::splitString("foo,", ','); + ASSERT_EQ(2, split.size()); + EXPECT_EQ("foo", split[0]); + EXPECT_EQ("", split[1]); +} + +TEST(SplitStringTest, MultipleEmptyElements) { + auto split = wavefront::splitString("foo,,bar", ','); + ASSERT_EQ(3, split.size()); + EXPECT_EQ("foo", split[0]); + EXPECT_EQ("", split[1]); + EXPECT_EQ("bar", split[2]); +} + +TEST(SplitStringTest, StartMultipleEmptyElements) { + auto split = wavefront::splitString(",,bar", ','); + ASSERT_EQ(3, split.size()); + EXPECT_EQ("", split[0]); + EXPECT_EQ("", split[1]); + EXPECT_EQ("bar", split[2]); +} + +TEST(SplitStringTest, EndMultipleEmptyElements) { + auto split = wavefront::splitString("foo,,", ','); + ASSERT_EQ(3, split.size()); + EXPECT_EQ("foo", split[0]); + EXPECT_EQ("", split[1]); + EXPECT_EQ("", split[2]); +} + +TEST(SplitStringTest, MaxCount) { + auto split = wavefront::splitString("a,b,c", ',', 1); + ASSERT_EQ(2, split.size()); + EXPECT_EQ("a", split[0]); + EXPECT_EQ("b,c", split[1]); +} diff --git a/tests/trim_string_tests.cpp b/tests/trim_string_tests.cpp new file mode 100644 index 0000000..c1d42da --- /dev/null +++ b/tests/trim_string_tests.cpp @@ -0,0 +1,30 @@ +#include + +#include +using namespace std; + +#include + +TEST(TrimStringTest, Both) { + string s = " a b "; + wavefront::trimString(s); + EXPECT_EQ("a b", s); +} + +TEST(TrimStringTest, Left) { + string s = " a b"; + wavefront::trimString(s); + EXPECT_EQ("a b", s); +} + +TEST(TrimStringTest, Right) { + string s = "a b "; + wavefront::trimString(s); + EXPECT_EQ("a b", s); +} + +TEST(TrimStringTest, NoTrim) { + string s = "a b"; + wavefront::trimString(s); + EXPECT_EQ("a b", s); +} diff --git a/tests/wavefront_material_tests.cpp b/tests/wavefront_material_tests.cpp new file mode 100644 index 0000000..ba595db --- /dev/null +++ b/tests/wavefront_material_tests.cpp @@ -0,0 +1,96 @@ +#include + +#include +#include +using namespace std; + +#include +using namespace wavefront; + +TEST(MaterialTest, Init) { + Material mat; + EXPECT_FLOAT_EQ(1.0, mat.specExp); + EXPECT_FLOAT_EQ(1.0, mat.alpha); +} + +TEST(MaterialTest, Empty) { + stringstream ss(""); + auto mats = Material::fromStream(ss); + EXPECT_TRUE(mats.empty()); +} + +TEST(MaterialTest, NoMaterial) { + stringstream ss("Ka 1.0 2.0 3.0"); + EXPECT_THROW(Material::fromStream(ss), MaterialLoadException); +} + +TEST(MaterialTest, MultiMaterial) { + stringstream ss("newmtl One\nnewmtl Two"); + auto mats = Material::fromStream(ss); + ASSERT_EQ(2, mats.size()); + EXPECT_EQ("One", mats[0]->name); + EXPECT_EQ("Two", mats[1]->name); +} + +TEST(MaterialTest, AttrErrorNoMaterial) { + vector attrs { + "Ka", + "Kd", + "Ks", + "Ni", + "d", + "map_Kd", + "map_Ks", + "map_bump", + }; + for (auto & attr : attrs) { + stringstream ss(attr); + EXPECT_THROW(Material::fromStream(ss), MaterialLoadException) << attr; + } +} + +TEST(MaterialTest, AttrErrorTooSmall) { + vector attrs { + "Ka", + "Kd", + "Ks", + }; + for (auto & attr : attrs) { + stringstream ss("newmtl a\n" + attr); + EXPECT_THROW(Material::fromStream(ss), MaterialLoadException) << attr; + } +} + +TEST(MaterialTest, AllAtributes) { + stringstream ss( + "# Comment on it's own line\n" + "newmtl mtlName # Comment after token\n" + "K # K too short\n" + "Kx # K second wrong\n" + "N # N too short\n" + "Nx # N second wrong\n" + "Ka 1.0 2.0 3.0\n" + "Kd 4.0 5.0 6.0\n" + "Ks 7.0 8.0 9.0\n" + "Ni 10.0\n" + "d 11.0\n" + "m # m to short\n" + "mapxx # m not map\n" + "map_x # map wrong\n" + "map_Kd tex_albedo.png\n" + "map_Ks tex_spec.png\n" + "map_bump tex_bump.png\n" + ""); + auto mats = Material::fromStream(ss); + ASSERT_EQ(1, mats.size()); + auto mat = mats[0]; + EXPECT_EQ("mtlName", mat->name); + EXPECT_EQ(vec3(1.0, 2.0, 3.0), mat->colAmbient); + EXPECT_EQ(vec3(4.0, 5.0, 6.0), mat->colDiffuse); + EXPECT_EQ(vec3(7.0, 8.0, 9.0), mat->colSpecular); + EXPECT_FLOAT_EQ(10.0, mat->specExp); + EXPECT_FLOAT_EQ(11.0, mat->alpha); + EXPECT_EQ("tex_albedo.png", mat->texAlbedo); + EXPECT_EQ("tex_spec.png", mat->texSpecular); + EXPECT_EQ("tex_bump.png", mat->texNormal); +} diff --git a/tests/wavefront_mesh_tests.cpp b/tests/wavefront_mesh_tests.cpp new file mode 100644 index 0000000..020a8ae --- /dev/null +++ b/tests/wavefront_mesh_tests.cpp @@ -0,0 +1,24 @@ +#include + +#include +#include +using namespace std; + +#include +using namespace wavefront; + +TEST(MeshTest, Init) { + Mesh mesh; + EXPECT_TRUE(mesh.name.empty()); + EXPECT_EQ(-1, mesh.matId); + EXPECT_TRUE(mesh.vertices.empty()); + EXPECT_TRUE(mesh.texcoords.empty()); + EXPECT_TRUE(mesh.normals.empty()); +} + +TEST(MeshTest, Size) { + Mesh mesh; + mesh.vertices.emplace_back(); + mesh.vertices.emplace_back(); + EXPECT_EQ(2, mesh.size()); +} diff --git a/tests/wavefront_model_tests.cpp b/tests/wavefront_model_tests.cpp new file mode 100644 index 0000000..8a6e4e3 --- /dev/null +++ b/tests/wavefront_model_tests.cpp @@ -0,0 +1,132 @@ +#include + +#include +#include +using namespace std; + +#include +using namespace wavefront; + +TEST(ModelTest, Empty) { + stringstream ss(""); + auto model = Model::fromStream(ss); + ASSERT_NE(nullptr, model); + EXPECT_TRUE(model->materials.empty()); + EXPECT_TRUE(model->objects.empty()); +} + +TEST(ModelTest, NoModel) { + stringstream ss("v 1.0 2.0 3.0"); + EXPECT_THROW(Model::fromStream(ss), ModelLoadException); +} + +TEST(ModelTest, MultiModel) { + stringstream ss("o One\no Two"); + auto model = Model::fromStream(ss); + ASSERT_NE(nullptr, model); + ASSERT_EQ(2, model->objects.size()); + EXPECT_EQ("One", model->objects[0]->name); + EXPECT_EQ("Two", model->objects[1]->name); +} + +TEST(ModelTest, AttrErrorTooSmall) { + vector attrs { + "v", + "vt", + "vn", + "f", + }; + for (auto & attr : attrs) { + stringstream ss("o a\n" + attr); + EXPECT_THROW(Model::fromStream(ss), ModelLoadException) << attr; + } +} + +TEST(ModelTest, AllAtributes) { + stringstream ss( + "# Comment on it's own line\n" + "o objName # Comment after token\n" + "vx # v second wrong\n" + "v 0.0 1.0 2.0\n" + "v 3.0 4.0 5.0\n" + "v 6.0 7.0 8.0\n" + "vt 9.0 10.0\n" + "vt 11.0 12.0\n" + "vt 13.0 14.0\n" + "vn 15.0 16.0 17.0\n" + "vn 18.0 19.0 20.0\n" + "vn 21.0 22.0 23.0\n" + "f 1/1/1 2/2/2 3/3/3\n" + ""); + auto model = Model::fromStream(ss); + ASSERT_NE(nullptr, model); + ASSERT_EQ(1, model->objects.size()); + EXPECT_EQ(0, model->materials.size()); + auto obj = model->objects[0]; + EXPECT_EQ("objName", obj->name); + EXPECT_EQ(-1, obj->matId); + EXPECT_EQ(3, obj->size()); + EXPECT_EQ(vec3(0.0, 1.0, 2.0), obj->vertices[0]); + EXPECT_EQ(vec3(3.0, 4.0, 5.0), obj->vertices[1]); + EXPECT_EQ(vec3(6.0, 7.0, 8.0), obj->vertices[2]); + EXPECT_EQ(vec2(9.0, 10.0), obj->texcoords[0]); + EXPECT_EQ(vec2(11.0, 12.0), obj->texcoords[1]); + EXPECT_EQ(vec2(13.0, 14.0), obj->texcoords[2]); + EXPECT_EQ(vec3(15.0, 16.0, 17.0), obj->normals[0]); + EXPECT_EQ(vec3(18.0, 19.0, 20.0), obj->normals[1]); + EXPECT_EQ(vec3(21.0, 22.0, 23.0), obj->normals[2]); +} + +TEST(ModelTest, FWrongArgCount) { + stringstream ss( + "o objName\n" + "f 1/1/1/1 2/2/2 3/3/3\n" + ""); + EXPECT_THROW(Model::fromStream(ss), ModelLoadException); +} + +TEST(ModelTest, VIndexOutOfRange) { + stringstream ss( + "o objName\n" + "f 1/1/1 2/2/2 3/3/3\n" + ""); + EXPECT_THROW(Model::fromStream(ss), ModelLoadException); +} + +TEST(ModelTest, TIndexOutOfRange) { + stringstream ss( + "o objName\n" + "v 0.0 1.0 2.0\n" + "f 1/1/1 2/2/2 3/3/3\n" + ""); + EXPECT_THROW(Model::fromStream(ss), ModelLoadException); +} + +TEST(ModelTest, NIndexOutOfRange) { + stringstream ss( + "o objName\n" + "v 0.0 1.0 2.0\n" + "vt 9.0 10.0\n" + "f 1/1/1 2/2/2 3/3/3\n" + ""); + EXPECT_THROW(Model::fromStream(ss), ModelLoadException); +} + +TEST(ModelTest, MissingMaterial) { + stringstream ss( + "o objName\n" + "usemtl missing\n" + ""); + EXPECT_THROW(Model::fromStream(ss), ModelLoadException); +} + +TEST(ModelTest, UNotUseMtl) { + stringstream ss( + "o objName\n" + "ux\n" + ""); + auto model = Model::fromStream(ss); + ASSERT_NE(nullptr, model); + EXPECT_EQ(1, model->objects.size()); + EXPECT_TRUE(model->materials.empty()); +}