Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/contrib/kyua/store/write_transaction.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 "store/write_transaction.hpp"
30
31
extern "C" {
32
#include <stdint.h>
33
}
34
35
#include <fstream>
36
#include <map>
37
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 "model/types.hpp"
44
#include "store/dbtypes.hpp"
45
#include "store/exceptions.hpp"
46
#include "store/write_backend.hpp"
47
#include "utils/datetime.hpp"
48
#include "utils/format/macros.hpp"
49
#include "utils/fs/path.hpp"
50
#include "utils/logging/macros.hpp"
51
#include "utils/noncopyable.hpp"
52
#include "utils/optional.ipp"
53
#include "utils/sanity.hpp"
54
#include "utils/stream.hpp"
55
#include "utils/sqlite/database.hpp"
56
#include "utils/sqlite/exceptions.hpp"
57
#include "utils/sqlite/statement.ipp"
58
#include "utils/sqlite/transaction.hpp"
59
60
namespace datetime = utils::datetime;
61
namespace fs = utils::fs;
62
namespace sqlite = utils::sqlite;
63
64
using utils::none;
65
using utils::optional;
66
67
68
namespace {
69
70
71
/// Stores the environment variables of a context.
72
///
73
/// \param db The SQLite database.
74
/// \param env The environment variables to store.
75
///
76
/// \throw sqlite::error If there is a problem storing the variables.
77
static void
78
put_env_vars(sqlite::database& db,
79
const std::map< std::string, std::string >& env)
80
{
81
sqlite::statement stmt = db.create_statement(
82
"INSERT INTO env_vars (var_name, var_value) "
83
"VALUES (:var_name, :var_value)");
84
for (std::map< std::string, std::string >::const_iterator iter =
85
env.begin(); iter != env.end(); iter++) {
86
stmt.bind(":var_name", (*iter).first);
87
stmt.bind(":var_value", (*iter).second);
88
stmt.step_without_results();
89
stmt.reset();
90
}
91
}
92
93
94
/// Calculates the last rowid of a table.
95
///
96
/// \param db The SQLite database.
97
/// \param table Name of the table.
98
///
99
/// \return The last rowid; 0 if the table is empty.
100
static int64_t
101
last_rowid(sqlite::database& db, const std::string& table)
102
{
103
sqlite::statement stmt = db.create_statement(
104
F("SELECT MAX(ROWID) AS max_rowid FROM %s") % table);
105
stmt.step();
106
if (stmt.column_type(0) == sqlite::type_null) {
107
return 0;
108
} else {
109
INV(stmt.column_type(0) == sqlite::type_integer);
110
return stmt.column_int64(0);
111
}
112
}
113
114
115
/// Stores a metadata object.
116
///
117
/// \param db The database into which to store the information.
118
/// \param md The metadata to store.
119
///
120
/// \return The identifier of the new metadata object.
121
static int64_t
122
put_metadata(sqlite::database& db, const model::metadata& md)
123
{
124
const model::properties_map props = md.to_properties();
125
126
const int64_t metadata_id = last_rowid(db, "metadatas");
127
128
sqlite::statement stmt = db.create_statement(
129
"INSERT INTO metadatas (metadata_id, property_name, property_value) "
130
"VALUES (:metadata_id, :property_name, :property_value)");
131
stmt.bind(":metadata_id", metadata_id);
132
133
for (model::properties_map::const_iterator iter = props.begin();
134
iter != props.end(); ++iter) {
135
stmt.bind(":property_name", (*iter).first);
136
stmt.bind(":property_value", (*iter).second);
137
stmt.step_without_results();
138
stmt.reset();
139
}
140
141
return metadata_id;
142
}
143
144
145
/// Stores an arbitrary file into the database as a BLOB.
146
///
147
/// \param db The database into which to store the file.
148
/// \param path Path to the file to be stored.
149
///
150
/// \return The identifier of the stored file, or none if the file was empty.
151
///
152
/// \throw sqlite::error If there are problems writing to the database.
153
static optional< int64_t >
154
put_file(sqlite::database& db, const fs::path& path)
155
{
156
std::ifstream input(path.c_str());
157
if (!input)
158
throw store::error(F("Cannot open file %s") % path);
159
160
try {
161
if (utils::stream_length(input) == 0)
162
return none;
163
} catch (const std::runtime_error& e) {
164
// Skipping empty files is an optimization. If we fail to calculate the
165
// size of the file, just ignore the problem. If there are real issues
166
// with the file, the read below will fail anyway.
167
LD(F("Cannot determine if file is empty: %s") % e.what());
168
}
169
170
// TODO(jmmv): This will probably cause an unreasonable amount of memory
171
// consumption if we decide to store arbitrary files in the database (other
172
// than stdout or stderr). Should this happen, we need to investigate a
173
// better way to feel blobs into SQLite.
174
const std::string contents = utils::read_stream(input);
175
176
sqlite::statement stmt = db.create_statement(
177
"INSERT INTO files (contents) VALUES (:contents)");
178
stmt.bind(":contents", sqlite::blob(contents.c_str(), contents.length()));
179
stmt.step_without_results();
180
181
return optional< int64_t >(db.last_insert_rowid());
182
}
183
184
185
} // anonymous namespace
186
187
188
/// Internal implementation for a store write-only transaction.
189
struct store::write_transaction::impl : utils::noncopyable {
190
/// The backend instance.
191
store::write_backend& _backend;
192
193
/// The SQLite database this transaction deals with.
194
sqlite::database _db;
195
196
/// The backing SQLite transaction.
197
sqlite::transaction _tx;
198
199
/// Opens a transaction.
200
///
201
/// \param backend_ The backend this transaction is connected to.
202
impl(write_backend& backend_) :
203
_backend(backend_),
204
_db(backend_.database()),
205
_tx(backend_.database().begin_transaction())
206
{
207
}
208
};
209
210
211
/// Creates a new write-only transaction.
212
///
213
/// \param backend_ The backend this transaction belongs to.
214
store::write_transaction::write_transaction(write_backend& backend_) :
215
_pimpl(new impl(backend_))
216
{
217
}
218
219
220
/// Destructor.
221
store::write_transaction::~write_transaction(void)
222
{
223
}
224
225
226
/// Commits the transaction.
227
///
228
/// \throw error If there is any problem when talking to the database.
229
void
230
store::write_transaction::commit(void)
231
{
232
try {
233
_pimpl->_tx.commit();
234
} catch (const sqlite::error& e) {
235
throw error(e.what());
236
}
237
}
238
239
240
/// Rolls the transaction back.
241
///
242
/// \throw error If there is any problem when talking to the database.
243
void
244
store::write_transaction::rollback(void)
245
{
246
try {
247
_pimpl->_tx.rollback();
248
} catch (const sqlite::error& e) {
249
throw error(e.what());
250
}
251
}
252
253
254
/// Puts a context into the database.
255
///
256
/// \pre The context has not been put yet.
257
/// \post The context is stored into the database with a new identifier.
258
///
259
/// \param context The context to put.
260
///
261
/// \throw error If there is any problem when talking to the database.
262
void
263
store::write_transaction::put_context(const model::context& context)
264
{
265
try {
266
sqlite::statement stmt = _pimpl->_db.create_statement(
267
"INSERT INTO contexts (cwd) VALUES (:cwd)");
268
stmt.bind(":cwd", context.cwd().str());
269
stmt.step_without_results();
270
271
put_env_vars(_pimpl->_db, context.env());
272
} catch (const sqlite::error& e) {
273
throw error(e.what());
274
}
275
}
276
277
278
/// Puts a test program into the database.
279
///
280
/// \pre The test program has not been put yet.
281
/// \post The test program is stored into the database with a new identifier.
282
///
283
/// \param test_program The test program to put.
284
///
285
/// \return The identifier of the inserted test program.
286
///
287
/// \throw error If there is any problem when talking to the database.
288
int64_t
289
store::write_transaction::put_test_program(
290
const model::test_program& test_program)
291
{
292
try {
293
const int64_t metadata_id = put_metadata(
294
_pimpl->_db, test_program.get_metadata());
295
296
sqlite::statement stmt = _pimpl->_db.create_statement(
297
"INSERT INTO test_programs (absolute_path, "
298
" root, relative_path, test_suite_name, "
299
" metadata_id, interface) "
300
"VALUES (:absolute_path, :root, :relative_path, "
301
" :test_suite_name, :metadata_id, :interface)");
302
stmt.bind(":absolute_path", test_program.absolute_path().str());
303
// TODO(jmmv): The root is not necessarily absolute. We need to ensure
304
// that we can recover the absolute path of the test program. Maybe we
305
// need to change the test_program to always ensure root is absolute?
306
stmt.bind(":root", test_program.root().str());
307
stmt.bind(":relative_path", test_program.relative_path().str());
308
stmt.bind(":test_suite_name", test_program.test_suite_name());
309
stmt.bind(":metadata_id", metadata_id);
310
stmt.bind(":interface", test_program.interface_name());
311
stmt.step_without_results();
312
return _pimpl->_db.last_insert_rowid();
313
} catch (const sqlite::error& e) {
314
throw error(e.what());
315
}
316
}
317
318
319
/// Puts a test case into the database.
320
///
321
/// \pre The test case has not been put yet.
322
/// \post The test case is stored into the database with a new identifier.
323
///
324
/// \param test_program The program containing the test case to be stored.
325
/// \param test_case_name The name of the test case to put.
326
/// \param test_program_id The test program this test case belongs to.
327
///
328
/// \return The identifier of the inserted test case.
329
///
330
/// \throw error If there is any problem when talking to the database.
331
int64_t
332
store::write_transaction::put_test_case(const model::test_program& test_program,
333
const std::string& test_case_name,
334
const int64_t test_program_id)
335
{
336
const model::test_case& test_case = test_program.find(test_case_name);
337
338
try {
339
const int64_t metadata_id = put_metadata(
340
_pimpl->_db, test_case.get_raw_metadata());
341
342
sqlite::statement stmt = _pimpl->_db.create_statement(
343
"INSERT INTO test_cases (test_program_id, name, metadata_id) "
344
"VALUES (:test_program_id, :name, :metadata_id)");
345
stmt.bind(":test_program_id", test_program_id);
346
stmt.bind(":name", test_case.name());
347
stmt.bind(":metadata_id", metadata_id);
348
stmt.step_without_results();
349
return _pimpl->_db.last_insert_rowid();
350
} catch (const sqlite::error& e) {
351
throw error(e.what());
352
}
353
}
354
355
356
/// Stores a file generated by a test case into the database as a BLOB.
357
///
358
/// \param name The name of the file to store in the database. This needs to be
359
/// unique per test case. The caller is free to decide what names to use
360
/// for which files. For example, it might make sense to always call
361
/// __STDOUT__ the stdout of the test case so that it is easy to locate.
362
/// \param path The path to the file to be stored.
363
/// \param test_case_id The identifier of the test case this file belongs to.
364
///
365
/// \return The identifier of the stored file, or none if the file was empty.
366
///
367
/// \throw store::error If there are problems writing to the database.
368
optional< int64_t >
369
store::write_transaction::put_test_case_file(const std::string& name,
370
const fs::path& path,
371
const int64_t test_case_id)
372
{
373
LD(F("Storing %s (%s) of test case %s") % name % path % test_case_id);
374
try {
375
const optional< int64_t > file_id = put_file(_pimpl->_db, path);
376
if (!file_id) {
377
LD("Not storing empty file");
378
return none;
379
}
380
381
sqlite::statement stmt = _pimpl->_db.create_statement(
382
"INSERT INTO test_case_files (test_case_id, file_name, file_id) "
383
"VALUES (:test_case_id, :file_name, :file_id)");
384
stmt.bind(":test_case_id", test_case_id);
385
stmt.bind(":file_name", name);
386
stmt.bind(":file_id", file_id.get());
387
stmt.step_without_results();
388
389
return optional< int64_t >(_pimpl->_db.last_insert_rowid());
390
} catch (const sqlite::error& e) {
391
throw error(e.what());
392
}
393
}
394
395
396
/// Puts a result into the database.
397
///
398
/// \pre The result has not been put yet.
399
/// \post The result is stored into the database with a new identifier.
400
///
401
/// \param result The result to put.
402
/// \param test_case_id The test case this result corresponds to.
403
/// \param start_time The time when the test started to run.
404
/// \param end_time The time when the test finished running.
405
///
406
/// \return The identifier of the inserted result.
407
///
408
/// \throw error If there is any problem when talking to the database.
409
int64_t
410
store::write_transaction::put_result(const model::test_result& result,
411
const int64_t test_case_id,
412
const datetime::timestamp& start_time,
413
const datetime::timestamp& end_time)
414
{
415
try {
416
sqlite::statement stmt = _pimpl->_db.create_statement(
417
"INSERT INTO test_results (test_case_id, result_type, "
418
" result_reason, start_time, "
419
" end_time) "
420
"VALUES (:test_case_id, :result_type, :result_reason, "
421
" :start_time, :end_time)");
422
stmt.bind(":test_case_id", test_case_id);
423
424
store::bind_test_result_type(stmt, ":result_type", result.type());
425
if (result.reason().empty())
426
stmt.bind(":result_reason", sqlite::null());
427
else
428
stmt.bind(":result_reason", result.reason());
429
430
store::bind_timestamp(stmt, ":start_time", start_time);
431
store::bind_timestamp(stmt, ":end_time", end_time);
432
433
stmt.step_without_results();
434
const int64_t result_id = _pimpl->_db.last_insert_rowid();
435
436
return result_id;
437
} catch (const sqlite::error& e) {
438
throw error(e.what());
439
}
440
}
441
442