Utopia 2
Framework for studying models of complex & adaptive systems.
Loading...
Searching...
No Matches
Namespaces | Functions
Test Tools

Gathers useful tools for writing tests in the Boost.Test framework. More...

Collaboration diagram for Test Tools:

Namespaces

namespace  Utopia
 
namespace  Utopia::TestTools
 

Functions

template<typename Callable = std::function<void(const DataIO::Config&)>>
void Utopia::TestTools::test_config_callable (Callable &&func, const DataIO::Config &test_cases, const std::string_view context_name="", const LocationInfo loc={})
 Repeatedly invokes a unary function that expects a Config node.
 
template<typename ExcT , typename Callable = std::function<void()>>
void Utopia::TestTools::check_exception (Callable &&func, const std::string_view match="", const LocationInfo loc={})
 Checks if a callable throws with the expected error type and message.
 

Detailed Description

Gathers useful tools for writing tests in the Boost.Test framework.

Have a look at the Utopia::TestTools namespace for more specific information.

Function Documentation

◆ check_exception()

template<typename ExcT , typename Callable = std::function<void()>>
void Utopia::TestTools::check_exception ( Callable &&  func,
const std::string_view  match = "",
const LocationInfo  loc = {} 
)

Checks if a callable throws with the expected error type and message.

An unexpected error type, error message, or lack of throwing an error is reported via BOOST_ERROR.

Note
This goes beyond BOOST_CHECK_EXCEPTION by providing more information on the error message match, which is not easily possible via the BOOST_CHECK_EXCEPTION predicate function.
Template Parameters
ExcTThe expected exception type
Parameters
funcThe callable that is expected to throw the exception. It must take no arguments.
matchIf given, it will be checked whether the error message contains this string. This can not be a regex or glob pattern. Uses contains to perform the check.
locLocation information. If provided, the BOOST_ERROR will include line and file information.
44 {})
45{
46 // Check if Callable can be invoked
47 static_assert(std::is_invocable_v<Callable>,
48 "Callable needs to be invocable without any arguments!");
49
50 // We need to handle the case where ExcT is std::exception separately,
51 // because it produces the -Wexceptions compiler warning (which currently
52 // cannot be pragma-ignored for gcc).
53 if constexpr (std::is_same<ExcT, std::exception>()) {
54 try {
55 func();
56 BOOST_ERROR(loc << "Should have thrown but did not!");
57 }
58 catch (ExcT& e) {
59 if (not match.empty() and not contains(e.what(), match)) {
60 BOOST_ERROR(loc << "Did not throw expected error message!\n"
61 " Expected match : " << match << "\n"
62 " But got : " << e.what() << "\n");
63 }
64 // else: everything ok
65 }
66 catch (...) {
67 BOOST_ERROR(loc << "Threw non-std::exception!");
68 }
69 }
70 else {
71 // For ExcT not being std::exception, also catch std::exception and
72 // supply type information, which can be useful if some other than the
73 // expected type was thrown.
74 try {
75 func();
76 BOOST_ERROR(loc << "Should have thrown but did not!");
77 }
78 catch (ExcT& e) {
79 if (not match.empty() and not contains(e.what(), match)) {
80 BOOST_ERROR(loc << "Did not throw expected error message!\n"
81 " Expected match : " << match << "\n"
82 " But got : " << e.what() << "\n");
83 }
84 // else: everything ok
85 }
86 catch (std::exception& e) {
87 BOOST_ERROR(loc << "Threw error of unexpected type ("
88 << boost::core::demangle(typeid(e).name())
89 << ") with message: " << e.what());
90 }
91 catch (...) {
92 BOOST_ERROR(loc << "Threw non-std::exception!");
93 }
94 }
95}
bool contains(const std::string_view s, const std::string_view match)
Returns true if the match string is contained within the given string.
Definition utils.hh:19

◆ test_config_callable()

template<typename Callable = std::function<void(const DataIO::Config&)>>
void Utopia::TestTools::test_config_callable ( Callable &&  func,
const DataIO::Config test_cases,
const std::string_view  context_name = "",
const LocationInfo  loc = {} 
)

Repeatedly invokes a unary function that expects a Config node.

The parameters with which the function is invoked are specified in a YAML mapping, test_cases. Each case also allows to specify whether the callable will throw an exception; this makes use of the Utopia::TestTools::check_exception method and supports most of the basic exception types.

Each of the test cases has the following form:

my_test_case:
params: {} # passed to callable
throws: true # (optional) If given, this should be the name of
# the expected exception thrown from the callable
match: "foobar" # (optional) If given, this string is expected to
# be found within the thrown exception's error
# message. It can be a substring.

Example YAML configuration for multiple test cases:

---
test_cases:
case1:
# The parameters that are passed to the callable
params: {foo: bar, some_number: 42}
case1_but_failing:
params: {foo: bar, some_number: -1}
# With these parameters, the callable is expected to throw:
throws: std::invalid_argument
case1_but_failing_with_match:
params: {foo: bar, some_number: -1}
throws: std::invalid_argument
# Can optionally also define a string that is meant to be contained
# in the full error message string.
match: "Expected a positive number but got: -1"
# More test cases ...
case2:
params: {foo: spam, some_number: 23}
case2_KeyError:
params: {some_number: 23}
throws: Utopia::KeyError
# Other, unrelated config parameters here
# ...

