Skip to content

Commit 62cc5ce

Browse files
committed
Add unit tests for the basic tcp server functionality
Unit tests: put helpers in common/ folder, clean up CMake config
1 parent f048715 commit 62cc5ce

File tree

11 files changed

+286
-25
lines changed

11 files changed

+286
-25
lines changed

testing/CMakeLists.txt

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,31 @@ enable_testing()
99
option(LSL_BENCHMARKS "Enable benchmarks in unit tests" OFF)
1010

1111
add_library(catch_main OBJECT catch_main.cpp)
12-
target_compile_features(catch_main PUBLIC cxx_std_11)
12+
target_compile_features(catch_main PUBLIC cxx_std_14)
1313
if(CMAKE_SYSTEM_NAME STREQUAL "Android")
1414
target_link_libraries(catch_main PUBLIC log)
1515
endif()
16+
find_package(Threads REQUIRED)
17+
target_link_libraries(catch_main PUBLIC Threads::Threads)
1618

1719
target_compile_definitions(catch_main PRIVATE LSL_VERSION_INFO="${LSL_VERSION_INFO}")
20+
target_include_directories(catch_main PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty)
1821
if(LSL_BENCHMARKS)
1922
target_compile_definitions(catch_main PUBLIC CATCH_CONFIG_ENABLE_BENCHMARKING)
2023
endif()
21-
target_include_directories(catch_main PUBLIC
24+
25+
add_library(common OBJECT
26+
common/bytecmp.cpp
27+
common/bytecmp.hpp
28+
common/create_streampair.hpp
29+
common/lsltypes.hpp
30+
)
31+
target_compile_features(common PUBLIC cxx_std_14)
32+
target_include_directories(common PUBLIC
2233
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty>
23-
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../src>)
34+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../src>
35+
)
36+
2437

