Skip to content

Commit 34974fc

Browse files
committed
WIP parallel test infra.
1 parent 121e5a8 commit 34974fc

5 files changed

Lines changed: 462 additions & 1 deletion

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* Unit Test
2+
*
3+
* From: https://github.com/PokemonAutomation/
4+
*
5+
*/
6+
7+
#ifndef PokemonAutomation_UnitTest_H
8+
#define PokemonAutomation_UnitTest_H
9+
10+
#include <string>
11+
#include "Common/Cpp/CancellableScope.h"
12+
13+
namespace PokemonAutomation{
14+
15+
16+
enum class UnitTestResult{
17+
NOT_RUN,
18+
PASSED,
19+
FAILED,
20+
SKIPPED,
21+
OOM,
22+
};
23+
24+
25+
class UnitTest{
26+
public:
27+
virtual ~UnitTest() = default;
28+
UnitTest(std::string name)
29+
: m_name(std::move(name))
30+
{}
31+
32+
const std::string& name() const{
33+
return m_name;
34+
}
35+
uint64_t memory() const{
36+
return m_memory;
37+
}
38+
size_t threads() const{
39+
return m_threads;
40+
}
41+
UnitTestResult result() const{
42+
return m_result;
43+
}
44+
const std::string& message() const{
45+
return m_message;
46+
}
47+
48+
virtual std::pair<UnitTestResult, std::string> run(CancellableScope& scope) = 0;
49+
50+
51+
protected:
52+
friend class UnitTestRunner;
53+
54+
const std::string m_name;
55+
uint64_t m_memory = 0;
56+
size_t m_threads = 1;
57+
58+
private:
59+
UnitTestResult m_result = UnitTestResult::NOT_RUN;
60+
std::string m_message;
61+
};
62+
63+
64+
65+
66+
67+
68+
}
69+
#endif
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/* Unit Test
2+
*
3+
* From: https://github.com/PokemonAutomation/
4+
*
5+
*/
6+
7+
#include "Common/Cpp/Exceptions.h"
8+
#include "Common/Cpp/PrettyPrint.h"
9+
#include "Common/Cpp/MemoryUtilization/MemoryUtilization.h"
10+
#include "Common/Cpp/Concurrency/ReverseLockGuard.h"
11+
#include "CommonFramework/Tools/GlobalThreadPools.h"
12+
#include "UnitTestRunner.h"
13+
14+
//#include <iostream>
15+
//using std::cout;
16+
//using std::endl;
17+
18+
namespace PokemonAutomation{
19+
20+
21+
22+
23+
UnitTestRunner::~UnitTestRunner(){
24+
detach();
25+
UnitTestRunner::cancel(nullptr);
26+
m_dispatcher.wait_and_ignore_exceptions();
27+
28+
29+
std::unique_lock<Mutex> lg(m_lock);
30+
m_cv.wait(lg, [this]{
31+
return m_currently_running.empty();
32+
});
33+
}
34+
UnitTestRunner::UnitTestRunner(
35+
CancellableScope* parent,
36+
Logger& logger,
37+
uint64_t max_memory,
38+
size_t max_threads
39+
)
40+
: m_logger(logger)
41+
, m_max_memory(max_memory)
42+
, m_max_threads(max_threads)
43+
{
44+
if (m_max_memory == 0){
45+
MemoryUsage memory = process_memory_usage();
46+
m_max_memory = memory.total_system_memory - memory.total_used_system_memory;
47+
}
48+
if (m_max_threads == 0){
49+
m_max_threads = std::thread::hardware_concurrency() * 2;
50+
}
51+
m_dispatcher = GlobalThreadPools::unlimited_normal().dispatch_now_blocking([this]{
52+
thread_loop();
53+
});
54+
if (parent){
55+
attach(*parent);
56+
}
57+
}
58+
59+
60+
void UnitTestRunner::add_test(std::unique_ptr<UnitTest> test){
61+
throw_if_cancelled();
62+
63+
if (test->name().empty()){
64+
InternalProgramError(&m_logger, PA_CURRENT_FUNCTION, "Test name cannot be null.");
65+
}
66+
67+
{
68+
std::lock_guard<Mutex> lg(m_lock);
69+
if (m_test_by_name.contains(test->name())){
70+
InternalProgramError(&m_logger, PA_CURRENT_FUNCTION, "Duplicate Test Name: " + test->name());
71+
}
72+
auto iter0 = m_test_by_name.end();
73+
auto iter1 = m_test_by_total_memory.end();
74+
auto iter2 = m_test_by_per_thread_memory.end();
75+
try{
76+
iter0 = m_test_by_name.emplace(test->name(), PendingEntry{}).first;
77+
iter1 = m_test_by_total_memory.emplace(test->memory(), test->name());
78+
iter2 = m_test_by_per_thread_memory.emplace((double)test->memory() / test->threads(), test->name());
79+
iter0->second.test = std::move(test);
80+
iter0->second.iter_total = iter1;
81+
iter0->second.iter_density = iter2;
82+
}catch (...){
83+
if (iter0 != m_test_by_name.end()){
84+
m_test_by_name.erase(iter0);
85+
}
86+
if (iter1 != m_test_by_total_memory.end()){
87+
m_test_by_total_memory.erase(iter1);
88+
}
89+
if (iter2 != m_test_by_per_thread_memory.end()){
90+
m_test_by_per_thread_memory.erase(iter2);
91+
}
92+
throw;
93+
}
94+
}
95+
96+
m_cv.notify_all();
97+
}
98+
99+
void UnitTestRunner::wait_for_all(){
100+
std::unique_lock<Mutex> lg(m_lock);
101+
while (!cancelled()){
102+
if (m_currently_running.empty() && m_test_by_name.empty()){
103+
return;
104+
}
105+
m_cv.wait(lg);
106+
}
107+
}
108+
109+
bool UnitTestRunner::cancel(std::exception_ptr reason) noexcept{
110+
if (CancellableScope::cancel(reason)){
111+
return true;
112+
}
113+
{
114+
std::lock_guard<Mutex> lg(m_lock);
115+
}
116+
m_cv.notify_all();
117+
return false;
118+
}
119+
120+
121+
void UnitTestRunner::thread_loop(){
122+
m_logger.log(
123+
"Starting UnitTestRunner with:"
124+
"\n Max Memory: " + tostr_bytes(m_max_memory) +
125+
"\n Max Threads: " + tostr_u_commas(m_max_threads)
126+
);
127+
// cout << "thread_loop() - start" << endl;
128+
std::unique_lock<Mutex> lg(m_lock);
129+
while (!cancelled()){
130+
if (m_test_by_name.empty()){
131+
m_cv.wait(lg);
132+
continue;
133+
}
134+
135+
// If nothing is running, always run the thing that uses the most memory.
136+
if (m_currently_running.empty()){
137+
dispatch_test(m_test_by_total_memory.rbegin()->second);
138+
continue;
139+
}
140+
141+
// We are over the resource limit. Don't run anything.
142+
if (m_current_memory >= m_max_memory || m_current_threads >= m_max_threads){
143+
m_cv.wait(lg);
144+
continue;
145+
}
146+
147+
// Try to run the test that uses the most memory.
148+
{
149+
const std::string& name = m_test_by_total_memory.rbegin()->second;
150+
PendingEntry& entry = m_test_by_name.find(name)->second;
151+
if (m_current_memory + entry.test->memory() <= m_max_memory ||
152+
m_current_threads + entry.test->threads() <= m_max_threads
153+
){
154+
dispatch_test(name);
155+
continue;
156+
}
157+
}
158+
159+
// Try run the smallest memory/thread ratio.
160+
{
161+
const std::string& name = m_test_by_per_thread_memory.begin()->second;
162+
PendingEntry& entry = m_test_by_name.find(name)->second;
163+
if (m_current_memory + entry.test->memory() <= m_max_memory ||
164+
m_current_threads + entry.test->threads() <= m_max_threads
165+
){
166+
dispatch_test(name);
167+
continue;
168+
}
169+
}
170+
171+
// Can't run anything.
172+
m_cv.wait(lg);
173+
}
174+
// cout << "thread_loop() - end" << endl;
175+
}
176+
177+
178+
void UnitTestRunner::dispatch_test(const std::string& name){
179+
// Must be called under the lock.
180+
181+
// cout << "dispatch_test() - start" << endl;
182+
auto iter = m_test_by_name.find(name);
183+
PendingEntry& entry = iter->second;
184+
185+
auto iter_current = m_currently_running.emplace(name, std::move(entry.test)).first;
186+
DispatchedEntry& node = iter_current->second;
187+
188+
UnitTest& test = *iter_current->second.test;
189+
m_current_memory += test.m_memory;
190+
m_current_threads += test.m_threads;
191+
192+
{
193+
ReverseLockGuard<Mutex> lg(m_lock);
194+
node.task = GlobalThreadPools::computation_normal().dispatch_now_blocking([this, iter_current]{
195+
UnitTest& test = *iter_current->second.test;
196+
test.m_result = UnitTestResult::FAILED;
197+
try{
198+
m_logger.log("Starting: " + test.name());
199+
auto ret = test.run(*this);
200+
test.m_result = ret.first;
201+
test.m_message = std::move(ret.second);
202+
}catch (Exception& e){
203+
test.m_message = e.to_str();
204+
}catch (std::bad_alloc&){
205+
test.m_result = UnitTestResult::OOM;
206+
}catch (std::exception& e){
207+
test.m_message = e.what();
208+
}catch (...){
209+
test.m_message = "Unknown exception.";
210+
}
211+
212+
switch (test.result()){
213+
case UnitTestResult::NOT_RUN:
214+
case UnitTestResult::PASSED:
215+
m_logger.log("Passed: " + test.name(), COLOR_BLUE);
216+
break;
217+
case UnitTestResult::FAILED:
218+
m_logger.log("Failed: " + test.name() + ", Message: " + test.message(), COLOR_RED);
219+
break;
220+
case UnitTestResult::SKIPPED:
221+
m_logger.log("Skipped: " + test.name(), COLOR_ORANGE);
222+
break;
223+
case UnitTestResult::OOM:
224+
m_logger.log("Out-of-Memory: " + test.name(), COLOR_ORANGE);
225+
break;
226+
}
227+
228+
{
229+
std::lock_guard<Mutex> lg(m_lock);
230+
auto node = m_currently_running.extract(iter_current);
231+
m_completed.insert(std::move(node));
232+
m_current_memory -= test.m_memory;
233+
m_current_threads -= test.m_threads;
234+
}
235+
m_cv.notify_all();
236+
});
237+
}
238+
239+
m_test_by_total_memory.erase(entry.iter_total);
240+
m_test_by_per_thread_memory.erase(entry.iter_density);
241+
m_test_by_name.erase(iter);
242+
// cout << "dispatch_test() - end" << endl;
243+
}
244+
245+
246+
247+
248+
}

0 commit comments

Comments
 (0)