#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; std::string escapeHtml(const std::string &data) { std::string buffer; buffer.reserve(data.size()); for (size_t pos = 0; pos != data.size(); ++pos) { switch (data[pos]) { case '&': buffer.append("&"); break; case '\"': buffer.append("""); break; case '\'': buffer.append("'"); break; case '<': buffer.append("<"); break; case '>': buffer.append(">"); break; default: buffer.append(&data[pos], 1); break; } } return buffer; } std::string ansiToHtml(const std::string &text) { // Convert text to HTML-safe first std::string htmlEscaped = escapeHtml(text); std::ostringstream oss; size_t i = 0; size_t len = htmlEscaped.length(); int openSpans = 0; auto closeSpans = [&oss, &openSpans]() { while (openSpans > 0) { oss << ""; --openSpans; } }; while (i < len) { // Look for ANSI CSI sequence '\033[' or '\x1b[' if ((htmlEscaped[i] == '\033' || htmlEscaped[i] == '\x1b') && i + 1 < len && htmlEscaped[i + 1] == '[') { size_t seqStart = i + 2; size_t seqEnd = htmlEscaped.find('m', seqStart); if (seqEnd != std::string::npos) { std::string codeStr = htmlEscaped.substr(seqStart, seqEnd - seqStart); i = seqEnd + 1; std::istringstream codeStream(codeStr); std::string codeVal; // Defaults if sequence is just \033[m (Reset) if (codeStr.empty()) { closeSpans(); continue; } while (std::getline(codeStream, codeVal, ';')) { int code = 0; try { code = std::stoi(codeVal); } catch (...) { continue; } switch (code) { case 0: // Reset closeSpans(); break; case 1: // Bold oss << ""; openSpans++; break; case 2: // Dim oss << ""; openSpans++; break; // Standard Foreground Colors case 30: oss << ""; openSpans++; break; // Black case 31: oss << ""; openSpans++; break; // Red case 32: oss << ""; openSpans++; break; // Green case 33: oss << ""; openSpans++; break; // Yellow case 34: oss << ""; openSpans++; break; // Blue case 35: oss << ""; openSpans++; break; // Magenta case 36: oss << ""; openSpans++; break; // Cyan case 37: oss << ""; openSpans++; break; // Light Gray // Bright Foreground Colors case 90: oss << ""; openSpans++; break; // Dark Gray case 91: oss << ""; openSpans++; break; // Bright Red case 92: oss << ""; openSpans++; break; // Bright Green case 93: oss << ""; openSpans++; break; // Bright Yellow case 94: oss << ""; openSpans++; break; // Bright Blue case 95: oss << ""; openSpans++; break; // Bright Magenta case 96: oss << ""; openSpans++; break; // Bright Cyan case 97: oss << ""; openSpans++; break; // White default: break; } } continue; } } oss << htmlEscaped[i]; ++i; } closeSpans(); return oss.str(); } std::vector wrapText( const std::string &text, size_t width ) { std::vector lines; std::istringstream words(text); std::string word, line; while (words >> word) { if (line.length() + word.length() + 1 > width) { if (!line.empty()) { lines.push_back(line); line.clear(); } if (word.length() > width) { lines.push_back(word.substr(0, width - 3) + "..."); continue; } } if (!line.empty()) line += " "; line += word; } if (!line.empty()) lines.push_back(line); if (lines.empty()) lines.push_back(""); return lines; } class CheckReporter : public Catch::StreamingReporterBase { struct TestCaseData { std::string name; std::string tags; bool passed; std::size_t assertionsPassed; std::size_t assertionsFailed; std::vector failureMessages; std::vector infoMessages; }; std::vector m_currentFailures; std::vector m_currentInfos; std::unordered_set m_currentInfoSequences; std::vector m_testRunData; void captureInfoMessages(Catch::AssertionStats const &assertionStats) { for (auto const &message : assertionStats.infoMessages) { if (m_currentInfoSequences.insert(message.sequence).second) { m_currentInfos.push_back(message.message); } } } public: explicit CheckReporter(Catch::ReporterConfig &&config) : Catch::StreamingReporterBase(std::move(config)) { // INFO messages are delivered through assertionEnded. Request passing // assertions as well so HTML logging does not depend on Catch2's -s // flag. m_preferences.shouldReportAllAssertions = true; // This reporter does not use assertionStarting events. Disabling them // preserves Catch2's successful-assertion fast path where possible. m_preferences.shouldReportAllAssertionStarts = false; } static std::string getDescription() { return "Console reporter with wrapping, tags, and collapsible HTML " "export " "with ANSI color rendering."; } void testRunStarting(Catch::TestRunInfo const &_testRunInfo) override { StreamingReporterBase::testRunStarting(_testRunInfo); std::cout << '\n'; std::cout << std::left << std::setw(85) << "Test Case Name" << "Status " << std::right << std::setw(8) << "Passed" << std::setw(8) << "Failed" << '\n'; std::cout << std::string(121, '-') << '\n'; } void assertionEnded(Catch::AssertionStats const &assertionStats) override { StreamingReporterBase::assertionEnded(assertionStats); // Capture every INFO message encountered by either a passing or failing // assertion. Message sequence IDs prevent a scoped INFO from being // repeated once for every assertion that occurs while it remains // active. captureInfoMessages(assertionStats); if (!assertionStats.assertionResult.isOk()) { auto const &result = assertionStats.assertionResult; std::ostringstream oss; oss << " \033[31m-> FAILED:\033[0m " << result.getSourceInfo().file << ":" << result.getSourceInfo().line << '\n'; oss << " " << result.getTestMacroName() << "( " << result.getExpression() << " )\n"; if (result.hasExpandedExpression()) { oss << " with expansion:\n" << " " << result.getExpandedExpression() << '\n'; } for (auto const &msg : assertionStats.infoMessages) { oss << " \033[36m[INFO]\033[0m " << msg.message << '\n'; } m_currentFailures.push_back(oss.str()); } } void testCaseEnded(Catch::TestCaseStats const &stats) override { StreamingReporterBase::testCaseEnded(stats); bool passed = stats.totals.assertions.allPassed(); std::string mark = passed ? "\033[32m✓\033[0m" : "\033[31m✗\033[0m"; std::string name = stats.testInfo->name; auto wrappedName = wrapText(name, 83); std::cout << std::left << std::setw(85) << wrappedName[0] << mark << " " << std::right << std::setw(8) << stats.totals.assertions.passed << std::setw(8) << stats.totals.assertions.failed << '\n'; for (size_t i = 1; i < wrappedName.size(); ++i) { std::cout << " \033[90m↳ \033[0m" // Dim indent arrow << std::left << std::setw(81) << wrappedName[i] << '\n'; } std::string tagsStr = stats.testInfo->tagsAsString(); if (!tagsStr.empty()) { auto wrappedTags = wrapText("Tags: " + tagsStr, 83); for (const auto &line : wrappedTags) { std::cout << " \033[36m" << line << "\033[0m\n"; // Cyan } } if (!m_currentFailures.empty()) { std::cout << '\n'; for (auto const &failure : m_currentFailures) { std::cout << failure << '\n'; } std::cout << std::string(121, '-') << '\n'; } m_testRunData.push_back( {name, tagsStr, passed, stats.totals.assertions.passed, stats.totals.assertions.failed, m_currentFailures, m_currentInfos} ); m_currentFailures.clear(); m_currentInfos.clear(); m_currentInfoSequences.clear(); } void testRunEnded(Catch::TestRunStats const &_testRunStats) override { StreamingReporterBase::testRunEnded(_testRunStats); std::cout << std::string(121, '=') << '\n'; auto const &tc = _testRunStats.totals.testCases; auto const &as = _testRunStats.totals.assertions; std::string tc_passed_str = tc.passed > 0 ? "\033[32m" + std::to_string(tc.passed) + " passed\033[0m" : "0 passed"; std::string tc_failed_str = tc.failed > 0 ? "\033[31m" + std::to_string(tc.failed) + " failed\033[0m" : "0 failed"; std::string as_passed_str = as.passed > 0 ? "\033[32m" + std::to_string(as.passed) + " passed\033[0m" : "0 passed"; std::string as_failed_str = as.failed > 0 ? "\033[31m" + std::to_string(as.failed) + " failed\033[0m" : "0 failed"; std::cout << "Test Cases: " << tc_passed_str << ", " << tc_failed_str << ", " << tc.total() << " total\n"; std::cout << "Assertions: " << as_passed_str << ", " << as_failed_str << ", " << as.total() << " total\n\n"; generateHtmlReport(_testRunStats); } private: void generateHtmlReport(Catch::TestRunStats const &stats) { std::ofstream html("test_summary.html"); if (!html) return; html << "\n\n\n" << "\n" << "\n" << "Test Run Summary\n" << "\n\n\n"; html << "