For the test_cases maping exemplified above, this can be invoked via:

[](auto cfg){
auto foo = get_as<std::string>("foo", cfg);
auto some_number = get_as<int>("some_number", cfg);
// Can do more tests here. Ideally, use BOOST_TEST( ... )
// ...
}, cfg["test_cases"], "My test cases", {__LINE__, __FILE__}
);
Container select_entities(const Manager &mngr, const DataIO::Config &sel_cfg)
Select entities according to parameters specified in a configuration.
Definition select.hh:213
void test_config_callable(Callable &&func, const DataIO::Config &test_cases, const std::string_view context_name="", const LocationInfo loc={})
Repeatedly invokes a unary function that expects a Config node.
Definition config.hh:109
Parameters
funcThe callable to repeatedly invoke. This must be a unary function that accepts a Config node.
test_casesA YAML::Node mapping that contains as keys the test cases and as value another mapping. The params key within each test case is passed to the callable. The throws and match keys are optional and control the exception handling.
context_nameA name that is added to the BOOST_TEST_CONTEXT which is created for each test case. The context helps to debug failing test cases, as it provides all parameters that were passed to the callable.
locOptional location information that can help tracing back where this is invoked from.
112 {})
113{
114 // Check if callable can be invoked
115 static_assert(std::is_invocable_v<Callable, DataIO::Config&>,
116 "Callable requires DataIO::Config as its only argument!");
117
118 // Iterate over multiple configuration cases
119 for (const auto& kv_pair : test_cases) {
120 // Extract name of the case that is to be tested and enter context
121 const auto case_name = kv_pair.first.template as<std::string>();
122 const auto case_cfg = kv_pair.second;
123
124 BOOST_TEST_CONTEXT(loc << context_name
125 << " -- Testing case '" << case_name
126 << "' ... with the following parameters:\n\n"
127 << case_cfg << "\n")
128 {
129 // Check if it this call is expected to throw; if so, check that it
130 // throws the expected error message
131 if (case_cfg["throws"]) {
132 // Bind the config to a new callable (which accepts no arguments)
133 const auto to_test = [func, case_cfg](){
134 func(case_cfg["params"]);
135 };
136
137 const auto exc_type = get_as<std::string>("throws", case_cfg);
138 const auto match = get_as<std::string>("match", case_cfg, "");
139
140 // Check the exception type
141 // ... have to define this manually for all types ...
142 if (exc_type == "std::exception") {
143 check_exception<std::exception>(to_test, match, loc);
144 }
145 else if (exc_type == "std::logic_error") {
146 check_exception<std::logic_error>(to_test, match, loc);
147 }
148 else if (exc_type == "std::invalid_argument") {
149 check_exception<std::invalid_argument>(to_test, match, loc);
150 }
151 else if (exc_type == "std::domain_error") {
152 check_exception<std::domain_error>(to_test, match, loc);
153 }
154 else if (exc_type == "std::length_error") {
155 check_exception<std::length_error>(to_test, match, loc);
156 }
157 else if (exc_type == "std::out_of_range") {
158 check_exception<std::out_of_range>(to_test, match, loc);
159 }
160 else if (exc_type == "std::runtime_error") {
161 check_exception<std::runtime_error>(to_test, match, loc);
162 }
163 else if (exc_type == "std::range_error") {
164 check_exception<std::range_error>(to_test, match, loc);
165 }
166 else if (exc_type == "std::overflow_error") {
167 check_exception<std::overflow_error>(to_test, match, loc);
168 }
169 else if (exc_type == "std::underflow_error") {
170 check_exception<std::underflow_error>(to_test, match, loc);
171 }
172 else if (exc_type == "Utopia::KeyError") {
173 check_exception<Utopia::KeyError>(to_test, match, loc);
174 }
175 else if (exc_type == "Utopia::Exception") {
176 check_exception<Utopia::Exception>(to_test, match, loc);
177 }
178 else if (exc_type == "YAML::Exception") {
179 check_exception<YAML::Exception>(to_test, match, loc);
180 }
181 else {
182 BOOST_ERROR(
183 "Invalid exception type '" << exc_type << "' given in "
184 "`throws` argument! Supported exception types are: "
185 "std::exception, std::logic_error, std::invalid_argument, "
186 "std::domain_error, std::length_error, std::out_of_range, "
187 "std::runtime_error, std::range_error, "
188 "std::overflow_error, std::underflow_error, "
189 "Utopia::KeyError, Utopia::Exception, "
190 "and YAML::Exception."
191 );
192 }
193
194 // No need to test anything else; continue in outer loop
195 continue;
196 }
197
198 // NOT expected to throw. Invoke the test callable with its params...
199 try {
200 func(case_cfg["params"]);
201 }
202 catch (std::exception& e) {
203 BOOST_ERROR(loc << "Unexpectedly threw an error ("
204 << boost::core::demangle(typeid(e).name())
205 << ") with message: " << e.what());
206 }
207 catch (...) {
208 BOOST_ERROR(loc << "Unexpectedly threw a non-std error!");
209 }
210
211 } // End of test context
212 }
213}