2538
add_executable(lsl_test_exported
2639
test_ext_DataType.cpp
@@ -29,10 +42,8 @@ add_executable(lsl_test_exported
2942
test_ext_streaminfo.cpp
3043
test_ext_time.cpp
3144
)
45+
target_link_libraries(lsl_test_exported PRIVATE lsl common catch_main)
3246

33-
target_link_libraries(lsl_test_exported PRIVATE lsl catch_main Threads::Threads)
34-
35-
find_package(Threads REQUIRED)
3647

3748
add_executable(lsl_test_internal
3849
test_int_inireader.cpp
@@ -43,28 +54,23 @@ add_executable(lsl_test_internal
4354
test_int_samples.cpp
4455
internal/postproc.cpp
4556
internal/serialization_v100.cpp
57+
internal/tcpserver.cpp
4658
)
47-
target_link_libraries(lsl_test_internal PRIVATE lslobj lslboost catch_main)
59+
target_link_libraries(lsl_test_internal PRIVATE lslobj lslboost common catch_main)
4860

4961
if(LSL_BENCHMARKS)
5062
# to get somewhat reproducible performance numbers:
51-
# /usr/bin/time -v testing/lsl_bench_exported --benchmark-samples 100 bounce
52-
# [unix only] | binary | nr. of samples | test name
53-
add_executable(lsl_bench_exported
63+
# /usr/bin/time -v testing/lsl_test_exported --benchmark-samples 100 bounce
64+
# [unix only] | binary | nr. of samples | test name
65+
target_sources(lsl_test_exported PRIVATE
5466
bench_ext_bounce.cpp
5567
bench_ext_common.cpp
5668
bench_ext_pushpull.cpp
5769
)
58-
target_link_libraries(lsl_bench_exported PRIVATE lsl catch_main Threads::Threads)
59-
installLSLApp(lsl_bench_exported)
60-
61-
add_executable(lsl_bench_internal
70+
target_sources(lsl_test_internal PRIVATE
6271
bench_int_sleep.cpp
6372
bench_int_timesync.cpp
6473
)
65-
target_link_libraries(lsl_bench_internal PRIVATE lslobj lslboost catch_main)
66-
target_include_directories(lsl_test_internal PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../src/)
67-
installLSLApp(lsl_bench_internal)
6874
endif()
6975

7076
set(LSL_TESTS lsl_test_exported lsl_test_internal)

testing/bench_ext_bounce.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
#include "helpers.h"
1+
#include "common/create_streampair.hpp"
2+
#include "common/lsltypes.hpp"
23
#include <catch2/catch.hpp>
34
#include <lsl_cpp.h>
45

testing/bench_ext_pushpull.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
#include "helper_type.hpp"
2-
#include "helpers.h"
1+
#include "common/create_streampair.hpp"
2+
#include "common/lsltypes.hpp"
33
#include <atomic>
44
#include <catch2/catch.hpp>
55
#include <list>

testing/bench_int_sleep.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#include <catch2/catch.hpp>
2+
#include <common.h>
23
#include <thread>
3-
#include "../src/common.h"
44

55
TEST_CASE("sleep") {
66
BENCHMARK("sleep1ms") { std::this_thread::sleep_for(std::chrono::milliseconds(1)); };

testing/common/bytecmp.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include "bytecmp.hpp"
2+
#include <algorithm>
3+
#include <catch2/catch.hpp>
4+
#include <cctype>
5+
#include <iomanip>
6+
#include <map>
7+
#include <sstream>
8+
9+
std::string bytes_to_hexstr(const std::string &str) {
10+
const std::map<char, char> shortcuts{{0x07, 'a'}, {0x08, 'b'}, {0x09, 't'}, {0x0a, 'n'},
11+
{0x0b, 'v'}, {0x0c, 'f'}, {0x0d, 'r'}, {0x5c, '\\'}, {0x22, '"'}, {0x27, '\''},
12+
{0x0a, 'n'}};
13+
std::ostringstream out;
14+
out << std::setfill('0');
15+
16+
for (auto it = str.cbegin(); it != str.cend(); ++it) {
17+
char c = *it;
18+
char next = (it + 1) == str.cend() ? '\0' : *(it + 1);
19+
20+
auto scit = shortcuts.find(c);
21+
if (scit != shortcuts.end())
22+
out << '\\' << scit->second;
23+
else if (std::isprint(c))
24+
out << c;
25+
else if (std::isxdigit(next))
26+
out << '\\' << std::oct << std::setw(3) << static_cast<int>(c);
27+
else if (c >= 0 && c < 8)
28+
out << '\\' << std::oct << std::setw(0) << static_cast<int>(c);
29+
else
30+
out << "\\x" << std::hex << std::setw(0) << (static_cast<int>(c) & 0xff);
31+
}
32+
return out.str();
33+
}
34+
35+
void cmp_binstr(const std::string &a, const std::string b) {
36+
CHECK(a.size() == b.size());
37+
const int context_bytes = 8;
38+
auto diff = std::mismatch(a.begin(), a.end(), b.begin(), b.end());
39+
if (diff.first == a.end() && diff.second == b.end()) return;
40+
auto pos = diff.first - a.begin();
41+
if (pos < context_bytes)
42+
REQUIRE(bytes_to_hexstr(a.substr(0, context_bytes)) ==
43+
bytes_to_hexstr(b.substr(0, context_bytes)));
44+
else {
45+
INFO("First mismatch offset: " << pos)
46+
std::string cmp_a = bytes_to_hexstr(a.substr(0, context_bytes)) + " ... " +
47+
bytes_to_hexstr(a.substr(pos - context_bytes, 2 * context_bytes));
48+
std::string cmp_b = bytes_to_hexstr(b.substr(0, context_bytes)) + " ... " +
49+
bytes_to_hexstr(b.substr(pos - context_bytes, 2 * context_bytes));
50+
REQUIRE(cmp_a == cmp_b);
51+
}
52+
}

testing/common/bytecmp.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#pragma once
2+
#include <string>
3+
4+
/// convert a binary string to the equivalent C-escaped string, e.g. "Hello\xaf\xbcWorld\0"
5+
std::string bytes_to_hexstr(const std::string &str);
6+
/// compare two binary strings, printing the location around the first mismatch
7+
void cmp_binstr(const std::string &a, const std::string b);

testing/helpers.h renamed to testing/common/create_streampair.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ struct Streampair {
99
: out_(std::move(out)), in_(std::move(in)) {}
1010
};
1111

12-
static Streampair create_streampair(const lsl::stream_info &info) {
12+
inline Streampair create_streampair(const lsl::stream_info &info) {
1313
lsl::stream_outlet out(info);
1414
auto found_stream_info(lsl::resolve_stream("name", info.name(), 1, 2.0));
1515
if (found_stream_info.empty()) throw std::runtime_error("outlet not found");
File renamed without changes.

testing/internal/tcpserver.cpp

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#include "../common/bytecmp.hpp"
2+
#include "sample.h"
3+
#include "send_buffer.h"
4+
#include "stream_info_impl.h"
5+
#include "tcp_server.h"
6+
#include <asio/read.hpp>
7+
#include <asio/read_until.hpp>
8+
#include <asio/write.hpp>
9+
#include <catch2/catch.hpp>
10+
#include <functional>
11+
#include <sstream>
12+
#include <thread>
13+
14+
using namespace asio::ip;
15+
using err_t = const asio::error_code &;
16+
using sock_t = tcp::socket;
17+
using sock_p = std::shared_ptr<sock_t>;
18+
19+
/// RAII wrapper that takes care of shutting down the IO thread when an exception happens
20+
class tcp_server_wrapper {
21+
std::shared_ptr<lsl::tcp_server> srv;
22+
std::shared_ptr<asio::io_context> srv_ctx;
23+
std::unique_ptr<std::thread> thread;
24+
public:
25+
tcp_server_wrapper(std::shared_ptr<lsl::stream_info_impl> info) {
26+
auto sendbuf = std::make_shared<lsl::send_buffer>(10);
27+
srv_ctx = std::make_shared<asio::io_context>(1);
28+
auto factory =
29+
std::make_shared<lsl::factory>(info->channel_format(), info->channel_count(), 10);
30+
srv = std::make_shared<lsl::tcp_server>(info, srv_ctx, sendbuf, factory, tcp::v4(), 5);
31+
srv->begin_serving();
32+
}
33+
~tcp_server_wrapper() noexcept {
34+
srv->end_serving();
35+
if (thread)
36+
thread->join();
37+
else
38+
srv_ctx->run();
39+
}
40+
void run() {
41+
thread = std::make_unique<std::thread>([this](){ this->srv_ctx->run(); });
42+
}
43+
lsl::tcp_server* operator->() { return srv.get(); }
44+
45+
};
46+
47+
// testpattern timestamp 123456.789
48+
#define TESTPAT_TIMESTAMP "\xc9v\xbe\x9f\f$\xfe@"
49+
// testpattern for two strings
50+
#define TESTPAT_STR "\1\00210\1\3-11"
51+
52+
void send_request(asio::io_context &ctx, const tcp::endpoint &ep, asio::const_buffer request,
53+
std::function<void(sock_p)> write_cb) {
54+
auto sock = std::make_shared<sock_t>(ctx);
55+
sock->async_connect(ep, [=](err_t connect_err) {
56+
INFO(connect_err.message())
57+
REQUIRE(connect_err.value() == 0);
58+
asio::async_write(*sock, request,
59+
[=, expected_bytes = request.size()](err_t write_err, std::size_t sent_bytes) {
60+
INFO("Sent " << sent_bytes << " bytes, outcome: " << write_err.message())
61+
REQUIRE(write_err.value() == 0);
62+
REQUIRE(sent_bytes == expected_bytes);
63+
write_cb(sock);
64+
});
65+
});
66+
}
67+
68+
auto with_read_callback(const char *name, std::function<void(const std::string &)> fun) {
69+
return [name, fun = std::move(fun)](sock_p sock) {
70+
auto buf = std::make_shared<std::string>();
71+
asio::async_read(*sock, asio::dynamic_buffer(*buf),
72+
[sock, buf, name, fun = std::move(fun)](err_t read_err, std::size_t len) {
73+
INFO("Test " << name << "\t– read " << len
74+
<< " bytes, outcome: " << read_err.message())
75+
if (read_err) REQUIRE(read_err == asio::error::eof);
76+
fun(*buf);
77+
});
78+
};
79+
}
80+
81+
void check_streamfeed_100_response(const std::string &res) {
82+
REQUIRE(res.substr(0, 3) == "\x7f\x01\x09");
83+
auto info_len = static_cast<std::size_t>((res[5] << 8) | res[4]);
84+
REQUIRE(res.size() > 6 + info_len);
85+
86+
auto info = lsl::stream_info_impl();
87+
info.from_fullinfo_message(res.substr(6, info_len));
88+
REQUIRE(info.source_id() == "abc123");
89+
90+
// precomputed string pattern for 2 string channels, serialized with Boost.Archive
91+
const char pat_str[] = "\0\0"
92+
"\1\2\b" TESTPAT_TIMESTAMP TESTPAT_STR // first sample
93+
"\1\2\b" TESTPAT_TIMESTAMP TESTPAT_STR; // second, identical sample
94+
const char pat_f32[] = "\0\0\1\2\b" TESTPAT_TIMESTAMP // first sample
95+
"\4\0\0\x80@" // 4 bytes, raw float data
96+
"\4\0\0\xa0\xc0"
97+
"\4\0\0\xc0@"
98+
"\1\2\b" TESTPAT_TIMESTAMP // second sample
99+
"\4\0\0\0@"
100+
"\4\0\0@\xc0"
101+
"\4\0\0\x80@";
102+
const std::string pat = info.channel_format() == cft_string
103+
? std::string(pat_str, sizeof(pat_str) - 1)
104+
: std::string(pat_f32, sizeof(pat_f32) - 1);
105+
cmp_binstr(res.substr(6 + info_len), pat);
106+
}
107+
108+
TEST_CASE("tcpserver", "[network]") {
109+
110+
asio::io_context ctx(1);
111+
112+
auto info = std::make_shared<lsl::stream_info_impl>("TCP_str", "", 2, 4., cft_string, "abc123");
113+
tcp_server_wrapper tcp_server(info);
114+
tcp::endpoint ep(address_v4(0x7f000001), info->v4data_port());
115+
116+
send_request(ctx, ep, asio::buffer("LSL:streamfeed/110 \n\r\n\r\n"),
117+
with_read_callback("basic", [](const std::string &res) {
118+
REQUIRE(res.substr(0, 14) == "LSL/110 200 OK");
119+
auto endofheader = res.find("\r\n\r\n");
120+
REQUIRE(endofheader != std::string::npos);
121+
std::string received_pattern = res.substr(endofheader + 4);
122+
std::string expected = "\2" TESTPAT_TIMESTAMP TESTPAT_STR;
123+
expected += expected;
124+
cmp_binstr(expected, received_pattern);
125+
}));
126+
127+
send_request(ctx, ep, asio::buffer("LSL:streamfeed/199 \nNative-byte-order:4321\r\n\r\n"),
128+
with_read_callback("endian", [](const std::string &res) {
129+
REQUIRE(res.substr(0, 14) == "LSL/110 200 OK");
130+
REQUIRE(res.find("Byte-Order: 4321") != std::string::npos);
131+
}));
132+
133+
send_request(ctx, ep, asio::buffer("LSL:streamfeed\n0 0\r\n"),
134+
with_read_callback("streamfeed 100", check_streamfeed_100_response));
135+
136+
send_request(ctx, ep, asio::buffer("LSL:fullinfo\r\n"),
137+
with_read_callback("fullinfo", [expected = info->to_fullinfo_message()](
138+
const std::string &res) { REQUIRE(res == expected); }));
139+
140+
tcp_server.run();
141+
ctx.run();
142+
}
143+
144+
TEST_CASE("tcpserver_float", "[network]") {
145+
auto srv_ctx = std::make_shared<asio::io_context>(1);
146+
asio::io_context ctx(1);
147+
148+
auto info =
149+
std::make_shared<lsl::stream_info_impl>("TCP_f32", "", 3, 4., cft_float32, "abc123");
150+
tcp_server_wrapper tcp_server(info);
151+
tcp::endpoint ep(address_v4(0x7f000001), info->v4data_port());
152+
153+
send_request(ctx, ep, asio::buffer("LSL:streamfeed/199 \nsupports-subnormals: 0\r\n\r\n"),
154+
with_read_callback("suppress-subnormals", [](const std::string &res) {
155+
REQUIRE(res.substr(0, 14) == "LSL/110 200 OK");
156+
REQUIRE(res.find("Suppress-Subnormals: 1") != std::string::npos);
157+
auto endofheader = res.find("\r\n\r\n");
158+
REQUIRE(endofheader != std::string::npos);
159+
std::string received_pattern = res.substr(endofheader + 4);
160+
const char expected[] = "\2" TESTPAT_TIMESTAMP // sample 1
161+
"\0\0\x80@"
162+
"\0\0\xa0\xc0"
163+
"\0\0\xc0@"
164+
"\2" TESTPAT_TIMESTAMP // sample 2
165+
"\0\0\0@"
166+
"\0\0@\xc0"
167+
"\0\0\x80@";
168+
cmp_binstr(std::string(expected, sizeof(expected) - 1), received_pattern);
169+
}));
170+
171+
send_request(ctx, ep, asio::buffer("LSL:streamfeed/110 \nNative-byte-order:4321\r\n\r\n"),
172+
with_read_callback("endian", [](const std::string &res) {
173+
REQUIRE(res.substr(0, 14) == "LSL/110 200 OK");
174+
REQUIRE(res.find("Byte-Order: 4321") != std::string::npos);
175+
auto endofheader = res.find("\r\n\r\n");
176+
REQUIRE(endofheader != std::string::npos);
177+
std::string received_pattern = res.substr(endofheader + 4);
178+
179+
const char expected[] = "\2" "@\xfe$\f\x9f\xbev\xc9" // sample 1
180+
"@\x80\0\0"
181+
"\xc0\xa0\0\0"
182+
"@\xc0\0\0"
183+
"\2" "@\xfe$\f\x9f\xbev\xc9" // sample 2
184+
"@\0\0\0"
185+
"\xc0@\0\0"
186+
"@\x80\0\0";
187+
cmp_binstr(std::string(expected, sizeof(expected) - 1), received_pattern);
188+
}));
189+
190+
send_request(ctx, ep, asio::buffer("LSL:streamfeed\n0 0\r\n"),
191+
with_read_callback("streamfeed 100", check_streamfeed_100_response));
192+
193+
tcp_server.run();
194+
ctx.run();
195+
}

testing/test_ext_DataType.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
#include "helper_type.hpp"
2-
#include "helpers.h"
1+
#include "common/create_streampair.hpp"
2+
#include "common/lsltypes.hpp"
33
#include <catch2/catch.hpp>
44
#include <cstdint>
55
#include <lsl_cpp.h>

0 commit comments

Comments
 (0)