Test Run Summary

\n"; // Summary Cards html << "
\n"; html << "

Total Cases

" << stats.totals.testCases.total() << "

\n"; html << "

Cases Passed

" << stats.totals.testCases.passed << "

\n"; html << "

Cases Failed

" << stats.totals.testCases.failed << "

\n"; html << "
\n"; for (const auto &test : m_testRunData) { std::string statusClass = test.passed ? "passed" : "failed"; html << "
\n"; html << "
\n"; html << "
\n"; html << "

" << escapeHtml(test.name) << "

\n"; if (!test.tags.empty()) { html << "
" << escapeHtml(test.tags) << "
\n"; } html << "
\n"; html << "
\n"; html << " ✓ " << test.assertionsPassed << " | "; html << " ✗ " << test.assertionsFailed << "\n"; html << "
\n"; html << "
\n"; // Collapsible INFO Messages section with ANSI color rendering if (!test.infoMessages.empty()) { html << "
\n"; html << " Info Logs (" << test.infoMessages.size() << ")\n"; html << "
";
                for (const auto &info : test.infoMessages) {
                    html << "[INFO] " << ansiToHtml(info) << "\n";
                }
                html << "
\n"; html << "
\n"; } // Collapsible Failures section with ANSI color rendering if (!test.failureMessages.empty()) { html << "
\n"; html << " Failure Details (" << test.failureMessages.size() << ")\n"; html << "
";
                for (const auto &msg : test.failureMessages) {
                    html << ansiToHtml(msg) << "\n";
                }
                html << "    
