Commit 6eff7c2b authored by Dominik Charousset's avatar Dominik Charousset Committed by Marian Triebe

Refactor unit testing framework, fix CMake setup

parent 58c573fe
......@@ -279,13 +279,31 @@ add_custom_target(uninstall
# path to caf core & io headers
set(LIBCAF_INCLUDE_DIRS
<<<<<<< HEAD
"${CMAKE_CURRENT_SOURCE_DIR}/libcaf_core"
"${CMAKE_CURRENT_SOURCE_DIR}/libcaf_io")
=======
"${CMAKE_SOURCE_DIR}/libcaf_test"
"${CMAKE_SOURCE_DIR}/libcaf_core"
"${CMAKE_SOURCE_DIR}/libcaf_io")
>>>>>>> Refactor unit testing framework, fix CMake setup
# path to caf opencl headers
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libcaf_opencl/CMakeLists.txt")
set(LIBCAF_INCLUDE_DIRS
"${CMAKE_CURRENT_SOURCE_DIR}/libcaf_opencl/" "${LIBCAF_INCLUDE_DIRS}")
endif()
# enable tests if not disabled
if(NOT CAF_NO_UNIT_TESTS)
enable_testing()
macro(add_unit_tests globstr)
file(GLOB_RECURSE tests "${globstr}")
set(CAF_ALL_UNIT_TESTS ${CAF_ALL_UNIT_TESTS} ${tests})
endmacro()
else()
macro(add_unit_tests globstr)
# do nothing (unit tests disabled)
endmacro()
endif()
# all projects need the headers of the core components
include_directories("${LIBCAF_INCLUDE_DIRS}")
......@@ -294,8 +312,14 @@ include_directories("${LIBCAF_INCLUDE_DIRS}")
# add subprojects #
################################################################################
# build libcaf_test if not being told otherwise
if(NOT CAF_NO_UNIT_TESTS)
#message(STATUS "Enter subdirectory libcaf_test")
#add_subdirectory(libcaf_test)
endif()
# core library
add_subdirectory(libcaf_core)
add_unit_tests("libcaf_core/test/*.cpp")
# set core lib for sub directories
if(NOT CAF_BUILD_STATIC_ONLY)
set(LIBCAF_CORE_LIBRARY libcaf_core_shared)
......@@ -326,15 +350,6 @@ add_dependencies(libcaf_io libcaf_core)
set(LIBCAF_LIBRARIES "${LIBCAF_CORE_LIBRARY}"
"${LIBCAF_IO_LIBRARY}"
"${LIBCAF_OPENCL_LIBRARY}")
# add unit tests if not being told otherwise
if(NOT CAF_NO_UNIT_TESTS)
enable_testing()
add_subdirectory(libcaf_test)
add_dependencies(all_unit_tests libcaf_io)
if(NOT CAF_NO_OPENCL AND EXISTS "${CMAKE_SOURCE_DIR}/libcaf_opencl/CMakeLists.txt")
add_subdirectory(libcaf_opencl/unit_testing)
endif()
endif()
# build examples if not being told otherwise
if(NOT CAF_NO_EXAMPLES)
add_subdirectory(examples)
......@@ -342,7 +357,6 @@ if(NOT CAF_NO_EXAMPLES)
if(NOT CAF_NO_OPENCL
AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libcaf_opencl/CMakeLists.txt")
add_subdirectory(libcaf_opencl/examples)
add_dependencies(opencl_examples libcaf_opencl)
endif()
endif()
# build RIAC if not being told otherwise
......@@ -404,6 +418,47 @@ else()
endif()
################################################################################
# unit tests setup #
################################################################################
if(NOT CAF_NO_UNIT_TESTS)
add_executable(caf-test
libcaf_test/src/caf-test.cpp
libcaf_test/caf/test/unit_test.hpp
libcaf_test/caf/test/unit_test_impl.hpp
${CAF_ALL_UNIT_TESTS})
target_link_libraries(caf-test
${LD_FLAGS}
${LIBCAF_LIBRARIES}
${PTHREAD_LIBRARIES})
add_custom_target(all_unit_tests)
add_dependencies(caf-test all_unit_tests)
# enumerate all test suites.
foreach(test ${CAF_ALL_UNIT_TESTS})
file(STRINGS ${test} contents)
foreach(line ${contents})
if ("${line}" MATCHES "CAF_SUITE (.*)")
string(REGEX REPLACE ".* CAF_SUITE (.*)" "\\1" suite ${line})
list(APPEND suites ${suite})
endif()
endforeach()
endforeach()
list(REMOVE_DUPLICATES suites)
# creates one CMake test per test suite.
macro (make_test suite)
string(REPLACE " " "_" test_name ${suite})
set(caf_test ${EXECUTABLE_OUTPUT_PATH}/caf-test)
add_test(${test_name} ${caf_test} -n -v 5 -s "${suite}" ${ARGN})
endmacro ()
list(LENGTH suites num_suites)
message(STATUS "Found ${num_suites} test suites")
foreach(suite ${suites})
make_test("${suite}")
endforeach ()
endif()
################################################################################
# LateX setup #
################################################################################
......
#include <string>
#include <typeinfo>
#include <iostream>
#include "test.hpp"
/******************************************************************************
* ____ _ _____ *
* / ___| / \ | ___| C++ *
* | | / _ \ | |_ Actor *
* | |___ / ___ \| _| Framework *
* \____/_/ \_|_| *
* *
* Copyright (C) 2011 - 2015 *
* Dominik Charousset <dominik.charousset (at) haw-hamburg.de> *
* *
* Distributed under the terms and conditions of the BSD 3-Clause License or *
* (at your option) under the terms and conditions of the Boost Software *
* License 1.0. See accompanying files LICENSE and LICENSE_ALTERNATIVE. *
* *
* If you did not receive a copy of the license files, see *
* http://opensource.org/licenses/BSD-3-Clause and *
* http://www.boost.org/LICENSE_1_0.txt. *
******************************************************************************/
#include "caf/all.hpp"
#include "caf/scoped_actor.hpp"
#define CAF_SUITE atom
#include "caf/test/unit_test.hpp"
namespace caf {
inline std::ostream& operator<<(std::ostream& out, const atom_value& a) {
return (out << to_string(a));
}
} // namespace caf
#include <string>
using std::cout;
using std::endl;
using std::string;
#include "caf/all.hpp"
using namespace caf;
namespace {
constexpr auto s_foo = atom("FooBar");
using abc_atom = atom_constant<atom("abc")>;
}
using testee = typed_actor<replies_to<abc_atom>::with<int>>;
testee::behavior_type testee_impl(testee::pointer self) {
return {
[=](abc_atom) {
CAF_CHECKPOINT();
self->quit();
return 42;
}
};
}
void test_typed_atom_interface() {
CAF_CHECKPOINT();
scoped_actor self;
auto tst = spawn_typed(testee_impl);
self->sync_send(tst, abc_atom::value).await(
[](int i) {
CAF_CHECK_EQUAL(i, 42);
},
CAF_UNEXPECTED_MSG_CB_REF(self)
);
}
} // namespace <anonymous>
template <atom_value AtomValue, class... Types>
void foo() {
CAF_PRINT("foo(" << static_cast<uint64_t>(AtomValue) << " = "
<< to_string(AtomValue) << ")");
CAF_TEST(basics) {
// check if there are leading bits that distinguish "zzz" and "000 "
CAF_CHECK(atom("zzz") != atom("000 "));
// check if there are leading bits that distinguish "abc" and " abc"
CAF_CHECK(atom("abc") != atom(" abc"));
// 'illegal' characters are mapped to whitespaces
CAF_CHECK_EQUAL(atom(" "), atom("@!?"));
// check to_string impl.
CAF_CHECK_EQUAL(to_string(s_foo), "FooBar");
}
struct send_to_self {
......@@ -65,16 +55,7 @@ struct send_to_self {
blocking_actor* m_self;
};
int main() {
CAF_TEST(test_atom);
// check if there are leading bits that distinguish "zzz" and "000 "
CAF_CHECK_NOT_EQUAL(atom("zzz"), atom("000 "));
// check if there are leading bits that distinguish "abc" and " abc"
CAF_CHECK_NOT_EQUAL(atom("abc"), atom(" abc"));
// 'illegal' characters are mapped to whitespaces
CAF_CHECK_EQUAL(atom(" "), atom("@!?"));
// check to_string impl.
CAF_CHECK_EQUAL(to_string(s_foo), "FooBar");
CAF_TEST(receive_atoms) {
scoped_actor self;
send_to_self f{self.get()};
f(atom("foo"), static_cast<uint32_t>(42));
......@@ -83,21 +64,18 @@ int main() {
f(atom("a"), atom("b"), atom("c"), 23.f);
bool matched_pattern[3] = {false, false, false};
int i = 0;
CAF_CHECKPOINT();
CAF_TEST_VERBOSE("start receive loop");
for (i = 0; i < 3; ++i) {
self->receive(
on(atom("foo"), arg_match) >> [&](uint32_t value) {
CAF_CHECKPOINT();
matched_pattern[0] = true;
CAF_CHECK_EQUAL(value, 42);
},
on(atom(":Attach"), atom(":Baz"), arg_match) >> [&](const string& str) {
CAF_CHECKPOINT();
on(atom(":Attach"), atom(":Baz"), arg_match) >> [&](const std::string& str) {
matched_pattern[1] = true;
CAF_CHECK_EQUAL(str, "cstring");
},
on(atom("a"), atom("b"), atom("c"), arg_match) >> [&](float value) {
CAF_CHECKPOINT();
matched_pattern[2] = true;
CAF_CHECK_EQUAL(value, 23.f);
});
......@@ -105,8 +83,12 @@ int main() {
CAF_CHECK(matched_pattern[0] && matched_pattern[1] && matched_pattern[2]);
self->receive(
// "erase" message { atom("b"), atom("a"), atom("c"), 23.f }
others >> CAF_CHECKPOINT_CB(),
after(std::chrono::seconds(0)) >> CAF_UNEXPECTED_TOUT_CB()
others >> [] {
CAF_TEST_VERBOSE("drain mailbox");
},
after(std::chrono::seconds(0)) >> [] {
CAF_TEST_ERROR("mailbox empty");
}
);
atom_value x = atom("abc");
atom_value y = abc_atom::value;
......@@ -114,10 +96,33 @@ int main() {
auto msg = make_message(abc_atom());
self->send(self, msg);
self->receive(
on(atom("abc")) >> CAF_CHECKPOINT_CB(),
others >> CAF_UNEXPECTED_MSG_CB_REF(self)
on(atom("abc")) >> [] {
CAF_TEST_VERBOSE("received 'abc'");
},
others >> [] {
CAF_TEST_ERROR("unexpected message");
}
);
}
using testee = typed_actor<replies_to<abc_atom>::with<int>>;
testee::behavior_type testee_impl(testee::pointer self) {
return {
[=](abc_atom) {
CAF_TEST_VERBOSE("received abc_atom");
self->quit();
return 42;
}
};
}
CAF_TEST(sync_send_atom_constants) {
scoped_actor self;
auto tst = spawn_typed(testee_impl);
self->sync_send(tst, abc_atom::value).await(
[](int i) {
CAF_CHECK_EQUAL(i, 42);
}
);
test_typed_atom_interface();
shutdown();
return CAF_TEST_RESULT();
}
......@@ -17,19 +17,63 @@
* http://www.boost.org/LICENSE_1_0.txt. *
******************************************************************************/
#define CAF_SUITE "example"
#define CAF_SUITE extract
#include "caf/test/unit_test.hpp"
CAF_TEST("foo")
{
CAF_CHECK(true);
CAF_CHECK(!false);
#include <string>
#include <vector>
#include "caf/all.hpp"
using namespace caf;
using std::string;
CAF_TEST(simple_ints) {
auto msg = make_message(1, 2, 3);
auto one = on(1) >> [] { };
auto two = on(2) >> [] { };
auto three = on(3) >> [] { };
auto skip_two = [](int i) -> optional<skip_message_t> {
if (i == 2) {
return skip_message();
}
return none;
};
CAF_CHECK_EQUAL(msg.extract(one), make_message(2, 3));
CAF_CHECK_EQUAL(msg.extract(two), make_message(1, 3));
CAF_CHECK_EQUAL(msg.extract(three), make_message(1, 2));
CAF_CHECK_EQUAL(msg.extract(skip_two), make_message(2));
}
CAF_TEST(type_sequences) {
auto _64 = uint64_t{64};
auto msg = make_message(1.0, 2.f, "str", 42, _64);
auto df = [](double, float) { };
auto fs = [](float, const string&) { };
auto iu = [](int, uint64_t) { };
CAF_CHECK_EQUAL(msg.extract(df), make_message("str", 42, _64));
CAF_CHECK_EQUAL(msg.extract(fs), make_message(1.0, 42, _64));
CAF_CHECK_EQUAL(msg.extract(iu), make_message(1.0, 2.f, "str"));
}
CAF_TEST("bar")
{
auto i = 42;
i *= 2;
CAF_REQUIRE(i == 84);
CAF_TEST(cli_args) {
std::vector<string> args{"-n", "-v", "5", "--out-file=/dev/null"};
int verbosity = 0;
string output_file;
string input_file;
auto res = message_builder(args.begin(), args.end()).extract_opts({
{"no-colors,n", "disable colors"},
{"out-file,o", "redirect output", output_file},
{"in-file,i", "read from file", input_file},
{"verbosity,v", "1-5", verbosity}
});
CAF_CHECK_EQUAL(res.remainder.size(), 0);
CAF_CHECK_EQUAL(to_string(res.remainder), to_string(message{}));
CAF_CHECK_EQUAL(res.opts.count("no-colors"), 1);
CAF_CHECK_EQUAL(res.opts.count("verbosity"), 1);
CAF_CHECK_EQUAL(res.opts.count("out-file"), 1);
CAF_CHECK_EQUAL(res.opts.count("in-file"), 0);
CAF_CHECK_EQUAL(output_file, "/dev/null");
CAF_CHECK_EQUAL(input_file, "");
}
cmake_minimum_required(VERSION 2.8)
project(caf_test C CXX)
file(GLOB LIBCAF_TEST_HDRS "caf/test/*.hpp")
file(GLOB_RECURSE LIBCAF_TEST_SRCS src/unit_test.cpp)
# build shared library if not compiling static only
if (NOT CAF_BUILD_STATIC_ONLY)
add_library(libcaf_test SHARED ${LIBCAF_TEST_SRCS} ${LIBCAF_TEST_HDRS})
target_link_libraries(libcaf_test ${LD_FLAGS})
set(LIBRARY_SOVERSION ${CAF_VERSION_MAJOR})
set_target_properties(libcaf_test
PROPERTIES
SOVERSION ${LIBRARY_SOVERSION}
VERSION ${CAF_VERSION}
OUTPUT_NAME caf_test)
if(NOT WIN32)
install(TARGETS libcaf_test LIBRARY DESTINATION lib)
endif()
endif ()
# build static library only if --build-static or --build-static-only was set
if (CAF_BUILD_STATIC_ONLY OR CAF_BUILD_STATIC)
add_library(libcaf_testStatic STATIC ${LIBCAF_TEST_HDRS} ${LIBCAF_TEST_SRCS})
set_target_properties(libcaf_testStatic
PROPERTIES OUTPUT_NAME caf_test_static)
if(NOT WIN32)
install(TARGETS libcaf_testStatic ARCHIVE DESTINATION lib)
endif()
endif ()
link_directories(${LD_DIRS})
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
# install includes
if(NOT WIN32)
install(DIRECTORY caf/ DESTINATION include/caf FILES_MATCHING PATTERN "*.hpp")
endif()
# The single unit test binary containing all tests.
file(GLOB_RECURSE tests test.cpp test/*.cpp)
add_executable(caf-test ${tests})
target_link_libraries(caf-test
libcaf_test
${LD_FLAGS}
${LIBCAF_CORE_LIBRARY}
${PTHREAD_LIBRARIES})
add_custom_target(all_unit_tests)
add_dependencies(caf-test all_unit_tests)
# Enumerate all test suites.
foreach(test ${tests})
file(STRINGS ${test} contents)
foreach(line ${contents})
if ("${line}" MATCHES "CAF_SUITE(.*)")
string(REGEX REPLACE ".*\"(.*)\".*" "\\1" suite ${line})
list(APPEND suites ${suite})
endif()
endforeach()
endforeach()
list(REMOVE_DUPLICATES suites)
# Creates one CMake test per test suite.
macro (make_test suite)
string(REPLACE " " "_" test_name ${suite})
set(caf_test ${EXECUTABLE_OUTPUT_PATH}/caf-test)
add_test(${test_name} ${caf_test} -n -v 3 -s "${suite}" ${ARGN})
endmacro ()
foreach(suite ${suites})
make_test("${suite}")
endforeach ()
This diff is collapsed.
This diff is collapsed.
#include <cstdlib>
#include "caf/all.hpp"
#include "caf/test/unit_test.hpp"
int main(int argc, char* argv[]) {
using namespace caf;
// Default values.
auto colorize = true;
std::string log_file;
int verbosity_console = 3;
int verbosity_file = 3;
int max_runtime = 10;
std::regex suites{".*"};
std::regex not_suites;
std::regex tests{".*"};
std::regex not_tests;
// Our simple command line parser.
message_handler parser{
on("-n") >> [&] {
colorize = false;
},
on("-l", arg_match) >> [&](const std::string& file_name) {
log_file = file_name;
},
on("-v", arg_match) >> [&](const std::string& n) {
verbosity_console = static_cast<int>(std::strtol(n.c_str(), nullptr, 10));
},
on("-V", arg_match) >> [&](const std::string& n) {
verbosity_file = static_cast<int>(std::strtol(n.c_str(), nullptr, 10));
},
on("-r", arg_match) >> [&](const std::string& n) {
max_runtime = static_cast<int>(std::strtol(n.c_str(), nullptr, 10));
},
on("-s", arg_match) >> [&](const std::string& str) {
suites = str;
},
on("-S", arg_match) >> [&](const std::string& str) {
not_suites = str;
},
on("-t", arg_match) >> [&](const std::string& str) {
tests = str;
},
on("-T", arg_match) >> [&](const std::string& str) {
not_tests = str;
},
others() >> [&] {
std::cerr << "invalid command line argument" << std::endl;
std::exit(1);
}
};
auto is_opt_char = [](char c) {
switch (c) {
default:
return false;
case 'n':
case 'l':
case 'v':
case 'V':
case 'r':
case 's':
case 'S':
case 't':
case 'T':
return true;
};
};
for (int i = 1; i < argc; ++i) {
message_builder builder;
std::string arg{argv[i]};
if (arg.empty() || arg[0] != '-') {
std::cerr << "invalid option: " << arg << std::endl;
return 1;
}
builder.append(std::move(arg));
if (i + 1 < argc) {
auto next = argv[i + 1];
if (*next == '\0' || next[0] != '-' || ! is_opt_char(next[1])) {
builder.append(std::string{next});
++i;
}
}
builder.apply(parser);
}
auto result = caf::test::engine::run(colorize, log_file, verbosity_console,
verbosity_file, max_runtime, suites,
not_suites, tests, not_tests);
return result ? 0 : 1;
}
/******************************************************************************
* ____ _ _____ *
* / ___| / \ | ___| C++ *
* | | / _ \ | |_ Actor *
* | |___ / ___ \| _| Framework *
* \____/_/ \_|_| *
* *
* Copyright (C) 2011 - 2015 *
* Dominik Charousset <dominik.charousset (at) haw-hamburg.de> *
* *
* Distributed under the terms and conditions of the BSD 3-Clause License or *
* (at your option) under the terms and conditions of the Boost Software *
* License 1.0. See accompanying files LICENSE and LICENSE_ALTERNATIVE. *
* *
* If you did not receive a copy of the license files, see *
* http://opensource.org/licenses/BSD-3-Clause and *
* http://www.boost.org/LICENSE_1_0.txt. *
******************************************************************************/
#include <iostream>
#include "ping_pong.hpp"
#define CAF_SUITE "spawn"
#include "caf/all.hpp"
#include "caf/detail/logging.hpp"
#include "caf/test/unit_test.hpp"
using std::cout;
using std::endl;
using namespace caf;
using namespace caf;
namespace {
size_t s_pongs = 0;
behavior ping_behavior(local_actor* self, size_t num_pings) {
return {
on(atom("pong"), arg_match) >> [=](int value)->message {
if (!self->current_sender()) {
CAF_TEST_ERROR("current_sender() invalid!");
}
CAF_TEST_INFO("received {'pong', " << value << "}");
// cout << to_string(self->current_message()) << endl;
if (++s_pongs >= num_pings) {
CAF_TEST_INFO("reached maximum, send {'EXIT', user_defined} "
<< "to last sender and quit with normal reason");
self->send_exit(self->current_sender(),
exit_reason::user_shutdown);
self->quit();
}
return make_message(atom("ping"), value);
},
others() >> [=] {
self->quit(exit_reason::user_shutdown);
}
};
}
behavior pong_behavior(local_actor* self) {
return {
on(atom("ping"), arg_match) >> [](int value)->message {
return make_message(atom("pong"), value + 1);
},
others() >> [=] {
self->quit(exit_reason::user_shutdown);
}
};
}
} // namespace <anonymous>
size_t pongs() { return s_pongs; }
void ping(blocking_actor* self, size_t num_pings) {
s_pongs = 0;
self->receive_loop(ping_behavior(self, num_pings));
}
void event_based_ping(event_based_actor* self, size_t num_pings) {
s_pongs = 0;
self->become(ping_behavior(self, num_pings));
}
void pong(blocking_actor* self, actor ping_actor) {
self->send(ping_actor, atom("pong"), 0); // kickoff
self->receive_loop(pong_behavior(self));
}
void event_based_pong(event_based_actor* self, actor ping_actor) {
CAF_LOGF_TRACE("ping_actor = " << to_string(ping_actor));
CAF_REQUIRE(ping_actor != invalid_actor);
self->send(ping_actor, atom("pong"), 0); // kickoff
self->become(pong_behavior(self));
}
CAF_TEST("ping-pong") {
using namespace caf;
scoped_actor self;
self->trap_exit(true);
auto ping_actor = self->spawn<monitored+blocking_api>(ping, 10);
auto pong_actor = self->spawn<monitored+blocking_api>(pong, ping_actor);
self->link_to(pong_actor);
int i = 0;
int flags = 0;
self->delayed_send(self, std::chrono::seconds(1), atom("FooBar"));
// wait for DOWN and EXIT messages of pong
self->receive_for(i, 4) (
[&](const exit_msg& em) {
CAF_CHECK(em.source == pong_actor);
CAF_CHECK(em.reason == exit_reason::user_shutdown);
flags |= 0x01;
},
[&](const down_msg& dm) {
if (dm.source == pong_actor) {
flags |= 0x02;
CAF_CHECK(dm.reason == exit_reason::user_shutdown);
}
else if (dm.source == ping_actor) {
flags |= 0x04;
CAF_CHECK(dm.reason == exit_reason::normal);
}
},
[&](const atom_value& val) {
CAF_CHECK(val == atom("FooBar"));
flags |= 0x08;
},
others() >> [&]() {
// FIXME: abort test with error message.
//CAF_FAILURE("unexpected message: " << to_string(self->current_message()));
},
after(std::chrono::seconds(5)) >> [&]() {
// FIXME: abort test with error message.
//CAF_FAILURE("timeout in file " << __FILE__ << " in line " << __LINE__);
}
);
// wait for termination of all spawned actors
self->await_all_other_actors_done();
CAF_CHECK(flags == 0x0F);
CAF_CHECK(pongs() == 10);
}
/******************************************************************************
* ____ _ _____ *
* / ___| / \ | ___| C++ *
* | | / _ \ | |_ Actor *
* | |___ / ___ \| _| Framework *
* \____/_/ \_|_| *
* *
* Copyright (C) 2011 - 2015 *
* Dominik Charousset <dominik.charousset (at) haw-hamburg.de> *
* *
* Distributed under the terms and conditions of the BSD 3-Clause License or *
* (at your option) under the terms and conditions of the Boost Software *
* License 1.0. See accompanying files LICENSE and LICENSE_ALTERNATIVE. *
* *
* If you did not receive a copy of the license files, see *
* http://opensource.org/licenses/BSD-3-Clause and *
* http://www.boost.org/LICENSE_1_0.txt. *
******************************************************************************/
#ifndef PING_PONG_HPP
#define PING_PONG_HPP
//#include "caf/actor.hpp"
#include <cstddef>
#include "caf/fwd.hpp"
void ping(caf::blocking_actor*, size_t num_pings);
void event_based_ping(caf::event_based_actor*, size_t num_pings);
void pong(caf::blocking_actor*, caf::actor ping_actor);
// returns the number of messages ping received
size_t pongs();
#endif // PING_PONG_HPP
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment