aboutsummaryrefslogtreecommitdiffstats
path: root/library/cpp/testing
diff options
context:
space:
mode:
authorDevtools Arcadia <arcadia-devtools@yandex-team.ru>2022-02-07 18:08:42 +0300
committerDevtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net>2022-02-07 18:08:42 +0300
commit1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch)
treee26c9fed0de5d9873cce7e00bc214573dc2195b7 /library/cpp/testing
downloadydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'library/cpp/testing')
-rw-r--r--library/cpp/testing/README.md16
-rw-r--r--library/cpp/testing/benchmark/README.md11
-rw-r--r--library/cpp/testing/benchmark/bench.cpp604
-rw-r--r--library/cpp/testing/benchmark/bench.h92
-rw-r--r--library/cpp/testing/benchmark/dummy.cpp8
-rw-r--r--library/cpp/testing/benchmark/examples/main.cpp215
-rw-r--r--library/cpp/testing/benchmark/examples/metrics/main.py7
-rw-r--r--library/cpp/testing/benchmark/examples/metrics/ya.make20
-rw-r--r--library/cpp/testing/benchmark/examples/ya.make12
-rw-r--r--library/cpp/testing/benchmark/main/main.cpp16
-rw-r--r--library/cpp/testing/benchmark/main/ya.make16
-rw-r--r--library/cpp/testing/benchmark/ya.make22
-rw-r--r--library/cpp/testing/common/env.cpp275
-rw-r--r--library/cpp/testing/common/env.h84
-rw-r--r--library/cpp/testing/common/network.cpp208
-rw-r--r--library/cpp/testing/common/network.h52
-rw-r--r--library/cpp/testing/common/probe.cpp1
-rw-r--r--library/cpp/testing/common/probe.h140
-rw-r--r--library/cpp/testing/common/scope.cpp1
-rw-r--r--library/cpp/testing/common/scope.h39
-rw-r--r--library/cpp/testing/common/ut/env_ut.cpp162
-rw-r--r--library/cpp/testing/common/ut/network_ut.cpp54
-rw-r--r--library/cpp/testing/common/ut/scope_ut.cpp28
-rw-r--r--library/cpp/testing/common/ut/ya.make19
-rw-r--r--library/cpp/testing/common/ya.make23
-rw-r--r--library/cpp/testing/gmock_in_unittest/events.cpp32
-rw-r--r--library/cpp/testing/gmock_in_unittest/events.h8
-rw-r--r--library/cpp/testing/gmock_in_unittest/example_ut/example_ut.cpp105
-rw-r--r--library/cpp/testing/gmock_in_unittest/example_ut/ya.make13
-rw-r--r--library/cpp/testing/gmock_in_unittest/gmock.h5
-rw-r--r--library/cpp/testing/gmock_in_unittest/registration.cpp20
-rw-r--r--library/cpp/testing/gmock_in_unittest/ya.make17
-rw-r--r--library/cpp/testing/gtest_extensions/README.md5
-rw-r--r--library/cpp/testing/gtest_extensions/assertions.cpp90
-rw-r--r--library/cpp/testing/gtest_extensions/assertions.h111
-rw-r--r--library/cpp/testing/gtest_extensions/gtest_extensions.cpp1
-rw-r--r--library/cpp/testing/gtest_extensions/gtest_extensions.h6
-rw-r--r--library/cpp/testing/gtest_extensions/matchers.cpp1
-rw-r--r--library/cpp/testing/gtest_extensions/matchers.h132
-rw-r--r--library/cpp/testing/gtest_extensions/pretty_printers.cpp1
-rw-r--r--library/cpp/testing/gtest_extensions/pretty_printers.h84
-rw-r--r--library/cpp/testing/gtest_extensions/probe.cpp13
-rw-r--r--library/cpp/testing/gtest_extensions/probe.h81
-rw-r--r--library/cpp/testing/gtest_extensions/ut/README.md1
-rw-r--r--library/cpp/testing/gtest_extensions/ut/gtest_extensions_ut.cpp346
-rw-r--r--library/cpp/testing/gtest_extensions/ut/probe_ut.cpp54
-rw-r--r--library/cpp/testing/gtest_extensions/ut/ya.make20
-rw-r--r--library/cpp/testing/gtest_extensions/ya.make26
-rw-r--r--library/cpp/testing/hook/README.md25
-rw-r--r--library/cpp/testing/hook/hook.cpp45
-rw-r--r--library/cpp/testing/hook/hook.h128
-rw-r--r--library/cpp/testing/hook/ya.make13
-rw-r--r--library/cpp/testing/unittest/checks.cpp31
-rw-r--r--library/cpp/testing/unittest/env.h3
-rw-r--r--library/cpp/testing/unittest/example_ut.cpp12
-rw-r--r--library/cpp/testing/unittest/fat/test_port_manager.cpp36
-rw-r--r--library/cpp/testing/unittest/fat/ya.make19
-rw-r--r--library/cpp/testing/unittest/gtest.cpp67
-rw-r--r--library/cpp/testing/unittest/gtest.h108
-rw-r--r--library/cpp/testing/unittest/plugin.cpp50
-rw-r--r--library/cpp/testing/unittest/plugin.h29
-rw-r--r--library/cpp/testing/unittest/registar.cpp513
-rw-r--r--library/cpp/testing/unittest/registar.h1013
-rw-r--r--library/cpp/testing/unittest/registar_ut.cpp360
-rw-r--r--library/cpp/testing/unittest/simple.h39
-rw-r--r--library/cpp/testing/unittest/tests_data.cpp104
-rw-r--r--library/cpp/testing/unittest/tests_data.h54
-rw-r--r--library/cpp/testing/unittest/ut/main.cpp93
-rw-r--r--library/cpp/testing/unittest/ut/ya.make10
-rw-r--r--library/cpp/testing/unittest/utmain.cpp771
-rw-r--r--library/cpp/testing/unittest/utmain.h5
-rw-r--r--library/cpp/testing/unittest/ya.make33
-rw-r--r--library/cpp/testing/unittest_main/main.cpp5
-rw-r--r--library/cpp/testing/unittest_main/ya.make14
-rw-r--r--library/cpp/testing/ya.make24
75 files changed, 6901 insertions, 0 deletions
diff --git a/library/cpp/testing/README.md b/library/cpp/testing/README.md
new file mode 100644
index 0000000000..e8e0391be4
--- /dev/null
+++ b/library/cpp/testing/README.md
@@ -0,0 +1,16 @@
+В этой директории лежат библиотеки для удобного написания тестов на C++, а именно:
+
+* `benchmark` — библиотека для реализации простых бенчмарков.
+* `boost_test` — реализация тестирования средствами библиотеки _boost_. **Не используйте этот фреймворк в новом коде.**
+* `boost_test_main` — реализация (средствами библиотеки _boost_) функции `int main(argc, argv)` для модуля `BOOSTTEST`. **Не используйте этот фреймворк в новом коде.**
+* `common` — независимые вспомогательные функции. Например функции для получения аркадийных путей.
+* `gmock` — прокси-библиотека для подключения `contrib/resticted/googltest/googlemock` без нарушения PEERDIR policy.
+* `gtest` — реализация модуля `GTEST` — средства для интеграции фреймворка _googletest_ в Аркадию.
+* `gtest_boost_extensions` — расширения gtest и gmock, улучшающие поддержку типов из boost.
+* `gtest_extensions` — расширения gtest и gmock, улучшающие поддержку Аркадийных типов. Все расширения включены в модуле `GTEST` по-умолчаниiю.
+* `gtest_main` — реализация `int main(argc, argv)` для модуля `GTEST` (вынесена в отдельную библиотеку, чтобы в перспективе была возможна реализация `GTEST_WITH_CUSTOM_ENTRY_POINT`).
+* `gtest_protobuf` — утилиты для работы с протобуфом в тестах.
+* `hook` — хуки для выполнения пользовательских функций в тестах и бенчмарках.
+* `mock_server` — реализация http-сервера для тестов.
+* `unittest` — реализация модуля UNITTEST — основного средства для тестирования тестов на С++ в Аркадии.
+* `unittest_main` — реализация `int main(argc, argv)` для модуля UNITTEST (она вынесена в отдельную библиотеку, чтобы оставить возможность для реализации `UNITTEST_WITH_CUSTOM_ENTRY_POINT` и `YT_UNITTEST`.
diff --git a/library/cpp/testing/benchmark/README.md b/library/cpp/testing/benchmark/README.md
new file mode 100644
index 0000000000..ad440e07a2
--- /dev/null
+++ b/library/cpp/testing/benchmark/README.md
@@ -0,0 +1,11 @@
+----------- YourBenchmarkName ---------------
+ samples: 403
+ iterations: 100576
+ iterations hr: 101K
+ run time: 5.016924443
+ per iteration: 119265.0829 (119K) cycles
+
+samples – сколько раз была вызвана функция с бенчмарком
+iterations – сколько всего итераций было сделано. при каждом вызове функции бенчмарка может быть разное кол-во итераций
+run time – скольку времени исполнялся бенчмарк
+per iteration – сколько времени (процессорных тактов) ушло на одну итерацию, в идеале это то сколько работает функция для которой ты написал бенчмарк \ No newline at end of file
diff --git a/library/cpp/testing/benchmark/bench.cpp b/library/cpp/testing/benchmark/bench.cpp
new file mode 100644
index 0000000000..08d8708005
--- /dev/null
+++ b/library/cpp/testing/benchmark/bench.cpp
@@ -0,0 +1,604 @@
+#include "bench.h"
+
+#include <contrib/libs/re2/re2/re2.h>
+
+#include <library/cpp/colorizer/output.h>
+#include <library/cpp/getopt/small/last_getopt.h>
+#include <library/cpp/json/json_value.h>
+#include <library/cpp/linear_regression/linear_regression.h>
+#include <library/cpp/threading/poor_man_openmp/thread_helper.h>
+
+#include <util/system/hp_timer.h>
+#include <util/system/info.h>
+#include <util/stream/output.h>
+#include <util/datetime/base.h>
+#include <util/random/random.h>
+#include <util/string/cast.h>
+#include <util/generic/xrange.h>
+#include <util/generic/algorithm.h>
+#include <util/generic/singleton.h>
+#include <util/system/spinlock.h>
+#include <util/generic/function.h>
+#include <util/generic/maybe.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/intrlist.h>
+#include <util/stream/format.h>
+#include <util/system/yield.h>
+
+using re2::RE2;
+
+using namespace NBench;
+using namespace NColorizer;
+using namespace NLastGetopt;
+
+namespace {
+ struct TOptions {
+ double TimeBudget;
+ };
+
+ struct TResult {
+ TStringBuf TestName;
+ ui64 Samples;
+ ui64 Iterations;
+ TMaybe<double> CyclesPerIteration;
+ TMaybe<double> SecondsPerIteration;
+ double RunTime;
+ size_t TestId; // Sequential test id (zero-based)
+ };
+
+ struct ITestRunner: public TIntrusiveListItem<ITestRunner> {
+ virtual ~ITestRunner() = default;
+ void Register();
+
+ virtual TStringBuf Name() const noexcept = 0;
+ virtual TResult Run(const TOptions& opts) = 0;
+ size_t SequentialId = 0;
+ };
+
+ struct TCpuBenchmark: public ITestRunner {
+ inline TCpuBenchmark(const char* name, NCpu::TUserFunc func)
+ : F(func)
+ , N(name)
+ {
+ Register();
+ }
+
+ TResult Run(const TOptions& opts) override;
+
+ TStringBuf Name() const noexcept override {
+ return N;
+ }
+
+ std::function<NCpu::TUserFunc> F;
+ const TStringBuf N;
+ };
+
+ inline TString DoFmtTime(double t) {
+ if (t > 0.1) {
+ return ToString(t) + " seconds";
+ }
+
+ t *= 1000.0;
+
+ if (t > 0.1) {
+ return ToString(t) + " milliseconds";
+ }
+
+ t *= 1000.0;
+
+ if (t > 0.1) {
+ return ToString(t) + " microseconds";
+ }
+
+ t *= 1000.0;
+
+ if (t < 0.05) {
+ t = 0.0;
+ }
+
+ return ToString(t) + " nanoseconds";
+ }
+
+ struct THiPerfTimer: public THPTimer {
+ static inline TString FmtTime(double t) {
+ return DoFmtTime(t);
+ }
+ };
+
+ struct TSimpleTimer {
+ inline double Passed() const noexcept {
+ return (TInstant::Now() - N).MicroSeconds() / 1000000.0;
+ }
+
+ static inline TString FmtTime(double t) {
+ return DoFmtTime(t);
+ }
+
+ const TInstant N = TInstant::Now();
+ };
+
+ struct TCycleTimer {
+ inline ui64 Passed() const noexcept {
+ return GetCycleCount() - N;
+ }
+
+ static inline TString FmtTime(double t) {
+ if (t < 0.5) {
+ t = 0.0;
+ }
+
+ TString hr;
+ if (t > 10 * 1000) {
+ hr = " (" + ToString(HumanReadableSize(t, ESizeFormat::SF_QUANTITY)) + ")";
+ }
+
+ return ToString(t) + hr + " cycles";
+ }
+
+ const ui64 N = GetCycleCount();
+ };
+
+ template <class TMyTimer, class T>
+ inline double Measure(T&& t, size_t n) {
+ TMyTimer timer;
+
+ t(n);
+
+ return timer.Passed();
+ }
+
+ struct TSampleIterator {
+ inline size_t Next() noexcept {
+ return M++;
+
+ N *= 1.02;
+ M += 1;
+
+ return Max<double>(N, M);
+ }
+
+ double N = 1.0;
+ size_t M = 1;
+ };
+
+ using TSample = std::pair<size_t, double>;
+ using TSamples = TVector<TSample>;
+
+ struct TLinFunc {
+ double A;
+ double B;
+
+ inline double operator()(double x) const noexcept {
+ return A * x + B;
+ }
+ };
+
+ TLinFunc CalcModel(const TSamples& s) {
+ TKahanSLRSolver solver;
+
+ for (const auto& p : s) {
+ solver.Add(p.first, p.second);
+ }
+
+ double c = 0;
+ double i = 0;
+
+ solver.Solve(c, i);
+
+ return TLinFunc{c, i};
+ }
+
+ inline TSamples RemoveOutliers(const TSamples& s, double fraction) {
+ if (s.size() < 20) {
+ return s;
+ }
+
+ const auto predictor = CalcModel(s);
+
+ const auto errfunc = [&predictor](const TSample& p) -> double {
+ //return (1.0 + fabs(predictor(p.first) - p.second)) / (1.0 + fabs(p.second));
+ //return fabs((predictor(p.first) - p.second)) / (1.0 + fabs(p.second));
+ //return fabs((predictor(p.first) - p.second)) / (1.0 + p.first);
+ return fabs((predictor(p.first) - p.second));
+ };
+
+ using TSampleWithError = std::pair<const TSample*, double>;
+ TVector<TSampleWithError> v;
+
+ v.reserve(s.size());
+
+ for (const auto& p : s) {
+ v.emplace_back(&p, errfunc(p));
+ }
+
+ Sort(v.begin(), v.end(), [](const TSampleWithError& l, const TSampleWithError& r) -> bool {
+ return (l.second < r.second) || ((l.second == r.second) && (l.first < r.first));
+ });
+
+ if (0) {
+ for (const auto& x : v) {
+ Cout << x.first->first << ", " << x.first->second << " -> " << x.second << Endl;
+ }
+ }
+
+ TSamples ret;
+
+ ret.reserve(v.size());
+
+ for (const auto i : xrange<size_t>(0, fraction * v.size())) {
+ ret.push_back(*v[i].first);
+ }
+
+ return ret;
+ }
+
+ template <class TMyTimer, class T>
+ static inline TResult RunTest(T&& func, double budget, ITestRunner& test) {
+ THPTimer start;
+
+ start.Passed();
+
+ TSampleIterator sample;
+ TSamples samples;
+ ui64 iters = 0;
+
+ //warm up
+ func(1);
+
+ while (start.Passed() < budget) {
+ if (start.Passed() < ((budget * samples.size()) / 2000000.0)) {
+ ThreadYield();
+ } else {
+ const size_t n = sample.Next();
+
+ iters += (ui64)n;
+ samples.emplace_back(n, Measure<TMyTimer>(func, n));
+ }
+ }
+
+ auto filtered = RemoveOutliers(samples, 0.9);
+
+ return {test.Name(), filtered.size(), iters, CalcModel(filtered).A, Nothing(), start.Passed(), test.SequentialId};
+ }
+
+ using TTests = TIntrusiveListWithAutoDelete<ITestRunner, TDestructor>;
+
+ inline TTests& Tests() {
+ return *Singleton<TTests>();
+ }
+
+ void ITestRunner::Register() {
+ Tests().PushBack(this);
+ }
+
+ TResult TCpuBenchmark::Run(const TOptions& opts) {
+ return RunTest<TCycleTimer>([this](size_t n) {
+ NCpu::TParams params{n};
+
+ F(params);
+ }, opts.TimeBudget, *this);
+ }
+
+ enum EOutFormat {
+ F_CONSOLE = 0 /* "console" */,
+ F_CSV /* "csv" */,
+ F_JSON /* "json" */
+ };
+
+ TAdaptiveLock STDOUT_LOCK;
+
+ struct IReporter {
+ virtual void Report(TResult&& result) = 0;
+
+ virtual void Finish() {
+ }
+
+ virtual ~IReporter() {
+ }
+ };
+
+ class TConsoleReporter: public IReporter {
+ public:
+ ~TConsoleReporter() override {
+ }
+
+ void Report(TResult&& r) override {
+ with_lock (STDOUT_LOCK) {
+ Cout << r;
+ }
+ }
+ };
+
+ class TCSVReporter: public IReporter {
+ public:
+ TCSVReporter() {
+ Cout << "Name\tSamples\tIterations\tRun_time\tPer_iteration_sec\tPer_iteration_cycles" << Endl;
+ }
+
+ ~TCSVReporter() override {
+ }
+
+ void Report(TResult&& r) override {
+ with_lock (STDOUT_LOCK) {
+ Cout << r.TestName
+ << '\t' << r.Samples
+ << '\t' << r.Iterations
+ << '\t' << r.RunTime;
+
+ Cout << '\t';
+ if (r.CyclesPerIteration) {
+ Cout << TCycleTimer::FmtTime(*r.CyclesPerIteration);
+ } else {
+ Cout << '-';
+ }
+
+ Cout << '\t';
+ if (r.SecondsPerIteration) {
+ Cout << DoFmtTime(*r.SecondsPerIteration);
+ } else {
+ Cout << '-';
+ }
+
+ Cout << Endl;
+ }
+ }
+ };
+
+ class TJSONReporter: public IReporter {
+ public:
+ ~TJSONReporter() override {
+ }
+
+ void Report(TResult&& r) override {
+ with_lock (ResultsLock_) {
+ Results_.emplace_back(std::move(r));
+ }
+ }
+
+ void Finish() override {
+ NJson::TJsonValue report;
+ auto& bench = report["benchmark"];
+ bench.SetType(NJson::JSON_ARRAY);
+
+ NJson::TJsonValue benchReport;
+
+ for (const auto& result : Results_) {
+ NJson::TJsonValue{}.Swap(benchReport);
+ benchReport["name"] = result.TestName;
+ benchReport["samples"] = result.Samples;
+ benchReport["run_time"] = result.RunTime;
+
+ if (result.CyclesPerIteration) {
+ benchReport["per_iteration_cycles"] = *result.CyclesPerIteration;
+ }
+
+ if (result.SecondsPerIteration) {
+ benchReport["per_iteration_secons"] = *result.SecondsPerIteration;
+ }
+
+ bench.AppendValue(benchReport);
+ }
+
+ Cout << report << Endl;
+ }
+
+ private:
+ TAdaptiveLock ResultsLock_;
+ TVector<TResult> Results_;
+ };
+
+ class TOrderedReporter: public IReporter {
+ public:
+ TOrderedReporter(THolder<IReporter> slave)
+ : Slave_(std::move(slave))
+ {
+ }
+
+ void Report(TResult&& result) override {
+ with_lock (ResultsLock_) {
+ OrderedResultQueue_.emplace(result.TestId, std::move(result));
+ while (!OrderedResultQueue_.empty() && OrderedResultQueue_.begin()->first <= ExpectedTestId_) {
+ Slave_->Report(std::move(OrderedResultQueue_.begin()->second));
+ OrderedResultQueue_.erase(OrderedResultQueue_.begin());
+ ++ExpectedTestId_;
+ }
+ }
+ }
+
+ void Finish() override {
+ for (auto& it : OrderedResultQueue_) {
+ Slave_->Report(std::move(it.second));
+ }
+ OrderedResultQueue_.clear();
+ Slave_->Finish();
+ }
+
+ private:
+ THolder<IReporter> Slave_;
+ size_t ExpectedTestId_ = 0;
+ TMap<size_t, TResult> OrderedResultQueue_;
+ TAdaptiveLock ResultsLock_;
+ };
+
+ THolder<IReporter> MakeReporter(const EOutFormat type) {
+ switch (type) {
+ case F_CONSOLE:
+ return MakeHolder<TConsoleReporter>();
+
+ case F_CSV:
+ return MakeHolder<TCSVReporter>();
+
+ case F_JSON:
+ return MakeHolder<TJSONReporter>();
+
+ default:
+ break;
+ }
+
+ return MakeHolder<TConsoleReporter>(); // make compiler happy
+ }
+
+ THolder<IReporter> MakeOrderedReporter(const EOutFormat type) {
+ return MakeHolder<TOrderedReporter>(MakeReporter(type));
+ }
+
+ void EnumerateTests(TVector<ITestRunner*>& tests) {
+ for (size_t id : xrange(tests.size())) {
+ tests[id]->SequentialId = id;
+ }
+ }
+}
+
+template <>
+EOutFormat FromStringImpl<EOutFormat>(const char* data, size_t len) {
+ const auto s = TStringBuf{data, len};
+
+ if (TStringBuf("console") == s) {
+ return F_CONSOLE;
+ } else if (TStringBuf("csv") == s) {
+ return F_CSV;
+ } else if (TStringBuf("json") == s) {
+ return F_JSON;
+ }
+
+ ythrow TFromStringException{} << "failed to convert '" << s << '\'';
+}
+
+template <>
+void Out<TResult>(IOutputStream& out, const TResult& r) {
+ out << "----------- " << LightRed() << r.TestName << Old() << " ---------------" << Endl
+ << " samples: " << White() << r.Samples << Old() << Endl
+ << " iterations: " << White() << r.Iterations << Old() << Endl
+ << " iterations hr: " << White() << HumanReadableSize(r.Iterations, SF_QUANTITY) << Old() << Endl
+ << " run time: " << White() << r.RunTime << Old() << Endl;
+
+ if (r.CyclesPerIteration) {
+ out << " per iteration: " << White() << TCycleTimer::FmtTime(*r.CyclesPerIteration) << Old() << Endl;
+ }
+
+ if (r.SecondsPerIteration) {
+ out << " per iteration: " << White() << DoFmtTime(*r.SecondsPerIteration) << Old() << Endl;
+ }
+}
+
+NCpu::TRegistar::TRegistar(const char* name, TUserFunc func) {
+ static_assert(sizeof(TCpuBenchmark) + alignof(TCpuBenchmark) < sizeof(Buf), "fix Buf size");
+
+ new (AlignUp(Buf, alignof(TCpuBenchmark))) TCpuBenchmark(name, func);
+}
+
+namespace {
+ struct TProgOpts {
+ TProgOpts(int argc, char** argv) {
+ TOpts opts = TOpts::Default();
+
+ opts.AddHelpOption();
+
+ opts.AddLongOption('b', "budget")
+ .StoreResult(&TimeBudget)
+ .RequiredArgument("SEC")
+ .Optional()
+ .Help("overall time budget");
+
+ opts.AddLongOption('l', "list")
+ .NoArgument()
+ .StoreValue(&ListTests, true)
+ .Help("list all tests");
+
+ opts.AddLongOption('t', "threads")
+ .StoreResult(&Threads)
+ .OptionalValue(ToString((NSystemInfo::CachedNumberOfCpus() + 1) / 2), "JOBS")
+ .DefaultValue("1")
+ .Help("run benchmarks in parallel");
+
+ opts.AddLongOption('f', "format")
+ .AddLongName("benchmark_format")
+ .StoreResult(&OutFormat)
+ .RequiredArgument("FORMAT")
+ .DefaultValue("console")
+ .Help("output format (console|csv|json)");
+
+ opts.SetFreeArgDefaultTitle("REGEXP", "RE2 regular expression to filter tests");
+
+ const TOptsParseResult parseResult{&opts, argc, argv};
+
+ for (const auto& regexp : parseResult.GetFreeArgs()) {
+ Filters.push_back(MakeHolder<RE2>(regexp.data(), RE2::Quiet));
+ Y_ENSURE(Filters.back()->ok(), "incorrect RE2 expression '" << regexp << "'");
+ }
+ }
+
+ bool MatchFilters(const TStringBuf& name) const {
+ if (!Filters) {
+ return true;
+ }
+
+ for (auto&& re : Filters) {
+ if (RE2::FullMatchN({name.data(), name.size()}, *re, nullptr, 0)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ bool ListTests = false;
+ double TimeBudget = -1.0;
+ TVector<THolder<RE2>> Filters;
+ size_t Threads = 0;
+ EOutFormat OutFormat;
+ };
+}
+
+int NBench::Main(int argc, char** argv) {
+ const TProgOpts opts(argc, argv);
+
+ TVector<ITestRunner*> tests;
+
+ for (auto&& it : Tests()) {
+ if (opts.MatchFilters(it.Name())) {
+ tests.push_back(&it);
+ }
+ }
+ EnumerateTests(tests);
+
+ if (opts.ListTests) {
+ for (const auto* const it : tests) {
+ Cout << it->Name() << Endl;
+ }
+
+ return 0;
+ }
+
+ if (!tests) {
+ return 0;
+ }
+
+ double timeBudget = opts.TimeBudget;
+
+ if (timeBudget < 0) {
+ timeBudget = 5.0 * tests.size();
+ }
+
+ const TOptions testOpts = {timeBudget / tests.size()};
+ const auto reporter = MakeOrderedReporter(opts.OutFormat);
+
+ std::function<void(ITestRunner**)> func = [&](ITestRunner** it) {
+ auto&& res = (*it)->Run(testOpts);
+
+ reporter->Report(std::move(res));
+ };
+
+ if (opts.Threads > 1) {
+ NYmp::SetThreadCount(opts.Threads);
+ NYmp::ParallelForStaticChunk(tests.data(), tests.data() + tests.size(), 1, func);
+ } else {
+ for (auto it : tests) {
+ func(&it);
+ }
+ }
+
+ reporter->Finish();
+
+ return 0;
+}
diff --git a/library/cpp/testing/benchmark/bench.h b/library/cpp/testing/benchmark/bench.h
new file mode 100644
index 0000000000..21551ad0dd
--- /dev/null
+++ b/library/cpp/testing/benchmark/bench.h
@@ -0,0 +1,92 @@
+#pragma once
+
+#include <util/system/compiler.h>
+#include <util/system/types.h>
+
+#include <utility>
+
+namespace NBench {
+ namespace NCpu {
+ struct TParams {
+ inline size_t Iterations() const noexcept {
+ return Iterations_;
+ }
+
+ const size_t Iterations_;
+ };
+
+ using TUserFunc = void(TParams&);
+
+ struct TRegistar {
+ TRegistar(const char* name, TUserFunc func);
+
+ char Buf[128];
+ };
+ }
+
+ /**
+ * Functions that states "I can read and write everywhere in memory".
+ *
+ * Use it to prevent optimizer from reordering or discarding memory writes prior to it's call,
+ * and force memory reads after it's call.
+ */
+ void Clobber();
+
+ /**
+ * Forces whatever `p` points to be in memory and not in register.
+ *
+ * @param Pointer to data.
+ */
+ template <typename T>
+ void Escape(T* p);
+
+#if defined(__GNUC__)
+ Y_FORCE_INLINE void Clobber() {
+ asm volatile(""
+ :
+ :
+ : "memory");
+ }
+#elif defined(_MSC_VER)
+ Y_FORCE_INLINE void Clobber() {
+ _ReadWriteBarrier();
+ }
+
+#else
+ Y_FORCE_INLINE void Clobber() {
+ }
+#endif
+
+#if defined(__GNUC__)
+ template <typename T>
+ Y_FORCE_INLINE void Escape(T* p) {
+ asm volatile(""
+ :
+ : "g"(p)
+ : "memory");
+ }
+#else
+ template <typename T>
+ Y_FORCE_INLINE void Escape(T*) {
+ }
+#endif
+
+ /**
+ * Use this function to prevent unused variables elimination.
+ *
+ * @param Unused variable (e.g. return value of benchmarked function).
+ */
+ template <typename T>
+ Y_FORCE_INLINE void DoNotOptimize(T&& datum) {
+ ::DoNotOptimizeAway(std::forward<T>(datum));
+ }
+
+ int Main(int argc, char** argv);
+}
+
+#define Y_CPU_BENCHMARK(name, cnt) \
+ namespace N_bench_##name { \
+ static void Run(::NBench::NCpu::TParams&); \
+ const ::NBench::NCpu::TRegistar benchmark(#name, &Run); \
+ } \
+ static void N_bench_##name::Run(::NBench::NCpu::TParams& cnt)
diff --git a/library/cpp/testing/benchmark/dummy.cpp b/library/cpp/testing/benchmark/dummy.cpp
new file mode 100644
index 0000000000..cdb05c19db
--- /dev/null
+++ b/library/cpp/testing/benchmark/dummy.cpp
@@ -0,0 +1,8 @@
+#include "bench.h"
+
+namespace NBench {
+ namespace NPrivate {
+ void UseCharPointer(volatile const char*) {
+ }
+ }
+}
diff --git a/library/cpp/testing/benchmark/examples/main.cpp b/library/cpp/testing/benchmark/examples/main.cpp
new file mode 100644
index 0000000000..ddd8b05ffc
--- /dev/null
+++ b/library/cpp/testing/benchmark/examples/main.cpp
@@ -0,0 +1,215 @@
+#include <library/cpp/testing/benchmark/bench.h>
+
+#include <util/generic/xrange.h>
+#include <util/generic/algorithm.h>
+#include <util/generic/vector.h>
+#include <util/generic/yexception.h>
+#include <util/generic/bt_exception.h>
+
+Y_CPU_BENCHMARK(F, iface) {
+ TVector<size_t> x;
+
+ x.reserve(iface.Iterations());
+
+ for (size_t i = 0; i < iface.Iterations(); ++i) {
+ x.push_back(i);
+ }
+}
+
+Y_CPU_BENCHMARK(EmptyF, iface) {
+ (void)iface;
+}
+
+Y_CPU_BENCHMARK(AlmostEmptyF, iface) {
+ (void)iface;
+
+ TVector<size_t> x;
+ x.resize(1);
+}
+
+Y_CPU_BENCHMARK(TestThrow, iface) {
+ for (size_t i = 0; i < iface.Iterations(); ++i) {
+ try {
+ ythrow yexception() << i;
+ } catch (...) {
+ //CurrentExceptionMessage();
+ }
+ }
+}
+
+Y_CPU_BENCHMARK(TestThrowBT, iface) {
+ for (size_t i = 0; i < iface.Iterations(); ++i) {
+ try {
+ ythrow TWithBackTrace<yexception>() << i;
+ } catch (...) {
+ //CurrentExceptionMessage();
+ }
+ }
+}
+
+Y_CPU_BENCHMARK(TestThrowCatch, iface) {
+ for (size_t i = 0; i < iface.Iterations(); ++i) {
+ try {
+ ythrow yexception() << i;
+ } catch (...) {
+ Y_DO_NOT_OPTIMIZE_AWAY(CurrentExceptionMessage());
+ }
+ }
+}
+
+Y_CPU_BENCHMARK(TestThrowCatchBT, iface) {
+ for (size_t i = 0; i < iface.Iterations(); ++i) {
+ try {
+ ythrow TWithBackTrace<yexception>() << i;
+ } catch (...) {
+ Y_DO_NOT_OPTIMIZE_AWAY(CurrentExceptionMessage());
+ }
+ }
+}
+
+Y_CPU_BENCHMARK(TestRobust, iface) {
+ if (iface.Iterations() % 100 == 0) {
+ usleep(100000);
+ }
+}
+
+Y_CPU_BENCHMARK(IterationSpeed, iface) {
+ const auto n = iface.Iterations();
+
+ for (size_t i = 0; i < n; ++i) {
+ Y_DO_NOT_OPTIMIZE_AWAY(i);
+ }
+}
+
+Y_CPU_BENCHMARK(XRangeSpeed, iface) {
+ for (auto i : xrange<size_t>(0, iface.Iterations())) {
+ Y_DO_NOT_OPTIMIZE_AWAY(i);
+ }
+}
+
+Y_NO_INLINE int FFF() {
+ return 0;
+}
+
+Y_NO_INLINE int FFF(int x) {
+ return x;
+}
+
+Y_NO_INLINE int FFF(int x, int y) {
+ return x + y;
+}
+
+Y_NO_INLINE size_t FS1(TStringBuf x) {
+ return x.size();
+}
+
+Y_NO_INLINE size_t FS1_2(TStringBuf x, TStringBuf y) {
+ return x.size() + y.size();
+}
+
+Y_NO_INLINE size_t FS2(const TStringBuf& x) {
+ return x.size();
+}
+
+Y_NO_INLINE size_t FS2_2(const TStringBuf& x, const TStringBuf& y) {
+ return x.size() + y.size();
+}
+
+Y_CPU_BENCHMARK(FunctionCallCost_StringBufVal1, iface) {
+ TStringBuf x;
+
+ for (auto i : xrange<size_t>(0, iface.Iterations())) {
+ (void)i;
+ NBench::Escape(&x);
+ Y_DO_NOT_OPTIMIZE_AWAY(FS1(x));
+ NBench::Clobber();
+ }
+}
+
+Y_CPU_BENCHMARK(FunctionCallCost_StringBufRef1, iface) {
+ TStringBuf x;
+
+ for (auto i : xrange<size_t>(0, iface.Iterations())) {
+ (void)i;
+ NBench::Escape(&x);
+ Y_DO_NOT_OPTIMIZE_AWAY(FS2(x));
+ NBench::Clobber();
+ }
+}
+
+Y_CPU_BENCHMARK(FunctionCallCost_StringBufVal2, iface) {
+ TStringBuf x;
+ TStringBuf y;
+
+ for (auto i : xrange<size_t>(0, iface.Iterations())) {
+ (void)i;
+ NBench::Escape(&x);
+ NBench::Escape(&y);
+ Y_DO_NOT_OPTIMIZE_AWAY(FS1_2(x, y));
+ NBench::Clobber();
+ }
+}
+
+Y_CPU_BENCHMARK(FunctionCallCost_StringBufRef2, iface) {
+ TStringBuf x;
+ TStringBuf y;
+
+ for (auto i : xrange<size_t>(0, iface.Iterations())) {
+ (void)i;
+ NBench::Escape(&x);
+ NBench::Escape(&y);
+ Y_DO_NOT_OPTIMIZE_AWAY(FS2_2(x, y));
+ NBench::Clobber();
+ }
+}
+
+Y_CPU_BENCHMARK(FunctionCallCost_NoArg, iface) {
+ for (auto i : xrange<size_t>(0, iface.Iterations())) {
+ (void)i;
+ Y_DO_NOT_OPTIMIZE_AWAY(FFF());
+ }
+}
+
+Y_CPU_BENCHMARK(FunctionCallCost_OneArg, iface) {
+ for (auto i : xrange<size_t>(0, iface.Iterations())) {
+ Y_DO_NOT_OPTIMIZE_AWAY(FFF(i));
+ }
+}
+
+Y_CPU_BENCHMARK(FunctionCallCost_TwoArg, iface) {
+ for (auto i : xrange<size_t>(0, iface.Iterations())) {
+ Y_DO_NOT_OPTIMIZE_AWAY(FFF(i, i));
+ }
+}
+
+/* An example of incorrect benchmark. As of r2581591 Clang 3.7 produced following assembly:
+ * @code
+ * │ push %rbp
+ * │ mov %rsp,%rbp
+ * │ push %rbx
+ * │ push %rax
+ * │ mov (%rdi),%rbx
+ * │ test %rbx,%rbx
+ * │ ↓ je 25
+ * │ xor %edi,%edi
+ * │ xor %esi,%esi
+ * │ → callq FS1(TBasicStringBuf<char, std::char_traits<char
+ * │ nop
+ * 100.00 │20:┌─→dec %rbx
+ * │ └──jne 20
+ * │25: add $0x8,%rsp
+ * │ pop %rbx
+ * │ pop %rbp
+ * │ ← retq
+ * @endcode
+ *
+ * So, this benchmark is measuring empty loop!
+ */
+Y_CPU_BENCHMARK(Incorrect_FunctionCallCost_StringBufVal1, iface) {
+ TStringBuf x;
+
+ for (auto i : xrange<size_t>(0, iface.Iterations())) {
+ (void)i;
+ Y_DO_NOT_OPTIMIZE_AWAY(FS1(x));
+ }
+}
diff --git a/library/cpp/testing/benchmark/examples/metrics/main.py b/library/cpp/testing/benchmark/examples/metrics/main.py
new file mode 100644
index 0000000000..8f9d9d06ae
--- /dev/null
+++ b/library/cpp/testing/benchmark/examples/metrics/main.py
@@ -0,0 +1,7 @@
+import yatest.common as yc
+
+
+def test_export_metrics(metrics):
+ metrics.set_benchmark(yc.execute_benchmark(
+ 'library/cpp/testing/benchmark/examples/examples',
+ threads=8))
diff --git a/library/cpp/testing/benchmark/examples/metrics/ya.make b/library/cpp/testing/benchmark/examples/metrics/ya.make
new file mode 100644
index 0000000000..a9dbdca9fa
--- /dev/null
+++ b/library/cpp/testing/benchmark/examples/metrics/ya.make
@@ -0,0 +1,20 @@
+OWNER(
+ pg
+ yazevnul
+)
+
+PY2TEST()
+
+SIZE(LARGE)
+
+TAG(
+ ya:force_sandbox
+ sb:intel_e5_2660v1
+ ya:fat
+)
+
+TEST_SRCS(main.py)
+
+DEPENDS(library/cpp/testing/benchmark/examples)
+
+END()
diff --git a/library/cpp/testing/benchmark/examples/ya.make b/library/cpp/testing/benchmark/examples/ya.make
new file mode 100644
index 0000000000..7e696e127a
--- /dev/null
+++ b/library/cpp/testing/benchmark/examples/ya.make
@@ -0,0 +1,12 @@
+OWNER(
+ pg
+ yazevnul
+)
+
+Y_BENCHMARK()
+
+SRCS(
+ main.cpp
+)
+
+END()
diff --git a/library/cpp/testing/benchmark/main/main.cpp b/library/cpp/testing/benchmark/main/main.cpp
new file mode 100644
index 0000000000..aabcb89c43
--- /dev/null
+++ b/library/cpp/testing/benchmark/main/main.cpp
@@ -0,0 +1,16 @@
+#include <library/cpp/testing/benchmark/bench.h>
+
+#include <util/generic/yexception.h>
+#include <util/stream/output.h>
+
+#include <cstdlib>
+
+int main(int argc, char** argv) {
+ try {
+ return NBench::Main(argc, argv);
+ } catch (...) {
+ Cerr << CurrentExceptionMessage() << Endl;
+ }
+
+ return EXIT_FAILURE;
+}
diff --git a/library/cpp/testing/benchmark/main/ya.make b/library/cpp/testing/benchmark/main/ya.make
new file mode 100644
index 0000000000..d00cdcf9fc
--- /dev/null
+++ b/library/cpp/testing/benchmark/main/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+OWNER(
+ pg
+ yazevnul
+)
+
+SRCS(
+ GLOBAL main.cpp
+)
+
+PEERDIR(
+ library/cpp/testing/benchmark
+)
+
+END()
diff --git a/library/cpp/testing/benchmark/ya.make b/library/cpp/testing/benchmark/ya.make
new file mode 100644
index 0000000000..f42be80698
--- /dev/null
+++ b/library/cpp/testing/benchmark/ya.make
@@ -0,0 +1,22 @@
+LIBRARY()
+
+OWNER(
+ pg
+ yazevnul
+)
+
+SRCS(
+ bench.cpp
+ dummy.cpp
+)
+
+PEERDIR(
+ contrib/libs/re2
+ library/cpp/colorizer
+ library/cpp/getopt/small
+ library/cpp/json
+ library/cpp/linear_regression
+ library/cpp/threading/poor_man_openmp
+)
+
+END()
diff --git a/library/cpp/testing/common/env.cpp b/library/cpp/testing/common/env.cpp
new file mode 100644
index 0000000000..fa3a47fe16
--- /dev/null
+++ b/library/cpp/testing/common/env.cpp
@@ -0,0 +1,275 @@
+#include "env.h"
+
+#include <build/scripts/c_templates/svnversion.h>
+
+#include <util/folder/dirut.h>
+#include <util/folder/path.h>
+#include <util/generic/singleton.h>
+#include <util/stream/file.h>
+#include <util/stream/fwd.h>
+#include <util/system/env.h>
+#include <util/system/file.h>
+#include <util/system/file_lock.h>
+#include <util/system/guard.h>
+
+#include <library/cpp/json/json_reader.h>
+#include <library/cpp/json/json_value.h>
+#include <library/cpp/json/json_writer.h>
+
+TString ArcadiaSourceRoot() {
+ if (const auto& sourceRoot = NPrivate::GetTestEnv().SourceRoot) {
+ return sourceRoot;
+ } else {
+ return GetArcadiaSourcePath();
+ }
+}
+
+TString BuildRoot() {
+ if (const auto& buildRoot = NPrivate::GetTestEnv().BuildRoot) {
+ return buildRoot;
+ } else {
+ return GetArcadiaSourcePath();
+ }
+}
+
+TString ArcadiaFromCurrentLocation(TStringBuf where, TStringBuf path) {
+ return (TFsPath(ArcadiaSourceRoot()) / TFsPath(where).Parent() / path).Fix();
+}
+
+TString BinaryPath(TStringBuf path) {
+ return (TFsPath(BuildRoot()) / path).Fix();
+}
+
+TString GetArcadiaTestsData() {
+ TString atdRoot = NPrivate::GetTestEnv().ArcadiaTestsDataDir;
+ if (atdRoot) {
+ return atdRoot;
+ }
+
+ TString path = NPrivate::GetCwd();
+ const char pathsep = GetDirectorySeparator();
+ while (!path.empty()) {
+ TString dataDir = path + "/arcadia_tests_data";
+ if (IsDir(dataDir)) {
+ return dataDir;
+ }
+
+ size_t pos = path.find_last_of(pathsep);
+ if (pos == TString::npos) {
+ pos = 0;
+ }
+ path.erase(pos);
+ }
+
+ return {};
+}
+
+TString GetWorkPath() {
+ TString workPath = NPrivate::GetTestEnv().WorkPath;
+ if (workPath) {
+ return workPath;
+ }
+
+ return NPrivate::GetCwd();
+}
+
+TFsPath GetOutputPath() {
+ return GetWorkPath() + "/testing_out_stuff";
+}
+
+const TString& GetRamDrivePath() {
+ return NPrivate::GetTestEnv().RamDrivePath;
+}
+
+const TString& GetYtHddPath() {
+ return NPrivate::GetTestEnv().YtHddPath;
+}
+
+const TString& GetOutputRamDrivePath() {
+ return NPrivate::GetTestEnv().TestOutputRamDrivePath;
+}
+
+const TString& GdbPath() {
+ return NPrivate::GetTestEnv().GdbPath;
+}
+
+const TString& GetTestParam(TStringBuf name) {
+ const static TString def = "";
+ return GetTestParam(name, def);
+}
+
+const TString& GetTestParam(TStringBuf name, const TString& def) {
+ auto& testParameters = NPrivate::GetTestEnv().TestParameters;
+ auto it = testParameters.find(name.data());
+ if (it != testParameters.end()) {
+ return it->second;
+ }
+ return def;
+}
+
+void AddEntryToCoreSearchFile(const TString& filename, TStringBuf cmd, int pid, const TFsPath& binaryPath = TFsPath(), const TFsPath& cwd = TFsPath()) {
+ auto lock = TFileLock(filename);
+ TGuard<TFileLock> guard(lock);
+
+ TOFStream output(TFile(filename, WrOnly | ForAppend | OpenAlways));
+
+ NJson::TJsonWriter writer(&output, false);
+ writer.OpenMap();
+ writer.Write("cmd", cmd);
+ writer.Write("pid", pid);
+ if (binaryPath) {
+ writer.Write("binary_path", binaryPath);
+ }
+ if (cwd) {
+ writer.Write("cwd", cwd);
+ }
+ writer.CloseMap();
+ writer.Flush();
+
+ output.Write("\n");
+}
+
+void WatchProcessCore(int pid, const TFsPath& binaryPath, const TFsPath& cwd) {
+ auto& filename = NPrivate::GetTestEnv().CoreSearchFile;
+ if (filename) {
+ AddEntryToCoreSearchFile(filename, "add", pid, binaryPath, cwd);
+ }
+}
+
+void StopProcessCoreWatching(int pid) {
+ auto& filename = NPrivate::GetTestEnv().CoreSearchFile;
+ if (filename) {
+ AddEntryToCoreSearchFile(filename, "drop", pid);
+ }
+}
+
+bool FromYaTest() {
+ return NPrivate::GetTestEnv().IsRunningFromTest;
+}
+
+namespace NPrivate {
+ TTestEnv::TTestEnv() {
+ ReInitialize();
+ }
+
+ void TTestEnv::ReInitialize() {
+ IsRunningFromTest = false;
+ ArcadiaTestsDataDir = "";
+ SourceRoot = "";
+ BuildRoot = "";
+ WorkPath = "";
+ RamDrivePath = "";
+ YtHddPath = "";
+ TestOutputRamDrivePath = "";
+ GdbPath = "";
+ CoreSearchFile = "";
+ TestParameters.clear();
+
+ const TString contextFilename = GetEnv("YA_TEST_CONTEXT_FILE");
+ if (contextFilename) {
+ NJson::TJsonValue context;
+ NJson::ReadJsonTree(TFileInput(contextFilename).ReadAll(), &context);
+
+ NJson::TJsonValue* value;
+
+ value = context.GetValueByPath("runtime.source_root");
+ if (value) {
+ SourceRoot = value->GetStringSafe("");
+ }
+
+ value = context.GetValueByPath("runtime.build_root");
+ if (value) {
+ BuildRoot = value->GetStringSafe("");
+ }
+
+ value = context.GetValueByPath("runtime.atd_root");
+ if (value) {
+ ArcadiaTestsDataDir = value->GetStringSafe("");
+ }
+
+ value = context.GetValueByPath("runtime.work_path");
+ if (value) {
+ WorkPath = value->GetStringSafe("");
+ }
+
+ value = context.GetValueByPath("runtime.ram_drive_path");
+ if (value) {
+ RamDrivePath = value->GetStringSafe("");
+ }
+
+ value = context.GetValueByPath("runtime.yt_hdd_path");
+ if (value) {
+ YtHddPath = value->GetStringSafe("");
+ }
+
+ value = context.GetValueByPath("runtime.test_output_ram_drive_path");
+ if (value) {
+ TestOutputRamDrivePath = value->GetStringSafe("");
+ }
+
+ value = context.GetValueByPath("runtime.gdb_bin");
+ if (value) {
+ GdbPath = value->GetStringSafe("");
+ }
+
+ value = context.GetValueByPath("runtime.test_params");
+ if (value) {
+ for (const auto& entry : context.GetValueByPath("runtime.test_params")->GetMap()) {
+ TestParameters[entry.first] = entry.second.GetStringSafe("");
+ }
+ }
+
+ value = context.GetValueByPath("internal.core_search_file");
+ if (value) {
+ CoreSearchFile = value->GetStringSafe("");
+ }
+ }
+
+ if (!YtHddPath) {
+ YtHddPath = GetEnv("HDD_PATH");
+ }
+
+ if (!SourceRoot) {
+ SourceRoot = GetEnv("ARCADIA_SOURCE_ROOT");
+ }
+
+ if (!BuildRoot) {
+ BuildRoot = GetEnv("ARCADIA_BUILD_ROOT");
+ }
+
+ if (!ArcadiaTestsDataDir) {
+ ArcadiaTestsDataDir = GetEnv("ARCADIA_TESTS_DATA_DIR");
+ }
+
+ if (!WorkPath) {
+ WorkPath = GetEnv("TEST_WORK_PATH");
+ }
+
+ if (!RamDrivePath) {
+ RamDrivePath = GetEnv("YA_TEST_RAM_DRIVE_PATH");
+ }
+
+ if (!TestOutputRamDrivePath) {
+ TestOutputRamDrivePath = GetEnv("YA_TEST_OUTPUT_RAM_DRIVE_PATH");
+ }
+
+ const TString fromEnv = GetEnv("YA_TEST_RUNNER");
+ IsRunningFromTest = (fromEnv == "1");
+ }
+
+ void TTestEnv::AddTestParam(TStringBuf name, TStringBuf value) {
+ TestParameters[TString{name}] = value;
+ }
+
+ TString GetCwd() {
+ try {
+ return NFs::CurrentWorkingDirectory();
+ } catch (...) {
+ return {};
+ }
+ }
+
+ const TTestEnv& GetTestEnv() {
+ return *Singleton<TTestEnv>();
+ }
+}
diff --git a/library/cpp/testing/common/env.h b/library/cpp/testing/common/env.h
new file mode 100644
index 0000000000..7b89aa1bed
--- /dev/null
+++ b/library/cpp/testing/common/env.h
@@ -0,0 +1,84 @@
+#pragma once
+
+#include <unordered_map>
+
+#include <util/folder/path.h>
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/system/src_location.h>
+
+// @brief return full path to arcadia root
+TString ArcadiaSourceRoot();
+
+// @brief return full path for file or folder specified by known source location `where` and `path` which is relative to parent folder of `where`
+// for the instance: there is 2 files in folder test example_ut.cpp and example.data, so full path to test/example.data can be obtained
+// from example_ut.cpp as ArcadiaFromCurrentLocation(__SOURCE_FILE__, "example.data")
+TString ArcadiaFromCurrentLocation(TStringBuf where, TStringBuf path);
+
+// @brief return build folder path
+TString BuildRoot();
+
+// @brief return full path to built artefact, where path is relative from arcadia root
+TString BinaryPath(TStringBuf path);
+
+// @brief return true if environment is testenv otherwise false
+bool FromYaTest();
+
+// @brief returns TestsData dir (from env:ARCADIA_TESTS_DATA_DIR or path to existing folder `arcadia_tests_data` within parent folders)
+TString GetArcadiaTestsData();
+
+// @brief return current working dir (from env:TEST_WORK_PATH or cwd)
+TString GetWorkPath();
+
+// @brief return tests output path (workdir + testing_out_stuff)
+TFsPath GetOutputPath();
+
+// @brief return path from env:YA_TEST_RAM_DRIVE_PATH
+const TString& GetRamDrivePath();
+
+// @brief return path from env:YA_TEST_OUTPUT_RAM_DRIVE_PATH
+const TString& GetOutputRamDrivePath();
+
+// @brief return test parameter by name. If not exists, return an empty string
+const TString& GetTestParam(TStringBuf name);
+
+// @brief return test parameter by name. If not exists, return specified default value
+const TString& GetTestParam(TStringBuf name, const TString& def);
+
+// @brief return path to the gdb
+const TString& GdbPath();
+
+// @brief register the process. Test suite will be marked as failed if the process is terminated with a core dump file after testing
+void WatchProcessCore(int pid, const TFsPath& binaryPath, const TFsPath& cwd = TFsPath());
+
+// @brief mark the process as successfully completed - a test machinery won't try to recover core dump file for the process
+void StopProcessCoreWatching(int pid);
+
+#define SRC_(path) ArcadiaFromCurrentLocation(__SOURCE_FILE__, path)
+
+namespace NPrivate {
+ class TTestEnv {
+ public:
+ TTestEnv();
+
+ void ReInitialize();
+
+ void AddTestParam(TStringBuf name, TStringBuf value);
+
+ bool IsRunningFromTest;
+ TString ArcadiaTestsDataDir;
+ TString SourceRoot;
+ TString BuildRoot;
+ TString WorkPath;
+ TString RamDrivePath;
+ TString YtHddPath;
+ TString TestOutputRamDrivePath;
+ TString GdbPath;
+ TString CoreSearchFile;
+ std::unordered_map<TString, TString> TestParameters;
+ };
+
+ TString GetCwd();
+
+ const TTestEnv& GetTestEnv();
+}
diff --git a/library/cpp/testing/common/network.cpp b/library/cpp/testing/common/network.cpp
new file mode 100644
index 0000000000..230c50ee6d
--- /dev/null
+++ b/library/cpp/testing/common/network.cpp
@@ -0,0 +1,208 @@
+#include "network.h"
+
+#include <util/folder/dirut.h>
+#include <util/folder/path.h>
+#include <util/generic/singleton.h>
+#include <util/generic/utility.h>
+#include <util/generic/vector.h>
+#include <util/generic/ylimits.h>
+#include <util/network/address.h>
+#include <util/network/sock.h>
+#include <util/random/random.h>
+#include <util/stream/file.h>
+#include <util/string/split.h>
+#include <util/system/env.h>
+#include <util/system/error.h>
+#include <util/system/file_lock.h>
+#include <util/system/fs.h>
+
+#ifdef _darwin_
+#include <sys/types.h>
+#include <sys/sysctl.h>
+#endif
+
+namespace {
+#define Y_VERIFY_SYSERROR(expr) \
+ do { \
+ if (!(expr)) { \
+ Y_FAIL(#expr ", errno=%d", LastSystemError()); \
+ } \
+ } while (false)
+
+ class TPortGuard : public NTesting::IPort {
+ public:
+ TPortGuard(ui16 port, THolder<TFileLock> lock)
+ : Lock_(std::move(lock))
+ , Port_(port)
+ {
+ }
+
+ ~TPortGuard() override {
+ Y_VERIFY_SYSERROR(NFs::Remove(Lock_->GetName()));
+ }
+
+ ui16 Get() override {
+ return Port_;
+ }
+
+ private:
+ THolder<TFileLock> Lock_;
+ ui16 Port_;
+ };
+
+ std::pair<ui16, ui16> GetEphemeralRange() {
+ // IANA suggestion
+ std::pair<ui16, ui16> pair{(1 << 15) + (1 << 14), (1 << 16) - 1};
+ #ifdef _linux_
+ if (NFs::Exists("/proc/sys/net/ipv4/ip_local_port_range")) {
+ TIFStream fileStream("/proc/sys/net/ipv4/ip_local_port_range");
+ fileStream >> pair.first >> pair.second;
+ }
+ #endif
+ #ifdef _darwin_
+ ui32 first, last;
+ size_t size;
+ sysctlbyname("net.inet.ip.portrange.first", &first, &size, NULL, 0);
+ sysctlbyname("net.inet.ip.portrange.last", &last, &size, NULL, 0);
+ pair.first = first;
+ pair.second = last;
+ #endif
+ return pair;
+ }
+
+ TVector<std::pair<ui16, ui16>> GetPortRanges() {
+ TString givenRange = GetEnv("VALID_PORT_RANGE");
+ TVector<std::pair<ui16, ui16>> ranges;
+ if (givenRange.Contains(':')) {
+ auto res = StringSplitter(givenRange).Split(':').Limit(2).ToList<TString>();
+ ranges.emplace_back(FromString<ui16>(res.front()), FromString<ui16>(res.back()));
+ } else {
+ const ui16 firstValid = 1025;
+ const ui16 lastValid = Max<ui16>();
+
+ auto [firstEphemeral, lastEphemeral] = GetEphemeralRange();
+ const ui16 firstInvalid = Max(firstEphemeral, firstValid);
+ const ui16 lastInvalid = Min(lastEphemeral, lastValid);
+
+ if (firstInvalid > firstValid)
+ ranges.emplace_back(firstValid, firstInvalid - 1);
+ if (lastInvalid < lastValid)
+ ranges.emplace_back(lastInvalid + 1, lastValid);
+ }
+ return ranges;
+ }
+
+ class TPortManager {
+ static constexpr size_t Retries = 20;
+ public:
+ TPortManager()
+ : SyncDir_(GetEnv("PORT_SYNC_PATH"))
+ , Ranges_(GetPortRanges())
+ , TotalCount_(0)
+ {
+ if (!SyncDir_.IsDefined()) {
+ SyncDir_ = TFsPath(GetSystemTempDir()) / "yandex_port_locks";
+ }
+ Y_VERIFY(SyncDir_.IsDefined());
+ NFs::MakeDirectoryRecursive(SyncDir_);
+
+ for (auto [left, right] : Ranges_) {
+ TotalCount_ += right - left;
+ }
+ Y_VERIFY(0 != TotalCount_);
+ }
+
+ NTesting::TPortHolder GetFreePort() const {
+ ui16 salt = RandomNumber<ui16>();
+ for (ui16 attempt = 0; attempt < TotalCount_; ++attempt) {
+ ui16 probe = (salt + attempt) % TotalCount_;
+
+ for (auto [left, right] : Ranges_) {
+ if (probe >= right - left)
+ probe -= right - left;
+ else {
+ probe += left;
+ break;
+ }
+ }
+
+ auto port = TryAcquirePort(probe);
+ if (port) {
+ return NTesting::TPortHolder{std::move(port)};
+ }
+ }
+
+ Y_FAIL("Cannot get free port!");
+ }
+
+ TVector<NTesting::TPortHolder> GetFreePortsRange(size_t count) const {
+ Y_VERIFY(count > 0);
+ TVector<NTesting::TPortHolder> ports(Reserve(count));
+ for (size_t i = 0; i < Retries; ++i) {
+ for (auto[left, right] : Ranges_) {
+ if (right - left < count) {
+ continue;
+ }
+ ui16 start = left + RandomNumber<ui16>((right - left) / 2);
+ if (right - start < count) {
+ continue;
+ }
+ for (ui16 probe = start; probe < right; ++probe) {
+ auto port = TryAcquirePort(probe);
+ if (port) {
+ ports.emplace_back(std::move(port));
+ } else {
+ ports.clear();
+ }
+ if (ports.size() == count) {
+ return ports;
+ }
+ }
+ // Can't find required number of ports without gap in the current range
+ ports.clear();
+ }
+ }
+ Y_FAIL("Cannot get range of %zu ports!", count);
+ }
+
+ private:
+ THolder<NTesting::IPort> TryAcquirePort(ui16 port) const {
+ auto lock = MakeHolder<TFileLock>(TString(SyncDir_ / ::ToString(port)));
+ if (!lock->TryAcquire()) {
+ return nullptr;
+ }
+
+ TInet6StreamSocket sock;
+ Y_VERIFY_SYSERROR(INVALID_SOCKET != static_cast<SOCKET>(sock));
+
+ TSockAddrInet6 addr("::", port);
+ if (sock.Bind(&addr) != 0) {
+ lock->Release();
+ Y_VERIFY(EADDRINUSE == LastSystemError(), "unexpected error: %d", LastSystemError());
+ return nullptr;
+ }
+ return MakeHolder<TPortGuard>(port, std::move(lock));
+ }
+
+ private:
+ TFsPath SyncDir_;
+ TVector<std::pair<ui16, ui16>> Ranges_;
+ size_t TotalCount_;
+ };
+}
+
+namespace NTesting {
+ TPortHolder GetFreePort() {
+ return Singleton<TPortManager>()->GetFreePort();
+ }
+
+ namespace NLegacy {
+ TVector<TPortHolder> GetFreePortsRange(size_t count) {
+ return Singleton<TPortManager>()->GetFreePortsRange(count);
+ }
+ }
+
+ IOutputStream& operator<<(IOutputStream& out, const TPortHolder& port) {
+ return out << static_cast<ui16>(port);
+ }
+}
diff --git a/library/cpp/testing/common/network.h b/library/cpp/testing/common/network.h
new file mode 100644
index 0000000000..eb4d32f3a1
--- /dev/null
+++ b/library/cpp/testing/common/network.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <util/generic/ptr.h>
+#include <util/generic/vector.h>
+
+namespace NTesting {
+
+ //@brief network port holder interface
+ class IPort {
+ public:
+ virtual ~IPort() {}
+
+ virtual ui16 Get() = 0;
+ };
+
+ class TPortHolder : private THolder<IPort> {
+ using TBase = THolder<IPort>;
+ public:
+ using TBase::TBase;
+ using TBase::Release;
+ using TBase::Reset;
+
+ operator ui16() const& {
+ return (*this)->Get();
+ }
+
+ operator ui16() const&& = delete;
+ };
+
+ IOutputStream& operator<<(IOutputStream& out, const TPortHolder& port);
+
+ //@brief Get first free port.
+ [[nodiscard]] TPortHolder GetFreePort();
+
+ namespace NLegacy {
+ // Do not use this method, it needs only for TPortManager from unittests.
+ // Returns continuous sequence of the specified number of ports.
+ [[nodiscard]] TVector<TPortHolder> GetFreePortsRange(size_t count);
+ }
+
+ //@brief helper class for inheritance
+ struct TFreePortOwner {
+ TFreePortOwner() : Port_(GetFreePort()) {}
+
+ ui16 GetPort() const {
+ return Port_;
+ }
+
+ private:
+ TPortHolder Port_;
+ };
+}
diff --git a/library/cpp/testing/common/probe.cpp b/library/cpp/testing/common/probe.cpp
new file mode 100644
index 0000000000..73f2fb6360
--- /dev/null
+++ b/library/cpp/testing/common/probe.cpp
@@ -0,0 +1 @@
+#include "probe.h"
diff --git a/library/cpp/testing/common/probe.h b/library/cpp/testing/common/probe.h
new file mode 100644
index 0000000000..19910979b5
--- /dev/null
+++ b/library/cpp/testing/common/probe.h
@@ -0,0 +1,140 @@
+#pragma once
+
+#include <util/system/yassert.h>
+
+namespace NTesting {
+ ////////////////////////////////////////////////////////////////////////////////
+
+ // Below there is a serie of probe classes for testing construction/destruction copying/moving of class.
+ // for examples see tests in probe_ut.cpp
+
+ struct TProbeState {
+ int Constructors = 0;
+ int Destructors = 0;
+ int ShadowDestructors = 0;
+ int CopyConstructors = 0;
+ int CopyAssignments = 0;
+ int MoveConstructors = 0;
+ int MoveAssignments = 0;
+ int Touches = 0;
+
+ TProbeState() = default;
+
+ void Reset() {
+ *this = TProbeState{};
+ }
+ };
+
+ // Used for probing the number of copies that occur if a type must be coerced.
+ class TCoercibleToProbe {
+ public:
+ TProbeState* State;
+ TProbeState* ShadowState;
+
+ public:
+ explicit TCoercibleToProbe(TProbeState* state)
+ : State(state)
+ , ShadowState(state)
+ {}
+
+ private:
+ TCoercibleToProbe(const TCoercibleToProbe&);
+ TCoercibleToProbe(TCoercibleToProbe&&);
+ TCoercibleToProbe& operator=(const TCoercibleToProbe&);
+ TCoercibleToProbe& operator=(TCoercibleToProbe&&);
+ };
+
+ // Used for probing the number of copies in an argument.
+ class TProbe {
+ public:
+ TProbeState* State;
+ TProbeState* ShadowState;
+
+ public:
+ static TProbe ExplicitlyCreateInvalidProbe() {
+ return TProbe();
+ }
+
+ explicit TProbe(TProbeState* state)
+ : State(state)
+ , ShadowState(state)
+ {
+ Y_ASSERT(State);
+ ++State->Constructors;
+ }
+
+ ~TProbe() {
+ if (State) {
+ ++State->Destructors;
+ }
+ if (ShadowState) {
+ ++ShadowState->ShadowDestructors;
+ }
+ }
+
+ TProbe(const TProbe& other)
+ : State(other.State)
+ , ShadowState(other.ShadowState)
+ {
+ Y_ASSERT(State);
+ ++State->CopyConstructors;
+ }
+
+ TProbe(TProbe&& other)
+ : State(other.State)
+ , ShadowState(other.ShadowState)
+ {
+ Y_ASSERT(State);
+ other.State = nullptr;
+ ++State->MoveConstructors;
+ }
+
+ TProbe(const TCoercibleToProbe& other)
+ : State(other.State)
+ , ShadowState(other.ShadowState)
+ {
+ Y_ASSERT(State);
+ ++State->CopyConstructors;
+ }
+
+ TProbe(TCoercibleToProbe&& other)
+ : State(other.State)
+ , ShadowState(other.ShadowState)
+ {
+ Y_ASSERT(State);
+ other.State = nullptr;
+ ++State->MoveConstructors;
+ }
+
+ TProbe& operator=(const TProbe& other) {
+ State = other.State;
+ ShadowState = other.ShadowState;
+ Y_ASSERT(State);
+ ++State->CopyAssignments;
+ return *this;
+ }
+
+ TProbe& operator=(TProbe&& other) {
+ State = other.State;
+ ShadowState = other.ShadowState;
+ Y_ASSERT(State);
+ other.State = nullptr;
+ ++State->MoveAssignments;
+ return *this;
+ }
+
+ void Touch() const {
+ Y_ASSERT(State);
+ ++State->Touches;
+ }
+
+ bool IsValid() const {
+ return nullptr != State;
+ }
+
+ private:
+ TProbe()
+ : State(nullptr)
+ {}
+ };
+} // namespace NTesting
diff --git a/library/cpp/testing/common/scope.cpp b/library/cpp/testing/common/scope.cpp
new file mode 100644
index 0000000000..c70d695c1b
--- /dev/null
+++ b/library/cpp/testing/common/scope.cpp
@@ -0,0 +1 @@
+#include "scope.h"
diff --git a/library/cpp/testing/common/scope.h b/library/cpp/testing/common/scope.h
new file mode 100644
index 0000000000..a2ca0e77e4
--- /dev/null
+++ b/library/cpp/testing/common/scope.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/system/env.h>
+
+#include <utility>
+
+namespace NTesting {
+ // @brief Assigns new values to the given environment variables and restores old values upon destruction.
+ // @note if there was no env variable with given name, it will be set to empty string upon destruction IGNIETFERRO-1486
+ struct TScopedEnvironment {
+ TScopedEnvironment(const TString& name, const TString& value)
+ : PreviousState{1, {name, ::GetEnv(name)}}
+ {
+ ::SetEnv(name, value);
+ }
+
+ TScopedEnvironment(const TVector<std::pair<TString, TString>>& vars)
+ : PreviousState(Reserve(vars.size()))
+ {
+ for (const auto& [k, v] : vars) {
+ PreviousState.emplace_back(k, ::GetEnv(k));
+ ::SetEnv(k, v);
+ }
+ }
+
+ ~TScopedEnvironment() {
+ for (const auto& [k, v] : PreviousState) {
+ ::SetEnv(k, v);
+ }
+ }
+
+ TScopedEnvironment(const TScopedEnvironment&) = delete;
+ TScopedEnvironment& operator=(const TScopedEnvironment&) = delete;
+ private:
+ TVector<std::pair<TString, TString>> PreviousState;
+ };
+}
diff --git a/library/cpp/testing/common/ut/env_ut.cpp b/library/cpp/testing/common/ut/env_ut.cpp
new file mode 100644
index 0000000000..2aed1e4a25
--- /dev/null
+++ b/library/cpp/testing/common/ut/env_ut.cpp
@@ -0,0 +1,162 @@
+#include <library/cpp/testing/common/env.h>
+#include <library/cpp/testing/common/scope.h>
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <util/folder/dirut.h>
+#include <util/stream/file.h>
+#include <util/system/env.h>
+#include <util/system/execpath.h>
+#include <util/system/fs.h>
+
+
+TEST(Runtime, ArcadiaSourceRoot) {
+ NTesting::TScopedEnvironment contextGuard("YA_TEST_CONTEXT_FILE", ""); // remove context filename
+ {
+ auto tmpDir = ::GetSystemTempDir();
+ NTesting::TScopedEnvironment guard("ARCADIA_SOURCE_ROOT", tmpDir);
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_EQ(tmpDir, ArcadiaSourceRoot());
+ }
+ {
+ NTesting::TScopedEnvironment guard("ARCADIA_SOURCE_ROOT", "");
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_FALSE(ArcadiaSourceRoot().empty());
+ }
+}
+
+TEST(Runtime, BuildRoot) {
+ NTesting::TScopedEnvironment contextGuard("YA_TEST_CONTEXT_FILE", ""); // remove context filename
+ {
+ auto tmpDir = ::GetSystemTempDir();
+ NTesting::TScopedEnvironment guard("ARCADIA_BUILD_ROOT", tmpDir);
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_EQ(tmpDir, BuildRoot());
+ }
+ {
+ NTesting::TScopedEnvironment guard("ARCADIA_BUILD_ROOT", "");
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_FALSE(BuildRoot().empty());
+ }
+}
+
+TEST(Runtime, BinaryPath) {
+ NTesting::TScopedEnvironment contextGuard("YA_TEST_CONTEXT_FILE", ""); // remove context filename
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_TRUE(TFsPath(BinaryPath("library/cpp/testing/common/ut")).Exists());
+}
+
+TEST(Runtime, GetArcadiaTestsData) {
+ NTesting::TScopedEnvironment contextGuard("YA_TEST_CONTEXT_FILE", ""); // remove context filename
+ {
+ auto tmpDir = ::GetSystemTempDir();
+ NTesting::TScopedEnvironment guard("ARCADIA_TESTS_DATA_DIR", tmpDir);
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_EQ(tmpDir, GetArcadiaTestsData());
+ }
+ {
+ NTesting::TScopedEnvironment guard("ARCADIA_TESTS_DATA_DIR", "");
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ auto path = GetArcadiaTestsData();
+ // it is not error if path is empty
+ const bool ok = (path.empty() || GetBaseName(path) == "arcadia_tests_data");
+ EXPECT_TRUE(ok);
+ }
+}
+
+TEST(Runtime, GetWorkPath) {
+ NTesting::TScopedEnvironment contextGuard("YA_TEST_CONTEXT_FILE", ""); // remove context filename
+ {
+ auto tmpDir = ::GetSystemTempDir();
+ NTesting::TScopedEnvironment guard("TEST_WORK_PATH", tmpDir);
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_EQ(tmpDir, GetWorkPath());
+ }
+ {
+ NTesting::TScopedEnvironment guard("TEST_WORK_PATH", "");
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_TRUE(!GetWorkPath().empty());
+ }
+}
+
+TEST(Runtime, GetOutputPath) {
+ NTesting::TScopedEnvironment contextGuard("YA_TEST_CONTEXT_FILE", ""); // remove context filename
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_EQ(GetOutputPath().Basename(), "testing_out_stuff");
+}
+
+TEST(Runtime, GetRamDrivePath) {
+ NTesting::TScopedEnvironment contextGuard("YA_TEST_CONTEXT_FILE", ""); // remove context filename
+ auto tmpDir = ::GetSystemTempDir();
+ NTesting::TScopedEnvironment guard("YA_TEST_RAM_DRIVE_PATH", tmpDir);
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_EQ(tmpDir, GetRamDrivePath());
+}
+
+TEST(Runtime, GetOutputRamDrivePath) {
+ NTesting::TScopedEnvironment contextGuard("YA_TEST_CONTEXT_FILE", ""); // remove context filename
+ auto tmpDir = ::GetSystemTempDir();
+ NTesting::TScopedEnvironment guard("YA_TEST_OUTPUT_RAM_DRIVE_PATH", tmpDir);
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_EQ(tmpDir, GetOutputRamDrivePath());
+}
+
+#ifdef _linux_
+TEST(Runtime, GdbPath) {
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+ EXPECT_TRUE(NFs::Exists(::GdbPath()));
+}
+#endif
+
+TString ReInitializeContext(TStringBuf data) {
+ auto tmpDir = ::GetSystemTempDir();
+ auto filename = tmpDir + "/context.json";
+ TOFStream stream(filename);
+ stream.Write(data.data(), data.size());
+ stream.Finish();
+
+ NTesting::TScopedEnvironment contextGuard("YA_TEST_CONTEXT_FILE", filename);
+ Singleton<NPrivate::TTestEnv>()->ReInitialize();
+
+ return filename;
+}
+
+TEST(Runtime, GetTestParam) {
+ TString context = R"json({
+ "runtime": {
+ "test_params": {
+ "a": "b",
+ "c": "d"
+ }
+ }
+ })json";
+ auto filename = ReInitializeContext(context);
+
+ EXPECT_EQ("b", GetTestParam("a"));
+ EXPECT_EQ("d", GetTestParam("c"));
+ EXPECT_EQ("", GetTestParam("e"));
+ EXPECT_EQ("w", GetTestParam("e", "w"));
+
+ Singleton<NPrivate::TTestEnv>()->AddTestParam("e", "e");
+ EXPECT_EQ("e", GetTestParam("e"));
+}
+
+TEST(Runtime, WatchProcessCore) {
+ TString context = R"json({
+ "internal": {
+ "core_search_file": "watch_core.txt"
+ }
+ })json";
+ auto filename = ReInitializeContext(context);
+
+ WatchProcessCore(1, "bin1", "pwd");
+ WatchProcessCore(2, "bin1");
+ StopProcessCoreWatching(2);
+
+ TIFStream file("watch_core.txt");
+ auto data = file.ReadAll();
+ TString expected = R"json({"cmd":"add","pid":1,"binary_path":"bin1","cwd":"pwd"}
+{"cmd":"add","pid":2,"binary_path":"bin1"}
+{"cmd":"drop","pid":2}
+)json";
+ EXPECT_EQ(expected, data);
+}
diff --git a/library/cpp/testing/common/ut/network_ut.cpp b/library/cpp/testing/common/ut/network_ut.cpp
new file mode 100644
index 0000000000..6a40775fd9
--- /dev/null
+++ b/library/cpp/testing/common/ut/network_ut.cpp
@@ -0,0 +1,54 @@
+#include <library/cpp/testing/common/network.h>
+#include <library/cpp/testing/common/scope.h>
+
+#include <util/generic/hash_set.h>
+
+#include <util/folder/dirut.h>
+#include <util/folder/path.h>
+#include <util/folder/tempdir.h>
+#include <util/network/sock.h>
+#include <util/system/fs.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+static TTempDir TmpDir;
+
+TEST(NetworkTest, FreePort) {
+ NTesting::TScopedEnvironment envGuard("PORT_SYNC_PATH", TmpDir.Name());
+
+ TVector<NTesting::TPortHolder> ports(Reserve(100));
+
+ for (size_t i = 0; i < 100; ++i) {
+ ports.push_back(NTesting::GetFreePort());
+ }
+
+ THashSet<ui16> uniqPorts;
+ for (auto& port : ports) {
+ const TString guardPath = TmpDir.Path() / ToString(static_cast<ui16>(port));
+ EXPECT_TRUE(NFs::Exists(guardPath));
+ EXPECT_TRUE(uniqPorts.emplace(port).second);
+
+ TInetStreamSocket sock;
+ TSockAddrInet addr(TIpHost{INADDR_ANY}, port);
+ ASSERT_EQ(0, SetSockOpt(sock, SOL_SOCKET, SO_REUSEADDR, 1));
+ EXPECT_EQ(0, sock.Bind(&addr));
+ }
+ ports.clear();
+ for (ui16 port : uniqPorts) {
+ const TString guardPath = TmpDir.Path() / ToString(port);
+ EXPECT_FALSE(NFs::Exists(guardPath));
+ }
+}
+
+
+TEST(FreePortTest, FreePortsRange) {
+ NTesting::TScopedEnvironment envGuard("PORT_SYNC_PATH", TmpDir.Name());
+
+ for (ui16 i = 2; i < 10; ++i) {
+ TVector<NTesting::TPortHolder> ports = NTesting::NLegacy::GetFreePortsRange(i);
+ ASSERT_EQ(i, ports.size());
+ for (ui16 j = 1; j < i; ++j) {
+ EXPECT_EQ(static_cast<ui16>(ports[j]), static_cast<ui16>(ports[0]) + j);
+ }
+ }
+}
diff --git a/library/cpp/testing/common/ut/scope_ut.cpp b/library/cpp/testing/common/ut/scope_ut.cpp
new file mode 100644
index 0000000000..4fb82c2466
--- /dev/null
+++ b/library/cpp/testing/common/ut/scope_ut.cpp
@@ -0,0 +1,28 @@
+#include <library/cpp/testing/common/scope.h>
+
+#include <util/system/env.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+TEST(TScopedEnvironment, SingleValue) {
+ auto before = GetEnv("ARCADIA_SOURCE_ROOT");
+ {
+ NTesting::TScopedEnvironment guard("ARCADIA_SOURCE_ROOT", "source");
+ EXPECT_EQ("source", GetEnv("ARCADIA_SOURCE_ROOT"));
+ }
+ EXPECT_EQ(before, GetEnv("ARCADIA_SOURCE_ROOT"));
+}
+
+TEST(TScopedEnvironment, MultiValue) {
+ TVector<TString> before{GetEnv("ARCADIA_SOURCE_ROOT"), GetEnv("ARCADIA_BUILD_ROOT")};
+ {
+ NTesting::TScopedEnvironment guard{{
+ {"ARCADIA_SOURCE_ROOT", "source"},
+ {"ARCADIA_BUILD_ROOT", "build"},
+ }};
+ EXPECT_EQ("source", GetEnv("ARCADIA_SOURCE_ROOT"));
+ EXPECT_EQ("build", GetEnv("ARCADIA_BUILD_ROOT"));
+ }
+ TVector<TString> after{GetEnv("ARCADIA_SOURCE_ROOT"), GetEnv("ARCADIA_BUILD_ROOT")};
+ EXPECT_EQ(before, after);
+}
diff --git a/library/cpp/testing/common/ut/ya.make b/library/cpp/testing/common/ut/ya.make
new file mode 100644
index 0000000000..053aa38079
--- /dev/null
+++ b/library/cpp/testing/common/ut/ya.make
@@ -0,0 +1,19 @@
+GTEST()
+OWNER(
+ amatanhead
+ bulatman
+ thegeorg
+ g:cpp-contrib
+)
+
+SRCS(
+ env_ut.cpp
+ network_ut.cpp
+ scope_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/testing/common
+)
+
+END()
diff --git a/library/cpp/testing/common/ya.make b/library/cpp/testing/common/ya.make
new file mode 100644
index 0000000000..2f4b0ce26e
--- /dev/null
+++ b/library/cpp/testing/common/ya.make
@@ -0,0 +1,23 @@
+LIBRARY()
+
+OWNER(
+ amatanhead
+ bulatman
+ thegeorg
+ g:cpp-contrib
+)
+
+SRCS(
+ env.cpp
+ network.cpp
+ probe.cpp
+ scope.cpp
+)
+
+PEERDIR(
+ library/cpp/json
+)
+
+END()
+
+RECURSE_FOR_TESTS(ut)
diff --git a/library/cpp/testing/gmock_in_unittest/events.cpp b/library/cpp/testing/gmock_in_unittest/events.cpp
new file mode 100644
index 0000000000..dbd65b727d
--- /dev/null
+++ b/library/cpp/testing/gmock_in_unittest/events.cpp
@@ -0,0 +1,32 @@
+#include "events.h"
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/generic/strbuf.h>
+#include <util/generic/string.h>
+#include <util/string/builder.h>
+
+void TGMockTestEventListener::OnTestPartResult(const testing::TestPartResult& result) {
+ if (result.failed()) {
+ const TString message = result.message();
+ const TString summary = result.summary();
+ TStringBuilder msg;
+ if (result.file_name())
+ msg << result.file_name() << TStringBuf(":");
+ if (result.line_number() != -1)
+ msg << result.line_number() << TStringBuf(":");
+ if (summary) {
+ if (msg) {
+ msg << TStringBuf("\n");
+ }
+ msg << summary;
+ }
+ if (message && summary != message) {
+ if (msg) {
+ msg << TStringBuf("\n");
+ }
+ msg << message;
+ }
+ NUnitTest::NPrivate::RaiseError(result.summary(), msg, result.fatally_failed());
+ }
+}
diff --git a/library/cpp/testing/gmock_in_unittest/events.h b/library/cpp/testing/gmock_in_unittest/events.h
new file mode 100644
index 0000000000..84c10a93de
--- /dev/null
+++ b/library/cpp/testing/gmock_in_unittest/events.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <gtest/gtest.h>
+
+class TGMockTestEventListener: public testing::EmptyTestEventListener {
+public:
+ void OnTestPartResult(const testing::TestPartResult& result) override;
+};
diff --git a/library/cpp/testing/gmock_in_unittest/example_ut/example_ut.cpp b/library/cpp/testing/gmock_in_unittest/example_ut/example_ut.cpp
new file mode 100644
index 0000000000..97f19050e4
--- /dev/null
+++ b/library/cpp/testing/gmock_in_unittest/example_ut/example_ut.cpp
@@ -0,0 +1,105 @@
+#include <library/cpp/testing/gmock_in_unittest/gmock.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/generic/string.h>
+
+// Set this variable to true if you want to see failures
+/////////////////////////////////////////////////////////
+static const bool fail = false;
+/////////////////////////////////////////////////////////
+
+class ITestIface {
+public:
+ virtual ~ITestIface() {
+ }
+
+ virtual void Func1() = 0;
+
+ virtual int Func2(const TString&) const = 0;
+};
+
+class TTestMock: public ITestIface {
+public:
+ MOCK_METHOD(void, Func1, (), (override));
+ MOCK_METHOD(int, Func2, (const TString&), (const, override));
+};
+
+using namespace testing;
+
+Y_UNIT_TEST_SUITE(TExampleGMockTest) {
+ Y_UNIT_TEST(TSimpleTest) {
+ TTestMock mock;
+ EXPECT_CALL(mock, Func1())
+ .Times(AtLeast(1));
+
+ if (!fail) {
+ mock.Func1();
+ }
+ }
+
+ Y_UNIT_TEST(TNonExpectedCallTest) {
+ TTestMock mock;
+ EXPECT_CALL(mock, Func1())
+ .Times(AtMost(1));
+ mock.Func1();
+ if (fail) {
+ mock.Func1();
+ }
+ }
+
+ Y_UNIT_TEST(TReturnValuesTest) {
+ TTestMock mock;
+ EXPECT_CALL(mock, Func2(TString("1")))
+ .WillOnce(Return(1))
+ .WillRepeatedly(Return(42));
+
+ EXPECT_CALL(mock, Func2(TString("hello")))
+ .WillOnce(Return(-1));
+
+ UNIT_ASSERT_VALUES_EQUAL(mock.Func2("hello"), -1);
+
+ UNIT_ASSERT_VALUES_EQUAL(mock.Func2("1"), 1);
+ UNIT_ASSERT_VALUES_EQUAL(mock.Func2("1"), 42);
+ UNIT_ASSERT_VALUES_EQUAL(mock.Func2("1"), 42);
+ UNIT_ASSERT_VALUES_EQUAL(mock.Func2("1"), 42);
+ UNIT_ASSERT_VALUES_EQUAL(mock.Func2("1"), 42);
+
+ if (fail) {
+ UNIT_ASSERT_VALUES_EQUAL(mock.Func2("hello"), -1); // expected to return -1 only once
+ }
+ }
+
+ Y_UNIT_TEST(TStrictCallSequenceTest) {
+ TTestMock mock;
+ {
+ InSequence seq;
+ EXPECT_CALL(mock, Func1())
+ .Times(1);
+ EXPECT_CALL(mock, Func2(_))
+ .Times(2)
+ .WillOnce(Return(1))
+ .WillOnce(Return(2));
+ EXPECT_CALL(mock, Func1());
+ }
+ mock.Func1();
+ UNIT_ASSERT_VALUES_EQUAL(mock.Func2("sample"), 1);
+ if (fail) {
+ mock.Func1();
+ }
+ UNIT_ASSERT_VALUES_EQUAL(mock.Func2(""), 2);
+ if (!fail) {
+ mock.Func1();
+ }
+ }
+
+ Y_UNIT_TEST(TUninterestingMethodIsFailureTest) {
+ StrictMock<TTestMock> mock;
+ EXPECT_CALL(mock, Func1())
+ .Times(1);
+ mock.Func1();
+ if (fail) {
+ mock.Func1();
+ }
+ }
+}
diff --git a/library/cpp/testing/gmock_in_unittest/example_ut/ya.make b/library/cpp/testing/gmock_in_unittest/example_ut/ya.make
new file mode 100644
index 0000000000..d2e5ee5d2a
--- /dev/null
+++ b/library/cpp/testing/gmock_in_unittest/example_ut/ya.make
@@ -0,0 +1,13 @@
+UNITTEST()
+
+OWNER(galaxycrab)
+
+PEERDIR(
+ library/cpp/testing/gmock_in_unittest
+)
+
+SRCS(
+ example_ut.cpp
+)
+
+END()
diff --git a/library/cpp/testing/gmock_in_unittest/gmock.h b/library/cpp/testing/gmock_in_unittest/gmock.h
new file mode 100644
index 0000000000..31f6aee1c3
--- /dev/null
+++ b/library/cpp/testing/gmock_in_unittest/gmock.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include <library/cpp/testing/gtest_extensions/gtest_extensions.h>
+
+#include <gmock/gmock.h>
diff --git a/library/cpp/testing/gmock_in_unittest/registration.cpp b/library/cpp/testing/gmock_in_unittest/registration.cpp
new file mode 100644
index 0000000000..c2872a4c27
--- /dev/null
+++ b/library/cpp/testing/gmock_in_unittest/registration.cpp
@@ -0,0 +1,20 @@
+#include "events.h"
+
+#include <gmock/gmock.h>
+
+#include <library/cpp/testing/unittest/plugin.h>
+
+namespace {
+ class TGMockUnittestPlugin: public NUnitTest::NPlugin::IPlugin {
+ public:
+ void OnStartMain(int argc, char* argv[]) override {
+ testing::InitGoogleMock(&argc, argv);
+ testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners();
+ delete listeners.Release(listeners.default_result_printer());
+ listeners.Append(new TGMockTestEventListener());
+ }
+ };
+
+ NUnitTest::NPlugin::TPluginRegistrator registerGMock(new TGMockUnittestPlugin());
+
+}
diff --git a/library/cpp/testing/gmock_in_unittest/ya.make b/library/cpp/testing/gmock_in_unittest/ya.make
new file mode 100644
index 0000000000..5de68ad98d
--- /dev/null
+++ b/library/cpp/testing/gmock_in_unittest/ya.make
@@ -0,0 +1,17 @@
+LIBRARY()
+
+OWNER(galaxycrab)
+
+PEERDIR(
+ contrib/restricted/googletest/googlemock
+ contrib/restricted/googletest/googletest
+ library/cpp/testing/gtest_extensions
+ library/cpp/testing/unittest
+)
+
+SRCS(
+ events.cpp
+ GLOBAL registration.cpp
+)
+
+END()
diff --git a/library/cpp/testing/gtest_extensions/README.md b/library/cpp/testing/gtest_extensions/README.md
new file mode 100644
index 0000000000..5445c7a464
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/README.md
@@ -0,0 +1,5 @@
+# Extensions for Gtest and Gmock
+
+Extensions that enable better support of util types in gtest and gmock: pretty printers, matchers, some convenience macros.
+
+If you're using `GTEST`, include `library/cpp/testing/gtest/gtest.h` and it will automatically enable these extensions. This is the preferred way to include gtest and gmock as opposed to including gtest, gmock and extensions directly. It eliminates chances of forgetting to include extensions.
diff --git a/library/cpp/testing/gtest_extensions/assertions.cpp b/library/cpp/testing/gtest_extensions/assertions.cpp
new file mode 100644
index 0000000000..f390409d1b
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/assertions.cpp
@@ -0,0 +1,90 @@
+#include "assertions.h"
+
+#include <util/string/builder.h>
+#include <util/string/split.h>
+#include <util/system/type_name.h>
+
+namespace NGTest::NInternal {
+ namespace {
+ void FormatActual(const std::exception& err, const TBackTrace* bt, TStringBuilder& out) {
+ out << "an exception of type " << TypeName(err) << " "
+ << "with message " << TString(err.what()).Quote() << ".";
+ if (bt) {
+ out << "\n Trace: ";
+ for (auto& line: StringSplitter(bt->PrintToString()).Split('\n')) {
+ out << " " << line.Token() << "\n";
+ }
+ }
+ }
+
+ void FormatActual(TStringBuilder& out) {
+ out << " Actual: it throws ";
+ auto exceptionPtr = std::current_exception();
+ if (exceptionPtr) {
+ try {
+ std::rethrow_exception(exceptionPtr);
+ } catch (const yexception& err) {
+ FormatActual(err, err.BackTrace(), out);
+ return;
+ } catch (const std::exception& err) {
+ FormatActual(err, nullptr, out);
+ return;
+ } catch (...) {
+ out << "an unknown exception.";
+ return;
+ }
+ }
+ out << "nothing.";
+ }
+
+ void FormatExpected(const char* statement, const char* type, const TString& contains, TStringBuilder& out) {
+ out << "Expected: ";
+ if (TStringBuf(statement).size() > 80) {
+ out << "statement";
+ } else {
+ out << statement;
+ }
+ out << " throws an exception of type " << type;
+
+ if (!contains.empty()) {
+ out << " with message containing " << contains.Quote();
+ }
+
+ out << ".";
+ }
+ }
+
+ TString FormatErrorWrongException(const char* statement, const char* type) {
+ return FormatErrorWrongException(statement, type, "");
+ }
+
+ TString FormatErrorWrongException(const char* statement, const char* type, TString contains) {
+ TStringBuilder out;
+
+ FormatExpected(statement, type, contains, out);
+ out << "\n";
+ FormatActual(out);
+
+ return out;
+ }
+
+ TString FormatErrorUnexpectedException(const char* statement) {
+ TStringBuilder out;
+
+ out << "Expected: ";
+ if (TStringBuf(statement).size() > 80) {
+ out << "statement";
+ } else {
+ out << statement;
+ }
+ out << " doesn't throw an exception.\n ";
+
+ FormatActual(out);
+
+ return out;
+ }
+
+ bool ExceptionMessageContains(const std::exception& err, TString contains) {
+ return TStringBuf(err.what()).Contains(contains);
+ }
+}
diff --git a/library/cpp/testing/gtest_extensions/assertions.h b/library/cpp/testing/gtest_extensions/assertions.h
new file mode 100644
index 0000000000..e8ea07b5df
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/assertions.h
@@ -0,0 +1,111 @@
+#pragma once
+
+#include <util/generic/string.h>
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+/**
+ * Check that the given statement throws an exception of the given type,
+ * and that the thrown exception message contains the given substring.
+ */
+#define EXPECT_THROW_MESSAGE_HAS_SUBSTR(statement, expectedException, substring) \
+ _Y_GTEST_EXPECT_THROW_MESSAGE_HAS_SUBSTR_IMPL_(statement, expectedException, substring, GTEST_NONFATAL_FAILURE_)
+
+/**
+ * Check that the given statement throws an exception of the given type,
+ * and that the thrown exception message contains the given substring.
+ */
+#define ASSERT_THROW_MESSAGE_HAS_SUBSTR(statement, expectedException, substring) \
+ _Y_GTEST_EXPECT_THROW_MESSAGE_HAS_SUBSTR_IMPL_(statement, expectedException, substring, GTEST_FATAL_FAILURE_)
+
+
+// Improve default macros. New implementation shows better exception messages.
+// See https://github.com/google/googletest/issues/2878
+
+#undef EXPECT_THROW
+#define EXPECT_THROW(statement, expectedException) \
+ _Y_GTEST_EXPECT_THROW_IMPL_(statement, expectedException, GTEST_NONFATAL_FAILURE_)
+
+#undef ASSERT_THROW
+#define ASSERT_THROW(statement, expectedException) \
+ _Y_GTEST_EXPECT_THROW_IMPL_(statement, expectedException, GTEST_FATAL_FAILURE_)
+
+#undef EXPECT_NO_THROW
+#define EXPECT_NO_THROW(statement) \
+ _Y_GTEST_EXPECT_NO_THROW_IMPL_(statement, GTEST_NONFATAL_FAILURE_)
+
+#undef ASSERT_NO_THROW
+#define ASSERT_NO_THROW(statement) \
+ _Y_GTEST_EXPECT_NO_THROW_IMPL_(statement, GTEST_FATAL_FAILURE_)
+
+
+// Implementation details
+
+namespace NGTest::NInternal {
+ TString FormatErrorWrongException(const char* statement, const char* type);
+ TString FormatErrorWrongException(const char* statement, const char* type, TString contains);
+ TString FormatErrorUnexpectedException(const char* statement);
+ bool ExceptionMessageContains(const std::exception& err, TString contains);
+}
+
+#define _Y_GTEST_EXPECT_THROW_IMPL_(statement, expectedException, fail) \
+ GTEST_AMBIGUOUS_ELSE_BLOCKER_ \
+ if (::TString gtestMsg = ""; ::testing::internal::AlwaysTrue()) { \
+ bool gtestCaughtExpected = false; \
+ try { \
+ GTEST_SUPPRESS_UNREACHABLE_CODE_WARNING_BELOW_(statement); \
+ } catch (expectedException const&) { \
+ gtestCaughtExpected = true; \
+ } catch (...) { \
+ gtestMsg = ::NGTest::NInternal::FormatErrorWrongException( \
+ #statement, #expectedException); \
+ goto GTEST_CONCAT_TOKEN_(gtest_label_testthrow_, __LINE__); \
+ } if (!gtestCaughtExpected) { \
+ gtestMsg = ::NGTest::NInternal::FormatErrorWrongException( \
+ #statement, #expectedException); \
+ goto GTEST_CONCAT_TOKEN_(gtest_label_testthrow_, __LINE__); \
+ } \
+ } else \
+ GTEST_CONCAT_TOKEN_(gtest_label_testthrow_, __LINE__): \
+ fail(gtestMsg.c_str())
+
+#define _Y_GTEST_EXPECT_THROW_MESSAGE_HAS_SUBSTR_IMPL_(statement, expectedException, substring, fail) \
+ GTEST_AMBIGUOUS_ELSE_BLOCKER_ \
+ if (::TString gtestMsg = ""; ::testing::internal::AlwaysTrue()) { \
+ bool gtestCaughtExpected = false; \
+ ::TString gtestSubstring{substring}; \
+ try { \
+ GTEST_SUPPRESS_UNREACHABLE_CODE_WARNING_BELOW_(statement); \
+ } catch (expectedException const& gtestError) { \
+ if (!::NGTest::NInternal::ExceptionMessageContains(gtestError, gtestSubstring)) { \
+ gtestMsg = ::NGTest::NInternal::FormatErrorWrongException( \
+ #statement, #expectedException, gtestSubstring); \
+ goto GTEST_CONCAT_TOKEN_(gtest_label_testthrowsubstr_, __LINE__); \
+ } \
+ gtestCaughtExpected = true; \
+ } catch (...) { \
+ gtestMsg = ::NGTest::NInternal::FormatErrorWrongException( \
+ #statement, #expectedException, gtestSubstring); \
+ goto GTEST_CONCAT_TOKEN_(gtest_label_testthrowsubstr_, __LINE__); \
+ } if (!gtestCaughtExpected) { \
+ gtestMsg = ::NGTest::NInternal::FormatErrorWrongException( \
+ #statement, #expectedException, gtestSubstring); \
+ goto GTEST_CONCAT_TOKEN_(gtest_label_testthrowsubstr_, __LINE__); \
+ } \
+ } else \
+ GTEST_CONCAT_TOKEN_(gtest_label_testthrowsubstr_, __LINE__): \
+ fail(gtestMsg.c_str())
+
+#define _Y_GTEST_EXPECT_NO_THROW_IMPL_(statement, fail) \
+ GTEST_AMBIGUOUS_ELSE_BLOCKER_ \
+ if (::TString gtestMsg = ""; ::testing::internal::AlwaysTrue()) { \
+ try { \
+ GTEST_SUPPRESS_UNREACHABLE_CODE_WARNING_BELOW_(statement); \
+ } catch (...) { \
+ gtestMsg = ::NGTest::NInternal::FormatErrorUnexpectedException(#statement); \
+ goto GTEST_CONCAT_TOKEN_(gtest_label_testnothrow_, __LINE__); \
+ } \
+ } else \
+ GTEST_CONCAT_TOKEN_(gtest_label_testnothrow_, __LINE__): \
+ fail(gtestMsg.c_str())
diff --git a/library/cpp/testing/gtest_extensions/gtest_extensions.cpp b/library/cpp/testing/gtest_extensions/gtest_extensions.cpp
new file mode 100644
index 0000000000..1277a804bc
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/gtest_extensions.cpp
@@ -0,0 +1 @@
+#include "gtest_extensions.h"
diff --git a/library/cpp/testing/gtest_extensions/gtest_extensions.h b/library/cpp/testing/gtest_extensions/gtest_extensions.h
new file mode 100644
index 0000000000..e20532241e
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/gtest_extensions.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include "assertions.h"
+#include "matchers.h"
+#include "pretty_printers.h"
+#include "probe.h"
diff --git a/library/cpp/testing/gtest_extensions/matchers.cpp b/library/cpp/testing/gtest_extensions/matchers.cpp
new file mode 100644
index 0000000000..7da7be8b3c
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/matchers.cpp
@@ -0,0 +1 @@
+#include "matchers.h"
diff --git a/library/cpp/testing/gtest_extensions/matchers.h b/library/cpp/testing/gtest_extensions/matchers.h
new file mode 100644
index 0000000000..044c1c3ee4
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/matchers.h
@@ -0,0 +1,132 @@
+#pragma once
+
+#include <util/generic/string.h>
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+namespace testing {
+ /**
+ * When matching `const TStringBuf&`, implicitly convert other strings and string views to `Eq` matchers.
+ */
+ template <typename T, typename TT>
+ class Matcher<const TBasicStringBuf<T, TT>&>: public internal::MatcherBase<const TBasicStringBuf<T, TT>&> {
+ public:
+ Matcher() {
+ }
+
+ explicit Matcher(const MatcherInterface<const TBasicStringBuf<T, TT>&>* impl)
+ : internal::MatcherBase<const TBasicStringBuf<T, TT>&>(impl) {
+ }
+
+ template <typename M, typename = typename std::remove_reference<M>::type::is_gtest_matcher>
+ Matcher(M&& m)
+ : internal::MatcherBase<const TBasicStringBuf<T, TT>&>(std::forward<M>(m)) {
+ }
+
+ Matcher(const TBasicString<T, TT>& s) {
+ *this = Eq(TBasicStringBuf<T, TT>(s));
+ }
+
+ Matcher(const T* s) {
+ *this = Eq(TBasicStringBuf<T, TT>(s));
+ }
+
+ Matcher(TBasicStringBuf<T, TT> s) {
+ *this = Eq(s);
+ }
+ };
+
+ /**
+ * When matching `TBasicBuf`, implicitly convert other strings and string views to `Eq` matchers.
+ */
+ template <typename T, typename TT>
+ class Matcher<TBasicStringBuf<T, TT>>: public internal::MatcherBase<TBasicStringBuf<T, TT>> {
+ public:
+ Matcher() {
+ }
+
+ explicit Matcher(const MatcherInterface <TBasicStringBuf<T, TT>>* impl)
+ : internal::MatcherBase<TBasicStringBuf<T, TT>>(impl) {
+ }
+
+ explicit Matcher(const MatcherInterface<const TBasicStringBuf<T, TT>&>* impl)
+ : internal::MatcherBase<TBasicStringBuf<T, TT>>(impl) {
+ }
+
+ template <typename M, typename = typename std::remove_reference<M>::type::is_gtest_matcher>
+ Matcher(M&& m)
+ : internal::MatcherBase<TBasicStringBuf<T, TT>>(std::forward<M>(m)) {
+ }
+
+ Matcher(const TBasicString<T, TT>& s) {
+ *this = Eq(TBasicString<T, TT>(s));
+ }
+
+ Matcher(const T* s) {
+ *this = Eq(TBasicString<T, TT>(s));
+ }
+
+ Matcher(TBasicStringBuf<T, TT> s) {
+ *this = Eq(s);
+ }
+ };
+
+ /**
+ * When matching `const TString&`, implicitly convert other strings and string views to `Eq` matchers.
+ */
+ template <typename T, typename TT>
+ class Matcher<const TBasicString<T, TT>&>: public internal::MatcherBase<const TBasicString<T, TT>&> {
+ public:
+ Matcher() {
+ }
+
+ explicit Matcher(const MatcherInterface<const TBasicString<T, TT>&>* impl)
+ : internal::MatcherBase<const TBasicString<T, TT>&>(impl) {
+ }
+
+ Matcher(const TBasicString<T, TT>& s) {
+ *this = Eq(s);
+ }
+
+ template <typename M, typename = typename std::remove_reference<M>::type::is_gtest_matcher>
+ Matcher(M&& m)
+ : internal::MatcherBase<const TBasicString<T, TT>&>(std::forward<M>(m)) {
+ }
+
+ Matcher(const T* s) {
+ *this = Eq(TBasicString<T, TT>(s));
+ }
+ };
+
+ /**
+ * When matching `TString`, implicitly convert other strings and string views to `Eq` matchers.
+ */
+ template <typename T, typename TT>
+ class Matcher<TBasicString<T, TT>>: public internal::MatcherBase<TBasicString<T, TT>> {
+ public:
+ Matcher() {
+ }
+
+ explicit Matcher(const MatcherInterface <TBasicString<T, TT>>* impl)
+ : internal::MatcherBase<TBasicString<T, TT>>(impl) {
+ }
+
+ explicit Matcher(const MatcherInterface<const TBasicString<T, TT>&>* impl)
+ : internal::MatcherBase<TBasicString<T, TT>>(impl) {
+ }
+
+ template <typename M, typename = typename std::remove_reference<M>::type::is_gtest_matcher>
+ Matcher(M&& m)
+ : internal::MatcherBase<TBasicString<T, TT>>(std::forward<M>(m)) {
+ }
+
+ Matcher(const TBasicString<T, TT>& s) {
+ *this = Eq(s);
+ }
+
+ Matcher(const T* s) {
+ *this = Eq(TBasicString<T, TT>(s));
+ }
+ };
+}
diff --git a/library/cpp/testing/gtest_extensions/pretty_printers.cpp b/library/cpp/testing/gtest_extensions/pretty_printers.cpp
new file mode 100644
index 0000000000..401745cbcb
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/pretty_printers.cpp
@@ -0,0 +1 @@
+#include "pretty_printers.h"
diff --git a/library/cpp/testing/gtest_extensions/pretty_printers.h b/library/cpp/testing/gtest_extensions/pretty_printers.h
new file mode 100644
index 0000000000..14d8284446
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/pretty_printers.h
@@ -0,0 +1,84 @@
+#pragma once
+
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/maybe.h>
+#include <util/generic/variant.h>
+#include <util/stream/output.h>
+#include <util/stream/str.h>
+#include <util/datetime/base.h>
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+/**
+ * Automatically define GTest pretty printer for type that can print itself to util's `IOutputStream`.
+ *
+ * Note that this macro should be instantiated in the same namespace as the type you're printing, otherwise
+ * ADL will not find it.
+ *
+ * Example:
+ *
+ * We define a struct `TMyContainer` and an output operator that works with `IOutputStream`. We then use this macro
+ * to automatically define GTest pretty printer:
+ *
+ * ```
+ * namespace NMy {
+ * struct TMyContainer {
+ * int x, y;
+ * };
+ * }
+ *
+ * template <>
+ * inline void Out<NMy::TMyContainer>(IOutputStream& stream, TTypeTraits<NMy::TMyContainer>::TFuncParam value) {
+ * stream << "{ x=" << value.x << ", y=" << value.y << " }";
+ * }
+ *
+ * namespace NMy {
+ * Y_GTEST_ARCADIA_PRINTER(TMyContainer)
+ * }
+ * ```
+ */
+#define Y_GTEST_ARCADIA_PRINTER(T) \
+ void PrintTo(const T& value, std::ostream* stream) { \
+ ::TString ss; \
+ ::TStringOutput s{ss}; \
+ s << value; \
+ *stream << ss; \
+ }
+
+
+template <typename TCharType, typename TCharTraits>
+void PrintTo(const TBasicString<TCharType, TCharTraits>& value, std::ostream* stream) {
+ *stream << value.Quote().c_str();
+}
+
+template <typename TCharType, typename TCharTraits>
+void PrintTo(TBasicStringBuf<TCharType, TCharTraits> value, std::ostream* stream) {
+ *stream << TBasicString<TCharType, TCharTraits>{value}.Quote().c_str();
+}
+
+template <typename T, typename P>
+void PrintTo(const TMaybe<T, P>& value, std::ostream* stream) {
+ if (value.Defined()) {
+ ::testing::internal::UniversalPrint(value.GetRef(), stream);
+ } else {
+ *stream << "nothing";
+ }
+}
+
+inline void PrintTo(TNothing /* value */, std::ostream* stream) {
+ *stream << "nothing";
+}
+
+inline void PrintTo(std::monostate /* value */, std::ostream* stream) {
+ *stream << "monostate";
+}
+
+inline void PrintTo(TInstant value, std::ostream* stream) {
+ *stream << value.ToString();
+}
+
+inline void PrintTo(TDuration value, std::ostream* stream) {
+ *stream << value.ToString();
+}
diff --git a/library/cpp/testing/gtest_extensions/probe.cpp b/library/cpp/testing/gtest_extensions/probe.cpp
new file mode 100644
index 0000000000..c3a49b9323
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/probe.cpp
@@ -0,0 +1,13 @@
+#include "probe.h"
+
+#include <ostream>
+
+namespace testing {
+ void PrintTo(const TProbeState& state, ::std::ostream* os) {
+ int copies = state.CopyConstructors + state.CopyAssignments;
+ int moves = state.MoveConstructors + state.MoveAssignments;
+ *os << state.Constructors << " ctors, " << state.Destructors << " dtors; "
+ << "copies: " << copies << " = " << state.CopyConstructors << " + " << state.CopyAssignments << "; "
+ << "moves: " << moves << " = " << state.MoveConstructors << " + " << state.MoveAssignments;
+ }
+} // namespace testing
diff --git a/library/cpp/testing/gtest_extensions/probe.h b/library/cpp/testing/gtest_extensions/probe.h
new file mode 100644
index 0000000000..7d1fee83d3
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/probe.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include <util/system/yassert.h>
+
+#include <library/cpp/testing/common/probe.h>
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+namespace testing {
+ using NTesting::TProbe;
+ using NTesting::TProbeState;
+ using NTesting::TCoercibleToProbe;
+
+ // A helper functor which extracts from probe-like objectss their state.
+ struct TProbableTraits {
+ static const TProbeState& ExtractState(const TProbeState& probe) {
+ return probe;
+ }
+
+ static const TProbeState& ExtractState(const TProbeState* probe) {
+ return *probe;
+ }
+
+ static const TProbeState& ExtractState(const TProbe& probe) {
+ return *probe.State;
+ }
+
+ static const TProbeState& ExtractState(const TCoercibleToProbe& probe) {
+ return *probe.State;
+ }
+ };
+
+ void PrintTo(const TProbeState& state, ::std::ostream* os);
+
+ inline void PrintTo(const TProbe& probe, ::std::ostream* os) {
+ PrintTo(TProbableTraits::ExtractState(probe), os);
+ }
+
+ inline void PrintTo(const TCoercibleToProbe& probe, ::std::ostream* os) {
+ PrintTo(TProbableTraits::ExtractState(probe), os);
+ }
+
+ MATCHER(IsAlive, "is alive") {
+ Y_UNUSED(result_listener);
+ const auto& state = TProbableTraits::ExtractState(arg);
+ return state.Destructors < state.Constructors + state.CopyConstructors + state.CopyAssignments;
+ }
+
+ MATCHER(IsDead, "is dead") {
+ Y_UNUSED(result_listener);
+ const auto& state = TProbableTraits::ExtractState(arg);
+ return state.Destructors == state.Constructors + state.CopyConstructors + state.CopyAssignments;
+ }
+
+ MATCHER_P2(HasCopyMoveCounts, copyCount, moveCount, "" + \
+ PrintToString(copyCount) + " copy constructors and " + \
+ PrintToString(moveCount) + " move constructors were called") {
+ Y_UNUSED(result_listener);
+ const auto& state = TProbableTraits::ExtractState(arg);
+ return state.CopyConstructors == copyCount && state.MoveConstructors == moveCount;
+ }
+
+ MATCHER(NoCopies, "no copies were made") {
+ Y_UNUSED(result_listener);
+ const auto& state = TProbableTraits::ExtractState(arg);
+ return 0 == state.CopyConstructors && 0 == state.CopyAssignments;
+ }
+
+ MATCHER(NoMoves, "no moves were made") {
+ Y_UNUSED(result_listener);
+ const auto& state = TProbableTraits::ExtractState(arg);
+ return 0 == state.MoveConstructors && 0 == state.MoveAssignments;
+ }
+
+ MATCHER(NoAssignments, "no assignments were made") {
+ Y_UNUSED(result_listener);
+ const auto& state = TProbableTraits::ExtractState(arg);
+ return 0 == state.CopyAssignments && 0 == state.MoveAssignments;
+ }
+}
diff --git a/library/cpp/testing/gtest_extensions/ut/README.md b/library/cpp/testing/gtest_extensions/ut/README.md
new file mode 100644
index 0000000000..ee8d212c18
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/ut/README.md
@@ -0,0 +1 @@
+Note: integration tests are located in */devtools/ya/test/tests/gtest_beta*. Launch them as well after changing this library.
diff --git a/library/cpp/testing/gtest_extensions/ut/gtest_extensions_ut.cpp b/library/cpp/testing/gtest_extensions/ut/gtest_extensions_ut.cpp
new file mode 100644
index 0000000000..81cdfd0427
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/ut/gtest_extensions_ut.cpp
@@ -0,0 +1,346 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <util/generic/string.h>
+#include <util/generic/maybe.h>
+#include <util/stream/output.h>
+#include <util/stream/str.h>
+
+namespace {
+ class IMock {
+ public:
+ virtual void M1(const TStringBuf&) = 0;
+ virtual void M2(TStringBuf) = 0;
+ virtual void M3(const TString&) = 0;
+ virtual void M4(TString) = 0;
+ };
+
+ class TSampleMock : IMock {
+ public:
+ MOCK_METHOD(void, M1, (const TStringBuf&));
+ MOCK_METHOD(void, M2, (TStringBuf));
+ MOCK_METHOD(void, M3, (const TString&));
+ MOCK_METHOD(void, M4, (TString));
+ };
+}
+
+
+TEST(MatchersSpecializations, String) {
+ TSampleMock mock;
+
+ TStringBuf simpleStringBuf = "SimpleStringBuf";
+ const TStringBuf constSimpleStringBuf = "ConstSimpleStringBuf";
+
+ TString simpleString = "SimpleString";
+ const TString constSimpleString = "ConstSimpleString";
+
+ EXPECT_CALL(mock, M1("ConstSimpleStringBuf")).Times(1);
+ EXPECT_CALL(mock, M2("SimpleStringBuf")).Times(1);
+ EXPECT_CALL(mock, M3("ConstSimpleString")).Times(1);
+ EXPECT_CALL(mock, M4("SimpleString")).Times(1);
+
+ mock.M1(constSimpleStringBuf);
+ mock.M2(simpleStringBuf);
+ mock.M3(constSimpleString);
+ mock.M4(simpleString);
+}
+
+template <typename T, typename M>
+std::pair<bool, std::string> Match(T&& t, M&& m) {
+ testing::StringMatchResultListener listener;
+ auto matches = testing::SafeMatcherCast<T>(std::forward<M>(m)).MatchAndExplain(std::forward<T>(t), &listener);
+ return {matches, listener.str()};
+}
+
+TEST(Matchers, Throws) {
+ auto matcher = testing::Throws<std::runtime_error>();
+
+ {
+ std::stringstream ss;
+ testing::SafeMatcherCast<void(*)()>(matcher).DescribeTo(&ss);
+ auto explanation = ss.str();
+
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::runtime_error("error message"); }, matcher);
+ EXPECT_TRUE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::logic_error("error message"); }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::logic_error"));
+ EXPECT_THAT(explanation, testing::HasSubstr("\"error message\""));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw 10; }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("throws an exception of an unknown type"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { (void)0; }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("does not throw any exception"));
+ }
+}
+
+TEST(Matchers, ThrowsMessage) {
+ auto matcher = testing::ThrowsMessage<std::runtime_error>(testing::HasSubstr("error message"));
+
+ {
+ std::stringstream ss;
+ testing::SafeMatcherCast<void(*)()>(matcher).DescribeTo(&ss);
+ auto explanation = ss.str();
+
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ EXPECT_THAT(explanation, testing::HasSubstr("\"error message\""));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::runtime_error("error message"); }, matcher);
+ EXPECT_TRUE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::runtime_error("message error"); }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::logic_error("error message"); }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::logic_error"));
+ EXPECT_THAT(explanation, testing::HasSubstr("\"error message\""));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw 10; }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("throws an exception of an unknown type"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { (void)0; }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("does not throw any exception"));
+ }
+}
+
+TEST(Matchers, ThrowsMessageHasSubstr) {
+ auto matcher = testing::ThrowsMessage<std::runtime_error>(testing::HasSubstr("error message"));
+
+ {
+ std::stringstream ss;
+ testing::SafeMatcherCast<void(*)()>(matcher).DescribeTo(&ss);
+ auto explanation = ss.str();
+
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ EXPECT_THAT(explanation, testing::HasSubstr("\"error message\""));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::runtime_error("error message"); }, matcher);
+ EXPECT_TRUE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::runtime_error("message error"); }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::logic_error("error message"); }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::logic_error"));
+ EXPECT_THAT(explanation, testing::HasSubstr("\"error message\""));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw 10; }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("throws an exception of an unknown type"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { (void)0; }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("does not throw any exception"));
+ }
+}
+
+TEST(Matchers, ThrowsCondition) {
+ auto matcher = testing::Throws<std::runtime_error>(
+ testing::Property(&std::exception::what, testing::HasSubstr("error message")));
+
+ {
+ std::stringstream ss;
+ testing::SafeMatcherCast<void(*)()>(matcher).DescribeTo(&ss);
+ auto explanation = ss.str();
+
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ EXPECT_THAT(explanation, testing::HasSubstr("\"error message\""));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::runtime_error("error message"); }, matcher);
+ EXPECT_TRUE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::runtime_error("message error"); }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error"));
+ EXPECT_THAT(explanation, testing::HasSubstr("\"message error\""));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw std::logic_error("error message"); }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("std::logic_error"));
+ EXPECT_THAT(explanation, testing::HasSubstr("\"error message\""));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { throw 10; }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("throws an exception of an unknown type"));
+ }
+
+ {
+ auto [matched, explanation] = Match([]() { (void)0; }, matcher);
+ EXPECT_FALSE(matched);
+ EXPECT_THAT(explanation, testing::HasSubstr("does not throw any exception"));
+ }
+}
+
+template <typename T>
+std::string GtestPrint(T&& v) {
+ std::stringstream ss;
+ testing::internal::UniversalPrint(std::forward<T>(v), &ss);
+ return ss.str();
+}
+
+struct TThrowsOnMove {
+ TThrowsOnMove() = default;
+ TThrowsOnMove(TThrowsOnMove&&) {
+ ythrow yexception() << "move failed";
+ }
+};
+
+TEST(PrettyPrinters, String) {
+ EXPECT_EQ(GtestPrint(TString("hello world")), "\"hello world\"");
+ EXPECT_EQ(GtestPrint(TStringBuf("hello world")), "\"hello world\"");
+}
+
+TEST(PrettyPrinters, Maybe) {
+ EXPECT_EQ(GtestPrint(TMaybe<TString>("hello world")), "\"hello world\"");
+ EXPECT_EQ(GtestPrint(TMaybe<TString>()), "nothing");
+ EXPECT_EQ(GtestPrint(Nothing()), "nothing");
+}
+
+struct T1 {
+ int x;
+};
+
+void PrintTo(T1 value, std::ostream* stream) {
+ *stream << "T1{" << value.x << "}";
+}
+
+struct T2 {
+ int x;
+};
+
+Y_DECLARE_OUT_SPEC(inline, T2, stream, value) {
+ stream << "T2{" << value.x << "}";
+}
+
+Y_GTEST_ARCADIA_PRINTER(T2)
+
+TEST(PrettyPrinters, Custom) {
+ EXPECT_EQ(GtestPrint(T1{10}), "T1{10}");
+}
+
+TEST(PrettyPrinters, CustomArcadia) {
+ EXPECT_EQ(GtestPrint(T2{10}), "T2{10}");
+}
+
+TEST(Exceptions, ExpectThrow) {
+ EXPECT_THROW(ythrow yexception() << "msg", yexception);
+}
+
+TEST(Exceptions, ExpectThrowStructuredBindings) {
+ auto [a, b] = std::make_pair("a", "b");
+ EXPECT_THROW(throw yexception() << a << "-" << b, yexception);
+}
+
+TEST(Exceptions, ExpectThrowSkipInThrowTest) {
+ // this test should be skipped, not failed
+ EXPECT_THROW(GTEST_SKIP(), yexception);
+}
+
+TEST(Exceptions, AssertThrow) {
+ ASSERT_THROW(ythrow yexception() << "msg", yexception);
+}
+
+TEST(Exceptions, AssertThrowStructuredBindings) {
+ auto [a, b] = std::make_pair("a", "b");
+ ASSERT_THROW(throw yexception() << a << "-" << b, yexception);
+}
+
+TEST(Exceptions, AssertThrowSkipInThrowTest) {
+ // this test should be skipped, not failed
+ ASSERT_THROW(GTEST_SKIP(), yexception);
+}
+
+TEST(Exceptions, ExpectThrowMessageHasSubstr) {
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(ythrow yexception() << "msg", yexception, "msg");
+}
+
+TEST(Exceptions, ExpectThrowMessageHasSubstrStructuredBindings) {
+ auto [a, b] = std::make_pair("a", "b");
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(throw yexception() << a << "-" << b, yexception, "-");
+}
+
+TEST(Exceptions, ExpectThrowMessageHasSubstrSkipInThrowTest) {
+ // this test should be skipped, not failed
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(GTEST_SKIP(), yexception, "-");
+}
+
+TEST(Exceptions, AssertThrowMessageHasSubstr) {
+ ASSERT_THROW_MESSAGE_HAS_SUBSTR(ythrow yexception() << "msg", yexception, "msg");
+}
+
+TEST(Exceptions, AssertThrowMessageHasSubstrStructuredBindings) {
+ auto [a, b] = std::make_pair("a", "b");
+ ASSERT_THROW_MESSAGE_HAS_SUBSTR(throw yexception() << a << "-" << b, yexception, "-");
+}
+
+TEST(Exceptions, AssertThrowMessageHasSubstrSkipInThrowTest) {
+ // this test should be skipped, not failed
+ ASSERT_THROW_MESSAGE_HAS_SUBSTR(GTEST_SKIP(), yexception, "-");
+}
+
+TEST(Exceptions, ExpectNoThrow) {
+ EXPECT_NO_THROW((void)0);
+}
+
+TEST(Exceptions, AssertNoThrow) {
+ ASSERT_NO_THROW((void)0);
+}
+
+TEST(Exceptions, ExpectAnyThrow) {
+ EXPECT_ANY_THROW(ythrow yexception() << "msg");
+}
+
+TEST(Exceptions, AssertAnyThrow) {
+ ASSERT_ANY_THROW(ythrow yexception() << "msg");
+}
diff --git a/library/cpp/testing/gtest_extensions/ut/probe_ut.cpp b/library/cpp/testing/gtest_extensions/ut/probe_ut.cpp
new file mode 100644
index 0000000000..a9d53f896a
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/ut/probe_ut.cpp
@@ -0,0 +1,54 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+using namespace testing;
+
+TEST(ProbeStateTest, Example) {
+ // check that our test function does not make a copy of passed argument
+ auto copyless = [](auto&& x) {
+ TProbe p(std::move(x));
+ p.Touch();
+ return p;
+ };
+
+ TProbeState state;
+ auto probe = copyless(TProbe(&state));
+ EXPECT_EQ(1, state.Touches);
+ EXPECT_THAT(state, HasCopyMoveCounts(0, 2));
+}
+
+TEST(ProbeTest, Construct) {
+ TProbeState state;
+ {
+ TProbe probe(&state);
+ EXPECT_THAT(state, IsAlive());
+ }
+ EXPECT_THAT(state, IsDead());
+}
+
+TEST(ProbeTest, Copy) {
+ TProbeState state;
+
+ TProbe probe(&state);
+ TProbe copy(probe);
+ EXPECT_THAT(state, HasCopyMoveCounts(1, 0));
+ EXPECT_THAT(state, NoAssignments());
+ EXPECT_THAT(state, NoMoves());
+
+ TProbe copy2 = TProbe::ExplicitlyCreateInvalidProbe();
+ copy2 = probe;
+ EXPECT_EQ(1, state.CopyAssignments);
+}
+
+TEST(ProbeTest, Move) {
+ TProbeState state;
+ TProbe probe(&state);
+ TProbe probe2(std::move(probe));
+ EXPECT_FALSE(probe.IsValid());
+ EXPECT_THAT(state, NoCopies());
+
+ EXPECT_THAT(state, HasCopyMoveCounts(0, 1));
+
+ TProbe probe3 = TProbe::ExplicitlyCreateInvalidProbe();
+ probe3 = std::move(probe2);
+ EXPECT_EQ(1, state.MoveAssignments);
+}
diff --git a/library/cpp/testing/gtest_extensions/ut/ya.make b/library/cpp/testing/gtest_extensions/ut/ya.make
new file mode 100644
index 0000000000..39b41cecfd
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/ut/ya.make
@@ -0,0 +1,20 @@
+GTEST()
+OWNER(
+ amatanhead
+ bulatman
+ dancingqueue
+ prettyboy
+ thegeorg
+ g:cpp-contrib
+)
+
+SRCS(
+ gtest_extensions_ut.cpp
+ probe_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/testing/gtest_extensions
+)
+
+END()
diff --git a/library/cpp/testing/gtest_extensions/ya.make b/library/cpp/testing/gtest_extensions/ya.make
new file mode 100644
index 0000000000..e24e81e8bd
--- /dev/null
+++ b/library/cpp/testing/gtest_extensions/ya.make
@@ -0,0 +1,26 @@
+LIBRARY()
+OWNER(
+ amatanhead
+ bulatman
+ dancingqueue
+ prettyboy
+ thegeorg
+ g:cpp-contrib
+)
+
+PEERDIR(
+ contrib/restricted/googletest/googlemock
+ contrib/restricted/googletest/googletest
+)
+
+SRCS(
+ assertions.cpp
+ gtest_extensions.cpp
+ matchers.cpp
+ pretty_printers.cpp
+ probe.cpp
+)
+
+END()
+
+RECURSE_FOR_TESTS(ut)
diff --git a/library/cpp/testing/hook/README.md b/library/cpp/testing/hook/README.md
new file mode 100644
index 0000000000..fac8b32ef5
--- /dev/null
+++ b/library/cpp/testing/hook/README.md
@@ -0,0 +1,25 @@
+# Hook for google benchmark and gtest
+
+Y_TEST_HOOK_BEFORE_INIT - вызывается перед инициализацией соотвествующего фреймворка
+Y_TEST_HOOK_BEFORE_RUN - вызывается перед запуском тестов
+Y_TEST_HOOK_AFTER_RUN - вызывается всегда после завершения выполнения тестов,
+ если этап инициализации был успешным
+
+## Примеры:
+
+```
+Y_TEST_HOOK_BEFORE_INIT(SetupMyApp) {
+ // ваш код для выполнения перед инициализацией фреймворка
+}
+
+Y_TEST_HOOK_BEFORE_RUN(InitMyApp) {
+ // ваш код для выполнения перед запуском тестов
+}
+
+Y_TEST_HOOK_AFTER_RUN(CleanMyApp) {
+ // ваш код для выполнения после завершения тестов
+}
+```
+
+## Тесты:
+тесты лаунчерах соотвествующих фреймворков (gtest, gbenchmark и unittest)
diff --git a/library/cpp/testing/hook/hook.cpp b/library/cpp/testing/hook/hook.cpp
new file mode 100644
index 0000000000..b3c599da89
--- /dev/null
+++ b/library/cpp/testing/hook/hook.cpp
@@ -0,0 +1,45 @@
+#include "hook.h"
+
+namespace {
+ NTesting::THook* BeforeInitHead = nullptr;
+ NTesting::THook* BeforeRunHead = nullptr;
+ NTesting::THook* AfterRunHead = nullptr;
+
+ void RegisterHook(NTesting::THook*& head, NTesting::THook* hook) {
+ hook->Next = head;
+ head = hook;
+ }
+
+ void CallHooks(NTesting::THook* head) {
+ while (nullptr != head) {
+ if (nullptr != head->Fn) {
+ (*head->Fn)();
+ }
+ head = head->Next;
+ }
+ }
+}
+
+void NTesting::THook::RegisterBeforeInit(NTesting::THook* hook) noexcept {
+ RegisterHook(BeforeInitHead, hook);
+}
+
+void NTesting::THook::CallBeforeInit() {
+ CallHooks(BeforeInitHead);
+}
+
+void NTesting::THook::RegisterBeforeRun(NTesting::THook* hook) noexcept {
+ RegisterHook(BeforeRunHead, hook);
+}
+
+void NTesting::THook::CallBeforeRun() {
+ CallHooks(BeforeRunHead);
+}
+
+void NTesting::THook::RegisterAfterRun(NTesting::THook* hook) noexcept {
+ RegisterHook(AfterRunHead, hook);
+}
+
+void NTesting::THook::CallAfterRun() {
+ CallHooks(AfterRunHead);
+}
diff --git a/library/cpp/testing/hook/hook.h b/library/cpp/testing/hook/hook.h
new file mode 100644
index 0000000000..c45289cb22
--- /dev/null
+++ b/library/cpp/testing/hook/hook.h
@@ -0,0 +1,128 @@
+#pragma once
+
+namespace NTesting {
+ /**
+ * Hook class and registration system.
+ *
+ * Default implementation of the `main` function for G_BENCHMARK and GTEST calls these hooks when executing.
+ * This is a useful feature if you want to customize behaviour of the `main` function,
+ * but you don't want to write `main` yourself.
+ *
+ * Hooks form an intrusive linked list that's built at application startup. Note that hooks execute
+ * in arbitrary order.
+ *
+ * Use macros below to define hooks.
+ */
+ struct THook {
+ using TFn = void (*)();
+
+ TFn Fn = nullptr;
+ THook* Next = nullptr;
+
+ static void RegisterBeforeInit(THook* hook) noexcept;
+
+ static void CallBeforeInit();
+
+ struct TRegisterBeforeInit {
+ explicit TRegisterBeforeInit(THook* hook) noexcept {
+ THook::RegisterBeforeInit(hook);
+ }
+ };
+
+ static void RegisterBeforeRun(THook* hook) noexcept;
+
+ static void CallBeforeRun();
+
+ struct TRegisterBeforeRun {
+ explicit TRegisterBeforeRun(THook* hook) noexcept {
+ THook::RegisterBeforeRun(hook);
+ }
+ };
+
+ static void RegisterAfterRun(THook* hook) noexcept;
+
+ static void CallAfterRun();
+
+ struct TRegisterAfterRun {
+ explicit TRegisterAfterRun(THook* hook) noexcept {
+ THook::RegisterAfterRun(hook);
+ }
+ };
+ };
+
+ /**
+ * Called right before initializing test programm
+ *
+ * This hook is intended for setting up default parameters. If you're doing initialization, consider
+ * using `Y_TEST_HOOK_BEFORE_RUN` instead.
+ *
+ * *Note:* hooks execute in arbitrary order.
+ *
+ *
+ * # Usage
+ *
+ * Instantiate this class in a cpp file. Pass a unique name for your hook,
+ * implement body right after macro instantiation:
+ *
+ * ```
+ * Y_TEST_HOOK_BEFORE_INIT(SetupParams) {
+ * // hook code
+ * }
+ * ```
+ */
+#define Y_TEST_HOOK_BEFORE_INIT(N) \
+ void N(); \
+ ::NTesting::THook N##Hook{&N, nullptr}; \
+ ::NTesting::THook::TRegisterBeforeInit N##HookReg{&N##Hook}; \
+ void N()
+
+ /**
+ * Called right before launching tests.
+ *
+ * Hooks execute in arbitrary order. As such, we recommend using this hook to set up an event listener,
+ * and performing initialization and cleanup in the corresponding event handlers. This is better than performing
+ * initialization and cleanup directly in the hook's code because it gives more control over
+ * order in which initialization is performed.
+ *
+ *
+ * # Usage
+ *
+ * Instantiate this class in a cpp file. Pass a unique name for your hook,
+ * implement body right after macro instantiation:
+ *
+ * ```
+ * Y_TEST_HOOK_BEFORE_RUN(InitMyApp) {
+ * // hook code
+ * }
+ * ```
+ */
+#define Y_TEST_HOOK_BEFORE_RUN(N) \
+ void N(); \
+ ::NTesting::THook N##Hook{&N, nullptr}; \
+ ::NTesting::THook::TRegisterBeforeRun N##HookReg{&N##Hook}; \
+ void N()
+
+ /**
+ * Called after all tests has finished, just before program exit.
+ *
+ * This hook is intended for simple cleanup routines that don't care about order in which hooks are executed.
+ * For more complex cases, we recommend using `Y_TEST_HOOK_BEFORE_RUN`.
+ *
+ *
+ * # Usage
+ *
+ * Instantiate this class in a cpp file. Pass a unique name for your hook,
+ * implement body right after macro instantiation:
+ *
+ * ```
+ * Y_TEST_HOOK_AFTER_RUN(StopMyApp) {
+ * // hook code
+ * }
+ * ```
+ */
+#define Y_TEST_HOOK_AFTER_RUN(N) \
+ void N(); \
+ ::NTesting::THook N##Hook{&N, nullptr}; \
+ ::NTesting::THook::TRegisterAfterRun N##HookReg{&N##Hook}; \
+ void N()
+}
diff --git a/library/cpp/testing/hook/ya.make b/library/cpp/testing/hook/ya.make
new file mode 100644
index 0000000000..db58f4e0ae
--- /dev/null
+++ b/library/cpp/testing/hook/ya.make
@@ -0,0 +1,13 @@
+LIBRARY()
+OWNER(
+ amatanhead
+ bulatman
+ thegeorg
+ g:cpp-contrib
+)
+
+SRCS(
+ hook.cpp
+)
+
+END()
diff --git a/library/cpp/testing/unittest/checks.cpp b/library/cpp/testing/unittest/checks.cpp
new file mode 100644
index 0000000000..c5712ae9d2
--- /dev/null
+++ b/library/cpp/testing/unittest/checks.cpp
@@ -0,0 +1,31 @@
+#include <util/generic/string.h>
+#include <util/string/type.h>
+
+bool CheckExceptionMessage(const char* msg, TString& err) {
+ static const char* badMsg[] = {
+ // Операция успешно завершена [cp1251]
+ "\xce\xef\xe5\xf0\xe0\xf6\xe8\xff\x20\xf3\xf1\xef\xe5\xf8\xed\xee\x20\xe7\xe0\xe2\xe5\xf0\xf8\xe5\xed\xe0",
+ "The operation completed successfully",
+ "No error"};
+
+ err.clear();
+
+ if (msg == nullptr) {
+ err = "Error message is null";
+ return false;
+ }
+
+ if (IsSpace(msg)) {
+ err = "Error message is empty";
+ return false;
+ }
+
+ for (auto& i : badMsg) {
+ if (strstr(msg, i) != nullptr) {
+ err = "Invalid error message: " + TString(msg);
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/library/cpp/testing/unittest/env.h b/library/cpp/testing/unittest/env.h
new file mode 100644
index 0000000000..4807539ab2
--- /dev/null
+++ b/library/cpp/testing/unittest/env.h
@@ -0,0 +1,3 @@
+// just shortcut
+#include <library/cpp/testing/common/env.h>
+
diff --git a/library/cpp/testing/unittest/example_ut.cpp b/library/cpp/testing/unittest/example_ut.cpp
new file mode 100644
index 0000000000..bcc1ce33f0
--- /dev/null
+++ b/library/cpp/testing/unittest/example_ut.cpp
@@ -0,0 +1,12 @@
+#include <library/cpp/testing/unittest/registar.h>
+
+/*
+ * just copy-paste it for good start point
+ */
+
+Y_UNIT_TEST_SUITE(TUnitTest) {
+ Y_UNIT_TEST(TestEqual) {
+ UNIT_ASSERT_EQUAL(0, 0);
+ UNIT_ASSERT_EQUAL(1, 1);
+ }
+}
diff --git a/library/cpp/testing/unittest/fat/test_port_manager.cpp b/library/cpp/testing/unittest/fat/test_port_manager.cpp
new file mode 100644
index 0000000000..f77d2e3a25
--- /dev/null
+++ b/library/cpp/testing/unittest/fat/test_port_manager.cpp
@@ -0,0 +1,36 @@
+#include <library/cpp/testing/unittest/registar.h>
+#include <library/cpp/testing/unittest/tests_data.h>
+
+bool IsFreePort(ui16 port) {
+ TInet6StreamSocket sock;
+ TSockAddrInet6 addr("::", port);
+ Y_ENSURE(SetSockOpt(sock, SOL_SOCKET, SO_REUSEADDR, 1) == 0);
+ SetReuseAddressAndPort(sock);
+ if (sock.Bind(&addr) == 0) {
+ return true;
+ }
+ return false;
+}
+
+void get_port_ranges() {
+ for (int i = 1; i < 10; ++i) {
+ TPortManager pm;
+ ui16 port = pm.GetPortsRange(1024, i);
+ for (int p = port; p < port + i; ++p) {
+ UNIT_ASSERT(IsFreePort(p));
+ }
+ }
+}
+
+Y_UNIT_TEST_SUITE(TestTPortManager) {
+ Y_UNIT_TEST(ParallelRun0) {get_port_ranges();}
+ Y_UNIT_TEST(ParallelRun1) {get_port_ranges();}
+ Y_UNIT_TEST(ParallelRun2) {get_port_ranges();}
+ Y_UNIT_TEST(ParallelRun3) {get_port_ranges();}
+ Y_UNIT_TEST(ParallelRun4) {get_port_ranges();}
+ Y_UNIT_TEST(ParallelRun5) {get_port_ranges();}
+ Y_UNIT_TEST(ParallelRun6) {get_port_ranges();}
+ Y_UNIT_TEST(ParallelRun7) {get_port_ranges();}
+ Y_UNIT_TEST(ParallelRun8) {get_port_ranges();}
+ Y_UNIT_TEST(ParallelRun9) {get_port_ranges();}
+}
diff --git a/library/cpp/testing/unittest/fat/ya.make b/library/cpp/testing/unittest/fat/ya.make
new file mode 100644
index 0000000000..d405e599ee
--- /dev/null
+++ b/library/cpp/testing/unittest/fat/ya.make
@@ -0,0 +1,19 @@
+UNITTEST()
+
+OWNER(g:yatool)
+
+SRCS(
+ test_port_manager.cpp
+)
+
+SIZE(LARGE)
+
+# We need to run tests at the same time on the single machine
+FORK_SUBTESTS()
+
+TAG(
+ ya:fat
+ ya:force_sandbox
+)
+
+END()
diff --git a/library/cpp/testing/unittest/gtest.cpp b/library/cpp/testing/unittest/gtest.cpp
new file mode 100644
index 0000000000..ebad1ea4d6
--- /dev/null
+++ b/library/cpp/testing/unittest/gtest.cpp
@@ -0,0 +1,67 @@
+#include "gtest.h"
+#include "simple.h"
+
+#include <util/generic/map.h>
+#include <util/generic/vector.h>
+#include <util/system/type_name.h>
+
+using namespace NUnitTest;
+using namespace NUnitTest::NPrivate;
+
+IGTestFactory::~IGTestFactory() {
+}
+
+namespace {
+ struct TCurrentTest: public TSimpleTestExecutor {
+ inline TCurrentTest(TStringBuf name)
+ : MyName(name)
+ {
+ }
+
+ TString TypeId() const override {
+ return TypeName(*this) + "-" + MyName;
+ }
+
+ TString Name() const noexcept override {
+ return TString(MyName);
+ }
+
+ const TStringBuf MyName;
+ };
+
+ struct TGTestFactory: public IGTestFactory {
+ inline TGTestFactory(TStringBuf name)
+ : Test(name)
+ {
+ }
+
+ ~TGTestFactory() override {
+ }
+
+ TString Name() const noexcept override {
+ return Test.Name();
+ }
+
+ TTestBase* ConstructTest() override {
+ return new TCurrentTest(Test);
+ }
+
+ void AddTest(const char* name, void (*body)(TTestContext&), bool forceFork) override {
+ Test.Tests.push_back(TBaseTestCase(name, body, forceFork));
+ }
+
+ TCurrentTest Test;
+ };
+}
+
+IGTestFactory* NUnitTest::NPrivate::ByName(const char* name) {
+ static TMap<TStringBuf, TAutoPtr<TGTestFactory>> tests;
+
+ auto& ret = tests[name];
+
+ if (!ret) {
+ ret = new TGTestFactory(name);
+ }
+
+ return ret.Get();
+}
diff --git a/library/cpp/testing/unittest/gtest.h b/library/cpp/testing/unittest/gtest.h
new file mode 100644
index 0000000000..b6768b1bf0
--- /dev/null
+++ b/library/cpp/testing/unittest/gtest.h
@@ -0,0 +1,108 @@
+#pragma once
+
+// WARNING: this is a legacy header that tries to mimic the gtest interface while using unittest
+// under the hood. Avoid using this interface -- use the genuine gtest instead (the GTEST macro).
+// If you're already using GTEST macro and you've found yourself here, you probably meant
+// to include `library/cpp/testing/gtest/gtest.h`.
+
+#include "registar.h"
+
+#include <util/generic/ymath.h>
+#include <util/generic/ylimits.h>
+
+namespace NUnitTest {
+ namespace NPrivate {
+ struct IGTestFactory: public ITestBaseFactory {
+ ~IGTestFactory() override;
+
+ virtual void AddTest(const char* name, void (*body)(TTestContext&), bool forceFork) = 0;
+ };
+
+ IGTestFactory* ByName(const char* name);
+ }
+}
+
+namespace NTesting {
+ struct TTest {
+ virtual void SetUp() {
+ }
+
+ virtual void TearDown() {
+ }
+
+ inline TTest* _This() noexcept {
+ return this;
+ }
+ };
+}
+
+namespace testing {
+ struct Test: public ::NTesting::TTest {
+ };
+}
+
+#define TEST_IMPL(N, NN, FF) \
+ void Test##N##NN(NUnitTest::TTestContext&); \
+ namespace NTestSuite##N##NN { \
+ struct TReg { \
+ inline TReg() { \
+ ::NUnitTest::NPrivate::ByName(#N)->AddTest(#NN, &(Test##N##NN), FF); \
+ } \
+ }; \
+ static TReg reg; \
+ } \
+ void Test##N##NN(NUnitTest::TTestContext&)
+
+#define TEST_F_IMPL(N, NN, FF) \
+ namespace NTestSuite##N##NN { \
+ struct TTestSuite: public N { \
+ inline TTestSuite() { \
+ this->_This()->SetUp(); \
+ } \
+ inline ~TTestSuite() { \
+ this->_This()->TearDown(); \
+ } \
+ void NN(); \
+ }; \
+ }; \
+ TEST_IMPL(N, NN, FF) { \
+ NTestSuite##N##NN::TTestSuite().NN(); \
+ } \
+ void NTestSuite##N##NN::TTestSuite::NN()
+
+#define TEST(A, B) TEST_IMPL(A, B, false)
+#define TEST_FORKED(A, B) TEST_IMPL(A, B, true)
+
+#define TEST_F(A, B) TEST_F_IMPL(A, B, false)
+#define TEST_F_FORKED(A, B) TEST_F_IMPL(A, B, true)
+
+#define EXPECT_EQ(A, B) UNIT_ASSERT_VALUES_EQUAL(A, B)
+#define EXPECT_NE(A, B) UNIT_ASSERT_UNEQUAL(A, B)
+#define EXPECT_LE(A, B) UNIT_ASSERT((A) <= (B))
+#define EXPECT_LT(A, B) UNIT_ASSERT((A) < (B))
+#define EXPECT_GE(A, B) UNIT_ASSERT((A) >= (B))
+#define EXPECT_GT(A, B) UNIT_ASSERT((A) > (B))
+#define EXPECT_NO_THROW(A) UNIT_ASSERT_NO_EXCEPTION(A)
+#define EXPECT_THROW(A, B) UNIT_ASSERT_EXCEPTION(A, B)
+#define EXPECT_NEAR(A, B, D) UNIT_ASSERT_DOUBLES_EQUAL(A, B, D)
+#define EXPECT_STREQ(A, B) UNIT_ASSERT_VALUES_EQUAL(A, B)
+
+#define EXPECT_DOUBLE_EQ_TOLERANCE(A, B, tolerance) UNIT_ASSERT_C(fabs((A) - (B)) < tolerance * std::numeric_limits<decltype(A)>::epsilon(), TString("\n") + ToString(A) + " <> " + ToString(B))
+#define EXPECT_DOUBLE_EQ(A, B) EXPECT_DOUBLE_EQ_TOLERANCE(A, B, 4.0)
+
+//conflicts with util/system/defaults.h
+#undef EXPECT_TRUE
+#define EXPECT_TRUE(X) UNIT_ASSERT(X)
+#undef EXPECT_FALSE
+#define EXPECT_FALSE(X) UNIT_ASSERT(!(X))
+
+#define ASSERT_EQ(A, B) EXPECT_EQ(A, B)
+#define ASSERT_NE(A, B) EXPECT_NE(A, B)
+#define ASSERT_GT(A, B) EXPECT_GT(A, B)
+#define ASSERT_LT(A, B) EXPECT_LT(A, B)
+#define ASSERT_FALSE(X) EXPECT_FALSE(X)
+#define ASSERT_TRUE(X) EXPECT_TRUE(X)
+#define ASSERT_THROW(A, B) EXPECT_THROW(A, B)
+#define ASSERT_NO_THROW(A) EXPECT_NO_THROW(A)
+#define ASSERT_DOUBLE_EQ(A, B) EXPECT_DOUBLE_EQ(A, B)
+#define ASSERT_STREQ(A, B) EXPECT_STREQ(A, B)
diff --git a/library/cpp/testing/unittest/plugin.cpp b/library/cpp/testing/unittest/plugin.cpp
new file mode 100644
index 0000000000..543112f7ac
--- /dev/null
+++ b/library/cpp/testing/unittest/plugin.cpp
@@ -0,0 +1,50 @@
+#include "plugin.h"
+
+#include <util/generic/singleton.h>
+#include <util/generic/vector.h>
+#include <util/generic/utility.h>
+
+namespace NUnitTest {
+ namespace NPlugin {
+ namespace {
+ class TPlugins {
+ public:
+ void OnStartMain(int argc, char* argv[]) const {
+ for (const auto& plugin : Plugins) {
+ plugin->OnStartMain(argc, argv);
+ }
+ }
+
+ void OnStopMain(int argc, char* argv[]) const {
+ for (const auto& plugin : Plugins) {
+ plugin->OnStopMain(argc, argv);
+ }
+ }
+
+ void Register(TSimpleSharedPtr<IPlugin> plugin) {
+ Plugins.emplace_back(std::move(plugin));
+ }
+
+ static TPlugins& Instance() {
+ return *Singleton<TPlugins>();
+ }
+
+ private:
+ TVector<TSimpleSharedPtr<IPlugin>> Plugins;
+ };
+ } // anonymous namespace
+
+ TPluginRegistrator::TPluginRegistrator(TSimpleSharedPtr<IPlugin> plugin) {
+ TPlugins::Instance().Register(std::move(plugin));
+ }
+
+ void OnStartMain(int argc, char* argv[]) {
+ TPlugins::Instance().OnStartMain(argc, argv);
+ }
+
+ void OnStopMain(int argc, char* argv[]) {
+ TPlugins::Instance().OnStopMain(argc, argv);
+ }
+
+ }
+}
diff --git a/library/cpp/testing/unittest/plugin.h b/library/cpp/testing/unittest/plugin.h
new file mode 100644
index 0000000000..102f2c1469
--- /dev/null
+++ b/library/cpp/testing/unittest/plugin.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <util/generic/ptr.h>
+
+namespace NUnitTest {
+ // Plugins are deprecated, please use Y_TEST_HOOK_* from library/cpp/hook/hook.h
+ namespace NPlugin {
+ class IPlugin {
+ public:
+ virtual ~IPlugin() {
+ }
+
+ virtual void OnStartMain(int /*argc*/, char* /*argv*/ []) {
+ }
+
+ virtual void OnStopMain(int /*argc*/, char* /*argv*/ []) {
+ }
+ };
+
+ void OnStartMain(int argc, char* argv[]);
+ void OnStopMain(int argc, char* argv[]);
+
+ class TPluginRegistrator {
+ public:
+ TPluginRegistrator(TSimpleSharedPtr<IPlugin> plugin);
+ };
+
+ }
+}
diff --git a/library/cpp/testing/unittest/registar.cpp b/library/cpp/testing/unittest/registar.cpp
new file mode 100644
index 0000000000..3679b768ed
--- /dev/null
+++ b/library/cpp/testing/unittest/registar.cpp
@@ -0,0 +1,513 @@
+#include "registar.h"
+
+#include <library/cpp/diff/diff.h>
+#include <library/cpp/colorizer/colors.h>
+
+#include <util/generic/bt_exception.h>
+#include <util/random/fast.h>
+#include <util/string/printf.h>
+#include <util/system/backtrace.h>
+#include <util/system/guard.h>
+#include <util/system/tls.h>
+#include <util/system/error.h>
+#include <util/string/cast.h>
+
+bool NUnitTest::ShouldColorizeDiff = true;
+bool NUnitTest::ContinueOnFail = false;
+
+TString NUnitTest::RandomString(size_t len, ui32 seed) {
+ TReallyFastRng32 rand(seed);
+ TString ret;
+
+ ret.reserve(len);
+
+ for (size_t i = 0; i < len; ++i) {
+ ret.push_back(char(rand.Uniform(1, 128)));
+ }
+
+ return ret;
+}
+
+Y_POD_STATIC_THREAD(bool)
+UnittestThread;
+Y_POD_STATIC_THREAD(NUnitTest::TTestBase*)
+currentTest;
+::NUnitTest::TRaiseErrorHandler RaiseErrorHandler;
+
+void ::NUnitTest::NPrivate::RaiseError(const char* what, const TString& msg, bool fatalFailure) {
+ Y_VERIFY(UnittestThread, "%s in non-unittest thread with message:\n%s", what, msg.data());
+ Y_VERIFY(GetCurrentTest());
+
+ if (RaiseErrorHandler) {
+ RaiseErrorHandler(what, msg, fatalFailure);
+ return;
+ }
+
+ // Default handler
+ TBackTrace bt;
+ bt.Capture();
+ GetCurrentTest()->AddError(msg.data(), bt.PrintToString());
+ if (::NUnitTest::ContinueOnFail || !fatalFailure) {
+ return;
+ }
+ throw TAssertException();
+}
+
+void ::NUnitTest::SetRaiseErrorHandler(::NUnitTest::TRaiseErrorHandler handler) {
+ Y_VERIFY(UnittestThread);
+ RaiseErrorHandler = std::move(handler);
+}
+
+void ::NUnitTest::NPrivate::SetUnittestThread(bool unittestThread) {
+ Y_VERIFY(UnittestThread != unittestThread, "state check");
+ UnittestThread = unittestThread;
+}
+
+void ::NUnitTest::NPrivate::SetCurrentTest(TTestBase* test) {
+ Y_VERIFY(!test || !currentTest, "state check");
+ currentTest = test;
+}
+
+NUnitTest::TTestBase* ::NUnitTest::NPrivate::GetCurrentTest() {
+ return currentTest;
+}
+
+struct TDiffColorizer {
+ NColorizer::TColors Colors;
+ bool Reverse = false;
+
+ explicit TDiffColorizer(bool reverse = false)
+ : Reverse(reverse)
+ {
+ }
+
+ TString Special(TStringBuf str) const {
+ return ToString(Colors.YellowColor()) + str;
+ }
+
+ TString Common(TArrayRef<const char> str) const {
+ return ToString(Colors.OldColor()) + TString(str.begin(), str.end());
+ }
+
+ TString Left(TArrayRef<const char> str) const {
+ return ToString(GetLeftColor()) + TString(str.begin(), str.end());
+ }
+
+ TString Right(TArrayRef<const char> str) const {
+ return ToString(GetRightColor()) + TString(str.begin(), str.end());
+ }
+
+ TStringBuf GetLeftColor() const {
+ return Reverse ? Colors.RedColor() : Colors.GreenColor();
+ }
+
+ TStringBuf GetRightColor() const {
+ return Reverse ? Colors.GreenColor() : Colors.RedColor();
+ }
+};
+
+struct TTraceDiffFormatter {
+ bool Reverse = false;
+
+ explicit TTraceDiffFormatter(bool reverse = false)
+ : Reverse(reverse)
+ {
+ }
+
+ TString Special(TStringBuf str) const {
+ return ToString(str);
+ }
+
+ TString Common(TArrayRef<const char> str) const {
+ return TString(str.begin(), str.end());
+ }
+
+ TString Left(TArrayRef<const char> str) const {
+ return NUnitTest::GetFormatTag("good") +
+ TString(str.begin(), str.end()) +
+ NUnitTest::GetResetTag();
+ }
+
+ TString Right(TArrayRef<const char> str) const {
+ return NUnitTest::GetFormatTag("bad") +
+ TString(str.begin(), str.end()) +
+ NUnitTest::GetResetTag();
+ }
+};
+
+TString NUnitTest::GetFormatTag(const char* name) {
+ return Sprintf("[[%s]]", name);
+}
+
+TString NUnitTest::GetResetTag() {
+ return TString("[[rst]]");
+}
+
+TString NUnitTest::ColoredDiff(TStringBuf s1, TStringBuf s2, const TString& delims, bool reverse) {
+ TStringStream res;
+ TVector<NDiff::TChunk<char>> chunks;
+ NDiff::InlineDiff(chunks, s1, s2, delims);
+ if (NUnitTest::ShouldColorizeDiff) {
+ NDiff::PrintChunks(res, TDiffColorizer(reverse), chunks);
+ } else {
+ res << NUnitTest::GetResetTag();
+ NDiff::PrintChunks(res, TTraceDiffFormatter(reverse), chunks);
+ }
+ return res.Str();
+}
+
+static TString MakeTestName(const NUnitTest::ITestSuiteProcessor::TTest& test) {
+ return TStringBuilder() << test.unit->name << "::" << test.name;
+}
+
+static size_t CountTests(const TMap<TString, size_t>& testErrors, bool succeeded) {
+ size_t cnt = 0;
+ for (const auto& t : testErrors) {
+ if (succeeded && t.second == 0) {
+ ++cnt;
+ } else if (!succeeded && t.second > 0) {
+ ++cnt;
+ }
+ }
+ return cnt;
+}
+
+NUnitTest::ITestSuiteProcessor::ITestSuiteProcessor() = default;
+
+NUnitTest::ITestSuiteProcessor::~ITestSuiteProcessor() = default;
+
+void NUnitTest::ITestSuiteProcessor::Start() {
+ OnStart();
+}
+
+void NUnitTest::ITestSuiteProcessor::End() {
+ OnEnd();
+}
+
+void NUnitTest::ITestSuiteProcessor::UnitStart(const TUnit& unit) {
+ CurTestErrors_.clear();
+
+ OnUnitStart(&unit);
+}
+
+void NUnitTest::ITestSuiteProcessor::UnitStop(const TUnit& unit) {
+ OnUnitStop(&unit);
+}
+
+void NUnitTest::ITestSuiteProcessor::Error(const TError& descr) {
+ AddTestError(*descr.test);
+
+ OnError(&descr);
+}
+
+void NUnitTest::ITestSuiteProcessor::BeforeTest(const TTest& test) {
+ OnBeforeTest(&test);
+}
+
+void NUnitTest::ITestSuiteProcessor::Finish(const TFinish& descr) {
+ AddTestFinish(*descr.test);
+
+ OnFinish(&descr);
+}
+
+unsigned NUnitTest::ITestSuiteProcessor::GoodTests() const noexcept {
+ return CountTests(TestErrors_, true);
+}
+
+unsigned NUnitTest::ITestSuiteProcessor::FailTests() const noexcept {
+ return CountTests(TestErrors_, false);
+}
+
+unsigned NUnitTest::ITestSuiteProcessor::GoodTestsInCurrentUnit() const noexcept {
+ return CountTests(CurTestErrors_, true);
+}
+
+unsigned NUnitTest::ITestSuiteProcessor::FailTestsInCurrentUnit() const noexcept {
+ return CountTests(CurTestErrors_, false);
+}
+
+bool NUnitTest::ITestSuiteProcessor::CheckAccess(TString /*name*/, size_t /*num*/) {
+ return true;
+}
+
+bool NUnitTest::ITestSuiteProcessor::CheckAccessTest(TString /*suite*/, const char* /*name*/) {
+ return true;
+}
+
+void NUnitTest::ITestSuiteProcessor::Run(std::function<void()> f, const TString& /*suite*/, const char* /*name*/, const bool /*forceFork*/) {
+ f();
+}
+
+bool NUnitTest::ITestSuiteProcessor::GetIsForked() const {
+ return false;
+}
+
+bool NUnitTest::ITestSuiteProcessor::GetForkTests() const {
+ return false;
+}
+
+void NUnitTest::ITestSuiteProcessor::OnStart() {
+}
+
+void NUnitTest::ITestSuiteProcessor::OnEnd() {
+}
+
+void NUnitTest::ITestSuiteProcessor::OnUnitStart(const TUnit* /*unit*/) {
+}
+
+void NUnitTest::ITestSuiteProcessor::OnUnitStop(const TUnit* /*unit*/) {
+}
+
+void NUnitTest::ITestSuiteProcessor::OnError(const TError* /*error*/) {
+}
+
+void NUnitTest::ITestSuiteProcessor::OnFinish(const TFinish* /*finish*/) {
+}
+
+void NUnitTest::ITestSuiteProcessor::OnBeforeTest(const TTest* /*test*/) {
+}
+
+void NUnitTest::ITestSuiteProcessor::AddTestError(const TTest& test) {
+ const TString name = MakeTestName(test);
+ ++TestErrors_[name];
+ ++CurTestErrors_[name];
+}
+
+void NUnitTest::ITestSuiteProcessor::AddTestFinish(const TTest& test) {
+ const TString name = MakeTestName(test);
+ TestErrors_[name]; // zero errors if not touched
+ CurTestErrors_[name]; // zero errors if not touched
+}
+
+NUnitTest::ITestBaseFactory::ITestBaseFactory() {
+ Register();
+}
+
+NUnitTest::ITestBaseFactory::~ITestBaseFactory() = default;
+
+void NUnitTest::ITestBaseFactory::Register() noexcept {
+ TTestFactory::Instance().Register(this);
+}
+
+NUnitTest::TTestBase::TTestBase() noexcept
+ : Parent_(nullptr)
+ , TestErrors_()
+ , CurrentSubtest_()
+{
+}
+
+NUnitTest::TTestBase::~TTestBase() = default;
+
+TString NUnitTest::TTestBase::TypeId() const {
+ return TypeName(*this);
+}
+
+void NUnitTest::TTestBase::SetUp() {
+}
+
+void NUnitTest::TTestBase::TearDown() {
+}
+
+void NUnitTest::TTestBase::AddError(const char* msg, const TString& backtrace, TTestContext* context) {
+ ++TestErrors_;
+ const NUnitTest::ITestSuiteProcessor::TUnit unit = {Name()};
+ const NUnitTest::ITestSuiteProcessor::TTest test = {&unit, CurrentSubtest_};
+ const NUnitTest::ITestSuiteProcessor::TError err = {&test, msg, backtrace, context};
+
+ Processor()->Error(err);
+}
+
+void NUnitTest::TTestBase::AddError(const char* msg, TTestContext* context) {
+ AddError(msg, TString(), context);
+}
+
+void NUnitTest::TTestBase::RunAfterTest(std::function<void()> f) {
+ with_lock (AfterTestFunctionsLock_) {
+ AfterTestFunctions_.emplace_back(std::move(f));
+ }
+}
+
+bool NUnitTest::TTestBase::CheckAccessTest(const char* test) {
+ return Processor()->CheckAccessTest(Name(), test);
+}
+
+void NUnitTest::TTestBase::BeforeTest(const char* func) {
+ const NUnitTest::ITestSuiteProcessor::TUnit unit = {Name()};
+ const NUnitTest::ITestSuiteProcessor::TTest test = {&unit, func};
+ rusage.Fill();
+ Processor()->BeforeTest(test);
+}
+
+void NUnitTest::TTestBase::Finish(const char* func, TTestContext* context) {
+ TRusage finishRusage = TRusage::Get();
+ context->Metrics["ru_rss"] = finishRusage.MaxRss - rusage.MaxRss;
+ context->Metrics["ru_major_pagefaults"] = finishRusage.MajorPageFaults - rusage.MajorPageFaults;
+ context->Metrics["ru_utime"] = (finishRusage.Utime - rusage.Utime).MicroSeconds();
+ context->Metrics["ru_stime"] = (finishRusage.Stime - rusage.Stime).MicroSeconds();
+
+ const NUnitTest::ITestSuiteProcessor::TUnit unit = {Name()};
+ const NUnitTest::ITestSuiteProcessor::TTest test = {&unit, func};
+ const NUnitTest::ITestSuiteProcessor::TFinish finish = {&test, context, TestErrors_ == 0};
+
+ Processor()->Finish(finish);
+}
+
+void NUnitTest::TTestBase::AtStart() {
+ const NUnitTest::ITestSuiteProcessor::TUnit unit = {Name()};
+
+ Processor()->UnitStart(unit);
+}
+
+void NUnitTest::TTestBase::AtEnd() {
+ const NUnitTest::ITestSuiteProcessor::TUnit unit = {Name()};
+
+ Processor()->UnitStop(unit);
+}
+
+void NUnitTest::TTestBase::Run(std::function<void()> f, const TString& suite, const char* name, const bool forceFork) {
+ TestErrors_ = 0;
+ CurrentSubtest_ = name;
+ Processor()->Run(f, suite, name, forceFork);
+}
+
+void NUnitTest::TTestBase::BeforeTest() {
+ SetUp();
+}
+
+void NUnitTest::TTestBase::AfterTest() {
+ TearDown();
+
+ TVector<std::function<void()>> afterTestFunctions;
+ with_lock (AfterTestFunctionsLock_) {
+ afterTestFunctions.swap(AfterTestFunctions_);
+ }
+
+ for (auto i = afterTestFunctions.rbegin(); i != afterTestFunctions.rend(); ++i) {
+ std::function<void()>& f = *i;
+ if (f) {
+ f();
+ }
+ }
+}
+
+bool NUnitTest::TTestBase::GetIsForked() const {
+ return Processor()->GetIsForked();
+}
+
+bool NUnitTest::TTestBase::GetForkTests() const {
+ return Processor()->GetForkTests();
+}
+
+NUnitTest::ITestSuiteProcessor* NUnitTest::TTestBase::Processor() const noexcept {
+ return Parent_->Processor();
+}
+
+NUnitTest::TTestBase::TCleanUp::TCleanUp(TTestBase* base)
+ : Base_(base)
+{
+ ::NUnitTest::NPrivate::SetCurrentTest(base);
+ ::NUnitTest::NPrivate::SetUnittestThread(true);
+ Base_->BeforeTest();
+}
+
+NUnitTest::TTestBase::TCleanUp::~TCleanUp() {
+ try {
+ Base_->AfterTest();
+ } catch (...) {
+ Base_->AddError(CurrentExceptionMessage().data());
+ }
+ ::NUnitTest::NPrivate::SetUnittestThread(false);
+ ::NUnitTest::NPrivate::SetCurrentTest(nullptr);
+}
+
+namespace {
+ /*
+ * by default do nothing
+ */
+ class TCommonProcessor: public NUnitTest::ITestSuiteProcessor {
+ public:
+ TCommonProcessor() = default;
+
+ ~TCommonProcessor() override = default;
+ };
+
+ struct TCmp {
+ template <class T>
+ inline bool operator()(const T& l, const T& r) const noexcept {
+ return stricmp(Fix(l.Name().data()), Fix(r.Name().data())) < 0;
+ }
+
+ static inline const char* Fix(const char* n) noexcept {
+ if (*n == 'T') {
+ return n + 1;
+ }
+
+ return n;
+ }
+ };
+}
+
+NUnitTest::TTestFactory::TTestFactory(ITestSuiteProcessor* processor)
+ : Processor_(processor)
+{
+}
+
+NUnitTest::TTestFactory::~TTestFactory() = default;
+
+NUnitTest::TTestFactory& NUnitTest::TTestFactory::Instance() {
+ static TCommonProcessor p;
+ static TTestFactory f(&p);
+
+ return f;
+}
+
+unsigned NUnitTest::TTestFactory::Execute() {
+ Items_.QuickSort(TCmp());
+ Processor_->Start();
+
+ TSet<TString> types;
+ size_t cnt = 0;
+
+ for (TIntrusiveList<ITestBaseFactory>::TIterator factory = Items_.Begin(); factory != Items_.End(); ++factory) {
+ if (!Processor_->CheckAccess(factory->Name(), cnt++)) {
+ continue;
+ }
+
+ THolder<TTestBase> test(factory->ConstructTest());
+
+#ifdef _unix_ // on Windows RTTI causes memory leaks
+ TString type = test->TypeId();
+ if (types.insert(type).second == false) {
+ warnx("Duplicate suite found: %s (%s). Probably you have copy-pasted suite without changing it name", factory->Name().c_str(), type.c_str());
+ return 1;
+ }
+#endif // _unix_
+
+ test->Parent_ = this;
+
+#ifdef UT_SKIP_EXCEPTIONS
+ try {
+#endif
+ test->Execute();
+#ifdef UT_SKIP_EXCEPTIONS
+ } catch (...) {
+ }
+#endif
+ }
+
+ Processor_->End();
+
+ return bool(Processor_->FailTests());
+}
+
+void NUnitTest::TTestFactory::SetProcessor(ITestSuiteProcessor* processor) {
+ Processor_ = processor;
+}
+
+void NUnitTest::TTestFactory::Register(ITestBaseFactory* b) noexcept {
+ Items_.PushBack(b);
+}
+
+NUnitTest::ITestSuiteProcessor* NUnitTest::TTestFactory::Processor() const noexcept {
+ return Processor_;
+}
diff --git a/library/cpp/testing/unittest/registar.h b/library/cpp/testing/unittest/registar.h
new file mode 100644
index 0000000000..332485bdf3
--- /dev/null
+++ b/library/cpp/testing/unittest/registar.h
@@ -0,0 +1,1013 @@
+#pragma once
+
+#include <library/cpp/dbg_output/dump.h>
+
+#include <util/generic/bt_exception.h>
+#include <util/generic/hash.h>
+#include <util/generic/intrlist.h>
+#include <util/generic/map.h>
+#include <util/generic/ptr.h>
+#include <util/generic/set.h>
+#include <util/generic/typetraits.h>
+#include <util/generic/vector.h>
+#include <util/generic/yexception.h>
+
+#include <util/string/builder.h>
+#include <util/string/cast.h>
+#include <util/string/printf.h>
+
+#include <util/system/defaults.h>
+#include <util/system/type_name.h>
+#include <util/system/spinlock.h>
+#include <util/system/src_location.h>
+
+#include <util/system/rusage.h>
+
+#include <cmath>
+#include <cstdio>
+#include <functional>
+
+extern bool CheckExceptionMessage(const char*, TString&);
+
+namespace NUnitTest {
+ class TTestBase;
+
+ namespace NPrivate {
+ void RaiseError(const char* what, const TString& msg, bool fatalFailure);
+ void SetUnittestThread(bool);
+ void SetCurrentTest(TTestBase*);
+ TTestBase* GetCurrentTest();
+ }
+
+ extern bool ShouldColorizeDiff;
+ extern bool ContinueOnFail;
+ TString ColoredDiff(TStringBuf s1, TStringBuf s2, const TString& delims = TString(), bool reverse = false);
+ TString GetFormatTag(const char* name);
+ TString GetResetTag();
+
+ // Raise error handler
+ // Used for testing library/cpp/testing/unittest macroses
+ // and unittest helpers.
+ // For all other unittests standard handler is used
+ using TRaiseErrorHandler = std::function<void(const char*, const TString&, bool)>;
+
+ void SetRaiseErrorHandler(TRaiseErrorHandler handler);
+
+ inline void ClearRaiseErrorHandler() {
+ SetRaiseErrorHandler(TRaiseErrorHandler());
+ }
+
+ class TAssertException: public yexception {
+ };
+
+ class ITestSuiteProcessor;
+
+ struct TTestContext {
+ TTestContext()
+ : Processor(nullptr)
+ {
+ }
+
+ explicit TTestContext(ITestSuiteProcessor* processor)
+ : Processor(processor)
+ {
+ }
+
+ using TMetrics = THashMap<TString, double>;
+ TMetrics Metrics;
+
+ ITestSuiteProcessor* Processor;
+ };
+
+ class ITestSuiteProcessor {
+ public:
+ struct TUnit {
+ const TString name;
+ };
+
+ struct TTest {
+ const TUnit* unit;
+ const char* name;
+ };
+
+ struct TError {
+ const TTest* test;
+ const char* msg;
+ TString BackTrace;
+ TTestContext* Context;
+ };
+
+ struct TFinish {
+ const TTest* test;
+ TTestContext* Context;
+ bool Success;
+ };
+
+ ITestSuiteProcessor();
+
+ virtual ~ITestSuiteProcessor();
+
+ void Start();
+
+ void End();
+
+ void UnitStart(const TUnit& unit);
+
+ void UnitStop(const TUnit& unit);
+
+ void Error(const TError& descr);
+
+ void BeforeTest(const TTest& test);
+
+ void Finish(const TFinish& descr);
+
+ unsigned GoodTests() const noexcept;
+
+ unsigned FailTests() const noexcept;
+
+ unsigned GoodTestsInCurrentUnit() const noexcept;
+
+ unsigned FailTestsInCurrentUnit() const noexcept;
+
+ // Should execute test suite?
+ virtual bool CheckAccess(TString /*name*/, size_t /*num*/);
+
+ // Should execute a test whitin suite?
+ virtual bool CheckAccessTest(TString /*suite*/, const char* /*name*/);
+
+ virtual void Run(std::function<void()> f, const TString& /*suite*/, const char* /*name*/, bool /*forceFork*/);
+
+ // This process is forked for current test
+ virtual bool GetIsForked() const;
+
+ // --fork-tests is set (warning: this may be false, but never the less test will be forked if called inside UNIT_FORKED_TEST)
+ virtual bool GetForkTests() const;
+
+ private:
+ virtual void OnStart();
+
+ virtual void OnEnd();
+
+ virtual void OnUnitStart(const TUnit* /*unit*/);
+
+ virtual void OnUnitStop(const TUnit* /*unit*/);
+
+ virtual void OnError(const TError* /*error*/);
+
+ virtual void OnFinish(const TFinish* /*finish*/);
+
+ virtual void OnBeforeTest(const TTest* /*test*/);
+
+ void AddTestError(const TTest& test);
+
+ void AddTestFinish(const TTest& test);
+
+ private:
+ TMap<TString, size_t> TestErrors_;
+ TMap<TString, size_t> CurTestErrors_;
+ };
+
+ class TTestBase;
+ class TTestFactory;
+
+ class ITestBaseFactory: public TIntrusiveListItem<ITestBaseFactory> {
+ public:
+ ITestBaseFactory();
+
+ virtual ~ITestBaseFactory();
+
+ // name of test suite
+ virtual TString Name() const noexcept = 0;
+ virtual TTestBase* ConstructTest() = 0;
+
+ private:
+ void Register() noexcept;
+ };
+
+ class TTestBase {
+ friend class TTestFactory;
+ TRusage rusage;
+
+ public:
+ TTestBase() noexcept;
+
+ virtual ~TTestBase();
+
+ virtual TString TypeId() const;
+
+ virtual TString Name() const noexcept = 0;
+ virtual void Execute() = 0;
+
+ virtual void SetUp();
+
+ virtual void TearDown();
+
+ void AddError(const char* msg, const TString& backtrace = TString(), TTestContext* context = nullptr);
+
+ void AddError(const char* msg, TTestContext* context);
+
+ void RunAfterTest(std::function<void()> f); // function like atexit to run after current unit test
+
+ protected:
+ bool CheckAccessTest(const char* test);
+
+ void BeforeTest(const char* func);
+
+ void Finish(const char* func, TTestContext* context);
+
+ void AtStart();
+
+ void AtEnd();
+
+ void Run(std::function<void()> f, const TString& suite, const char* name, bool forceFork);
+
+ class TCleanUp {
+ public:
+ explicit TCleanUp(TTestBase* base);
+
+ ~TCleanUp();
+
+ private:
+ TTestBase* Base_;
+ };
+
+ void BeforeTest();
+
+ void AfterTest();
+
+ bool GetIsForked() const;
+
+ bool GetForkTests() const;
+
+ ITestSuiteProcessor* Processor() const noexcept;
+
+ private:
+ TTestFactory* Parent_;
+ size_t TestErrors_;
+ const char* CurrentSubtest_;
+ TAdaptiveLock AfterTestFunctionsLock_;
+ TVector<std::function<void()>> AfterTestFunctions_;
+ };
+
+#define UNIT_TEST_SUITE(N) \
+ typedef N TThisUnitTestSuite; \
+ \
+public: \
+ static TString StaticName() noexcept { \
+ return TString(#N); \
+ } \
+ \
+private: \
+ virtual TString Name() const noexcept override { \
+ return this->StaticName(); \
+ } \
+ \
+ virtual void Execute() override { \
+ this->AtStart();
+
+#define UNIT_TEST_SUITE_DEMANGLE(N) \
+ typedef N TThisUnitTestSuite; \
+ \
+public: \
+ static TString StaticName() noexcept { \
+ return TypeName<N>(); \
+ } \
+ \
+private: \
+ virtual TString Name() const noexcept override { \
+ return this->StaticName(); \
+ } \
+ \
+ virtual void Execute() override { \
+ this->AtStart();
+
+#ifndef UT_SKIP_EXCEPTIONS
+#define CATCH_REACTION(FN, e, context) this->AddError(("(" + TypeName(e) + ") " + e.what()).data(), context)
+#define CATCH_REACTION_BT(FN, e, context) this->AddError(("(" + TypeName(e) + ") " + e.what()).data(), (e.BackTrace() ? e.BackTrace()->PrintToString() : TString()), context)
+#else
+#define CATCH_REACTION(FN, e, context) throw
+#define CATCH_REACTION_BT(FN, e, context) throw
+#endif
+
+#define UNIT_TEST_CHECK_TEST_IS_DECLARED_ONLY_ONCE(F) \
+ /* If you see this message - delete multiple UNIT_TEST(TestName) with same TestName. */ \
+ /* It's forbidden to declare same test twice because it breaks --fork-tests logic. */ \
+ int You_have_declared_test_##F##_multiple_times_This_is_forbidden; \
+ Y_UNUSED(You_have_declared_test_##F##_multiple_times_This_is_forbidden);
+
+#define UNIT_TEST_RUN(F, FF, context) \
+ this->BeforeTest((#F)); \
+ { \
+ struct T##F##Caller { \
+ static void X(TThisUnitTestSuite* thiz, NUnitTest::TTestContext&) { \
+ TCleanUp cleaner(thiz); \
+ thiz->F(); \
+ } \
+ }; \
+ this->TTestBase::Run(std::bind(&T##F##Caller::X, this, context), StaticName(), (#F), FF); \
+ }
+
+#define UNIT_TEST_IMPL(F, FF) \
+ UNIT_TEST_CHECK_TEST_IS_DECLARED_ONLY_ONCE(F) { \
+ NUnitTest::TTestContext context(this->TTestBase::Processor()); \
+ if (this->CheckAccessTest((#F))) { \
+ try { \
+ UNIT_TEST_RUN(F, FF, context) \
+ } catch (const ::NUnitTest::TAssertException&) { \
+ } catch (const yexception& e) { \
+ CATCH_REACTION_BT((#F), e, &context); \
+ } catch (const std::exception& e) { \
+ CATCH_REACTION((#F), e, &context); \
+ } catch (...) { \
+ this->AddError("non-std exception!", &context); \
+ } \
+ this->Finish((#F), &context); \
+ } \
+ }
+
+#define UNIT_TEST(F) UNIT_TEST_IMPL(F, false)
+
+#define UNIT_FORKED_TEST(F) UNIT_TEST_IMPL(F, true)
+
+#define UNIT_TEST_EXCEPTION(F, E) \
+ /* main process with "--fork-tests" flag treats exceptions as errors - it's result of forked test run */ \
+ if (this->GetForkTests() && !this->GetIsForked()) { \
+ UNIT_TEST_IMPL(F, false); \
+ /* forked process (or main without "--fork-tests") treats some exceptions as success - it's exception test! */ \
+ } else { \
+ NUnitTest::TTestContext context(this->TTestBase::Processor()); \
+ if (this->CheckAccessTest((#F))) { \
+ try { \
+ UNIT_TEST_RUN(F, false, context) \
+ this->AddError("exception expected", &context); \
+ } catch (const ::NUnitTest::TAssertException&) { \
+ } catch (const E& e) { \
+ TString err; \
+ if (!CheckExceptionMessage(e.what(), err)) \
+ this->AddError(err.c_str(), &context); \
+ } catch (const std::exception& e) { \
+ this->AddError(e.what(), &context); \
+ } catch (...) { \
+ this->AddError("non-std exception!", &context); \
+ } \
+ this->Finish((#F), &context); \
+ } \
+ }
+
+#define UNIT_TEST_SUITE_END() \
+ this->AtEnd(); \
+ } \
+ \
+public: \
+ /*for ; after macros*/ void sub##F()
+
+#define UNIT_FAIL_IMPL(R, M) \
+ do { \
+ ::NUnitTest::NPrivate::RaiseError(R, ::TStringBuilder() << R << " at " << __LOCATION__ << ", " << __PRETTY_FUNCTION__ << ": " << M, true); \
+ } while (false)
+
+#define UNIT_FAIL_NONFATAL_IMPL(R, M) \
+ do { \
+ ::NUnitTest::NPrivate::RaiseError(R, ::TStringBuilder() << R << " at " << __LOCATION__ << ", " << __PRETTY_FUNCTION__ << ": " << M, false); \
+ } while (false)
+
+#define UNIT_FAIL(M) UNIT_FAIL_IMPL("forced failure", M)
+#define UNIT_FAIL_NONFATAL(M) UNIT_FAIL_NONFATAL_IMPL("forced failure", M)
+
+//types
+#define UNIT_ASSERT_TYPES_EQUAL(A, B) \
+ do { \
+ if (!std::is_same<A, B>::value) { \
+ UNIT_FAIL_IMPL("types equal assertion failed", (::TStringBuilder() << #A << " (" << TypeName<A>() << ") != " << #B << " (" << TypeName<B>() << ")").data()); \
+ } \
+ } while (false)
+
+//doubles
+// UNIT_ASSERT_DOUBLES_EQUAL_DEPRECATED* macros do not handle NaNs correctly (see IGNIETFERRO-1419) and are for backward compatibility
+// only. Consider switching to regular UNIT_ASSERT_DOUBLES_EQUAL* macros if you're still using the deprecated version.
+#define UNIT_ASSERT_DOUBLES_EQUAL_DEPRECATED_C(E, A, D, C) \
+ do { \
+ if (std::abs((E) - (A)) > (D)) { \
+ const auto _es = ToString((long double)(E)); \
+ const auto _as = ToString((long double)(A)); \
+ const auto _ds = ToString((long double)(D)); \
+ auto&& failMsg = Sprintf("std::abs(%s - %s) > %s %s", _es.data(), _as.data(), _ds.data(), (::TStringBuilder() << C).data()); \
+ UNIT_FAIL_IMPL("assertion failure", failMsg); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_DOUBLES_EQUAL_DEPRECATED(E, A, D) UNIT_ASSERT_DOUBLES_EQUAL_DEPRECATED_C(E, A, D, "")
+
+#define UNIT_ASSERT_DOUBLES_EQUAL_C(E, A, D, C) \
+ do { \
+ const auto _ed = (E); \
+ const auto _ad = (A); \
+ const auto _dd = (D); \
+ if (std::isnan((long double)_ed) && !std::isnan((long double)_ad)) { \
+ const auto _as = ToString((long double)_ad); \
+ auto&& failMsg = Sprintf("expected NaN, got %s %s", _as.data(), (::TStringBuilder() << C).data()); \
+ UNIT_FAIL_IMPL("assertion failure", failMsg); \
+ } \
+ if (!std::isnan((long double)_ed) && std::isnan((long double)_ad)) { \
+ const auto _es = ToString((long double)_ed); \
+ auto&& failMsg = Sprintf("expected %s, got NaN %s", _es.data(), (::TStringBuilder() << C).data()); \
+ UNIT_FAIL_IMPL("assertion failure", failMsg); \
+ } \
+ if (std::abs((_ed) - (_ad)) > (_dd)) { \
+ const auto _es = ToString((long double)_ed); \
+ const auto _as = ToString((long double)_ad); \
+ const auto _ds = ToString((long double)_dd); \
+ auto&& failMsg = Sprintf("std::abs(%s - %s) > %s %s", _es.data(), _as.data(), _ds.data(), (::TStringBuilder() << C).data()); \
+ UNIT_FAIL_IMPL("assertion failure", failMsg); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_DOUBLES_EQUAL(E, A, D) UNIT_ASSERT_DOUBLES_EQUAL_C(E, A, D, "")
+
+//strings
+#define UNIT_ASSERT_STRINGS_EQUAL_C(A, B, C) \
+ do { \
+ const TString _a(A); \
+ const TString _b(B); \
+ if (_a != _b) { \
+ auto&& failMsg = Sprintf("%s != %s %s", ToString(_a).data(), ToString(_b).data(), (::TStringBuilder() << C).data()); \
+ UNIT_FAIL_IMPL("strings equal assertion failed", failMsg); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_STRINGS_EQUAL(A, B) UNIT_ASSERT_STRINGS_EQUAL_C(A, B, "")
+
+#define UNIT_ASSERT_STRING_CONTAINS_C(A, B, C) \
+ do { \
+ const TString _a(A); \
+ const TString _b(B); \
+ if (!_a.Contains(_b)) { \
+ auto&& msg = Sprintf("\"%s\" does not contain \"%s\", %s", ToString(_a).data(), ToString(_b).data(), (::TStringBuilder() << C).data()); \
+ UNIT_FAIL_IMPL("strings contains assertion failed", msg); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_STRING_CONTAINS(A, B) UNIT_ASSERT_STRING_CONTAINS_C(A, B, "")
+
+#define UNIT_ASSERT_NO_DIFF(A, B) \
+ do { \
+ const TString _a(A); \
+ const TString _b(B); \
+ if (_a != _b) { \
+ UNIT_FAIL_IMPL("strings (" #A ") and (" #B ") are different", Sprintf("\n%s", ::NUnitTest::ColoredDiff(_a, _b, " \t\n.,:;'\"").data())); \
+ } \
+ } while (false)
+
+//strings
+#define UNIT_ASSERT_STRINGS_UNEQUAL_C(A, B, C) \
+ do { \
+ const TString _a(A); \
+ const TString _b(B); \
+ if (_a == _b) { \
+ auto&& msg = Sprintf("%s == %s %s", ToString(_a).data(), ToString(_b).data(), (::TStringBuilder() << C).data()); \
+ UNIT_FAIL_IMPL("strings unequal assertion failed", msg); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_STRINGS_UNEQUAL(A, B) UNIT_ASSERT_STRINGS_UNEQUAL_C(A, B, "")
+
+//bool
+#define UNIT_ASSERT_C(A, C) \
+ do { \
+ if (!(A)) { \
+ UNIT_FAIL_IMPL("assertion failed", Sprintf("(%s) %s", #A, (::TStringBuilder() << C).data())); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT(A) UNIT_ASSERT_C(A, "")
+
+//general
+#define UNIT_ASSERT_EQUAL_C(A, B, C) \
+ do { \
+ if (!((A) == (B))) { \
+ UNIT_FAIL_IMPL("equal assertion failed", Sprintf("%s == %s %s", #A, #B, (::TStringBuilder() << C).data())); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_EQUAL(A, B) UNIT_ASSERT_EQUAL_C(A, B, "")
+
+#define UNIT_ASSERT_UNEQUAL_C(A, B, C) \
+ do { \
+ if ((A) == (B)) { \
+ UNIT_FAIL_IMPL("unequal assertion failed", Sprintf("%s != %s %s", #A, #B, (::TStringBuilder() << C).data()));\
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_UNEQUAL(A, B) UNIT_ASSERT_UNEQUAL_C(A, B, "")
+
+#define UNIT_ASSERT_LT_C(A, B, C) \
+ do { \
+ if (!((A) < (B))) { \
+ UNIT_FAIL_IMPL("less-than assertion failed", Sprintf("%s < %s %s", #A, #B, (::TStringBuilder() << C).data())); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_LT(A, B) UNIT_ASSERT_LT_C(A, B, "")
+
+#define UNIT_ASSERT_LE_C(A, B, C) \
+ do { \
+ if (!((A) <= (B))) { \
+ UNIT_FAIL_IMPL("less-or-equal assertion failed", Sprintf("%s <= %s %s", #A, #B, (::TStringBuilder() << C).data())); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_LE(A, B) UNIT_ASSERT_LE_C(A, B, "")
+
+#define UNIT_ASSERT_GT_C(A, B, C) \
+ do { \
+ if (!((A) > (B))) { \
+ UNIT_FAIL_IMPL("greater-than assertion failed", Sprintf("%s > %s %s", #A, #B, (::TStringBuilder() << C).data())); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_GT(A, B) UNIT_ASSERT_GT_C(A, B, "")
+
+#define UNIT_ASSERT_GE_C(A, B, C) \
+ do { \
+ if (!((A) >= (B))) { \
+ UNIT_FAIL_IMPL("greater-or-equal assertion failed", Sprintf("%s >= %s %s", #A, #B, (::TStringBuilder() << C).data())); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_GE(A, B) UNIT_ASSERT_GE_C(A, B, "")
+
+#define UNIT_CHECK_GENERATED_EXCEPTION_C(A, E, C) \
+ do { \
+ try { \
+ (void)(A); \
+ } catch (const ::NUnitTest::TAssertException&) { \
+ throw; \
+ } catch (const E&) { \
+ break; \
+ } \
+ UNIT_ASSERT_C(0, "Exception hasn't been thrown, but it should have happened " << C); \
+ } while (false)
+
+#define UNIT_CHECK_GENERATED_EXCEPTION(A, E) UNIT_CHECK_GENERATED_EXCEPTION_C(A, E, "")
+
+#define UNIT_CHECK_GENERATED_NO_EXCEPTION_C(A, E, C) \
+ do { \
+ try { \
+ (void)(A); \
+ } catch (const ::NUnitTest::TAssertException&) { \
+ throw; \
+ } catch (const E&) { \
+ UNIT_ASSERT_C(0, "Exception has been thrown, but it shouldn't have happened " << C); \
+ } \
+ } while (false)
+
+#define UNIT_CHECK_GENERATED_NO_EXCEPTION(A, E) UNIT_CHECK_GENERATED_NO_EXCEPTION_C(A, E, "and exception message is:\n" << CurrentExceptionMessage())
+
+// Same as UNIT_ASSERT_EXCEPTION_SATISFIES but prints additional string C when nothing was thrown
+#define UNIT_ASSERT_EXCEPTION_SATISFIES_C(A, E, pred, C) \
+ do { \
+ bool _thrown = false; \
+ try { \
+ (void)(A); \
+ } catch (const ::NUnitTest::TAssertException&) { \
+ throw; \
+ } catch (const E& e) { \
+ _thrown = true; \
+ UNIT_ASSERT_C(pred(e), "Exception does not satisfy predicate '" \
+ << #pred << "'"); \
+ } catch (...) { \
+ _thrown = true; \
+ UNIT_FAIL_IMPL("exception assertion failed", \
+ #A << " did not throw " << #E \
+ << ", but threw other exception " \
+ << "with message:\n" \
+ << CurrentExceptionMessage()); \
+ } \
+ if (!_thrown) { \
+ UNIT_FAIL_IMPL("exception assertion failed", \
+ #A << " did not throw any exception" \
+ << " (expected " << #E << ") " << C); \
+ } \
+ } while (false)
+
+// Assert that a specific exception is thrown and satisfies predicate pred(e), where e is the exception instance.
+// Example:
+// UNIT_ASSERT_EXCEPTION_SATISFIES(MakeRequest(invalidData), TError,
+// [](const TError& e){ return e.Status == HTTP_BAD_REQUEST; })
+// This code validates that MakeRequest with invalidData throws TError with code 400.
+#define UNIT_ASSERT_EXCEPTION_SATISFIES(A, E, pred) \
+ UNIT_ASSERT_EXCEPTION_SATISFIES_C(A, E, pred, "")
+
+// Same as UNIT_ASSERT_EXCEPTION_CONTAINS but prints additional string C when nothing was thrown
+#define UNIT_ASSERT_EXCEPTION_CONTAINS_C(A, E, substr, C) \
+ do { \
+ const TString _substr{substr}; \
+ UNIT_ASSERT_EXCEPTION_SATISFIES_C(A, E, \
+ [&_substr](const E&){ \
+ if (!_substr.empty()) { \
+ UNIT_ASSERT_C(CurrentExceptionMessage() \
+ .Contains(_substr), \
+ "Exception message does not contain \"" \
+ << _substr << "\".\n" \
+ << "Exception message: " \
+ << CurrentExceptionMessage()); \
+ } \
+ return true; \
+ }, \
+ C); \
+ } while (false)
+
+// Assert that a specific exception is thrown and CurrentExceptionMessage() contains substr
+#define UNIT_ASSERT_EXCEPTION_CONTAINS(A, E, substr) \
+ UNIT_ASSERT_EXCEPTION_CONTAINS_C(A, E, substr, "")
+
+// Same as UNIT_ASSERT_EXCEPTION but prints additional string C when nothing was thrown
+#define UNIT_ASSERT_EXCEPTION_C(A, E, C) UNIT_ASSERT_EXCEPTION_SATISFIES_C(A, E, [](const E&){ return true; }, C)
+
+// Assert that a specific exception is thrown
+#define UNIT_ASSERT_EXCEPTION(A, E) UNIT_ASSERT_EXCEPTION_C(A, E, "")
+
+#define UNIT_ASSERT_NO_EXCEPTION_C(A, C) \
+ do { \
+ try { \
+ (void)(A); \
+ } catch (const ::NUnitTest::TAssertException&) { \
+ throw; \
+ } catch (...) { \
+ UNIT_FAIL_IMPL("exception-free assertion failed", Sprintf("%s throws %s\nException message: %s", #A, (::TStringBuilder() << C).data(), CurrentExceptionMessage().data())); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_NO_EXCEPTION(A) UNIT_ASSERT_NO_EXCEPTION_C(A, "")
+
+ namespace NPrivate {
+ template <class T, class U, bool Integers>
+ struct TCompareValuesImpl {
+ static inline bool Compare(const T& a, const U& b) {
+ return a == b;
+ }
+ };
+
+ template <class T, class U>
+ struct TCompareValuesImpl<T, U, true> {
+ static inline bool Compare(const T& a, const U& b) {
+ return ::ToString(a) == ::ToString(b);
+ }
+ };
+
+ template <class T, class U>
+ using TCompareValues = TCompareValuesImpl<T, U, std::is_integral<T>::value && std::is_integral<U>::value>;
+
+ template <typename T, typename U>
+ static inline bool CompareEqual(const T& a, const U& b) {
+ return TCompareValues<T, U>::Compare(a, b);
+ }
+
+ static inline bool CompareEqual(const char* a, const char* b) {
+ return 0 == strcmp(a, b);
+ }
+
+ // helper method to avoid double evaluation of A and B expressions in UNIT_ASSERT_VALUES_EQUAL_C
+ template <typename T, typename U>
+ static inline bool CompareAndMakeStrings(const T& a, const U& b, TString& as, TString& asInd, TString& bs, TString& bsInd, bool& usePlainDiff, bool want) {
+ const bool have = CompareEqual(a, b);
+ usePlainDiff = std::is_integral<T>::value && std::is_integral<U>::value;
+
+ if (want == have) {
+ return true;
+ }
+
+ as = ::TStringBuilder() << ::DbgDump(a);
+ bs = ::TStringBuilder() << ::DbgDump(b);
+ asInd = ::TStringBuilder() << ::DbgDump(a).SetIndent(true);
+ bsInd = ::TStringBuilder() << ::DbgDump(b).SetIndent(true);
+
+ return false;
+ }
+ }
+
+//values
+#define UNIT_ASSERT_VALUES_EQUAL_IMPL(A, B, C, EQflag, EQstr, NEQstr) \
+ do { \
+ TString _as; \
+ TString _bs; \
+ TString _asInd; \
+ TString _bsInd; \
+ bool _usePlainDiff; \
+ if (!::NUnitTest::NPrivate::CompareAndMakeStrings(A, B, _as, _asInd, _bs, _bsInd, _usePlainDiff, EQflag)) { \
+ auto&& failMsg = Sprintf("(%s %s %s) failed: (%s %s %s) %s", #A, EQstr, #B, _as.data(), NEQstr, _bs.data(), (::TStringBuilder() << C).data()); \
+ if (EQflag && !_usePlainDiff) { \
+ failMsg += ", with diff:\n"; \
+ failMsg += ::NUnitTest::ColoredDiff(_asInd, _bsInd); \
+ } \
+ UNIT_FAIL_IMPL("assertion failed", failMsg); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_VALUES_EQUAL_C(A, B, C) \
+ UNIT_ASSERT_VALUES_EQUAL_IMPL(A, B, C, true, "==", "!=")
+
+#define UNIT_ASSERT_VALUES_UNEQUAL_C(A, B, C) \
+ UNIT_ASSERT_VALUES_EQUAL_IMPL(A, B, C, false, "!=", "==")
+
+#define UNIT_ASSERT_VALUES_EQUAL(A, B) UNIT_ASSERT_VALUES_EQUAL_C(A, B, "")
+#define UNIT_ASSERT_VALUES_UNEQUAL(A, B) UNIT_ASSERT_VALUES_UNEQUAL_C(A, B, "")
+
+// Checks that test will fail while executing given expression
+// Macro for using in unitests for ut helpers
+#define UNIT_ASSERT_TEST_FAILS_C(A, C) \
+ do { \
+ ::NUnitTest::TUnitTestFailChecker checker; \
+ try { \
+ auto guard = checker.InvokeGuard(); \
+ (void)(A); \
+ } catch (...) { \
+ UNIT_FAIL_IMPL("fail test assertion failure", \
+ "code is expected to generate test failure, " \
+ "but it throws exception with message: " \
+ << CurrentExceptionMessage()); \
+ } \
+ if (!checker.Failed()) { \
+ UNIT_FAIL_IMPL("fail test assertion failure", \
+ "code is expected to generate test failure"); \
+ } \
+ } while (false)
+
+#define UNIT_ASSERT_TEST_FAILS(A) UNIT_ASSERT_TEST_FAILS_C(A, "")
+
+#define UNIT_ADD_METRIC(name, value) ut_context.Metrics[name] = value
+
+ class TTestFactory {
+ friend class TTestBase;
+ friend class ITestBaseFactory;
+
+ public:
+ static TTestFactory& Instance();
+
+ unsigned Execute();
+
+ void SetProcessor(ITestSuiteProcessor* processor);
+
+ private:
+ void Register(ITestBaseFactory* b) noexcept;
+
+ ITestSuiteProcessor* Processor() const noexcept;
+
+ private:
+ explicit TTestFactory(ITestSuiteProcessor* processor);
+
+ ~TTestFactory();
+
+ private:
+ TIntrusiveList<ITestBaseFactory> Items_;
+ ITestSuiteProcessor* Processor_;
+ };
+
+ template <class T>
+ class TTestBaseFactory: public ITestBaseFactory {
+ public:
+ ~TTestBaseFactory() override = default;
+
+ inline TTestBase* ConstructTest() override {
+ return new T;
+ }
+
+ inline TString Name() const noexcept override {
+ return T::StaticName();
+ }
+ };
+
+ struct TBaseTestCase {
+ // NOTE: since EACH test case is instantiated for listing tests, its
+ // ctor/dtor are not the best place to do heavy preparations in test fixtures.
+ //
+ // Consider using SetUp()/TearDown() methods instead
+
+ inline TBaseTestCase()
+ : TBaseTestCase(nullptr, nullptr, false)
+ {
+ }
+
+ inline TBaseTestCase(const char* name, std::function<void(TTestContext&)> body, bool forceFork)
+ : Name_(name)
+ , Body_(std::move(body))
+ , ForceFork_(forceFork)
+ {
+ }
+
+ virtual ~TBaseTestCase() = default;
+
+ // Each test case is executed in 3 steps:
+ //
+ // 1. SetUp() (from fixture)
+ // 2. Execute_() (test body from Y_UNIT_TEST macro)
+ // 3. TearDown() (from fixture)
+ //
+ // Both SetUp() and TearDown() may use UNIT_* check macros and are only
+ // called when the test is executed.
+
+ virtual void SetUp(TTestContext& /* context */) {
+ }
+
+ virtual void TearDown(TTestContext& /* context */) {
+ }
+
+ virtual void Execute_(TTestContext& context) {
+ Body_(context);
+ }
+
+ const char* Name_;
+ std::function<void(TTestContext&)> Body_;
+ bool ForceFork_;
+ };
+
+ using TBaseFixture = TBaseTestCase;
+
+ // Class for checking that code raises unittest failure
+ class TUnitTestFailChecker {
+ public:
+ struct TInvokeGuard {
+ explicit TInvokeGuard(TUnitTestFailChecker& parent)
+ : Parent(&parent)
+ {
+ Parent->SetHandler();
+ }
+
+ TInvokeGuard(TInvokeGuard&& guard) noexcept
+ : Parent(guard.Parent)
+ {
+ guard.Parent = nullptr;
+ }
+
+ ~TInvokeGuard() {
+ if (Parent) {
+ ClearRaiseErrorHandler();
+ }
+ }
+
+ TUnitTestFailChecker* Parent;
+ };
+
+ TUnitTestFailChecker() = default;
+ TUnitTestFailChecker(const TUnitTestFailChecker&) = delete;
+ TUnitTestFailChecker(TUnitTestFailChecker&&) = delete;
+
+ TInvokeGuard InvokeGuard() {
+ return TInvokeGuard(*this);
+ }
+
+ const TString& What() const {
+ return What_;
+ }
+
+ const TString& Msg() const {
+ return Msg_;
+ }
+
+ bool FatalFailure() const {
+ return FatalFailure_;
+ }
+
+ bool Failed() const {
+ return Failed_;
+ }
+
+ private:
+ void Handler(const char* what, const TString& msg, bool fatalFailure) {
+ What_ = what;
+ Msg_ = msg;
+ FatalFailure_ = fatalFailure;
+ Failed_ = true;
+ }
+
+ void SetHandler() {
+ TRaiseErrorHandler handler = [this](const char* what, const TString& msg, bool fatalFailure) {
+ Handler(what, msg, fatalFailure);
+ };
+ SetRaiseErrorHandler(std::move(handler));
+ }
+
+ private:
+ TString What_;
+ TString Msg_;
+ bool FatalFailure_ = false;
+ bool Failed_ = false;
+ };
+
+#define UNIT_TEST_SUITE_REGISTRATION(T) \
+ static const ::NUnitTest::TTestBaseFactory<T> Y_GENERATE_UNIQUE_ID(UTREG_);
+
+#define Y_UNIT_TEST_SUITE_IMPL_F(N, T, F) \
+ namespace NTestSuite##N { \
+ class TCurrentTestCase: public F { \
+ }; \
+ class TCurrentTest: public T { \
+ private: \
+ typedef std::function<THolder<NUnitTest::TBaseTestCase>()> TTestCaseFactory; \
+ typedef TVector<TTestCaseFactory> TTests; \
+ \
+ static TTests& Tests() { \
+ static TTests tests; \
+ return tests; \
+ } \
+ \
+ public: \
+ static TString StaticName() { \
+ return #N; \
+ } \
+ virtual TString Name() const noexcept { \
+ return StaticName(); \
+ } \
+ \
+ static void AddTest(const char* name, \
+ const std::function<void(NUnitTest::TTestContext&)>& body, bool forceFork) \
+ { \
+ Tests().push_back([=]{ return MakeHolder<NUnitTest::TBaseTestCase>(name, body, forceFork); }); \
+ } \
+ \
+ static void AddTest(TTestCaseFactory testCaseFactory) { \
+ Tests().push_back(std::move(testCaseFactory)); \
+ } \
+ \
+ virtual void Execute() { \
+ this->AtStart(); \
+ for (TTests::iterator it = Tests().begin(), ie = Tests().end(); it != ie; ++it) { \
+ const auto i = (*it)(); \
+ if (!this->CheckAccessTest(i->Name_)) { \
+ continue; \
+ } \
+ NUnitTest::TTestContext context(this->TTestBase::Processor()); \
+ try { \
+ this->BeforeTest(i->Name_); \
+ { \
+ TCleanUp cleaner(this); \
+ auto testCase = [&i, &context] { \
+ i->SetUp(context); \
+ i->Execute_(context); \
+ i->TearDown(context); \
+ }; \
+ this->T::Run(testCase, StaticName(), i->Name_, i->ForceFork_); \
+ } \
+ } catch (const ::NUnitTest::TAssertException&) { \
+ } catch (const yexception& e) { \
+ CATCH_REACTION_BT(i->Name_, e, &context); \
+ } catch (const std::exception& e) { \
+ CATCH_REACTION(i->Name_, e, &context); \
+ } catch (...) { \
+ this->AddError("non-std exception!", &context); \
+ } \
+ this->Finish(i->Name_, &context); \
+ } \
+ this->AtEnd(); \
+ } \
+ }; \
+ UNIT_TEST_SUITE_REGISTRATION(TCurrentTest) \
+ } \
+ namespace NTestSuite##N
+
+#define Y_UNIT_TEST_SUITE_IMPL(N, T) Y_UNIT_TEST_SUITE_IMPL_F(N, T, ::NUnitTest::TBaseTestCase)
+#define Y_UNIT_TEST_SUITE(N) Y_UNIT_TEST_SUITE_IMPL(N, TTestBase)
+#define Y_UNIT_TEST_SUITE_F(N, F) Y_UNIT_TEST_SUITE_IMPL_F(N, TTestBase, F)
+#define RUSAGE_UNIT_TEST_SUITE(N) Y_UNIT_TEST_SUITE_IMPL(N, NUnitTest::TRusageTest, ::NUnitTest::TBaseTestCase)
+
+#define Y_UNIT_TEST_IMPL_REGISTER(N, FF, F) \
+ struct TTestCase##N : public F { \
+ TTestCase##N() \
+ : F() \
+ { \
+ Name_ = #N; \
+ ForceFork_ = FF; \
+ } \
+ static THolder<NUnitTest::TBaseTestCase> Create() { \
+ return ::MakeHolder<TTestCase##N>(); \
+ } \
+ void Execute_(NUnitTest::TTestContext&) override; \
+ }; \
+ struct TTestRegistration##N { \
+ TTestRegistration##N() { \
+ TCurrentTest::AddTest(TTestCase##N::Create); \
+ } \
+ }; \
+ static const TTestRegistration##N testRegistration##N;
+
+#define Y_UNIT_TEST_IMPL(N, FF, F) \
+ Y_UNIT_TEST_IMPL_REGISTER(N, FF, F) \
+ void TTestCase##N::Execute_(NUnitTest::TTestContext& ut_context Y_DECLARE_UNUSED)
+
+#define Y_UNIT_TEST(N) Y_UNIT_TEST_IMPL(N, false, TCurrentTestCase)
+#define Y_UNIT_TEST_F(N, F) Y_UNIT_TEST_IMPL(N, false, F)
+#define SIMPLE_UNIT_FORKED_TEST(N) Y_UNIT_TEST_IMPL(N, true, TCurrentTestCase)
+
+#define Y_UNIT_TEST_SUITE_IMPLEMENTATION(N) \
+ namespace NTestSuite##N
+
+#define Y_UNIT_TEST_DECLARE(N) \
+ struct TTestCase##N
+
+#define Y_UNIT_TEST_FRIEND(N, T) \
+ friend NTestSuite##N::TTestCase##T \
+
+ TString RandomString(size_t len, ui32 seed = 0);
+}
+
+using ::NUnitTest::TTestBase;
diff --git a/library/cpp/testing/unittest/registar_ut.cpp b/library/cpp/testing/unittest/registar_ut.cpp
new file mode 100644
index 0000000000..aada471030
--- /dev/null
+++ b/library/cpp/testing/unittest/registar_ut.cpp
@@ -0,0 +1,360 @@
+#include <library/cpp/testing/unittest/registar.h>
+
+Y_UNIT_TEST_SUITE(TUnitTestMacroTest) {
+ Y_UNIT_TEST(Assert) {
+ auto unitAssert = [] {
+ UNIT_ASSERT(false);
+ };
+ UNIT_ASSERT_TEST_FAILS(unitAssert());
+
+ UNIT_ASSERT(true);
+ }
+
+ Y_UNIT_TEST(TypesEqual) {
+ auto typesEqual = [] {
+ UNIT_ASSERT_TYPES_EQUAL(int, long);
+ };
+ UNIT_ASSERT_TEST_FAILS(typesEqual());
+
+ UNIT_ASSERT_TYPES_EQUAL(TString, TString);
+ }
+
+ Y_UNIT_TEST(DoublesEqual) {
+ auto doublesEqual = [](double d1, double d2, double precision) {
+ UNIT_ASSERT_DOUBLES_EQUAL(d1, d2, precision);
+ };
+ UNIT_ASSERT_TEST_FAILS(doublesEqual(0.0, 0.5, 0.1));
+ UNIT_ASSERT_TEST_FAILS(doublesEqual(0.1, -0.1, 0.1));
+
+ UNIT_ASSERT_DOUBLES_EQUAL(0.0, 0.01, 0.1);
+ UNIT_ASSERT_DOUBLES_EQUAL(0.01, 0.0, 0.1);
+
+ constexpr auto nan = std::numeric_limits<double>::quiet_NaN();
+ UNIT_ASSERT_TEST_FAILS(doublesEqual(nan, 0.5, 0.1));
+ UNIT_ASSERT_TEST_FAILS(doublesEqual(0.5, nan, 0.1));
+ UNIT_ASSERT_DOUBLES_EQUAL(nan, nan, 0.1);
+ }
+
+ Y_UNIT_TEST(StringsEqual) {
+ auto stringsEqual = [](auto s1, auto s2) {
+ UNIT_ASSERT_STRINGS_EQUAL(s1, s2);
+ };
+ UNIT_ASSERT_TEST_FAILS(stringsEqual("q", "w"));
+ UNIT_ASSERT_TEST_FAILS(stringsEqual("q", TString("w")));
+ UNIT_ASSERT_TEST_FAILS(stringsEqual(TString("q"), "w"));
+ UNIT_ASSERT_TEST_FAILS(stringsEqual(TString("a"), TString("b")));
+ UNIT_ASSERT_TEST_FAILS(stringsEqual(TString("a"), TStringBuf("b")));
+ UNIT_ASSERT_TEST_FAILS(stringsEqual("a", TStringBuf("b")));
+ UNIT_ASSERT_TEST_FAILS(stringsEqual(TStringBuf("a"), "b"));
+
+ TString empty;
+ TStringBuf emptyBuf;
+ UNIT_ASSERT_STRINGS_EQUAL("", empty);
+ UNIT_ASSERT_STRINGS_EQUAL(empty, emptyBuf);
+ UNIT_ASSERT_STRINGS_EQUAL("", static_cast<const char*>(nullptr));
+ }
+
+ Y_UNIT_TEST(StringContains) {
+ auto stringContains = [](auto s, auto substr) {
+ UNIT_ASSERT_STRING_CONTAINS(s, substr);
+ };
+ UNIT_ASSERT_TEST_FAILS(stringContains("", "a"));
+ UNIT_ASSERT_TEST_FAILS(stringContains("lurkmore", "moar"));
+
+ UNIT_ASSERT_STRING_CONTAINS("", "");
+ UNIT_ASSERT_STRING_CONTAINS("a", "");
+ UNIT_ASSERT_STRING_CONTAINS("failure", "fail");
+ UNIT_ASSERT_STRING_CONTAINS("lurkmore", "more");
+ }
+
+ Y_UNIT_TEST(NoDiff) {
+ auto noDiff = [](auto s1, auto s2) {
+ UNIT_ASSERT_NO_DIFF(s1, s2);
+ };
+ UNIT_ASSERT_TEST_FAILS(noDiff("q", "w"));
+ UNIT_ASSERT_TEST_FAILS(noDiff("q", ""));
+
+ UNIT_ASSERT_NO_DIFF("", "");
+ UNIT_ASSERT_NO_DIFF("a", "a");
+ }
+
+ Y_UNIT_TEST(StringsUnequal) {
+ auto stringsUnequal = [](auto s1, auto s2) {
+ UNIT_ASSERT_STRINGS_UNEQUAL(s1, s2);
+ };
+ UNIT_ASSERT_TEST_FAILS(stringsUnequal("1", "1"));
+ UNIT_ASSERT_TEST_FAILS(stringsUnequal("", ""));
+ UNIT_ASSERT_TEST_FAILS(stringsUnequal("42", TString("42")));
+ UNIT_ASSERT_TEST_FAILS(stringsUnequal(TString("4"), "4"));
+ UNIT_ASSERT_TEST_FAILS(stringsUnequal("d", TStringBuf("d")));
+ UNIT_ASSERT_TEST_FAILS(stringsUnequal(TStringBuf("yandex"), "yandex"));
+ UNIT_ASSERT_TEST_FAILS(stringsUnequal(TStringBuf("index"), TString("index")));
+ UNIT_ASSERT_TEST_FAILS(stringsUnequal(TString("diff"), TStringBuf("diff")));
+
+ UNIT_ASSERT_STRINGS_UNEQUAL("1", "2");
+ UNIT_ASSERT_STRINGS_UNEQUAL("", "3");
+ UNIT_ASSERT_STRINGS_UNEQUAL("green", TStringBuf("red"));
+ UNIT_ASSERT_STRINGS_UNEQUAL(TStringBuf("solomon"), "golovan");
+ UNIT_ASSERT_STRINGS_UNEQUAL("d", TString("f"));
+ UNIT_ASSERT_STRINGS_UNEQUAL(TString("yandex"), "index");
+ UNIT_ASSERT_STRINGS_UNEQUAL(TString("mail"), TStringBuf("yandex"));
+ UNIT_ASSERT_STRINGS_UNEQUAL(TStringBuf("C++"), TString("python"));
+ }
+
+ Y_UNIT_TEST(Equal) {
+ auto equal = [](auto v1, auto v2) {
+ UNIT_ASSERT_EQUAL(v1, v2);
+ };
+ UNIT_ASSERT_TEST_FAILS(equal("1", TString("2")));
+ UNIT_ASSERT_TEST_FAILS(equal(1, 2));
+ UNIT_ASSERT_TEST_FAILS(equal(42ul, static_cast<unsigned short>(24)));
+
+ UNIT_ASSERT_EQUAL("abc", TString("abc"));
+ UNIT_ASSERT_EQUAL(12l, 12);
+ UNIT_ASSERT_EQUAL(55, 55);
+ }
+
+ Y_UNIT_TEST(Unequal) {
+ auto unequal = [](auto v1, auto v2) {
+ UNIT_ASSERT_UNEQUAL(v1, v2);
+ };
+ UNIT_ASSERT_TEST_FAILS(unequal("x", TString("x")));
+ UNIT_ASSERT_TEST_FAILS(unequal(1, 1));
+ UNIT_ASSERT_TEST_FAILS(unequal(static_cast<unsigned short>(42), 42ul));
+
+ UNIT_ASSERT_UNEQUAL("abc", TString("cba"));
+ UNIT_ASSERT_UNEQUAL(12l, 10);
+ UNIT_ASSERT_UNEQUAL(33, 50);
+ }
+
+ Y_UNIT_TEST(LessThan) {
+ auto lt = [](auto v1, auto v2) {
+ UNIT_ASSERT_LT(v1, v2);
+ };
+
+ // less than
+ UNIT_ASSERT_LT(TStringBuf("1"), "2");
+ UNIT_ASSERT_LT("2", TString("3"));
+ UNIT_ASSERT_LT("abc", TString("azz"));
+ UNIT_ASSERT_LT(2, 4);
+ UNIT_ASSERT_LT(42ul, static_cast<unsigned short>(48));
+
+ // equals
+ UNIT_ASSERT_TEST_FAILS(lt(TStringBuf("2"), "2"));
+ UNIT_ASSERT_TEST_FAILS(lt("2", TString("2")));
+ UNIT_ASSERT_TEST_FAILS(lt("abc", TString("abc")));
+ UNIT_ASSERT_TEST_FAILS(lt(2, 2));
+ UNIT_ASSERT_TEST_FAILS(lt(42ul, static_cast<unsigned short>(42)));
+
+ // greater than
+ UNIT_ASSERT_TEST_FAILS(lt(TStringBuf("2"), "1"));
+ UNIT_ASSERT_TEST_FAILS(lt("3", TString("2")));
+ UNIT_ASSERT_TEST_FAILS(lt("azz", TString("abc")));
+ UNIT_ASSERT_TEST_FAILS(lt(5, 2));
+ UNIT_ASSERT_TEST_FAILS(lt(100ul, static_cast<unsigned short>(42)));
+ }
+
+ Y_UNIT_TEST(LessOrEqual) {
+ auto le = [](auto v1, auto v2) {
+ UNIT_ASSERT_LE(v1, v2);
+ };
+
+ // less than
+ UNIT_ASSERT_LE(TStringBuf("1"), "2");
+ UNIT_ASSERT_LE("2", TString("3"));
+ UNIT_ASSERT_LE("abc", TString("azz"));
+ UNIT_ASSERT_LE(2, 4);
+ UNIT_ASSERT_LE(42ul, static_cast<unsigned short>(48));
+
+ // equals
+ UNIT_ASSERT_LE(TStringBuf("2"), "2");
+ UNIT_ASSERT_LE("2", TString("2"));
+ UNIT_ASSERT_LE("abc", TString("abc"));
+ UNIT_ASSERT_LE(2, 2);
+ UNIT_ASSERT_LE(42ul, static_cast<unsigned short>(42));
+
+ // greater than
+ UNIT_ASSERT_TEST_FAILS(le(TStringBuf("2"), "1"));
+ UNIT_ASSERT_TEST_FAILS(le("3", TString("2")));
+ UNIT_ASSERT_TEST_FAILS(le("azz", TString("abc")));
+ UNIT_ASSERT_TEST_FAILS(le(5, 2));
+ UNIT_ASSERT_TEST_FAILS(le(100ul, static_cast<unsigned short>(42)));
+ }
+
+ Y_UNIT_TEST(GreaterThan) {
+ auto gt = [](auto v1, auto v2) {
+ UNIT_ASSERT_GT(v1, v2);
+ };
+
+ // less than
+ UNIT_ASSERT_TEST_FAILS(gt(TStringBuf("1"), "2"));
+ UNIT_ASSERT_TEST_FAILS(gt("2", TString("3")));
+ UNIT_ASSERT_TEST_FAILS(gt("abc", TString("azz")));
+ UNIT_ASSERT_TEST_FAILS(gt(2, 4));
+ UNIT_ASSERT_TEST_FAILS(gt(42ul, static_cast<unsigned short>(48)));
+
+ // equals
+ UNIT_ASSERT_TEST_FAILS(gt(TStringBuf("2"), "2"));
+ UNIT_ASSERT_TEST_FAILS(gt("2", TString("2")));
+ UNIT_ASSERT_TEST_FAILS(gt("abc", TString("abc")));
+ UNIT_ASSERT_TEST_FAILS(gt(2, 2));
+ UNIT_ASSERT_TEST_FAILS(gt(42ul, static_cast<unsigned short>(42)));
+
+ // greater than
+ UNIT_ASSERT_GT(TStringBuf("2"), "1");
+ UNIT_ASSERT_GT("3", TString("2"));
+ UNIT_ASSERT_GT("azz", TString("abc"));
+ UNIT_ASSERT_GT(5, 2);
+ UNIT_ASSERT_GT(100ul, static_cast<unsigned short>(42));
+ }
+
+ Y_UNIT_TEST(GreaterOrEqual) {
+ auto ge = [](auto v1, auto v2) {
+ UNIT_ASSERT_GE(v1, v2);
+ };
+
+ // less than
+ UNIT_ASSERT_TEST_FAILS(ge(TStringBuf("1"), "2"));
+ UNIT_ASSERT_TEST_FAILS(ge("2", TString("3")));
+ UNIT_ASSERT_TEST_FAILS(ge("abc", TString("azz")));
+ UNIT_ASSERT_TEST_FAILS(ge(2, 4));
+ UNIT_ASSERT_TEST_FAILS(ge(42ul, static_cast<unsigned short>(48)));
+
+ // equals
+ UNIT_ASSERT_GE(TStringBuf("2"), "2");
+ UNIT_ASSERT_GE("2", TString("2"));
+ UNIT_ASSERT_GE("abc", TString("abc"));
+ UNIT_ASSERT_GE(2, 2);
+ UNIT_ASSERT_GE(42ul, static_cast<unsigned short>(42));
+
+ // greater than
+ UNIT_ASSERT_GE(TStringBuf("2"), "1");
+ UNIT_ASSERT_GE("3", TString("2"));
+ UNIT_ASSERT_GE("azz", TString("abc"));
+ UNIT_ASSERT_GE(5, 2);
+ UNIT_ASSERT_GE(100ul, static_cast<unsigned short>(42));
+ }
+
+ Y_UNIT_TEST(ValuesEqual) {
+ auto valuesEqual = [](auto v1, auto v2) {
+ UNIT_ASSERT_VALUES_EQUAL(v1, v2);
+ };
+ UNIT_ASSERT_TEST_FAILS(valuesEqual(1, 2));
+ UNIT_ASSERT_TEST_FAILS(valuesEqual(1l, static_cast<short>(2)));
+
+ UNIT_ASSERT_VALUES_EQUAL("yandex", TString("yandex"));
+ UNIT_ASSERT_VALUES_EQUAL(1.0, 1.0);
+ }
+
+ Y_UNIT_TEST(ValuesUnequal) {
+ auto valuesUnequal = [](auto v1, auto v2) {
+ UNIT_ASSERT_VALUES_UNEQUAL(v1, v2);
+ };
+ UNIT_ASSERT_TEST_FAILS(valuesUnequal(5, 5));
+ UNIT_ASSERT_TEST_FAILS(valuesUnequal(static_cast<char>(5), 5l));
+ TString test("test");
+ UNIT_ASSERT_TEST_FAILS(valuesUnequal("test", test.data()));
+
+ UNIT_ASSERT_VALUES_UNEQUAL("UNIT_ASSERT_VALUES_UNEQUAL", "UNIT_ASSERT_VALUES_EQUAL");
+ UNIT_ASSERT_VALUES_UNEQUAL(1.0, 1.1);
+ }
+
+ class TTestException: public yexception {
+ public:
+ TTestException(const TString& text = "test exception", bool throwMe = true)
+ : ThrowMe(throwMe)
+ {
+ *this << text;
+ }
+
+ virtual ~TTestException() = default;
+
+ virtual void Throw() {
+ if (ThrowMe) {
+ throw *this;
+ }
+ }
+
+ void AssertNoException() {
+ UNIT_ASSERT_NO_EXCEPTION(Throw());
+ }
+
+ template <class TExpectedException>
+ void AssertException() {
+ UNIT_ASSERT_EXCEPTION(Throw(), TExpectedException);
+ }
+
+ template <class TExpectedException, class T>
+ void AssertExceptionContains(const T& substr) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(Throw(), TExpectedException, substr);
+ }
+
+ template <class TExpectedException, class P>
+ void AssertExceptionSatisfies(const P& predicate) {
+ UNIT_ASSERT_EXCEPTION_SATISFIES(Throw(), TExpectedException, predicate);
+ }
+
+ int GetValue() const {
+ return 5; // just some value for predicate testing
+ }
+
+ bool ThrowMe;
+ };
+
+ class TOtherTestException: public TTestException {
+ public:
+ using TTestException::TTestException;
+
+ // Throws other type of exception
+ void Throw() override {
+ if (ThrowMe) {
+ throw *this;
+ }
+ }
+ };
+
+ Y_UNIT_TEST(Exception) {
+ UNIT_ASSERT_TEST_FAILS(TTestException("", false).AssertException<TTestException>());
+ UNIT_ASSERT_TEST_FAILS(TTestException().AssertException<TOtherTestException>());
+
+ UNIT_ASSERT_EXCEPTION(TOtherTestException().Throw(), TTestException);
+ UNIT_ASSERT_EXCEPTION(TTestException().Throw(), TTestException);
+ }
+
+ Y_UNIT_TEST(ExceptionAssertionContainsOtherExceptionMessage) {
+ NUnitTest::TUnitTestFailChecker checker;
+ {
+ auto guard = checker.InvokeGuard();
+ TTestException("custom exception message").AssertException<TOtherTestException>();
+ }
+ UNIT_ASSERT(checker.Failed());
+ UNIT_ASSERT_STRING_CONTAINS(checker.Msg(), "custom exception message");
+ }
+
+ Y_UNIT_TEST(NoException) {
+ UNIT_ASSERT_TEST_FAILS(TTestException().AssertNoException());
+
+ UNIT_ASSERT_NO_EXCEPTION(TTestException("", false).Throw());
+ }
+
+ Y_UNIT_TEST(ExceptionContains) {
+ UNIT_ASSERT_TEST_FAILS(TTestException("abc").AssertExceptionContains<TTestException>("cba"));
+ UNIT_ASSERT_TEST_FAILS(TTestException("abc").AssertExceptionContains<TTestException>(TStringBuf("cba")));
+ UNIT_ASSERT_TEST_FAILS(TTestException("abc").AssertExceptionContains<TTestException>(TString("cba")));
+ UNIT_ASSERT_TEST_FAILS(TTestException("abc").AssertExceptionContains<TTestException>(TStringBuilder() << "cba"));
+
+ UNIT_ASSERT_TEST_FAILS(TTestException("abc", false).AssertExceptionContains<TTestException>("bc"));
+
+ UNIT_ASSERT_TEST_FAILS(TTestException("abc").AssertExceptionContains<TOtherTestException>("b"));
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TTestException("abc").Throw(), TTestException, "a");
+ }
+
+ Y_UNIT_TEST(ExceptionSatisfies) {
+ const auto goodPredicate = [](const TTestException& e) { return e.GetValue() == 5; };
+ const auto badPredicate = [](const TTestException& e) { return e.GetValue() != 5; };
+ UNIT_ASSERT_NO_EXCEPTION(TTestException().AssertExceptionSatisfies<TTestException>(goodPredicate));
+ UNIT_ASSERT_TEST_FAILS(TTestException().AssertExceptionSatisfies<TTestException>(badPredicate));
+ UNIT_ASSERT_TEST_FAILS(TTestException().AssertExceptionSatisfies<TOtherTestException>(goodPredicate));
+ }
+}
diff --git a/library/cpp/testing/unittest/simple.h b/library/cpp/testing/unittest/simple.h
new file mode 100644
index 0000000000..2a5300d886
--- /dev/null
+++ b/library/cpp/testing/unittest/simple.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "registar.h"
+
+namespace NUnitTest {
+ struct TSimpleTestExecutor: public TTestBase {
+ typedef TVector<TBaseTestCase> TTests;
+
+ TTests Tests;
+
+ virtual void Execute() override final {
+ AtStart();
+
+ for (typename TTests::iterator i = Tests.begin(), ie = Tests.end(); i != ie; ++i) {
+ if (!CheckAccessTest(i->Name_)) {
+ continue;
+ }
+ TTestContext context(this->Processor());
+ try {
+ BeforeTest(i->Name_);
+ {
+ TCleanUp cleaner(this);
+ TTestBase::Run([i, &context] { i->Body_(context); }, Name(), i->Name_, i->ForceFork_);
+ }
+ } catch (const ::NUnitTest::TAssertException&) {
+ } catch (const yexception& e) {
+ CATCH_REACTION_BT(i->Name_, e, &context);
+ } catch (const std::exception& e) {
+ CATCH_REACTION(i->Name_, e, &context);
+ } catch (...) {
+ AddError("non-std exception!", &context);
+ }
+ Finish(i->Name_, &context);
+ }
+
+ AtEnd();
+ }
+ };
+}
diff --git a/library/cpp/testing/unittest/tests_data.cpp b/library/cpp/testing/unittest/tests_data.cpp
new file mode 100644
index 0000000000..b51cbc4b87
--- /dev/null
+++ b/library/cpp/testing/unittest/tests_data.cpp
@@ -0,0 +1,104 @@
+#include "tests_data.h"
+#include "registar.h"
+
+#include <library/cpp/testing/common/network.h>
+
+#include <util/system/env.h>
+#include <util/system/mutex.h>
+
+class TPortManager::TPortManagerImpl {
+public:
+ TPortManagerImpl(bool reservePortsForCurrentTest)
+ : EnableReservePortsForCurrentTest(reservePortsForCurrentTest)
+ , DisableRandomPorts(!GetEnv("NO_RANDOM_PORTS").empty())
+ {
+ }
+
+ ui16 GetPort(ui16 port) {
+ if (port && DisableRandomPorts) {
+ return port;
+ }
+
+ TAtomicSharedPtr<NTesting::IPort> holder(NTesting::GetFreePort().Release());
+ ReservePortForCurrentTest(holder);
+
+ TGuard<TMutex> g(Lock);
+ ReservedPorts.push_back(holder);
+ return holder->Get();
+ }
+
+ ui16 GetUdpPort(ui16 port) {
+ return GetPort(port);
+ }
+
+ ui16 GetTcpPort(ui16 port) {
+ return GetPort(port);
+ }
+
+ ui16 GetTcpAndUdpPort(ui16 port) {
+ return GetPort(port);
+ }
+
+ ui16 GetPortsRange(const ui16 startPort, const ui16 range) {
+ Y_UNUSED(startPort);
+ auto ports = NTesting::NLegacy::GetFreePortsRange(range);
+ ui16 first = ports[0];
+ TGuard<TMutex> g(Lock);
+ for (auto& port : ports) {
+ ReservedPorts.emplace_back(port.Release());
+ ReservePortForCurrentTest(ReservedPorts.back());
+ }
+ return first;
+ }
+
+private:
+ void ReservePortForCurrentTest(const TAtomicSharedPtr<NTesting::IPort>& portGuard) {
+ if (EnableReservePortsForCurrentTest) {
+ TTestBase* currentTest = NUnitTest::NPrivate::GetCurrentTest();
+ if (currentTest != nullptr) {
+ currentTest->RunAfterTest([guard = portGuard]() mutable {
+ guard = nullptr; // remove reference for allocated port
+ });
+ }
+ }
+ }
+
+private:
+ TMutex Lock;
+ TVector<TAtomicSharedPtr<NTesting::IPort>> ReservedPorts;
+ const bool EnableReservePortsForCurrentTest;
+ const bool DisableRandomPorts;
+};
+
+TPortManager::TPortManager(bool reservePortsForCurrentTest)
+ : Impl_(new TPortManagerImpl(reservePortsForCurrentTest))
+{
+}
+
+TPortManager::~TPortManager() {
+}
+
+ui16 TPortManager::GetPort(ui16 port) {
+ return Impl_->GetTcpPort(port);
+}
+
+ui16 TPortManager::GetTcpPort(ui16 port) {
+ return Impl_->GetTcpPort(port);
+}
+
+ui16 TPortManager::GetUdpPort(ui16 port) {
+ return Impl_->GetUdpPort(port);
+}
+
+ui16 TPortManager::GetTcpAndUdpPort(ui16 port) {
+ return Impl_->GetTcpAndUdpPort(port);
+}
+
+ui16 TPortManager::GetPortsRange(const ui16 startPort, const ui16 range) {
+ return Impl_->GetPortsRange(startPort, range);
+}
+
+ui16 GetRandomPort() {
+ TPortManager* pm = Singleton<TPortManager>(false);
+ return pm->GetPort();
+}
diff --git a/library/cpp/testing/unittest/tests_data.h b/library/cpp/testing/unittest/tests_data.h
new file mode 100644
index 0000000000..6536bc1ae6
--- /dev/null
+++ b/library/cpp/testing/unittest/tests_data.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include <library/cpp/testing/common/env.h>
+
+#include <util/generic/noncopyable.h>
+#include <util/generic/ptr.h>
+#include <util/generic/string.h>
+#include <util/network/sock.h>
+
+class TInet6StreamSocket;
+
+// set two options: SO_REUSEADDR and SO_REUSEPORT, both are required for
+// correct implementation of TPortManager because of different operating systems
+// incompatibility: singe SO_REUSEADDR is enough for Linux, but not enough for Darwin
+template <class TSocketType>
+void SetReuseAddressAndPort(const TSocketType& sock) {
+ const int retAddr = SetSockOpt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
+ if (retAddr < 0) {
+ ythrow yexception() << "can't set SO_REUSEADDR: " << LastSystemErrorText(-retAddr);
+ }
+
+#ifdef SO_REUSEPORT
+ const int retPort = SetSockOpt(sock, SOL_SOCKET, SO_REUSEPORT, 1);
+ if (retPort < 0) {
+ ythrow yexception() << "can't set SO_REUSEPORT: " << LastSystemErrorText(-retPort);
+ }
+#endif
+}
+
+class TPortManager: public TNonCopyable {
+public:
+ TPortManager(bool reservePortsForCurrentTest = true);
+ ~TPortManager();
+
+ // Gets free TCP port
+ ui16 GetPort(ui16 port = 0);
+
+ // Gets free TCP port
+ ui16 GetTcpPort(ui16 port = 0);
+
+ // Gets free UDP port
+ ui16 GetUdpPort(ui16 port = 0);
+
+ // Gets one free port for use in both TCP and UDP protocols
+ ui16 GetTcpAndUdpPort(ui16 port = 0);
+
+ ui16 GetPortsRange(const ui16 startPort, const ui16 range);
+
+private:
+ class TPortManagerImpl;
+ THolder<TPortManagerImpl> Impl_;
+};
+
+ui16 GetRandomPort();
diff --git a/library/cpp/testing/unittest/ut/main.cpp b/library/cpp/testing/unittest/ut/main.cpp
new file mode 100644
index 0000000000..e303e21e30
--- /dev/null
+++ b/library/cpp/testing/unittest/ut/main.cpp
@@ -0,0 +1,93 @@
+#include <library/cpp/testing/unittest/gtest.h>
+#include <library/cpp/testing/unittest/registar.h>
+#include <library/cpp/testing/unittest/tests_data.h>
+
+#include <util/generic/set.h>
+#include <util/network/sock.h>
+#include <util/system/env.h>
+#include <util/system/fs.h>
+
+TEST(GTest, Test1) {
+ UNIT_ASSERT_EQUAL(1, 1);
+}
+
+TEST(GTest, Test2) {
+ UNIT_ASSERT_EQUAL(2, 2);
+}
+
+namespace {
+ struct TFixture : ::testing::Test {
+ TFixture()
+ : I(0)
+ {
+ }
+
+ void SetUp() override {
+ I = 5;
+ }
+
+ int I;
+ };
+
+ struct TSimpleFixture : public NUnitTest::TBaseFixture {
+ size_t Value = 24;
+ };
+
+ struct TOtherFixture : public NUnitTest::TBaseFixture {
+ size_t TheAnswer = 42;
+ };
+
+ struct TSetUpTearDownFixture : public NUnitTest::TBaseFixture {
+ int Magic = 3;
+
+ void SetUp(NUnitTest::TTestContext&) override {
+ UNIT_ASSERT_VALUES_EQUAL(Magic, 3);
+ Magic = 17;
+ }
+
+ void TearDown(NUnitTest::TTestContext&) override {
+ UNIT_ASSERT_VALUES_EQUAL(Magic, 42);
+ Magic = 100;
+ }
+ };
+}
+
+TEST_F(TFixture, Test1) {
+ ASSERT_EQ(I, 5);
+}
+
+TEST(ETest, Test1) {
+ UNIT_CHECK_GENERATED_EXCEPTION(ythrow yexception(), yexception);
+ UNIT_CHECK_GENERATED_NO_EXCEPTION(true, yexception);
+}
+
+Y_UNIT_TEST_SUITE(TestSingleTestFixture)
+{
+ Y_UNIT_TEST_F(Test3, TSimpleFixture) {
+ UNIT_ASSERT_EQUAL(Value, 24);
+ }
+}
+
+Y_UNIT_TEST_SUITE_F(TestSuiteFixture, TSimpleFixture)
+{
+ Y_UNIT_TEST(Test1) {
+ UNIT_ASSERT(Value == 24);
+ Value = 25;
+ }
+
+ Y_UNIT_TEST(Test2) {
+ UNIT_ASSERT_EQUAL(Value, 24);
+ }
+
+ Y_UNIT_TEST_F(Test3, TOtherFixture) {
+ UNIT_ASSERT_EQUAL(TheAnswer, 42);
+ }
+}
+
+Y_UNIT_TEST_SUITE(TestSetUpTearDownFixture)
+{
+ Y_UNIT_TEST_F(Test1, TSetUpTearDownFixture) {
+ UNIT_ASSERT_VALUES_EQUAL(Magic, 17);
+ Magic = 42;
+ }
+}
diff --git a/library/cpp/testing/unittest/ut/ya.make b/library/cpp/testing/unittest/ut/ya.make
new file mode 100644
index 0000000000..6d4c0959cc
--- /dev/null
+++ b/library/cpp/testing/unittest/ut/ya.make
@@ -0,0 +1,10 @@
+UNITTEST_FOR(library/cpp/testing/unittest)
+
+OWNER(snowball)
+
+SRCS(
+ main.cpp
+ registar_ut.cpp
+)
+
+END()
diff --git a/library/cpp/testing/unittest/utmain.cpp b/library/cpp/testing/unittest/utmain.cpp
new file mode 100644
index 0000000000..305bc6b40f
--- /dev/null
+++ b/library/cpp/testing/unittest/utmain.cpp
@@ -0,0 +1,771 @@
+#include "plugin.h"
+#include "registar.h"
+#include "utmain.h"
+
+#include <library/cpp/colorizer/colors.h>
+
+#include <library/cpp/json/writer/json.h>
+#include <library/cpp/json/writer/json_value.h>
+#include <library/cpp/testing/common/env.h>
+#include <library/cpp/testing/hook/hook.h>
+
+#include <util/datetime/base.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/scope.h>
+#include <util/generic/string.h>
+#include <util/generic/yexception.h>
+
+#include <util/network/init.h>
+
+#include <util/stream/file.h>
+#include <util/stream/output.h>
+#include <util/string/join.h>
+#include <util/string/util.h>
+
+#include <util/system/defaults.h>
+#include <util/system/execpath.h>
+#include <util/system/valgrind.h>
+#include <util/system/shellcommand.h>
+
+#if defined(_win_)
+#include <fcntl.h>
+#include <io.h>
+#include <windows.h>
+#include <crtdbg.h>
+#endif
+
+#if defined(_unix_)
+#include <unistd.h>
+#endif
+
+#ifdef WITH_VALGRIND
+#define NOTE_IN_VALGRIND(test) VALGRIND_PRINTF("%s::%s", test->unit->name.data(), test->name)
+#else
+#define NOTE_IN_VALGRIND(test)
+#endif
+
+const size_t MAX_COMMENT_MESSAGE_LENGTH = 1024 * 1024; // 1 MB
+
+using namespace NUnitTest;
+
+class TNullTraceWriterProcessor: public ITestSuiteProcessor {
+};
+
+class TTraceWriterProcessor: public ITestSuiteProcessor {
+public:
+ inline TTraceWriterProcessor(const char* traceFilePath, EOpenMode mode)
+ : PrevTime(TInstant::Now())
+ {
+ TraceFile = new TUnbufferedFileOutput(TFile(traceFilePath, mode | WrOnly | Seq));
+ }
+
+private:
+ TAutoPtr<TUnbufferedFileOutput> TraceFile;
+ TString TraceFilePath;
+ TInstant PrevTime;
+ TVector<TString> ErrorMessages;
+
+ inline void Trace(const TString eventName, const NJson::TJsonValue eventValue) {
+ NJsonWriter::TBuf json(NJsonWriter::HEM_UNSAFE);
+ json.BeginObject();
+
+ json.WriteKey("name").WriteString(eventName);
+ json.WriteKey("value").WriteJsonValue(&eventValue);
+ json.WriteKey("timestamp").WriteDouble(TInstant::Now().SecondsFloat(), PREC_NDIGITS, 14);
+
+ json.EndObject();
+
+ json.FlushTo(TraceFile.Get());
+ *TraceFile << "\n";
+ }
+
+ inline void TraceSubtestFinished(const char* className, const char* subtestName, const char* status, const TString comment, const TTestContext* context) {
+ const TInstant now = TInstant::Now();
+ NJson::TJsonValue event;
+ event.InsertValue("class", className);
+ event.InsertValue("subtest", subtestName);
+ event.InsertValue("status", status);
+ event.InsertValue("comment", comment.data());
+ event.InsertValue("time", (now - PrevTime).SecondsFloat());
+ if (context) {
+ for (const auto& metric : context->Metrics) {
+ event["metrics"].InsertValue(metric.first, metric.second);
+ }
+ }
+ Trace("subtest-finished", event);
+
+ PrevTime = now;
+ TString marker = Join("", "\n###subtest-finished:", className, "::", subtestName, "\n");
+ Cout << marker;
+ Cout.Flush();
+ Cerr << comment;
+ Cerr << marker;
+ Cerr.Flush();
+ }
+
+ virtual TString BuildComment(const char* message, const char* backTrace) {
+ return NUnitTest::GetFormatTag("bad") +
+ TString(message).substr(0, MAX_COMMENT_MESSAGE_LENGTH) +
+ NUnitTest::GetResetTag() +
+ TString("\n") +
+ NUnitTest::GetFormatTag("alt1") +
+ TString(backTrace).substr(0, MAX_COMMENT_MESSAGE_LENGTH) +
+ NUnitTest::GetResetTag();
+ }
+
+ void OnBeforeTest(const TTest* test) override {
+ NJson::TJsonValue event;
+ event.InsertValue("class", test->unit->name);
+ event.InsertValue("subtest", test->name);
+ Trace("subtest-started", event);
+ TString marker = Join("", "\n###subtest-started:", test->unit->name, "::", test->name, "\n");
+ Cout << marker;
+ Cout.Flush();
+ Cerr << marker;
+ Cerr.Flush();
+ }
+
+ void OnUnitStart(const TUnit* unit) override {
+ NJson::TJsonValue event;
+ event.InsertValue("class", unit->name);
+ Trace("test-started", event);
+ }
+
+ void OnUnitStop(const TUnit* unit) override {
+ NJson::TJsonValue event;
+ event.InsertValue("class", unit->name);
+ Trace("test-finished", event);
+ }
+
+ void OnError(const TError* descr) override {
+ const TString comment = BuildComment(descr->msg, descr->BackTrace.data());
+ ErrorMessages.push_back(comment);
+ }
+
+ void OnFinish(const TFinish* descr) override {
+ if (descr->Success) {
+ TraceSubtestFinished(descr->test->unit->name.data(), descr->test->name, "good", "", descr->Context);
+ } else {
+ TStringBuilder msgs;
+ for (const TString& m : ErrorMessages) {
+ if (msgs) {
+ msgs << TStringBuf("\n");
+ }
+ msgs << m;
+ }
+ if (msgs) {
+ msgs << TStringBuf("\n");
+ }
+ TraceSubtestFinished(descr->test->unit->name.data(), descr->test->name, "fail", msgs, descr->Context);
+ ErrorMessages.clear();
+ }
+ }
+};
+
+class TColoredProcessor: public ITestSuiteProcessor, public NColorizer::TColors {
+public:
+ inline TColoredProcessor(const TString& appName)
+ : PrintBeforeSuite_(true)
+ , PrintBeforeTest_(true)
+ , PrintAfterTest_(true)
+ , PrintAfterSuite_(true)
+ , PrintTimes_(false)
+ , PrintSummary_(true)
+ , PrevTime_(TInstant::Now())
+ , ShowFails(true)
+ , Start(0)
+ , End(Max<size_t>())
+ , AppName(appName)
+ , ForkTests(false)
+ , IsForked(false)
+ , Loop(false)
+ , ForkExitedCorrectly(false)
+ , TraceProcessor(new TNullTraceWriterProcessor())
+ {
+ }
+
+ ~TColoredProcessor() override {
+ }
+
+ inline void Disable(const char* name) {
+ size_t colon = TString(name).find("::");
+ if (colon == TString::npos) {
+ DisabledSuites_.insert(name);
+ } else {
+ TString suite = TString(name).substr(0, colon);
+ DisabledTests_.insert(name);
+ }
+ }
+
+ inline void Enable(const char* name) {
+ size_t colon = TString(name).rfind("::");
+ if (colon == TString::npos) {
+ EnabledSuites_.insert(name);
+ EnabledTests_.insert(TString() + name + "::*");
+ } else {
+ TString suite = TString(name).substr(0, colon);
+ EnabledSuites_.insert(suite);
+ EnabledSuites_.insert(name);
+ EnabledTests_.insert(name);
+ EnabledTests_.insert(TString() + name + "::*");
+ }
+ }
+
+ inline void SetPrintBeforeSuite(bool print) {
+ PrintBeforeSuite_ = print;
+ }
+
+ inline void SetPrintAfterSuite(bool print) {
+ PrintAfterSuite_ = print;
+ }
+
+ inline void SetPrintBeforeTest(bool print) {
+ PrintBeforeTest_ = print;
+ }
+
+ inline void SetPrintAfterTest(bool print) {
+ PrintAfterTest_ = print;
+ }
+
+ inline void SetPrintTimes(bool print) {
+ PrintTimes_ = print;
+ }
+
+ inline void SetPrintSummary(bool print) {
+ PrintSummary_ = print;
+ }
+
+ inline bool GetPrintSummary() {
+ return PrintSummary_;
+ }
+
+ inline void SetShowFails(bool show) {
+ ShowFails = show;
+ }
+
+ void SetContinueOnFail(bool val) {
+ NUnitTest::ContinueOnFail = val;
+ }
+
+ inline void BeQuiet() {
+ SetPrintTimes(false);
+ SetPrintBeforeSuite(false);
+ SetPrintAfterSuite(false);
+ SetPrintBeforeTest(false);
+ SetPrintAfterTest(false);
+ SetPrintSummary(false);
+ }
+
+ inline void SetStart(size_t val) {
+ Start = val;
+ }
+
+ inline void SetEnd(size_t val) {
+ End = val;
+ }
+
+ inline void SetForkTests(bool val) {
+ ForkTests = val;
+ }
+
+ inline bool GetForkTests() const override {
+ return ForkTests;
+ }
+
+ inline void SetIsForked(bool val) {
+ IsForked = val;
+ SetIsTTY(IsForked || CalcIsTTY(stderr));
+ }
+
+ inline bool GetIsForked() const override {
+ return IsForked;
+ }
+
+ inline void SetLoop(bool loop) {
+ Loop = loop;
+ }
+
+ inline bool IsLoop() const {
+ return Loop;
+ }
+
+ inline void SetTraceProcessor(TAutoPtr<ITestSuiteProcessor> traceProcessor) {
+ TraceProcessor = traceProcessor;
+ }
+
+private:
+ void OnUnitStart(const TUnit* unit) override {
+ TraceProcessor->UnitStart(*unit);
+ if (IsForked) {
+ return;
+ }
+ if (PrintBeforeSuite_ || PrintBeforeTest_) {
+ fprintf(stderr, "%s<-----%s %s\n", LightBlueColor().data(), OldColor().data(), unit->name.data());
+ }
+ }
+
+ void OnUnitStop(const TUnit* unit) override {
+ TraceProcessor->UnitStop(*unit);
+ if (IsForked) {
+ return;
+ }
+ if (!PrintAfterSuite_) {
+ return;
+ }
+
+ fprintf(stderr, "%s----->%s %s -> ok: %s%u%s",
+ LightBlueColor().data(), OldColor().data(), unit->name.data(),
+ LightGreenColor().data(), GoodTestsInCurrentUnit(), OldColor().data());
+ if (FailTestsInCurrentUnit()) {
+ fprintf(stderr, ", err: %s%u%s",
+ LightRedColor().data(), FailTestsInCurrentUnit(), OldColor().data());
+ }
+ fprintf(stderr, "\n");
+ }
+
+ void OnBeforeTest(const TTest* test) override {
+ TraceProcessor->BeforeTest(*test);
+ if (IsForked) {
+ return;
+ }
+ if (PrintBeforeTest_) {
+ fprintf(stderr, "[%sexec%s] %s::%s...\n", LightBlueColor().data(), OldColor().data(), test->unit->name.data(), test->name);
+ }
+ }
+
+ void OnError(const TError* descr) override {
+ TraceProcessor->Error(*descr);
+ if (!IsForked && ForkExitedCorrectly) {
+ return;
+ }
+ if (!PrintAfterTest_) {
+ return;
+ }
+
+ const TString err = Sprintf("[%sFAIL%s] %s::%s -> %s%s%s\n%s%s%s", LightRedColor().data(), OldColor().data(),
+ descr->test->unit->name.data(),
+ descr->test->name,
+ LightRedColor().data(), descr->msg, OldColor().data(), LightCyanColor().data(), descr->BackTrace.data(), OldColor().data());
+ const TDuration test_duration = SaveTestDuration();
+ if (ShowFails) {
+ if (PrintTimes_) {
+ Fails.push_back(Sprintf("%s %s", test_duration.ToString().data(), err.data()));
+ } else {
+ Fails.push_back(err);
+ }
+ }
+ fprintf(stderr, "%s", err.data());
+ NOTE_IN_VALGRIND(descr->test);
+ PrintTimes(test_duration);
+ if (IsForked) {
+ fprintf(stderr, "%s", ForkCorrectExitMsg);
+ }
+ }
+
+ void OnFinish(const TFinish* descr) override {
+ TraceProcessor->Finish(*descr);
+ if (!IsForked && ForkExitedCorrectly) {
+ return;
+ }
+ if (!PrintAfterTest_) {
+ return;
+ }
+
+ if (descr->Success) {
+ fprintf(stderr, "[%sgood%s] %s::%s\n", LightGreenColor().data(), OldColor().data(),
+ descr->test->unit->name.data(),
+ descr->test->name);
+ NOTE_IN_VALGRIND(descr->test);
+ PrintTimes(SaveTestDuration());
+ if (IsForked) {
+ fprintf(stderr, "%s", ForkCorrectExitMsg);
+ }
+ }
+ }
+
+ inline TDuration SaveTestDuration() {
+ const TInstant now = TInstant::Now();
+ TDuration d = now - PrevTime_;
+ PrevTime_ = now;
+ return d;
+ }
+
+ inline void PrintTimes(TDuration d) {
+ if (!PrintTimes_) {
+ return;
+ }
+
+ Cerr << d << "\n";
+ }
+
+ void OnEnd() override {
+ TraceProcessor->End();
+ if (IsForked) {
+ return;
+ }
+
+ if (!PrintSummary_) {
+ return;
+ }
+
+ fprintf(stderr, "[%sDONE%s] ok: %s%u%s",
+ YellowColor().data(), OldColor().data(),
+ LightGreenColor().data(), GoodTests(), OldColor().data());
+ if (FailTests())
+ fprintf(stderr, ", err: %s%u%s",
+ LightRedColor().data(), FailTests(), OldColor().data());
+ fprintf(stderr, "\n");
+
+ if (ShowFails) {
+ for (size_t i = 0; i < Fails.size(); ++i) {
+ printf("%s", Fails[i].data());
+ }
+ }
+ }
+
+ bool CheckAccess(TString name, size_t num) override {
+ if (num < Start) {
+ return false;
+ }
+
+ if (num >= End) {
+ return false;
+ }
+
+ if (DisabledSuites_.find(name.data()) != DisabledSuites_.end()) {
+ return false;
+ }
+
+ if (EnabledSuites_.empty()) {
+ return true;
+ }
+
+ return EnabledSuites_.find(name.data()) != EnabledSuites_.end();
+ }
+
+ bool CheckAccessTest(TString suite, const char* test) override {
+ TString name = suite + "::" + test;
+ if (DisabledTests_.find(name) != DisabledTests_.end()) {
+ return false;
+ }
+
+ if (EnabledTests_.empty()) {
+ return true;
+ }
+
+ if (EnabledTests_.find(TString() + suite + "::*") != EnabledTests_.end()) {
+ return true;
+ }
+
+ return EnabledTests_.find(name) != EnabledTests_.end();
+ }
+
+ void Run(std::function<void()> f, const TString& suite, const char* name, const bool forceFork) override {
+ if (!(ForkTests || forceFork) || GetIsForked()) {
+ return f();
+ }
+
+ TList<TString> args(1, "--is-forked-internal");
+ args.push_back(Sprintf("+%s::%s", suite.data(), name));
+
+ // stdin is ignored - unittest should not need them...
+ TShellCommand cmd(AppName, args,
+ TShellCommandOptions().SetUseShell(false).SetCloseAllFdsOnExec(true).SetAsync(false).SetLatency(1));
+ cmd.Run();
+
+ const TString& err = cmd.GetError();
+ const size_t msgIndex = err.find(ForkCorrectExitMsg);
+
+ // everything is printed by parent process except test's result output ("good" or "fail")
+ // which is printed by child. If there was no output - parent process prints default message.
+ ForkExitedCorrectly = msgIndex != TString::npos;
+
+ // TODO: stderr output is always printed after stdout
+ Cout.Write(cmd.GetOutput());
+ Cerr.Write(err.c_str(), Min(msgIndex, err.size()));
+
+ // do not use default case, so gcc will warn if new element in enum will be added
+ switch (cmd.GetStatus()) {
+ case TShellCommand::SHELL_FINISHED: {
+ // test could fail with zero status if it calls exit(0) in the middle.
+ if (ForkExitedCorrectly)
+ break;
+ [[fallthrough]];
+ }
+ case TShellCommand::SHELL_ERROR: {
+ ythrow yexception() << "Forked test failed";
+ }
+
+ case TShellCommand::SHELL_NONE: {
+ ythrow yexception() << "Forked test finished with unknown status";
+ }
+ case TShellCommand::SHELL_RUNNING: {
+ Y_VERIFY(false, "This can't happen, we used sync mode, it's a bug!");
+ }
+ case TShellCommand::SHELL_INTERNAL_ERROR: {
+ ythrow yexception() << "Forked test failed with internal error: " << cmd.GetInternalError();
+ }
+ }
+ }
+
+private:
+ bool PrintBeforeSuite_;
+ bool PrintBeforeTest_;
+ bool PrintAfterTest_;
+ bool PrintAfterSuite_;
+ bool PrintTimes_;
+ bool PrintSummary_;
+ THashSet<TString> DisabledSuites_;
+ THashSet<TString> EnabledSuites_;
+ THashSet<TString> DisabledTests_;
+ THashSet<TString> EnabledTests_;
+ TInstant PrevTime_;
+ bool ShowFails;
+ TVector<TString> Fails;
+ size_t Start;
+ size_t End;
+ TString AppName;
+ bool ForkTests;
+ bool IsForked;
+ bool Loop;
+ static const char* const ForkCorrectExitMsg;
+ bool ForkExitedCorrectly;
+ TAutoPtr<ITestSuiteProcessor> TraceProcessor;
+};
+
+const char* const TColoredProcessor::ForkCorrectExitMsg = "--END--";
+
+class TEnumeratingProcessor: public ITestSuiteProcessor {
+public:
+ TEnumeratingProcessor(bool verbose, IOutputStream& stream) noexcept
+ : Verbose_(verbose)
+ , Stream_(stream)
+ {
+ }
+
+ ~TEnumeratingProcessor() override {
+ }
+
+ bool CheckAccess(TString name, size_t /*num*/) override {
+ if (Verbose_) {
+ return true;
+ } else {
+ Stream_ << name << "\n";
+ return false;
+ }
+ }
+
+ bool CheckAccessTest(TString suite, const char* name) override {
+ Stream_ << suite << "::" << name << "\n";
+ return false;
+ }
+
+private:
+ bool Verbose_;
+ IOutputStream& Stream_;
+};
+
+#ifdef _win_
+class TWinEnvironment {
+public:
+ TWinEnvironment()
+ : OutputCP(GetConsoleOutputCP())
+ {
+ setmode(fileno(stdout), _O_BINARY);
+ SetConsoleOutputCP(CP_UTF8);
+
+ _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
+
+ if (!IsDebuggerPresent()) {
+ _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG);
+ _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
+ _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG);
+ _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
+ }
+ }
+ ~TWinEnvironment() {
+ if (!IsDebuggerPresent()) {
+ _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
+ _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
+ }
+
+ SetConsoleOutputCP(OutputCP); // restore original output CP at program exit
+ }
+
+private:
+ UINT OutputCP; // original codepage
+};
+static const TWinEnvironment Instance;
+#endif // _win_
+
+static int DoList(bool verbose, IOutputStream& stream) {
+ TEnumeratingProcessor eproc(verbose, stream);
+ TTestFactory::Instance().SetProcessor(&eproc);
+ TTestFactory::Instance().Execute();
+ return 0;
+}
+
+static int DoUsage(const char* progname) {
+ Cout << "Usage: " << progname << " [options] [[+|-]test]...\n\n"
+ << "Options:\n"
+ << " -h, --help print this help message\n"
+ << " -l, --list print a list of available tests\n"
+ << " -A --list-verbose print a list of available subtests\n"
+ << " --print-before-test print each test name before running it\n"
+ << " --print-before-suite print each test suite name before running it\n"
+ << " --show-fails print a list of all failed tests at the end\n"
+ << " --dont-show-fails do not print a list of all failed tests at the end\n"
+ << " --continue-on-fail print a message and continue running test suite instead of break\n"
+ << " --print-times print wall clock duration of each test\n"
+ << " --fork-tests run each test in a separate process\n"
+ << " --trace-path path to the trace file to be generated\n"
+ << " --trace-path-append path to the trace file to be appended\n";
+ return 0;
+}
+
+#if defined(_linux_) && defined(CLANG_COVERAGE)
+extern "C" int __llvm_profile_write_file(void);
+
+static void GracefulShutdownHandler(int) {
+ try {
+ __llvm_profile_write_file();
+ } catch (...) {
+ }
+ abort();
+}
+#endif
+
+int NUnitTest::RunMain(int argc, char** argv) {
+#if defined(_linux_) && defined(CLANG_COVERAGE)
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = GracefulShutdownHandler;
+ sa.sa_flags = SA_SIGINFO | SA_RESTART;
+ Y_VERIFY(!sigaction(SIGUSR2, &sa, nullptr));
+ }
+#endif
+ NTesting::THook::CallBeforeInit();
+ InitNetworkSubSystem();
+
+ try {
+ GetExecPath();
+ } catch (...) {
+ }
+
+#ifndef UT_SKIP_EXCEPTIONS
+ try {
+#endif
+ NTesting::THook::CallBeforeRun();
+ Y_DEFER { NTesting::THook::CallAfterRun(); };
+
+ NPlugin::OnStartMain(argc, argv);
+ Y_DEFER { NPlugin::OnStopMain(argc, argv); };
+
+ TColoredProcessor processor(GetExecPath());
+ IOutputStream* listStream = &Cout;
+ THolder<IOutputStream> listFile;
+
+ enum EListType {
+ DONT_LIST,
+ LIST,
+ LIST_VERBOSE
+ };
+ EListType listTests = DONT_LIST;
+
+ for (size_t i = 1; i < (size_t)argc; ++i) {
+ const char* name = argv[i];
+
+ if (name && *name) {
+ if (strcmp(name, "--help") == 0 || strcmp(name, "-h") == 0) {
+ return DoUsage(argv[0]);
+ } else if (strcmp(name, "--list") == 0 || strcmp(name, "-l") == 0) {
+ listTests = LIST;
+ } else if (strcmp(name, "--list-verbose") == 0 || strcmp(name, "-A") == 0) {
+ listTests = LIST_VERBOSE;
+ } else if (strcmp(name, "--print-before-suite=false") == 0) {
+ processor.SetPrintBeforeSuite(false);
+ } else if (strcmp(name, "--print-before-test=false") == 0) {
+ processor.SetPrintBeforeTest(false);
+ } else if (strcmp(name, "--print-before-suite") == 0) {
+ processor.SetPrintBeforeSuite(true);
+ } else if (strcmp(name, "--print-before-test") == 0) {
+ processor.SetPrintBeforeTest(true);
+ } else if (strcmp(name, "--show-fails") == 0) {
+ processor.SetShowFails(true);
+ } else if (strcmp(name, "--dont-show-fails") == 0) {
+ processor.SetShowFails(false);
+ } else if (strcmp(name, "--continue-on-fail") == 0) {
+ processor.SetContinueOnFail(true);
+ } else if (strcmp(name, "--print-times") == 0) {
+ processor.SetPrintTimes(true);
+ } else if (strcmp(name, "--from") == 0) {
+ ++i;
+ processor.SetStart(FromString<size_t>(argv[i]));
+ } else if (strcmp(name, "--to") == 0) {
+ ++i;
+ processor.SetEnd(FromString<size_t>(argv[i]));
+ } else if (strcmp(name, "--fork-tests") == 0) {
+ processor.SetForkTests(true);
+ } else if (strcmp(name, "--is-forked-internal") == 0) {
+ processor.SetIsForked(true);
+ } else if (strcmp(name, "--loop") == 0) {
+ processor.SetLoop(true);
+ } else if (strcmp(name, "--trace-path") == 0) {
+ ++i;
+ processor.BeQuiet();
+ NUnitTest::ShouldColorizeDiff = false;
+ processor.SetTraceProcessor(new TTraceWriterProcessor(argv[i], CreateAlways));
+ } else if (strcmp(name, "--trace-path-append") == 0) {
+ ++i;
+ processor.BeQuiet();
+ NUnitTest::ShouldColorizeDiff = false;
+ processor.SetTraceProcessor(new TTraceWriterProcessor(argv[i], OpenAlways | ForAppend));
+ } else if (strcmp(name, "--list-path") == 0) {
+ ++i;
+ listFile = MakeHolder<TFixedBufferFileOutput>(argv[i]);
+ listStream = listFile.Get();
+ } else if (strcmp(name, "--test-param") == 0) {
+ ++i;
+ TString param(argv[i]);
+ size_t assign = param.find('=');
+ Singleton<::NPrivate::TTestEnv>()->AddTestParam(param.substr(0, assign), param.substr(assign + 1));
+ } else if (TString(name).StartsWith("--")) {
+ return DoUsage(argv[0]), 1;
+ } else if (*name == '-') {
+ processor.Disable(name + 1);
+ } else if (*name == '+') {
+ processor.Enable(name + 1);
+ } else {
+ processor.Enable(name);
+ }
+ }
+ }
+ if (listTests != DONT_LIST) {
+ return DoList(listTests == LIST_VERBOSE, *listStream);
+ }
+
+ TTestFactory::Instance().SetProcessor(&processor);
+
+ unsigned ret;
+ for (;;) {
+ ret = TTestFactory::Instance().Execute();
+ if (!processor.GetIsForked() && ret && processor.GetPrintSummary()) {
+ Cerr << "SOME TESTS FAILED!!!!" << Endl;
+ }
+
+ if (0 != ret || !processor.IsLoop()) {
+ break;
+ }
+ }
+ return ret;
+#ifndef UT_SKIP_EXCEPTIONS
+ } catch (...) {
+ Cerr << "caught exception in test suite(" << CurrentExceptionMessage() << ")" << Endl;
+ }
+#endif
+
+ return 1;
+}
diff --git a/library/cpp/testing/unittest/utmain.h b/library/cpp/testing/unittest/utmain.h
new file mode 100644
index 0000000000..65e8082ee1
--- /dev/null
+++ b/library/cpp/testing/unittest/utmain.h
@@ -0,0 +1,5 @@
+#pragma once
+
+namespace NUnitTest {
+ int RunMain(int argc, char** argv);
+}
diff --git a/library/cpp/testing/unittest/ya.make b/library/cpp/testing/unittest/ya.make
new file mode 100644
index 0000000000..aaa4f2ba85
--- /dev/null
+++ b/library/cpp/testing/unittest/ya.make
@@ -0,0 +1,33 @@
+LIBRARY()
+
+PROVIDES(test_framework)
+
+OWNER(
+ pg
+ galaxycrab
+)
+
+PEERDIR(
+ library/cpp/colorizer
+ library/cpp/dbg_output
+ library/cpp/diff
+ library/cpp/json/writer
+ library/cpp/testing/common
+ library/cpp/testing/hook
+)
+
+SRCS(
+ gtest.cpp
+ checks.cpp
+ plugin.cpp
+ registar.cpp
+ tests_data.cpp
+ utmain.cpp
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ fat
+ ut
+)
diff --git a/library/cpp/testing/unittest_main/main.cpp b/library/cpp/testing/unittest_main/main.cpp
new file mode 100644
index 0000000000..fc5d2d9418
--- /dev/null
+++ b/library/cpp/testing/unittest_main/main.cpp
@@ -0,0 +1,5 @@
+#include <library/cpp/testing/unittest/utmain.h>
+
+int main(int argc, char** argv) {
+ return NUnitTest::RunMain(argc, argv);
+}
diff --git a/library/cpp/testing/unittest_main/ya.make b/library/cpp/testing/unittest_main/ya.make
new file mode 100644
index 0000000000..80a6cc699b
--- /dev/null
+++ b/library/cpp/testing/unittest_main/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+OWNER(pg)
+
+PEERDIR(
+ library/cpp/testing/unittest
+ library/cpp/terminate_handler
+)
+
+SRCS(
+ main.cpp
+)
+
+END()
diff --git a/library/cpp/testing/ya.make b/library/cpp/testing/ya.make
new file mode 100644
index 0000000000..6a57ac2ee6
--- /dev/null
+++ b/library/cpp/testing/ya.make
@@ -0,0 +1,24 @@
+RECURSE(
+ common
+ benchmark
+ benchmark/examples
+ benchmark/examples/metrics
+ benchmark/main
+ boost_test
+ boost_test_main
+ dump_clang_coverage
+ gbenchmark_main
+ gmock
+ gmock_in_unittest
+ gmock_in_unittest/example_ut
+ gtest
+ gtest_boost_extensions
+ gtest_extensions
+ gtest_main
+ gtest_protobuf
+ hook
+ mock_server
+ unittest
+ unittest_main
+ urlnorm
+)