├── .gitignore ├── .ycm_extra_conf.py ├── LICENSE ├── Makefile.in ├── README.md ├── configure ├── do_install ├── example ├── main.cpp └── output.cpp └── include └── jest ├── detail ├── expect.hpp ├── group.hpp ├── optional.hpp └── test.hpp └── jest.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | Makefile 3 | 4 | # Output directories 5 | bin/ 6 | 7 | # Vim 8 | .ycm_extra_conf.pyc 9 | -------------------------------------------------------------------------------- /.ycm_extra_conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ycm_core 3 | 4 | flags = [ 5 | '-Wall', 6 | '-Wextra', 7 | '-pedantic', 8 | '-std=c++1y', 9 | #'-stdlib=libc++', 10 | '-x', 11 | 'c++', 12 | '-Isrc', 13 | '-Iinclude', 14 | 15 | '-isystem', 16 | '../BoostParts', 17 | '-isystem', 18 | '/System/Library/Frameworks/Python.framework/Headers', 19 | '-isystem', 20 | '../llvm/include', 21 | '-isystem', 22 | '../llvm/tools/clang/include', 23 | '-I', 24 | '.', 25 | '-I', 26 | './ClangCompleter', 27 | '-isystem', 28 | './tests/gmock/gtest', 29 | '-isystem', 30 | './tests/gmock/gtest/include', 31 | '-isystem', 32 | './tests/gmock', 33 | '-isystem', 34 | './tests/gmock/include', 35 | '-isystem', 36 | '/usr/include', 37 | '-isystem', 38 | '/usr/local/include', 39 | '-isystem', 40 | '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../lib/c++/v1', 41 | '-isystem', 42 | '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include', 43 | ] 44 | 45 | compilation_database_folder = '' 46 | 47 | if os.path.exists( compilation_database_folder ): 48 | database = ycm_core.CompilationDatabase( compilation_database_folder ) 49 | else: 50 | database = None 51 | 52 | SOURCE_EXTENSIONS = [ '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ] 53 | 54 | def DirectoryOfThisScript(): 55 | return os.path.dirname( os.path.abspath( __file__ ) ) 56 | 57 | 58 | def MakeRelativePathsInFlagsAbsolute( flags, working_directory ): 59 | if not working_directory: 60 | return list( flags ) 61 | new_flags = [] 62 | make_next_absolute = False 63 | path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ] 64 | for flag in flags: 65 | new_flag = flag 66 | 67 | if make_next_absolute: 68 | make_next_absolute = False 69 | if not flag.startswith( '/' ): 70 | new_flag = os.path.join( working_directory, flag ) 71 | 72 | for path_flag in path_flags: 73 | if flag == path_flag: 74 | make_next_absolute = True 75 | break 76 | 77 | if flag.startswith( path_flag ): 78 | path = flag[ len( path_flag ): ] 79 | new_flag = path_flag + os.path.join( working_directory, path ) 80 | break 81 | 82 | if new_flag: 83 | new_flags.append( new_flag ) 84 | return new_flags 85 | 86 | 87 | def IsHeaderFile( filename ): 88 | extension = os.path.splitext( filename )[ 1 ] 89 | return extension in [ '.h', '.hxx', '.hpp', '.hh' ] 90 | 91 | 92 | def GetCompilationInfoForFile( filename ): 93 | if IsHeaderFile( filename ): 94 | basename = os.path.splitext( filename )[ 0 ] 95 | for extension in SOURCE_EXTENSIONS: 96 | replacement_file = basename + extension 97 | if os.path.exists( replacement_file ): 98 | compilation_info = database.GetCompilationInfoForFile( 99 | replacement_file ) 100 | if compilation_info.compiler_flags_: 101 | return compilation_info 102 | return None 103 | return database.GetCompilationInfoForFile( filename ) 104 | 105 | 106 | def FlagsForFile( filename, **kwargs ): 107 | if database: 108 | compilation_info = GetCompilationInfoForFile( filename ) 109 | if not compilation_info: 110 | return None 111 | 112 | final_flags = MakeRelativePathsInFlagsAbsolute( 113 | compilation_info.compiler_flags_, 114 | compilation_info.compiler_working_dir_ ) 115 | 116 | else: 117 | relative_to = DirectoryOfThisScript() 118 | final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to ) 119 | 120 | return { 121 | 'flags': final_flags, 122 | 'do_cache': True 123 | } 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jesse 'Jeaye' Wilkerson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile.in: -------------------------------------------------------------------------------- 1 | # Sources 2 | OUT_DIR=bin/ 3 | SOURCES= \ 4 | example/main.cpp \ 5 | example/output.cpp 6 | OBJECTS=$(SOURCES:.cpp=.cpp.o) 7 | TEST_OBJECTS=$(SOURCES:.cpp=.test) 8 | TESTS=$(foreach test,$(SOURCES), $(addprefix $(OUT_DIR), $(notdir $(test)))) 9 | 10 | # Generation/Installation 11 | PREFIX=%PREFIX% 12 | INCLUDEDIR=%INCLUDEDIR% 13 | THREADS=%THREADS% 14 | 15 | # Compiler flags 16 | CXX=c++ 17 | CXX_WARN_FLAGS=-Wall -Wextra -Werror -pedantic 18 | CXX_DEBUG_FLAGS= 19 | CXX_OPTIM_FLAGS=-O3 20 | CXX_INCLUDE_FLAGS=-Iinclude/ 21 | CXX_PLATFORM_FLAGS=%CXX_PLATFORM_FLAGS% 22 | CXX_FLAGS=-std=c++1y \ 23 | $(CXX_PLATFORM_FLAGS) \ 24 | $(CXX_INCLUDE_FLAGS) \ 25 | $(CXX_WARN_FLAGS) \ 26 | $(CXX_DEBUG_FLAGS) \ 27 | $(CXX_OPTIM_FLAGS) 28 | 29 | LD_PLATFORM_LIBS=%LD_PLATFORM_LIBS% 30 | LD_LIBS=$(LD_PLATFORM_LIBS) 31 | 32 | PROJECT=%PROJECT% 33 | 34 | .PHONY: all threaded setup clean ${PROJECT} ${PROJECT}_setup install uninstall test test_setup 35 | 36 | .SILENT: 37 | 38 | all: 39 | $(MAKE) -j $(THREADS) threaded 40 | exit $$? 41 | 42 | threaded: ${PROJECT} 43 | echo "Success" 44 | 45 | setup: clean 46 | echo "Preparing" 47 | mkdir -p $(OUT_DIR) 48 | 49 | clean: 50 | rm -rf $(OUT_DIR) 51 | echo "Cleaned" 52 | 53 | ${PROJECT}: ${PROJECT}_setup $(OBJECTS) 54 | echo "Done building ${PROJECT} tests" 55 | 56 | ${PROJECT}_setup: setup 57 | echo "Building ${PROJECT} tests" 58 | 59 | %.cpp.o: %.cpp ${PROJECT}_setup 60 | echo " Compiling $<" 61 | $(CXX) $(CXX_FLAGS) $< $(LD_LIBS) -o $(addprefix $(OUT_DIR), $(notdir $<)) 62 | 63 | install: 64 | export installdir=$(DESTDIR)$(INCLUDEDIR) && \ 65 | export project=$(PROJECT) && \ 66 | ./do_install safe 67 | 68 | uninstall: 69 | export installdir=$(DESTDIR)$(INCLUDEDIR) && \ 70 | export project=$(PROJECT) && \ 71 | ./do_install undo 72 | 73 | test: test_setup $(TEST_OBJECTS) 74 | 75 | test_setup: 76 | 77 | %.test: test_setup 78 | echo 79 | $(OUT_DIR)/$(notdir $(shell echo $@ | sed 's/\.test//')).cpp || true 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jest (C++14 unit test framework) 2 | --- 3 | 4 | jest is a sane and minimal (header only) C++ unit test framework that uses a template-based approach to registering tests. Absolutely no macros are used or needed for test writing and the whole API can be described in the following example: 5 | 6 | ```cpp 7 | #include 8 | 9 | /* Step 1: Define a group type and object. */ 10 | struct ex_1{ }; 11 | using ex_1_group = jest::group; 12 | ex_1_group const ex_1_obj{ "example" }; 13 | 14 | /* Step 2: Specialize for your group and all tests you'd like. */ 15 | namespace jest 16 | { 17 | template <> template <> 18 | void ex_1_group::test<0>() /* Tests are numbered; the order does not matter. */ 19 | { 20 | int const i{}; 21 | float const f{ 3.14 }; 22 | expect(i == f); 23 | } 24 | template <> template <> /* Double template bit here is required. */ 25 | void ex_1_group::test<1>() 26 | { 27 | int const i{}; 28 | float const f{ 3.14 }; 29 | float const f2{ f * 2.0f }; 30 | expect_equal(i, f, f2); /* Variadic; compares all permutations of pairs. */ 31 | } 32 | template <> template <> 33 | void ex_1_group::test<2>() 34 | { fail("woops"); } 35 | template <> template <> 36 | void ex_1_group::test<3>() 37 | { fail(); } 38 | template <> template <> 39 | void ex_1_group::test<4>() 40 | { expect_equal(0, 0.0f, 0x00, 0000, 0b000); } 41 | template <> template <> 42 | void ex_1_group::test<5>() 43 | { expect_almost_equal(3.140000f, 3.140001f); } /* Using combined tolerance. */ 44 | template <> template <> 45 | void ex_1_group::test<28>() /* Test numbers do not need to be sequential. */ 46 | { expect_equal("string", "String"); } 47 | template <> template <> 48 | void ex_1_group::test<29>() 49 | { 50 | expect_exception([] /* Variadic; any number of exception types. */ 51 | { throw std::runtime_error{""}; }); 52 | } 53 | } 54 | 55 | /* Step 3: Create a worker which will run the tests. */ 56 | int main() 57 | { 58 | jest::worker const j{}; 59 | return j(); 60 | } 61 | ``` 62 | Possible output: 63 | ``` 64 | running group 'example' 65 | test 0 failure: failed 'unexpected' (false) 66 | test 1 failure: failed 'not equal' (0, 3.14) 67 | test 2 failure: failed 'explicit fail' ("woops") 68 | test 3 failure: failed 'explicit fail' ("") 69 | test 4 success 70 | test 28 failure: failed 'not equal' ("string", "String") 71 | test 29 success 72 | finished group 'example' 73 | 74 | 5/7 test(s) failed 75 | ``` 76 | 77 | ### What the hell is `template <> template <>`? 78 | You're specializing a member function of `jest::group`, which is parameterized on your test type. It also inherits from your test type, giving direct access to your test type's member variables from within `jest::group::test`. 79 | 80 | An example of where I use this group-specific data is for testing the output of certain functions to stdout. Add a `std::stringstream` to the test data, redirect `std::cout` to it, and now you can check its contents for each test. Example: 81 | ```cpp 82 | #include 83 | 84 | #include 85 | #include 86 | 87 | /* My test type is output, which has a stringstream. */ 88 | struct output 89 | { 90 | output() 91 | { std::cout.rdbuf(out.rdbuf()); } 92 | 93 | std::stringstream out; /* This will be accessible in each test. */ 94 | }; 95 | using output_group = jest::group; 96 | output_group const output_obj{ "output" }; 97 | 98 | namespace jest 99 | { 100 | namespace detail 101 | { 102 | void speak() 103 | { std::cout << "BARK"; } 104 | void yell(std::string const &s) 105 | { 106 | std::transform(std::begin(s), std::end(s), 107 | std::ostream_iterator(std::cout), 108 | [](char const c) 109 | { return std::toupper(c); }); 110 | } 111 | } 112 | 113 | template <> template <> 114 | void output_group::test<0>() 115 | { 116 | /* Here, I can access `out`, a member variable of my test type. */ 117 | detail::speak(); 118 | expect_equal(out.str(), "BARK"); 119 | out.str(""); 120 | } 121 | template <> template <> 122 | void output_group::test<1>() 123 | { 124 | detail::yell("testing is the best"); 125 | expect_equal(out.str(), "TESTING IS THE BEST"); 126 | out.str(""); 127 | } 128 | } 129 | 130 | int main() 131 | { 132 | jest::worker const j{}; 133 | return j(); 134 | } 135 | ``` 136 | Possible output: 137 | ``` 138 | running group 'output' 139 | test 0 success 140 | test 1 success 141 | finished group 'output' 142 | 143 | all 2 tests passed 144 | ``` 145 | 146 | ### Installation 147 | Since jest is a header-only library, simply copy over the contents of `include` to your project, or, better yet, add jest as a submodule and introduce `jest/include` to your header search paths. 148 | 149 | Full installation can also be achieved by using `./configure && make install`. See the `configure` script for prefix options. 150 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Re-invoke the configure script 4 | # and keep the output at bay. 5 | if [ "$1" != "quiet" ]; 6 | then 7 | $0 "quiet" $@ > /dev/null 8 | exit $? 9 | fi 10 | shift 11 | 12 | set -o errexit 13 | set -o nounset 14 | 15 | # Determine system 16 | uname_linux=0 17 | uname_cygwin=0 18 | uname_osx=0 19 | uname=$(uname) 20 | if [ "$uname" = "Linux" ]; 21 | then 22 | uname_linux=1 23 | elif [ "$uname" = "Darwin" ]; 24 | then 25 | uname_osx=1 26 | elif [ "$(uname -o)" = "Cygwin" ]; 27 | then 28 | uname_cygwin=1 29 | else 30 | echo "Invalid uname ($uname): unsuppported platform" 1>&2 31 | exit 1 32 | fi 33 | 34 | log() { echo "$@" 1>&2; } 35 | log_start() { printf "$@" 1>&2; } 36 | log_end() { echo "$@" 1>&2; } 37 | log_exit() { echo "$@" 1>&2; exit 1; } 38 | 39 | project=jest 40 | log "Configuring ${project}" 41 | 42 | prefix=/usr/local 43 | includedir="$prefix/include" 44 | threads=4 45 | cxx_platform_flags= 46 | ld_platform_libs= 47 | 48 | # Project-specific flags 49 | 50 | if [ "1" -eq "$uname_linux" ]; 51 | then 52 | log "Platform: Linux" 53 | elif [ "1" -eq "$uname_osx" ]; 54 | then 55 | log "Platform: OS X" 56 | cxx_platform_flags="-stdlib=libc++ -I/opt/local/include" 57 | ld_platform_libs="-lc++" 58 | elif [ "1" -eq "$uname_cygwin" ]; 59 | then 60 | log "Platform: Cygwin (NOT TESTED)" 61 | fi 62 | 63 | function show_help 64 | { 65 | log "Usage: $0 [OPTION...]" 66 | log 67 | log "General:" 68 | log " -h, --help Show this help message" 69 | log " --prefix=[/usr/local] Set installation prefix" 70 | log " --includedir=[/usr/local/include] Set include prefix" 71 | log " --threads=[4] Set number of threads to use" 72 | log 73 | exit 0 74 | } 75 | 76 | # Parse params 77 | for i in "$@" 78 | do 79 | case $i in 80 | --prefix) 81 | shift 82 | prefix="$1" 83 | includedir=$prefix/include 84 | shift 85 | ;; 86 | --prefix=*) 87 | prefix="${i#*=}" 88 | includedir=$prefix/include 89 | shift 90 | ;; 91 | 92 | --includedir) 93 | shift 94 | includedir="$1" 95 | shift 96 | ;; 97 | --includedir=*) 98 | includedir="${i#*=}" 99 | shift 100 | ;; 101 | 102 | --threads) 103 | shift 104 | threads=$1 105 | shift 106 | ;; 107 | --threads=*) 108 | threads=${i#*=} 109 | shift 110 | ;; 111 | 112 | -h) 113 | show_help 114 | ;; 115 | --help*) 116 | show_help 117 | ;; 118 | 119 | *) 120 | # Unknown option 121 | ;; 122 | esac 123 | done 124 | 125 | # Update after params 126 | log 127 | log "Install prefix: $prefix" 128 | log "Install include prefix: $includedir" 129 | log "Compilation threads: $threads" 130 | log 131 | 132 | # Configure the makefile 133 | log_start "Populating Makefile..." 134 | rm -f Makefile 135 | sed "s#%CXX_PLATFORM_FLAGS%#${cxx_platform_flags}#" ./Makefile.in |\ 136 | sed "s#%LD_PLATFORM_LIBS%#${ld_platform_libs}#" |\ 137 | sed "s#%PREFIX%#${prefix}#" |\ 138 | sed "s#%INCLUDEDIR%#${includedir}#" |\ 139 | sed "s#%PROJECT%#${project}#" |\ 140 | sed "s#%THREADS%#${threads}#" > Makefile 141 | log_end " done" 142 | 143 | log "Done configuring ${project}" 144 | 145 | # Describe next steps 146 | log 147 | log "To run tests, use \`make && make test\`" 148 | log "To install headers, use \`make install\` with the appropriate permissions for your prefix" 149 | -------------------------------------------------------------------------------- /do_install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright © 2015 Jesse 'Jeaye' Wilkerson 4 | # See licensing in LICENSE file, or at: 5 | # http://www.opensource.org/licenses/MIT 6 | # 7 | # File: do_install 8 | # Author: Jesse 'Jeaye' Wilkerson 9 | 10 | # XXX: Do not call this script directly: use `make install` 11 | set -o errexit 12 | set -o nounset 13 | shopt -s globstar 14 | 15 | if [[ $# == 0 ]]; 16 | then 17 | echo "error: Use \`make install\` to install" 18 | exit 1 19 | fi 20 | 21 | uninstall=0 22 | if [[ $# == 1 && "$1" = "undo" ]]; 23 | then 24 | uninstall=1 25 | echo "Uninstalling from $installdir" 26 | else 27 | echo "Installing from $PWD to $installdir" 28 | fi 29 | 30 | mkdir -p $installdir 31 | for file in "$PWD"/include/**/*.hpp ; do 32 | out=$installdir/$project/$(echo "$file" | sed "s_^.*${project}/__"); 33 | 34 | if [ $uninstall -eq 1 ]; 35 | then 36 | rm -f "$out"; 37 | else 38 | mkdir -p $(dirname "$out") 39 | install -m 0644 "$file" "$out"; 40 | fi 41 | done 42 | 43 | if [ $uninstall -eq 1 ]; 44 | then 45 | rm -r "$installdir/$project" || true 46 | fi 47 | echo "Done" 48 | -------------------------------------------------------------------------------- /example/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct ex_1{ }; 4 | using ex_1_group = jest::group; 5 | ex_1_group const ex_1_obj{ "example" }; 6 | 7 | namespace jest 8 | { 9 | template <> template <> 10 | void ex_1_group::test<0>() 11 | { 12 | int const i{}; 13 | float const f{ 3.14 }; 14 | expect(i == f); 15 | } 16 | template <> template <> 17 | void ex_1_group::test<1>() 18 | { 19 | int const i{}; 20 | float const f{ 3.14 }; 21 | float const f2{ f * 2.0f }; 22 | expect_equal(i, f, f2); 23 | } 24 | template <> template <> 25 | void ex_1_group::test<2>() 26 | { fail("woops"); } 27 | template <> template <> 28 | void ex_1_group::test<3>() 29 | { fail(); } 30 | template <> template <> 31 | void ex_1_group::test<4>() 32 | { expect_equal(0, 0.0f, 0x00, 0000, 0b000); } 33 | template <> template <> 34 | void ex_1_group::test<5>() 35 | { expect_almost_equal(3.140000f, 3.140001f); } 36 | template <> template <> 37 | void ex_1_group::test<28>() 38 | { expect_equal("string", "String"); } 39 | template <> template <> 40 | void ex_1_group::test<29>() 41 | { 42 | expect_exception([] 43 | { throw std::runtime_error{""}; }); 44 | expect_exception([] 45 | { throw std::logic_error{""}; }); 46 | } 47 | template <> template <> 48 | void ex_1_group::test<30>() 49 | { expect_exception([]{ }); } 50 | template <> template <> 51 | void ex_1_group::test<31>() 52 | { expect_exception([]{ throw 2.0f; }); } 53 | } 54 | 55 | int main() 56 | { 57 | jest::worker const j{}; 58 | return j(); 59 | } 60 | -------------------------------------------------------------------------------- /example/output.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | struct output 7 | { 8 | output() 9 | { std::cout.rdbuf(out.rdbuf()); } 10 | 11 | std::stringstream out; 12 | }; 13 | using output_group = jest::group; 14 | output_group const output_obj{ "output" }; 15 | 16 | namespace jest 17 | { 18 | namespace detail 19 | { 20 | void speak() 21 | { std::cout << "BARK"; } 22 | void yell(std::string const &s) 23 | { 24 | std::transform(std::begin(s), std::end(s), 25 | std::ostream_iterator(std::cout), 26 | [](char const c) 27 | { return std::toupper(c); }); 28 | } 29 | } 30 | 31 | template <> template <> 32 | void output_group::test<0>() 33 | { 34 | detail::speak(); 35 | expect_equal(out.str(), "BARK"); 36 | out.str(""); 37 | } 38 | template <> template <> 39 | void output_group::test<1>() 40 | { 41 | detail::yell("testing is the best"); 42 | expect_equal(out.str(), "TESTING IS THE BEST"); 43 | out.str(""); 44 | } 45 | } 46 | 47 | int main() 48 | { 49 | jest::worker const j{}; 50 | return j(); 51 | } 52 | -------------------------------------------------------------------------------- /include/jest/detail/expect.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace jest 13 | { 14 | namespace detail 15 | { 16 | struct no_exception 17 | { }; 18 | 19 | /* TODO: suffix for types? 'f' for float, 'i' for int */ 20 | template 21 | void render_component(std::stringstream &ss, T const &t) 22 | { ss << t << ", "; } 23 | template <> 24 | inline void render_component(std::stringstream &ss, 25 | std::string const &s) 26 | { ss << "\"" << s << "\", "; } 27 | template 28 | void render_component(std::stringstream &ss, char const (&s)[N]) 29 | { ss << "\"" << s << "\", "; } 30 | template <> 31 | inline void render_component(std::stringstream &ss, 32 | std::nullptr_t const&) 33 | { ss << "nullptr, "; } 34 | 35 | template 36 | void fail(std::string const &msg, Args const &... args) 37 | { 38 | std::stringstream ss; 39 | ss << std::boolalpha; 40 | int const _[]{ (render_component(ss, args), 0)... }; 41 | static_cast(_); 42 | throw std::runtime_error 43 | { 44 | "failed '" + msg + "' (" + 45 | ss.str().substr(0, ss.str().size() - 2) + ")" 46 | }; 47 | } 48 | 49 | template 50 | void expect_equal_impl(C const &comp, size_t const n, 51 | T const &t, Args const &... args) 52 | { 53 | int const _[] 54 | { (comp(t, args), 0)... }; 55 | (void)_; 56 | if(n != sizeof...(Args)) 57 | { expect_equal_impl(comp, n + 1, args..., t); } 58 | } 59 | 60 | template 61 | struct filter_exception; 62 | template <> 63 | struct filter_exception<> 64 | { 65 | void operator ()() const 66 | try 67 | { std::rethrow_exception(std::current_exception()); } 68 | catch(std::exception const &e) 69 | { throw std::runtime_error{ std::string{"unexpected exception: "} + e.what() }; } 70 | }; 71 | template 72 | struct filter_exception 73 | { 74 | void operator ()() const 75 | try 76 | { throw; } 77 | catch(T const &) 78 | { } 79 | catch(...) 80 | { filter_exception{}(); } 81 | }; 82 | } 83 | 84 | template 85 | void expect_exception(std::function const &f) 86 | try 87 | { 88 | f(); 89 | throw detail::no_exception{}; 90 | } 91 | catch(detail::no_exception const &) 92 | { throw std::runtime_error{ "expected exception, none was thrown" }; } 93 | catch(...) 94 | { detail::filter_exception{}(); } 95 | 96 | template 97 | void expect_equal(Args const &... args) 98 | { 99 | detail::expect_equal_impl([](auto const &t1, auto const &t2) 100 | { 101 | if(t1 != t2) 102 | { detail::fail("not equal", t1, t2); } 103 | }, 104 | 0, args...); 105 | } 106 | 107 | template 108 | void expect_not_equal(Args const &... args) 109 | { 110 | detail::expect_equal_impl([](auto const &t1, auto const &t2) 111 | { 112 | if(t1 == t2) 113 | { detail::fail("equal", t1, t2); } 114 | }, 115 | 0, args...); 116 | } 117 | 118 | template 119 | void expect_almost_equal(Args const &... args) 120 | { 121 | detail::expect_equal_impl([](auto const &t1, auto const &t2) 122 | { 123 | using common = std::decay_t>; 124 | common const max 125 | { 126 | std::max({ common{ 1.0 }, std::fabs(common{ t1 }), 127 | std::fabs(common{ t2 }) }) 128 | }; 129 | if(std::fabs(common{ t1 - t2 }) > 0.0000001 * max) 130 | { detail::fail("not almost equal", t1, t2); } 131 | }, 132 | 0, args...); 133 | } 134 | 135 | template 136 | void expect(bool const b) 137 | { 138 | if(!b) 139 | { detail::fail("unexpected", b); } 140 | } 141 | 142 | inline void fail(std::string const &msg) 143 | { detail::fail("explicit fail", msg); } 144 | inline void fail() 145 | { fail(""); } 146 | } 147 | -------------------------------------------------------------------------------- /include/jest/detail/group.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "test.hpp" 9 | 10 | namespace jest 11 | { 12 | namespace detail 13 | { 14 | struct group_concept 15 | { 16 | virtual ~group_concept() = default; 17 | virtual tally_results run() = 0; 18 | }; 19 | 20 | class registrar 21 | { 22 | public: 23 | using vec_t = std::vector>; 24 | using const_iterator = vec_t::const_iterator; 25 | 26 | static registrar& get() 27 | { 28 | static registrar r; 29 | return r; 30 | } 31 | 32 | const_iterator begin() const 33 | { return groups_.begin(); } 34 | const_iterator end() const 35 | { return groups_.end(); } 36 | 37 | void add(group_concept &g) 38 | { groups_.emplace_back(std::ref(g)); } 39 | 40 | private: 41 | vec_t groups_; 42 | }; 43 | } 44 | 45 | template 46 | class group : public Data, public detail::group_concept 47 | { 48 | public: 49 | group() = delete; 50 | group(std::string &&name) 51 | : name_{ std::move(name) } 52 | { detail::registrar::get().add(*this); } 53 | 54 | detail::tally_results run() override 55 | { return detail::run_impl(*this, name_, std::make_index_sequence()); } 56 | 57 | template 58 | void test() 59 | { throw detail::default_test{}; } 60 | 61 | private: 62 | std::string name_; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /include/jest/detail/optional.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* Depending on boost is an ass, but std::optional 4 | * won't be around until next year or so... */ 5 | 6 | #include 7 | 8 | namespace jest 9 | { 10 | namespace detail 11 | { 12 | /* Super simple and not standard-compliant. */ 13 | template 14 | class optional 15 | { 16 | public: 17 | optional() = default; 18 | optional(T &&t) 19 | : valid_{ true }, data_{ std::forward(t) } 20 | { } 21 | 22 | operator bool() const 23 | { return valid_; } 24 | 25 | T& value() 26 | { return data_; } 27 | T const& value() const 28 | { return data_; } 29 | 30 | private: 31 | bool valid_{}; 32 | T data_{}; 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /include/jest/detail/test.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "optional.hpp" 10 | 11 | namespace jest 12 | { 13 | namespace detail 14 | { 15 | using optional_failure = optional>; 16 | 17 | struct default_test 18 | { }; 19 | struct failed_test : std::runtime_error 20 | { using std::runtime_error::runtime_error; }; 21 | 22 | struct tally_results 23 | { size_t const total, failed; }; 24 | 25 | inline void log_failure(size_t const n, std::string const &msg) 26 | { std::cerr << " test " << n << " failure: " << msg << std::endl; } 27 | inline void log_success(size_t const n) 28 | { std::cerr << " test " << n << " success" << std::endl; } 29 | 30 | template 31 | optional_failure test_impl(Group &g) 32 | try 33 | { 34 | try 35 | { 36 | g.template test(); 37 | log_success(TN); 38 | return {{ true }}; 39 | } 40 | catch(std::exception const &e) 41 | { throw failed_test{ e.what() }; } 42 | catch(default_test) 43 | { return {}; } 44 | catch(...) 45 | { throw failed_test{ "unknown exception thrown" }; } 46 | } 47 | catch(failed_test const &e) 48 | { 49 | log_failure(TN, e.what()); 50 | return {{ false }}; 51 | } 52 | 53 | template 54 | tally_results run_impl(Group &g, std::string const &name, 55 | std::integer_sequence) 56 | { 57 | std::cerr << "running group '" + name << "'" << std::endl; 58 | optional_failure const results[]{ test_impl(g)... }; 59 | size_t total{}, failed{}; 60 | for(size_t i{}; i < sizeof...(Ns); ++i) 61 | { 62 | if(results[i]) 63 | { 64 | ++total; 65 | if(!results[i].value().value()) 66 | { ++failed; } 67 | } 68 | } 69 | std::cerr << "finished group '" + name << "'" << std::endl; 70 | return { total, failed }; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /include/jest/jest.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "detail/group.hpp" 7 | #include "detail/expect.hpp" 8 | 9 | namespace jest 10 | { 11 | struct worker 12 | { 13 | worker() = default; 14 | 15 | bool operator ()() const 16 | { 17 | auto ®istrar(detail::registrar::get()); 18 | size_t total{}, failed{}; 19 | for(auto &group : registrar) 20 | { 21 | auto const tally(group.get().run()); 22 | total += tally.total; 23 | failed += tally.failed; 24 | } 25 | if(failed) 26 | { std::cerr << failed << "/" << total << " test(s) failed" << std::endl; } 27 | else 28 | { std::cerr << "all " << total << " tests passed" << std::endl; } 29 | 30 | return failed; 31 | } 32 | }; 33 | } 34 | --------------------------------------------------------------------------------