mirror of
https://github.com/facebook/rocksdb.git
synced 2024-11-26 16:30:56 +00:00
5cf6ab6f31
Summary: Ran `find ./db/ -type f | xargs clang-format -i`. Excluded minor changes it tried to make on db/db_impl/. Everything else it changed was directly under db/ directory. Included minor manual touchups mentioned in PR commit history. Pull Request resolved: https://github.com/facebook/rocksdb/pull/10910 Reviewed By: riversand963 Differential Revision: D40880683 Pulled By: ajkr fbshipit-source-id: cfe26cda05b3fb9a72e3cb82c286e21d8c5c4174
659 lines
21 KiB
C++
659 lines
21 KiB
C++
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
|
// This source code is licensed under both the GPLv2 (found in the
|
|
// COPYING file in the root directory) and Apache 2.0 License
|
|
// (found in the LICENSE.Apache file in the root directory).
|
|
|
|
#include "db/db_iter.h"
|
|
#include "db/dbformat.h"
|
|
#include "rocksdb/comparator.h"
|
|
#include "rocksdb/options.h"
|
|
#include "rocksdb/slice.h"
|
|
#include "test_util/testharness.h"
|
|
#include "util/random.h"
|
|
#include "util/string_util.h"
|
|
#include "utilities/merge_operators.h"
|
|
|
|
#ifdef GFLAGS
|
|
|
|
#include "util/gflags_compat.h"
|
|
|
|
using GFLAGS_NAMESPACE::ParseCommandLineFlags;
|
|
|
|
DEFINE_bool(verbose, false,
|
|
"Print huge, detailed trace. Intended for debugging failures.");
|
|
|
|
#else
|
|
|
|
void ParseCommandLineFlags(int*, char***, bool) {}
|
|
bool FLAGS_verbose = false;
|
|
|
|
#endif
|
|
|
|
namespace ROCKSDB_NAMESPACE {
|
|
|
|
class DBIteratorStressTest : public testing::Test {
|
|
public:
|
|
Env* env_;
|
|
|
|
DBIteratorStressTest() : env_(Env::Default()) {}
|
|
};
|
|
|
|
namespace {
|
|
|
|
struct Entry {
|
|
std::string key;
|
|
ValueType type; // kTypeValue, kTypeDeletion, kTypeMerge
|
|
uint64_t sequence;
|
|
std::string ikey; // internal key, made from `key`, `sequence` and `type`
|
|
std::string value;
|
|
// If false, we'll pretend that this entry doesn't exist.
|
|
bool visible = true;
|
|
|
|
bool operator<(const Entry& e) const {
|
|
if (key != e.key) return key < e.key;
|
|
return std::tie(sequence, type) > std::tie(e.sequence, e.type);
|
|
}
|
|
};
|
|
|
|
struct Data {
|
|
std::vector<Entry> entries;
|
|
|
|
// Indices in `entries` with `visible` = false.
|
|
std::vector<size_t> hidden;
|
|
// Keys of entries whose `visible` changed since the last seek of iterators.
|
|
std::set<std::string> recently_touched_keys;
|
|
};
|
|
|
|
struct StressTestIterator : public InternalIterator {
|
|
Data* data;
|
|
Random64* rnd;
|
|
InternalKeyComparator cmp;
|
|
|
|
// Each operation will return error with this probability...
|
|
double error_probability = 0;
|
|
// ... and add/remove entries with this probability.
|
|
double mutation_probability = 0;
|
|
// The probability of adding vs removing entries will be chosen so that the
|
|
// amount of removed entries stays somewhat close to this number.
|
|
double target_hidden_fraction = 0;
|
|
// If true, print all mutations to stdout for debugging.
|
|
bool trace = false;
|
|
|
|
int iter = -1;
|
|
Status status_;
|
|
|
|
StressTestIterator(Data* _data, Random64* _rnd, const Comparator* _cmp)
|
|
: data(_data), rnd(_rnd), cmp(_cmp) {}
|
|
|
|
bool Valid() const override {
|
|
if (iter >= 0 && iter < (int)data->entries.size()) {
|
|
assert(status_.ok());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Status status() const override { return status_; }
|
|
|
|
bool MaybeFail() {
|
|
if (rnd->Next() >=
|
|
static_cast<double>(std::numeric_limits<uint64_t>::max()) *
|
|
error_probability) {
|
|
return false;
|
|
}
|
|
if (rnd->Next() % 2) {
|
|
status_ = Status::Incomplete("test");
|
|
} else {
|
|
status_ = Status::IOError("test");
|
|
}
|
|
if (trace) {
|
|
std::cout << "injecting " << status_.ToString() << std::endl;
|
|
}
|
|
iter = -1;
|
|
return true;
|
|
}
|
|
|
|
void MaybeMutate() {
|
|
if (rnd->Next() >=
|
|
static_cast<double>(std::numeric_limits<uint64_t>::max()) *
|
|
mutation_probability) {
|
|
return;
|
|
}
|
|
do {
|
|
// If too many entries are hidden, hide less, otherwise hide more.
|
|
double hide_probability =
|
|
data->hidden.size() > data->entries.size() * target_hidden_fraction
|
|
? 1. / 3
|
|
: 2. / 3;
|
|
if (data->hidden.empty()) {
|
|
hide_probability = 1;
|
|
}
|
|
bool do_hide = rnd->Next() <
|
|
static_cast<double>(std::numeric_limits<uint64_t>::max()) *
|
|
hide_probability;
|
|
if (do_hide) {
|
|
// Hide a random entry.
|
|
size_t idx = rnd->Next() % data->entries.size();
|
|
Entry& e = data->entries[idx];
|
|
if (e.visible) {
|
|
if (trace) {
|
|
std::cout << "hiding idx " << idx << std::endl;
|
|
}
|
|
e.visible = false;
|
|
data->hidden.push_back(idx);
|
|
data->recently_touched_keys.insert(e.key);
|
|
} else {
|
|
// Already hidden. Let's go unhide something instead, just because
|
|
// it's easy and it doesn't really matter what we do.
|
|
do_hide = false;
|
|
}
|
|
}
|
|
if (!do_hide) {
|
|
// Unhide a random entry.
|
|
size_t hi = rnd->Next() % data->hidden.size();
|
|
size_t idx = data->hidden[hi];
|
|
if (trace) {
|
|
std::cout << "unhiding idx " << idx << std::endl;
|
|
}
|
|
Entry& e = data->entries[idx];
|
|
assert(!e.visible);
|
|
e.visible = true;
|
|
data->hidden[hi] = data->hidden.back();
|
|
data->hidden.pop_back();
|
|
data->recently_touched_keys.insert(e.key);
|
|
}
|
|
} while (rnd->Next() % 3 != 0); // do 3 mutations on average
|
|
}
|
|
|
|
void SkipForward() {
|
|
while (iter < (int)data->entries.size() && !data->entries[iter].visible) {
|
|
++iter;
|
|
}
|
|
}
|
|
void SkipBackward() {
|
|
while (iter >= 0 && !data->entries[iter].visible) {
|
|
--iter;
|
|
}
|
|
}
|
|
|
|
void SeekToFirst() override {
|
|
if (MaybeFail()) return;
|
|
MaybeMutate();
|
|
|
|
status_ = Status::OK();
|
|
iter = 0;
|
|
SkipForward();
|
|
}
|
|
void SeekToLast() override {
|
|
if (MaybeFail()) return;
|
|
MaybeMutate();
|
|
|
|
status_ = Status::OK();
|
|
iter = (int)data->entries.size() - 1;
|
|
SkipBackward();
|
|
}
|
|
|
|
void Seek(const Slice& target) override {
|
|
if (MaybeFail()) return;
|
|
MaybeMutate();
|
|
|
|
status_ = Status::OK();
|
|
// Binary search.
|
|
auto it = std::partition_point(
|
|
data->entries.begin(), data->entries.end(),
|
|
[&](const Entry& e) { return cmp.Compare(e.ikey, target) < 0; });
|
|
iter = (int)(it - data->entries.begin());
|
|
SkipForward();
|
|
}
|
|
void SeekForPrev(const Slice& target) override {
|
|
if (MaybeFail()) return;
|
|
MaybeMutate();
|
|
|
|
status_ = Status::OK();
|
|
// Binary search.
|
|
auto it = std::partition_point(
|
|
data->entries.begin(), data->entries.end(),
|
|
[&](const Entry& e) { return cmp.Compare(e.ikey, target) <= 0; });
|
|
iter = (int)(it - data->entries.begin());
|
|
--iter;
|
|
SkipBackward();
|
|
}
|
|
|
|
void Next() override {
|
|
assert(Valid());
|
|
if (MaybeFail()) return;
|
|
MaybeMutate();
|
|
++iter;
|
|
SkipForward();
|
|
}
|
|
void Prev() override {
|
|
assert(Valid());
|
|
if (MaybeFail()) return;
|
|
MaybeMutate();
|
|
--iter;
|
|
SkipBackward();
|
|
}
|
|
|
|
Slice key() const override {
|
|
assert(Valid());
|
|
return data->entries[iter].ikey;
|
|
}
|
|
Slice value() const override {
|
|
assert(Valid());
|
|
return data->entries[iter].value;
|
|
}
|
|
|
|
bool IsKeyPinned() const override { return true; }
|
|
bool IsValuePinned() const override { return true; }
|
|
};
|
|
|
|
// A small reimplementation of DBIter, supporting only some of the features,
|
|
// and doing everything in O(log n).
|
|
// Skips all keys that are in recently_touched_keys.
|
|
struct ReferenceIterator {
|
|
Data* data;
|
|
uint64_t sequence; // ignore entries with sequence number below this
|
|
|
|
bool valid = false;
|
|
std::string key;
|
|
std::string value;
|
|
|
|
ReferenceIterator(Data* _data, uint64_t _sequence)
|
|
: data(_data), sequence(_sequence) {}
|
|
|
|
bool Valid() const { return valid; }
|
|
|
|
// Finds the first entry with key
|
|
// greater/less/greater-or-equal/less-or-equal than `key`, depending on
|
|
// arguments: if `skip`, inequality is strict; if `forward`, it's
|
|
// greater/greater-or-equal, otherwise less/less-or-equal.
|
|
// Sets `key` to the result.
|
|
// If no such key exists, returns false. Doesn't check `visible`.
|
|
bool FindNextKey(bool skip, bool forward) {
|
|
valid = false;
|
|
auto it = std::partition_point(data->entries.begin(), data->entries.end(),
|
|
[&](const Entry& e) {
|
|
if (forward != skip) {
|
|
return e.key < key;
|
|
} else {
|
|
return e.key <= key;
|
|
}
|
|
});
|
|
if (forward) {
|
|
if (it != data->entries.end()) {
|
|
key = it->key;
|
|
return true;
|
|
}
|
|
} else {
|
|
if (it != data->entries.begin()) {
|
|
--it;
|
|
key = it->key;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FindValueForCurrentKey() {
|
|
if (data->recently_touched_keys.count(key)) {
|
|
return false;
|
|
}
|
|
|
|
// Find the first entry for the key. The caller promises that it exists.
|
|
auto it = std::partition_point(data->entries.begin(), data->entries.end(),
|
|
[&](const Entry& e) {
|
|
if (e.key != key) {
|
|
return e.key < key;
|
|
}
|
|
return e.sequence > sequence;
|
|
});
|
|
|
|
// Find the first visible entry.
|
|
for (;; ++it) {
|
|
if (it == data->entries.end()) {
|
|
return false;
|
|
}
|
|
Entry& e = *it;
|
|
if (e.key != key) {
|
|
return false;
|
|
}
|
|
assert(e.sequence <= sequence);
|
|
if (!e.visible) continue;
|
|
if (e.type == kTypeDeletion) {
|
|
return false;
|
|
}
|
|
if (e.type == kTypeValue) {
|
|
value = e.value;
|
|
valid = true;
|
|
return true;
|
|
}
|
|
assert(e.type == kTypeMerge);
|
|
break;
|
|
}
|
|
|
|
// Collect merge operands.
|
|
std::vector<Slice> operands;
|
|
for (; it != data->entries.end(); ++it) {
|
|
Entry& e = *it;
|
|
if (e.key != key) {
|
|
break;
|
|
}
|
|
assert(e.sequence <= sequence);
|
|
if (!e.visible) continue;
|
|
if (e.type == kTypeDeletion) {
|
|
break;
|
|
}
|
|
operands.push_back(e.value);
|
|
if (e.type == kTypeValue) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Do a merge.
|
|
value = operands.back().ToString();
|
|
for (int i = (int)operands.size() - 2; i >= 0; --i) {
|
|
value.append(",");
|
|
value.append(operands[i].data(), operands[i].size());
|
|
}
|
|
|
|
valid = true;
|
|
return true;
|
|
}
|
|
|
|
// Start at `key` and move until we encounter a valid value.
|
|
// `forward` defines the direction of movement.
|
|
// If `skip` is true, we're looking for key not equal to `key`.
|
|
void DoTheThing(bool skip, bool forward) {
|
|
while (FindNextKey(skip, forward) && !FindValueForCurrentKey()) {
|
|
skip = true;
|
|
}
|
|
}
|
|
|
|
void Seek(const Slice& target) {
|
|
key = target.ToString();
|
|
DoTheThing(false, true);
|
|
}
|
|
void SeekForPrev(const Slice& target) {
|
|
key = target.ToString();
|
|
DoTheThing(false, false);
|
|
}
|
|
void SeekToFirst() { Seek(""); }
|
|
void SeekToLast() {
|
|
key = data->entries.back().key;
|
|
DoTheThing(false, false);
|
|
}
|
|
void Next() {
|
|
assert(Valid());
|
|
DoTheThing(true, true);
|
|
}
|
|
void Prev() {
|
|
assert(Valid());
|
|
DoTheThing(true, false);
|
|
}
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
// Use an internal iterator that sometimes returns errors and sometimes
|
|
// adds/removes entries on the fly. Do random operations on a DBIter and
|
|
// check results.
|
|
// TODO: can be improved for more coverage:
|
|
// * Override IsKeyPinned() and IsValuePinned() to actually use
|
|
// PinnedIteratorManager and check that there's no use-after free.
|
|
// * Try different combinations of prefix_extractor, total_order_seek,
|
|
// prefix_same_as_start, iterate_lower_bound, iterate_upper_bound.
|
|
TEST_F(DBIteratorStressTest, StressTest) {
|
|
// We use a deterministic RNG, and everything happens in a single thread.
|
|
Random64 rnd(826909345792864532ll);
|
|
|
|
auto gen_key = [&](int max_key) {
|
|
assert(max_key > 0);
|
|
int len = 0;
|
|
int a = max_key;
|
|
while (a) {
|
|
a /= 10;
|
|
++len;
|
|
}
|
|
std::string s = std::to_string(rnd.Next() % static_cast<uint64_t>(max_key));
|
|
s.insert(0, len - (int)s.size(), '0');
|
|
return s;
|
|
};
|
|
|
|
Options options;
|
|
options.merge_operator = MergeOperators::CreateFromStringId("stringappend");
|
|
ReadOptions ropt;
|
|
|
|
size_t num_matching = 0;
|
|
size_t num_at_end = 0;
|
|
size_t num_not_ok = 0;
|
|
size_t num_recently_removed = 0;
|
|
|
|
// Number of iterations for each combination of parameters
|
|
// (there are ~250 of those).
|
|
// Tweak this to change the test run time.
|
|
// As of the time of writing, the test takes ~4 seconds for value of 5000.
|
|
const int num_iterations = 5000;
|
|
// Enable this to print all the operations for debugging.
|
|
bool trace = FLAGS_verbose;
|
|
|
|
for (int num_entries : {5, 10, 100}) {
|
|
for (double key_space : {0.1, 1.0, 3.0}) {
|
|
for (ValueType prevalent_entry_type :
|
|
{kTypeValue, kTypeDeletion, kTypeMerge}) {
|
|
for (double error_probability : {0.01, 0.1}) {
|
|
for (double mutation_probability : {0.01, 0.5}) {
|
|
for (double target_hidden_fraction : {0.1, 0.5}) {
|
|
std::string trace_str =
|
|
"entries: " + std::to_string(num_entries) +
|
|
", key_space: " + std::to_string(key_space) +
|
|
", error_probability: " + std::to_string(error_probability) +
|
|
", mutation_probability: " +
|
|
std::to_string(mutation_probability) +
|
|
", target_hidden_fraction: " +
|
|
std::to_string(target_hidden_fraction);
|
|
SCOPED_TRACE(trace_str);
|
|
if (trace) {
|
|
std::cout << trace_str << std::endl;
|
|
}
|
|
|
|
// Generate data.
|
|
Data data;
|
|
int max_key = (int)(num_entries * key_space) + 1;
|
|
for (int i = 0; i < num_entries; ++i) {
|
|
Entry e;
|
|
e.key = gen_key(max_key);
|
|
if (rnd.Next() % 10 != 0) {
|
|
e.type = prevalent_entry_type;
|
|
} else {
|
|
const ValueType types[] = {kTypeValue, kTypeDeletion,
|
|
kTypeMerge};
|
|
e.type =
|
|
types[rnd.Next() % (sizeof(types) / sizeof(types[0]))];
|
|
}
|
|
e.sequence = i;
|
|
e.value = "v" + std::to_string(i);
|
|
ParsedInternalKey internal_key(e.key, e.sequence, e.type);
|
|
AppendInternalKey(&e.ikey, internal_key);
|
|
|
|
data.entries.push_back(e);
|
|
}
|
|
std::sort(data.entries.begin(), data.entries.end());
|
|
if (trace) {
|
|
std::cout << "entries:";
|
|
for (size_t i = 0; i < data.entries.size(); ++i) {
|
|
Entry& e = data.entries[i];
|
|
std::cout << "\n idx " << i << ": \"" << e.key << "\": \""
|
|
<< e.value << "\" seq: " << e.sequence << " type: "
|
|
<< (e.type == kTypeValue ? "val"
|
|
: e.type == kTypeDeletion ? "del"
|
|
: "merge");
|
|
}
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
std::unique_ptr<Iterator> db_iter;
|
|
std::unique_ptr<ReferenceIterator> ref_iter;
|
|
for (int iteration = 0; iteration < num_iterations; ++iteration) {
|
|
SCOPED_TRACE(iteration);
|
|
// Create a new iterator every ~30 operations.
|
|
if (db_iter == nullptr || rnd.Next() % 30 == 0) {
|
|
uint64_t sequence = rnd.Next() % (data.entries.size() + 2);
|
|
ref_iter.reset(new ReferenceIterator(&data, sequence));
|
|
if (trace) {
|
|
std::cout << "new iterator, seq: " << sequence << std::endl;
|
|
}
|
|
|
|
auto internal_iter =
|
|
new StressTestIterator(&data, &rnd, BytewiseComparator());
|
|
internal_iter->error_probability = error_probability;
|
|
internal_iter->mutation_probability = mutation_probability;
|
|
internal_iter->target_hidden_fraction =
|
|
target_hidden_fraction;
|
|
internal_iter->trace = trace;
|
|
db_iter.reset(NewDBIterator(
|
|
env_, ropt, ImmutableOptions(options),
|
|
MutableCFOptions(options), BytewiseComparator(),
|
|
internal_iter, nullptr /* version */, sequence,
|
|
options.max_sequential_skip_in_iterations,
|
|
nullptr /*read_callback*/));
|
|
}
|
|
|
|
// Do a random operation. It's important to do it on ref_it
|
|
// later than on db_iter to make sure ref_it sees the correct
|
|
// recently_touched_keys.
|
|
std::string old_key;
|
|
bool forward = rnd.Next() % 2 > 0;
|
|
// Do Next()/Prev() ~90% of the time.
|
|
bool seek = !ref_iter->Valid() || rnd.Next() % 10 == 0;
|
|
if (trace) {
|
|
std::cout << iteration << ": ";
|
|
}
|
|
|
|
if (!seek) {
|
|
assert(db_iter->Valid());
|
|
old_key = ref_iter->key;
|
|
if (trace) {
|
|
std::cout << (forward ? "Next" : "Prev") << std::endl;
|
|
}
|
|
|
|
if (forward) {
|
|
db_iter->Next();
|
|
ref_iter->Next();
|
|
} else {
|
|
db_iter->Prev();
|
|
ref_iter->Prev();
|
|
}
|
|
} else {
|
|
data.recently_touched_keys.clear();
|
|
// Do SeekToFirst less often than Seek.
|
|
if (rnd.Next() % 4 == 0) {
|
|
if (trace) {
|
|
std::cout << (forward ? "SeekToFirst" : "SeekToLast")
|
|
<< std::endl;
|
|
}
|
|
|
|
if (forward) {
|
|
old_key = "";
|
|
db_iter->SeekToFirst();
|
|
ref_iter->SeekToFirst();
|
|
} else {
|
|
old_key = data.entries.back().key;
|
|
db_iter->SeekToLast();
|
|
ref_iter->SeekToLast();
|
|
}
|
|
} else {
|
|
old_key = gen_key(max_key);
|
|
if (trace) {
|
|
std::cout << (forward ? "Seek" : "SeekForPrev") << " \""
|
|
<< old_key << '"' << std::endl;
|
|
}
|
|
if (forward) {
|
|
db_iter->Seek(old_key);
|
|
ref_iter->Seek(old_key);
|
|
} else {
|
|
db_iter->SeekForPrev(old_key);
|
|
ref_iter->SeekForPrev(old_key);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check the result.
|
|
if (db_iter->Valid()) {
|
|
ASSERT_TRUE(db_iter->status().ok());
|
|
if (data.recently_touched_keys.count(
|
|
db_iter->key().ToString())) {
|
|
// Ended on a key that may have been mutated during the
|
|
// operation. Reference iterator skips such keys, so we
|
|
// can't check the exact result.
|
|
|
|
// Check that the key moved in the right direction.
|
|
if (forward) {
|
|
if (seek)
|
|
ASSERT_GE(db_iter->key().ToString(), old_key);
|
|
else
|
|
ASSERT_GT(db_iter->key().ToString(), old_key);
|
|
} else {
|
|
if (seek)
|
|
ASSERT_LE(db_iter->key().ToString(), old_key);
|
|
else
|
|
ASSERT_LT(db_iter->key().ToString(), old_key);
|
|
}
|
|
|
|
if (ref_iter->Valid()) {
|
|
// Check that DBIter didn't miss any non-mutated key.
|
|
if (forward) {
|
|
ASSERT_LT(db_iter->key().ToString(), ref_iter->key);
|
|
} else {
|
|
ASSERT_GT(db_iter->key().ToString(), ref_iter->key);
|
|
}
|
|
}
|
|
// Tell the next iteration of the loop to reseek the
|
|
// iterators.
|
|
ref_iter->valid = false;
|
|
|
|
++num_recently_removed;
|
|
} else {
|
|
ASSERT_TRUE(ref_iter->Valid());
|
|
ASSERT_EQ(ref_iter->key, db_iter->key().ToString());
|
|
ASSERT_EQ(ref_iter->value, db_iter->value());
|
|
++num_matching;
|
|
}
|
|
} else if (db_iter->status().ok()) {
|
|
ASSERT_FALSE(ref_iter->Valid());
|
|
++num_at_end;
|
|
} else {
|
|
// Non-ok status. Nothing to check here.
|
|
// Tell the next iteration of the loop to reseek the
|
|
// iterators.
|
|
ref_iter->valid = false;
|
|
++num_not_ok;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check that all cases were hit many times.
|
|
EXPECT_GT(num_matching, 10000);
|
|
EXPECT_GT(num_at_end, 10000);
|
|
EXPECT_GT(num_not_ok, 10000);
|
|
EXPECT_GT(num_recently_removed, 10000);
|
|
|
|
std::cout << "stats:\n exact matches: " << num_matching
|
|
<< "\n end reached: " << num_at_end
|
|
<< "\n non-ok status: " << num_not_ok
|
|
<< "\n mutated on the fly: " << num_recently_removed << std::endl;
|
|
}
|
|
|
|
} // namespace ROCKSDB_NAMESPACE
|
|
|
|
int main(int argc, char** argv) {
|
|
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
ParseCommandLineFlags(&argc, &argv, true);
|
|
return RUN_ALL_TESTS();
|
|
}
|