Xclox
C++11 header-only cross-platform date and time library with an asynchronous NTP client
client.h
1 /*
2  * Copyright (c) 2024 Abdullatif Kalla.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE.txt file in the root directory of this source tree.
6  */
7 
8 #include "xclox/ntp/client.hpp"
9 
10 #include "tools/server.hpp"
11 #include "tools/tracer.hpp"
12 
13 #include "tools/helper.hpp"
14 
15 using namespace xclox::ntp;
16 using namespace std::chrono;
17 
18 TEST_SUITE("Client")
19 {
20  struct Context {
21  Context()
22  : server1(32101, serverTracer1.callable())
23  , server2(32102, serverTracer2.callable())
24  , server3(32103, serverTracer3.callable())
25  , server4(32104, serverTracer4.callable())
26  , server5(32105, serverTracer5.callable())
27  {
28  }
29  Tracer<asio::ip::udp::endpoint, asio::error_code, const uint8_t*, size_t> serverTracer1, serverTracer2, serverTracer3, serverTracer4, serverTracer5;
30  Tracer<std::string, std::string, Client::Status, Packet, steady_clock::duration> clientTracer;
31  Server server1, server2, server3, server4, server5;
32  };
33 
34  TEST_CASE_FIXTURE(Context, "query" * doctest::timeout(6))
35  {
36  Client client(clientTracer.callable());
37  client.query("x.y");
38  client.query("255.255.255.255");
39  client.query("time.windows.com");
40  CHECK(clientTracer.wait(3, seconds(5)) == 3);
41  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
42  return name == "x.y" && address == "" && status == Client::Status::ResolveError && packet.isNull() && rtt == seconds(0);
43  }) == 1);
44  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
45  return name == "255.255.255.255" && address == "255.255.255.255:123" && status == Client::Status::SendError && isClientPacket(packet) && rtt < seconds(1);
46  }) == 1);
47  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
48  return name == "time.windows.com" && !address.empty() && asio::ip::make_address(address.substr(0, address.size() - 4)).is_v4() && status == Client::Status::Succeeded && isServerPacket(packet) && rtt < seconds(5);
49  }) == 1);
50  }
51 
52  TEST_CASE_FIXTURE(Context, "reset callback" * doctest::timeout(3))
53  {
54  Client client(clientTracer.callable());
55  client.query("x.y");
56  CHECK(clientTracer.wait(1) == 1);
57  client.setCallback({});
58  client.query("x.y");
59  CHECK(clientTracer.wait(2) == 1);
60  }
61 
62  TEST_CASE_FIXTURE(Context, "query concurrently" * doctest::timeout(5))
63  {
64  Client client(clientTracer.callable());
65  const std::string& host1 = stringify(server1.endpoint());
66  const std::string& host2 = stringify(server2.endpoint());
67  const std::string& host3 = stringify(server3.endpoint());
68  const int QueryCount = 99;
69  server1.loop(QueryCount);
70  server2.loop(QueryCount);
71  server3.loop(QueryCount);
72  asio::thread_pool pool;
73  for (int i = 0; i < QueryCount; ++i) {
74  asio::post(pool, [&] { client.query(host1); });
75  asio::post(pool, [&] { client.query(host2); });
76  asio::post(pool, [&] { client.query(host3); });
77  }
78  pool.join();
79  CHECK(serverTracer1.wait(QueryCount * 2) == QueryCount * 2);
80  CHECK(serverTracer2.wait(QueryCount * 2) == QueryCount * 2);
81  CHECK(serverTracer3.wait(QueryCount * 2) == QueryCount * 2);
82  CHECK(clientTracer.wait(QueryCount * 3) == QueryCount * 3);
83  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
84  return name == host1 && address == host1 && status == Client::Status::Succeeded && isClientPacket(packet) && rtt < seconds(1);
85  }) == QueryCount);
86  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
87  return name == host2 && address == host2 && status == Client::Status::Succeeded && isClientPacket(packet) && rtt < seconds(1);
88  }) == QueryCount);
89  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
90  return name == host3 && address == host3 && status == Client::Status::Succeeded && isClientPacket(packet) && rtt < seconds(1);
91  }) == QueryCount);
92  }
93 
94  TEST_CASE_FIXTURE(Context, "cancel queries" * doctest::timeout(4))
95  {
96  const std::string& host = stringify(server1.endpoint());
97  Client client(clientTracer.callable());
98  client.cancel();
99  server1.replay(nullptr, 0, milliseconds(100));
100  client.query(host);
101  CHECK(serverTracer1.wait(1) == 1);
102  client.cancel();
103  client.query("255.255.255.255");
104  CHECK(clientTracer.wait(2) == 2);
105  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
106  return name == host && address == "" && status == Client::Status::Cancelled && packet.isNull() && rtt == seconds(0);
107  }) == 1);
108  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
109  return name == "255.255.255.255" && address == "255.255.255.255:123" && status == Client::Status::SendError && isClientPacket(packet) && rtt < seconds(1);
110  }) == 1);
111  client.cancel();
112  client.cancel();
113  server1.replay();
114  client.query(host);
115  CHECK(clientTracer.wait(3) == 3);
116  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
117  return name == host && address == host && status == Client::Status::Succeeded && isClientPacket(packet) && rtt < seconds(1);
118  }) == 1);
119  }
120 
121  TEST_CASE_FIXTURE(Context, "cancel queries concurrently" * doctest::timeout(10))
122  {
123  std::vector<Server*> serverList { &server1, &server2, &server3, &server4, &server5 };
124  const size_t ServerCount = serverList.size();
125  const size_t CancelCount = 99;
126  Client client(clientTracer.callable());
127  asio::thread_pool pool;
128  for (size_t j = 0; j < CancelCount; ++j) {
129  for (size_t i = 0; i < ServerCount; ++i) {
130  serverList.at(i)->replay();
131  client.query(stringify(serverList.at(i)->endpoint()), milliseconds(i));
132  asio::post(pool, [&] { client.cancel(); });
133  }
134  CHECK(clientTracer.wait(ServerCount) == ServerCount);
135  clientTracer.reset();
136  }
137  }
138 
139  TEST_CASE_FIXTURE(Context, "wait all queries upon destruction" * doctest::timeout(11))
140  {
141  server1.replay(nullptr, 0, milliseconds(50));
142  const std::string& host = stringify(server1.endpoint());
143  Client(clientTracer.callable()).query(host);
144  CHECK(clientTracer.wait() == 1);
145  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Client::Status status, const Packet& packet, const steady_clock::duration& rtt) {
146  return name == host && address == host && status == Query::Status::Succeeded && isClientPacket(packet) && compare(rtt, milliseconds(50));
147  }) == 1);
148  }
149 
150  TEST_CASE_FIXTURE(Context, "time out" * doctest::timeout(2))
151  {
152  const std::string& host = stringify(server1.endpoint());
153  for (int i = 0; i < 3; ++i) {
154  const auto& start = steady_clock::now();
155  server1.receive();
156  Client client(clientTracer.callable());
157  client.query(host, milliseconds(i * 100));
158  CHECK(clientTracer.wait() == 1);
159  CHECK(clientTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
160  return name == host && address == "" && status == Query::Status::TimeoutError && packet.isNull() && rtt == seconds(0);
161  }) == 1);
162  CHECK(compare(start, milliseconds(i * 100)));
163  clientTracer.reset();
164  }
165  }
166 } // TEST_SUITE
Client is an asynchronous multi-query NTP client.
Definition: client.hpp:47
void query(const std::string &server, const std::chrono::milliseconds &timeout=std::chrono::milliseconds(DefaultTimeout::ms))
Place a NTP query [thread-safe].
Definition: client.hpp:80
Packet is an immutable raw NTP packet.
Definition: packet.hpp:54
Status
Type of query status.
Definition: query.hpp:55