diff options
-rw-r--r-- | src/lib/utils/os_utils.cpp | 30 | ||||
-rw-r--r-- | src/lib/utils/os_utils.h | 9 | ||||
-rw-r--r-- | src/lib/utils/thread_utils/info.txt | 3 | ||||
-rw-r--r-- | src/lib/utils/thread_utils/thread_pool.cpp | 103 | ||||
-rw-r--r-- | src/lib/utils/thread_utils/thread_pool.h | 81 | ||||
-rwxr-xr-x | src/scripts/ci_build.py | 7 | ||||
-rw-r--r-- | src/tests/main.cpp | 12 | ||||
-rw-r--r-- | src/tests/test_rng.cpp | 12 | ||||
-rw-r--r-- | src/tests/test_runner.cpp | 131 | ||||
-rw-r--r-- | src/tests/test_runner.h | 3 | ||||
-rw-r--r-- | src/tests/test_thread_utils.cpp | 56 | ||||
-rw-r--r-- | src/tests/tests.cpp | 28 | ||||
-rw-r--r-- | src/tests/tests.h | 63 |
13 files changed, 454 insertions, 84 deletions
diff --git a/src/lib/utils/os_utils.cpp b/src/lib/utils/os_utils.cpp index f64b85c18..71f4f12d4 100644 --- a/src/lib/utils/os_utils.cpp +++ b/src/lib/utils/os_utils.cpp @@ -263,20 +263,9 @@ size_t OS::get_memory_locking_limit() * programs), but small enough that we should not cause problems * even if many processes are mlocking on the same machine. */ - size_t mlock_requested = BOTAN_MLOCK_ALLOCATOR_MAX_LOCKED_KB; + const size_t user_req = read_env_variable_sz("BOTAN_MLOCK_POOL_SIZE", BOTAN_MLOCK_ALLOCATOR_MAX_LOCKED_KB); - /* - * Allow override via env variable - */ - if(const char* env = read_env_variable("BOTAN_MLOCK_POOL_SIZE")) - { - try - { - const size_t user_req = std::stoul(env, nullptr); - mlock_requested = std::min(user_req, mlock_requested); - } - catch(std::exception&) { /* ignore it */ } - } + const size_t mlock_requested = std::min<size_t>(user_req, BOTAN_MLOCK_ALLOCATOR_MAX_LOCKED_KB); if(mlock_requested > 0) { @@ -327,6 +316,21 @@ const char* OS::read_env_variable(const std::string& name) return std::getenv(name.c_str()); } +size_t OS::read_env_variable_sz(const std::string& name, size_t def) + { + if(const char* env = read_env_variable(name)) + { + try + { + const size_t val = std::stoul(env, nullptr); + return val; + } + catch(std::exception&) { /* ignore it */ } + } + + return def; + } + std::vector<void*> OS::allocate_locked_pages(size_t count) { std::vector<void*> result; diff --git a/src/lib/utils/os_utils.h b/src/lib/utils/os_utils.h index 37a8d3a9c..82f3aad04 100644 --- a/src/lib/utils/os_utils.h +++ b/src/lib/utils/os_utils.h @@ -90,6 +90,15 @@ size_t system_page_size(); const char* read_env_variable(const std::string& var_name); /** +* Read the value of an environment variable and convert it to an +* integer. If not set or conversion fails, returns the default value. +* +* If the process seems to be running in a privileged state (such as setuid) +* then always returns nullptr, similiar to glibc's secure_getenv. +*/ +size_t read_env_variable_sz(const std::string& var_name, size_t def_value = 0); + +/** * Request @count pages of RAM which are locked into memory using mlock, * VirtualLock, or some similar OS specific API. Free it with free_locked_pages. * diff --git a/src/lib/utils/thread_utils/info.txt b/src/lib/utils/thread_utils/info.txt index 826a9d734..80ce2d389 100644 --- a/src/lib/utils/thread_utils/info.txt +++ b/src/lib/utils/thread_utils/info.txt @@ -1,10 +1,11 @@ <defines> -THREAD_UTILS -> 20180112 +THREAD_UTILS -> 20190122 </defines> <header:internal> barrier.h semaphore.h +thread_pool.h </header:internal> <os_features> diff --git a/src/lib/utils/thread_utils/thread_pool.cpp b/src/lib/utils/thread_utils/thread_pool.cpp new file mode 100644 index 000000000..4ccefe8dc --- /dev/null +++ b/src/lib/utils/thread_utils/thread_pool.cpp @@ -0,0 +1,103 @@ +/* +* (C) 2019 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include <botan/internal/thread_pool.h> +#include <botan/internal/os_utils.h> +#include <botan/exceptn.h> +#include <thread> + +namespace Botan { + +//static +Thread_Pool& Thread_Pool::global_instance() + { + static Thread_Pool g_thread_pool(OS::read_env_variable_sz("BOTAN_THREAD_POOL_SIZE")); + return g_thread_pool; + } + +Thread_Pool::Thread_Pool(size_t pool_size) + { + if(pool_size == 0) + { + pool_size = std::thread::hardware_concurrency(); + + /* + * For large machines don't create too many threads, unless + * explicitly asked to by the caller. + */ + if(pool_size > 16) + pool_size = 16; + } + + if(pool_size <= 1) + pool_size = 2; + + m_shutdown = false; + + for(size_t i = 0; i != pool_size; ++i) + { + m_workers.push_back(std::thread(&Thread_Pool::worker_thread, this)); + } + } + +void Thread_Pool::shutdown() + { + { + std::unique_lock<std::mutex> lock(m_mutex); + + if(m_shutdown == true) + return; + + m_shutdown = true; + + m_more_tasks.notify_all(); + } + + for(auto&& thread : m_workers) + { + thread.join(); + } + m_workers.clear(); + } + +void Thread_Pool::queue_thunk(std::function<void ()> fn) + { + std::unique_lock<std::mutex> lock(m_mutex); + + if(m_shutdown) + throw Invalid_State("Cannot add work after thread pool has shut down"); + + m_tasks.push_back(fn); + m_more_tasks.notify_one(); + } + +void Thread_Pool::worker_thread() + { + for(;;) + { + std::function<void()> task; + + { + std::unique_lock<std::mutex> lock(m_mutex); + m_more_tasks.wait(lock, [this]{ return m_shutdown || !m_tasks.empty(); }); + + if(m_tasks.empty()) + { + if(m_shutdown) + return; + else + continue; + } + + task = m_tasks.front(); + m_tasks.pop_front(); + } + + task(); + } + } + +} diff --git a/src/lib/utils/thread_utils/thread_pool.h b/src/lib/utils/thread_utils/thread_pool.h new file mode 100644 index 000000000..d48975090 --- /dev/null +++ b/src/lib/utils/thread_utils/thread_pool.h @@ -0,0 +1,81 @@ +/* +* (C) 2019 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_THREAD_POOL_H_ +#define BOTAN_THREAD_POOL_H_ + +#include <botan/types.h> +#include <functional> +#include <deque> +#include <vector> +#include <memory> +#include <utility> +#include <type_traits> +#include <mutex> +#include <thread> +#include <future> +#include <condition_variable> + +namespace Botan { + +class BOTAN_TEST_API Thread_Pool + { + public: + /** + * Return an instance to a shared thread pool + */ + static Thread_Pool& global_instance(); + + /** + * Initialize a thread pool with some number of threads + * @param pool_size number of threads in the pool, if 0 + * then some default value is chosen + */ + Thread_Pool(size_t pool_size = 0); + + ~Thread_Pool() { shutdown(); } + + void shutdown(); + + Thread_Pool(const Thread_Pool&) = delete; + Thread_Pool& operator=(const Thread_Pool&) = delete; + + // Does this work? + Thread_Pool(Thread_Pool&&) = default; + Thread_Pool& operator=(Thread_Pool&&) = default; + + /* + * Enqueue some work + */ + void queue_thunk(std::function<void ()>); + + template<class F, class... Args> + auto run(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> + { + typedef typename std::result_of<F(Args...)>::type return_type; + + auto future_work = std::bind(std::forward<F>(f), std::forward<Args>(args)...); + auto task = std::make_shared<std::packaged_task<return_type ()>>(future_work); + auto future_result = task->get_future(); + queue_thunk([task]() { (*task)(); }); + return future_result; + } + + private: + void worker_thread(); + + // Only touched in constructor and destructor + std::vector<std::thread> m_workers; + + std::mutex m_mutex; + std::condition_variable m_more_tasks; + std::deque<std::function<void ()>> m_tasks; + bool m_shutdown; + }; + +} + +#endif diff --git a/src/scripts/ci_build.py b/src/scripts/ci_build.py index 314ededa5..77ba19662 100755 --- a/src/scripts/ci_build.py +++ b/src/scripts/ci_build.py @@ -22,7 +22,7 @@ def get_concurrency(): try: import multiprocessing - return max(def_concurrency, multiprocessing.cpu_count()) + return multiprocessing.cpu_count() except ImportError: return def_concurrency @@ -48,6 +48,9 @@ def determine_flags(target, target_os, target_cpu, target_cc, cc_bin, ccache, ro test_prefix = [] test_cmd = [os.path.join(root_dir, 'botan-test')] + if target in ['shared', 'static', 'sanitizer', 'gcc4.8', 'cross-i386', 'bsi', 'nist']: + test_cmd += ['--test-threads=%d' % (get_concurrency())] + fast_tests = ['block', 'aead', 'hash', 'stream', 'mac', 'modes', 'kdf', 'hmac_drbg', 'hmac_drbg_unit', 'tls', 'ffi', @@ -146,7 +149,7 @@ def determine_flags(target, target_os, target_cpu, target_cc, cc_bin, ccache, ro cc_bin = 'x86_64-w64-mingw32-g++' flags += ['--cpu=x86_64', '--cc-abi-flags=-static', '--ar-command=x86_64-w64-mingw32-ar', '--without-os-feature=threads'] - test_cmd = [os.path.join(root_dir, 'botan-test.exe')] + test_cmd = [os.path.join(root_dir, 'botan-test.exe')] + test_cmd[1:] # No runtime prefix required for Wine else: # Build everything but restrict what is run diff --git a/src/tests/main.cpp b/src/tests/main.cpp index 995510ec7..3cf7526f0 100644 --- a/src/tests/main.cpp +++ b/src/tests/main.cpp @@ -61,7 +61,7 @@ int main(int argc, char* argv[]) const std::string arg_spec = "botan-test --verbose --help --data-dir= --pkcs11-lib= --provider= " "--log-success --abort-on-first-fail --no-avoid-undefined --skip-tests= " - "--run-long-tests --run-online-tests --test-runs=1 --drbg-seed= " + "--test-threads=1 --run-long-tests --run-online-tests --test-runs=1 --drbg-seed= " "*suites"; Botan_CLI::Argument_Parser parser(arg_spec); @@ -74,6 +74,15 @@ int main(int argc, char* argv[]) return 0; } +#if defined(BOTAN_TARGET_OS_HAS_POSIX1) && defined(BOTAN_TARGET_OS_HAS_THREADS) + // The mlock pool becomes a major contention point when many + // threads are running. + if(parser.get_arg_sz("test-threads") != 1) + { + ::setenv("BOTAN_MLOCK_POOL_SIZE", "0", 1); + } +#endif + const Botan_Tests::Test_Options opts( parser.get_arg_list("suites"), parser.get_arg_list("skip-tests"), @@ -82,6 +91,7 @@ int main(int argc, char* argv[]) parser.get_arg("provider"), parser.get_arg("drbg-seed"), parser.get_arg_sz("test-runs"), + parser.get_arg_sz("test-threads"), parser.flag_set("verbose"), parser.flag_set("log-success"), parser.flag_set("run-online-tests"), diff --git a/src/tests/test_rng.cpp b/src/tests/test_rng.cpp index a7510b650..e62f81623 100644 --- a/src/tests/test_rng.cpp +++ b/src/tests/test_rng.cpp @@ -60,9 +60,19 @@ class Stateful_RNG_Tests : public Test results.push_back(test_broken_entropy_input()); results.push_back(test_check_nonce()); results.push_back(test_prediction_resistance()); - results.push_back(test_fork_safety()); results.push_back(test_randomize_with_ts_input()); results.push_back(test_security_level()); + + /* + * This test uses the library in both parent and child processes. But + * this causes a race with other threads, where if any other test thread + * is holding the mlock pool mutex, it is killed after the fork. Then, + * in the child, any attempt to allocate or free memory will cause a + * deadlock. + */ + if(Test::options().test_threads() == 1) + results.push_back(test_fork_safety()); + return results; } diff --git a/src/tests/test_runner.cpp b/src/tests/test_runner.cpp index e346e9afa..86ceab1f8 100644 --- a/src/tests/test_runner.cpp +++ b/src/tests/test_runner.cpp @@ -12,6 +12,10 @@ #include <botan/loadstor.h> #include <botan/cpuid.h> +#if defined(BOTAN_HAS_THREAD_UTILS) + #include <botan/internal/thread_pool.h> +#endif + namespace Botan_Tests { Test_Runner::Test_Runner(std::ostream& out) : m_output(out) {} @@ -196,7 +200,7 @@ int Test_Runner::run(const Test_Options& opts) Botan_Tests::Test::set_test_rng(std::move(rng)); - const size_t failed = run_tests(req, i, opts.test_runs()); + const size_t failed = run_tests(req, opts.test_threads(), i, opts.test_runs()); if(failed > 0) return static_cast<int>(failed); } @@ -236,9 +240,68 @@ std::string report_out(const std::vector<Botan_Tests::Test::Result>& results, return out.str(); } +std::vector<Test::Result> run_a_test(const std::string& test_name) + { + std::vector<Test::Result> results; + + try + { + if(test_name == "simd_32" && Botan::CPUID::has_simd_32() == false) + { + results.push_back(Test::Result::Note(test_name, "SIMD not available on this platform")); + } + else if(std::unique_ptr<Test> test = Test::get_test(test_name)) + { + std::vector<Test::Result> test_results = test->run(); + results.insert(results.end(), test_results.begin(), test_results.end()); + } + else + { + results.push_back(Test::Result::Note(test_name, "Test missing or unavailable")); + } + } + catch(std::exception& e) + { + results.push_back(Test::Result::Failure(test_name, e.what())); + } + catch(...) + { + results.push_back(Test::Result::Failure(test_name, "unknown exception")); + } + + return results; + } + +std::string test_summary(size_t test_run, size_t tot_test_runs, uint64_t total_ns, + size_t tests_ran, size_t tests_failed) + { + std::ostringstream oss; + + if(test_run == 0 && tot_test_runs == 1) + oss << "Tests"; + else + oss << "Test run " << (1+test_run) << "/" << tot_test_runs; + + oss << " complete ran " << tests_ran << " tests in " + << Botan_Tests::Test::format_time(total_ns) << " "; + + if(tests_failed > 0) + { + oss << tests_failed << " tests failed"; + } + else if(tests_ran > 0) + { + oss << "all tests ok"; + } + + oss << "\n"; + return oss.str(); + } + } size_t Test_Runner::run_tests(const std::vector<std::string>& tests_to_run, + size_t test_threads, size_t test_run, size_t tot_test_runs) { @@ -246,60 +309,48 @@ size_t Test_Runner::run_tests(const std::vector<std::string>& tests_to_run, const uint64_t start_time = Botan_Tests::Test::timestamp(); - for(auto const& test_name : tests_to_run) + if(test_threads != 1) { - output() << test_name << ':' << std::endl; +#if defined(BOTAN_HAS_THREAD_UTILS) + // If 0 then we let thread pool select the count + Botan::Thread_Pool pool(test_threads); - std::vector<Test::Result> results; + std::vector<std::future<std::vector<Test::Result>>> m_fut_results; - try + for(auto const& test_name : tests_to_run) { - if(test_name == "simd_32" && Botan::CPUID::has_simd_32() == false) - { - results.push_back(Test::Result::Note(test_name, "SIMD not available on this platform")); - } - else if(Test* test = Test::get_test(test_name)) - { - std::vector<Test::Result> test_results = test->run(); - results.insert(results.end(), test_results.begin(), test_results.end()); - } - else - { - results.push_back(Test::Result::Note(test_name, "Test missing or unavailable")); - } + m_fut_results.push_back(pool.run(run_a_test, test_name)); } - catch(std::exception& e) - { - results.push_back(Test::Result::Failure(test_name, e.what())); - } - catch(...) + + for(size_t i = 0; i != m_fut_results.size(); ++i) { - results.push_back(Test::Result::Failure(test_name, "unknown exception")); + output() << tests_to_run[i] << ':' << std::endl; + const std::vector<Test::Result> results = m_fut_results[i].get(); + output() << report_out(results, tests_failed, tests_ran) << std::flush; } - output() << report_out(results, tests_failed, tests_ran) << std::flush; - } - - const uint64_t total_ns = Botan_Tests::Test::timestamp() - start_time; + pool.shutdown(); - if(test_run == 0 && tot_test_runs == 1) - output() << "Tests"; - else - output() << "Test run " << (1+test_run) << "/" << tot_test_runs; + const uint64_t total_ns = Botan_Tests::Test::timestamp() - start_time; - output() << " complete ran " << tests_ran << " tests in " - << Botan_Tests::Test::format_time(total_ns) << " "; + output() << test_summary(test_run, tot_test_runs, total_ns, tests_ran, tests_failed); - if(tests_failed > 0) - { - output() << tests_failed << " tests failed"; + return tests_failed; +#else + output() << "Running tests in multiple threads not enabled in this build\n"; +#endif } - else if(tests_ran > 0) + + for(auto const& test_name : tests_to_run) { - output() << "all tests ok"; + output() << test_name << ':' << std::endl; + const std::vector<Test::Result> results = run_a_test(test_name); + output() << report_out(results, tests_failed, tests_ran) << std::flush; } - output() << std::endl; + const uint64_t total_ns = Botan_Tests::Test::timestamp() - start_time; + + output() << test_summary(test_run, tot_test_runs, total_ns, tests_ran, tests_failed); return tests_failed; } diff --git a/src/tests/test_runner.h b/src/tests/test_runner.h index 0bde5cc4f..47d4f1363 100644 --- a/src/tests/test_runner.h +++ b/src/tests/test_runner.h @@ -26,8 +26,9 @@ class Test_Runner final std::ostream& output() const { return m_output; } size_t run_tests(const std::vector<std::string>& tests_to_run, + size_t test_threads, size_t test_run, - const size_t tot_test_runs); + size_t tot_test_runs); std::ostream& m_output; }; diff --git a/src/tests/test_thread_utils.cpp b/src/tests/test_thread_utils.cpp new file mode 100644 index 000000000..2374df860 --- /dev/null +++ b/src/tests/test_thread_utils.cpp @@ -0,0 +1,56 @@ +/* +* (C) 2019 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "tests.h" + +#if defined(BOTAN_TARGET_OS_HAS_THREADS) && defined(BOTAN_HAS_THREAD_UTILS) + +#include <botan/internal/thread_pool.h> +#include <chrono> + +namespace Botan_Tests { + +// TODO test Barrier +// TODO test Semaphore + +namespace { + +Test::Result thread_pool() + { + Test::Result result("Thread_Pool"); + + // Using lots of threads since here the works spend most of the time sleeping + Botan::Thread_Pool pool(16); + + auto sleep_and_return = [](size_t x) -> size_t { + std::this_thread::sleep_for(std::chrono::milliseconds((x*97)%127)); + return x; + }; + + std::vector<std::future<size_t>> futures; + for(size_t i = 0; i != 100; ++i) + { + auto fut = pool.run(sleep_and_return, i); + futures.push_back(std::move(fut)); + } + + for(size_t i = 0; i != futures.size(); ++i) + { + result.test_eq("Expected return value", futures[i].get(), i); + } + + pool.shutdown(); + + return result; + } + +BOTAN_REGISTER_TEST_FN("thread_pool", thread_pool); + +} + +} + +#endif diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp index 2300dfaea..6b44bccd9 100644 --- a/src/tests/tests.cpp +++ b/src/tests/tests.cpp @@ -29,18 +29,6 @@ namespace Botan_Tests { -Test::Registration::Registration(const std::string& name, Test* test) - { - if(Test::global_registry().count(name) == 0) - { - Test::global_registry().insert(std::make_pair(name, std::unique_ptr<Test>(test))); - } - else - { - throw Test_Error("Duplicate registration of test '" + name + "'"); - } - } - void Test::Result::merge(const Result& other) { if(who() != other.who()) @@ -484,9 +472,9 @@ std::string Test::Result::result_string() const // static Test:: functions //static -std::map<std::string, std::unique_ptr<Test>>& Test::global_registry() +std::map<std::string, std::function<Test* ()>>& Test::global_registry() { - static std::map<std::string, std::unique_ptr<Test>> g_test_registry; + static std::map<std::string, std::function<Test* ()>> g_test_registry; return g_test_registry; } @@ -504,12 +492,12 @@ std::set<std::string> Test::registered_tests() } //static -Test* Test::get_test(const std::string& test_name) +std::unique_ptr<Test> Test::get_test(const std::string& test_name) { auto i = Test::global_registry().find(test_name); if(i != Test::global_registry().end()) { - return i->second.get(); + return std::unique_ptr<Test>(i->second()); } return nullptr; } @@ -596,6 +584,14 @@ void Test::set_test_options(const Test_Options& opts) //static void Test::set_test_rng(std::unique_ptr<Botan::RandomNumberGenerator> rng) { +#if defined(BOTAN_TARGET_OS_HAS_THREADS) + if(m_opts.test_threads() != 1) + { + m_test_rng.reset(new Botan::Serialized_RNG(rng.release())); + return; + } +#endif + m_test_rng.reset(rng.release()); } diff --git a/src/tests/tests.h b/src/tests/tests.h index 93e972900..22651ff8c 100644 --- a/src/tests/tests.h +++ b/src/tests/tests.h @@ -60,6 +60,7 @@ class Test_Options const std::string& provider, const std::string& drbg_seed, size_t test_runs, + size_t test_threads, bool verbose, bool log_success, bool run_online_tests, @@ -73,6 +74,7 @@ class Test_Options m_provider(provider), m_drbg_seed(drbg_seed), m_test_runs(test_runs), + m_test_threads(test_threads), m_verbose(verbose), m_log_success(log_success), m_run_online_tests(run_online_tests), @@ -98,6 +100,8 @@ class Test_Options size_t test_runs() const { return m_test_runs; } + size_t test_threads() const { return m_test_threads; } + bool log_success() const { return m_log_success; } bool run_online_tests() const { return m_run_online_tests; } @@ -126,6 +130,7 @@ class Test_Options std::string m_provider; std::string m_drbg_seed; size_t m_test_runs; + size_t m_test_threads; bool m_verbose; bool m_log_success; bool m_run_online_tests; @@ -436,22 +441,30 @@ class Test std::vector<std::string> m_log; }; - class Registration final - { - public: - Registration(const std::string& name, Test* test); - }; - virtual ~Test() = default; virtual std::vector<Test::Result> run() = 0; virtual std::vector<std::string> possible_providers(const std::string&); - static std::map<std::string, std::unique_ptr<Test>>& global_registry(); + template<typename Test_Class> + class Registration + { + public: + Registration(const std::string& name) + { + if(Test::global_registry().count(name) != 0) + throw Test_Error("Duplicate registration of test '" + name + "'"); + + auto maker = []() -> Test* { return new Test_Class; }; + Test::global_registry().insert(std::make_pair(name, maker)); + } + }; + + static std::map<std::string, std::function<Test* ()>>& global_registry(); static std::set<std::string> registered_tests(); - static Test* get_test(const std::string& test_name); + static std::unique_ptr<Test> get_test(const std::string& test_name); static std::string data_file(const std::string& what); @@ -515,7 +528,39 @@ class Test * Register the test with the runner */ #define BOTAN_REGISTER_TEST(type, Test_Class) \ - Test::Registration reg_ ## Test_Class ## _tests(type, new Test_Class) + Test::Registration<Test_Class> reg_ ## Test_Class ## _tests(type) + +typedef Test::Result (*test_fn)(); + +class FnTest : public Test + { + public: + FnTest(test_fn fn) : m_fn(fn) {} + + std::vector<Test::Result> run() override + { + return {m_fn()}; + } + + private: + test_fn m_fn; + }; + +class FnRegistration + { + public: + FnRegistration(const std::string& name, test_fn fn) + { + if(Test::global_registry().count(name) != 0) + throw Test_Error("Duplicate registration of test '" + name + "'"); + + auto maker = [=]() -> Test* { return new FnTest(fn); }; + Test::global_registry().insert(std::make_pair(name, maker)); + } + }; + +#define BOTAN_REGISTER_TEST_FN(test_name, fn_name) \ + FnRegistration reg_ ## fn_name(test_name, fn_name) class VarMap { |