catch_reporter_helpers.cpp
1 2 // Copyright Catch2 Authors 3 // Distributed under the Boost Software License, Version 1.0. 4 // (See accompanying file LICENSE.txt or copy at 5 // https://www.boost.org/LICENSE_1_0.txt) 6 7 // SPDX-License-Identifier: BSL-1.0 8 9 #include <catch2/reporters/catch_reporter_helpers.hpp> 10 #include <catch2/interfaces/catch_interfaces_config.hpp> 11 #include <catch2/internal/catch_console_width.hpp> 12 #include <catch2/internal/catch_errno_guard.hpp> 13 #include <catch2/internal/catch_textflow.hpp> 14 #include <catch2/internal/catch_reusable_string_stream.hpp> 15 #include <catch2/internal/catch_string_manip.hpp> 16 #include <catch2/internal/catch_console_colour.hpp> 17 #include <catch2/catch_tostring.hpp> 18 #include <catch2/catch_test_case_info.hpp> 19 20 #include <algorithm> 21 #include <cfloat> 22 #include <cstdio> 23 #include <ostream> 24 #include <iomanip> 25 26 namespace Catch { 27 28 namespace { 29 void listTestNamesOnly(std::ostream& out, 30 std::vector<TestCaseHandle> const& tests) { 31 for (auto const& test : tests) { 32 auto const& testCaseInfo = test.getTestCaseInfo(); 33 34 if (startsWith(testCaseInfo.name, '#')) { 35 out << '"' << testCaseInfo.name << '"'; 36 } else { 37 out << testCaseInfo.name; 38 } 39 40 out << '\n'; 41 } 42 out << std::flush; 43 } 44 } // end unnamed namespace 45 46 47 // Because formatting using c++ streams is stateful, drop down to C is 48 // required Alternatively we could use stringstream, but its performance 49 // is... not good. 50 std::string getFormattedDuration( double duration ) { 51 // Max exponent + 1 is required to represent the whole part 52 // + 1 for decimal point 53 // + 3 for the 3 decimal places 54 // + 1 for null terminator 55 const std::size_t maxDoubleSize = DBL_MAX_10_EXP + 1 + 1 + 3 + 1; 56 char buffer[maxDoubleSize]; 57 58 // Save previous errno, to prevent sprintf from overwriting it 59 ErrnoGuard guard; 60 #ifdef _MSC_VER 61 size_t printedLength = static_cast<size_t>( 62 sprintf_s( buffer, "%.3f", duration ) ); 63 #else 64 size_t printedLength = static_cast<size_t>( 65 std::snprintf( buffer, maxDoubleSize, "%.3f", duration ) ); 66 #endif 67 return std::string( buffer, printedLength ); 68 } 69 70 bool shouldShowDuration( IConfig const& config, double duration ) { 71 if ( config.showDurations() == ShowDurations::Always ) { 72 return true; 73 } 74 if ( config.showDurations() == ShowDurations::Never ) { 75 return false; 76 } 77 const double min = config.minDuration(); 78 return min >= 0 && duration >= min; 79 } 80 81 std::string serializeFilters( std::vector<std::string> const& filters ) { 82 // We add a ' ' separator between each filter 83 size_t serialized_size = filters.size() - 1; 84 for (auto const& filter : filters) { 85 serialized_size += filter.size(); 86 } 87 88 std::string serialized; 89 serialized.reserve(serialized_size); 90 bool first = true; 91 92 for (auto const& filter : filters) { 93 if (!first) { 94 serialized.push_back(' '); 95 } 96 first = false; 97 serialized.append(filter); 98 } 99 100 return serialized; 101 } 102 103 std::ostream& operator<<( std::ostream& out, lineOfChars value ) { 104 for ( size_t idx = 0; idx < CATCH_CONFIG_CONSOLE_WIDTH - 1; ++idx ) { 105 out.put( value.c ); 106 } 107 return out; 108 } 109 110 void 111 defaultListReporters( std::ostream& out, 112 std::vector<ReporterDescription> const& descriptions, 113 Verbosity verbosity ) { 114 out << "Available reporters:\n"; 115 const auto maxNameLen = 116 std::max_element( descriptions.begin(), 117 descriptions.end(), 118 []( ReporterDescription const& lhs, 119 ReporterDescription const& rhs ) { 120 return lhs.name.size() < rhs.name.size(); 121 } ) 122 ->name.size(); 123 124 for ( auto const& desc : descriptions ) { 125 if ( verbosity == Verbosity::Quiet ) { 126 out << TextFlow::Column( desc.name ) 127 .indent( 2 ) 128 .width( 5 + maxNameLen ) 129 << '\n'; 130 } else { 131 out << TextFlow::Column( desc.name + ':' ) 132 .indent( 2 ) 133 .width( 5 + maxNameLen ) + 134 TextFlow::Column( desc.description ) 135 .initialIndent( 0 ) 136 .indent( 2 ) 137 .width( CATCH_CONFIG_CONSOLE_WIDTH - maxNameLen - 8 ) 138 << '\n'; 139 } 140 } 141 out << '\n' << std::flush; 142 } 143 144 void defaultListListeners( std::ostream& out, 145 std::vector<ListenerDescription> const& descriptions ) { 146 out << "Registered listeners:\n"; 147 148 if(descriptions.empty()) { 149 return; 150 } 151 152 const auto maxNameLen = 153 std::max_element( descriptions.begin(), 154 descriptions.end(), 155 []( ListenerDescription const& lhs, 156 ListenerDescription const& rhs ) { 157 return lhs.name.size() < rhs.name.size(); 158 } ) 159 ->name.size(); 160 161 for ( auto const& desc : descriptions ) { 162 out << TextFlow::Column( static_cast<std::string>( desc.name ) + 163 ':' ) 164 .indent( 2 ) 165 .width( maxNameLen + 5 ) + 166 TextFlow::Column( desc.description ) 167 .initialIndent( 0 ) 168 .indent( 2 ) 169 .width( CATCH_CONFIG_CONSOLE_WIDTH - maxNameLen - 8 ) 170 << '\n'; 171 } 172 173 out << '\n' << std::flush; 174 } 175 176 void defaultListTags( std::ostream& out, 177 std::vector<TagInfo> const& tags, 178 bool isFiltered ) { 179 if ( isFiltered ) { 180 out << "Tags for matching test cases:\n"; 181 } else { 182 out << "All available tags:\n"; 183 } 184 185 for ( auto const& tagCount : tags ) { 186 ReusableStringStream rss; 187 rss << " " << std::setw( 2 ) << tagCount.count << " "; 188 auto str = rss.str(); 189 auto wrapper = TextFlow::Column( tagCount.all() ) 190 .initialIndent( 0 ) 191 .indent( str.size() ) 192 .width( CATCH_CONFIG_CONSOLE_WIDTH - 10 ); 193 out << str << wrapper << '\n'; 194 } 195 out << pluralise(tags.size(), "tag"_sr) << "\n\n" << std::flush; 196 } 197 198 void defaultListTests(std::ostream& out, ColourImpl* streamColour, std::vector<TestCaseHandle> const& tests, bool isFiltered, Verbosity verbosity) { 199 // We special case this to provide the equivalent of old 200 // `--list-test-names-only`, which could then be used by the 201 // `--input-file` option. 202 if (verbosity == Verbosity::Quiet) { 203 listTestNamesOnly(out, tests); 204 return; 205 } 206 207 if (isFiltered) { 208 out << "Matching test cases:\n"; 209 } else { 210 out << "All available test cases:\n"; 211 } 212 213 for (auto const& test : tests) { 214 auto const& testCaseInfo = test.getTestCaseInfo(); 215 Colour::Code colour = testCaseInfo.isHidden() 216 ? Colour::SecondaryText 217 : Colour::None; 218 auto colourGuard = streamColour->guardColour( colour ).engage( out ); 219 220 out << TextFlow::Column(testCaseInfo.name).indent(2) << '\n'; 221 if (verbosity >= Verbosity::High) { 222 out << TextFlow::Column(Catch::Detail::stringify(testCaseInfo.lineInfo)).indent(4) << '\n'; 223 } 224 if (!testCaseInfo.tags.empty() && 225 verbosity > Verbosity::Quiet) { 226 out << TextFlow::Column(testCaseInfo.tagsAsString()).indent(6) << '\n'; 227 } 228 } 229 230 if (isFiltered) { 231 out << pluralise(tests.size(), "matching test case"_sr); 232 } else { 233 out << pluralise(tests.size(), "test case"_sr); 234 } 235 out << "\n\n" << std::flush; 236 } 237 238 namespace { 239 class SummaryColumn { 240 public: 241 SummaryColumn( std::string suffix, Colour::Code colour ): 242 m_suffix( CATCH_MOVE( suffix ) ), m_colour( colour ) {} 243 244 SummaryColumn&& addRow( std::uint64_t count ) && { 245 std::string row = std::to_string(count); 246 auto const new_width = std::max( m_width, row.size() ); 247 if ( new_width > m_width ) { 248 for ( auto& oldRow : m_rows ) { 249 oldRow.insert( 0, new_width - m_width, ' ' ); 250 } 251 } else { 252 row.insert( 0, m_width - row.size(), ' ' ); 253 } 254 m_width = new_width; 255 m_rows.push_back( row ); 256 return std::move( *this ); 257 } 258 259 std::string const& getSuffix() const { return m_suffix; } 260 Colour::Code getColour() const { return m_colour; } 261 std::string const& getRow( std::size_t index ) const { 262 return m_rows[index]; 263 } 264 265 private: 266 std::string m_suffix; 267 Colour::Code m_colour; 268 std::size_t m_width = 0; 269 std::vector<std::string> m_rows; 270 }; 271 272 void printSummaryRow( std::ostream& stream, 273 ColourImpl& colour, 274 StringRef label, 275 std::vector<SummaryColumn> const& cols, 276 std::size_t row ) { 277 for ( auto const& col : cols ) { 278 auto const& value = col.getRow( row ); 279 auto const& suffix = col.getSuffix(); 280 if ( suffix.empty() ) { 281 stream << label << ": "; 282 if ( value != "0" ) { 283 stream << value; 284 } else { 285 stream << colour.guardColour( Colour::Warning ) 286 << "- none -"; 287 } 288 } else if ( value != "0" ) { 289 stream << colour.guardColour( Colour::LightGrey ) << " | " 290 << colour.guardColour( col.getColour() ) << value 291 << ' ' << suffix; 292 } 293 } 294 stream << '\n'; 295 } 296 } // namespace 297 298 void printTestRunTotals( std::ostream& stream, 299 ColourImpl& streamColour, 300 Totals const& totals ) { 301 if ( totals.testCases.total() == 0 ) { 302 stream << streamColour.guardColour( Colour::Warning ) 303 << "No tests ran\n"; 304 return; 305 } 306 307 if ( totals.assertions.total() > 0 && totals.testCases.allPassed() ) { 308 stream << streamColour.guardColour( Colour::ResultSuccess ) 309 << "All tests passed"; 310 stream << " (" 311 << pluralise( totals.assertions.passed, "assertion"_sr ) 312 << " in " 313 << pluralise( totals.testCases.passed, "test case"_sr ) 314 << ')' << '\n'; 315 return; 316 } 317 318 std::vector<SummaryColumn> columns; 319 // Don't include "skipped assertions" in total count 320 const auto totalAssertionCount = 321 totals.assertions.total() - totals.assertions.skipped; 322 columns.push_back( SummaryColumn( "", Colour::None ) 323 .addRow( totals.testCases.total() ) 324 .addRow( totalAssertionCount ) ); 325 columns.push_back( SummaryColumn( "passed", Colour::Success ) 326 .addRow( totals.testCases.passed ) 327 .addRow( totals.assertions.passed ) ); 328 columns.push_back( SummaryColumn( "failed", Colour::ResultError ) 329 .addRow( totals.testCases.failed ) 330 .addRow( totals.assertions.failed ) ); 331 columns.push_back( SummaryColumn( "skipped", Colour::Skip ) 332 .addRow( totals.testCases.skipped ) 333 // Don't print "skipped assertions" 334 .addRow( 0 ) ); 335 columns.push_back( 336 SummaryColumn( "failed as expected", Colour::ResultExpectedFailure ) 337 .addRow( totals.testCases.failedButOk ) 338 .addRow( totals.assertions.failedButOk ) ); 339 printSummaryRow( stream, streamColour, "test cases"_sr, columns, 0 ); 340 printSummaryRow( stream, streamColour, "assertions"_sr, columns, 1 ); 341 } 342 343 } // namespace Catch