// Copyright Epic Games, Inc. All Rights Reserved. #include "zencore/testing.h" #include "zencore/logging.h" #if ZEN_WITH_TESTS # include # include # include # include # include # include namespace zen::testing { using namespace std::literals; struct TestListener : public doctest::IReporter { const std::string_view ColorYellow = "\033[0;33m"sv; const std::string_view ColorNone = "\033[0m"sv; // constructor has to accept the ContextOptions by ref as a single argument TestListener(const doctest::ContextOptions&) {} void report_query(const doctest::QueryData& /*in*/) override {} void test_run_start() override { RunStart = std::chrono::steady_clock::now(); } void test_run_end(const doctest::TestRunStats& in) override { auto elapsed = std::chrono::steady_clock::now() - RunStart; double elapsedSeconds = std::chrono::duration_cast(elapsed).count() / 1000.0; // Write machine-readable summary to file if requested (used by xmake test summary table) const char* summaryFile = std::getenv("ZEN_TEST_SUMMARY_FILE"); if (summaryFile && summaryFile[0] != '\0') { if (FILE* f = std::fopen(summaryFile, "w")) { std::fprintf(f, "cases_total=%u\ncases_passed=%u\nassertions_total=%d\nassertions_passed=%d\n" "elapsed_seconds=%.3f\n", in.numTestCasesPassingFilters, in.numTestCasesPassingFilters - in.numTestCasesFailed, in.numAsserts, in.numAsserts - in.numAssertsFailed, elapsedSeconds); for (const auto& failure : FailedTests) { std::fprintf(f, "failed=%s|%s|%u\n", failure.Name.c_str(), failure.File.c_str(), failure.Line); } std::fclose(f); } } } void test_case_start(const doctest::TestCaseData& in) override { Current = ∈ ZEN_CONSOLE("{}======== TEST_CASE: {:<50} ========{}", ColorYellow, Current->m_name, ColorNone); } // called when a test case is reentered because of unfinished subcases void test_case_reenter(const doctest::TestCaseData& /*in*/) override { ZEN_CONSOLE("{}-------------------------------------------------------------------------------{}", ColorYellow, ColorNone); } void test_case_end(const doctest::CurrentTestCaseStats& in) override { if (!in.testCaseSuccess && Current) { FailedTests.push_back({Current->m_name, Current->m_file.c_str(), Current->m_line}); } Current = nullptr; } void test_case_exception(const doctest::TestCaseException& /*in*/) override {} void subcase_start(const doctest::SubcaseSignature& in) override { ZEN_CONSOLE("{}-------- SUBCASE: {:<50} --------{}", ColorYellow, fmt::format("{}/{}", Current->m_name, in.m_name.c_str()), ColorNone); } void subcase_end() override {} void log_assert(const doctest::AssertData& /*in*/) override {} void log_message(const doctest::MessageData& /*in*/) override {} void test_case_skipped(const doctest::TestCaseData& /*in*/) override {} const doctest::TestCaseData* Current = nullptr; std::chrono::steady_clock::time_point RunStart = {}; struct FailedTestInfo { std::string Name; std::string File; unsigned Line; }; std::vector FailedTests; }; struct TestRunner::Impl { Impl() { REGISTER_LISTENER("ZenTestListener", 1, TestListener); } doctest::Context Session; }; TestRunner::TestRunner() { m_Impl = std::make_unique(); } TestRunner::~TestRunner() { } int TestRunner::ApplyCommandLine(int argc, char const* const* argv) { m_Impl->Session.applyCommandLine(argc, argv); for (int i = 1; i < argc; ++i) { if (argv[i] == "--debug"sv) { zen::logging::SetLogLevel(zen::logging::level::Debug); } else if (argv[i] == "--verbose"sv) { zen::logging::SetLogLevel(zen::logging::level::Trace); } } return 0; } int TestRunner::Run() { return m_Impl->Session.run(); } } // namespace zen::testing #endif // ZEN_WITH_TESTS