FastLED 3.9.15
Loading...
Searching...
No Matches
fltest.cpp.hpp
Go to the documentation of this file.
1
3
4#include "fl/test/fltest.h"
5#include "fl/log/log.h"
6#include "fl/stl/stdio.h"
7#include "fl/stl/noexcept.h"
8
9namespace fl {
10namespace test {
11
12// =============================================================================
13// DefaultReporter implementation
14// =============================================================================
15
17 fl::printf("\n");
18 fl::printf("===============================================================================\n");
19 fl::printf("FL TEST: Running tests...\n");
20 fl::printf("===============================================================================\n");
21}
22
24 fl::printf("\n");
25 fl::printf("===============================================================================\n");
26 fl::printf("FL TEST: Results\n");
27 fl::printf("-------------------------------------------------------------------------------\n");
28 if (stats.mTestCasesSkipped > 0) {
29 fl::printf("Test cases: %u passed, %u failed, %u skipped, %u total\n",
30 stats.mTestCasesPassed, stats.mTestCasesFailed,
31 stats.mTestCasesSkipped, stats.mTestCasesRun);
32 } else {
33 fl::printf("Test cases: %u passed, %u failed, %u total\n",
34 stats.mTestCasesPassed, stats.mTestCasesFailed, stats.mTestCasesRun);
35 }
36 fl::printf("Assertions: %u passed, %u failed\n",
37 stats.mAssertsPassed, stats.mAssertsFailed);
38 if (stats.mTotalDurationMs > 0) {
39 fl::printf("Duration: %u ms\n", stats.mTotalDurationMs);
40 }
41 fl::printf("===============================================================================\n");
42
43 if (stats.allPassed()) {
44 fl::printf("Status: SUCCESS\n");
45 } else {
46 fl::printf("Status: FAILURE\n");
47 }
48 fl::printf("\n");
49}
50
52 fl::printf("\n--- Test: %s\n", name);
53}
54
55void DefaultReporter::testCaseEnd(bool passed, fl::u32 durationMs) FL_NOEXCEPT {
56 if (passed) {
57 if (durationMs > 0) {
58 fl::printf(" [PASSED] (%u ms)\n", durationMs);
59 } else {
60 fl::printf(" [PASSED]\n");
61 }
62 } else {
63 if (durationMs > 0) {
64 fl::printf(" [FAILED] (%u ms)\n", durationMs);
65 } else {
66 fl::printf(" [FAILED]\n");
67 }
68 }
69}
70
72 fl::printf(" Subcase: %s\n", name);
73}
74
76 // Nothing needed
77}
78
80 if (!result.mPassed) {
81 fl::printf(" FAILED: %s:%d\n", result.mLocation.mFile, result.mLocation.mLine);
82 fl::printf(" Expression: %s\n", result.mExpression.c_str());
83 if (!result.mExpanded.empty()) {
84 fl::printf(" Expanded: %s\n", result.mExpanded.c_str());
85 }
86 }
87}
88
89// =============================================================================
90// TestContext implementation
91// =============================================================================
92
96
98 static TestContext ctx;
99 return ctx;
100}
101
102int TestContext::registerTest(TestFunc func, const char* name, const char* file, int line) FL_NOEXCEPT {
103 TestCaseInfo info;
104 info.mFunc = func;
105 info.mName = name;
106 info.mFile = file;
107 info.mLine = line;
108 mTestCases.push_back(info);
109 return static_cast<int>(mTestCases.size());
110}
111
112int TestContext::run(int argc, const char* const* argv) FL_NOEXCEPT {
113 // Extract filter from first arg if present
114 const char* filter = nullptr;
115 if (argc > 1 && argv && argv[1]) {
116 filter = argv[1];
117 }
118 return run(filter);
119}
120
121int TestContext::run(const char* filter) FL_NOEXCEPT {
122 mStats.reset();
123 mReporter->testRunStart();
124
125 for (fl::size i = 0; i < mTestCases.size(); ++i) {
126 // Apply filter if provided
127 if (filter && filter[0] != '\0') {
128 if (!matchesFilter(mTestCases[i].mName.c_str(), filter)) {
129 continue; // Skip tests that don't match
130 }
131 }
133 }
134
135 mReporter->testRunEnd(mStats);
136 return mStats.allPassed() ? 0 : 1;
137}
138
139fl::size TestContext::listTests(const char* filter) const FL_NOEXCEPT {
140 fl::size count = 0;
141 fl::printf("\nRegistered tests:\n");
142 fl::printf("----------------\n");
143
144 for (fl::size i = 0; i < mTestCases.size(); ++i) {
145 // Apply filter if provided
146 if (filter && filter[0] != '\0') {
147 if (!matchesFilter(mTestCases[i].mName.c_str(), filter)) {
148 continue;
149 }
150 }
151 count++;
152 fl::printf(" [%u] %s\n", static_cast<fl::u32>(count), mTestCases[i].mName.c_str());
153 fl::printf(" File: %s:%d\n", mTestCases[i].mFile, mTestCases[i].mLine);
154 }
155
156 fl::printf("----------------\n");
157 fl::printf("Total: %u tests\n\n", static_cast<fl::u32>(count));
158 return count;
159}
160
161bool TestContext::matchesFilter(const char* name, const char* filter) const FL_NOEXCEPT {
162 if (!filter || filter[0] == '\0') {
163 return true; // No filter means match all
164 }
165
166 // Simple wildcard matching: * matches any sequence, ? matches one char
167 const char* n = name;
168 const char* f = filter;
169
170 // Check if filter contains wildcards
171 bool hasWildcards = false;
172 for (const char* p = filter; *p; ++p) {
173 if (*p == '*' || *p == '?') {
174 hasWildcards = true;
175 break;
176 }
177 }
178
179 if (!hasWildcards) {
180 // No wildcards - do substring match
181 while (*n) {
182 const char* nn = n;
183 const char* ff = filter;
184 while (*nn && *ff && *nn == *ff) {
185 ++nn;
186 ++ff;
187 }
188 if (*ff == '\0') {
189 return true; // Found substring match
190 }
191 ++n;
192 }
193 return false;
194 }
195
196 // Wildcard matching using recursion simulation with stack
197 while (*f) {
198 if (*f == '*') {
199 ++f;
200 if (*f == '\0') {
201 return true; // Trailing * matches everything
202 }
203 // Try matching remaining pattern at each position
204 while (*n) {
205 if (matchesFilter(n, f)) {
206 return true;
207 }
208 ++n;
209 }
210 return false;
211 } else if (*f == '?') {
212 if (*n == '\0') {
213 return false; // ? needs a character to match
214 }
215 ++f;
216 ++n;
217 } else {
218 if (*n != *f) {
219 return false; // Literal mismatch
220 }
221 ++f;
222 ++n;
223 }
224 }
225
226 return *n == '\0'; // Both must be exhausted
227}
228
229// Skip test support - global state
230// Thread-local would be ideal, but for embedded compatibility we use a global
231static bool sCurrentTestSkipped = false;
232static const char* sSkipReason = nullptr;
233
235 mStats.mTestCasesRun++;
236 mReporter->testCaseStart(info.mName.c_str());
237
238 mCurrentTestFailed = false;
239 mCurrentTestTimedOut = false;
240 mCurrentTestName = info.mName.c_str();
241 mSubcaseStack.clear();
242 mNextSubcaseStack.clear();
243 mFullyTraversedHashes.clear();
246 mShouldReenter = true;
247
248 // Reset skip flag for this test
249 sCurrentTestSkipped = false;
250
251 // Record test start time if we have a time function
252 if (mGetMillis) {
254 }
255
256 // Run the test multiple times to explore all subcase paths
257 // On each run, we follow the path in mNextSubcaseStack and then discover one new subcase
259 mShouldReenter = false;
262
263 // Copy next stack to current stack for this iteration
265 mNextSubcaseStack.clear();
266
267 // Call the test function
268 info.mFunc();
269
270 // Check for skip (FL_SKIP was called)
272 break;
273 }
274
275 // Check for timeout after each iteration
276 if (mDefaultTimeoutMs > 0 && checkTimeout()) {
277 break;
278 }
279 }
280
281 mCurrentTestName = nullptr;
282
283 // Calculate test duration
284 fl::u32 testDurationMs = 0;
285 if (mGetMillis) {
286 testDurationMs = mGetMillis() - mCurrentTestStartMs;
287 mStats.mTotalDurationMs += testDurationMs;
288 }
289
291 mStats.mTestCasesSkipped++;
292 mReporter->testCaseEnd(true, testDurationMs); // Skipped tests count as "not failed"
294 mStats.mTestCasesFailed++;
295 mReporter->testCaseEnd(false, testDurationMs);
296 } else {
297 mStats.mTestCasesPassed++;
298 mReporter->testCaseEnd(true, testDurationMs);
299 }
300}
301
303 if (!mGetMillis || mDefaultTimeoutMs == 0) {
304 return false; // No timeout configured
305 }
306
307 fl::u32 now = mGetMillis();
308 fl::u32 elapsed = now - mCurrentTestStartMs;
309
310 if (elapsed > mDefaultTimeoutMs) {
312
313 // Call timeout handler if set
314 if (mTimeoutHandler) {
315 mTimeoutHandler(mCurrentTestName ? mCurrentTestName : "unknown", elapsed);
316 } else {
317 // Default behavior: print timeout message
318 fl::printf(" [TIMEOUT] Test exceeded %u ms (elapsed: %u ms)\n",
319 mDefaultTimeoutMs, elapsed);
320 }
321 return true;
322 }
323 return false;
324}
325
327 if (!mGetMillis) {
328 return 0;
329 }
331}
332
334 // We're at subcase discovery depth mSubcaseDiscoveryDepth
335 // mSubcaseStack contains the path we're following this iteration
336
338 // We're following a predetermined path
340 // This is the subcase we should enter
343 mReporter->subcaseStart(sig.mName);
344 return true;
345 }
346 // Not the subcase we're looking for, skip it
347 return false;
348 } else {
349 // We're past our predetermined path, discovering new subcases
350 fl::u32 pathHash = hashCurrentPath(sig);
351
352 if (isFullyTraversed(pathHash)) {
353 // This subcase path has already been fully explored
354 return false;
355 }
356
357 if (!mShouldReenter) {
358 // First untraversed subcase at this level - enter it
361 mReporter->subcaseStart(sig.mName);
362 return true;
363 } else {
364 // We've already found a subcase to explore next time
365 // Just record this one for future exploration
367 // Build the path to this subcase for next iteration
368 mNextSubcaseStack.clear();
369 for (fl::size i = 0; i < mSubcaseDiscoveryDepth && i < mSubcaseStack.size(); ++i) {
370 mNextSubcaseStack.push_back(mSubcaseStack[i]);
371 }
372 mNextSubcaseStack.push_back(sig);
373 }
374 return false;
375 }
376 }
377}
378
382
383 // If we don't have a next path queued, we might need to reenter
384 // to explore sibling subcases
386 // Mark this path as traversed
388 } else if (mNextSubcaseStack.size() > 0) {
389 // We have more paths to explore
390 mShouldReenter = true;
391 }
392
393 mReporter->subcaseEnd();
394}
395
397 if (result.mPassed) {
398 mStats.mAssertsPassed++;
399 } else {
400 mStats.mAssertsFailed++;
401 mCurrentTestFailed = true;
402 }
403 mReporter->assertResult(result);
404}
405
406void TestContext::checkFailed(const char* expr, const char* file, int line) FL_NOEXCEPT {
407 AssertResult result(false);
408 result.mExpression = expr;
409 result.mLocation = SourceLocation(file, line);
411}
412
413void TestContext::requireFailed(const char* expr, const char* file, int line) FL_NOEXCEPT {
414 checkFailed(expr, file, line);
415 // Note: The return happens in the macro
416}
417
419 fl::u32 hash = 0;
420 for (fl::size i = 0; i < mSubcaseDiscoveryDepth && i < mSubcaseStack.size(); ++i) {
421 hash = hash * 31 + hashSubcaseSignature(mSubcaseStack[i]);
422 }
423 hash = hash * 31 + hashSubcaseSignature(sig);
424 return hash;
425}
426
428 for (fl::size i = 0; i < mFullyTraversedHashes.size(); ++i) {
429 if (mFullyTraversedHashes[i] == hash) {
430 return true;
431 }
432 }
433 return false;
434}
435
437 if (!isFullyTraversed(hash)) {
438 mFullyTraversedHashes.push_back(hash);
439 }
440}
441
442// =============================================================================
443// Subcase implementation
444// =============================================================================
445
446Subcase::Subcase(const char* name, const char* file, int line) FL_NOEXCEPT
447 : mSignature{name, file, line}
448 , mEntered(false) {
449 mEntered = TestContext::instance().enterSubcase(mSignature);
450}
451
457
458// =============================================================================
459// Message/Capture/Fail helpers
460// =============================================================================
461
462void outputMessage(const char* msg, const char* file, int line) FL_NOEXCEPT {
463 fl::printf(" [MESSAGE] %s:%d: %s\n", file, line, msg);
464}
465
466void outputCapture(const char* name, const char* value, const char* file, int line) FL_NOEXCEPT {
467 fl::printf(" [CAPTURE] %s:%d: %s := %s\n", file, line, name, value);
468}
469
470void fail(const char* msg, const char* file, int line, bool isFatal) FL_NOEXCEPT {
471 AssertResult result(false);
472 result.mExpression = msg;
473 result.mLocation = SourceLocation(file, line);
475 if (isFatal) {
476 fl::printf(" FAIL (fatal): %s:%d: %s\n", file, line, msg);
477 } else {
478 fl::printf(" FAIL_CHECK: %s:%d: %s\n", file, line, msg);
479 }
480}
481
482// =============================================================================
483// Skip test support functions
484// =============================================================================
485
486// Note: sCurrentTestSkipped and sSkipReason are defined above runTestCase()
487
488void skipTest(const char* reason, const char* file, int line) FL_NOEXCEPT {
489 sCurrentTestSkipped = true;
490 sSkipReason = reason;
491 fl::printf(" [SKIPPED] %s:%d: %s\n", file, line, reason);
492}
493
497
498// =============================================================================
499// SerialReporter implementation
500// =============================================================================
501
502// Helper function to print using either custom print func or fl::printf
503void SerialReporter::print(const char* msg) FL_NOEXCEPT {
504 if (mPrintFunc) {
505 mPrintFunc(msg);
506 } else {
507 fl::printf("%s", msg);
508 }
509}
510
512 print("\n");
513 print("====== FL TEST: Running tests... ======\n");
514}
515
517 print("\n====== FL TEST: Results ======\n");
518
519 // Format stats using snprintf-style approach
520 fl::sstream ss;
521 ss << "Passed: " << stats.mTestCasesPassed
522 << "/" << stats.mTestCasesRun << " tests\n";
523 print(ss.str().c_str());
524
525 if (stats.mTestCasesSkipped > 0) {
526 fl::sstream ss_skip;
527 ss_skip << "Skipped: " << stats.mTestCasesSkipped << "\n";
528 print(ss_skip.str().c_str());
529 }
530
531 if (stats.mAssertsFailed > 0) {
532 fl::sstream ss2;
533 ss2 << "Failed assertions: " << stats.mAssertsFailed << "\n";
534 print(ss2.str().c_str());
535 }
536
537 if (stats.allPassed()) {
538 print("Status: PASS\n");
539 } else {
540 print("Status: FAIL\n");
541 }
542}
543
545 fl::sstream ss;
546 ss << "\n[TEST] " << name << "\n";
547 print(ss.str().c_str());
548}
549
550void SerialReporter::testCaseEnd(bool passed, fl::u32 durationMs) FL_NOEXCEPT {
551 if (passed) {
552 if (durationMs > 0) {
553 fl::sstream ss;
554 ss << "[PASS] (" << durationMs << " ms)\n";
555 print(ss.str().c_str());
556 } else {
557 print("[PASS]\n");
558 }
559 } else {
560 if (durationMs > 0) {
561 fl::sstream ss;
562 ss << "[FAIL] (" << durationMs << " ms)\n";
563 print(ss.str().c_str());
564 } else {
565 print("[FAIL]\n");
566 }
567 }
568}
569
571 fl::sstream ss;
572 ss << " [SUBCASE] " << name << "\n";
573 print(ss.str().c_str());
574}
575
577 // Nothing needed
578}
579
581 if (!result.mPassed) {
582 fl::sstream ss;
583 ss << " FAIL: " << result.mLocation.mFile
584 << ":" << result.mLocation.mLine << "\n";
585 ss << " Expr: " << result.mExpression << "\n";
586 if (!result.mExpanded.empty()) {
587 ss << " Got: " << result.mExpanded << "\n";
588 }
589 print(ss.str().c_str());
590 }
591}
592
593// =============================================================================
594// XMLReporter implementation (JUnit format)
595// =============================================================================
596
598 if (!text) return fl::string();
599
601 for (const char* p = text; *p; ++p) {
602 switch (*p) {
603 case '&': result += "&amp;"; break;
604 case '<': result += "&lt;"; break;
605 case '>': result += "&gt;"; break;
606 case '"': result += "&quot;"; break;
607 case '\'': result += "&apos;"; break;
608 default: result += *p; break;
609 }
610 }
611 return result;
612}
613
617
619 if (!mOutput) return;
620
621 fl::sstream ss;
622
623 // XML header
624 ss << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
625
626 // Testsuite element with summary
627 ss << "<testsuite name=\"" << escapeXml(mSuiteName) << "\" ";
628 ss << "tests=\"" << stats.mTestCasesRun << "\" ";
629 ss << "failures=\"" << stats.mTestCasesFailed << "\" ";
630 ss << "errors=\"0\" ";
631 ss << "skipped=\"" << stats.mTestCasesSkipped << "\">\n";
632
633 // Add all test case results
634 for (fl::size i = 0; i < mTestCaseResults.size(); ++i) {
635 ss << mTestCaseResults[i];
636 }
637
638 ss << "</testsuite>\n";
639
640 *mOutput = ss.str();
641}
642
644 mCurrentTestName = name ? name : "unknown";
645 mCurrentTestFailures.clear();
646 mCurrentTestPassed = true;
647}
648
649void XMLReporter::testCaseEnd(bool passed, fl::u32 durationMs) FL_NOEXCEPT {
650 fl::sstream ss;
651 ss << " <testcase name=\"" << escapeXml(mCurrentTestName.c_str()) << "\"";
652
653 // Add time attribute if duration is available (convert ms to seconds)
654 if (durationMs > 0) {
655 // Format as seconds with millisecond precision
656 fl::u32 secs = durationMs / 1000;
657 fl::u32 millis = durationMs % 1000;
658 ss << " time=\"" << secs << "." << (millis < 100 ? (millis < 10 ? "00" : "0") : "") << millis << "\"";
659 }
660
661 if (passed) {
662 ss << "/>\n";
663 } else {
664 ss << ">\n";
665 ss << " <failure message=\"Test failed\">\n";
666 ss << "<![CDATA[" << mCurrentTestFailures << "]]>\n";
667 ss << " </failure>\n";
668 ss << " </testcase>\n";
669 }
670
671 mTestCaseResults.push_back(ss.str());
672}
673
674void XMLReporter::subcaseStart(const char* /*name*/) FL_NOEXCEPT {
675 // Subcases are noted in the failure message if they fail
676}
677
679 // Nothing to do
680}
681
683 if (!result.mPassed) {
684 mCurrentTestPassed = false;
685
686 fl::sstream ss;
687 ss << result.mLocation.mFile << ":" << result.mLocation.mLine << "\n";
688 ss << " Expression: " << result.mExpression << "\n";
689 if (!result.mExpanded.empty()) {
690 ss << " Expanded: " << result.mExpanded << "\n";
691 }
692 ss << "\n";
694 }
695}
696
697// =============================================================================
698// JSONReporter implementation
699// =============================================================================
700
702 if (!text) return fl::string();
703
705 for (const char* p = text; *p; ++p) {
706 switch (*p) {
707 case '\\': result += "\\\\"; break;
708 case '"': result += "\\\""; break;
709 case '\n': result += "\\n"; break;
710 case '\r': result += "\\r"; break;
711 case '\t': result += "\\t"; break;
712 default:
713 // Handle control characters
714 if (static_cast<unsigned char>(*p) < 32) {
715 // Skip control characters or encode them
716 result += "\\u00";
717 char hex[3];
718 hex[0] = "0123456789abcdef"[(*p >> 4) & 0xF];
719 hex[1] = "0123456789abcdef"[*p & 0xF];
720 hex[2] = '\0';
721 result += hex;
722 } else {
723 result += *p;
724 }
725 break;
726 }
727 }
728 return result;
729}
730
734
736 if (!mOutput) return;
737
738 fl::sstream ss;
739
740 ss << "{\n";
741 ss << " \"summary\": {\n";
742 ss << " \"total\": " << stats.mTestCasesRun << ",\n";
743 ss << " \"passed\": " << stats.mTestCasesPassed << ",\n";
744 ss << " \"failed\": " << stats.mTestCasesFailed << ",\n";
745 ss << " \"skipped\": " << stats.mTestCasesSkipped << ",\n";
746 ss << " \"assertionsPassed\": " << stats.mAssertsPassed << ",\n";
747 ss << " \"assertionsFailed\": " << stats.mAssertsFailed << "\n";
748 ss << " },\n";
749 ss << " \"tests\": [\n";
750
751 for (fl::size i = 0; i < mTestResults.size(); ++i) {
752 ss << mTestResults[i];
753 if (i < mTestResults.size() - 1) {
754 ss << ",";
755 }
756 ss << "\n";
757 }
758
759 ss << " ]\n";
760 ss << "}\n";
761
762 *mOutput = ss.str();
763}
764
766 mCurrentTestName = name ? name : "unknown";
767 mCurrentTestFailures.clear();
768 mCurrentTestPassed = true;
769}
770
771void JSONReporter::testCaseEnd(bool passed, fl::u32 durationMs) FL_NOEXCEPT {
772 fl::sstream ss;
773 ss << " {\n";
774 ss << " \"name\": \"" << escapeJson(mCurrentTestName.c_str()) << "\",\n";
775 ss << " \"passed\": " << (passed ? "true" : "false");
776
777 // Add duration if available
778 if (durationMs > 0) {
779 ss << ",\n";
780 ss << " \"durationMs\": " << durationMs;
781 }
782
783 if (!passed && !mCurrentTestFailures.empty()) {
784 ss << ",\n";
785 ss << " \"failures\": [\n";
786 for (fl::size i = 0; i < mCurrentTestFailures.size(); ++i) {
787 ss << " " << mCurrentTestFailures[i];
788 if (i < mCurrentTestFailures.size() - 1) {
789 ss << ",";
790 }
791 ss << "\n";
792 }
793 ss << " ]\n";
794 } else {
795 ss << "\n";
796 }
797
798 ss << " }";
799
800 mTestResults.push_back(ss.str());
801}
802
803void JSONReporter::subcaseStart(const char* /*name*/) FL_NOEXCEPT {
804 // Subcases are noted in failures
805}
806
808 // Nothing to do
809}
810
812 if (!result.mPassed) {
813 mCurrentTestPassed = false;
814
815 fl::sstream ss;
816 ss << "{\n";
817 ss << " \"file\": \"" << escapeJson(result.mLocation.mFile) << "\",\n";
818 ss << " \"line\": " << result.mLocation.mLine << ",\n";
819 ss << " \"expression\": \"" << escapeJson(result.mExpression.c_str()) << "\"";
820
821 if (!result.mExpanded.empty()) {
822 ss << ",\n";
823 ss << " \"expanded\": \"" << escapeJson(result.mExpanded.c_str()) << "\"";
824 }
825 ss << "\n }";
826
827 mCurrentTestFailures.push_back(ss.str());
828 }
829}
830
831// =============================================================================
832// TAPReporter implementation (Test Anything Protocol)
833// =============================================================================
834
835void TAPReporter::output(const char* line) FL_NOEXCEPT {
836 if (mPrintFunc) {
837 mPrintFunc(line);
838 mPrintFunc("\n");
839 } else if (mOutput) {
840 mStreamingOutput += line;
841 mStreamingOutput += "\n";
842 }
843}
844
846 mTestNumber = 0;
847 mStreamingOutput.clear();
848
849 // Output TAP version header
850 output("TAP version 13");
851
852 // If we know the total, output the plan at the start
853 if (mTotalTests > 0) {
854 fl::sstream ss;
855 ss << "1.." << mTotalTests;
856 output(ss.str().c_str());
857 }
858}
859
861 // If we didn't know the total, output the plan at the end
862 if (mTotalTests == 0) {
863 fl::sstream ss;
864 ss << "1.." << stats.mTestCasesRun;
865 output(ss.str().c_str());
866 }
867
868 // Output summary as diagnostics
869 fl::sstream ss;
870 ss << "# Tests: " << stats.mTestCasesRun
871 << ", Passed: " << stats.mTestCasesPassed
872 << ", Failed: " << stats.mTestCasesFailed;
873 if (stats.mTestCasesSkipped > 0) {
874 ss << ", Skipped: " << stats.mTestCasesSkipped;
875 }
876 output(ss.str().c_str());
877
878 // Copy to output buffer if using buffer mode
879 if (mOutput) {
881 }
882}
883
885 mTestNumber++;
886 mCurrentTestName = name ? name : "unknown";
887 mCurrentTestPassed = true;
888 mDiagnostics.clear();
889}
890
891void TAPReporter::testCaseEnd(bool passed, fl::u32 durationMs) FL_NOEXCEPT {
892 fl::sstream ss;
893
894 if (passed) {
895 ss << "ok " << mTestNumber << " - " << mCurrentTestName;
896 } else {
897 ss << "not ok " << mTestNumber << " - " << mCurrentTestName;
898 }
899
900 // TAP supports duration via directive (non-standard but widely recognized)
901 if (durationMs > 0) {
902 ss << " # (" << durationMs << " ms)";
903 }
904 output(ss.str().c_str());
905
906 // Output diagnostics for failed tests
907 for (fl::size i = 0; i < mDiagnostics.size(); ++i) {
908 fl::sstream diag;
909 diag << "# " << mDiagnostics[i];
910 output(diag.str().c_str());
911 }
912}
913
915 // TAP doesn't have native subcase support, but we can add diagnostics
916 fl::sstream ss;
917 ss << " Subcase: " << (name ? name : "unknown");
918 mDiagnostics.push_back(ss.str());
919}
920
922 // Nothing to do
923}
924
926 if (!result.mPassed) {
927 mCurrentTestPassed = false;
928
929 fl::sstream ss;
930 ss << " Failed at " << result.mLocation.mFile << ":" << result.mLocation.mLine;
931 mDiagnostics.push_back(ss.str());
932
933 fl::sstream ss2;
934 ss2 << " Expression: " << result.mExpression;
935 mDiagnostics.push_back(ss2.str());
936
937 if (!result.mExpanded.empty()) {
938 fl::sstream ss3;
939 ss3 << " Expanded: " << result.mExpanded;
940 mDiagnostics.push_back(ss3.str());
941 }
942 }
943}
944
945} // namespace test
946} // namespace fl
const char * c_str() const FL_NOEXCEPT
string str() const FL_NOEXCEPT
Definition strstream.h:43
void subcaseStart(const char *name) FL_NOEXCEPT override
void testCaseEnd(bool passed, fl::u32 durationMs=0) FL_NOEXCEPT override
Called when a test case ends.
void testRunEnd(const TestStats &stats) FL_NOEXCEPT override
void testRunStart() FL_NOEXCEPT override
void subcaseEnd() FL_NOEXCEPT override
void assertResult(const AssertResult &result) FL_NOEXCEPT override
void testCaseStart(const char *name) FL_NOEXCEPT override
fl::vector< fl::string > mCurrentTestFailures
Definition fltest.h:620
void testCaseEnd(bool passed, fl::u32 durationMs=0) FL_NOEXCEPT override
Called when a test case ends.
void subcaseEnd() FL_NOEXCEPT override
static fl::string escapeJson(const char *text) FL_NOEXCEPT
void testRunStart() FL_NOEXCEPT override
void subcaseStart(const char *name) FL_NOEXCEPT override
fl::string mCurrentTestName
Definition fltest.h:619
fl::vector< fl::string > mTestResults
Definition fltest.h:624
void testRunEnd(const TestStats &stats) FL_NOEXCEPT override
void testCaseStart(const char *name) FL_NOEXCEPT override
void assertResult(const AssertResult &result) FL_NOEXCEPT override
fl::string * mOutput
Definition fltest.h:616
void subcaseEnd() FL_NOEXCEPT override
void assertResult(const AssertResult &result) FL_NOEXCEPT override
void testCaseStart(const char *name) FL_NOEXCEPT override
void testRunStart() FL_NOEXCEPT override
SerialPrintFunc mPrintFunc
Definition fltest.h:537
void testRunEnd(const TestStats &stats) FL_NOEXCEPT override
void testCaseEnd(bool passed, fl::u32 durationMs=0) FL_NOEXCEPT override
Called when a test case ends.
void print(const char *msg) FL_NOEXCEPT
void subcaseStart(const char *name) FL_NOEXCEPT override
~Subcase() FL_NOEXCEPT
Subcase(const char *name, const char *file, int line) FL_NOEXCEPT
SubcaseSignature mSignature
Definition fltest.h:296
void output(const char *line) FL_NOEXCEPT
fl::string mCurrentTestName
Definition fltest.h:676
void subcaseEnd() FL_NOEXCEPT override
fl::string * mOutput
Definition fltest.h:670
void testRunEnd(const TestStats &stats) FL_NOEXCEPT override
SerialPrintFunc mPrintFunc
Definition fltest.h:671
void testCaseEnd(bool passed, fl::u32 durationMs=0) FL_NOEXCEPT override
Called when a test case ends.
void subcaseStart(const char *name) FL_NOEXCEPT override
fl::string mStreamingOutput
Definition fltest.h:679
fl::vector< fl::string > mDiagnostics
Definition fltest.h:678
void testRunStart() FL_NOEXCEPT override
void testCaseStart(const char *name) FL_NOEXCEPT override
void assertResult(const AssertResult &result) FL_NOEXCEPT override
fl::u32 mDefaultTimeoutMs
Definition fltest.h:268
fl::vector< SubcaseSignature > mNextSubcaseStack
Definition fltest.h:253
bool matchesFilter(const char *name, const char *filter) const FL_NOEXCEPT
fl::size mSubcaseDiscoveryDepth
Definition fltest.h:263
TestContext() FL_NOEXCEPT
DefaultReporter mDefaultReporter
Definition fltest.h:257
fl::u32 hashCurrentPath(const SubcaseSignature &sig) const FL_NOEXCEPT
TimeoutHandlerFunc mTimeoutHandler
Definition fltest.h:267
void runTestCase(const TestCaseInfo &info) FL_NOEXCEPT
fl::size mCurrentSubcaseDepth
Definition fltest.h:262
fl::vector< TestCaseInfo > mTestCases
Definition fltest.h:251
GetMillisFunc mGetMillis
Definition fltest.h:266
bool enterSubcase(const SubcaseSignature &sig) FL_NOEXCEPT
fl::vector< fl::u32 > mFullyTraversedHashes
Definition fltest.h:254
fl::vector< SubcaseSignature > mSubcaseStack
Definition fltest.h:252
void(* TestFunc)()
Definition fltest.h:183
static TestContext & instance() FL_NOEXCEPT
void markFullyTraversed(fl::u32 hash) FL_NOEXCEPT
int registerTest(TestFunc func, const char *name, const char *file, int line) FL_NOEXCEPT
void requireFailed(const char *expr, const char *file, int line) FL_NOEXCEPT
bool isFullyTraversed(fl::u32 hash) const FL_NOEXCEPT
fl::u32 mCurrentTestStartMs
Definition fltest.h:269
void reportAssert(const AssertResult &result) FL_NOEXCEPT
fl::size listTests(const char *filter=nullptr) const FL_NOEXCEPT
bool checkTimeout() FL_NOEXCEPT
Check if current test has timed out (call periodically in long tests) Returns true if timed out.
int run(int argc=0, const char *const *argv=nullptr) FL_NOEXCEPT
void exitSubcase(const SubcaseSignature &sig) FL_NOEXCEPT
void checkFailed(const char *expr, const char *file, int line) FL_NOEXCEPT
const char * mCurrentTestName
Definition fltest.h:270
fl::u32 getElapsedMs() const FL_NOEXCEPT
Get elapsed time for current test (in ms)
IReporter * mReporter
Definition fltest.h:256
void assertResult(const AssertResult &result) FL_NOEXCEPT override
void testRunStart() FL_NOEXCEPT override
void testCaseEnd(bool passed, fl::u32 durationMs=0) FL_NOEXCEPT override
Called when a test case ends.
fl::string * mOutput
Definition fltest.h:574
void testCaseStart(const char *name) FL_NOEXCEPT override
void testRunEnd(const TestStats &stats) FL_NOEXCEPT override
void subcaseStart(const char *name) FL_NOEXCEPT override
static fl::string escapeXml(const char *text) FL_NOEXCEPT
const char * mSuiteName
Definition fltest.h:575
fl::vector< fl::string > mTestCaseResults
Definition fltest.h:581
void subcaseEnd() FL_NOEXCEPT override
fl::string mCurrentTestName
Definition fltest.h:578
fl::string mCurrentTestFailures
Definition fltest.h:579
Portable test framework for FastLED.
Centralized logging categories for FastLED hardware interfaces and subsystems.
static bool sCurrentTestSkipped
void skipTest(const char *reason, const char *file, int line) FL_NOEXCEPT
Record that the current test should be skipped.
void fail(const char *msg, const char *file, int line, bool isFatal) FL_NOEXCEPT
Helper for FAIL macros.
fl::u32 hashSubcaseSignature(const SubcaseSignature &sig) FL_NOEXCEPT
Definition fltest.h:150
bool isTestSkipped() FL_NOEXCEPT
Check if current test has been marked as skipped.
void outputCapture(const char *name, const char *value, const char *file, int line) FL_NOEXCEPT
Helper to output CAPTURE variable.
void outputMessage(const char *msg, const char *file, int line) FL_NOEXCEPT
Helper to output INFO/MESSAGE during test execution.
static const char * sSkipReason
constexpr int type_rank< T >::value
void print(const char *str)
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
void printf(const char *format, const Args &... args) FL_NOEXCEPT
Printf-like formatting function that prints directly to the platform output.
Definition stdio.h:635
const hex_t hex
Definition ios.cpp.hpp:6
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT