Xclox
C++11 header-only cross-platform date and time library with an asynchronous NTP client
query.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/query.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("Query")
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, Query::Status, Packet, steady_clock::duration> queryTracer;
31  Server server1, server2, server3, server4, server5;
32  asio::thread_pool pool;
33  };
34 
35  TEST_CASE_FIXTURE(Context, "non-existing domain" * doctest::timeout(2))
36  {
37  const std::string& host = "x.y";
38  Query::start(pool, host, queryTracer.callable());
39  CHECK(queryTracer.wait() == 1);
40  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
41  return name == host && address.empty() && status == Query::Status::ResolveError && packet.isNull() && rtt == seconds(0);
42  }) == 1);
43  }
44 
45  TEST_CASE_FIXTURE(Context, "non-existing server" * doctest::timeout(2))
46  {
47  const std::string& host = "255.255.255.255";
48  Query::start(pool, host, queryTracer.callable());
49  CHECK(queryTracer.wait() == 1);
50  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
51  return name == host && address == host + ":123" && status == Query::Status::SendError && !packet.isNull() && rtt > nanoseconds(0) && compare(rtt, milliseconds(1));
52  }) == 1);
53  }
54 
55  TEST_CASE_FIXTURE(Context, "custom port" * doctest::timeout(2))
56  {
57  SUBCASE("name")
58  {
59  const std::string& host = "255.255.255.255:ntp";
60  Query::start(pool, host, queryTracer.callable());
61  CHECK(queryTracer.wait() == 1);
62  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
63  return name == host && address == "255.255.255.255:123" && status == Query::Status::SendError && !packet.isNull() && rtt > nanoseconds(0) && compare(rtt, milliseconds(1));
64  }) == 1);
65  }
66  SUBCASE("number")
67  {
68  const std::string& host = "255.255.255.255:123";
69  Query::start(pool, host, queryTracer.callable());
70  CHECK(queryTracer.wait() == 1);
71  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
72  return name == host && address == host && status == Query::Status::SendError && !packet.isNull() && rtt > nanoseconds(0) && compare(rtt, milliseconds(1));
73  }) == 1);
74  }
75  }
76 
77  TEST_CASE_FIXTURE(Context, "bogus server" * doctest::timeout(2))
78  {
79  uint8_t data {};
80  server1.replay(&data, 1);
81  const std::string& host = stringify(server1.endpoint());
82  Query::start(pool, host, queryTracer.callable());
83  CHECK(queryTracer.wait() == 1);
84  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
85  return name == host && address == host && status == Query::Status::ReceiveError && packet.isNull() && rtt > nanoseconds(0) && compare(rtt, milliseconds(1));
86  }) == 1);
87  }
88 
89  TEST_CASE_FIXTURE(Context, "success" * doctest::timeout(2))
90  {
91  server1.replay(nullptr, 0, milliseconds(100));
92  const std::string& host = stringify(server1.endpoint());
93  Query::start(pool, host, queryTracer.callable());
94  CHECK(queryTracer.wait() == 1);
95  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
96  return name == host && address == host && status == Query::Status::Succeeded && !packet.isNull() && compare(rtt, milliseconds(100));
97  }) == 1);
98  }
99 
100  TEST_CASE_FIXTURE(Context, "non-blocking" * doctest::timeout(2))
101  {
102  server1.replay(nullptr, 0, milliseconds(200));
103  const std::string& host = stringify(server1.endpoint());
104  const auto& start = steady_clock::now();
105  Query::start(pool, host, queryTracer.callable());
106  CHECK(compare(start, milliseconds(1)));
107  CHECK(queryTracer.wait() == 1);
108  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
109  return name == host && address == host && status == Query::Status::Succeeded && !packet.isNull() && compare(rtt, milliseconds(200));
110  }) == 1);
111  }
112 
113  TEST_CASE_FIXTURE(Context, "traceable" * doctest::timeout(2))
114  {
115  const std::string host = "x.y";
116  auto query = Query::start(pool, host, queryTracer.callable());
117  CHECK_FALSE(query.expired());
118  CHECK(queryTracer.wait() == 1);
119  CHECK(query.expired());
120  }
121 
122  TEST_CASE_FIXTURE(Context, "no callback" * doctest::timeout(1))
123  {
124  auto query = Query::start(pool, "254.254.254.254:1234", {});
125  CHECK(query.expired());
126  }
127 
128  TEST_CASE_FIXTURE(Context, "timeout - lookup" * doctest::timeout(11))
129  {
130  const auto& start = steady_clock::now();
131  const std::string& host = "1234567890";
132  const auto& timeoutMs = 100;
133  auto query = Query::start(pool, host, queryTracer.callable(), milliseconds(timeoutMs));
134  CHECK_FALSE(query.expired());
135  CHECK(queryTracer.wait() == 1);
136  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
137  return name == host && address == "" && status == Query::Status::TimeoutError && packet.isNull() && rtt == seconds(0);
138  }) == 1);
139  CHECK(query.expired());
140  CHECK(compare(start, milliseconds(timeoutMs)));
141  }
142 
143  TEST_CASE_FIXTURE(Context, "timeout - query" * doctest::timeout(2))
144  {
145  for (int i = 0; i < 3; ++i) {
146  server1.receive();
147  const auto& start = steady_clock::now();
148  const std::string& host = stringify(server1.endpoint());
149  auto query = Query::start(pool, host, queryTracer.callable(), milliseconds(i * 100));
150  CHECK_FALSE(query.expired());
151  CHECK(queryTracer.wait() == 1);
152  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
153  return name == host && address == "" && status == Query::Status::TimeoutError && packet.isNull() && rtt == seconds(0);
154  }) == 1);
155  CHECK(compare(start, milliseconds(i * 100)));
156  CHECK(query.expired());
157  queryTracer.reset();
158  }
159  }
160 
161  TEST_CASE_FIXTURE(Context, "cancellable during lookup" * doctest::timeout(11))
162  {
163  const auto& start = steady_clock::now();
164  const std::string& host = "1234567890";
165  auto query = Query::start(pool, host, queryTracer.callable());
166  CHECK_FALSE(query.expired());
167  query.lock()->cancel();
168  CHECK(queryTracer.wait() == 1);
169  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
170  return name == host && address == "" && status == Query::Status::Cancelled && packet.isNull() && rtt == seconds(0);
171  }) == 1);
172  CHECK(query.expired());
173  CHECK(compare(start, milliseconds(1)));
174  }
175 
176  TEST_CASE_FIXTURE(Context, "cancellable during query - multiple times" * doctest::timeout(4))
177  {
178  const auto& start = steady_clock::now();
179  server1.replay(nullptr, 0, milliseconds(400));
180  const std::string& host = stringify(server1.endpoint());
181  auto query = Query::start(pool, host, queryTracer.callable());
182  CHECK(serverTracer1.wait() == 1);
183  CHECK_FALSE(query.expired());
184  for (int i = 0; i < 10; ++i) {
185  asio::post(pool, [query] {
186  if (auto handle = query.lock()) {
187  std::this_thread::sleep_for(milliseconds(10));
188  handle->cancel();
189  }
190  });
191  }
192  CHECK(queryTracer.wait() == 1);
193  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
194  return name == host && address == "" && status == Query::Status::Cancelled && packet.isNull() && rtt == seconds(0);
195  }) == 1);
196  // The query lasts a bit before it expires on Linux
197  std::this_thread::sleep_for(milliseconds(100));
198  CHECK(query.expired());
199  CHECK(serverTracer1.counter() == 1);
200  CHECK(serverTracer1.wait(2) == 2);
201  CHECK(compare(start, milliseconds(400)));
202  }
203 
204  TEST_CASE_FIXTURE(Context, "cancellable concurrently" * doctest::timeout(2))
205  {
206  std::vector<Server*> serverList { &server1, &server2, &server3, &server4, &server5 };
207  const size_t QueryCount = serverList.size();
208  for (size_t j = 0; j < QueryCount; ++j) {
209  for (size_t i = 0; i < QueryCount; ++i) {
210  serverList.at(i)->replay(nullptr, 0, milliseconds(1));
211  auto query = Query::start(pool, stringify(serverList.at(i)->endpoint()), queryTracer.callable(), milliseconds(0));
212  asio::post(pool, [query] {
213  if (auto shared = query.lock()) {
214  shared->cancel();
215  }
216  });
217  }
218  CHECK(queryTracer.wait(QueryCount) == QueryCount);
219  queryTracer.reset();
220  }
221  }
222 
223  TEST_CASE_FIXTURE(Context, "domain name" * doctest::timeout(6))
224  {
225  const std::string& host = "time.windows.com";
226  Query::start(pool, host, queryTracer.callable());
227  CHECK(queryTracer.wait() == 1);
228  CHECK(queryTracer.find([&](const std::string& name, const std::string& address, Query::Status status, const Packet& packet, const steady_clock::duration& rtt) {
229  return name == host && !address.empty() && asio::ip::make_address(address.substr(0, address.size() - 4)).is_v4() && status == Query::Status::Succeeded && isServerPacket(packet) && rtt < seconds(1);
230  }) == 1);
231  }
232 } // TEST_SUITE
Packet is an immutable raw NTP packet.
Definition: packet.hpp:54
static std::weak_ptr< Query > start(asio::thread_pool &pool, const std::string &server, Callback callback, const std::chrono::milliseconds &timeout=std::chrono::milliseconds(DefaultTimeout::ms))
Starts querying all resolved addresses of server one at a time until success.
Definition: query.hpp:87
Status
Type of query status.
Definition: query.hpp:55