diff options
author | monster <monster@ydb.tech> | 2022-07-07 14:41:37 +0300 |
---|---|---|
committer | monster <monster@ydb.tech> | 2022-07-07 14:41:37 +0300 |
commit | 06e5c21a835c0e923506c4ff27929f34e00761c2 (patch) | |
tree | 75efcbc6854ef9bd476eb8bf00cc5c900da436a2 /library/cpp/testing | |
parent | 03f024c4412e3aa613bb543cf1660176320ba8f4 (diff) | |
download | ydb-06e5c21a835c0e923506c4ff27929f34e00761c2.tar.gz |
fix ya.make
Diffstat (limited to 'library/cpp/testing')
-rw-r--r-- | library/cpp/testing/gbenchmark_main/main.cpp | 16 | ||||
-rw-r--r-- | library/cpp/testing/gtest/README.md | 9 | ||||
-rw-r--r-- | library/cpp/testing/gtest/gtest.cpp | 12 | ||||
-rw-r--r-- | library/cpp/testing/gtest/gtest.h | 37 | ||||
-rw-r--r-- | library/cpp/testing/gtest/main.cpp | 427 | ||||
-rw-r--r-- | library/cpp/testing/gtest/main.h | 95 | ||||
-rw-r--r-- | library/cpp/testing/gtest_main/README.md | 3 | ||||
-rw-r--r-- | library/cpp/testing/gtest_main/main.cpp | 5 |
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); +} |