Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/contrib/kyua/drivers/run_tests.cpp
39478 views
1
// Copyright 2011 The Kyua Authors.
2
// All rights reserved.
3
//
4
// Redistribution and use in source and binary forms, with or without
5
// modification, are permitted provided that the following conditions are
6
// met:
7
//
8
// * Redistributions of source code must retain the above copyright
9
// notice, this list of conditions and the following disclaimer.
10
// * Redistributions in binary form must reproduce the above copyright
11
// notice, this list of conditions and the following disclaimer in the
12
// documentation and/or other materials provided with the distribution.
13
// * Neither the name of Google Inc. nor the names of its contributors
14
// may be used to endorse or promote products derived from this software
15
// without specific prior written permission.
16
//
17
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29
#include "drivers/run_tests.hpp"
30
31
#include <utility>
32
33
#include "engine/config.hpp"
34
#include "engine/filters.hpp"
35
#include "engine/kyuafile.hpp"
36
#include "engine/scanner.hpp"
37
#include "engine/scheduler.hpp"
38
#include "model/context.hpp"
39
#include "model/metadata.hpp"
40
#include "model/test_case.hpp"
41
#include "model/test_program.hpp"
42
#include "model/test_result.hpp"
43
#include "store/write_backend.hpp"
44
#include "store/write_transaction.hpp"
45
#include "utils/config/tree.ipp"
46
#include "utils/datetime.hpp"
47
#include "utils/defs.hpp"
48
#include "utils/format/macros.hpp"
49
#include "utils/logging/macros.hpp"
50
#include "utils/noncopyable.hpp"
51
#include "utils/optional.ipp"
52
#include "utils/passwd.hpp"
53
#include "utils/text/operations.ipp"
54
55
namespace config = utils::config;
56
namespace datetime = utils::datetime;
57
namespace fs = utils::fs;
58
namespace passwd = utils::passwd;
59
namespace scheduler = engine::scheduler;
60
namespace text = utils::text;
61
62
using utils::none;
63
using utils::optional;
64
65
66
namespace {
67
68
69
/// Map of test program identifiers (relative paths) to their identifiers in the
70
/// database. We need to keep this in memory because test programs can be
71
/// returned by the scanner in any order, and we only want to put each test
72
/// program once.
73
typedef std::map< fs::path, int64_t > path_to_id_map;
74
75
76
/// Map of in-flight PIDs to their corresponding test case IDs.
77
typedef std::map< int, int64_t > pid_to_id_map;
78
79
80
/// Pair of PID to a test case ID.
81
typedef pid_to_id_map::value_type pid_and_id_pair;
82
83
84
/// Puts a test program in the store and returns its identifier.
85
///
86
/// This function is idempotent: we maintain a side cache of already-put test
87
/// programs so that we can return their identifiers without having to put them
88
/// again.
89
/// TODO(jmmv): It's possible that the store module should offer this
90
/// functionality and not have to do this ourselves here.
91
///
92
/// \param test_program The test program being put.
93
/// \param [in,out] tx Writable transaction on the store.
94
/// \param [in,out] ids_cache Cache of already-put test programs.
95
///
96
/// \return A test program identifier.
97
static int64_t
98
find_test_program_id(const model::test_program_ptr test_program,
99
store::write_transaction& tx,
100
path_to_id_map& ids_cache)
101
{
102
const fs::path& key = test_program->relative_path();
103
std::map< fs::path, int64_t >::const_iterator iter = ids_cache.find(key);
104
if (iter == ids_cache.end()) {
105
const int64_t id = tx.put_test_program(*test_program);
106
ids_cache.insert(std::make_pair(key, id));
107
return id;
108
} else {
109
return (*iter).second;
110
}
111
}
112
113
114
/// Stores the result of an execution in the database.
115
///
116
/// \param test_case_id Identifier of the test case in the database.
117
/// \param result The result of the execution.
118
/// \param [in,out] tx Writable transaction where to store the result data.
119
static void
120
put_test_result(const int64_t test_case_id,
121
const scheduler::test_result_handle& result,
122
store::write_transaction& tx)
123
{
124
tx.put_result(result.test_result(), test_case_id,
125
result.start_time(), result.end_time());
126
tx.put_test_case_file("__STDOUT__", result.stdout_file(), test_case_id);
127
tx.put_test_case_file("__STDERR__", result.stderr_file(), test_case_id);
128
129
}
130
131
132
/// Cleans up a test case and folds any errors into the test result.
133
///
134
/// \param handle The result handle for the test.
135
///
136
/// \return The test result if the cleanup succeeds; a broken test result
137
/// otherwise.
138
model::test_result
139
safe_cleanup(scheduler::test_result_handle handle) throw()
140
{
141
try {
142
handle.cleanup();
143
return handle.test_result();
144
} catch (const std::exception& e) {
145
return model::test_result(
146
model::test_result_broken,
147
F("Failed to clean up test case's work directory %s: %s") %
148
handle.work_directory() % e.what());
149
}
150
}
151
152
153
/// Starts a test asynchronously.
154
///
155
/// \param handle Scheduler handle.
156
/// \param match Test program and test case to start.
157
/// \param [in,out] tx Writable transaction to obtain test IDs.
158
/// \param [in,out] ids_cache Cache of already-put test cases.
159
/// \param user_config The end-user configuration properties.
160
/// \param hooks The hooks for this execution.
161
///
162
/// \returns The PID for the started test and the test case's identifier in the
163
/// store.
164
pid_and_id_pair
165
start_test(scheduler::scheduler_handle& handle,
166
const engine::scan_result& match,
167
store::write_transaction& tx,
168
path_to_id_map& ids_cache,
169
const config::tree& user_config,
170
drivers::run_tests::base_hooks& hooks)
171
{
172
const model::test_program_ptr test_program = match.first;
173
const std::string& test_case_name = match.second;
174
175
hooks.got_test_case(*test_program, test_case_name);
176
177
const int64_t test_program_id = find_test_program_id(
178
test_program, tx, ids_cache);
179
const int64_t test_case_id = tx.put_test_case(
180
*test_program, test_case_name, test_program_id);
181
182
const scheduler::exec_handle exec_handle = handle.spawn_test(
183
test_program, test_case_name, user_config);
184
return std::make_pair(exec_handle, test_case_id);
185
}
186
187
188
/// Processes the completion of a test.
189
///
190
/// \param [in,out] result_handle The completion handle of the test subprocess.
191
/// \param test_case_id Identifier of the test case as returned by start_test().
192
/// \param [in,out] tx Writable transaction to put the test results.
193
/// \param hooks The hooks for this execution.
194
///
195
/// \post result_handle is cleaned up. The caller cannot clean it up again.
196
void
197
finish_test(scheduler::result_handle_ptr result_handle,
198
const int64_t test_case_id,
199
store::write_transaction& tx,
200
drivers::run_tests::base_hooks& hooks)
201
{
202
const scheduler::test_result_handle* test_result_handle =
203
dynamic_cast< const scheduler::test_result_handle* >(
204
result_handle.get());
205
206
put_test_result(test_case_id, *test_result_handle, tx);
207
208
const model::test_result test_result = safe_cleanup(*test_result_handle);
209
hooks.got_result(
210
*test_result_handle->test_program(),
211
test_result_handle->test_case_name(),
212
test_result_handle->test_result(),
213
result_handle->end_time() - result_handle->start_time());
214
}
215
216
217
/// Extracts the keys of a pid_to_id_map and returns them as a string.
218
///
219
/// \param map The PID to test ID map from which to get the PIDs.
220
///
221
/// \return A user-facing string with the collection of PIDs.
222
static std::string
223
format_pids(const pid_to_id_map& map)
224
{
225
std::set< pid_to_id_map::key_type > pids;
226
for (pid_to_id_map::const_iterator iter = map.begin(); iter != map.end();
227
++iter) {
228
pids.insert(iter->first);
229
}
230
return text::join(pids, ",");
231
}
232
233
234
} // anonymous namespace
235
236
237
/// Pure abstract destructor.
238
drivers::run_tests::base_hooks::~base_hooks(void)
239
{
240
}
241
242
243
/// Executes the operation.
244
///
245
/// \param kyuafile_path The path to the Kyuafile to be loaded.
246
/// \param build_root If not none, path to the built test programs.
247
/// \param store_path The path to the store to be used.
248
/// \param filters The test case filters as provided by the user.
249
/// \param user_config The end-user configuration properties.
250
/// \param hooks The hooks for this execution.
251
///
252
/// \returns A structure with all results computed by this driver.
253
drivers::run_tests::result
254
drivers::run_tests::drive(const fs::path& kyuafile_path,
255
const optional< fs::path > build_root,
256
const fs::path& store_path,
257
const std::set< engine::test_filter >& filters,
258
const config::tree& user_config,
259
base_hooks& hooks)
260
{
261
scheduler::scheduler_handle handle = scheduler::setup();
262
263
const engine::kyuafile kyuafile = engine::kyuafile::load(
264
kyuafile_path, build_root, user_config, handle);
265
store::write_backend db = store::write_backend::open_rw(store_path);
266
store::write_transaction tx = db.start_write();
267
268
{
269
const model::context context = scheduler::current_context();
270
(void)tx.put_context(context);
271
}
272
273
engine::scanner scanner(kyuafile.test_programs(), filters);
274
275
path_to_id_map ids_cache;
276
pid_to_id_map in_flight;
277
std::vector< engine::scan_result > exclusive_tests;
278
279
const std::size_t slots = user_config.lookup< config::positive_int_node >(
280
"parallelism");
281
INV(slots >= 1);
282
do {
283
INV(in_flight.size() <= slots);
284
285
// Spawn as many jobs as needed to fill our execution slots. We do this
286
// first with the assumption that the spawning is faster than any single
287
// job, so we want to keep as many jobs in the background as possible.
288
while (in_flight.size() < slots) {
289
optional< engine::scan_result > match = scanner.yield();
290
if (!match)
291
break;
292
const model::test_program_ptr test_program = match.get().first;
293
const std::string& test_case_name = match.get().second;
294
295
const model::test_case& test_case = test_program->find(
296
test_case_name);
297
if (test_case.get_metadata().is_exclusive()) {
298
// Exclusive tests get processed later, separately.
299
exclusive_tests.push_back(match.get());
300
continue;
301
}
302
303
const pid_and_id_pair pid_id = start_test(
304
handle, match.get(), tx, ids_cache, user_config, hooks);
305
INV_MSG(in_flight.find(pid_id.first) == in_flight.end(),
306
F("Spawned test has PID of still-tracked process %s") %
307
pid_id.first);
308
in_flight.insert(pid_id);
309
}
310
311
// If there are any used slots, consume any at random and return the
312
// result. We consume slots one at a time to give preference to the
313
// spawning of new tests as detailed above.
314
if (!in_flight.empty()) {
315
scheduler::result_handle_ptr result_handle = handle.wait_any();
316
317
const pid_to_id_map::iterator iter = in_flight.find(
318
result_handle->original_pid());
319
INV_MSG(iter != in_flight.end(),
320
F("Lost track of in-flight PID %s; tracking %s") %
321
result_handle->original_pid() % format_pids(in_flight));
322
const int64_t test_case_id = (*iter).second;
323
in_flight.erase(iter);
324
325
finish_test(result_handle, test_case_id, tx, hooks);
326
}
327
} while (!in_flight.empty() || !scanner.done());
328
329
// Run any exclusive tests that we spotted earlier sequentially.
330
for (std::vector< engine::scan_result >::const_iterator
331
iter = exclusive_tests.begin(); iter != exclusive_tests.end();
332
++iter) {
333
const pid_and_id_pair data = start_test(
334
handle, *iter, tx, ids_cache, user_config, hooks);
335
scheduler::result_handle_ptr result_handle = handle.wait_any();
336
finish_test(result_handle, data.second, tx, hooks);
337
}
338
339
tx.commit();
340
341
handle.cleanup();
342
343
return result(scanner.unused_filters());
344
}
345
346