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
11#include <algorithm>
12#include <cassert>
13#include <chrono>
14#include <cstddef>
15#include <cstdlib>
16#include <exception>
17#include <iomanip>
18#include <iostream>
19#include <memory>
20#include <mutex>
21#include <sstream>
22#include <string>
23#include <thread>
24#include <type_traits>
25#include <utility>
26#include <vector>
27
28namespace xrpl {
29
30namespace detail {
31
32std::string
33fmtdur(typename ClockType::duration const& d)
34{
35 using namespace std::chrono;
36 auto const ms = duration_cast<milliseconds>(d);
37 if (ms < seconds{1})
38 return std::to_string(ms.count()) + "ms";
40 ss << std::fixed << std::setprecision(1) << (ms.count() / 1000.) << "s";
41 return ss.str();
42}
43
44//------------------------------------------------------------------------------
45
46void
48{
49 ++cases;
50 total += r.total;
51 failed += r.failed;
52}
53
54//------------------------------------------------------------------------------
55
56void
58{
59 ++suites;
60 total += r.total;
61 cases += r.cases;
62 failed += r.failed;
63 auto const elapsed = ClockType::now() - r.start;
64 if (elapsed >= std::chrono::seconds{1})
65 {
66 // NOLINTNEXTLINE(modernize-use-ranges)
67 auto const iter = std::lower_bound(
68 top.begin(), top.end(), elapsed, [](RunTime const& t1, ClockType::duration const& t2) {
69 return t1.second > t2;
70 });
71
72 if (iter != top.end())
73 {
74 if (top.size() == kMaxTop && iter == top.end() - 1)
75 {
76 // avoid invalidating the iterator
77 *iter = RunTime{StaticString{StaticString::string_view_type{r.name}}, elapsed};
78 }
79 else
80 {
81 if (top.size() == kMaxTop)
82 top.resize(top.size() - 1);
83 top.emplace(iter, StaticString{StaticString::string_view_type{r.name}}, elapsed);
84 }
85 }
86 else if (top.size() < kMaxTop)
87 {
88 top.emplace_back(StaticString{StaticString::string_view_type{r.name}}, elapsed);
89 }
90 }
91}
92
93void
95{
96 suites += r.suites;
97 total += r.total;
98 cases += r.cases;
99 failed += r.failed;
100
101 // combine the two top collections
102 boost::container::static_vector<RunTime, 2 * kMaxTop> topResult;
103 topResult.resize(top.size() + r.top.size());
104 std::ranges::merge(top, r.top, topResult.begin(), [](RunTime const& t1, RunTime const& t2) {
105 return t1.second > t2.second;
106 });
107
108 if (topResult.size() > kMaxTop)
109 topResult.resize(kMaxTop);
110
111 top = topResult;
112}
113
114template <class S>
115void
117{
118 using namespace beast::unit_test;
119
120 if (!top.empty())
121 {
122 s << "Longest suite times:\n";
123 for (auto const& [name, dur] : top)
124 s << std::setw(8) << fmtdur(dur) << " " << name << '\n';
125 }
127 auto const elapsed = ClockType::now() - start;
128 s << fmtdur(elapsed) << ", " << Amount{suites, "suite"} << ", " << Amount{cases, "case"} << ", "
129 << Amount{total, "test"} << " total, " << Amount{failed, "failure"} << std::endl;
130}
131
132//------------------------------------------------------------------------------
133
134template <bool IsParent>
140
141template <bool IsParent>
147
148template <bool IsParent>
149bool
154
155template <bool IsParent>
156void
159 anyFailedFlag = anyFailedFlag || v;
160}
161
162template <bool IsParent>
166 std::scoped_lock const l{m};
167 return results.total;
169
170template <bool IsParent>
173{
175 return results.suites;
176}
178template <bool IsParent>
179void
182 ++keepAlive;
183}
184
185template <bool IsParent>
191
192template <bool IsParent>
193void
195{
196 std::scoped_lock const l{m};
197 results.merge(r);
198}
199
200template <bool IsParent>
201template <class S>
202void
208
209template <bool IsParent>
211{
212 try
213 {
214 if (IsParent)
215 {
216 // cleanup any leftover state for any previous failed runs
217 boost::interprocess::shared_memory_object::remove(kSharedMemName);
218 boost::interprocess::message_queue::remove(kMessageQueueName);
219 }
220
221 sharedMem_ = boost::interprocess::shared_memory_object{
223 IsParent,
224 boost::interprocess::create_only_t,
225 boost::interprocess::open_only_t>{},
227 boost::interprocess::read_write};
228
229 if (IsParent)
230 {
231 sharedMem_.truncate(sizeof(Inner));
233 boost::interprocess::create_only,
235 /*max messages*/ 16,
236 /*max message size*/ 1 << 20);
237 }
238 else
239 {
241 boost::interprocess::open_only, kMessageQueueName);
242 }
243
244 region_ = boost::interprocess::mapped_region{sharedMem_, boost::interprocess::read_write};
245 if (IsParent)
246 {
247 inner_ = new (region_.get_address()) Inner{};
248 }
249 else
250 {
251 inner_ = reinterpret_cast<Inner*>(region_.get_address());
252 }
253 }
254 catch (...)
255 {
256 if (IsParent)
257 {
258 boost::interprocess::shared_memory_object::remove(kSharedMemName);
259 boost::interprocess::message_queue::remove(kMessageQueueName);
260 }
261 throw;
262 }
263}
264
265template <bool IsParent>
267{
268 if (IsParent)
269 {
270 inner_->~Inner();
271 boost::interprocess::shared_memory_object::remove(kSharedMemName);
272 boost::interprocess::message_queue::remove(kMessageQueueName);
273 }
274}
275
276template <bool IsParent>
279{
280 return inner_->checkoutTestIndex();
281}
282
283template <bool IsParent>
286{
287 return inner_->checkoutJobIndex();
288}
289
290template <bool IsParent>
291bool
293{
294 return inner_->anyFailed();
295}
296
297template <bool IsParent>
298void
300{
301 return inner_->anyFailed(v);
302}
303
304template <bool IsParent>
305void
307{
308 inner_->add(r);
309}
310
311template <bool IsParent>
312void
314{
315 inner_->incKeepAliveCount();
316}
317
318template <bool IsParent>
321{
322 return inner_->getKeepAliveCount();
323}
324
325template <bool IsParent>
326template <class S>
327void
329{
330 inner_->printResults(s);
331}
332
333template <bool IsParent>
334void
336{
337 // must use a mutex since the two "sends" must happen in order
338 std::scoped_lock const l{inner_->m};
339 messageQueue_->send(&mt, sizeof(mt), /*priority*/ 0);
340 messageQueue_->send(s.c_str(), s.size(), /*priority*/ 0);
341}
342
343template <bool IsParent>
346{
347 return inner_->tests();
348}
349
350template <bool IsParent>
353{
354 return inner_->suites();
355}
356
357template <bool IsParent>
358void
360{
361 Results results;
362 results.failed += failures;
363 add(results);
364 anyFailed(failures != 0);
365}
366
367} // namespace detail
368
369namespace test {
370
371//------------------------------------------------------------------------------
372
374{
376 std::vector<char> buf(1 << 20);
377 while (this->continueMessageQueue_ || this->messageQueue_->get_num_msg())
378 {
379 // let children know the parent is still alive
380 this->incKeepAliveCount();
381 if (!this->messageQueue_->get_num_msg())
382 {
383 // If a child does not see the keep alive count incremented,
384 // it will assume the parent has died. This sleep time needs
385 // to be small enough so the child will see increments from
386 // a live parent.
388 continue;
389 }
390 try
391 {
392 std::size_t recvdSize = 0;
393 unsigned int priority = 0;
394 this->messageQueue_->receive(buf.data(), buf.size(), recvdSize, priority);
395 if (!recvdSize)
396 continue;
397 assert(recvdSize == 1);
398 MessageType const mt{*reinterpret_cast<MessageType*>(buf.data())};
399
400 this->messageQueue_->receive(buf.data(), buf.size(), recvdSize, priority);
401 if (recvdSize)
402 {
403 std::string s{buf.data(), recvdSize};
404 switch (mt)
405 {
406 case MessageType::Log:
407 this->os_ << s;
408 this->os_.flush();
409 break;
410 case MessageType::TestStart:
411 runningSuites_.insert(std::move(s));
412 break;
413 case MessageType::TestEnd:
414 runningSuites_.erase(s);
415 break;
416 default:
417 assert(0); // unknown message type
418 }
419 }
420 }
421 catch (std::exception const& e)
422 {
423 std::cerr << "Error: " << e.what() << " reading unit test message queue.\n";
424 return;
425 }
426 catch (...)
427 {
428 std::cerr << "Unknown error reading unit test message queue.\n";
429 return;
430 }
431 }
432 });
433}
434
436{
437 using namespace beast::unit_test;
438
439 continueMessageQueue_ = false;
440 messageQueueThread_.join();
441
443
445
446 for (auto const& s : runningSuites_)
447 {
448 os_ << "\nSuite: " << s << " failed to complete. The child process may have crashed.\n";
449 }
450}
451
452bool
457
463
469
470void
475
476//------------------------------------------------------------------------------
477
478MultiRunnerChild::MultiRunnerChild(std::size_t numJobs, bool quiet, bool printLog)
479 : jobIndex_{checkoutJobIndex()}, numJobs_{numJobs}, quiet_{quiet}, printLog_{!quiet || printLog}
480{
481 if (numJobs_ > 1)
482 {
484 std::size_t lastCount = getKeepAliveCount();
485 while (this->continueKeepAlive_)
486 {
487 // Use a small sleep time so in the normal case the child
488 // process may shutdown quickly. However, to protect against
489 // false alarms, use a longer sleep time later on.
491 auto curCount = this->getKeepAliveCount();
492 if (curCount == lastCount)
493 {
494 // longer sleep time to protect against false alarms
496 curCount = this->getKeepAliveCount();
497 if (curCount == lastCount)
498 {
499 // assume parent process is no longer alive
500 std::cerr << "multi_runner_child " << jobIndex_
501 << ": Assuming parent died, exiting.\n";
502 std::exit(EXIT_FAILURE);
503 }
504 }
505 lastCount = curCount;
506 }
507 });
508 }
509}
510
512{
513 if (numJobs_ > 1)
514 {
515 continueKeepAlive_ = false;
516 keepAliveThread_.join();
517 }
518
519 add(results_);
520}
521
524{
525 return results_.total;
526}
527
530{
531 return results_.suites;
532}
533
534void
536{
537 results_.failed += failures;
538 anyFailed(failures != 0);
539}
540
541void
547
548void
550{
551 if (printLog_ || suiteResults_.failed > 0)
552 {
554 if (numJobs_ > 1)
555 s << jobIndex_ << "> ";
556 s << (suiteResults_.failed > 0 ? "failed: " : "") << suiteResults_.name << " had "
557 << suiteResults_.failed << " failures." << std::endl;
558 messageQueueSend(MessageType::Log, s.str());
559 }
561 messageQueueSend(MessageType::TestEnd, suiteResults_.name);
562}
563
564void
566{
568
569 if (quiet_)
570 return;
571
573 if (numJobs_ > 1)
574 s << jobIndex_ << "> ";
575 s << suiteResults_.name << (caseResults_.name.empty() ? "" : (" " + caseResults_.name)) << '\n';
576 messageQueueSend(MessageType::Log, s.str());
577}
578
579void
584
585void
587{
588 ++caseResults_.total;
589}
590
591void
593{
594 ++caseResults_.failed;
595 ++caseResults_.total;
597 if (numJobs_ > 1)
598 s << jobIndex_ << "> ";
599 s << "#" << caseResults_.total << " failed" << (reason.empty() ? "" : ": ") << reason << '\n';
600 messageQueueSend(MessageType::Log, s.str());
601}
602
603void
605{
606 if (!printLog_)
607 return;
608
610 if (numJobs_ > 1)
611 s << jobIndex_ << "> ";
612 s << msg;
613 messageQueueSend(MessageType::Log, s.str());
614}
615
616} // namespace test
617
618namespace detail {
619template class MultiRunnerBase<true>;
620template class MultiRunnerBase<false>;
621} // namespace detail
622
623} // 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:20
std::string fullName() const
Return the canonical suite name as a string.
Definition suite_info.h:78
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
boost::container::static_vector< RunTime, kMaxTop > top
std::pair< StaticString, ClockType::duration > RunTime
void merge(Results const &r)
boost::beast::static_string< 256 > StaticString
void add(SuiteResults const &r)
ClockType::time_point start
ClockType::time_point start
void add(CaseResults const &r)
T to_string(T... args)
T what(T... args)