LCOV - code coverage report
Current view: top level - src/tools - run_unit_tests.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 100 177 56.5 %
Date: 2025-09-03 06:42:31 Functions: 8 9 88.9 %

          Line data    Source code
       1             : // SPDX-FileCopyrightText: 2024 PairInteraction Developers
       2             : // SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4             : #define DOCTEST_CONFIG_IMPLEMENT
       5             : 
       6             : #include "pairinteraction/tools/run_unit_tests.hpp"
       7             : 
       8             : #include "pairinteraction/database/Database.hpp"
       9             : #include "pairinteraction/utils/paths.hpp"
      10             : #include "pairinteraction/utils/streamed.hpp"
      11             : #include "pairinteraction/version.hpp"
      12             : 
      13             : #include <cstdlib>
      14             : #include <doctest/doctest.h>
      15             : #include <filesystem>
      16             : #include <httplib.h>
      17             : #include <mutex>
      18             : #include <spdlog/spdlog.h>
      19             : 
      20             : // Create a reporter for doctest that logs to spdlog
      21             : namespace doctest {
      22             : 
      23             : // The code of the LoggingReporter is based on the ConsoleReporter from doctest,
      24             : // https://github.com/doctest/doctest/blob/ae7a13539fb71f270b87eb2e874fbac80bc8dda2/doctest/parts/doctest.cpp#L2868.
      25             : //
      26             : // SPDX-SnippetBegin
      27             : // SPDX-FileCopyrightText: (c) 2016-2025 Viktor Kirilov, Sebastian Weber
      28             : // SPDX-License-Identifier: MIT
      29             : 
      30             : struct LoggingReporter : public ConsoleReporter {
      31           1 :     LoggingReporter(const ContextOptions &co) : ConsoleReporter(co) {}
      32             : 
      33         121 :     void log_contexts() {}
      34             : 
      35         157 :     void logTestStart() {}
      36             : 
      37           1 :     void test_run_end(const TestRunStats &p) override {
      38           1 :         if (opt.minimal && p.numTestCasesFailed == 0) {
      39           0 :             return;
      40             :         }
      41             : 
      42           1 :         std::stringstream ss;
      43           1 :         ss << Color::Yellow
      44           1 :            << "==============================================================================="
      45           1 :            << Color::None << "\n";
      46           1 :         ss << std::dec;
      47             : 
      48             :         auto totwidth =
      49           2 :             int(std::ceil(log10(static_cast<double>(std::max(p.numTestCasesPassingFilters,
      50           1 :                                                              static_cast<unsigned>(p.numAsserts))) +
      51           1 :                                 1)));
      52             :         auto passwidth =
      53           2 :             int(std::ceil(log10(static_cast<double>(std::max(
      54           2 :                                     p.numTestCasesPassingFilters - p.numTestCasesFailed,
      55           1 :                                     static_cast<unsigned>(p.numAsserts - p.numAssertsFailed))) +
      56           1 :                                 1)));
      57             :         auto failwidth = int(
      58           2 :             std::ceil(log10(static_cast<double>(std::max(
      59           1 :                                 p.numTestCasesFailed, static_cast<unsigned>(p.numAssertsFailed))) +
      60           1 :                             1)));
      61           1 :         const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0;
      62           1 :         ss << "test cases: " << std::setw(totwidth) << p.numTestCasesPassingFilters << " | "
      63           1 :            << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None : Color::Green)
      64           1 :            << std::setw(passwidth) << p.numTestCasesPassingFilters - p.numTestCasesFailed
      65           1 :            << " passed" << Color::None << " | "
      66           1 :            << (p.numTestCasesFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth)
      67           1 :            << p.numTestCasesFailed << " failed" << Color::None << " |";
      68           1 :         if (!opt.no_skipped_summary) {
      69           1 :             const unsigned int numSkipped = p.numTestCases - p.numTestCasesPassingFilters;
      70           1 :             ss << " " << (numSkipped == 0 ? Color::None : Color::Yellow) << numSkipped << " skipped"
      71           1 :                << Color::None;
      72             :         }
      73           1 :         ss << "\n";
      74           1 :         ss << "assertions: " << std::setw(totwidth) << p.numAsserts << " | "
      75           1 :            << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green)
      76           1 :            << std::setw(passwidth) << (p.numAsserts - p.numAssertsFailed) << " passed"
      77           1 :            << Color::None << " | " << (p.numAssertsFailed > 0 ? Color::Red : Color::None)
      78           1 :            << std::setw(failwidth) << p.numAssertsFailed << " failed" << Color::None << " |\n";
      79           1 :         ss << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green)
      80           1 :            << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl;
      81             : 
      82           1 :         if (p.numTestCasesFailed > 0) {
      83           0 :             for (std::string line; std::getline(ss, line);) {
      84           0 :                 SPDLOG_ERROR(line);
      85           0 :             }
      86             :         } else {
      87           5 :             for (std::string line; std::getline(ss, line);) {
      88           4 :                 SPDLOG_INFO(line);
      89           1 :             }
      90             :         }
      91           1 :     }
      92             : 
      93          36 :     void test_case_end(const CurrentTestCaseStats &st) override {
      94          36 :         if (tc->m_no_output) {
      95           0 :             return;
      96             :         }
      97             : 
      98          36 :         if (opt.duration ||
      99           0 :             (st.failure_flags != 0 &&
     100           0 :              st.failure_flags != static_cast<int>(TestCaseFailureReason::AssertFailure))) {
     101          36 :             logTestStart();
     102             :         }
     103             : 
     104          36 :         if (opt.duration) {
     105          36 :             std::stringstream ss;
     106          36 :             ss << std::setprecision(6) << std::fixed << st.seconds << " s: " << tc->m_name;
     107          36 :             SPDLOG_INFO(ss.str());
     108          36 :         }
     109             : 
     110          36 :         if ((st.failure_flags & TestCaseFailureReason::Timeout) != 0) {
     111           0 :             std::stringstream ss;
     112           0 :             ss << Color::Red << "Test case exceeded time limit of " << std::setprecision(6)
     113           0 :                << std::fixed << tc->m_timeout << "!" << Color::None;
     114           0 :             SPDLOG_ERROR(ss.str());
     115           0 :         }
     116             : 
     117          36 :         if ((st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) != 0) {
     118           0 :             std::stringstream ss;
     119           0 :             ss << Color::Red << "Should have failed but didn't! Marking it as failed!"
     120           0 :                << Color::None;
     121           0 :             SPDLOG_ERROR(ss.str());
     122          36 :         } else if ((st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) != 0) {
     123           0 :             std::stringstream ss;
     124           0 :             ss << Color::Yellow << "Failed as expected so marking it as not failed" << Color::None;
     125           0 :             SPDLOG_WARN(ss.str());
     126          36 :         } else if ((st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) != 0) {
     127           0 :             std::stringstream ss;
     128           0 :             ss << Color::Yellow << "Allowed to fail so marking it as not failed" << Color::None;
     129           0 :             SPDLOG_WARN(ss.str());
     130          36 :         } else if ((st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) != 0) {
     131           0 :             std::stringstream ss;
     132           0 :             ss << Color::Red << "Didn't fail exactly " << tc->m_expected_failures
     133           0 :                << " times so marking it as failed!" << Color::None;
     134           0 :             SPDLOG_ERROR(ss.str());
     135          36 :         } else if ((st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) != 0) {
     136           0 :             std::stringstream ss;
     137           0 :             ss << Color::Yellow << "Failed exactly " << tc->m_expected_failures
     138           0 :                << " times as expected so marking it as not failed!" << Color::None;
     139           0 :             SPDLOG_WARN(ss.str());
     140           0 :         }
     141             : 
     142          36 :         if ((st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) != 0) {
     143           0 :             std::stringstream ss;
     144           0 :             ss << Color::Red << "Aborting - too many failed asserts!" << Color::None;
     145           0 :             SPDLOG_ERROR(ss.str());
     146           0 :         }
     147             :     }
     148             : 
     149           0 :     void test_case_exception(const TestCaseException &e) override {
     150           0 :         if (tc->m_no_output) {
     151           0 :             return;
     152             :         }
     153             : 
     154           0 :         DOCTEST_LOCK_MUTEX(mutex)
     155             : 
     156           0 :         logTestStart();
     157             : 
     158           0 :         std::stringstream ss;
     159           0 :         ss << "[" << skipPathFromFilename(tc->m_file.c_str()) << (opt.gnu_file_line ? ":" : "(")
     160           0 :            << (opt.no_line_numbers ? 0 : tc->m_line) << (opt.gnu_file_line ? "" : ")") << "] ";
     161           0 :         std::string loc = ss.str();
     162           0 :         ss.str("");
     163           0 :         ss << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: ")
     164           0 :            << Color::None << e.error_string;
     165           0 :         for (std::string line; std::getline(ss, line);) {
     166           0 :             SPDLOG_ERROR(loc + line);
     167           0 :         }
     168           0 :     }
     169             : 
     170        1686 :     void log_assert(const AssertData &rb) override {
     171        1686 :         if ((!rb.m_failed && !opt.success) || tc->m_no_output) {
     172        1686 :             return;
     173             :         }
     174             : 
     175           0 :         DOCTEST_LOCK_MUTEX(mutex)
     176             : 
     177           0 :         logTestStart();
     178             : 
     179           0 :         std::stringstream ss;
     180           0 :         ss << "[" << skipPathFromFilename(rb.m_file) << (opt.gnu_file_line ? ":" : "(")
     181           0 :            << (opt.no_line_numbers ? 0 : rb.m_line) << (opt.gnu_file_line ? "" : ")") << "] ";
     182           0 :         std::string loc = ss.str();
     183           0 :         ss.str("");
     184           0 :         fulltext_log_assert_to_stream(ss, rb);
     185           0 :         if (rb.m_failed) {
     186           0 :             for (std::string line; std::getline(ss, line);) {
     187           0 :                 SPDLOG_ERROR(loc + line);
     188           0 :             }
     189             :         } else {
     190           0 :             for (std::string line; std::getline(ss, line);) {
     191           0 :                 SPDLOG_INFO(loc + line);
     192           0 :             }
     193             :         }
     194             : 
     195           0 :         log_contexts();
     196           0 :     }
     197             : 
     198         121 :     void log_message(const MessageData &mb) override {
     199         121 :         if (tc->m_no_output) {
     200           0 :             return;
     201             :         }
     202             : 
     203         121 :         DOCTEST_LOCK_MUTEX(mutex)
     204             : 
     205         121 :         logTestStart();
     206             : 
     207         121 :         std::stringstream ss;
     208         121 :         ss << "[" << skipPathFromFilename(mb.m_file) << (opt.gnu_file_line ? ":" : "(")
     209         121 :            << (opt.no_line_numbers ? 0 : mb.m_line) << (opt.gnu_file_line ? "" : ")") << "] ";
     210         121 :         std::string loc = ss.str();
     211         121 :         ss.str("");
     212         121 :         ss << getSuccessOrFailColor(false, mb.m_severity)
     213         121 :            << getSuccessOrFailString((mb.m_severity & assertType::is_warn) != 0, mb.m_severity,
     214             :                                      "MESSAGE")
     215         121 :            << ": " << Color::None << mb.m_string;
     216         242 :         for (std::string line; std::getline(ss, line);) {
     217         121 :             SPDLOG_INFO(loc + line);
     218         121 :         }
     219             : 
     220         121 :         log_contexts();
     221         121 :     }
     222             : };
     223             : 
     224             : // SPDX-SnippetEnd
     225             : 
     226             : REGISTER_REPORTER("logging", 1, doctest::LoggingReporter);
     227             : } // namespace doctest
     228             : 
     229             : constexpr std::string_view OS_NAME =
     230             : #if defined(_WIN32)
     231             :     "Windows";
     232             : #elif defined(__APPLE__)
     233             :     "macOS";
     234             : #elif defined(__linux__)
     235             :     "Linux";
     236             : #else
     237             :     "Unknown";
     238             : #endif
     239             : 
     240             : namespace pairinteraction {
     241           1 : int run_unit_tests(int argc, char **argv, bool download_missing, bool use_cache,
     242             :                    std::filesystem::path database_dir) {
     243             : 
     244             :     // Setup the tests
     245           1 :     doctest::Context ctx;
     246           1 :     ctx.setOption("abort-after", 5);
     247           1 :     ctx.setOption("no-run", 0);
     248           1 :     ctx.setOption("duration", true);
     249           1 :     ctx.setOption("no-path-filenames", true);
     250           1 :     ctx.applyCommandLine(argc, argv);
     251           1 :     ctx.setOption("no-colors", true);
     252           1 :     ctx.setOption("no-breaks", true);
     253           1 :     ctx.setOption("reporters", "logging");
     254           1 :     ctx.setOption("no-intro", true);
     255             : 
     256             :     // Log the version and system information
     257           2 :     SPDLOG_INFO("Version of pairinteraction: {}.{}.{}", VERSION_MAJOR, VERSION_MINOR,
     258             :                 VERSION_PATCH);
     259           2 :     SPDLOG_INFO("Operating system: {}", OS_NAME);
     260             : 
     261             :     // Create a global database instance and run the tests
     262           1 :     Database::get_global_instance(download_missing, use_cache, std::move(database_dir));
     263           1 :     int exitcode = ctx.run();
     264             : 
     265           1 :     std::filesystem::path logdir = paths::get_cache_directory() / "logs";
     266           2 :     SPDLOG_INFO("The log was stored to {}", logdir.string());
     267             : 
     268           1 :     if (exitcode != 0) {
     269           0 :         if (download_missing) {
     270           0 :             httplib::Client client("https://www.github.com");
     271           0 :             auto res = client.Head("/");
     272           0 :             if (!res) {
     273           0 :                 SPDLOG_ERROR(
     274             :                     "Test failed. Please check your internet connection. An internet "
     275             :                     "connection is required to download databases of atomic states and matrix "
     276             :                     "elements if they are not available locally. The log was stored to {}",
     277             :                     logdir.string());
     278             :             } else {
     279           0 :                 SPDLOG_ERROR(
     280             :                     "Tests failed. Consider creating an issue on "
     281             :                     "https://github.com/pairinteraction/pairinteraction/issues, attaching the "
     282             :                     "log. The log was stored to {}",
     283             :                     logdir.string());
     284             :             }
     285           0 :         } else {
     286           0 :             SPDLOG_ERROR(
     287             :                 "Tests failed. Consider creating an issue on "
     288             :                 "https://github.com/pairinteraction/pairinteraction/issues, attaching the "
     289             :                 "log. If the tests failed because of unavailable states or "
     290             :                 "matrix elements, consider downloading missing databases by calling "
     291             :                 "the test function with 'download_missing = true'. The log was stored to {}",
     292             :                 logdir.string());
     293             :         }
     294             :     }
     295             : 
     296           1 :     return exitcode;
     297           1 : };
     298             : } // namespace pairinteraction

Generated by: LCOV version 1.16