Added docs for QTestExtensions.h

This commit is contained in:
Seiji Emery 2015-06-23 15:27:49 -07:00
parent ede365acc6
commit 57b86c0762
4 changed files with 336 additions and 238 deletions

View file

@ -9,36 +9,51 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_QTestExtensions_hpp
#define hifi_QTestExtensions_hpp
#include <QtTest/QtTest>
#include <functional>
// Adds some additional functionality to QtTest (eg. explicitely defined fuzzy comparison
// of float and custom data types), and some extension mechanisms to provide other
// test functionality as needed.
// QFUZZY_COMPARE (actual_expr, expected_expr, epsilon / error tolerance):
// Requires that you have two functions defined:
// Implements several extensions to QtTest.
//
// V fuzzyCompare (const T & a, const T & b)
// QTextStream & operator << (const T & v)
// 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.
//
// fuzzyCompare should take a data type, T, and return the difference between two
// such values / objects in terms of a second type, V (which should match the error
// value type). For glm::vec3, T = glm::vec3, V = float, for example
// To solve this, we have:
// - QFUZZY_COMPARE (compares floats, or *any other type* using explicitely defined error thresholds.
// To use it, you need to have a fuzzyCompare 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).
//
// Generic function that reimplements the debugging output of a QCOMPARE failure via QFAIL.
// Use this to implement your own QCOMPARE-ish macros (see QEXPLICIT_FUZZY_COMPARE for
// more info).
// This version provides a callback to write additional messages.
// If the messages span more than one line, wrap them with '\n\t' to get proper indentation.
// Generates a QCOMPARE-style failure message that can be passed to QTest::qFail.
//
// Formatting looks like this:
// <qFail message> <failMessage....>
// Actual: (<stringified actual expr>) : <actual value>
// Expected: (<stringified expected expr>): <expected value>
// < additional messages (should be separated by "\n\t" for indent formatting)>
// Loc: [<file path....>(<linenum>)]
//
// 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 <typename T>
inline QString QTest_generateCompareFailureMessage (const char * failMessage, const T & actual, const T & expected, const char * actual_expr, const char * expected_expr, std::function<QTextStream & (QTextStream &)> writeAdditionalMessages)
{
inline QString QTest_generateCompareFailureMessage (
const char * failMessage,
const T & actual, const T & expected,
const char * actual_expr, const char * expected_expr,
std::function<QTextStream & (QTextStream &)> writeAdditionalMessages
) {
QString s1 = actual_expr, s2 = expected_expr;
int pad1_ = qMax(s2.length() - s1.length(), 0);
int pad2_ = qMax(s1.length() - s2.length(), 0);
@ -55,9 +70,21 @@ inline QString QTest_generateCompareFailureMessage (const char * failMessage, co
return msg;
}
// Generates a QCOMPARE-style failure message that can be passed to QTest::qFail.
//
// Formatting looks like this:
// <qFail message> <failMessage....>
// Actual: (<stringified actual expr>) : <actual value>
// Expected: (<stringified expected expr>): <expected value>
// Loc: [<file path....>(<linenum>)]
// (no message callback)
//
template <typename T>
inline QString QTest_generateCompareFailureMessage (const char * failMessage, const T & actual, const T & expected, const char * actual_expr, const char * expected_expr)
{
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);
@ -73,7 +100,8 @@ inline QString QTest_generateCompareFailureMessage (const char * failMessage, co
return msg;
}
// Why does qt have to make things so complicated...?
// Hacky function that can assemble a QString from a QTextStream via a callback
// (ie. stream operations w/out qDebug())
inline QString makeMessageFromStream (std::function<void(QTextStream &)> writeMessage) {
QString msg;
QTextStream stream(&msg);
@ -81,83 +109,137 @@ inline QString makeMessageFromStream (std::function<void(QTextStream &)> writeMe
return msg;
}
inline void QTest_failWithCustomMessage (std::function<void(QTextStream &stream)> writeMessage, int line, const char *file)
{
inline void QTest_failWithCustomMessage (
std::function<void(QTextStream &stream)> 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)
inline void foo () {
int thing = 2;
QFAIL_WITH_MESSAGE("Message " << thing << ";");
}
// Generates a QCOMPARE style failure message with custom arguments.
// This is expected to be wrapped in a macro (see QFUZZY_COMPARE), and it must
// actually return on failure (unless other functionality is desired).
// 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 <typename T>
inline void QTest_failWithMessage(const char * failMessage, const T & actual, const T & expected, const char * actualExpr, const char * expectedExpr, int line, const char * file)
{
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);
}
// Generates a QCOMPARE style failure message with custom arguments.
// Writing additional lines (eg:)
// Actual (<expr>): <actual value>
// Expected (<expr>): <expected value>
// <Your additional messages here...>
// Loc: [<filepath...>(<line num>)]
// is provided via a lamdbda / closure that can write to the textstream.
// Be aware that newlines are actually "\n\t" (with this impl), so use that to get
// proper indenting (and add extra '\t's to get additional indentation).
// 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 <typename T>
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<QTextStream &(QTextStream&)> writeAdditionalMessageLines) {
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<QTextStream &(QTextStream&)> writeAdditionalMessageLines
) {
QTest::qFail(qPrintable(QTest_generateCompareFailureMessage(failMessage, actual, expected, actualExpr, expectedExpr, writeAdditionalMessageLines)), file, line);
}
// Implements QFUZZY_COMPARE
template <typename T, typename V>
inline auto QTest_fuzzyCompare(const T & actual, const T & expected, const char * actual_expr, const char * expected_expr, int line, const char * file, const V & epsilon) -> decltype(fuzzyCompare(actual, expected))
{
if (fuzzyCompare(actual, expected) > epsilon) {
QTest::qFail(qPrintable(QTest_generateCompareFailureMessage(
QTest_failWithMessage(
"Compared values are not the same (fuzzy compare)",
actual, expected, actual_expr, expected_expr,
actual, expected, actual_expr, expected_expr, line, file,
[&] (QTextStream & stream) -> QTextStream & {
return stream << "Err tolerance: " << fuzzyCompare((actual), (expected)) << " > " << epsilon;
})), file, line);
});
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:
// <T, V> V fuzzyCompare (const T& a, const T& b) (should return the absolute, max difference between a and b)
// <T> QTextStream & operator << (QTextStream& stream, const T& value)
//
// Here's an implementation for glm::vec3:
// inline float fuzzyCompare (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 QFUZZY_COMPARE(actual, expected, epsilon) \
do { \
if (!QTest_fuzzyCompare(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)
} 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)
} 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(actual, expected, testClosure, failMessage) \
do { \
if (!testClosure()) \
QTest_failWithMessage(failMessage, actual, expected, #actual, #expected, __LINE__, __FILE__); \
return; \
} \
} while (0)
#endif

View file

@ -21,7 +21,7 @@ private slots:
void testConeRollerConstraint();
};
// Enable QFUZZY_COMPARE for glm::quat
// Use QFUZZY_COMPARE and define it for glm::quat
#include <glm/glm.hpp>
float fuzzyCompare (const glm::quat & a, const glm::quat & b);
QTextStream & operator << (QTextStream & stream, const glm::quat & q);

View file

@ -24,199 +24,203 @@ quint64 MovingMinMaxAvgTests::randQuint64() {
return ret;
}
void MovingMinMaxAvgTests::runAllTests() {
{
// quint64 test
void MovingMinMaxAvgTests::testQuint64() {
// quint64 test
const int INTERVAL_LENGTH = 100;
const int WINDOW_INTERVALS = 50;
const int INTERVAL_LENGTH = 100;
const int WINDOW_INTERVALS = 50;
MovingMinMaxAvg<quint64> stats(INTERVAL_LENGTH, WINDOW_INTERVALS);
MovingMinMaxAvg<quint64> stats(INTERVAL_LENGTH, WINDOW_INTERVALS);
quint64 min = std::numeric_limits<quint64>::max();
quint64 max = 0;
double average = 0.0;
int totalSamples = 0;
quint64 min = std::numeric_limits<quint64>::max();
quint64 max = 0;
double average = 0.0;
int totalSamples = 0;
quint64 windowMin;
quint64 windowMax;
double windowAverage;
quint64 windowMin;
quint64 windowMax;
double windowAverage;
QQueue<quint64> windowSamples;
// fill window samples
for (int i = 0; i < 100000; i++) {
QQueue<quint64> windowSamples;
// fill window samples
for (int i = 0; i < 100000; i++) {
quint64 sample = randQuint64();
quint64 sample = randQuint64();
windowSamples.enqueue(sample);
if (windowSamples.size() > INTERVAL_LENGTH * WINDOW_INTERVALS) {
windowSamples.dequeue();
windowSamples.enqueue(sample);
if (windowSamples.size() > INTERVAL_LENGTH * WINDOW_INTERVALS) {
windowSamples.dequeue();
}
stats.update(sample);
min = std::min(min, sample);
max = std::max(max, sample);
average = (average * totalSamples + sample) / (totalSamples + 1);
totalSamples++;
QCOMPARE(stats.getMin(), min);
QCOMPARE(stats.getMax(), max);
QFUZZY_COMPARE((float) stats.getAverage() / (float) average, 1.0f, EPSILON);
QFUZZY_COMPARE((float) stats.getAverage(), (float) average, EPSILON);
// QCOMPARE(fabsf(
// (float)stats.getAverage() / (float)average - 1.0f
// ) < EPSILON ||
// fabsf(
// (float)stats.getAverage() - (float)average) < EPSILON);
if ((i + 1) % INTERVAL_LENGTH == 0) {
assert(stats.getNewStatsAvailableFlag());
stats.clearNewStatsAvailableFlag();
windowMin = std::numeric_limits<quint64>::max();
windowMax = 0;
windowAverage = 0.0;
foreach(quint64 s, windowSamples) {
windowMin = std::min(windowMin, s);
windowMax = std::max(windowMax, s);
windowAverage += (double)s;
}
windowAverage /= (double)windowSamples.size();
stats.update(sample);
min = std::min(min, sample);
max = std::max(max, sample);
average = (average * totalSamples + sample) / (totalSamples + 1);
totalSamples++;
assert(stats.getMin() == min);
assert(stats.getMax() == max);
assert(stats.getWindowMin() == windowMin);
assert(stats.getWindowMax() == windowMax);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON ||
fabsf((float)stats.getAverage() - (float)average) < EPSILON);
if ((i + 1) % INTERVAL_LENGTH == 0) {
assert(stats.getNewStatsAvailableFlag());
stats.clearNewStatsAvailableFlag();
windowMin = std::numeric_limits<quint64>::max();
windowMax = 0;
windowAverage = 0.0;
foreach(quint64 s, windowSamples) {
windowMin = std::min(windowMin, s);
windowMax = std::max(windowMax, s);
windowAverage += (double)s;
}
windowAverage /= (double)windowSamples.size();
assert(stats.getWindowMin() == windowMin);
assert(stats.getWindowMax() == windowMax);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON ||
fabsf((float)stats.getAverage() - (float)average) < EPSILON);
} else {
assert(!stats.getNewStatsAvailableFlag());
}
} else {
assert(!stats.getNewStatsAvailableFlag());
}
}
}
void MovingMinMaxAvgTests::testInt() {
// int test
const int INTERVAL_LENGTH = 1;
const int WINDOW_INTERVALS = 75;
MovingMinMaxAvg<int> stats(INTERVAL_LENGTH, WINDOW_INTERVALS);
int min = std::numeric_limits<int>::max();
int max = 0;
double average = 0.0;
int totalSamples = 0;
int windowMin;
int windowMax;
double windowAverage;
QQueue<int> windowSamples;
// fill window samples
for (int i = 0; i < 100000; i++) {
int sample = rand();
windowSamples.enqueue(sample);
if (windowSamples.size() > INTERVAL_LENGTH * WINDOW_INTERVALS) {
windowSamples.dequeue();
}
stats.update(sample);
min = std::min(min, sample);
max = std::max(max, sample);
average = (average * totalSamples + sample) / (totalSamples + 1);
totalSamples++;
assert(stats.getMin() == min);
assert(stats.getMax() == max);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON);
if ((i + 1) % INTERVAL_LENGTH == 0) {
assert(stats.getNewStatsAvailableFlag());
stats.clearNewStatsAvailableFlag();
windowMin = std::numeric_limits<int>::max();
windowMax = 0;
windowAverage = 0.0;
foreach(int s, windowSamples) {
windowMin = std::min(windowMin, s);
windowMax = std::max(windowMax, s);
windowAverage += (double)s;
}
windowAverage /= (double)windowSamples.size();
assert(stats.getWindowMin() == windowMin);
assert(stats.getWindowMax() == windowMax);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON);
} else {
assert(!stats.getNewStatsAvailableFlag());
}
}
}
void MovingMinMaxAvgTests::testFloat() {
// float test
const int INTERVAL_LENGTH = 57;
const int WINDOW_INTERVALS = 1;
MovingMinMaxAvg<float> stats(INTERVAL_LENGTH, WINDOW_INTERVALS);
float min = std::numeric_limits<float>::max();
float max = 0;
double average = 0.0;
int totalSamples = 0;
float windowMin;
float windowMax;
double windowAverage;
QQueue<float> windowSamples;
// fill window samples
for (int i = 0; i < 100000; i++) {
float sample = randFloat();
windowSamples.enqueue(sample);
if (windowSamples.size() > INTERVAL_LENGTH * WINDOW_INTERVALS) {
windowSamples.dequeue();
}
stats.update(sample);
min = std::min(min, sample);
max = std::max(max, sample);
average = (average * totalSamples + (double)sample) / (totalSamples + 1);
totalSamples++;
assert(stats.getMin() == min);
assert(stats.getMax() == max);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON);
if ((i + 1) % INTERVAL_LENGTH == 0) {
assert(stats.getNewStatsAvailableFlag());
stats.clearNewStatsAvailableFlag();
windowMin = std::numeric_limits<float>::max();
windowMax = 0;
windowAverage = 0.0;
foreach(float s, windowSamples) {
windowMin = std::min(windowMin, s);
windowMax = std::max(windowMax, s);
windowAverage += (double)s;
}
windowAverage /= (double)windowSamples.size();
assert(stats.getWindowMin() == windowMin);
assert(stats.getWindowMax() == windowMax);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON);
} else {
assert(!stats.getNewStatsAvailableFlag());
}
}
{
// int test
const int INTERVAL_LENGTH = 1;
const int WINDOW_INTERVALS = 75;
MovingMinMaxAvg<int> stats(INTERVAL_LENGTH, WINDOW_INTERVALS);
int min = std::numeric_limits<int>::max();
int max = 0;
double average = 0.0;
int totalSamples = 0;
int windowMin;
int windowMax;
double windowAverage;
QQueue<int> windowSamples;
// fill window samples
for (int i = 0; i < 100000; i++) {
int sample = rand();
windowSamples.enqueue(sample);
if (windowSamples.size() > INTERVAL_LENGTH * WINDOW_INTERVALS) {
windowSamples.dequeue();
}
stats.update(sample);
min = std::min(min, sample);
max = std::max(max, sample);
average = (average * totalSamples + sample) / (totalSamples + 1);
totalSamples++;
assert(stats.getMin() == min);
assert(stats.getMax() == max);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON);
if ((i + 1) % INTERVAL_LENGTH == 0) {
assert(stats.getNewStatsAvailableFlag());
stats.clearNewStatsAvailableFlag();
windowMin = std::numeric_limits<int>::max();
windowMax = 0;
windowAverage = 0.0;
foreach(int s, windowSamples) {
windowMin = std::min(windowMin, s);
windowMax = std::max(windowMax, s);
windowAverage += (double)s;
}
windowAverage /= (double)windowSamples.size();
assert(stats.getWindowMin() == windowMin);
assert(stats.getWindowMax() == windowMax);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON);
} else {
assert(!stats.getNewStatsAvailableFlag());
}
}
}
{
// float test
const int INTERVAL_LENGTH = 57;
const int WINDOW_INTERVALS = 1;
MovingMinMaxAvg<float> stats(INTERVAL_LENGTH, WINDOW_INTERVALS);
float min = std::numeric_limits<float>::max();
float max = 0;
double average = 0.0;
int totalSamples = 0;
float windowMin;
float windowMax;
double windowAverage;
QQueue<float> windowSamples;
// fill window samples
for (int i = 0; i < 100000; i++) {
float sample = randFloat();
windowSamples.enqueue(sample);
if (windowSamples.size() > INTERVAL_LENGTH * WINDOW_INTERVALS) {
windowSamples.dequeue();
}
stats.update(sample);
min = std::min(min, sample);
max = std::max(max, sample);
average = (average * totalSamples + (double)sample) / (totalSamples + 1);
totalSamples++;
assert(stats.getMin() == min);
assert(stats.getMax() == max);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON);
if ((i + 1) % INTERVAL_LENGTH == 0) {
assert(stats.getNewStatsAvailableFlag());
stats.clearNewStatsAvailableFlag();
windowMin = std::numeric_limits<float>::max();
windowMax = 0;
windowAverage = 0.0;
foreach(float s, windowSamples) {
windowMin = std::min(windowMin, s);
windowMax = std::max(windowMax, s);
windowAverage += (double)s;
}
windowAverage /= (double)windowSamples.size();
assert(stats.getWindowMin() == windowMin);
assert(stats.getWindowMax() == windowMax);
assert(fabsf((float)stats.getAverage() / (float)average - 1.0f) < EPSILON);
} else {
assert(!stats.getNewStatsAvailableFlag());
}
}
}
printf("moving min/max/avg test passed!\n");
}

View file

@ -12,14 +12,26 @@
#ifndef hifi_MovingMinMaxAvgTests_h
#define hifi_MovingMinMaxAvgTests_h
#include <QtTest/QtTest>
inline float fuzzyCompare (float a, float b) {
return fabsf(a - b);
}
#include "../QTestExtensions.hpp"
#include "MovingMinMaxAvg.h"
#include "SharedUtil.h"
namespace MovingMinMaxAvgTests {
class MovingMinMaxAvgTests : public QObject {
private slots:
void testQuint64 ();
void testInt ();
void testFloat ();
private:
quint64 randQuint64();
void runAllTests();
}
};
#endif // hifi_MovingMinMaxAvgTests_h