yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
bc6b82666
master
1// test-reporter.cpp 2#include "test-reporter.h" 3 4#include "../../source/core/slang-process-util.h" 5#include "../../source/core/slang-string-util.h" 6#include "options.h" 7 8#include <mutex> 9#include <stdio.h> 10#include <stdlib.h> 11 12using namespace Slang ; 13 14/* static */ TestReporter * TestReporter ::s_reporter = nullptr ; 15 16static void appendXmlEncode (char c ,StringBuilder & out ) 17{ 18switch (c ) 19 { 20case '&' : 21out <<"&" ; 22break ; 23case '<' : 24out <<"<" ; 25break ; 26case '>' : 27out <<">" ; 28break ; 29case '\'' : 30out <<"'" ; 31break ; 32case '"' : 33out <<""" ; 34break ; 35default : 36out .append (c ); 37 } 38} 39 40static bool isXmlEncodeChar (char c ) 41{ 42switch (c ) 43 { 44case '&' : 45case '<' : 46case '>' : 47 { 48return true; 49 } 50 } 51return false; 52} 53 54static void appendXmlEncode (const String & in ,StringBuilder & out ) 55{ 56const char * cur = in .getBuffer (); 57const char * end = cur + in .getLength (); 58 59while (cur < end ) 60 { 61const char * start = cur ; 62// Look for a run of non encoded 63while (cur < end && !isXmlEncodeChar (* cur )) 64 { 65cur ++ ; 66 } 67// Write it 68if (cur > start ) 69 { 70out .append (start ,UInt (end - start )); 71 } 72 73// if not at the end, we must be on an xml encoded character, so just output it xml encoded. 74if (cur < end ) 75 { 76const char encodeChar = * cur ++ ; 77assert (isXmlEncodeChar (encodeChar )); 78appendXmlEncode (encodeChar ,out ); 79 } 80 } 81} 82 83TestReporter ::TestReporter () 84 :m_outputMode (TestOutputMode ::Default ) 85{ 86m_totalTestCount = 0 ; 87m_passedTestCount = 0 ; 88m_failedTestCount = 0 ; 89m_ignoredTestCount = 0 ; 90m_expectedFailedTestCount = 0 ; 91m_maxFailTestResults = 10 ; 92 93m_inTest = false; 94m_dumpOutputOnFailure = false; 95m_verbosity = VerbosityLevel ::Info ; 96} 97 98Result TestReporter ::init ( 99TestOutputMode outputMode , 100const HashSet < String >& expectedFailureList , 101bool isSubReporter ) 102{ 103m_outputMode = outputMode ; 104m_isSubReporter = isSubReporter ; 105m_expectedFailureList = expectedFailureList ; 106return SLANG_OK ; 107} 108 109TestReporter ::~TestReporter () {} 110 111bool TestReporter ::canWriteStdError ()const 112{ 113switch (m_outputMode ) 114 { 115case TestOutputMode ::XUnit : 116case TestOutputMode ::XUnit2 : 117 { 118return false; 119 } 120default : 121return true; 122 } 123} 124 125void TestReporter ::startTest (const char * testName ) 126{ 127// Must be in a suite 128assert (m_suiteStack .getCount ()); 129assert (!m_inTest ); 130 131m_inTest = true; 132 133m_numCurrentResults = 0 ; 134m_numFailResults = 0 ; 135 136m_currentInfo = TestInfo (); 137m_currentInfo .name = testName ; 138m_currentMessage .clear (); 139} 140 141void TestReporter ::endTest () 142{ 143assert (m_suiteStack .getCount ()); 144assert (m_inTest ); 145 146m_currentInfo .message = m_currentMessage ; 147 148_addResult (m_currentInfo ); 149 150m_inTest = false; 151} 152 153void TestReporter ::addResult (TestResult result ) 154{ 155assert (m_inTest ); 156 157 std::lock_guard < std::recursive_mutex > lock (m_mutex ); 158if (result == TestResult ::Fail && m_expectedFailureList .contains (m_currentInfo .name )) 159result = TestResult ::ExpectedFail ; 160m_currentInfo .testResult = combine (m_currentInfo .testResult ,result ); 161m_numCurrentResults ++ ; 162} 163 164TestResult TestReporter ::getResult ()const 165{ 166return m_currentInfo .testResult ; 167} 168 169void TestReporter ::addExecutionTime (double time ) 170{ 171 std::lock_guard < std::recursive_mutex > lock (m_mutex ); 172 173m_currentInfo .executionTime = time ; 174} 175 176void TestReporter ::addResultWithLocation ( 177TestResult result , 178const char * testText , 179const char * file , 180int line ) 181{ 182assert (m_inTest ); 183 184 std::lock_guard < std::recursive_mutex > lock (m_mutex ); 185result = adjustResult (m_currentInfo .name .getUnownedSlice (),result ); 186 187m_numCurrentResults ++ ; 188 189m_currentInfo .testResult = combine (m_currentInfo .testResult ,result ); 190if (result != TestResult ::Fail ) 191 { 192// We don't need to output the result if it 193return ; 194 } 195 196m_numFailResults ++ ; 197 198if (m_maxFailTestResults > 0 ) 199 { 200if (m_numFailResults > m_maxFailTestResults ) 201 { 202if (m_numFailResults == m_maxFailTestResults + 1 ) 203 { 204// It's a failure, but to show that there are more than are going to be shown, just 205// show '...' 206message (TestMessageType ::TestFailure ,"..." ); 207 } 208return ; 209 } 210 } 211 212StringBuilder buf ; 213buf <<testText <<" - " <<file <<" (" <<line <<")" ; 214 215message (TestMessageType ::TestFailure ,buf ); 216} 217 218void TestReporter ::addResultWithLocation ( 219bool testSucceeded , 220const char * testText , 221const char * file , 222int line ) 223{ 224addResultWithLocation ( 225testSucceeded ?TestResult ::Pass :TestResult ::Fail , 226testText , 227file , 228line ); 229} 230 231TestResult TestReporter ::addTest (const String & testName ,bool isPass ) 232{ 233const TestResult res = isPass ?TestResult ::Pass :TestResult ::Fail ; 234addTest (testName ,res ); 235return res ; 236} 237 238void TestReporter ::consolidateWith (TestReporter * other ) 239{ 240m_testInfos .addRange (other -> m_testInfos ); 241m_failedTestCount += other -> m_failedTestCount ; 242m_ignoredTestCount += other -> m_ignoredTestCount ; 243m_passedTestCount += other -> m_passedTestCount ; 244m_expectedFailedTestCount += other -> m_expectedFailedTestCount ; 245m_totalTestCount += other -> m_totalTestCount ; 246} 247 248void TestReporter ::dumpOutputDifference (const String & expectedOutput ,const String & actualOutput ) 249{ 250StringBuilder builder ; 251 252StringUtil ::appendFormat ( 253builder , 254"ERROR:\n" 255"EXPECTED{{{\n%s}}}\n" 256"ACTUAL{{{\n%s}}}\n" , 257expectedOutput .getBuffer (), 258actualOutput .getBuffer ()); 259 260// Add to the m_currentInfo 261message (TestMessageType ::TestFailure ,builder ); 262} 263 264static char _getTeamCityEscapeChar (char c ) 265{ 266switch (c ) 267 { 268case '|' : 269return '|' ; 270case '\'' : 271return '\'' ; 272case '\n' : 273return 'n' ; 274case '\r' : 275return 'r' ; 276case '[' : 277return '[' ; 278case ']' : 279return ']' ; 280default : 281return 0 ; 282 } 283} 284 285static void _appendEncodedTeamCityString (const UnownedStringSlice & in ,StringBuilder & builder ) 286{ 287const char * start = in .begin (); 288const char * cur = start ; 289const char * end = in .end (); 290 291for (const char * cur = start ;cur < end ;cur ++ ) 292 { 293const char c = * cur ; 294const char escapeChar = _getTeamCityEscapeChar (c ); 295if (escapeChar ) 296 { 297// Flush 298if (cur > start ) 299 { 300builder .append (start ,UInt (cur - start )); 301 } 302 303builder .append ('|' ); 304builder .append (escapeChar ); 305start = cur + 1 ; 306 } 307 } 308 309// Flush the end 310if (end > start ) 311 { 312builder .append (start ,UInt (end - start )); 313 } 314} 315 316static void _appendTime (double timeInSec ,StringBuilder & out ) 317{ 318SLANG_ASSERT (timeInSec >=0.0 ); 319if (timeInSec == 0.0 || timeInSec >=1.0 ) 320 { 321out <<timeInSec <<"s" ; 322return ; 323 } 324timeInSec *=1000.0f ; 325if (timeInSec > 1.0f ) 326 { 327out <<timeInSec <<"ms" ; 328return ; 329 } 330timeInSec *=1000.0f ; 331if (timeInSec > 1.0f ) 332 { 333out <<timeInSec <<"us" ; 334return ; 335 } 336 337timeInSec *=1000.0f ; 338out <<timeInSec <<"ns" ; 339} 340 341void TestReporter ::_addResult (TestInfo info ) 342{ 343if (info .testResult == TestResult ::Ignored && m_hideIgnored ) 344 { 345return ; 346 } 347info .testResult = adjustResult (info .name .getUnownedSlice (),info .testResult ); 348 349m_totalTestCount ++ ; 350 351switch (info .testResult ) 352 { 353case TestResult ::Fail : 354m_failedTestCount ++ ; 355break ; 356 357case TestResult ::Pass : 358m_passedTestCount ++ ; 359break ; 360case TestResult ::ExpectedFail : 361m_expectedFailedTestCount ++ ; 362break ; 363 364case TestResult ::Ignored : 365m_ignoredTestCount ++ ; 366break ; 367 368default : 369assert (!"unexpected" ); 370break ; 371 } 372 373m_testInfos .add (info ); 374 375auto defaultOutputFunc = [this ](const TestInfo & info ) 376 { 377// Skip output for passed/ignored tests if verbosity < Info 378if (m_verbosity < VerbosityLevel ::Info ) 379 { 380if (info .testResult == TestResult ::Pass || info .testResult == TestResult ::Ignored ) 381 { 382return ; 383 } 384 } 385 386char const * resultString = "UNEXPECTED" ; 387switch (info .testResult ) 388 { 389case TestResult ::Fail : 390resultString = "FAILED" ; 391break ; 392case TestResult ::ExpectedFail : 393resultString = "failed(expected)" ; 394break ; 395case TestResult ::Pass : 396resultString = "passed" ; 397break ; 398case TestResult ::Ignored : 399resultString = "ignored" ; 400break ; 401default : 402assert (!"unexpected" ); 403break ; 404 } 405 406StringBuilder buffer ; 407if (info .executionTime > 0.0f ) 408 { 409_appendTime (info .executionTime ,buffer ); 410 } 411printf ( 412"%s test: '%S' %s\n" , 413resultString , 414info .name .toWString ().begin (), 415buffer .getBuffer ()); 416fflush (stdout ); 417 }; 418 419switch (m_outputMode ) 420 { 421default : 422 { 423defaultOutputFunc (info ); 424break ; 425 } 426case TestOutputMode ::TeamCity : 427 { 428StringBuilder escapedTestName ; 429_appendEncodedTeamCityString (info .name .getUnownedSlice (),escapedTestName ); 430 431printf ("##teamcity[testStarted name='%s']\n" ,escapedTestName .begin ()); 432 433switch (info .testResult ) 434 { 435case TestResult ::Fail : 436 { 437if (info .message .getLength ()) 438 { 439StringBuilder escapedMessage ; 440_appendEncodedTeamCityString ( 441info .message .getUnownedSlice (), 442escapedMessage ); 443printf ( 444"##teamcity[testFailed name='%s' message='%s']\n" , 445escapedTestName .begin (), 446escapedMessage .begin ()); 447 } 448else 449 { 450printf ("##teamcity[testFailed name='%s']\n" ,escapedTestName .begin ()); 451 } 452break ; 453 } 454case TestResult ::Pass : 455case TestResult ::ExpectedFail : 456 { 457StringBuilder message ; 458message <<info .message ; 459// Add execution time if one is set 460if (info .executionTime > 0.0 ) 461 { 462if (message .getLength ()) 463 { 464message <<" " ; 465 } 466_appendTime (info .executionTime ,message ); 467 } 468 469if (message .getLength ()) 470 { 471StringBuilder escapedMessage ; 472_appendEncodedTeamCityString (message .getUnownedSlice (),escapedMessage ); 473printf ( 474"##teamcity[testStdOut name='%s' out='%s']\n" , 475escapedTestName .begin (), 476escapedMessage .begin ()); 477 } 478break ; 479 } 480case TestResult ::Ignored : 481 { 482if (info .message .getLength ()) 483 { 484StringBuilder escapedMessage ; 485_appendEncodedTeamCityString ( 486info .message .getUnownedSlice (), 487escapedMessage ); 488 489printf ( 490"##teamcity[testIgnored name='%s' message='%s']\n" , 491escapedTestName .begin (), 492escapedMessage .begin ()); 493 } 494else 495 { 496printf ("##teamcity[testIgnored name='%s']\n" ,escapedTestName .begin ()); 497 } 498break ; 499 } 500default : 501assert (!"unexpected" ); 502break ; 503 } 504 505printf ("##teamcity[testFinished name='%s']\n" ,escapedTestName .begin ()); 506fflush (stdout ); 507break ; 508 } 509case TestOutputMode ::XUnit2 : 510case TestOutputMode ::XUnit : 511 { 512// Don't output anything -> we'll output all in one go at the end 513break ; 514 } 515case TestOutputMode ::AppVeyor : 516 { 517char const * resultString = "None" ; 518switch (info .testResult ) 519 { 520case TestResult ::Fail : 521resultString = "Failed" ; 522break ; 523case TestResult ::Pass : 524resultString = "Passed" ; 525break ; 526case TestResult ::Ignored : 527resultString = "Ignored" ; 528break ; 529case TestResult ::ExpectedFail : 530resultString = "ExpectedFail" ; 531break ; 532 533default : 534assert (!"unexpected" ); 535break ; 536 } 537 538// https://www.appveyor.com/docs/build-worker-api/#add-tests 539 540CommandLine cmdLine ; 541cmdLine .setExecutableLocation (ExecutableLocation ("appveyor" )); 542cmdLine .addArg ("AddTest" ); 543cmdLine .addArg (info .name ); 544cmdLine .addArg ("-FileName" ); 545// TODO: this isn't actually a file name in all cases 546cmdLine .addArg (info .name ); 547cmdLine .addArg ("-Framework" ); 548cmdLine .addArg ("slang-test" ); 549cmdLine .addArg ("-Outcome" ); 550cmdLine .addArg (resultString ); 551 552// If has execution time output it 553if (info .executionTime > 0.0 ) 554 { 555StringBuilder builder ; 556_appendTime (info .executionTime ,builder ); 557cmdLine .addArg ("-StdOut" ); 558cmdLine .addArg (builder ); 559 } 560 561ExecuteResult exeRes ; 562SlangResult res = ProcessUtil ::execute (cmdLine ,exeRes ); 563 564if (SLANG_FAILED (res )) 565 { 566messageFormat ( 567TestMessageType ::Info , 568"failed to add appveyor test results for '%S'\n" , 569info .name .toWString ().begin ()); 570 571#if 0 572String cmdLineString = ProcessUtil ::getCommandLineString (cmdLine ); 573fprintf (stderr ,"[%d] TEST RESULT: %s {%d} {%s} {%s}\n" ,err ,cmdLineString .getBuffer (), 574exeRes .resultCode , 575exeRes .standardOutput .begin (), 576exeRes .standardError .begin ()); 577#endif 578 } 579defaultOutputFunc (info ); 580break ; 581 } 582 } 583} 584 585void TestReporter ::addTest (const String & testName ,TestResult testResult ) 586{ 587// Can't add this way if in test 588assert (!m_inTest ); 589 590TestInfo info ; 591info .name = testName ; 592info .testResult = testResult ; 593_addResult (info ); 594} 595 596void TestReporter ::message (TestMessageType type ,const String & message ) 597{ 598 std::lock_guard < std::recursive_mutex > lock (m_mutex ); 599 600if (type == TestMessageType ::Info ) 601 { 602if (m_verbosity == VerbosityLevel ::Verbose && canWriteStdError ()) 603 { 604fputs (message .getBuffer (),stderr ); 605 } 606fflush (stderr ); 607// Just dump out if can dump out 608return ; 609 } 610 611if (canWriteStdError ()) 612 { 613fprintf (stderr ,"[%s] " ,m_currentInfo .name .getBuffer ()); 614if (type == TestMessageType ::RunError || type == TestMessageType ::TestFailure ) 615 { 616fputs (message .getBuffer (),stderr ); 617fprintf (stderr ,"\n" ); 618 } 619else 620 { 621fputs (message .getBuffer (),stderr ); 622 } 623fflush (stderr ); 624 } 625 626if (m_currentMessage .getLength ()> 0 ) 627 { 628m_currentMessage <<"\n" ; 629 } 630 631if (m_currentInfo .name .getLength ()) 632m_currentMessage <<"[" <<m_currentInfo .name <<"] " ; 633m_currentMessage .append (message ); 634} 635 636void TestReporter ::messageFormat (TestMessageType type ,char const * format , ...) 637{ 638StringBuilder builder ; 639 640va_list args ; 641va_start (args ,format ); 642StringUtil ::append (format ,args ,builder ); 643va_end (args ); 644 645message (type ,builder ); 646} 647 648void TestReporter ::message (TestMessageType type ,const char * messageContent ) 649{ 650message (type ,String (messageContent )); 651} 652 653 654bool TestReporter ::didAllSucceed ()const 655{ 656return m_failedTestCount == 0 ; 657} 658 659void TestReporter ::outputSummary () 660{ 661auto passCount = m_passedTestCount ; 662auto rawTotal = m_totalTestCount ; 663auto ignoredCount = m_ignoredTestCount ; 664 665auto runTotal = rawTotal - ignoredCount ; 666 667switch (m_outputMode ) 668 { 669default : 670 { 671if (!m_totalTestCount ) 672 { 673printf ("no tests run\n" ); 674return ; 675 } 676 677int percentPassed = 0 ; 678if (runTotal > 0 ) 679 { 680percentPassed = (passCount * 100 ) /runTotal ; 681 } 682 683printf ("\n===\n%d%% of tests passed (%d/%d)" ,percentPassed ,passCount ,runTotal ); 684if (ignoredCount ) 685 { 686printf (", %d tests ignored" ,ignoredCount ); 687 } 688if (m_expectedFailedTestCount ) 689 { 690printf (", %d tests failed expectedly" ,m_expectedFailedTestCount ); 691printf ("\n===\n\n" ); 692printf ("\npassing tests that are expected to fail:\n" ); 693printf ("---\n" ); 694for (const auto & testInfo :m_testInfos ) 695 { 696if (testInfo .testResult == TestResult ::Pass ) 697 { 698if (m_expectedFailureList .contains (testInfo .name )) 699 { 700printf ("%s\n" ,testInfo .name .getBuffer ()); 701 } 702 } 703 } 704printf ("---\n" ); 705 } 706printf ("\n===\n\n" ); 707if (m_failedTestCount ) 708 { 709printf ("%d failing tests:\n" ,m_failedTestCount ); 710printf ("---\n" ); 711for (const auto & testInfo :m_testInfos ) 712 { 713if (testInfo .testResult == TestResult ::Fail ) 714 { 715printf ("%s\n" ,testInfo .name .getBuffer ()); 716 } 717 } 718printf ("---\n" ); 719 } 720 721break ; 722 } 723 724case TestOutputMode ::XUnit : 725 { 726// xUnit 1.0 format 727 728printf ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ); 729printf ( 730"<testsuites tests=\"%d\" failures=\"%d\" disabled=\"%d\" errors=\"0\" " 731"name=\"AllTests\">\n" , 732m_totalTestCount , 733m_failedTestCount , 734m_ignoredTestCount ); 735printf ( 736" <testsuite name=\"all\" tests=\"%d\" failures=\"%d\" disabled=\"%d\" " 737"errors=\"0\" time=\"0\">\n" , 738m_totalTestCount , 739m_failedTestCount , 740m_ignoredTestCount ); 741 742for (const auto & testInfo :m_testInfos ) 743 { 744const int numFailed = (testInfo .testResult == TestResult ::Fail ); 745const int numIgnored = (testInfo .testResult == TestResult ::Ignored ); 746// int numPassed = (testInfo.testResult == TestResult::ePass); 747 748if (testInfo .testResult == TestResult ::Pass ) 749 { 750printf ( 751" <testcase name=\"%s\" status=\"run\"/>\n" , 752testInfo .name .getBuffer ()); 753 } 754else 755 { 756printf ( 757" <testcase name=\"%s\" status=\"run\">\n" , 758testInfo .name .getBuffer ()); 759switch (testInfo .testResult ) 760 { 761case TestResult ::Fail : 762 { 763StringBuilder buf ; 764appendXmlEncode (testInfo .message ,buf ); 765 766printf (" <error>\n" ); 767printf ("%s" ,buf .getBuffer ()); 768printf (" </error>\n" ); 769break ; 770 } 771case TestResult ::Ignored : 772 { 773printf (" <skip>Ignored</skip>\n" ); 774break ; 775 } 776default : 777break ; 778 } 779printf (" </testcase>\n" ); 780 } 781 } 782 783printf (" </testsuite>\n" ); 784printf ("</testSuites>\n" ); 785break ; 786 } 787case TestOutputMode ::XUnit2 : 788 { 789// https://xunit.github.io/docs/format-xml-v2 790assert ("Not currently supported" ); 791break ; 792 } 793case TestOutputMode ::TeamCity : 794 { 795// Don't output a summary 796break ; 797 } 798 } 799} 800 801void TestReporter ::startSuite (const String & name ) 802{ 803m_suiteStack .add (name ); 804 805switch (m_outputMode ) 806 { 807case TestOutputMode ::TeamCity : 808 { 809if (!m_isSubReporter ) 810 { 811StringBuilder escapedSuiteName ; 812_appendEncodedTeamCityString (name .getUnownedSlice (),escapedSuiteName ); 813printf ("##teamcity[testSuiteStarted name='%s']\n" ,escapedSuiteName .begin ()); 814 } 815break ; 816 } 817default : 818break ; 819 } 820} 821 822void TestReporter ::endSuite () 823{ 824assert (m_suiteStack .getCount ()); 825 826switch (m_outputMode ) 827 { 828case TestOutputMode ::TeamCity : 829 { 830if (!m_isSubReporter ) 831 { 832const String & name = m_suiteStack .getLast (); 833StringBuilder escapedSuiteName ; 834_appendEncodedTeamCityString (name .getUnownedSlice (),escapedSuiteName ); 835printf ("##teamcity[testSuiteFinished name='%s']\n" ,escapedSuiteName .begin ()); 836 } 837break ; 838 } 839default : 840break ; 841 } 842 843m_suiteStack .removeLast (); 844} 845 846TestResult TestReporter ::adjustResult (UnownedStringSlice testName ,TestResult result ) 847{ 848if (result == TestResult ::Fail && m_expectedFailureList .contains (testName )) 849result = TestResult ::ExpectedFail ; 850return result ; 851}