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 clock_type::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 = clock_type::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(),
69 top.end(),
70 elapsed,
71 [](run_time const& t1, clock_type::duration const& t2) { return t1.second > t2; });
72
73 if (iter != top.end())
74 {
75 if (top.size() == kMaxTop && iter == top.end() - 1)
76 {
77 // avoid invalidating the iterator
78 *iter = run_time{static_string{static_string::string_view_type{r.name}}, elapsed};
79 }
80 else
81 {
82 if (top.size() == kMaxTop)
83 top.resize(top.size() - 1);
84 top.emplace(iter, static_string{static_string::string_view_type{r.name}}, elapsed);
85 }
86 }
87 else if (top.size() < kMaxTop)
88 {
89 top.emplace_back(static_string{static_string::string_view_type{r.name}}, elapsed);
90 }
91 }
92}
93
94void
96{
97 suites += r.suites;
98 total += r.total;
99 cases += r.cases;
100 failed += r.failed;
101
102 // combine the two top collections
103 boost::container::static_vector<run_time, 2 * kMaxTop> topResult;
104 topResult.resize(top.size() + r.top.size());
105 std::ranges::merge(top, r.top, topResult.begin(), [](run_time const& t1, run_time const& t2) {
106 return t1.second > t2.second;
107 });
109 if (topResult.size() > kMaxTop)
110 topResult.resize(kMaxTop);
112 top = topResult;
113}
115template <class S>
116void
118{
119 using namespace beast::unit_test;
121 if (!top.empty())
122 {
123 s << "Longest suite times:\n";
124 for (auto const& [name, dur] : top)
125 s << std::setw(8) << fmtdur(dur) << " " << name << '\n';
127
128 auto const elapsed = clock_type::now() - start;
129 s << fmtdur(elapsed) << ", " << Amount{suites, "suite"} << ", " << Amount{cases, "case"} << ", "
130 << Amount{total, "test"} << " total, " << Amount{failed, "failure"} << std::endl;
131}
133//------------------------------------------------------------------------------
134
135template <bool IsParent>
141
142template <bool IsParent>
148
149template <bool IsParent>
150bool
156template <bool IsParent>
157void
160 anyFailedFlag = anyFailedFlag || v;
161}
163template <bool IsParent>
166{
167 std::scoped_lock const l{m};
168 return results.total;
169}
170
171template <bool IsParent>
175 std::scoped_lock const l{m};
176 return results.suites;
178
179template <bool IsParent>
180void
185
186template <bool IsParent>
189{
190 return keepAlive;
191}
192
193template <bool IsParent>
194void
196{
197 std::scoped_lock const l{m};
198 results.merge(r);
199}
200
201template <bool IsParent>
202template <class S>
203void
209
210template <bool IsParent>
212{
213 try
214 {
215 if (IsParent)
216 {
217 // cleanup any leftover state for any previous failed runs
218 boost::interprocess::shared_memory_object::remove(kSharedMemName);
219 boost::interprocess::message_queue::remove(kMessageQueueName);
220 }
221
222 sharedMem_ = boost::interprocess::shared_memory_object{
224 IsParent,
225 boost::interprocess::create_only_t,
226 boost::interprocess::open_only_t>{},
228 boost::interprocess::read_write};
229
230 if (IsParent)
231 {
232 sharedMem_.truncate(sizeof(Inner));
234 boost::interprocess::create_only,
236 /*max messages*/ 16,
237 /*max message size*/ 1 << 20);
238 }
239 else
240 {
242 boost::interprocess::open_only, kMessageQueueName);
243 }
244
245 region_ = boost::interprocess::mapped_region{sharedMem_, boost::interprocess::read_write};
246 if (IsParent)
247 {
248 inner_ = new (region_.get_address()) Inner{};
249 }
250 else
251 {
252 inner_ = reinterpret_cast<Inner*>(region_.get_address());
253 }
254 }
255 catch (...)
256 {
257 if (IsParent)
258 {
259 boost::interprocess::shared_memory_object::remove(kSharedMemName);
260 boost::interprocess::message_queue::remove(kMessageQueueName);
261 }
262 throw;
263 }
264}
265
266template <bool IsParent>
268{
269 if (IsParent)
270 {
271 inner_->~Inner();
272 boost::interprocess::shared_memory_object::remove(kSharedMemName);
273 boost::interprocess::message_queue::remove(kMessageQueueName);
274 }
275}
276
277template <bool IsParent>
280{
281 return inner_->checkoutTestIndex();
282}
283
284template <bool IsParent>
287{
288 return inner_->checkoutJobIndex();
289}
290
291template <bool IsParent>
292bool
294{
295 return inner_->anyFailed();
296}
297
298template <bool IsParent>
299void
301{
302 return inner_->anyFailed(v);
303}
304
305template <bool IsParent>
306void
308{
309 inner_->add(r);
310}
311
312template <bool IsParent>
313void
315{
316 inner_->incKeepAliveCount();
317}
318
319template <bool IsParent>
322{
323 return inner_->getKeepAliveCount();
324}
325
326template <bool IsParent>
327template <class S>
328void
330{
331 inner_->printResults(s);
332}
333
334template <bool IsParent>
335void
337{
338 // must use a mutex since the two "sends" must happen in order
339 std::scoped_lock const l{inner_->m};
340 messageQueue_->send(&mt, sizeof(mt), /*priority*/ 0);
341 messageQueue_->send(s.c_str(), s.size(), /*priority*/ 0);
342}
343
344template <bool IsParent>
347{
348 return inner_->tests();
349}
350
351template <bool IsParent>
354{
355 return inner_->suites();
356}
357
358template <bool IsParent>
359void
361{
362 Results results;
363 results.failed += failures;
364 add(results);
365 anyFailed(failures != 0);
366}
367
368} // namespace detail
369
370namespace test {
371
372//------------------------------------------------------------------------------
373
375{
377 std::vector<char> buf(1 << 20);
378 while (this->continueMessageQueue_ || this->messageQueue_->get_num_msg())
379 {
380 // let children know the parent is still alive
381 this->incKeepAliveCount();
382 if (!this->messageQueue_->get_num_msg())
383 {
384 // If a child does not see the keep alive count incremented,
385 // it will assume the parent has died. This sleep time needs
386 // to be small enough so the child will see increments from
387 // a live parent.
389 continue;
390 }
391 try
392 {
393 std::size_t recvdSize = 0;
394 unsigned int priority = 0;
395 this->messageQueue_->receive(buf.data(), buf.size(), recvdSize, priority);
396 if (!recvdSize)
397 continue;
398 assert(recvdSize == 1);
399 MessageType const mt{*reinterpret_cast<MessageType*>(buf.data())};
400
401 this->messageQueue_->receive(buf.data(), buf.size(), recvdSize, priority);
402 if (recvdSize)
403 {
404 std::string s{buf.data(), recvdSize};
405 switch (mt)
406 {
407 case MessageType::Log:
408 this->os_ << s;
409 this->os_.flush();
410 break;
411 case MessageType::TestStart:
412 runningSuites_.insert(std::move(s));
413 break;
414 case MessageType::TestEnd:
415 runningSuites_.erase(s);
416 break;
417 default:
418 assert(0); // unknown message type
419 }
420 }
421 }
422 catch (std::exception const& e)
423 {
424 std::cerr << "Error: " << e.what() << " reading unit test message queue.\n";
425 return;
426 }
427 catch (...)
428 {
429 std::cerr << "Unknown error reading unit test message queue.\n";
430 return;
431 }
432 }
433 });
434}
435
437{
438 using namespace beast::unit_test;
439
440 continueMessageQueue_ = false;
441 messageQueueThread_.join();
442
444
446
447 for (auto const& s : runningSuites_)
448 {
449 os_ << "\nSuite: " << s << " failed to complete. The child process may have crashed.\n";
450 }
451}
452
453bool
458
464
470
471void
476
477//------------------------------------------------------------------------------
478
479MultiRunnerChild::MultiRunnerChild(std::size_t numJobs, bool quiet, bool printLog)
480 : jobIndex_{checkoutJobIndex()}, numJobs_{numJobs}, quiet_{quiet}, printLog_{!quiet || printLog}
481{
482 if (numJobs_ > 1)
483 {
485 std::size_t lastCount = getKeepAliveCount();
486 while (this->continueKeepAlive_)
487 {
488 // Use a small sleep time so in the normal case the child
489 // process may shutdown quickly. However, to protect against
490 // false alarms, use a longer sleep time later on.
492 auto curCount = this->getKeepAliveCount();
493 if (curCount == lastCount)
494 {
495 // longer sleep time to protect against false alarms
497 curCount = this->getKeepAliveCount();
498 if (curCount == lastCount)
499 {
500 // assume parent process is no longer alive
501 std::cerr << "multi_runner_child " << jobIndex_
502 << ": Assuming parent died, exiting.\n";
503 std::exit(EXIT_FAILURE);
504 }
505 }
506 lastCount = curCount;
507 }
508 });
509 }
510}
511
513{
514 if (numJobs_ > 1)
515 {
516 continueKeepAlive_ = false;
517 keepAliveThread_.join();
518 }
519
520 add(results_);
521}
522
525{
526 return results_.total;
527}
528
531{
532 return results_.suites;
533}
534
535void
537{
538 results_.failed += failures;
539 anyFailed(failures != 0);
540}
541
542void
548
549void
551{
552 if (printLog_ || suiteResults_.failed > 0)
553 {
555 if (numJobs_ > 1)
556 s << jobIndex_ << "> ";
557 s << (suiteResults_.failed > 0 ? "failed: " : "") << suiteResults_.name << " had "
558 << suiteResults_.failed << " failures." << std::endl;
559 messageQueueSend(MessageType::Log, s.str());
560 }
562 messageQueueSend(MessageType::TestEnd, suiteResults_.name);
563}
564
565void
567{
569
570 if (quiet_)
571 return;
572
574 if (numJobs_ > 1)
575 s << jobIndex_ << "> ";
576 s << suiteResults_.name << (caseResults_.name.empty() ? "" : (" " + caseResults_.name)) << '\n';
577 messageQueueSend(MessageType::Log, s.str());
578}
579
580void
585
586void
588{
589 ++caseResults_.total;
590}
591
592void
594{
595 ++caseResults_.failed;
596 ++caseResults_.total;
598 if (numJobs_ > 1)
599 s << jobIndex_ << "> ";
600 s << "#" << caseResults_.total << " failed" << (reason.empty() ? "" : ": ") << reason << '\n';
601 messageQueueSend(MessageType::Log, s.str());
602}
603
604void
606{
607 if (!printLog_)
608 return;
609
611 if (numJobs_ > 1)
612 s << jobIndex_ << "> ";
613 s << msg;
614 messageQueueSend(MessageType::Log, s.str());
615}
616
617} // namespace test
618
619namespace detail {
620template class MultiRunnerBase<true>;
621template class MultiRunnerBase<false>;
622} // namespace detail
623
624} // 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
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 to_string(T... args)
T what(T... args)