aboutsummaryrefslogtreecommitdiffstats
path: root/library/cpp/testing/gtest/main.cpp
diff options
context:
space:
mode:
authormonster <monster@ydb.tech>2022-07-07 14:41:37 +0300
committermonster <monster@ydb.tech>2022-07-07 14:41:37 +0300
commit06e5c21a835c0e923506c4ff27929f34e00761c2 (patch)
tree75efcbc6854ef9bd476eb8bf00cc5c900da436a2 /library/cpp/testing/gtest/main.cpp
parent03f024c4412e3aa613bb543cf1660176320ba8f4 (diff)
downloadydb-06e5c21a835c0e923506c4ff27929f34e00761c2.tar.gz
fix ya.make
Diffstat (limited to 'library/cpp/testing/gtest/main.cpp')
-rw-r--r--library/cpp/testing/gtest/main.cpp427
1 files changed, 427 insertions, 0 deletions
diff --git a/library/cpp/testing/gtest/main.cpp b/library/cpp/testing/gtest/main.cpp
new file mode 100644
index 0000000000..38504b5690
--- /dev/null
+++ b/library/cpp/testing/gtest/main.cpp
@@ -0,0 +1,427 @@
+#include "main.h"
+#include "gtest.h"
+
+#include <library/cpp/string_utils/relaxed_escaper/relaxed_escaper.h>
+#include <library/cpp/testing/common/env.h>
+#include <library/cpp/testing/hook/hook.h>
+#include <util/generic/scope.h>
+#include <util/string/join.h>
+#include <util/system/src_root.h>
+
+#include <fstream>
+
+namespace {
+ bool StartsWith(const char* str, const char* pre) {
+ return strncmp(pre, str, strlen(pre)) == 0;
+ }
+
+ void Unsupported(const char* flag) {
+ std::cerr << "This GTest wrapper does not support flag " << flag << std::endl;
+ exit(2);
+ }
+
+ void Unknown(const char* flag) {
+ std::cerr << "Unknown support flag " << flag << std::endl;
+ exit(2);
+ }
+
+ std::pair<std::string_view, std::string_view> ParseName(std::string_view name) {
+ auto pos = name.find("::");
+ if (pos == std::string_view::npos) {
+ return {name, "*"};
+ } else {
+ return {name.substr(0, pos), name.substr(pos + 2, name.size())};
+ }
+ }
+
+ std::pair<std::string_view, std::string_view> ParseParam(std::string_view param) {
+ auto pos = param.find("=");
+ if (pos == std::string_view::npos) {
+ return {param, ""};
+ } else {
+ return {param.substr(0, pos), param.substr(pos + 1, param.size())};
+ }
+ }
+
+ constexpr std::string_view StripRoot(std::string_view f) noexcept {
+ return ::NPrivate::StripRoot(::NPrivate::TStaticBuf(f.data(), f.size())).As<std::string_view>();
+ }
+
+ std::string EscapeJson(std::string_view str) {
+ TString result;
+ NEscJ::EscapeJ<true, true>(str, result);
+ return result;
+ }
+
+ class TTraceWriter: public ::testing::EmptyTestEventListener {
+ public:
+ explicit TTraceWriter(std::ostream* trace)
+ : Trace_(trace)
+ {
+ }
+
+ private:
+ void OnTestProgramStart(const testing::UnitTest& test) override {
+ auto ts = std::chrono::duration_cast<std::chrono::duration<double>>(
+ std::chrono::system_clock::now().time_since_epoch());
+
+ for (int i = 0; i < test.total_test_suite_count(); ++i) {
+ auto suite = test.GetTestSuite(i);
+ for (int j = 0; j < suite->total_test_count(); ++j) {
+ auto testInfo = suite->GetTestInfo(j);
+ if (testInfo->is_reportable() && !testInfo->should_run()) {
+ PrintTestStatus(*testInfo, "skipped", "test is disabled", {}, ts);
+ }
+ }
+ }
+ }
+
+ void OnTestStart(const ::testing::TestInfo& testInfo) override {
+ // We fully format this marker before printing it to stderr/stdout because we want to print it atomically.
+ // If we were to write `std::cout << "\n###subtest-finished:" << name`, there would be a chance that
+ // someone else could sneak in and print something between `"\n###subtest-finished"` and `name`
+ // (this happens when test binary uses both `Cout` and `std::cout`).
+ auto marker = Join("", "\n###subtest-started:", testInfo.test_suite_name(), "::", testInfo.name(), "\n");
+
+ // Theoretically, we don't need to flush both `Cerr` and `std::cerr` here because both ultimately
+ // result in calling `fflush(stderr)`. However, there may be additional buffering logic
+ // going on (custom `std::cerr.tie()`, for example), so just to be sure, we flush both of them.
+ std::cout << std::flush;
+ Cout << marker << Flush;
+
+ std::cerr << std::flush;
+ Cerr << marker << Flush;
+
+ auto ts = std::chrono::duration_cast<std::chrono::duration<double>>(
+ std::chrono::system_clock::now().time_since_epoch());
+
+ (*Trace_)
+ << "{"
+ << "\"name\": \"subtest-started\", "
+ << "\"timestamp\": " << std::setprecision(14) << ts.count() << ", "
+ << "\"value\": {"
+ << "\"class\": " << EscapeJson(testInfo.test_suite_name()) << ", "
+ << "\"subtest\": " << EscapeJson(testInfo.name())
+ << "}"
+ << "}"
+ << "\n"
+ << std::flush;
+ }
+
+ void OnTestPartResult(const testing::TestPartResult& result) override {
+ if (!result.passed()) {
+ if (result.file_name()) {
+ std::cerr << StripRoot(result.file_name()) << ":" << result.line_number() << ":" << "\n";
+ }
+ std::cerr << result.message() << "\n";
+ std::cerr << std::flush;
+ }
+ }
+
+ void OnTestEnd(const ::testing::TestInfo& testInfo) override {
+ auto ts = std::chrono::duration_cast<std::chrono::duration<double>>(
+ std::chrono::system_clock::now().time_since_epoch());
+
+ std::string_view status = "good";
+ if (testInfo.result()->Failed()) {
+ status = "fail";
+ } else if (testInfo.result()->Skipped()) {
+ status = "skipped";
+ }
+
+ std::ostringstream messages;
+ std::unordered_map<std::string, double> properties;
+
+ {
+ if (testInfo.value_param()) {
+ messages << "Value param:\n " << testInfo.value_param() << "\n";
+ }
+
+ if (testInfo.type_param()) {
+ messages << "Type param:\n " << testInfo.type_param() << "\n";
+ }
+
+ std::string_view sep;
+ for (int i = 0; i < testInfo.result()->total_part_count(); ++i) {
+ auto part = testInfo.result()->GetTestPartResult(i);
+ if (part.failed()) {
+ messages << sep;
+ if (part.file_name()) {
+ messages << StripRoot(part.file_name()) << ":" << part.line_number() << ":\n";
+ }
+ messages << part.message();
+ messages << "\n";
+ sep = "\n";
+ }
+ }
+
+ for (int i = 0; i < testInfo.result()->test_property_count(); ++i) {
+ auto& property = testInfo.result()->GetTestProperty(i);
+
+ double value;
+
+ try {
+ value = std::stod(property.value());
+ } catch (std::invalid_argument&) {
+ messages
+ << sep
+ << "Arcadia CI only supports numeric properties, property "
+ << property.key() << "=" << EscapeJson(property.value()) << " is not a number\n";
+ std::cerr
+ << "Arcadia CI only supports numeric properties, property "
+ << property.key() << "=" << EscapeJson(property.value()) << " is not a number\n"
+ << std::flush;
+ status = "fail";
+ sep = "\n";
+ continue;
+ } catch (std::out_of_range&) {
+ messages
+ << sep
+ << "Property " << property.key() << "=" << EscapeJson(property.value())
+ << " is too big for a double precision value\n";
+ std::cerr
+ << "Property " << property.key() << "=" << EscapeJson(property.value())
+ << " is too big for a double precision value\n"
+ << std::flush;
+ status = "fail";
+ sep = "\n";
+ continue;
+ }
+
+ properties[property.key()] = value;
+ }
+ }
+
+ auto marker = Join("", "\n###subtest-finished:", testInfo.test_suite_name(), "::", testInfo.name(), "\n");
+
+ std::cout << std::flush;
+ Cout << marker << Flush;
+
+ std::cerr << std::flush;
+ Cerr << marker << Flush;
+
+ PrintTestStatus(testInfo, status, messages.str(), properties, ts);
+ }
+
+ void PrintTestStatus(
+ const ::testing::TestInfo& testInfo,
+ std::string_view status,
+ std::string_view messages,
+ const std::unordered_map<std::string, double>& properties,
+ std::chrono::duration<double> ts)
+ {
+ (*Trace_)
+ << "{"
+ << "\"name\": \"subtest-finished\", "
+ << "\"timestamp\": " << std::setprecision(14) << ts.count() << ", "
+ << "\"value\": {"
+ << "\"class\": " << EscapeJson(testInfo.test_suite_name()) << ", "
+ << "\"subtest\": " << EscapeJson(testInfo.name()) << ", "
+ << "\"comment\": " << EscapeJson(messages) << ", "
+ << "\"status\": " << EscapeJson(status) << ", "
+ << "\"time\": " << (testInfo.result()->elapsed_time() * (1 / 1000.0)) << ", "
+ << "\"metrics\": {";
+ {
+ std::string_view sep = "";
+ for (auto& [key, value]: properties) {
+ (*Trace_) << sep << EscapeJson(key) << ": " << value;
+ sep = ", ";
+ }
+ }
+ (*Trace_)
+ << "}"
+ << "}"
+ << "}"
+ << "\n"
+ << std::flush;
+ }
+
+ std::ostream* Trace_;
+ };
+}
+
+int NGTest::Main(int argc, char** argv) {
+ auto flags = ParseFlags(argc, argv);
+
+ ::testing::GTEST_FLAG(filter) = flags.Filter;
+
+ std::ofstream trace;
+ if (!flags.TracePath.empty()) {
+ trace.open(flags.TracePath, (flags.AppendTrace ? std::ios::app : std::ios::out) | std::ios::binary);
+
+ if (!trace.is_open()) {
+ std::cerr << "Failed to open file " << flags.TracePath << " for write" << std::endl;
+ exit(2);
+ }
+
+ UnsetDefaultReporter();
+ SetTraceReporter(&trace);
+ }
+
+ NTesting::THook::CallBeforeInit();
+
+ ::testing::InitGoogleMock(&flags.GtestArgc, flags.GtestArgv.data());
+
+ ListTests(flags.ListLevel, flags.ListPath);
+
+ NTesting::THook::CallBeforeRun();
+
+ Y_DEFER { NTesting::THook::CallAfterRun(); };
+
+ return RUN_ALL_TESTS();
+}
+
+NGTest::TFlags NGTest::ParseFlags(int argc, char** argv) {
+ TFlags result;
+
+ std::ostringstream filtersPos;
+ std::string_view filterPosSep = "";
+ std::ostringstream filtersNeg;
+ std::string_view filterNegSep = "";
+
+ if (argc > 0) {
+ result.GtestArgv.push_back(argv[0]);
+ }
+
+ for (int i = 1; i < argc; ++i) {
+ auto name = argv[i];
+
+ if (strcmp(name, "--help") == 0) {
+ result.GtestArgv.push_back(name);
+ break;
+ } else if (StartsWith(name, "--gtest_") || StartsWith(name, "--gmock_")) {
+ result.GtestArgv.push_back(name);
+ } else if (strcmp(name, "--list") == 0 || strcmp(name, "-l") == 0) {
+ result.ListLevel = std::max(result.ListLevel, 1);
+ } else if (strcmp(name, "--list-verbose") == 0) {
+ result.ListLevel = std::max(result.ListLevel, 2);
+ } else if (strcmp(name, "--print-before-suite") == 0) {
+ Unsupported("--print-before-suite");
+ } else if (strcmp(name, "--print-before-test") == 0) {
+ Unsupported("--print-before-test");
+ } else if (strcmp(name, "--show-fails") == 0) {
+ Unsupported("--show-fails");
+ } else if (strcmp(name, "--dont-show-fails") == 0) {
+ Unsupported("--dont-show-fails");
+ } else if (strcmp(name, "--print-times") == 0) {
+ Unsupported("--print-times");
+ } else if (strcmp(name, "--from") == 0) {
+ Unsupported("--from");
+ } else if (strcmp(name, "--to") == 0) {
+ Unsupported("--to");
+ } else if (strcmp(name, "--fork-tests") == 0) {
+ Unsupported("--fork-tests");
+ } else if (strcmp(name, "--is-forked-internal") == 0) {
+ Unsupported("--is-forked-internal");
+ } else if (strcmp(name, "--loop") == 0) {
+ Unsupported("--loop");
+ } else if (strcmp(name, "--trace-path") == 0 || strcmp(name, "--trace-path-append") == 0) {
+ ++i;
+
+ if (i >= argc) {
+ std::cerr << "Missing value for argument --trace-path" << std::endl;
+ exit(2);
+ } else if (!result.TracePath.empty()) {
+ std::cerr << "Multiple --trace-path or --trace-path-append given" << std::endl;
+ exit(2);
+ }
+
+ result.TracePath = argv[i];
+ result.AppendTrace = strcmp(name, "--trace-path-append") == 0;
+ } else if (strcmp(name, "--list-path") == 0) {
+ ++i;
+
+ if (i >= argc) {
+ std::cerr << "Missing value for argument --list-path" << std::endl;
+ exit(2);
+ }
+
+ result.ListPath = argv[i];
+ } else if (strcmp(name, "--test-param") == 0) {
+ ++i;
+
+ if (i >= argc) {
+ std::cerr << "Missing value for argument --test-param" << std::endl;
+ exit(2);
+ }
+
+ auto [key, value] = ParseParam(argv[i]);
+
+ Singleton<NPrivate::TTestEnv>()->AddTestParam(key, value);
+ } else if (StartsWith(name, "--")) {
+ Unknown(name);
+ } else if (*name == '-') {
+ auto [suite, test] = ParseName(name + 1);
+ filtersNeg << filterNegSep << suite << "." << test;
+ filterNegSep = ":";
+ } else if (*name == '+') {
+ auto [suite, test] = ParseName(name + 1);
+ filtersPos << filterPosSep << suite << "." << test;
+ filterPosSep = ":";
+ } else {
+ auto [suite, test] = ParseName(name);
+ filtersPos << filterPosSep << suite << "." << test;
+ filterPosSep = ":";
+ }
+ }
+
+ if (!filtersPos.str().empty() || !filtersNeg.str().empty()) {
+ result.Filter = filtersPos.str();
+ if (!filtersNeg.str().empty()) {
+ result.Filter += "-";
+ result.Filter += filtersNeg.str();
+ }
+ }
+
+ // Main-like functions need a null sentinel at the end of `argv' argument.
+ // This sentinel is not counted in `argc' argument.
+ result.GtestArgv.push_back(nullptr);
+ result.GtestArgc = static_cast<int>(result.GtestArgv.size()) - 1;
+
+ return result;
+}
+
+void NGTest::ListTests(int listLevel, const std::string& listPath) {
+ // NOTE: do not use `std::endl`, use `\n`; `std::endl` produces `\r\n`s on windows,
+ // and ya make does not handle them well.
+
+ if (listLevel > 0) {
+ std::ostream* listOut = &std::cout;
+ std::ofstream listFile;
+
+ if (!listPath.empty()) {
+ listFile.open(listPath, std::ios::out | std::ios::binary);
+ if (!listFile.is_open()) {
+ std::cerr << "Failed to open file " << listPath << " for write" << std::endl;
+ exit(2);
+ }
+ listOut = &listFile;
+ }
+
+ for (int i = 0; i < testing::UnitTest::GetInstance()->total_test_suite_count(); ++i) {
+ auto suite = testing::UnitTest::GetInstance()->GetTestSuite(i);
+ if (listLevel > 1) {
+ for (int j = 0; j < suite->total_test_count(); ++j) {
+ auto test = suite->GetTestInfo(j);
+ (*listOut) << suite->name() << "::" << test->name() << "\n";
+ }
+ } else {
+ (*listOut) << suite->name() << "\n";
+ }
+ }
+
+ (*listOut) << std::flush;
+
+ exit(0);
+ }
+}
+
+void NGTest::UnsetDefaultReporter() {
+ ::testing::TestEventListeners& listeners = ::testing::UnitTest::GetInstance()->listeners();
+ delete listeners.Release(listeners.default_result_printer());
+}
+
+void NGTest::SetTraceReporter(std::ostream* traceFile) {
+ ::testing::TestEventListeners& listeners = ::testing::UnitTest::GetInstance()->listeners();
+ listeners.Append(new TTraceWriter{traceFile});
+}