aboutsummaryrefslogtreecommitdiffstats
path: root/library/cpp/testing
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
parent03f024c4412e3aa613bb543cf1660176320ba8f4 (diff)
downloadydb-06e5c21a835c0e923506c4ff27929f34e00761c2.tar.gz
fix ya.make
Diffstat (limited to 'library/cpp/testing')
-rw-r--r--library/cpp/testing/gbenchmark_main/main.cpp16
-rw-r--r--library/cpp/testing/gtest/README.md9
-rw-r--r--library/cpp/testing/gtest/gtest.cpp12
-rw-r--r--library/cpp/testing/gtest/gtest.h37
-rw-r--r--library/cpp/testing/gtest/main.cpp427
-rw-r--r--library/cpp/testing/gtest/main.h95
-rw-r--r--library/cpp/testing/gtest_main/README.md3
-rw-r--r--library/cpp/testing/gtest_main/main.cpp5
8 files changed, 604 insertions, 0 deletions
diff --git a/library/cpp/testing/gbenchmark_main/main.cpp b/library/cpp/testing/gbenchmark_main/main.cpp
new file mode 100644
index 0000000000..523d18901a
--- /dev/null
+++ b/library/cpp/testing/gbenchmark_main/main.cpp
@@ -0,0 +1,16 @@
+#include <benchmark/benchmark.h>
+
+#include <library/cpp/testing/hook/hook.h>
+#include <util/generic/scope.h>
+
+int main(int argc, char** argv) {
+ NTesting::THook::CallBeforeInit();
+ ::benchmark::Initialize(&argc, argv);
+ if (::benchmark::ReportUnrecognizedArguments(argc, argv)) {
+ return 1;
+ }
+ NTesting::THook::CallBeforeRun();
+ Y_DEFER { NTesting::THook::CallAfterRun(); };
+ ::benchmark::RunSpecifiedBenchmarks();
+ return 0;
+}
diff --git a/library/cpp/testing/gtest/README.md b/library/cpp/testing/gtest/README.md
new file mode 100644
index 0000000000..3ddf593462
--- /dev/null
+++ b/library/cpp/testing/gtest/README.md
@@ -0,0 +1,9 @@
+# Gtest support in Arcadia
+
+Gtest wrapper that reports results exactly how Arcadia CI wants it.
+
+How to use:
+
+- use `GTEST` in your `ya.make`;
+- include `gtest.h` from this library. Don't include `<gtest/gtest.h>` and `<gmock/gmock.h>` directly because then you'll not get our extensions, including pretty printers for util types;
+- write tests and enjoy.
diff --git a/library/cpp/testing/gtest/gtest.cpp b/library/cpp/testing/gtest/gtest.cpp
new file mode 100644
index 0000000000..4c7a9e8f5e
--- /dev/null
+++ b/library/cpp/testing/gtest/gtest.cpp
@@ -0,0 +1,12 @@
+#include "gtest.h"
+
+#include <library/cpp/testing/common/env.h>
+
+std::optional<std::string_view> NGTest::GetTestParam(std::string_view name) {
+ auto val = ::GetTestParam(name);
+ if (val.empty()) {
+ return {};
+ } else {
+ return {val};
+ }
+}
diff --git a/library/cpp/testing/gtest/gtest.h b/library/cpp/testing/gtest/gtest.h
new file mode 100644
index 0000000000..4ef30c3ada
--- /dev/null
+++ b/library/cpp/testing/gtest/gtest.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <library/cpp/testing/gtest_extensions/gtest_extensions.h>
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+#include <optional>
+#include <string_view>
+
+/**
+ * Bridge between GTest framework and Arcadia CI.
+ */
+namespace NGTest {
+ /**
+ * Get custom test parameter.
+ *
+ * You can pass custom parameters to your test using the `--test-param` flag:
+ *
+ * ```
+ * $ ya make -t --test-param NAME=VALUE
+ * ```
+ *
+ * You can later access these parameters from tests using this function:
+ *
+ * ```
+ * TEST(Suite, Name) {
+ * EXPECT_EQ(GetTestParam("NAME").value_or("NOT_SET"), "VALUE");
+ * }
+ * ```
+ *
+ * @param name name of the parameter.
+ * @return value of the parameter, as passed to the program arguments,
+ * or nothing, if parameter not found.
+ */
+ std::optional<std::string_view> GetTestParam(std::string_view name);
+}
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});
+}
diff --git a/library/cpp/testing/gtest/main.h b/library/cpp/testing/gtest/main.h
new file mode 100644
index 0000000000..ee025d52da
--- /dev/null
+++ b/library/cpp/testing/gtest/main.h
@@ -0,0 +1,95 @@
+#pragma once
+
+#include <iosfwd>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+
+/**
+ * You need to use these functions if you're customizing tests initialization
+ * or writing a custom `main`.
+ */
+
+namespace NGTest {
+ /**
+ * Default `main` implementation.
+ */
+ int Main(int argc, char** argv);
+
+ /**
+ * CLI parsing result.
+ */
+ struct TFlags {
+ /**
+ * Argument for `ListTests` function.
+ */
+ int ListLevel = 0;
+
+ /**
+ * Where to print listed tests. Empty string means print to `stdout`.
+ */
+ std::string ListPath = "";
+
+ /**
+ * Path to trace file. If empty, tracing is not enabled.
+ */
+ std::string TracePath = "";
+
+ /**
+ * Should trace file be opened for append rather than just write.
+ */
+ bool AppendTrace = false;
+
+ /**
+ * Test filters.
+ */
+ std::string Filter = "*";
+
+ /**
+ * Number of CLI arguments for GTest init function (not counting the last null one).
+ */
+ int GtestArgc = 0;
+
+ /**
+ * CLI arguments for GTest init function.
+ * The last one is nullptr.
+ */
+ std::vector<char*> GtestArgv = {};
+ };
+
+ /**
+ * Parse unittest-related flags. Test binaries support flags from `library/cpp/testing/unittest` and flags from gtest.
+ * This means that there are usually two parsing passes. The first one parses arguments as recognized
+ * by the `library/cpp/testing/unittest`, so things like `--trace-path` and filters. The second one parses flags
+ * as recognized by gtest.
+ */
+ TFlags ParseFlags(int argc, char** argv);
+
+ /**
+ * List tests using the unittest style and exit.
+ *
+ * This function should be called after initializing google tests because test parameters are instantiated
+ * during initialization.
+ *
+ * @param listLevel verbosity of test list. `0` means don't print anything and don't exit, `1` means print
+ * test suites, `2` means print individual tests.
+ */
+ void ListTests(int listLevel, const std::string& listPath);
+
+ /**
+ * Remove default result reporter, the one that prints to stdout.
+ */
+ void UnsetDefaultReporter();
+
+ /**
+ * Set trace reporter.
+ *
+ * Trace files are used by arcadia CI to interact with test runner. They consist of JSON objects, one per line.
+ * Each object represents an event, such as 'test started' or 'test finished'.
+ *
+ * @param traceFile where to write trace file. This stream should exist for the entire duration of test run.
+ */
+ void SetTraceReporter(std::ostream* traceFile);
+}
diff --git a/library/cpp/testing/gtest_main/README.md b/library/cpp/testing/gtest_main/README.md
new file mode 100644
index 0000000000..5c454e1d29
--- /dev/null
+++ b/library/cpp/testing/gtest_main/README.md
@@ -0,0 +1,3 @@
+# Glue for `GTEST` macro
+
+Provides `main` function. This library is automatically linked into any test binary that uses `GTEST`. This way, you don't have to implement `main` yourself every time you write a test target.
diff --git a/library/cpp/testing/gtest_main/main.cpp b/library/cpp/testing/gtest_main/main.cpp
new file mode 100644
index 0000000000..eea70dec54
--- /dev/null
+++ b/library/cpp/testing/gtest_main/main.cpp
@@ -0,0 +1,5 @@
+#include <library/cpp/testing/gtest/main.h>
+
+int main(int argc, char** argv) {
+ return NGTest::Main(argc, argv);
+}