tdlight/tdutils/td/utils/tests.cpp

280 lines
7.1 KiB
C++

//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2024
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "td/utils/tests.h"
#include "td/utils/crypto.h"
#include "td/utils/filesystem.h"
#include "td/utils/Parser.h"
#include "td/utils/PathView.h"
#include "td/utils/port/path.h"
#include "td/utils/port/Stat.h"
#include "td/utils/Random.h"
#include "td/utils/ScopeGuard.h"
#include "td/utils/SliceBuilder.h"
#include "td/utils/StringBuilder.h"
#include "td/utils/Time.h"
#include <map>
namespace td {
string rand_string(int from, int to, size_t len) {
string res(len, '\0');
for (auto &c : res) {
c = static_cast<char>(Random::fast(from, to));
}
return res;
}
vector<string> rand_split(Slice str) {
vector<string> res;
size_t pos = 0;
while (pos < str.size()) {
size_t len;
if (Random::fast_bool()) {
len = Random::fast(1, 10);
} else {
len = Random::fast(100, 200);
}
res.push_back(str.substr(pos, len).str());
pos += len;
}
return res;
}
struct TestInfo {
string name;
string result_hash; // base64
};
StringBuilder &operator<<(StringBuilder &sb, const TestInfo &info) {
// should I use JSON?
CHECK(!info.name.empty());
CHECK(!info.result_hash.empty());
return sb << info.name << " " << info.result_hash << "\n";
}
class RegressionTesterImpl final : public RegressionTester {
public:
static void destroy(CSlice db_path) {
unlink(db_path).ignore();
}
RegressionTesterImpl(string db_path, string db_cache_dir)
: db_path_(std::move(db_path)), db_cache_dir_(std::move(db_cache_dir)) {
load_db(db_path_).ignore();
if (db_cache_dir_.empty()) {
db_cache_dir_ = PathView(db_path_).without_extension().str() + ".cache/";
}
mkdir(db_cache_dir_).ensure();
}
Status verify_test(Slice name, Slice result) final {
#if TD_HAVE_OPENSSL
auto hash = PSTRING() << format::as_hex_dump<0>(Slice(sha256(result)));
#else
auto hash = to_string(crc64(result));
#endif
TestInfo &old_test_info = tests_[name.str()];
if (!old_test_info.result_hash.empty() && old_test_info.result_hash != hash) {
auto wa_path = db_cache_dir_ + "WA";
write_file(wa_path, result).ensure();
return Status::Error(PSLICE() << "Test " << name << " changed: " << tag("expected", old_test_info.result_hash)
<< tag("received", hash));
}
auto result_cache_path = db_cache_dir_ + hash;
if (stat(result_cache_path).is_error()) {
write_file(result_cache_path, result).ensure();
}
if (!old_test_info.result_hash.empty()) {
return Status::OK();
}
old_test_info.name = name.str();
old_test_info.result_hash = hash;
is_dirty_ = true;
return Status::OK();
}
void save_db() final {
if (!is_dirty_) {
return;
}
SCOPE_EXIT {
is_dirty_ = false;
};
string buf(2000000, ' ');
StringBuilder sb(buf);
save_db(sb);
string new_db_path = db_path_ + ".new";
write_file(new_db_path, sb.as_cslice()).ensure();
rename(new_db_path, db_path_).ensure();
}
Slice magic() const {
return Slice("abce");
}
void save_db(StringBuilder &sb) {
sb << magic() << "\n";
for (const auto &it : tests_) {
sb << it.second;
}
}
Status load_db(CSlice path) {
TRY_RESULT(data, read_file(path));
ConstParser parser(data.as_slice());
auto db_magic = parser.read_word();
if (db_magic != magic()) {
return Status::Error(PSLICE() << "Wrong magic " << db_magic);
}
while (true) {
TestInfo info;
info.name = parser.read_word().str();
if (info.name.empty()) {
break;
}
info.result_hash = parser.read_word().str();
tests_[info.name] = info;
}
return Status::OK();
}
private:
string db_path_;
string db_cache_dir_;
bool is_dirty_{false};
std::map<string, TestInfo> tests_;
};
void RegressionTester::destroy(CSlice path) {
RegressionTesterImpl::destroy(path);
}
unique_ptr<RegressionTester> RegressionTester::create(string db_path, string db_cache_dir) {
return td::make_unique<RegressionTesterImpl>(std::move(db_path), std::move(db_cache_dir));
}
TestsRunner &TestsRunner::get_default() {
static TestsRunner default_runner;
return default_runner;
}
void TestsRunner::add_test(string name, std::function<unique_ptr<Test>()> test) {
for (auto &it : tests_) {
if (it.first == name) {
LOG(FATAL) << "Test name collision " << name;
}
}
tests_.emplace_back(std::move(name), TestInfo{std::move(test), nullptr});
}
void TestsRunner::add_substr_filter(string str) {
if (str[0] != '+' && str[0] != '-') {
str = "+" + str;
}
substr_filters_.push_back(std::move(str));
}
void TestsRunner::set_offset(string str) {
offset_ = std::move(str);
}
void TestsRunner::set_regression_tester(unique_ptr<RegressionTester> regression_tester) {
regression_tester_ = std::move(regression_tester);
}
void TestsRunner::set_stress_flag(bool flag) {
stress_flag_ = flag;
}
void TestsRunner::run_all() {
while (run_all_step()) {
}
}
bool TestsRunner::run_all_step() {
Guard guard(this);
if (state_.it == state_.end) {
state_.end = tests_.size();
state_.it = 0;
}
bool skip_tests = true;
while (state_.it != state_.end) {
auto &name = tests_[state_.it].first;
auto &test = tests_[state_.it].second.test;
if (!state_.is_running) {
bool ok = true;
for (const auto &filter : substr_filters_) {
bool is_match = name.find(filter.substr(1)) != string::npos;
if (is_match != (filter[0] == '+')) {
ok = false;
break;
}
}
if (name.find(offset_) != string::npos) {
skip_tests = false;
}
if (!ok || skip_tests) {
++state_.it;
continue;
}
LOG(ERROR) << "Run test " << tag("name", name);
state_.start = Time::now();
state_.start_unadjusted = Time::now_unadjusted();
state_.is_running = true;
CHECK(!test);
test = tests_[state_.it].second.creator();
}
if (test->step()) {
break;
}
test = {};
auto passed = Time::now() - state_.start;
auto real_passed = Time::now_unadjusted() - state_.start_unadjusted;
if (real_passed + 1e-1 > passed) {
LOG(ERROR) << format::as_time(passed);
} else {
LOG(ERROR) << format::as_time(real_passed) << " adjusted [" << format::as_time(real_passed) << "]";
}
if (regression_tester_) {
regression_tester_->save_db();
}
state_.is_running = false;
++state_.it;
}
auto ret = state_.it != state_.end;
if (!ret) {
state_ = State();
}
return ret || stress_flag_;
}
Slice TestsRunner::name() {
CHECK(state_.is_running);
return tests_[state_.it].first;
}
Status TestsRunner::verify(Slice data) {
if (!regression_tester_) {
LOG(INFO) << data;
LOG(ERROR) << "Cannot verify and save <" << name() << "> answer. Use --regression <regression_db> option";
return Status::OK();
}
return regression_tester_->verify_test(PSLICE() << name() << "_default", data);
}
} // namespace td