diff options
author | Devtools Arcadia <arcadia-devtools@yandex-team.ru> | 2022-02-07 18:08:42 +0300 |
---|---|---|
committer | Devtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net> | 2022-02-07 18:08:42 +0300 |
commit | 1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch) | |
tree | e26c9fed0de5d9873cce7e00bc214573dc2195b7 /library/cpp/testing/unittest | |
download | ydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz |
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'library/cpp/testing/unittest')
20 files changed, 3350 insertions, 0 deletions
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 +) |