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