xrpld
Loading...
Searching...
No Matches
multi_runner.cpp
1#include <test/unit_test/multi_runner.h>
2
3#include <xrpl/beast/unit_test/amount.h>
4#include <xrpl/beast/unit_test/suite_info.h>
5
6#include <boost/container/static_vector.hpp>
7#include <boost/interprocess/creation_tags.hpp>
8#include <boost/interprocess/detail/os_file_functions.hpp>
9#include <boost/interprocess/shared_memory_object.hpp>
10#include <boost/lexical_cast.hpp>
11
12#include <algorithm>
13#include <cassert>
14#include <chrono>
15#include <cstddef>
16#include <cstdlib>
17#include <exception>
18#include <iomanip>
19#include <iostream>
20#include <memory>
21#include <mutex>
22#include <sstream>
23#include <string>
24#include <thread>
25#include <type_traits>
26#include <utility>
27#include <vector>
28
29namespace xrpl {
30
31namespace detail {
32
33std::string
34fmtdur(typename clock_type::duration const& d)
35{
36 using namespace std::chrono;
37 auto const ms = duration_cast<milliseconds>(d);
38 if (ms < seconds{1})
39 return boost::lexical_cast<std::string>(ms.count()) + "ms";
41 ss << std::fixed << std::setprecision(1) << (ms.count() / 1000.) << "s";
42 return ss.str();
43}
44
45//------------------------------------------------------------------------------
46
47void
49{
50 ++cases;
51 total += r.total;
52 failed += r.failed;
53}
54
55//------------------------------------------------------------------------------
56
57void
59{
60 ++suites;
61 total += r.total;
62 cases += r.cases;
63 failed += r.failed;
64 auto const elapsed = clock_type::now() - r.start;
65 if (elapsed >= std::chrono::seconds{1})
66 {
67 // NOLINTNEXTLINE(modernize-use-ranges)
68 auto const iter = std::lower_bound(
69 top.begin(),
70 top.end(),
71 elapsed,
72 [](run_time const& t1, clock_type::duration const& t2) { return t1.second > t2; });
73
74 if (iter != top.end())
75 {
76 if (top.size() == kMaxTop && iter == top.end() - 1)
77 {
78 // avoid invalidating the iterator
79 *iter = run_time{static_string{static_string::string_view_type{r.name}}, elapsed};
80 }
81 else
82 {
83 if (top.size() == kMaxTop)
84 top.resize(top.size() - 1);
85 top.emplace(iter, static_string{static_string::string_view_type{r.name}}, elapsed);
86 }
87 }
88 else if (top.size() < kMaxTop)
89 {
90 top.emplace_back(static_string{static_string::string_view_type{r.name}}, elapsed);
91 }
92 }
93}
94
95void
97{
98 suites += r.suites;
99 total += r.total;
100 cases += r.cases;
101 failed += r.failed;
102
103 // combine the two top collections
104 boost::container::static_vector<run_time, 2 * kMaxTop> topResult;
105 topResult.resize(top.size() + r.top.size());
106 std::ranges::merge(top, r.top, topResult.begin(), [](run_time const& t1, run_time const& t2) {
107 return t1.second > t2.second;
108 });
110 if (topResult.size() > kMaxTop)
111 topResult.resize(kMaxTop);
112
113 top = topResult;
114}
116template <class S>
117void
119{
120 using namespace beast::unit_test;
122 if (!top.empty())
123 {
124 s << "Longest suite times:\n";
125 for (auto const& [name, dur] : top)
126 s << std::setw(8) << fmtdur(dur) << " " << name << '\n';
128
129 auto const elapsed = clock_type::now() - start;
130 s << fmtdur(elapsed) << ", " << Amount{suites, "suite"} << ", " << Amount{cases, "case"} << ", "
131 << Amount{total, "test"} << " total, " << Amount{failed, "failure"} << std::endl;
132}
133
134//------------------------------------------------------------------------------
135
136template <bool IsParent>
142
143template <bool IsParent>
149
150template <bool IsParent>
151bool
154 return anyFailedFlag;
155}
156
157template <bool IsParent>
158void
161 anyFailedFlag = anyFailedFlag || v;
162}
164template <bool IsParent>
167{
168 std::scoped_lock const l{m};
169 return results.total;
170}
171
172template <bool IsParent>
175{
177 return results.suites;
178}
179
180template <bool IsParent>
181void
186
187template <bool IsParent>
193
194template <bool IsParent>
195void
197{
198 std::scoped_lock const l{m};
199 results.merge(r);
200}
201
202template <bool IsParent>
203template <class S>
204void
210
211template <bool IsParent>
213{
214 try
215 {
216 if (IsParent)
217 {
218 // cleanup any leftover state for any previous failed runs
219 boost::interprocess::shared_memory_object::remove(kSharedMemName);
220 boost::interprocess::message_queue::remove(kMessageQueueName);
221 }
222
223 sharedMem_ = boost::interprocess::shared_memory_object{
225 IsParent,
226 boost::interprocess::create_only_t,
227 boost::interprocess::open_only_t>{},
229 boost::interprocess::read_write};
230
231 if (IsParent)
232 {
233 sharedMem_.truncate(sizeof(Inner));
235 boost::interprocess::create_only,
237 /*max messages*/ 16,
238 /*max message size*/ 1 << 20);
239 }
240 else
241 {
243 boost::interprocess::open_only, kMessageQueueName);
244 }
245
246 region_ = boost::interprocess::mapped_region{sharedMem_, boost::interprocess::read_write};
247 if (IsParent)
248 {
249 inner_ = new (region_.get_address()) Inner{};
250 }
251 else
252 {
253 inner_ = reinterpret_cast<Inner*>(region_.get_address());
254 }
255 }
256 catch (...)
257 {
258 if (IsParent)
259 {
260 boost::interprocess::shared_memory_object::remove(kSharedMemName);
261 boost::interprocess::message_queue::remove(kMessageQueueName);
262 }
263 throw;
264 }
265}
266
267template <bool IsParent>
269{
270 if (IsParent)
271 {
272 inner_->~Inner();
273 boost::interprocess::shared_memory_object::remove(kSharedMemName);
274 boost::interprocess::message_queue::remove(kMessageQueueName);
275 }
276}
277
278template <bool IsParent>
281{
282 return inner_->checkoutTestIndex();
283}
284
285template <bool IsParent>
288{
289 return inner_->checkoutJobIndex();
290}
291
292template <bool IsParent>
293bool
295{
296 return inner_->anyFailed();
297}
298
299template <bool IsParent>
300void
302{
303 return inner_->anyFailed(v);
304}
305
306template <bool IsParent>
307void
309{
310 inner_->add(r);
311}
312
313template <bool IsParent>
314void
316{
317 inner_->incKeepAliveCount();
318}
319
320template <bool IsParent>
323{
324 return inner_->getKeepAliveCount();
325}
326
327template <bool IsParent>
328template <class S>
329void
331{
332 inner_->printResults(s);
333}
334
335template <bool IsParent>
336void
338{
339 // must use a mutex since the two "sends" must happen in order
340 std::scoped_lock const l{inner_->m};
341 messageQueue_->send(&mt, sizeof(mt), /*priority*/ 0);
342 messageQueue_->send(s.c_str(), s.size(), /*priority*/ 0);
343}
344
345template <bool IsParent>
348{
349 return inner_->tests();
350}
351
352template <bool IsParent>
355{
356 return inner_->suites();
357}
358
359template <bool IsParent>
360void
362{
363 Results results;
364 results.failed += failures;
365 add(results);
366 anyFailed(failures != 0);
367}
368
369} // namespace detail
370
371namespace test {
372
373//------------------------------------------------------------------------------
374
376{
378 std::vector<char> buf(1 << 20);
379 while (this->continueMessageQueue_ || this->messageQueue_->get_num_msg())
380 {
381 // let children know the parent is still alive
382 this->incKeepAliveCount();
383 if (!this->messageQueue_->get_num_msg())
384 {
385 // If a child does not see the keep alive count incremented,
386 // it will assume the parent has died. This sleep time needs
387 // to be small enough so the child will see increments from
388 // a live parent.
390 continue;
391 }
392 try
393 {
394 std::size_t recvdSize = 0;
395 unsigned int priority = 0;
396 this->messageQueue_->receive(buf.data(), buf.size(), recvdSize, priority);
397 if (!recvdSize)
398 continue;
399 assert(recvdSize == 1);
400 MessageType const mt{*reinterpret_cast<MessageType*>(buf.data())};
401
402 this->messageQueue_->receive(buf.data(), buf.size(), recvdSize, priority);
403 if (recvdSize)
404 {
405 std::string s{buf.data(), recvdSize};
406 switch (mt)
407 {
408 case MessageType::Log:
409 this->os_ << s;
410 this->os_.flush();
411 break;
412 case MessageType::TestStart:
413 runningSuites_.insert(std::move(s));
414 break;
415 case MessageType::TestEnd:
416 runningSuites_.erase(s);
417 break;
418 default:
419 assert(0); // unknown message type
420 }
421 }
422 }
423 catch (std::exception const& e)
424 {
425 std::cerr << "Error: " << e.what() << " reading unit test message queue.\n";
426 return;
427 }
428 catch (...)
429 {
430 std::cerr << "Unknown error reading unit test message queue.\n";
431 return;
432 }
433 }
434 });
435}
436
438{
439 using namespace beast::unit_test;
440
441 continueMessageQueue_ = false;
442 messageQueueThread_.join();
443
445
447
448 for (auto const& s : runningSuites_)
449 {
450 os_ << "\nSuite: " << s << " failed to complete. The child process may have crashed.\n";
451 }
452}
453
454bool
459
465
471
472void
477
478//------------------------------------------------------------------------------
479
480MultiRunnerChild::MultiRunnerChild(std::size_t numJobs, bool quiet, bool printLog)
481 : jobIndex_{checkoutJobIndex()}, numJobs_{numJobs}, quiet_{quiet}, printLog_{!quiet || printLog}
482{
483 if (numJobs_ > 1)
484 {
486 std::size_t lastCount = getKeepAliveCount();
487 while (this->continueKeepAlive_)
488 {
489 // Use a small sleep time so in the normal case the child
490 // process may shutdown quickly. However, to protect against
491 // false alarms, use a longer sleep time later on.
493 auto curCount = this->getKeepAliveCount();
494 if (curCount == lastCount)
495 {
496 // longer sleep time to protect against false alarms
498 curCount = this->getKeepAliveCount();
499 if (curCount == lastCount)
500 {
501 // assume parent process is no longer alive
502 std::cerr << "multi_runner_child " << jobIndex_
503 << ": Assuming parent died, exiting.\n";
504 std::exit(EXIT_FAILURE);
505 }
506 }
507 lastCount = curCount;
508 }
509 });
510 }
511}
512
514{
515 if (numJobs_ > 1)
516 {
517 continueKeepAlive_ = false;
518 keepAliveThread_.join();
519 }
520
521 add(results_);
522}
523
526{
527 return results_.total;
528}
529
532{
533 return results_.suites;
534}
535
536void
538{
539 results_.failed += failures;
540 anyFailed(failures != 0);
541}
542
543void
549
550void
552{
553 if (printLog_ || suiteResults_.failed > 0)
554 {
556 if (numJobs_ > 1)
557 s << jobIndex_ << "> ";
558 s << (suiteResults_.failed > 0 ? "failed: " : "") << suiteResults_.name << " had "
559 << suiteResults_.failed << " failures." << std::endl;
560 messageQueueSend(MessageType::Log, s.str());
561 }
563 messageQueueSend(MessageType::TestEnd, suiteResults_.name);
564}
565
566void
568{
570
571 if (quiet_)
572 return;
573
575 if (numJobs_ > 1)
576 s << jobIndex_ << "> ";
577 s << suiteResults_.name << (caseResults_.name.empty() ? "" : (" " + caseResults_.name)) << '\n';
578 messageQueueSend(MessageType::Log, s.str());
579}
580
581void
586
587void
589{
590 ++caseResults_.total;
591}
592
593void
595{
596 ++caseResults_.failed;
597 ++caseResults_.total;
599 if (numJobs_ > 1)
600 s << jobIndex_ << "> ";
601 s << "#" << caseResults_.total << " failed" << (reason.empty() ? "" : ": ") << reason << '\n';
602 messageQueueSend(MessageType::Log, s.str());
603}
604
605void
607{
608 if (!printLog_)
609 return;
610
612 if (numJobs_ > 1)
613 s << jobIndex_ << "> ";
614 s << msg;
615 messageQueueSend(MessageType::Log, s.str());
616}
617
618} // namespace test
619
620namespace detail {
621template class MultiRunnerBase<true>;
622template class MultiRunnerBase<false>;
623} // namespace detail
624
625} // namespace xrpl
T c_str(T... args)
Utility for producing nicely composed output of amounts with units.
Associates a unit test type with metadata.
Definition suite_info.h:18
std::string fullName() const
Return the canonical suite name as a string.
Definition suite_info.h:72
std::unique_ptr< boost::interprocess::message_queue > messageQueue_
static constexpr char const * kSharedMemName
void addFailures(std::size_t failures)
void add(Results const &r)
boost::interprocess::shared_memory_object sharedMem_
static constexpr char const * kMessageQueueName
boost::interprocess::mapped_region region_
void messageQueueSend(MessageType mt, std::string const &s)
void onSuiteEnd() override
Called when a suite ends.
void onFail(std::string const &reason) override
Called for each failing condition.
void onPass() override
Called for each passing condition.
void onSuiteBegin(beast::unit_test::SuiteInfo const &info) override
Called when a new suite starts.
void onLog(std::string const &s) override
Called when a test logs output.
detail::SuiteResults suiteResults_
void addFailures(std::size_t failures)
detail::CaseResults caseResults_
void onCaseEnd() override
Called when a new case ends.
std::atomic< bool > continueKeepAlive_
void onCaseBegin(std::string const &name) override
Called when a new case starts.
MultiRunnerChild(MultiRunnerChild const &)=delete
void addFailures(std::size_t failures)
std::set< std::string > runningSuites_
std::atomic< bool > continueMessageQueue_
MultiRunnerParent(MultiRunnerParent const &)=delete
T data(T... args)
T duration_cast(T... args)
T empty(T... args)
T endl(T... args)
T exit(T... args)
T fixed(T... args)
T lower_bound(T... args)
T make_unique(T... args)
T merge(T... args)
STL namespace.
std::string fmtdur(std::chrono::duration< Period, Rep > const &d)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
T setprecision(T... args)
T setw(T... args)
T size(T... args)
T sleep_for(T... args)
T str(T... args)
std::atomic< std::size_t > keepAlive
boost::interprocess::interprocess_mutex m
std::atomic< std::size_t > jobIndex
std::atomic< std::size_t > testIndex
static constexpr auto kMaxTop
std::pair< static_string, clock_type::duration > run_time
boost::container::static_vector< run_time, kMaxTop > top
boost::beast::static_string< 256 > static_string
clock_type::time_point start
void merge(Results const &r)
void add(SuiteResults const &r)
clock_type::time_point start
void add(CaseResults const &r)
T what(T... args)