FastLED 3.9.15
Loading...
Searching...
No Matches
fltest.h
Go to the documentation of this file.
1#pragma once
2
24
25#include "fl/stl/compiler_control.h" // FL_PRETTY_FUNCTION
26#include "fl/stl/stdint.h"
27#include "fl/stl/vector.h"
28#include "fl/stl/strstream.h"
29#include "fl/stl/cstring.h"
31
32// Conditionally include <exception> for platforms with exception support
33// This must be at the top of the file (before any namespace declarations)
34#if defined(__cpp_exceptions) || defined(__EXCEPTIONS)
35// IWYU pragma: begin_keep
36#include <exception>
37#include "fl/stl/noexcept.h"
38// IWYU pragma: end_keep // For std::exception in CHECK_THROWS_WITH
39#endif
40
41// Configure maximum test depth and counts
42#ifndef FLTEST_MAX_SUBCASE_DEPTH
43#define FLTEST_MAX_SUBCASE_DEPTH 8
44#endif
45
46#ifndef FLTEST_MAX_TEST_CASES
47#define FLTEST_MAX_TEST_CASES 512
48#endif
49
50namespace fl {
51namespace test {
52
53// Forward declarations
54class TestCase; // IWYU pragma: keep
55class Subcase; // IWYU pragma: keep
56class TestContext; // IWYU pragma: keep
57
58// Source location info
60 const char* mFile;
61 int mLine;
62
63 SourceLocation(const char* file = "", int line = 0) FL_NOEXCEPT
64 : mFile(file), mLine(line) {}
65};
66
67// Result of an assertion
69 bool mPassed;
71 fl::string mExpanded; // Expanded values
73
74 AssertResult(bool passed = true) FL_NOEXCEPT
75 : mPassed(passed) {}
76};
77
78// Test statistics
79struct TestStats {
80 fl::u32 mTestCasesRun = 0;
81 fl::u32 mTestCasesPassed = 0;
82 fl::u32 mTestCasesFailed = 0;
83 fl::u32 mTestCasesSkipped = 0; // Tracks tests skipped via FL_SKIP
84 fl::u32 mAssertsPassed = 0;
85 fl::u32 mAssertsFailed = 0;
86 fl::u32 mTotalDurationMs = 0; // Total duration of all tests in milliseconds
87
97
98 bool allPassed() const FL_NOEXCEPT {
99 return mAssertsFailed == 0 && mTestCasesFailed == 0;
100 }
101};
102
103// Reporter interface for outputting results
105public:
106 virtual ~IReporter() FL_NOEXCEPT = default;
107
108 virtual void testRunStart() FL_NOEXCEPT = 0;
109 virtual void testRunEnd(const TestStats& stats) FL_NOEXCEPT = 0;
110 virtual void testCaseStart(const char* name) FL_NOEXCEPT = 0;
114 virtual void testCaseEnd(bool passed, fl::u32 durationMs = 0) FL_NOEXCEPT = 0;
115 virtual void subcaseStart(const char* name) FL_NOEXCEPT = 0;
116 virtual void subcaseEnd() FL_NOEXCEPT = 0;
117 virtual void assertResult(const AssertResult& result) FL_NOEXCEPT = 0;
118};
119
120// Default reporter that uses FL_DBG/printf
122public:
123 void testRunStart() FL_NOEXCEPT override;
124 void testRunEnd(const TestStats& stats) FL_NOEXCEPT override;
125 void testCaseStart(const char* name) FL_NOEXCEPT override;
126 void testCaseEnd(bool passed, fl::u32 durationMs = 0) FL_NOEXCEPT override;
127 void subcaseStart(const char* name) FL_NOEXCEPT override;
128 void subcaseEnd() FL_NOEXCEPT override;
129 void assertResult(const AssertResult& result) FL_NOEXCEPT override;
130};
131
132// Subcase signature for tracking which subcases have been run
134 const char* mName;
135 const char* mFile;
136 int mLine;
137
138 bool operator==(const SubcaseSignature& other) const FL_NOEXCEPT {
139 return mLine == other.mLine &&
140 fl::strcmp(mFile, other.mFile) == 0 &&
141 fl::strcmp(mName, other.mName) == 0;
142 }
143
144 bool operator!=(const SubcaseSignature& other) const FL_NOEXCEPT {
145 return !(*this == other);
146 }
147};
148
149// Hash function for SubcaseSignature
151 fl::u32 hash = 0;
152 for (const char* p = sig.mName; *p; ++p) {
153 hash = hash * 31 + static_cast<fl::u32>(*p);
154 }
155 for (const char* p = sig.mFile; *p; ++p) {
156 hash = hash * 31 + static_cast<fl::u32>(*p);
157 }
158 hash = hash * 31 + static_cast<fl::u32>(sig.mLine);
159 return hash;
160}
161
162// =============================================================================
163// Timeout support types
164// =============================================================================
165// For embedded devices, we use a callback-based timeout mechanism.
166// The user provides a function to get current time (in milliseconds)
167// and optionally a timeout duration per test.
168
171typedef fl::u32 (*GetMillisFunc)();
172
175typedef bool (*TimeoutHandlerFunc)(const char* testName, fl::u32 elapsedMs);
176
177// Global test context - manages test execution state
179public:
181
182 // Test registration
183 typedef void (*TestFunc)();
186 fl::string mName; // Owns the string to handle dynamically generated names (e.g., template tests)
187 const char* mFile;
188 int mLine;
189 };
190
191 int registerTest(TestFunc func, const char* name, const char* file, int line) FL_NOEXCEPT;
192
193 // Run all tests
194 // argc/argv: command line args (filter pattern from first arg)
195 // filter: explicit filter pattern (if provided, overrides argv)
196 int run(int argc = 0, const char* const* argv = nullptr) FL_NOEXCEPT;
197
198 // Run tests matching filter pattern
199 // Filter supports:
200 // - "*" matches any sequence of characters
201 // - "?" matches any single character
202 // - Exact substring match if no wildcards
203 int run(const char* filter) FL_NOEXCEPT;
204
205 // List all registered test names without running them
206 // Returns the number of tests listed
207 fl::size listTests(const char* filter = nullptr) const FL_NOEXCEPT;
208
209 // Subcase management
212 bool needsReentry() const FL_NOEXCEPT { return !mNextSubcaseStack.empty(); }
213
214 // Assertion handling
215 void reportAssert(const AssertResult& result) FL_NOEXCEPT;
216 void checkFailed(const char* expr, const char* file, int line) FL_NOEXCEPT;
217 void requireFailed(const char* expr, const char* file, int line) FL_NOEXCEPT;
218
219 // Reporter
222
223 // Stats
225 const TestStats& stats() const FL_NOEXCEPT { return mStats; }
226
227 // Current test state
230
231 // Timeout support
234
237
239 void setDefaultTimeoutMs(fl::u32 timeoutMs) FL_NOEXCEPT { mDefaultTimeoutMs = timeoutMs; }
240
243 bool checkTimeout() FL_NOEXCEPT;
244
246 fl::u32 getElapsedMs() const FL_NOEXCEPT;
247
248private:
250
252 fl::vector<SubcaseSignature> mSubcaseStack; // Current path being followed
253 fl::vector<SubcaseSignature> mNextSubcaseStack; // Path for next iteration
254 fl::vector<fl::u32> mFullyTraversedHashes; // Fully explored paths
255
259
260 bool mCurrentTestFailed = false;
261 bool mShouldReenter = false; // Need another iteration?
262 fl::size mCurrentSubcaseDepth = 0; // How deep in subcases
263 fl::size mSubcaseDiscoveryDepth = 0; // Position in discovery
264
265 // Timeout support
268 fl::u32 mDefaultTimeoutMs = 0; // 0 = no timeout
270 const char* mCurrentTestName = nullptr;
272
273 // Run a single test case
274 void runTestCase(const TestCaseInfo& info) FL_NOEXCEPT;
275
276 // Hash the path to a subcase
277 fl::u32 hashCurrentPath(const SubcaseSignature& sig) const FL_NOEXCEPT;
278
279 // Check if a path has been fully traversed
280 bool isFullyTraversed(fl::u32 hash) const FL_NOEXCEPT;
281 void markFullyTraversed(fl::u32 hash) FL_NOEXCEPT;
282
283 // Pattern matching helper for test filtering
284 bool matchesFilter(const char* name, const char* filter) const FL_NOEXCEPT;
285};
286
287// RAII subcase class
288class Subcase {
289public:
290 Subcase(const char* name, const char* file, int line) FL_NOEXCEPT;
292
293 explicit operator bool() const FL_NOEXCEPT { return mEntered; }
294
295private:
297 bool mEntered = false;
298};
299
300// Test registration helper
302 TestRegistrar(TestContext::TestFunc func, const char* name, const char* file, int line) FL_NOEXCEPT {
303 TestContext::instance().registerTest(func, name, file, line);
304 }
305};
306
307// Expression decomposition for better error messages
308template<typename T>
312
314 fl::sstream ss;
315 ss << value;
316 mStringified = ss.str();
317 }
318
319 operator T() const FL_NOEXCEPT { return mValue; }
320 const char* str() const FL_NOEXCEPT { return mStringified.c_str(); }
321};
322
323// Comparison helpers
324// Use decay to remove references and cv qualifiers for comparison function types
325template<typename L, typename R>
326struct CompareEq {
327 bool operator()(const L& lhs, const R& rhs) const FL_NOEXCEPT { return lhs == rhs; }
328};
329
330template<typename L, typename R>
331struct CompareNe {
332 bool operator()(const L& lhs, const R& rhs) const FL_NOEXCEPT { return lhs != rhs; }
333};
334
335template<typename L, typename R>
336struct CompareLt {
337 bool operator()(const L& lhs, const R& rhs) const FL_NOEXCEPT { return lhs < rhs; }
338};
339
340template<typename L, typename R>
341struct CompareGt {
342 bool operator()(const L& lhs, const R& rhs) const FL_NOEXCEPT { return lhs > rhs; }
343};
344
345template<typename L, typename R>
346struct CompareLe {
347 bool operator()(const L& lhs, const R& rhs) const FL_NOEXCEPT { return lhs <= rhs; }
348};
349
350template<typename L, typename R>
351struct CompareGe {
352 bool operator()(const L& lhs, const R& rhs) const FL_NOEXCEPT { return lhs >= rhs; }
353};
354
355// Binary assertion helper
356template<typename L, typename R, typename Cmp>
357bool binaryAssert(const L& lhs, const R& rhs, Cmp cmp,
358 const char* lhsExpr, const char* op, const char* rhsExpr,
359 const char* file, int line) FL_NOEXCEPT {
360 bool result = cmp(lhs, rhs);
361
363 ar.mLocation = SourceLocation(file, line);
364
365 fl::sstream exprSS;
366 exprSS << lhsExpr << " " << op << " " << rhsExpr;
367 ar.mExpression = exprSS.str();
368
369 if (!result) {
370 fl::sstream expandedSS;
371 expandedSS << lhs << " " << op << " " << rhs;
372 ar.mExpanded = expandedSS.str();
373 }
374
375 TestContext::instance().reportAssert(ar);
376 return result;
377}
378
379// =============================================================================
380// Approx class for floating point comparison
381// =============================================================================
382
396class Approx {
397public:
398 explicit Approx(double value) FL_NOEXCEPT
399 : mValue(value)
400 , mEpsilon(1e-5) // Default relative epsilon
401 , mMargin(0.0) // Default no absolute margin
402 , mScale(1.0)
403 {}
404
407 Approx& epsilon(double newEpsilon) FL_NOEXCEPT {
408 mEpsilon = newEpsilon;
409 return *this;
410 }
411
414 Approx& margin(double newMargin) FL_NOEXCEPT {
415 mMargin = newMargin < 0 ? 0 : newMargin;
416 return *this;
417 }
418
420 Approx& scale(double newScale) FL_NOEXCEPT {
421 mScale = newScale;
422 return *this;
423 }
424
425 double value() const FL_NOEXCEPT { return mValue; }
426 double getEpsilon() const FL_NOEXCEPT { return mEpsilon; }
427 double getMargin() const FL_NOEXCEPT { return mMargin; }
428 double getScale() const FL_NOEXCEPT { return mScale; }
429
430 // Comparison operators
431 friend bool operator==(double lhs, const Approx& rhs) FL_NOEXCEPT {
432 double diff = lhs - rhs.mValue;
433 if (diff < 0) diff = -diff;
434
435 // Check absolute margin first (if set)
436 if (rhs.mMargin > 0.0 && diff <= rhs.mMargin) {
437 return true;
438 }
439
440 // Check relative epsilon
441 double lhsAbs = lhs < 0 ? -lhs : lhs;
442 double rhsAbs = rhs.mValue < 0 ? -rhs.mValue : rhs.mValue;
443 double maxAbs = lhsAbs > rhsAbs ? lhsAbs : rhsAbs;
444 double relativeMargin = rhs.mEpsilon * (rhs.mScale + maxAbs);
445 return diff <= relativeMargin;
446 }
447
448 friend bool operator==(const Approx& lhs, double rhs) FL_NOEXCEPT {
449 return rhs == lhs;
450 }
451
452 friend bool operator!=(double lhs, const Approx& rhs) FL_NOEXCEPT {
453 return !(lhs == rhs);
454 }
455
456 friend bool operator!=(const Approx& lhs, double rhs) FL_NOEXCEPT {
457 return !(lhs == rhs);
458 }
459
460 friend bool operator<=(double lhs, const Approx& rhs) FL_NOEXCEPT {
461 return lhs < rhs.mValue || lhs == rhs;
462 }
463
464 friend bool operator>=(double lhs, const Approx& rhs) FL_NOEXCEPT {
465 return lhs > rhs.mValue || lhs == rhs;
466 }
467
468 friend bool operator<(double lhs, const Approx& rhs) FL_NOEXCEPT {
469 return lhs < rhs.mValue && !(lhs == rhs);
470 }
471
472 friend bool operator>(double lhs, const Approx& rhs) FL_NOEXCEPT {
473 return lhs > rhs.mValue && !(lhs == rhs);
474 }
475
476private:
477 double mValue;
478 double mEpsilon;
479 double mMargin;
480 double mScale;
481};
482
483// Stringify Approx for output
485 os << "Approx(" << approx.value() << ")";
486 return os;
487}
488
489// =============================================================================
490// Message output helper
491// =============================================================================
492
494void outputMessage(const char* msg, const char* file, int line) FL_NOEXCEPT;
495
497void outputCapture(const char* name, const char* value, const char* file, int line) FL_NOEXCEPT;
498
500void fail(const char* msg, const char* file, int line, bool isFatal) FL_NOEXCEPT;
501
502// =============================================================================
503// SerialReporter for embedded device output
504// =============================================================================
505// This reporter outputs to a serial port interface. On embedded devices like
506// ESP32 or Teensy, include this header and set:
507// fl::test::TestContext::instance().setReporter(&mySerialReporter);
508//
509// The reporter uses a configurable print function (default: fl::printf)
510// which can be overridden to use Serial.print, etc.
511
513typedef void (*SerialPrintFunc)(const char* msg);
514
519class SerialReporter : public IReporter {
520public:
522 explicit SerialReporter(SerialPrintFunc printFunc = nullptr) FL_NOEXCEPT
523 : mPrintFunc(printFunc) {}
524
525 void testRunStart() FL_NOEXCEPT override;
526 void testRunEnd(const TestStats& stats) FL_NOEXCEPT override;
527 void testCaseStart(const char* name) FL_NOEXCEPT override;
528 void testCaseEnd(bool passed, fl::u32 durationMs = 0) FL_NOEXCEPT override;
529 void subcaseStart(const char* name) FL_NOEXCEPT override;
530 void subcaseEnd() FL_NOEXCEPT override;
531 void assertResult(const AssertResult& result) FL_NOEXCEPT override;
532
535
536private:
538 void print(const char* msg) FL_NOEXCEPT;
539};
540
541// =============================================================================
542// XMLReporter for CI/CD integration (JUnit format)
543// =============================================================================
544// This reporter outputs test results in JUnit XML format, which is widely
545// supported by CI systems like Jenkins, GitHub Actions, GitLab CI, etc.
546//
547// Usage:
548// fl::string xmlOutput;
549// fl::test::XMLReporter reporter(&xmlOutput);
550// fl::test::TestContext::instance().setReporter(&reporter);
551// fl::test::TestContext::instance().run();
552// // xmlOutput now contains the JUnit XML
553
556class XMLReporter : public IReporter {
557public:
559 explicit XMLReporter(fl::string* outputBuffer, const char* suiteName = "fltest") FL_NOEXCEPT
560 : mOutput(outputBuffer), mSuiteName(suiteName) {}
561
562 void testRunStart() FL_NOEXCEPT override;
563 void testRunEnd(const TestStats& stats) FL_NOEXCEPT override;
564 void testCaseStart(const char* name) FL_NOEXCEPT override;
565 void testCaseEnd(bool passed, fl::u32 durationMs = 0) FL_NOEXCEPT override;
566 void subcaseStart(const char* name) FL_NOEXCEPT override;
567 void subcaseEnd() FL_NOEXCEPT override;
568 void assertResult(const AssertResult& result) FL_NOEXCEPT override;
569
571 void setSuiteName(const char* name) FL_NOEXCEPT { mSuiteName = name; }
572
573private:
575 const char* mSuiteName;
576
577 // Current test case state
582
583 // Helper to escape XML special characters
584 static fl::string escapeXml(const char* text) FL_NOEXCEPT;
585};
586
587// =============================================================================
588// JSONReporter for modern CI/CD integration
589// =============================================================================
590// This reporter outputs test results in JSON format, useful for custom
591// dashboards, APIs, and modern CI systems.
592//
593// Usage:
594// fl::string jsonOutput;
595// fl::test::JSONReporter reporter(&jsonOutput);
596// fl::test::TestContext::instance().setReporter(&reporter);
597// fl::test::TestContext::instance().run();
598// // jsonOutput now contains the JSON
599
601class JSONReporter : public IReporter {
602public:
604 explicit JSONReporter(fl::string* outputBuffer) FL_NOEXCEPT
605 : mOutput(outputBuffer) {}
606
607 void testRunStart() FL_NOEXCEPT override;
608 void testRunEnd(const TestStats& stats) FL_NOEXCEPT override;
609 void testCaseStart(const char* name) FL_NOEXCEPT override;
610 void testCaseEnd(bool passed, fl::u32 durationMs = 0) FL_NOEXCEPT override;
611 void subcaseStart(const char* name) FL_NOEXCEPT override;
612 void subcaseEnd() FL_NOEXCEPT override;
613 void assertResult(const AssertResult& result) FL_NOEXCEPT override;
614
615private:
616 fl::string* mOutput;
617
618 // Current test case state
622
623 // All test results as JSON objects
625
626 // Helper to escape JSON special characters
627 static fl::string escapeJson(const char* text) FL_NOEXCEPT;
628};
629
630// =============================================================================
631// TAPReporter for TAP (Test Anything Protocol) output
632// =============================================================================
633// TAP is a simple text-based protocol for test results, widely supported by
634// CI systems and test harnesses. See https://testanything.org/
635//
636// TAP output looks like:
637// TAP version 13
638// 1..5
639// ok 1 - test name
640// not ok 2 - failing test
641// # diagnostic message
642// ok 3 - another test
643// ...
644
647class TAPReporter : public IReporter {
648public:
650 explicit TAPReporter(fl::string* outputBuffer) FL_NOEXCEPT
651 : mOutput(outputBuffer), mPrintFunc(nullptr), mTestNumber(0), mTotalTests(0) {}
652
655 : mOutput(nullptr), mPrintFunc(printFunc), mTestNumber(0), mTotalTests(0) {}
656
657 void testRunStart() FL_NOEXCEPT override;
658 void testRunEnd(const TestStats& stats) FL_NOEXCEPT override;
659 void testCaseStart(const char* name) FL_NOEXCEPT override;
660 void testCaseEnd(bool passed, fl::u32 durationMs = 0) FL_NOEXCEPT override;
661 void subcaseStart(const char* name) FL_NOEXCEPT override;
662 void subcaseEnd() FL_NOEXCEPT override;
663 void assertResult(const AssertResult& result) FL_NOEXCEPT override;
664
667 void setTotalTests(fl::u32 total) FL_NOEXCEPT { mTotalTests = total; }
668
669private:
672 fl::u32 mTestNumber;
673 fl::u32 mTotalTests;
674
675 // Current test state
679 fl::string mStreamingOutput; // Used for streaming mode
680
681 // Helper to output a line
682 void output(const char* line) FL_NOEXCEPT;
683};
684
685// =============================================================================
686// Test skip support
687// =============================================================================
688// Allows tests to be skipped with a message
689
691void skipTest(const char* reason, const char* file, int line) FL_NOEXCEPT;
692
695
696} // namespace test
697} // namespace fl
698
699// =============================================================================
700// Macros
701// =============================================================================
702
703// Unique identifier generation
704#define FLTEST_CAT_IMPL(a, b) a##b
705#define FLTEST_CAT(a, b) FLTEST_CAT_IMPL(a, b)
706
707// Use __LINE__ for unique identifiers - more reliable than __COUNTER__
708// which increments on each expansion
709#define FLTEST_UNIQUE(x) FLTEST_CAT(x, __LINE__)
710
711// Test case registration
712// Uses __LINE__ to ensure all three usages get the same unique suffix
713#define FL_TEST_CASE(name) \
714 static void FLTEST_UNIQUE(FLTEST_FUNC_)(); \
715 static fl::test::TestRegistrar FLTEST_UNIQUE(FLTEST_REG_)( \
716 FLTEST_UNIQUE(FLTEST_FUNC_), name, __FILE__, __LINE__); \
717 static void FLTEST_UNIQUE(FLTEST_FUNC_)()
718
719// Subcase
720#define FL_SUBCASE(name) \
721 if (const fl::test::Subcase& FLTEST_UNIQUE(FLTEST_SUB_) = \
722 fl::test::Subcase(name, __FILE__, __LINE__))
723
724// Basic assertions
725#define FL_CHECK(expr) \
726 do { \
727 if (!(expr)) { \
728 fl::test::TestContext::instance().checkFailed(#expr, __FILE__, __LINE__); \
729 } else { \
730 fl::test::AssertResult ar(true); \
731 ar.mExpression = #expr; \
732 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
733 fl::test::TestContext::instance().reportAssert(ar); \
734 } \
735 } while (0)
736
737#define FL_CHECK_FALSE(expr) \
738 do { \
739 if (expr) { \
740 fl::test::TestContext::instance().checkFailed("!(" #expr ")", __FILE__, __LINE__); \
741 } else { \
742 fl::test::AssertResult ar(true); \
743 ar.mExpression = "!(" #expr ")"; \
744 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
745 fl::test::TestContext::instance().reportAssert(ar); \
746 } \
747 } while (0)
748
749#define FL_REQUIRE(expr) \
750 do { \
751 if (!(expr)) { \
752 fl::test::TestContext::instance().requireFailed(#expr, __FILE__, __LINE__); \
753 return; \
754 } else { \
755 fl::test::AssertResult ar(true); \
756 ar.mExpression = #expr; \
757 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
758 fl::test::TestContext::instance().reportAssert(ar); \
759 } \
760 } while (0)
761
762#define FL_REQUIRE_FALSE(expr) \
763 do { \
764 if (expr) { \
765 fl::test::TestContext::instance().requireFailed("!(" #expr ")", __FILE__, __LINE__); \
766 return; \
767 } else { \
768 fl::test::AssertResult ar(true); \
769 ar.mExpression = "!(" #expr ")"; \
770 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
771 fl::test::TestContext::instance().reportAssert(ar); \
772 } \
773 } while (0)
774
775// Binary comparison assertions - use decay_t to strip cv-qualifiers and references
776#define FL_CHECK_EQ(lhs, rhs) \
777 fl::test::binaryAssert(lhs, rhs, fl::test::CompareEq<fl::decay_t<decltype(lhs)>, fl::decay_t<decltype(rhs)>>{}, \
778 #lhs, "==", #rhs, __FILE__, __LINE__)
779
780#define FL_CHECK_NE(lhs, rhs) \
781 fl::test::binaryAssert(lhs, rhs, fl::test::CompareNe<fl::decay_t<decltype(lhs)>, fl::decay_t<decltype(rhs)>>{}, \
782 #lhs, "!=", #rhs, __FILE__, __LINE__)
783
784#define FL_CHECK_LT(lhs, rhs) \
785 fl::test::binaryAssert(lhs, rhs, fl::test::CompareLt<fl::decay_t<decltype(lhs)>, fl::decay_t<decltype(rhs)>>{}, \
786 #lhs, "<", #rhs, __FILE__, __LINE__)
787
788#define FL_CHECK_GT(lhs, rhs) \
789 fl::test::binaryAssert(lhs, rhs, fl::test::CompareGt<fl::decay_t<decltype(lhs)>, fl::decay_t<decltype(rhs)>>{}, \
790 #lhs, ">", #rhs, __FILE__, __LINE__)
791
792#define FL_CHECK_LE(lhs, rhs) \
793 fl::test::binaryAssert(lhs, rhs, fl::test::CompareLe<fl::decay_t<decltype(lhs)>, fl::decay_t<decltype(rhs)>>{}, \
794 #lhs, "<=", #rhs, __FILE__, __LINE__)
795
796#define FL_CHECK_GE(lhs, rhs) \
797 fl::test::binaryAssert(lhs, rhs, fl::test::CompareGe<fl::decay_t<decltype(lhs)>, fl::decay_t<decltype(rhs)>>{}, \
798 #lhs, ">=", #rhs, __FILE__, __LINE__)
799
800#define FL_REQUIRE_EQ(lhs, rhs) \
801 do { \
802 if (!FL_CHECK_EQ(lhs, rhs)) return; \
803 } while (0)
804
805#define FL_REQUIRE_NE(lhs, rhs) \
806 do { \
807 if (!FL_CHECK_NE(lhs, rhs)) return; \
808 } while (0)
809
810#define FL_REQUIRE_LT(lhs, rhs) \
811 do { \
812 if (!FL_CHECK_LT(lhs, rhs)) return; \
813 } while (0)
814
815#define FL_REQUIRE_GT(lhs, rhs) \
816 do { \
817 if (!FL_CHECK_GT(lhs, rhs)) return; \
818 } while (0)
819
820#define FL_REQUIRE_LE(lhs, rhs) \
821 do { \
822 if (!FL_CHECK_LE(lhs, rhs)) return; \
823 } while (0)
824
825#define FL_REQUIRE_GE(lhs, rhs) \
826 do { \
827 if (!FL_CHECK_GE(lhs, rhs)) return; \
828 } while (0)
829
830// =============================================================================
831// Message and Capture macros
832// =============================================================================
833
834// FL_MESSAGE - output a message during test execution (non-failing)
835#define FL_MESSAGE(msg) \
836 do { \
837 fl::sstream FLTEST_UNIQUE(ss_); \
838 FLTEST_UNIQUE(ss_) << msg; \
839 fl::test::outputMessage(FLTEST_UNIQUE(ss_).str().c_str(), __FILE__, __LINE__); \
840 } while (0)
841
842// FL_INFO - alias for FL_MESSAGE (doctest compatibility)
843// Only define if not already defined (e.g., by fl/log/log.h)
844#ifndef FL_INFO
845#define FL_INFO(msg) FL_MESSAGE(msg)
846#endif
847
848// FL_CAPTURE - capture and print a variable's value
849#define FL_CAPTURE(x) \
850 do { \
851 fl::sstream FLTEST_UNIQUE(ss_); \
852 FLTEST_UNIQUE(ss_) << (x); \
853 fl::test::outputCapture(#x, FLTEST_UNIQUE(ss_).str().c_str(), __FILE__, __LINE__); \
854 } while (0)
855
856// FL_FAIL - explicit failure (fatal, stops test)
857#define FL_FAIL(msg) \
858 do { \
859 fl::sstream FLTEST_UNIQUE(ss_); \
860 FLTEST_UNIQUE(ss_) << msg; \
861 fl::test::fail(FLTEST_UNIQUE(ss_).str().c_str(), __FILE__, __LINE__, true); \
862 return; \
863 } while (0)
864
865// FL_FAIL_CHECK - explicit failure (non-fatal, test continues)
866#define FL_FAIL_CHECK(msg) \
867 do { \
868 fl::sstream FLTEST_UNIQUE(ss_); \
869 FLTEST_UNIQUE(ss_) << msg; \
870 fl::test::fail(FLTEST_UNIQUE(ss_).str().c_str(), __FILE__, __LINE__, false); \
871 } while (0)
872
873// FL_WARN - warning assertion (logs but doesn't affect pass/fail)
874// Note: Only define if not already defined by fl/log/log.h (which provides streaming support)
875#ifndef FL_WARN
876#define FL_WARN(expr) \
877 do { \
878 if (!(expr)) { \
879 fl::test::outputMessage("Warning: " #expr " is false", __FILE__, __LINE__); \
880 } \
881 } while (0)
882#endif
883
884// FL_WARN_FALSE - warning assertion for false conditions
885#define FL_WARN_FALSE(expr) \
886 do { \
887 if (expr) { \
888 fl::test::outputMessage("Warning: !(" #expr ") is false", __FILE__, __LINE__); \
889 } \
890 } while (0)
891
892// =============================================================================
893// WARN comparison macros - like CHECK variants but don't affect pass/fail
894// =============================================================================
895// These macros log warnings when conditions aren't met, but don't fail the test
896
897#define FL_WARN_EQ(lhs, rhs) \
898 do { \
899 if (!((lhs) == (rhs))) { \
900 fl::sstream _fl_warn_ss; \
901 _fl_warn_ss << "Warning: " << #lhs << " == " << #rhs \
902 << " failed: " << (lhs) << " != " << (rhs); \
903 fl::test::outputMessage(_fl_warn_ss.str().c_str(), __FILE__, __LINE__); \
904 } \
905 } while (0)
906
907#define FL_WARN_NE(lhs, rhs) \
908 do { \
909 if (!((lhs) != (rhs))) { \
910 fl::sstream _fl_warn_ss; \
911 _fl_warn_ss << "Warning: " << #lhs << " != " << #rhs \
912 << " failed: both equal " << (lhs); \
913 fl::test::outputMessage(_fl_warn_ss.str().c_str(), __FILE__, __LINE__); \
914 } \
915 } while (0)
916
917#define FL_WARN_LT(lhs, rhs) \
918 do { \
919 if (!((lhs) < (rhs))) { \
920 fl::sstream _fl_warn_ss; \
921 _fl_warn_ss << "Warning: " << #lhs << " < " << #rhs \
922 << " failed: " << (lhs) << " >= " << (rhs); \
923 fl::test::outputMessage(_fl_warn_ss.str().c_str(), __FILE__, __LINE__); \
924 } \
925 } while (0)
926
927#define FL_WARN_GT(lhs, rhs) \
928 do { \
929 if (!((lhs) > (rhs))) { \
930 fl::sstream _fl_warn_ss; \
931 _fl_warn_ss << "Warning: " << #lhs << " > " << #rhs \
932 << " failed: " << (lhs) << " <= " << (rhs); \
933 fl::test::outputMessage(_fl_warn_ss.str().c_str(), __FILE__, __LINE__); \
934 } \
935 } while (0)
936
937#define FL_WARN_LE(lhs, rhs) \
938 do { \
939 if (!((lhs) <= (rhs))) { \
940 fl::sstream _fl_warn_ss; \
941 _fl_warn_ss << "Warning: " << #lhs << " <= " << #rhs \
942 << " failed: " << (lhs) << " > " << (rhs); \
943 fl::test::outputMessage(_fl_warn_ss.str().c_str(), __FILE__, __LINE__); \
944 } \
945 } while (0)
946
947#define FL_WARN_GE(lhs, rhs) \
948 do { \
949 if (!((lhs) >= (rhs))) { \
950 fl::sstream _fl_warn_ss; \
951 _fl_warn_ss << "Warning: " << #lhs << " >= " << #rhs \
952 << " failed: " << (lhs) << " < " << (rhs); \
953 fl::test::outputMessage(_fl_warn_ss.str().c_str(), __FILE__, __LINE__); \
954 } \
955 } while (0)
956
957// FL_SKIP - skip the current test with a reason
958// Usage: FL_SKIP("Not implemented yet");
959#define FL_SKIP(reason) \
960 do { \
961 fl::test::skipTest(reason, __FILE__, __LINE__); \
962 return; \
963 } while (0)
964
965// =============================================================================
966// CHECK_MESSAGE / REQUIRE_MESSAGE - assertions with custom messages
967// =============================================================================
968// These macros allow adding a custom message when an assertion fails
969// Usage: FL_CHECK_MESSAGE(x > 0, "x should be positive, got: " << x);
970
971#define FL_CHECK_MESSAGE(expr, msg) \
972 do { \
973 if (!(expr)) { \
974 fl::sstream FLTEST_UNIQUE(ss_); \
975 FLTEST_UNIQUE(ss_) << msg; \
976 fl::test::AssertResult ar(false); \
977 ar.mExpression = #expr; \
978 ar.mExpanded = FLTEST_UNIQUE(ss_).str(); \
979 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
980 fl::test::TestContext::instance().reportAssert(ar); \
981 } else { \
982 fl::test::AssertResult ar(true); \
983 ar.mExpression = #expr; \
984 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
985 fl::test::TestContext::instance().reportAssert(ar); \
986 } \
987 } while (0)
988
989#define FL_REQUIRE_MESSAGE(expr, msg) \
990 do { \
991 if (!(expr)) { \
992 fl::sstream FLTEST_UNIQUE(ss_); \
993 FLTEST_UNIQUE(ss_) << msg; \
994 fl::test::AssertResult ar(false); \
995 ar.mExpression = #expr; \
996 ar.mExpanded = FLTEST_UNIQUE(ss_).str(); \
997 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
998 fl::test::TestContext::instance().reportAssert(ar); \
999 return; \
1000 } else { \
1001 fl::test::AssertResult ar(true); \
1002 ar.mExpression = #expr; \
1003 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1004 fl::test::TestContext::instance().reportAssert(ar); \
1005 } \
1006 } while (0)
1007
1008// =============================================================================
1009// TEST_SUITE macro - groups tests under a named suite
1010// =============================================================================
1011// TEST_SUITE works by defining a namespace scope for test registration.
1012// Tests registered inside the suite will have the suite name prepended.
1013
1014// Helper to track current suite name
1015namespace fl {
1016namespace test {
1017namespace detail {
1018 inline const char*& currentSuiteName() FL_NOEXCEPT {
1019 static const char* name = nullptr;
1020 return name;
1021 }
1022
1023 struct SuiteScope {
1024 const char* mPreviousName;
1025 SuiteScope(const char* name) FL_NOEXCEPT {
1027 currentSuiteName() = name;
1028 }
1032 };
1033} // namespace detail
1034} // namespace test
1035} // namespace fl
1036
1037// Note: FL_TEST_SUITE creates a namespace scope. Usage:
1038// FL_TEST_SUITE("MySuite") {
1039// FL_TEST_CASE("test 1") { ... }
1040// FL_TEST_CASE("test 2") { ... }
1041// }
1042// This will name tests as "MySuite/test 1" and "MySuite/test 2"
1043#define FL_TEST_SUITE(name) \
1044 namespace FLTEST_UNIQUE(FLTEST_SUITE_NS_) { \
1045 static fl::test::detail::SuiteScope FLTEST_UNIQUE(FLTEST_SUITE_SCOPE_)(name); \
1046 } \
1047 namespace FLTEST_UNIQUE(FLTEST_SUITE_NS_)
1048
1049// Alternative TEST_SUITE syntax without braces:
1050// FL_TEST_SUITE_BEGIN("MySuite")
1051// FL_TEST_CASE("test 1") { ... }
1052// FL_TEST_CASE("test 2") { ... }
1053// FL_TEST_SUITE_END()
1054//
1055// This is useful for migration from doctest TEST_SUITE_BEGIN/END.
1056// Note: Unlike FL_TEST_SUITE, BEGIN/END creates a file-level scope,
1057// so all tests between BEGIN and END are in the suite.
1058#define FL_TEST_SUITE_BEGIN(name) \
1059 namespace { \
1060 static fl::test::detail::SuiteScope FLTEST_UNIQUE(FLTEST_SUITE_SCOPE_)(name);
1061
1062#define FL_TEST_SUITE_END() \
1063 }
1064
1065// =============================================================================
1066// BDD-style macros (SCENARIO/GIVEN/WHEN/THEN)
1067// =============================================================================
1068// These macros provide BDD (Behavior-Driven Development) syntax sugar.
1069// They're essentially wrappers around TEST_CASE and SUBCASE with prefixes.
1070
1071#define FL_SCENARIO(name) FL_TEST_CASE("Scenario: " name)
1072#define FL_GIVEN(name) FL_SUBCASE("Given: " name)
1073#define FL_WHEN(name) FL_SUBCASE("When: " name)
1074#define FL_AND_WHEN(name) FL_SUBCASE("And when: " name)
1075#define FL_THEN(name) FL_SUBCASE("Then: " name)
1076#define FL_AND_THEN(name) FL_SUBCASE("And: " name)
1077
1078// =============================================================================
1079// CHECK_CLOSE - absolute tolerance floating point comparison
1080// =============================================================================
1081// Unlike Approx which uses relative epsilon, CHECK_CLOSE uses absolute tolerance
1082
1083#define FL_CHECK_CLOSE(a, b, epsilon) \
1084 do { \
1085 auto _fl_a = (a); \
1086 auto _fl_b = (b); \
1087 auto _fl_diff = _fl_a - _fl_b; \
1088 if (_fl_diff < 0) _fl_diff = -_fl_diff; \
1089 bool _fl_result = _fl_diff <= (epsilon); \
1090 fl::test::AssertResult ar(_fl_result); \
1091 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1092 fl::sstream ss; \
1093 ss << #a << " ~= " << #b << " (eps=" << (epsilon) << ")"; \
1094 ar.mExpression = ss.str(); \
1095 if (!_fl_result) { \
1096 fl::sstream ess; \
1097 ess << _fl_a << " ~= " << _fl_b << " (diff=" << _fl_diff << ")"; \
1098 ar.mExpanded = ess.str(); \
1099 } \
1100 fl::test::TestContext::instance().reportAssert(ar); \
1101 } while (0)
1102
1103#define FL_REQUIRE_CLOSE(a, b, epsilon) \
1104 do { \
1105 auto _fl_a = (a); \
1106 auto _fl_b = (b); \
1107 auto _fl_diff = _fl_a - _fl_b; \
1108 if (_fl_diff < 0) _fl_diff = -_fl_diff; \
1109 bool _fl_result = _fl_diff <= (epsilon); \
1110 fl::test::AssertResult ar(_fl_result); \
1111 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1112 fl::sstream ss; \
1113 ss << #a << " ~= " << #b << " (eps=" << (epsilon) << ")"; \
1114 ar.mExpression = ss.str(); \
1115 if (!_fl_result) { \
1116 fl::sstream ess; \
1117 ess << _fl_a << " ~= " << _fl_b << " (diff=" << _fl_diff << ")"; \
1118 ar.mExpanded = ess.str(); \
1119 fl::test::TestContext::instance().reportAssert(ar); \
1120 return; \
1121 } \
1122 fl::test::TestContext::instance().reportAssert(ar); \
1123 } while (0)
1124
1125// =============================================================================
1126// TEST_CASE_FIXTURE - run test with fixture setup/teardown
1127// =============================================================================
1128// Creates a test that inherits from a fixture class.
1129// The fixture's constructor is called before each test run,
1130// and the destructor is called after.
1131//
1132// Usage:
1133// struct MyFixture {
1134// int value;
1135// MyFixture() : value(42) {}
1136// ~MyFixture() { /* cleanup */ }
1137// };
1138//
1139// FL_TEST_CASE_FIXTURE(MyFixture, "test name") {
1140// FL_CHECK_EQ(value, 42); // Accesses fixture members directly
1141// }
1142
1143#define FL_TEST_CASE_FIXTURE(fixture, name) \
1144 struct FLTEST_UNIQUE(FLTEST_FIXTURE_) : public fixture { \
1145 void run(); \
1146 }; \
1147 static void FLTEST_UNIQUE(FLTEST_FIXTURE_FUNC_)() { \
1148 FLTEST_UNIQUE(FLTEST_FIXTURE_) instance; \
1149 instance.run(); \
1150 } \
1151 static fl::test::TestRegistrar FLTEST_UNIQUE(FLTEST_FIXTURE_REG_)( \
1152 FLTEST_UNIQUE(FLTEST_FIXTURE_FUNC_), name, __FILE__, __LINE__); \
1153 void FLTEST_UNIQUE(FLTEST_FIXTURE_)::run()
1154
1155// =============================================================================
1156// String comparison macros
1157// =============================================================================
1158// Useful for comparing string types without needing iostream
1159
1160#define FL_CHECK_STR_EQ(a, b) \
1161 do { \
1162 fl::string _fl_a_str(a); \
1163 fl::string _fl_b_str(b); \
1164 bool _fl_result = (_fl_a_str == _fl_b_str); \
1165 fl::test::AssertResult ar(_fl_result); \
1166 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1167 fl::sstream ss; \
1168 ss << #a << " == " << #b; \
1169 ar.mExpression = ss.str(); \
1170 if (!_fl_result) { \
1171 fl::sstream ess; \
1172 ess << "\"" << _fl_a_str << "\" != \"" << _fl_b_str << "\""; \
1173 ar.mExpanded = ess.str(); \
1174 } \
1175 fl::test::TestContext::instance().reportAssert(ar); \
1176 } while (0)
1177
1178#define FL_CHECK_STR_NE(a, b) \
1179 do { \
1180 fl::string _fl_a_str(a); \
1181 fl::string _fl_b_str(b); \
1182 bool _fl_result = (_fl_a_str != _fl_b_str); \
1183 fl::test::AssertResult ar(_fl_result); \
1184 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1185 fl::sstream ss; \
1186 ss << #a << " != " << #b; \
1187 ar.mExpression = ss.str(); \
1188 if (!_fl_result) { \
1189 fl::sstream ess; \
1190 ess << "Both equal: \"" << _fl_a_str << "\""; \
1191 ar.mExpanded = ess.str(); \
1192 } \
1193 fl::test::TestContext::instance().reportAssert(ar); \
1194 } while (0)
1195
1196// Check if string contains substring
1197#define FL_CHECK_STR_CONTAINS(haystack, needle) \
1198 do { \
1199 fl::string _fl_haystack(haystack); \
1200 fl::string _fl_needle(needle); \
1201 bool _fl_result = (_fl_haystack.find(_fl_needle) != fl::string::npos); \
1202 fl::test::AssertResult ar(_fl_result); \
1203 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1204 fl::sstream _fl_ss; \
1205 _fl_ss << #haystack << " contains " << #needle; \
1206 ar.mExpression = _fl_ss.str(); \
1207 if (!_fl_result) { \
1208 fl::sstream _fl_ess; \
1209 _fl_ess << "\"" << _fl_haystack << "\" does not contain \"" << _fl_needle << "\""; \
1210 ar.mExpanded = _fl_ess.str(); \
1211 } \
1212 fl::test::TestContext::instance().reportAssert(ar); \
1213 } while (0)
1214
1215#define FL_REQUIRE_STR_EQ(a, b) \
1216 do { \
1217 fl::string _fl_a_str(a); \
1218 fl::string _fl_b_str(b); \
1219 bool _fl_result = (_fl_a_str == _fl_b_str); \
1220 fl::test::AssertResult ar(_fl_result); \
1221 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1222 fl::sstream ss; \
1223 ss << #a << " == " << #b; \
1224 ar.mExpression = ss.str(); \
1225 if (!_fl_result) { \
1226 fl::sstream ess; \
1227 ess << "\"" << _fl_a_str << "\" != \"" << _fl_b_str << "\""; \
1228 ar.mExpanded = ess.str(); \
1229 fl::test::TestContext::instance().reportAssert(ar); \
1230 return; \
1231 } \
1232 fl::test::TestContext::instance().reportAssert(ar); \
1233 } while (0)
1234
1235#define FL_REQUIRE_STR_CONTAINS(haystack, needle) \
1236 do { \
1237 fl::string _fl_haystack(haystack); \
1238 fl::string _fl_needle(needle); \
1239 bool _fl_result = (_fl_haystack.find(_fl_needle) != fl::string::npos); \
1240 fl::test::AssertResult ar(_fl_result); \
1241 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1242 fl::sstream _fl_ss; \
1243 _fl_ss << #haystack << " contains " << #needle; \
1244 ar.mExpression = _fl_ss.str(); \
1245 if (!_fl_result) { \
1246 fl::sstream _fl_ess; \
1247 _fl_ess << "\"" << _fl_haystack << "\" does not contain \"" << _fl_needle << "\""; \
1248 ar.mExpanded = _fl_ess.str(); \
1249 fl::test::TestContext::instance().reportAssert(ar); \
1250 return; \
1251 } \
1252 fl::test::TestContext::instance().reportAssert(ar); \
1253 } while (0)
1254
1255// =============================================================================
1256// Array/Container comparison macros
1257// =============================================================================
1258// Compare arrays element-by-element with detailed mismatch reporting
1259
1260// FL_CHECK_ARRAY_EQ - Compare two arrays/spans element by element
1261// Usage: FL_CHECK_ARRAY_EQ(actual, expected, size)
1262// Note: Uses a lambda to encapsulate the loop to avoid C++11 scope issues
1263#define FL_CHECK_ARRAY_EQ(actual, expected, arrsize) \
1264 do { \
1265 bool _fl_arr_match = true; \
1266 fl::size _fl_arr_mismatch_idx = 0; \
1267 { \
1268 fl::size _fl_arr_i = 0; \
1269 fl::size _fl_arr_sz = static_cast<fl::size>(arrsize); \
1270 while (_fl_arr_i < _fl_arr_sz) { \
1271 if (!((actual)[_fl_arr_i] == (expected)[_fl_arr_i])) { \
1272 _fl_arr_match = false; \
1273 _fl_arr_mismatch_idx = _fl_arr_i; \
1274 break; \
1275 } \
1276 ++_fl_arr_i; \
1277 } \
1278 } \
1279 fl::test::AssertResult _fl_arr_ar(_fl_arr_match); \
1280 _fl_arr_ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1281 fl::sstream _fl_arr_ss; \
1282 _fl_arr_ss << #actual << " == " << #expected << " (size=" << (arrsize) << ")"; \
1283 _fl_arr_ar.mExpression = _fl_arr_ss.str(); \
1284 if (!_fl_arr_match) { \
1285 fl::sstream _fl_arr_ess; \
1286 _fl_arr_ess << "Mismatch at index " << _fl_arr_mismatch_idx \
1287 << ": " << (actual)[_fl_arr_mismatch_idx] \
1288 << " != " << (expected)[_fl_arr_mismatch_idx]; \
1289 _fl_arr_ar.mExpanded = _fl_arr_ess.str(); \
1290 } \
1291 fl::test::TestContext::instance().reportAssert(_fl_arr_ar); \
1292 } while (0)
1293
1294#define FL_REQUIRE_ARRAY_EQ(actual, expected, arrsize) \
1295 do { \
1296 bool _fl_arr_match = true; \
1297 fl::size _fl_arr_mismatch_idx = 0; \
1298 { \
1299 fl::size _fl_arr_i = 0; \
1300 fl::size _fl_arr_sz = static_cast<fl::size>(arrsize); \
1301 while (_fl_arr_i < _fl_arr_sz) { \
1302 if (!((actual)[_fl_arr_i] == (expected)[_fl_arr_i])) { \
1303 _fl_arr_match = false; \
1304 _fl_arr_mismatch_idx = _fl_arr_i; \
1305 break; \
1306 } \
1307 ++_fl_arr_i; \
1308 } \
1309 } \
1310 fl::test::AssertResult _fl_arr_ar(_fl_arr_match); \
1311 _fl_arr_ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1312 fl::sstream _fl_arr_ss; \
1313 _fl_arr_ss << #actual << " == " << #expected << " (size=" << (arrsize) << ")"; \
1314 _fl_arr_ar.mExpression = _fl_arr_ss.str(); \
1315 if (!_fl_arr_match) { \
1316 fl::sstream _fl_arr_ess; \
1317 _fl_arr_ess << "Mismatch at index " << _fl_arr_mismatch_idx \
1318 << ": " << (actual)[_fl_arr_mismatch_idx] \
1319 << " != " << (expected)[_fl_arr_mismatch_idx]; \
1320 _fl_arr_ar.mExpanded = _fl_arr_ess.str(); \
1321 fl::test::TestContext::instance().reportAssert(_fl_arr_ar); \
1322 return; \
1323 } \
1324 fl::test::TestContext::instance().reportAssert(_fl_arr_ar); \
1325 } while (0)
1326
1327// FL_CHECK_UNARY_PRED - Check that a unary predicate holds for an expression
1328// Usage: FL_CHECK_UNARY_PRED(pred, expr) where pred is a callable
1329// Note: Removed - requires C++14 (auto in lambdas). Use FL_CHECK directly instead.
1330// Example: FL_CHECK(isPositive(42)); FL_CHECK(isEven(100));
1331
1332// =============================================================================
1333// Exception testing macros (for platforms that support exceptions)
1334// =============================================================================
1335// Note: On embedded devices, exceptions may be disabled. These macros
1336// gracefully handle that case.
1337
1338#if defined(__cpp_exceptions) || defined(__EXCEPTIONS)
1339#define FLTEST_EXCEPTIONS_ENABLED 1
1340#else
1341#define FLTEST_EXCEPTIONS_ENABLED 0
1342#endif
1343
1344#if FLTEST_EXCEPTIONS_ENABLED
1345
1346#define FL_CHECK_THROWS(expr) \
1347 do { \
1348 bool _fl_threw = false; \
1349 try { (void)(expr); } \
1350 catch (...) { _fl_threw = true; } \
1351 fl::test::AssertResult ar(_fl_threw); \
1352 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1353 ar.mExpression = #expr " throws"; \
1354 if (!_fl_threw) { \
1355 ar.mExpanded = "No exception was thrown"; \
1356 } \
1357 fl::test::TestContext::instance().reportAssert(ar); \
1358 } while (0)
1359
1360#define FL_CHECK_NOTHROW(expr) \
1361 do { \
1362 bool _fl_threw = false; \
1363 try { (void)(expr); } \
1364 catch (...) { _fl_threw = true; } \
1365 fl::test::AssertResult ar(!_fl_threw); \
1366 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1367 ar.mExpression = #expr " nothrow"; \
1368 if (_fl_threw) { \
1369 ar.mExpanded = "An exception was thrown"; \
1370 } \
1371 fl::test::TestContext::instance().reportAssert(ar); \
1372 } while (0)
1373
1374#define FL_REQUIRE_THROWS(expr) \
1375 do { \
1376 bool _fl_threw = false; \
1377 try { (void)(expr); } \
1378 catch (...) { _fl_threw = true; } \
1379 fl::test::AssertResult ar(_fl_threw); \
1380 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1381 ar.mExpression = #expr " throws"; \
1382 if (!_fl_threw) { \
1383 ar.mExpanded = "No exception was thrown"; \
1384 fl::test::TestContext::instance().reportAssert(ar); \
1385 return; \
1386 } \
1387 fl::test::TestContext::instance().reportAssert(ar); \
1388 } while (0)
1389
1390#define FL_REQUIRE_NOTHROW(expr) \
1391 do { \
1392 bool _fl_threw = false; \
1393 try { (void)(expr); } \
1394 catch (...) { _fl_threw = true; } \
1395 fl::test::AssertResult ar(!_fl_threw); \
1396 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1397 ar.mExpression = #expr " nothrow"; \
1398 if (_fl_threw) { \
1399 ar.mExpanded = "An exception was thrown"; \
1400 fl::test::TestContext::instance().reportAssert(ar); \
1401 return; \
1402 } \
1403 fl::test::TestContext::instance().reportAssert(ar); \
1404 } while (0)
1405
1406// CHECK_THROWS_AS - Check that expression throws a specific exception type
1407// Note: exType must be a complete type (e.g., std::runtime_error, int, MyException)
1408#define FL_CHECK_THROWS_AS(expr, exType) \
1409 do { \
1410 bool _fl_threw_correct = false; \
1411 bool _fl_threw_any = false; \
1412 try { (void)(expr); } \
1413 catch (const exType&) { _fl_threw_correct = true; _fl_threw_any = true; } \
1414 catch (...) { _fl_threw_any = true; } \
1415 fl::test::AssertResult ar(_fl_threw_correct); \
1416 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1417 fl::sstream _fl_expr_ss; \
1418 _fl_expr_ss << #expr << " throws " << #exType; \
1419 ar.mExpression = _fl_expr_ss.str(); \
1420 if (!_fl_threw_correct) { \
1421 if (_fl_threw_any) { \
1422 ar.mExpanded = "Threw a different exception type"; \
1423 } else { \
1424 ar.mExpanded = "No exception was thrown"; \
1425 } \
1426 } \
1427 fl::test::TestContext::instance().reportAssert(ar); \
1428 } while (0)
1429
1430#define FL_REQUIRE_THROWS_AS(expr, exType) \
1431 do { \
1432 bool _fl_threw_correct = false; \
1433 bool _fl_threw_any = false; \
1434 try { (void)(expr); } \
1435 catch (const exType&) { _fl_threw_correct = true; _fl_threw_any = true; } \
1436 catch (...) { _fl_threw_any = true; } \
1437 fl::test::AssertResult ar(_fl_threw_correct); \
1438 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1439 fl::sstream _fl_expr_ss; \
1440 _fl_expr_ss << #expr << " throws " << #exType; \
1441 ar.mExpression = _fl_expr_ss.str(); \
1442 if (!_fl_threw_correct) { \
1443 if (_fl_threw_any) { \
1444 ar.mExpanded = "Threw a different exception type"; \
1445 } else { \
1446 ar.mExpanded = "No exception was thrown"; \
1447 } \
1448 fl::test::TestContext::instance().reportAssert(ar); \
1449 return; \
1450 } \
1451 fl::test::TestContext::instance().reportAssert(ar); \
1452 } while (0)
1453
1454// CHECK_THROWS_WITH - Check that expression throws and exception.what() contains message
1455// Note: This only works with exceptions derived from std::exception
1456#define FL_CHECK_THROWS_WITH(expr, msg) \
1457 do { \
1458 bool _fl_threw = false; \
1459 bool _fl_msg_match = false; \
1460 fl::string _fl_actual_msg; \
1461 try { (void)(expr); } \
1462 catch (const std::exception& e) { \
1463 _fl_threw = true; \
1464 _fl_actual_msg = e.what(); \
1465 _fl_msg_match = (_fl_actual_msg.find(msg) != fl::string::npos); \
1466 } \
1467 catch (...) { _fl_threw = true; } \
1468 fl::test::AssertResult ar(_fl_threw && _fl_msg_match); \
1469 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1470 fl::sstream _fl_expr_ss; \
1471 _fl_expr_ss << #expr << " throws with \"" << msg << "\""; \
1472 ar.mExpression = _fl_expr_ss.str(); \
1473 if (!_fl_threw) { \
1474 ar.mExpanded = "No exception was thrown"; \
1475 } else if (!_fl_msg_match) { \
1476 fl::sstream _fl_exp_ss; \
1477 _fl_exp_ss << "Exception message: \"" << _fl_actual_msg << "\""; \
1478 ar.mExpanded = _fl_exp_ss.str(); \
1479 } \
1480 fl::test::TestContext::instance().reportAssert(ar); \
1481 } while (0)
1482
1483#define FL_REQUIRE_THROWS_WITH(expr, msg) \
1484 do { \
1485 bool _fl_threw = false; \
1486 bool _fl_msg_match = false; \
1487 fl::string _fl_actual_msg; \
1488 try { (void)(expr); } \
1489 catch (const std::exception& e) { \
1490 _fl_threw = true; \
1491 _fl_actual_msg = e.what(); \
1492 _fl_msg_match = (_fl_actual_msg.find(msg) != fl::string::npos); \
1493 } \
1494 catch (...) { _fl_threw = true; } \
1495 fl::test::AssertResult ar(_fl_threw && _fl_msg_match); \
1496 ar.mLocation = fl::test::SourceLocation(__FILE__, __LINE__); \
1497 fl::sstream _fl_expr_ss; \
1498 _fl_expr_ss << #expr << " throws with \"" << msg << "\""; \
1499 ar.mExpression = _fl_expr_ss.str(); \
1500 if (!_fl_threw) { \
1501 ar.mExpanded = "No exception was thrown"; \
1502 } else if (!_fl_msg_match) { \
1503 fl::sstream _fl_exp_ss; \
1504 _fl_exp_ss << "Exception message: \"" << _fl_actual_msg << "\""; \
1505 ar.mExpanded = _fl_exp_ss.str(); \
1506 } \
1507 fl::test::TestContext::instance().reportAssert(ar); \
1508 if (!(_fl_threw && _fl_msg_match)) return; \
1509 } while (0)
1510
1511// =============================================================================
1512// WARN exception macros - log warnings but don't affect pass/fail
1513// =============================================================================
1514// These macros check exception behavior but only log warnings, never failing the test
1515
1516#define FL_WARN_THROWS(expr) \
1517 do { \
1518 bool _fl_threw = false; \
1519 try { (void)(expr); } \
1520 catch (...) { _fl_threw = true; } \
1521 if (!_fl_threw) { \
1522 fl::test::outputMessage("Warning: " #expr " did not throw", __FILE__, __LINE__); \
1523 } \
1524 } while (0)
1525
1526#define FL_WARN_NOTHROW(expr) \
1527 do { \
1528 bool _fl_threw = false; \
1529 try { (void)(expr); } \
1530 catch (...) { _fl_threw = true; } \
1531 if (_fl_threw) { \
1532 fl::test::outputMessage("Warning: " #expr " threw an exception", __FILE__, __LINE__); \
1533 } \
1534 } while (0)
1535
1536#define FL_WARN_THROWS_AS(expr, exType) \
1537 do { \
1538 bool _fl_threw_correct = false; \
1539 bool _fl_threw_any = false; \
1540 try { (void)(expr); } \
1541 catch (const exType&) { _fl_threw_correct = true; _fl_threw_any = true; } \
1542 catch (...) { _fl_threw_any = true; } \
1543 if (!_fl_threw_correct) { \
1544 if (_fl_threw_any) { \
1545 fl::test::outputMessage("Warning: " #expr " threw different type than " #exType, __FILE__, __LINE__); \
1546 } else { \
1547 fl::test::outputMessage("Warning: " #expr " did not throw " #exType, __FILE__, __LINE__); \
1548 } \
1549 } \
1550 } while (0)
1551
1552#define FL_WARN_THROWS_WITH(expr, msg) \
1553 do { \
1554 bool _fl_threw = false; \
1555 bool _fl_msg_match = false; \
1556 fl::string _fl_actual_msg; \
1557 try { (void)(expr); } \
1558 catch (const std::exception& e) { \
1559 _fl_threw = true; \
1560 _fl_actual_msg = e.what(); \
1561 _fl_msg_match = (_fl_actual_msg.find(msg) != fl::string::npos); \
1562 } \
1563 catch (...) { _fl_threw = true; } \
1564 if (!_fl_threw) { \
1565 fl::test::outputMessage("Warning: " #expr " did not throw", __FILE__, __LINE__); \
1566 } else if (!_fl_msg_match) { \
1567 fl::sstream _fl_warn_ss; \
1568 _fl_warn_ss << "Warning: " #expr " threw but message \"" << _fl_actual_msg \
1569 << "\" does not contain \"" << msg << "\""; \
1570 fl::test::outputMessage(_fl_warn_ss.str().c_str(), __FILE__, __LINE__); \
1571 } \
1572 } while (0)
1573
1574#else // !FLTEST_EXCEPTIONS_ENABLED
1575
1576// Stub macros for platforms without exception support
1577#define FL_CHECK_THROWS(expr) \
1578 do { \
1579 fl::test::outputMessage("CHECK_THROWS skipped (exceptions disabled)", __FILE__, __LINE__); \
1580 } while (0)
1581
1582#define FL_CHECK_NOTHROW(expr) \
1583 do { (void)(expr); } while (0) // Just execute it
1584
1585#define FL_REQUIRE_THROWS(expr) \
1586 do { \
1587 fl::test::outputMessage("REQUIRE_THROWS skipped (exceptions disabled)", __FILE__, __LINE__); \
1588 } while (0)
1589
1590#define FL_REQUIRE_NOTHROW(expr) \
1591 do { (void)(expr); } while (0) // Just execute it
1592
1593#define FL_CHECK_THROWS_AS(expr, exType) \
1594 do { \
1595 fl::test::outputMessage("CHECK_THROWS_AS skipped (exceptions disabled)", __FILE__, __LINE__); \
1596 } while (0)
1597
1598#define FL_REQUIRE_THROWS_AS(expr, exType) \
1599 do { \
1600 fl::test::outputMessage("REQUIRE_THROWS_AS skipped (exceptions disabled)", __FILE__, __LINE__); \
1601 } while (0)
1602
1603#define FL_CHECK_THROWS_WITH(expr, msg) \
1604 do { \
1605 fl::test::outputMessage("CHECK_THROWS_WITH skipped (exceptions disabled)", __FILE__, __LINE__); \
1606 } while (0)
1607
1608#define FL_REQUIRE_THROWS_WITH(expr, msg) \
1609 do { \
1610 fl::test::outputMessage("REQUIRE_THROWS_WITH skipped (exceptions disabled)", __FILE__, __LINE__); \
1611 } while (0)
1612
1613#define FL_WARN_THROWS(expr) \
1614 do { \
1615 fl::test::outputMessage("WARN_THROWS skipped (exceptions disabled)", __FILE__, __LINE__); \
1616 } while (0)
1617
1618#define FL_WARN_NOTHROW(expr) \
1619 do { (void)(expr); } while (0)
1620
1621#define FL_WARN_THROWS_AS(expr, exType) \
1622 do { \
1623 fl::test::outputMessage("WARN_THROWS_AS skipped (exceptions disabled)", __FILE__, __LINE__); \
1624 } while (0)
1625
1626#define FL_WARN_THROWS_WITH(expr, msg) \
1627 do { \
1628 fl::test::outputMessage("WARN_THROWS_WITH skipped (exceptions disabled)", __FILE__, __LINE__); \
1629 } while (0)
1630
1631#endif // FLTEST_EXCEPTIONS_ENABLED
1632
1633// =============================================================================
1634// Compatibility aliases for gradual migration from doctest
1635// =============================================================================
1636#ifdef FLTEST_ENABLE_DOCTEST_COMPAT
1637#define TEST_CASE(name) FL_TEST_CASE(name)
1638#define SUBCASE(name) FL_SUBCASE(name)
1639#define CHECK(expr) FL_CHECK(expr)
1640#define CHECK_FALSE(expr) FL_CHECK_FALSE(expr)
1641#define CHECK_EQ(a, b) FL_CHECK_EQ(a, b)
1642#define CHECK_NE(a, b) FL_CHECK_NE(a, b)
1643#define CHECK_LT(a, b) FL_CHECK_LT(a, b)
1644#define CHECK_GT(a, b) FL_CHECK_GT(a, b)
1645#define CHECK_LE(a, b) FL_CHECK_LE(a, b)
1646#define CHECK_GE(a, b) FL_CHECK_GE(a, b)
1647#define REQUIRE(expr) FL_REQUIRE(expr)
1648#define REQUIRE_FALSE(expr) FL_REQUIRE_FALSE(expr)
1649#define REQUIRE_EQ(a, b) FL_REQUIRE_EQ(a, b)
1650#define REQUIRE_NE(a, b) FL_REQUIRE_NE(a, b)
1651#define REQUIRE_LT(a, b) FL_REQUIRE_LT(a, b)
1652#define REQUIRE_GT(a, b) FL_REQUIRE_GT(a, b)
1653#define REQUIRE_LE(a, b) FL_REQUIRE_LE(a, b)
1654#define REQUIRE_GE(a, b) FL_REQUIRE_GE(a, b)
1655#define MESSAGE(msg) FL_MESSAGE(msg)
1656#define INFO(msg) FL_INFO(msg)
1657#define CAPTURE(x) FL_CAPTURE(x)
1658#define FAIL(msg) FL_FAIL(msg)
1659#define FAIL_CHECK(msg) FL_FAIL_CHECK(msg)
1660#define WARN(expr) FL_WARN(expr)
1661// WARN comparison macros (log warnings but don't fail)
1662#define WARN_FALSE(expr) FL_WARN_FALSE(expr)
1663#define WARN_EQ(a, b) FL_WARN_EQ(a, b)
1664#define WARN_NE(a, b) FL_WARN_NE(a, b)
1665#define WARN_LT(a, b) FL_WARN_LT(a, b)
1666#define WARN_GT(a, b) FL_WARN_GT(a, b)
1667#define WARN_LE(a, b) FL_WARN_LE(a, b)
1668#define WARN_GE(a, b) FL_WARN_GE(a, b)
1669// CHECK_MESSAGE/REQUIRE_MESSAGE for assertions with custom messages
1670#define CHECK_MESSAGE(expr, msg) FL_CHECK_MESSAGE(expr, msg)
1671#define REQUIRE_MESSAGE(expr, msg) FL_REQUIRE_MESSAGE(expr, msg)
1672// BDD-style macros
1673#define SCENARIO(name) FL_SCENARIO(name)
1674#define GIVEN(name) FL_GIVEN(name)
1675#define WHEN(name) FL_WHEN(name)
1676#define AND_WHEN(name) FL_AND_WHEN(name)
1677#define THEN(name) FL_THEN(name)
1678#define AND_THEN(name) FL_AND_THEN(name)
1679// Fixture and suite
1680#define TEST_CASE_FIXTURE(fixture, name) FL_TEST_CASE_FIXTURE(fixture, name)
1681#define TEST_SUITE(name) FL_TEST_SUITE(name)
1682// CHECK_CLOSE for absolute tolerance
1683#define CHECK_CLOSE(a, b, eps) FL_CHECK_CLOSE(a, b, eps)
1684#define REQUIRE_CLOSE(a, b, eps) FL_REQUIRE_CLOSE(a, b, eps)
1685// String comparison macros
1686#define CHECK_STR_EQ(a, b) FL_CHECK_STR_EQ(a, b)
1687#define CHECK_STR_NE(a, b) FL_CHECK_STR_NE(a, b)
1688#define CHECK_STR_CONTAINS(str, substr) FL_CHECK_STR_CONTAINS(str, substr)
1689#define REQUIRE_STR_EQ(a, b) FL_REQUIRE_STR_EQ(a, b)
1690#define REQUIRE_STR_CONTAINS(str, substr) FL_REQUIRE_STR_CONTAINS(str, substr)
1691// Exception testing macros
1692#define CHECK_THROWS(expr) FL_CHECK_THROWS(expr)
1693#define CHECK_NOTHROW(expr) FL_CHECK_NOTHROW(expr)
1694#define REQUIRE_THROWS(expr) FL_REQUIRE_THROWS(expr)
1695#define REQUIRE_NOTHROW(expr) FL_REQUIRE_NOTHROW(expr)
1696#define CHECK_THROWS_AS(expr, exType) FL_CHECK_THROWS_AS(expr, exType)
1697#define REQUIRE_THROWS_AS(expr, exType) FL_REQUIRE_THROWS_AS(expr, exType)
1698#define CHECK_THROWS_WITH(expr, msg) FL_CHECK_THROWS_WITH(expr, msg)
1699#define REQUIRE_THROWS_WITH(expr, msg) FL_REQUIRE_THROWS_WITH(expr, msg)
1700// WARN exception macros (log warnings but don't fail)
1701#define WARN_THROWS(expr) FL_WARN_THROWS(expr)
1702#define WARN_NOTHROW(expr) FL_WARN_NOTHROW(expr)
1703#define WARN_THROWS_AS(expr, exType) FL_WARN_THROWS_AS(expr, exType)
1704#define WARN_THROWS_WITH(expr, msg) FL_WARN_THROWS_WITH(expr, msg)
1705// Skip macro
1706#define SKIP(reason) FL_SKIP(reason)
1707// Array/container comparison macros
1708#define CHECK_ARRAY_EQ(actual, expected, size) FL_CHECK_ARRAY_EQ(actual, expected, size)
1709#define REQUIRE_ARRAY_EQ(actual, expected, size) FL_REQUIRE_ARRAY_EQ(actual, expected, size)
1710// Template test macros
1711#define TYPE_TO_STRING(type, str) FL_TYPE_TO_STRING(type, str)
1712#define TYPE_TO_STRING_AS(str, type) FL_TYPE_TO_STRING_AS(str, type)
1713#define TEST_CASE_TEMPLATE(name, T, ...) FL_TEST_CASE_TEMPLATE(name, T, __VA_ARGS__)
1714#define TEST_CASE_TEMPLATE_DEFINE(name, T, id) FL_TEST_CASE_TEMPLATE_DEFINE(name, T, id)
1715#define TEST_CASE_TEMPLATE_INVOKE(id, ...) FL_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__)
1716#define TEST_CASE_TEMPLATE_APPLY(id, typelist) FL_TEST_CASE_TEMPLATE_APPLY(id, typelist)
1717
1718// When using doctest compatibility, also provide doctest::Approx alias
1719namespace doctest {
1720 using Approx = fl::test::Approx;
1721}
1722#endif
1723
1724// =============================================================================
1725// TEST_CASE_TEMPLATE - Parameterized type testing
1726// =============================================================================
1727// Runs the same test body for multiple types. This is useful for testing
1728// generic code with different type parameters.
1729//
1730// Usage:
1731// FL_TEST_CASE_TEMPLATE("vector operations", T, int, float, double) {
1732// fl::vector<T> v;
1733// v.push_back(T(42));
1734// FL_CHECK_EQ(v.size(), 1u);
1735// }
1736//
1737// The test will run once for each type in the list. The type name is appended
1738// to the test case name: "vector operations<int>", "vector operations<float>", etc.
1739//
1740// For custom type names, use FL_TYPE_TO_STRING:
1741// FL_TYPE_TO_STRING(MyType, "MyType")
1742// FL_TEST_CASE_TEMPLATE("test", T, MyType, int) { ... }
1743
1744namespace fl {
1745namespace test {
1746namespace detail {
1747
1748// Type name extraction using compiler-specific intrinsics
1749// This extracts the type name from FL_PRETTY_FUNCTION (__PRETTY_FUNCTION__ or __FUNCSIG__)
1750template <typename T>
1752#if defined(__clang__) || defined(__GNUC__)
1753 // FL_PRETTY_FUNCTION format: "fl::string fl::test::detail::getTypeName() [T = int]"
1754 const char* func = FL_PRETTY_FUNCTION;
1755 const char* begin = func;
1756 // Find "T = "
1757 while (*begin && !(begin[0] == 'T' && begin[1] == ' ' && begin[2] == '=' && begin[3] == ' '))
1758 ++begin;
1759 if (*begin) {
1760 begin += 4; // Skip "T = "
1761 const char* end = begin;
1762 int depth = 0;
1763 while (*end) {
1764 if (*end == '<') ++depth;
1765 else if (*end == '>') {
1766 if (depth == 0) break;
1767 --depth;
1768 }
1769 else if (*end == ']' && depth == 0) break;
1770 ++end;
1771 }
1772 return fl::string(begin, static_cast<fl::size>(end - begin));
1773 }
1774#elif defined(_MSC_VER)
1775 // FL_PRETTY_FUNCTION format: "class fl::string __cdecl fl::test::detail::getTypeName<int>(void)"
1776 const char* func = FL_PRETTY_FUNCTION;
1777 const char* begin = func;
1778 // Find "getTypeName<"
1779 while (*begin && !(begin[0] == 'g' && begin[1] == 'e' && begin[2] == 't' && begin[3] == 'T' &&
1780 begin[4] == 'y' && begin[5] == 'p' && begin[6] == 'e' && begin[7] == 'N' &&
1781 begin[8] == 'a' && begin[9] == 'm' && begin[10] == 'e' && begin[11] == '<'))
1782 ++begin;
1783 if (*begin) {
1784 begin += 12; // Skip "getTypeName<"
1785 const char* end = begin;
1786 int depth = 1; // We're inside the first <
1787 while (*end && depth > 0) {
1788 if (*end == '<') ++depth;
1789 else if (*end == '>') --depth;
1790 ++end;
1791 }
1792 if (end > begin && depth == 0) --end; // Back up before final >
1793 return fl::string(begin, static_cast<fl::size>(end - begin));
1794 }
1795#endif
1796 return fl::string("{unknown}");
1797}
1798
1799// Specialization holder for custom type names
1800template <typename T>
1802 static const char* name() FL_NOEXCEPT {
1803 static fl::string cachedName = getTypeName<T>();
1804 return cachedName.c_str();
1805 }
1806};
1807
1808// TypeList for storing types
1809template <typename... Ts>
1810struct TypeList {};
1811
1812// Helper to iterate over TypeList and register tests
1813template <typename TL, typename TestFunc>
1815
1816// Base case: empty TypeList
1817template <typename TestFunc>
1818struct TypeIterator<TypeList<>, TestFunc> {
1819 static void iterate(const char*, const char*, int, int) FL_NOEXCEPT {}
1820};
1821
1822// Recursive case: TypeList with at least one type
1823template <typename T, typename... Rest, typename TestFunc>
1824struct TypeIterator<TypeList<T, Rest...>, TestFunc> {
1825 static void iterate(const char* baseName, const char* file, int line, int index) FL_NOEXCEPT {
1826 // Build the test name: "baseName<TypeName>"
1827 fl::sstream ss;
1828 ss << baseName << "<" << TypeNameHolder<T>::name() << ">";
1829 fl::string testName = ss.str();
1830
1831 // Register this instantiation
1832 TestContext::instance().registerTest(TestFunc::template run<T>, testName.c_str(), file, line);
1833
1834 // Continue with remaining types
1835 TypeIterator<TypeList<Rest...>, TestFunc>::iterate(baseName, file, line, index + 1);
1836 }
1837};
1838
1839} // namespace detail
1840} // namespace test
1841} // namespace fl
1842
1843// FL_TYPE_TO_STRING - Define custom stringification for a type
1844// Usage: FL_TYPE_TO_STRING(MyType, "MyType")
1845#define FL_TYPE_TO_STRING(type, str) \
1846 namespace fl { namespace test { namespace detail { \
1847 template <> \
1848 struct TypeNameHolder<type> { \
1849 static const char* name() { return str; } \
1850 }; \
1851 }}}
1852
1853// FL_TYPE_TO_STRING_AS - Alias for FL_TYPE_TO_STRING (doctest compatibility)
1854#define FL_TYPE_TO_STRING_AS(str, type) FL_TYPE_TO_STRING(type, str)
1855
1856// FL_TEST_CASE_TEMPLATE - Define a test that runs for multiple types
1857// Usage: FL_TEST_CASE_TEMPLATE("test name", T, int, float, double) { ... }
1858//
1859// Implementation note: We use a struct with a static template method to hold
1860// the test function, which allows us to instantiate it for each type.
1861#define FL_TEST_CASE_TEMPLATE(name, T, ...) \
1862 template <typename T> \
1863 static void FLTEST_UNIQUE(FLTEST_TMPL_FUNC_)(); \
1864 struct FLTEST_UNIQUE(FLTEST_TMPL_REG_) { \
1865 template <typename T> \
1866 static void run() { \
1867 FLTEST_UNIQUE(FLTEST_TMPL_FUNC_)<T>(); \
1868 } \
1869 }; \
1870 static struct FLTEST_UNIQUE(FLTEST_TMPL_INIT_) { \
1871 FLTEST_UNIQUE(FLTEST_TMPL_INIT_)() { \
1872 using TL = fl::test::detail::TypeList<__VA_ARGS__>; \
1873 fl::test::detail::TypeIterator<TL, FLTEST_UNIQUE(FLTEST_TMPL_REG_)>::iterate( \
1874 name, __FILE__, __LINE__, 0); \
1875 } \
1876 } FLTEST_UNIQUE(FLTEST_TMPL_INST_); \
1877 template <typename T> \
1878 static void FLTEST_UNIQUE(FLTEST_TMPL_FUNC_)()
1879
1880// FL_TEST_CASE_TEMPLATE_DEFINE - Define a template test without immediate instantiation
1881// Usage:
1882// FL_TEST_CASE_TEMPLATE_DEFINE("test name", T, my_test_id) { ... }
1883// FL_TEST_CASE_TEMPLATE_INVOKE(my_test_id, int, float, double)
1884#define FL_TEST_CASE_TEMPLATE_DEFINE(name, T, id) \
1885 template <typename T> \
1886 static void FLTEST_CAT(id, _FUNC_)(); \
1887 struct FLTEST_CAT(id, _REG_) { \
1888 template <typename T> \
1889 static void run() { \
1890 FLTEST_CAT(id, _FUNC_)<T>(); \
1891 } \
1892 static const char* baseName() { return name; } \
1893 }; \
1894 template <typename T> \
1895 static void FLTEST_CAT(id, _FUNC_)()
1896
1897// FL_TEST_CASE_TEMPLATE_INVOKE - Instantiate a previously defined template test
1898// Usage: FL_TEST_CASE_TEMPLATE_INVOKE(id, int, float, double); // Note: semicolon required
1899#define FL_TEST_CASE_TEMPLATE_INVOKE(id, ...) \
1900 static struct FLTEST_UNIQUE(FLTEST_TMPL_INVOKE_) { \
1901 FLTEST_UNIQUE(FLTEST_TMPL_INVOKE_)() { \
1902 using TL = fl::test::detail::TypeList<__VA_ARGS__>; \
1903 fl::test::detail::TypeIterator<TL, FLTEST_CAT(id, _REG_)>::iterate( \
1904 FLTEST_CAT(id, _REG_)::baseName(), __FILE__, __LINE__, 0); \
1905 } \
1906 } FLTEST_UNIQUE(FLTEST_TMPL_INVOKE_INST_); \
1907 FL_STATIC_ASSERT(true, "")
1908
1909// FL_TEST_CASE_TEMPLATE_APPLY - Same as INVOKE but takes a TypeList instead of types
1910// Usage: FL_TEST_CASE_TEMPLATE_APPLY(my_test_id, MyTypeList); // Note: semicolon required
1911// where MyTypeList = fl::test::detail::TypeList<int, float, double>
1912#define FL_TEST_CASE_TEMPLATE_APPLY(id, typelist) \
1913 static struct FLTEST_UNIQUE(FLTEST_TMPL_APPLY_) { \
1914 FLTEST_UNIQUE(FLTEST_TMPL_APPLY_)() { \
1915 fl::test::detail::TypeIterator<typelist, FLTEST_CAT(id, _REG_)>::iterate( \
1916 FLTEST_CAT(id, _REG_)::baseName(), __FILE__, __LINE__, 0); \
1917 } \
1918 } FLTEST_UNIQUE(FLTEST_TMPL_APPLY_INST_); \
1919 FL_STATIC_ASSERT(true, "")
1920
1921// =============================================================================
1922// Standalone main implementation macro
1923// =============================================================================
1924// Define FLTEST_IMPLEMENT_MAIN in exactly one source file to provide a main()
1925// that runs all registered tests. This is useful for standalone test executables.
1926//
1927// Usage:
1928// // In your main test file:
1929// #define FLTEST_IMPLEMENT_MAIN
1930// #include "fl/test/fltest.h"
1931//
1932// FL_TEST_CASE("my test") { FL_CHECK(1 == 1); }
1933//
1934// For embedded devices, you can instead call fl::test::TestContext::instance().run()
1935// directly from your setup() function.
1936
1937#ifdef FLTEST_IMPLEMENT_MAIN
1938int main(int argc, char** argv) {
1939 return fl::test::TestContext::instance().run(argc, const_cast<const char**>(argv));
1940}
1941#endif
1942
1943// =============================================================================
1944// Arduino/ESP32 test runner macro
1945// =============================================================================
1946// For Arduino-style embedded environments, use this in your setup() function:
1947//
1948// #include "fl/test/fltest.h"
1949//
1950// void setup() {
1951// Serial.begin(115200);
1952// FL_RUN_ALL_TESTS_ARDUINO(Serial);
1953// }
1954//
1955// This macro:
1956// 1. Creates a SerialReporter that outputs to the provided serial port
1957// 2. Runs all tests and prints results
1958// 3. Halts (infinite loop) after tests complete
1959
1960#define FL_RUN_ALL_TESTS_ARDUINO(serial_obj) \
1961 do { \
1962 auto _fl_serial_print = [](const char* msg) { serial_obj.print(msg); }; \
1963 fl::test::SerialReporter _fl_reporter(_fl_serial_print); \
1964 fl::test::TestContext::instance().setReporter(&_fl_reporter); \
1965 int _fl_result = fl::test::TestContext::instance().run(); \
1966 if (_fl_result == 0) { \
1967 serial_obj.println("\n=== ALL TESTS PASSED ==="); \
1968 } else { \
1969 serial_obj.println("\n=== TESTS FAILED ==="); \
1970 } \
1971 while (true) { delay(1000); } /* Halt after tests */ \
1972 } while (0)
Serial print("Error: ")
const char * c_str() const FL_NOEXCEPT
string str() const FL_NOEXCEPT
Definition strstream.h:43
Approx & margin(double newMargin) FL_NOEXCEPT
Set absolute margin for comparison When margin > 0, passes if |a - b| <= margin.
Definition fltest.h:414
friend bool operator>(double lhs, const Approx &rhs) FL_NOEXCEPT
Definition fltest.h:472
friend bool operator<=(double lhs, const Approx &rhs) FL_NOEXCEPT
Definition fltest.h:460
friend bool operator==(double lhs, const Approx &rhs) FL_NOEXCEPT
Definition fltest.h:431
double getScale() const FL_NOEXCEPT
Definition fltest.h:428
double mEpsilon
Definition fltest.h:478
Approx & epsilon(double newEpsilon) FL_NOEXCEPT
Set custom relative epsilon for comparison Comparison uses: epsilon * (scale + max(|a|,...
Definition fltest.h:407
friend bool operator!=(double lhs, const Approx &rhs) FL_NOEXCEPT
Definition fltest.h:452
Approx & scale(double newScale) FL_NOEXCEPT
Set custom scale for comparison.
Definition fltest.h:420
friend bool operator==(const Approx &lhs, double rhs) FL_NOEXCEPT
Definition fltest.h:448
double mValue
Definition fltest.h:477
double mScale
Definition fltest.h:480
double getMargin() const FL_NOEXCEPT
Definition fltest.h:427
Approx(double value) FL_NOEXCEPT
Definition fltest.h:398
double mMargin
Definition fltest.h:479
friend bool operator>=(double lhs, const Approx &rhs) FL_NOEXCEPT
Definition fltest.h:464
friend bool operator!=(const Approx &lhs, double rhs) FL_NOEXCEPT
Definition fltest.h:456
friend bool operator<(double lhs, const Approx &rhs) FL_NOEXCEPT
Definition fltest.h:468
double getEpsilon() const FL_NOEXCEPT
Definition fltest.h:426
double value() const FL_NOEXCEPT
Definition fltest.h:425
Helper class for approximate floating-point comparisons.
Definition fltest.h:396
virtual void testCaseStart(const char *name) FL_NOEXCEPT=0
virtual void testRunStart() FL_NOEXCEPT=0
virtual void assertResult(const AssertResult &result) FL_NOEXCEPT=0
virtual void testRunEnd(const TestStats &stats) FL_NOEXCEPT=0
virtual void subcaseStart(const char *name) FL_NOEXCEPT=0
virtual ~IReporter() FL_NOEXCEPT=default
virtual void testCaseEnd(bool passed, fl::u32 durationMs=0) FL_NOEXCEPT=0
Called when a test case ends.
virtual void subcaseEnd() FL_NOEXCEPT=0
fl::vector< fl::string > mCurrentTestFailures
Definition fltest.h:620
static fl::string escapeJson(const char *text) FL_NOEXCEPT
fl::string mCurrentTestName
Definition fltest.h:619
fl::vector< fl::string > mTestResults
Definition fltest.h:624
JSONReporter(fl::string *outputBuffer) FL_NOEXCEPT
Create a JSON reporter that writes to the given string buffer.
Definition fltest.h:604
fl::string * mOutput
Definition fltest.h:616
void setPrintFunc(SerialPrintFunc func) FL_NOEXCEPT
Set the print function.
Definition fltest.h:534
SerialReporter(SerialPrintFunc printFunc=nullptr) FL_NOEXCEPT
Create a serial reporter with custom print function.
Definition fltest.h:522
SerialPrintFunc mPrintFunc
Definition fltest.h:537
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
fl::string * mOutput
Definition fltest.h:670
TAPReporter(SerialPrintFunc printFunc) FL_NOEXCEPT
Create a TAP reporter that uses a print function for streaming output.
Definition fltest.h:654
SerialPrintFunc mPrintFunc
Definition fltest.h:671
void setTotalTests(fl::u32 total) FL_NOEXCEPT
Set the total number of tests (for TAP plan line) If not set, plan is output at the end instead.
Definition fltest.h:667
fl::string mStreamingOutput
Definition fltest.h:679
fl::vector< fl::string > mDiagnostics
Definition fltest.h:678
TAPReporter(fl::string *outputBuffer) FL_NOEXCEPT
Create a TAP reporter that writes to the given string buffer.
Definition fltest.h:650
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
bool hasFailure() const FL_NOEXCEPT
Definition fltest.h:228
const TestStats & stats() const FL_NOEXCEPT
Definition fltest.h:225
TimeoutHandlerFunc mTimeoutHandler
Definition fltest.h:267
bool needsReentry() const FL_NOEXCEPT
Definition fltest.h:212
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
void setCurrentTestFailed(bool failed) FL_NOEXCEPT
Definition fltest.h:229
fl::vector< fl::u32 > mFullyTraversedHashes
Definition fltest.h:254
fl::vector< SubcaseSignature > mSubcaseStack
Definition fltest.h:252
void(* TestFunc)()
Definition fltest.h:183
IReporter * reporter() FL_NOEXCEPT
Definition fltest.h:221
static TestContext & instance() FL_NOEXCEPT
void markFullyTraversed(fl::u32 hash) FL_NOEXCEPT
void setTimeoutHandler(TimeoutHandlerFunc func) FL_NOEXCEPT
Set the timeout handler callback.
Definition fltest.h:236
int registerTest(TestFunc func, const char *name, const char *file, int line) FL_NOEXCEPT
bool isFullyTraversed(fl::u32 hash) const FL_NOEXCEPT
fl::u32 mCurrentTestStartMs
Definition fltest.h:269
void setDefaultTimeoutMs(fl::u32 timeoutMs) FL_NOEXCEPT
Set default timeout for all tests (0 = no timeout)
Definition fltest.h:239
fl::size listTests(const char *filter=nullptr) const FL_NOEXCEPT
int run(int argc=0, const char *const *argv=nullptr) FL_NOEXCEPT
void exitSubcase(const SubcaseSignature &sig) FL_NOEXCEPT
const char * mCurrentTestName
Definition fltest.h:270
void setGetMillis(GetMillisFunc func) FL_NOEXCEPT
Set the function to get current time in milliseconds.
Definition fltest.h:233
void setReporter(IReporter *reporter) FL_NOEXCEPT
Definition fltest.h:220
IReporter * mReporter
Definition fltest.h:256
TestStats & stats() FL_NOEXCEPT
Definition fltest.h:224
fl::string * mOutput
Definition fltest.h:574
static fl::string escapeXml(const char *text) FL_NOEXCEPT
void setSuiteName(const char *name) FL_NOEXCEPT
Set the test suite name (used in XML output)
Definition fltest.h:571
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
XMLReporter(fl::string *outputBuffer, const char *suiteName="fltest") FL_NOEXCEPT
Create an XML reporter that writes to the given string buffer.
Definition fltest.h:559
fl::string getTypeName() FL_NOEXCEPT
Definition fltest.h:1751
const char *& currentSuiteName() FL_NOEXCEPT
Definition fltest.h:1018
bool binaryAssert(const L &lhs, const R &rhs, Cmp cmp, const char *lhsExpr, const char *op, const char *rhsExpr, const char *file, int line) FL_NOEXCEPT
Definition fltest.h:357
fl::u32(* GetMillisFunc)()
Callback type for getting current time in milliseconds Example: uint32_t getMillis() { return millis(...
Definition fltest.h:171
bool(* TimeoutHandlerFunc)(const char *testName, fl::u32 elapsedMs)
Callback type for timeout handler Called when a test times out.
Definition fltest.h:175
void skipTest(const char *reason, const char *file, int line) FL_NOEXCEPT
Record that the current test should be skipped.
fl::sstream & operator<<(fl::sstream &os, const Approx &approx) FL_NOEXCEPT
Definition fltest.h:484
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(* SerialPrintFunc)(const char *msg)
Type for the serial print function callback.
Definition fltest.h:513
constexpr int type_rank< T >::value
constexpr T * begin(T(&array)[N]) FL_NOEXCEPT
constexpr T * end(T(&array)[N]) FL_NOEXCEPT
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
int strcmp(const char *s1, const char *s2) FL_NOEXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_PRETTY_FUNCTION
#define FL_NOEXCEPT
Portable compile-time assertion wrapper.
fl::string mExpanded
Definition fltest.h:71
fl::string mExpression
Definition fltest.h:70
AssertResult(bool passed=true) FL_NOEXCEPT
Definition fltest.h:74
SourceLocation mLocation
Definition fltest.h:72
bool operator()(const L &lhs, const R &rhs) const FL_NOEXCEPT
Definition fltest.h:327
bool operator()(const L &lhs, const R &rhs) const FL_NOEXCEPT
Definition fltest.h:352
bool operator()(const L &lhs, const R &rhs) const FL_NOEXCEPT
Definition fltest.h:342
bool operator()(const L &lhs, const R &rhs) const FL_NOEXCEPT
Definition fltest.h:347
bool operator()(const L &lhs, const R &rhs) const FL_NOEXCEPT
Definition fltest.h:337
bool operator()(const L &lhs, const R &rhs) const FL_NOEXCEPT
Definition fltest.h:332
ExpressionValue(const T &value) FL_NOEXCEPT
Definition fltest.h:313
fl::string mStringified
Definition fltest.h:311
const char * str() const FL_NOEXCEPT
Definition fltest.h:320
const char * mFile
Definition fltest.h:60
SourceLocation(const char *file="", int line=0) FL_NOEXCEPT
Definition fltest.h:63
bool operator!=(const SubcaseSignature &other) const FL_NOEXCEPT
Definition fltest.h:144
bool operator==(const SubcaseSignature &other) const FL_NOEXCEPT
Definition fltest.h:138
TestRegistrar(TestContext::TestFunc func, const char *name, const char *file, int line) FL_NOEXCEPT
Definition fltest.h:302
fl::u32 mTestCasesRun
Definition fltest.h:80
void reset() FL_NOEXCEPT
Definition fltest.h:88
bool allPassed() const FL_NOEXCEPT
Definition fltest.h:98
fl::u32 mTestCasesFailed
Definition fltest.h:82
fl::u32 mTestCasesSkipped
Definition fltest.h:83
fl::u32 mTotalDurationMs
Definition fltest.h:86
fl::u32 mAssertsPassed
Definition fltest.h:84
fl::u32 mTestCasesPassed
Definition fltest.h:81
fl::u32 mAssertsFailed
Definition fltest.h:85
SuiteScope(const char *name) FL_NOEXCEPT
Definition fltest.h:1025
~SuiteScope() FL_NOEXCEPT
Definition fltest.h:1029
static void iterate(const char *baseName, const char *file, int line, int index) FL_NOEXCEPT
Definition fltest.h:1825
static void iterate(const char *, const char *, int, int) FL_NOEXCEPT
Definition fltest.h:1819
static const char * name() FL_NOEXCEPT
Definition fltest.h:1802