\n"; html << "
\n"; } html << "
\n"; } html << "\n\n"; } }; CATCH_REGISTER_REPORTER( "check", CheckReporter ) int main( int argc, char *argv[] ) { fourdst::config::Config cfg; CLI::App app{"Mean Field Tests"}; app.allow_extras(); app.set_help_flag("--config-help", "Show mean-field configuration options"); fourdst::config::register_as_cli(cfg, app); std::vector config_arguments; std::vector forced_catch_arguments; config_arguments.emplace_back(argv[0]); bool parsing_catch_arguments = false; for (int i = 1; i < argc; ++i) { if (std::string_view(argv[i]) == "--catch2") { parsing_catch_arguments = true; continue; } if (parsing_catch_arguments) { forced_catch_arguments.emplace_back(argv[i]); } else { config_arguments.emplace_back(argv[i]); } } std::vector config_argv; config_argv.reserve(config_arguments.size()); for (const std::string &argument : config_arguments) { config_argv.push_back(argument.c_str()); } try { app.parse(static_cast(config_argv.size()), config_argv.data()); } catch (const CLI::ParseError &error) { return app.exit(error); } std::vector catch_arguments; catch_arguments.emplace_back(argv[0]); for (const std::string &argument : app.remaining()) { catch_arguments.push_back(argument); } for (const std::string &argument : forced_catch_arguments) { catch_arguments.push_back(argument); } const auto is_reporter_option = [](const std::string &argument) { return argument == "-r" || argument == "--reporter" || argument.starts_with("-r=") || argument.starts_with("--reporter="); }; if (const bool has_reporter = std::ranges::any_of(catch_arguments, is_reporter_option); !has_reporter) { catch_arguments.emplace_back("--reporter"); catch_arguments.emplace_back("check"); } std::vector catch_argv; catch_argv.reserve(catch_arguments.size()); for (const std::string &argument : catch_arguments) { catch_argv.push_back(argument.c_str()); } Catch::Session session; if (const int catch_parse_result = session.applyCommandLine(static_cast(catch_argv.size()), catch_argv.data()); catch_parse_result != 0) { return catch_parse_result; } mfem::Mpi::Init(argc, argv); constexpr std::string device_config = "cpu"; mfem::Device device(device_config); const int hdiv_max_q1d = mfem::DeviceDofQuadLimits::Get().HDIV_MAX_Q1D; std::cout << "H(div) maximum Q1D = " << hdiv_max_q1d << '\n'; std::cout << "Approximate maximum safe integration order = " << 2 * hdiv_max_q1d - 1 << '\n'; mean_field::utils::Args test_args = cfg.main(); if (app.count("--mesh_file") == 0) { test_args.mesh_file = "sandbox.smesh"; } if (app.count("--p.rtol") == 0) { test_args.p.rtol = 1.0e-12; } if (app.count("--p.atol") == 0) { test_args.p.atol = 1.0e-12; } test_utils::set_args(std::move(test_args)); return session.run(); }