// // QTestExtensions.h // tests/ // // Created by Seiji Emery on 6/20/15. // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #ifndef hifi_QTestExtensions_hpp #define hifi_QTestExtensions_hpp #include #include #include "GLMTestUtils.h" // Implements several extensions to QtTest. // // Problems with QtTest: // - QCOMPARE can compare float values (using a fuzzy compare), but uses an internal threshold // that cannot be set explicitely (and we need explicit, adjustable error thresholds for our physics // and math test code). // - QFAIL takes a const char * failure message, and writing custom messages to it is complicated. // // To solve this, we have: // - QCOMPARE_WITH_ABS_ERROR (compares floats, or *any other type* using explicitely defined error thresholds. // To use it, you need to have a compareWithAbsError function ((T, T) -> V), and operator << for QTextStream). // - QFAIL_WITH_MESSAGE("some " << streamed << " message"), which builds, writes to, and stringifies // a QTextStream using black magic. // - QCOMPARE_WITH_LAMBDA / QCOMPARE_WITH_FUNCTION, which implements QCOMPARE, but with a user-defined // test function ((T, T) -> bool). // - A simple framework to write additional custom test macros as needed (QCOMPARE is reimplemented // from scratch using QTest::qFail, for example). // float getErrorDifference(const float& a, const float& b) { return fabsf(a - b); } // Generates a QCOMPARE-style failure message that can be passed to QTest::qFail. // // Formatting looks like this: // // Actual: () : // Expected: (): // < additional messages (should be separated by "\n\t" for indent formatting)> // Loc: [()] // // Additional messages (after actual/expected) can be written using the std::function callback. // If these messages span more than one line, wrap them with "\n\t" to get proper indentation / formatting) // template inline QString QTest_generateCompareFailureMessage ( const char* failMessage, const T& actual, const T& expected, const char* actual_expr, const char* expected_expr, std::function writeAdditionalMessages ) { QString s1 = actual_expr, s2 = expected_expr; int pad1_ = qMax(s2.length() - s1.length(), 0); int pad2_ = qMax(s1.length() - s2.length(), 0); QString pad1 = QString(")").rightJustified(pad1_, ' '); QString pad2 = QString(")").rightJustified(pad2_, ' '); QString msg; QTextStream stream (&msg); stream << failMessage << "\n\t" "Actual: (" << actual_expr << pad1 << ": " << actual << "\n\t" "Expected: (" << expected_expr << pad2 << ": " << expected << "\n\t"; writeAdditionalMessages(stream); return msg; } // Generates a QCOMPARE-style failure message that can be passed to QTest::qFail. // // Formatting looks like this: // // Actual: () : // Expected: (): // Loc: [()] // (no message callback) // template inline QString QTest_generateCompareFailureMessage ( const char* failMessage, const T& actual, const T& expected, const char* actual_expr, const char* expected_expr ) { QString s1 = actual_expr, s2 = expected_expr; int pad1_ = qMax(s2.length() - s1.length(), 0); int pad2_ = qMax(s1.length() - s2.length(), 0); QString pad1 = QString("): ").rightJustified(pad1_, ' '); QString pad2 = QString("): ").rightJustified(pad2_, ' '); QString msg; QTextStream stream (&msg); stream << failMessage << "\n\t" "Actual: (" << actual_expr << pad1 << actual << "\n\t" "Expected: (" << expected_expr << pad2 << expected; return msg; } // Hacky function that can assemble a QString from a QTextStream via a callback // (ie. stream operations w/out qDebug()) inline QString makeMessageFromStream (std::function writeMessage) { QString msg; QTextStream stream(&msg); writeMessage(stream); return msg; } inline void QTest_failWithCustomMessage ( std::function writeMessage, int line, const char* file ) { QTest::qFail(qPrintable(makeMessageFromStream(writeMessage)), file, line); } // Equivalent to QFAIL, but takes a message that can be formatted using stream operators. // Writes to a QTextStream internally, and calls QTest::qFail (the internal impl of QFAIL, // with the current file and line number) // // example: // inline void foo () { // int thing = 2; // QFAIL_WITH_MESSAGE("Message " << thing << ";"); // } // #define QFAIL_WITH_MESSAGE(...) \ do { \ QTest_failWithCustomMessage([&](QTextStream& stream) { stream << __VA_ARGS__; }, __LINE__, __FILE__); \ return; \ } while(0) // Calls qFail using QTest_generateCompareFailureMessage. // This is (usually) wrapped in macros, but if you call this directly you should return immediately to get QFAIL semantics. template inline void QTest_failWithMessage( const char* failMessage, const T& actual, const T& expected, const char* actualExpr, const char* expectedExpr, int line, const char* file ) { QTest::qFail(qPrintable(QTest_generateCompareFailureMessage( failMessage, actual, expected, actualExpr, expectedExpr)), file, line); } // Calls qFail using QTest_generateCompareFailureMessage. // This is (usually) wrapped in macros, but if you call this directly you should return immediately to get QFAIL semantics. template inline void QTest_failWithMessage( const char* failMessage, const T& actual, const T& expected, const char* actualExpr, const char* expectedExpr, int line, const char* file, std::function writeAdditionalMessageLines ) { QTest::qFail(qPrintable(QTest_generateCompareFailureMessage( failMessage, actual, expected, actualExpr, expectedExpr, writeAdditionalMessageLines)), file, line); } // Implements QCOMPARE_WITH_ABS_ERROR template inline bool QTest_compareWithAbsError( const T& actual, const T& expected, const char* actual_expr, const char* expected_expr, int line, const char* file, const V& epsilon ) { if (fabsf(getErrorDifference(actual, expected)) > fabsf(epsilon)) { QTest_failWithMessage( "Compared values are not the same (fuzzy compare)", actual, expected, actual_expr, expected_expr, line, file, [&] (QTextStream& stream) -> QTextStream& { return stream << "Err tolerance: " << getErrorDifference((actual), (expected)) << " > " << epsilon; }); return false; } return true; } // Implements a fuzzy QCOMPARE using an explicit epsilon error value. // If you use this, you must have the following functions defined for the types you're using: // V compareWithAbsError (const T& a, const T& b) (should return the absolute, max difference between a and b) // QTextStream & operator << (QTextStream& stream, const T& value) // // Here's an implementation for glm::vec3: // inline float compareWithAbsError (const glm::vec3 & a, const glm::vec3 & b) { // returns // return glm::distance(a, b); // } // inline QTextStream & operator << (QTextStream & stream, const T & v) { // return stream << "glm::vec3 { " << v.x << ", " << v.y << ", " << v.z << " }" // } // #define QCOMPARE_WITH_ABS_ERROR(actual, expected, epsilon) \ do { \ if (!QTest_compareWithAbsError((actual), (expected), #actual, #expected, __LINE__, __FILE__, epsilon)) \ return; \ } while(0) // Implements QCOMPARE using an explicit, externally defined test function. // The advantage of this (over a manual check or what have you) is that the values of actual and // expected are printed in the event that the test fails. // // testFunc(const T & actual, const T & expected) -> bool: true (test succeeds) | false (test fails) // #define QCOMPARE_WITH_FUNCTION(actual, expected, testFunc) \ do { \ if (!(testFunc((actual), (expected)))) { \ QTest_failWithMessage("Compared values are not the same", (actual), (expected), #actual, #expected, __LINE__, __FILE__); \ return; \ } \ } while (0) // Implements QCOMPARE using an explicit, externally defined test function. // Unlike QCOMPARE_WITH_FUNCTION, this func / closure takes no arguments (which is much more convenient // if you're using a c++11 closure / lambda). // // usage: // QCOMPARE_WITH_LAMBDA(foo, expectedFoo, [&foo, &expectedFoo] () { // return foo->isFooish() && foo->fooishness() >= expectedFoo->fooishness(); // }); // (fails if foo is not as fooish as expectedFoo) // #define QCOMPARE_WITH_LAMBDA(actual, expected, testClosure) \ do { \ if (!(testClosure())) { \ QTest_failWithMessage("Compared values are not the same", (actual), (expected), #actual, #expected, __LINE__, __FILE__); \ return; \ } \ } while (0) // Same as QCOMPARE_WITH_FUNCTION, but with a custom fail message #define QCOMPARE_WITH_FUNCTION_AND_MESSAGE(actual, expected, testfunc, failMessage) \ do { \ if (!(testFunc((actual), (expected)))) { \ QTest_failWithMessage((failMessage), (actual), (expected), #actual, #expected, __LINE__, __FILE__); \ return; \ } \ } while (0) // Same as QCOMPARE_WITH_FUNCTION, but with a custom fail message #define QCOMPARE_WITH_LAMBDA_AND_MESSAGE(actual, expected, testClosure, failMessage) \ do { \ if (!(testClosure())) { \ QTest_failWithMessage((failMessage), (actual), (expected), #actual, #expected, __LINE__, __FILE__); \ return; \ } \ } while (0) #endif #define QCOMPARE_WITH_EXPR(actual, expected, testExpr) \ do { \ if (!(testExpr)) { \ QTest_failWithMessage("Compared values are not the same", (actual), (expected), #actual, #expected, __LINE__, __FILE__); \ } \ } while(0) struct ByteData { ByteData (const char* data, size_t length) : data(data), length(length) {} const char* data; size_t length; }; QTextStream & operator << (QTextStream& stream, const ByteData & wrapper) { // Print bytes as hex stream << QByteArray::fromRawData(wrapper.data, wrapper.length).toHex(); return stream; } bool compareData (const char* data, const char* expectedData, size_t length) { return memcmp(data, expectedData, length) == 0; } #define COMPARE_DATA(actual, expected, length) \ QCOMPARE_WITH_EXPR((ByteData ( actual, length )), (ByteData ( expected, length )), compareData(actual, expected, length)) // Produces a relative error test for float usable QCOMPARE_WITH_LAMBDA. inline auto errorTest (float actual, float expected, float acceptableRelativeError) -> std::function { return [actual, expected, acceptableRelativeError] () { if (fabsf(expected) <= acceptableRelativeError) { return fabsf(actual - expected) < fabsf(acceptableRelativeError); } return fabsf((actual - expected) / expected) < fabsf(acceptableRelativeError); }; } #define QCOMPARE_WITH_RELATIVE_ERROR(actual, expected, relativeError) \ QCOMPARE_WITH_LAMBDA(actual, expected, errorTest(actual, expected, relativeError))