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/utmain.cpp | |
download | ydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz |
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'library/cpp/testing/unittest/utmain.cpp')
-rw-r--r-- | library/cpp/testing/unittest/utmain.cpp | 771 |
1 files changed, 771 insertions, 0 deletions
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; +} |