Last active
March 10, 2018 07:12
-
-
Save eth-p/bffaa1f5dd940ba5f8f34ec98b45e146 to your computer and use it in GitHub Desktop.
A (debatably) small C++ testing library.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "testy.h" | |
using namespace std; | |
using testy::Expect; | |
int main() { | |
// -------------------------------------------- | |
// Basic Usage | |
// How to run a standalone test. | |
// -------------------------------------------- | |
// | |
// testy::immediate(std::string name, std::function<void(testy::Expect)> lambda); | |
// --> Runs a test function. | |
// | |
// name : The test name. | |
// lambda : The test function. | |
// | |
testy::immediate("Running Tests", [](Expect expect) { | |
// Test code goes here. | |
}); | |
// -------------------------------------------- | |
// Creating Expectations | |
// How to actually test things. | |
// -------------------------------------------- | |
// | |
// Expect<T>(T value) | |
// --> Creates an expectation for a value type. | |
// | |
// <T> : The expectation type. This can be inferred by the compiler. | |
// value : The unknown value to assert. | |
// | |
testy::immediate("Expectations", [](Expect expect) { | |
int unknown = /* do something that returns an int */; | |
expect(unknown); // Does nothing yet. | |
}); | |
// | |
// Expect.function(std::function<void()> function) | |
// --> Creates an expectation for a function type. | |
// | |
// function : The function to assert. | |
// | |
testy::immediate("Your Expectations Are Running", [](Expect expect) { | |
int unknown = /* do something that returns an int */; | |
expect.function([] { | |
// Does nothing until asserted. | |
}); | |
}); | |
// -------------------------------------------- | |
// Expectations | |
// The syntactic sugar of assertions. | |
// -------------------------------------------- | |
// | |
// ValueExpectation<T>.equals(T value) | |
// --> Assert two values using the == operator. | |
// | |
// value : The expected value. | |
// | |
// NOTE -- The ==(T, T) operator must be implemented. | |
// | |
testy::immediate("Equality", [](Expect expect) { | |
int three = 3; | |
int five = 5; | |
expect(three).equals(five); // Test failed. | |
}); | |
// | |
// Expectation<T>.never().[...] | |
// --> Negate an assertion. | |
// | |
// WARNING -- This is reset after each assertion. | |
// | |
testy::immediate("Negation", [](Expect expect) { | |
int three = 3; | |
int five = 5; | |
expect(three).never().equals(five); // Passed. | |
}); | |
// | |
// Expectation<T>.that(std::string description).[...] | |
// --> Describe an assertion. | |
// | |
// description : The description. | |
// | |
testy::immediate("Descriptions", [](Expect expect) { | |
expect(2*2).that("Two times two is four.").equals(3); | |
}); | |
// | |
// FunctionExpectation.couts(std::string value) | |
// --> Assert that the function prints a specific string to std::cout. | |
// | |
// value : The expected string. | |
// | |
testy::immediate("Couting", [](Expect expect) { | |
expect.function([] { | |
cout << "I'm sad\n"; | |
}).couts("I'm happy\n"); // Test failed. | |
}); | |
// | |
// FunctionExpectation.cerrs(std::string value) | |
// --> Assert that the function prints a specific string to std::cerr. | |
// | |
// value : The expected string. | |
// | |
testy::immediate("Couting, With Cerr", [](Expect expect) { | |
expect.function([] { | |
cerr << "This went wrong.\n"; | |
}).cerrs(""); // Test failed. | |
}); | |
// | |
// FunctionExpectation.throws<T>() | |
// --> Assert that the function throws a specific exception. | |
// | |
// <t> : The expected exception type. | |
// value : The expected string. | |
// | |
testy::immediate("Except", [](Expect expect) { | |
expect.function([] { | |
throw runtime_error("Javelin"); | |
}).throws<runtime_error>(); // Test passed. | |
}); | |
// -------------------------------------------- | |
// Logging | |
// Where did it go wrong? | |
// -------------------------------------------- | |
// | |
// testy will tell you how things went wrong. | |
// It's better than leaving a bunch of | |
// std::cout << statements everywhere. | |
// | |
testy::immediate("Oops", [](Expect expect) { | |
expect(true).that("Contradiction").equals(!true); | |
// Oops: .................................... [failed] | |
// ! Failure: Contradiction | |
// ! Expected: 1 | |
// ! Received: 0 | |
}); | |
// | |
// testy will automatically record everything | |
// that went into std::cout and std::cerr. If | |
// the test doesn't fail, you won't be bothered. | |
// | |
testy::immediate("Logging", [](Expect expect) { | |
cout << "Good." << endl; | |
cerr << "Bad." << endl; | |
expect(true).that("Tautology").equals(true); | |
// Logging: ................................. [passed] | |
}); | |
// | |
// Sometimes, you have a bunch of things to test, | |
// but you don't want 50 different tests. Thanks to | |
// Expectation.suppress, you can test everything in one. | |
// | |
testy::immediate("Suppression", [](Expect expect) { | |
expect.suppress = true; | |
cout << "Hello from the beginning." << endl; | |
expect(true).that("Tautology").equals(true); | |
expect(true).that("Contradiction").equals(false); | |
expect(static_cast<string>("typo")).that("Spelling").equals("ytpo"); // C++ likes to default to const char* ... | |
cerr << "Hello from the end." << endl; | |
// Suppression: ............................. [failed] | |
// ! Failure: Contradiction | |
// ! Expected: 1 | |
// ! Received: 0 | |
// ! Failure: Spelling | |
// ! Expected: typo | |
// ! Received: ytpo | |
// | Hello from the beginning. | |
// | Hello from the end. | |
}); | |
return 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
// --------------------------------------------------------------------------------------------------------------------- | |
// testy.h | A simple C++ testing library. | Copyright (C) 2018 Ethan Pini <epini@sfu.ca> | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Includes: | |
#include <iostream> | |
#include <iomanip> | |
#include <string> | |
#include <exception> | |
#include <functional> | |
#include <memory> | |
#include <sstream> | |
#include <vector> | |
#include <type_traits> | |
#include <typeinfo> | |
#include <cstdint> | |
// --------------------------------------------------------------------------------------------------------------------- | |
namespace testy { | |
// --------------------------------------------------------------------------------------------------------------------- | |
// META: | |
/** | |
* SFINAE things... | |
*/ | |
namespace META { | |
/** | |
* std::ostream& << T | |
*/ | |
template<typename T> | |
class streamable { | |
template<typename C> | |
static auto check(int) -> decltype(std::declval<std::ostream &>() << std::declval<C>(), std::true_type()); | |
template<typename> | |
static auto check(...) -> std::false_type; | |
public: | |
static const bool value = decltype(check<T>(0)) | |
::value; | |
}; | |
/** | |
* T == T | |
*/ | |
template<typename T> | |
class comparable { | |
template<typename C> | |
static auto check(int) -> decltype(std::declval<C>() == std::declval<C>(), std::true_type()); | |
template<typename> | |
static auto check(...) -> std::false_type; | |
public: | |
static const bool value = decltype(check<T>(0)) | |
::value; | |
}; | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Util: | |
namespace Util { | |
/** | |
* Convert a non-streamable type to a string. | |
* This uses a stringstream to convert it. | |
* @tparam T The type. | |
* @return The string representation of the type. | |
*/ | |
template<typename T> | |
std::string to_string(typename std::enable_if<!META::streamable<T>::value, T>::type); | |
/** | |
* Convert a non-streamable type to a string. | |
* This just returns the type name. | |
* @tparam T The type. | |
* @return The string representation of the type. | |
*/ | |
template<typename T> | |
std::string to_string(typename std::enable_if<META::streamable<T>::value, T>::type); | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Color: | |
/** | |
* Formatting codes for colored text output. | |
*/ | |
class Color { | |
protected: | |
const std::string ansi; | |
public: | |
/** | |
* Creates a new Color object. | |
* @param ansi The ANSI escape sequence for this color. | |
*/ | |
Color(const std::string &ansi); | |
/** | |
* Gets the color as an ANSI escape sequence. | |
* @return The ANSI escape sequence. | |
*/ | |
const std::string &asANSI() const; | |
/** | |
* Casts to a string. | |
* @return The ANSI escape sequence. | |
*/ | |
operator std::string() const; | |
// Constants: | |
static const Color RED; | |
static const Color GREEN; | |
static const Color YELLOW; | |
static const Color PURPLE; | |
static const Color PINK; | |
static const Color BLUE; | |
static const Color RESET; | |
}; | |
const Color Color::RED = Color("\x1B[31m"); | |
const Color Color::GREEN = Color("\x1B[32m"); | |
const Color Color::YELLOW = Color("\x1B[33m"); | |
const Color Color::PURPLE = Color("\x1B[34m"); | |
const Color Color::PINK = Color("\x1B[35m"); | |
const Color Color::BLUE = Color("\x1B[36m"); | |
const Color Color::RESET = Color("\x1B[39m"); | |
// --------------------------------------------------------------------------------------------------------------------- | |
// FailureReason: | |
/** | |
* An object containing information regarding why a test failed. | |
*/ | |
class FailureReason { | |
public: | |
virtual ~FailureReason() = default; | |
/** | |
* Gets a unique identifier representing to object kind. | |
* This is used to identify the class before casting. | |
* @return A unique identifier. | |
*/ | |
virtual std::string kind() const = 0; | |
/** | |
* Converts the failure reason to a string. | |
* @param formatted Whether or not to use console-friendly formatting. | |
* @return A string representing the failure reason. | |
*/ | |
virtual std::string toString(bool formatted = true) const = 0; | |
/** | |
* Casts to a string. | |
* @return The string. | |
*/ | |
operator std::string() const; | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Failure: | |
/** | |
* A runtime error for signaling the failure of a test. | |
*/ | |
class Failure : public std::runtime_error { | |
protected: | |
const std::shared_ptr <FailureReason> failureReason; | |
public: | |
/** | |
* Creates a new test failure error. | |
* @param what The failure message. | |
* @param reasons The failure reason. | |
*/ | |
Failure(const std::string &what, const std::shared_ptr <FailureReason> reasons); | |
virtual ~Failure() = default; | |
/** | |
* Gets the reason why the test failed. | |
* @return The failure reason. | |
*/ | |
const std::shared_ptr <FailureReason> reason() const; | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// FailureListener: | |
/** | |
* A class which listens for failures. | |
*/ | |
class FailureListener { | |
public: | |
virtual ~FailureListener() = default; | |
/** | |
* Callback. This is called when a test failure occurs. | |
* @param failure The failure. | |
*/ | |
virtual void fail(const Failure &failure) = 0; | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// UncaughtExceptionReason: | |
/** | |
* A failure reason that represents an uncaught exception. | |
*/ | |
class UncaughtExceptionReason : public FailureReason { | |
protected: | |
const std::string whatValue; | |
public: | |
/** | |
* Creates a new UncaughtExceptionReason. | |
* @param what The exception what() string. | |
*/ | |
UncaughtExceptionReason(const std::string &what); | |
~UncaughtExceptionReason() = default; | |
/** | |
* Gets the exception what() string. | |
* @return The what string. | |
*/ | |
const std::string &what() const; | |
/** | |
* @inheritDoc | |
*/ | |
std::string kind() const override; | |
/** | |
* @inheritDoc | |
*/ | |
std::string toString(bool formatted = true) const override; | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// ExpectationReason: | |
/** | |
* A failure reason that represents a failed expectation. | |
*/ | |
class ExpectationReason : public FailureReason { | |
protected: | |
const std::string valueExpected; | |
const std::string valueReceived; | |
const bool conditionNegated; | |
public: | |
/** | |
* Creates a new ExpectationReason. | |
* @param expected The expected value. | |
* @param received The received value. | |
* @param negated Whether the expectation was negated or not. | |
*/ | |
ExpectationReason(std::string expected, std::string received, bool negated); | |
~ExpectationReason() = default; | |
/** | |
* Gets the expected value. | |
* @return The expected value as a string. | |
*/ | |
const std::string &expected() const; | |
/** | |
* Gets the received value. | |
* @return The received value as a string. | |
*/ | |
const std::string &received() const; | |
/** | |
* @inheritDoc | |
*/ | |
std::string kind() const override; | |
/** | |
* @inheritDoc | |
*/ | |
std::string toString(bool formatted = true) const override; | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Expectation: | |
/** | |
* An abstract class for asserting expectations. | |
*/ | |
template<class S> | |
class Expectation { | |
protected: | |
bool negate; | |
std::string name; | |
FailureListener* listener; | |
/** | |
* Cleans up leftovers from the previous asserable expectation. | |
* Currently, this just resets the negate flag and name. | |
*/ | |
virtual void _reset(); | |
/** | |
* Sends a failure to the listener, or throws if it's null. | |
* @param failure The failure. | |
*/ | |
virtual void _throw(const Failure &failure); | |
/** | |
* Expect a specific string is sent to a stream. | |
* @param func The function to run. | |
* @param expected The expected value sent to cout. | |
*/ | |
virtual void _streamEquals(std::ostream*, std::function<void()> func, std::string expected); | |
public: | |
/** | |
* Creates a new abstract expectation. | |
* @param listener The failure listener. | |
*/ | |
Expectation(FailureListener* listener = nullptr); | |
virtual ~Expectation() = default; | |
/** | |
* Sets the expectation name. | |
* @param name The expectation name. | |
* @return The expectation (for chaining). | |
*/ | |
S &that(std::string name); | |
/** | |
* Negate the expectation. | |
* This negation is reset after every call. | |
* @return The expectation (for chaining). | |
*/ | |
S &never(); | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// ValueExpectation: | |
/** | |
* A class for asserting expectations on values. | |
* @tparam T The type of value. | |
*/ | |
template<typename T> | |
class ValueExpectation : public Expectation<ValueExpectation<T>> { | |
protected: | |
const T value; | |
public: | |
/** | |
* Creates a new expectation for a value. | |
* @param value The value to have expectations for. | |
* @param listener The failure listener. | |
*/ | |
ValueExpectation(const T &value, FailureListener* listener); | |
/** | |
* Expects that the values are equal. | |
* @param expected The expected value. | |
* @return The expectation (for chaining). | |
* @throws Failure | |
*/ | |
ValueExpectation<T> &equals(T expected); | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// FunctionExpectation: | |
/** | |
* A class for asserting expectations on functions. | |
*/ | |
class FunctionExpectation : public Expectation<FunctionExpectation> { | |
protected: | |
const std::function<void()> func; | |
public: | |
/** | |
* Creates a new expectation for a function. | |
* @param func The function to have expectations for. | |
* @param listener The failure listener. | |
*/ | |
FunctionExpectation(std::function<void()> func, FailureListener* listener) | |
: Expectation<FunctionExpectation>(listener), | |
func(func) {} | |
/** | |
* Expects cout to be equal to a provided string. | |
* @param expected The expected value of cout. | |
*/ | |
FunctionExpectation &couts(const std::string &expected); | |
/** | |
* Expects cerr to be equal to a provided string. | |
* @param expected The expected value of cerr. | |
*/ | |
FunctionExpectation &cerrs(const std::string &expected); | |
/** | |
* Expects an exception to be thrown. | |
* @tparam E The expected exception. | |
*/ | |
template<class E> | |
FunctionExpectation &throws(); | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Expect: | |
/** | |
* A class for creating expectations. | |
*/ | |
class Expect { | |
protected: | |
FailureListener* listener; | |
public: | |
/** | |
* Creates a new Expect object. | |
*/ | |
Expect(); | |
/** | |
* Creates a new Expect object with a failure listener. | |
* @param listener The failure listener. | |
*/ | |
Expect(FailureListener* listener); | |
/** | |
* Whether or not failed expectations will throw an error, or silently let the test continue. | |
*/ | |
bool suppress = true; | |
/** | |
* Creates an expectation with a value. | |
* @tparam T The type to expect on. | |
* @param value The value which the expectation applies to. | |
*/ | |
template<typename T> | |
ValueExpectation<T> operator()(const T &value); | |
/** | |
* Creates an expectation with a function. | |
* @tparam T The function template. | |
* @param func The value which the expectation applies to. | |
*/ | |
FunctionExpectation function(const std::function<void()> &func); | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Test: | |
/** | |
* A runnable test. | |
*/ | |
class Test : public FailureListener { | |
protected: | |
const std::string testName; | |
const std::function<void(Expect &)> runnable; | |
std::vector <Failure> testFailures; | |
std::string testStdout; | |
std::string testStderr; | |
bool testPassed; | |
public: | |
/** | |
* Creates a new test. | |
* @param name The test name. | |
* @param func The test function. | |
*/ | |
Test(std::string name, std::function<void(Expect &)> func); | |
/** | |
* Runs the test. | |
* @return True if the test passed, false otherwise. | |
*/ | |
virtual bool run(); | |
/** | |
* @inheritDoc | |
*/ | |
virtual void fail(const Failure &failure) override; | |
/** | |
* Gets the cout result of the test. | |
* @return The cout result. | |
*/ | |
virtual std::stringstream stdout() const; | |
/** | |
* Gets the cerr result of the test. | |
* @return The cerr result. | |
*/ | |
virtual std::stringstream stderr() const; | |
/** | |
* Gets whether or not the test passed. | |
* @return Whether or not the test passed. | |
*/ | |
bool passed() const; | |
/** | |
* Gets the reasons why the test failed. | |
* @return The test failures. | |
*/ | |
const std::vector <Failure> &failures() const; | |
/** | |
* Gets the name of the test. | |
* @return | |
*/ | |
const std::string &name() const; | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Testy: | |
/** | |
* The main class of Testy. | |
* | |
* This could be considered a test suite. | |
*/ | |
class Testy { | |
protected: | |
std::vector <Test> testList; | |
size_t testNameWidth; | |
/** | |
* Prints a stream to another stream. | |
* @param in The stream to read from. | |
* @param out The stream to write to. | |
* @param prefix The prefix to write before each line. | |
*/ | |
virtual void _printStream(std::istream &in, std::ostream &out, const std::string &prefix) const; | |
public: | |
Testy(); | |
virtual ~Testy() = default; | |
/** | |
* Gets the list of tests. | |
* @return The vector containing the tests. | |
*/ | |
std::vector <Test> tests(); | |
/** | |
* Adds a test to the suite. | |
* @param name The name of the test. | |
* @param test The test function. | |
*/ | |
virtual void test(std::string name, std::function<void(Expect)> test); | |
/** | |
* Runs all the tests. | |
*/ | |
virtual void run(); | |
/** | |
* Reports on all the tests. | |
* @param verbose Whether or not to print details for all tests, or just failed ones. | |
* @param stream The stream to report to. | |
*/ | |
virtual void report(bool verbose = false, std::ostream &stream = std::cout); | |
}; | |
// --------------------------------------------------------------------------------------------------------------------- | |
// immediate | |
/** | |
* Immediately run a test. | |
* @param name The name of the test. | |
* @param test The test function. | |
*/ | |
void immediate(std::string name, std::function<void(Expect)> test); | |
// --------------------------------------------------------------------------------------------------------------------- | |
} | |
// ===================================================================================================================== | |
// ===================================================================================================================== | |
// ===================================================================================================================== | |
// --------------------------------------------------------------------------------------------------------------------- | |
// testy.cpp | A simple C++ testing library. | Copyright (C) 2018 Ethan Pini <epini@sfu.ca> | |
// --------------------------------------------------------------------------------------------------------------------- | |
namespace testy { | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Util: | |
template<typename T> | |
std::string Util::to_string(typename std::enable_if<!META::streamable<T>::value, T>::type) { | |
return (std::string) "<" + typeid(T).name() + ">"; | |
} | |
template<typename T> | |
std::string Util::to_string(typename std::enable_if<META::streamable<T>::value, T>::type value) { | |
std::stringstream convert; | |
convert << value; | |
return convert.str(); | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Color: | |
Color::Color(const std::string &ansi) : ansi(ansi) { | |
} | |
Color::operator std::string() const { | |
return this->ansi; | |
} | |
const std::string &Color::asANSI() const { | |
return this->ansi; | |
} | |
std::ostream &operator<<(std::ostream &stream, const Color &color) { | |
stream << static_cast<std::string>(color); | |
return stream; | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Expectation: | |
template<typename T> | |
Expectation<T>::Expectation(FailureListener* listener) | |
: listener(listener) { | |
this->name = "[Anonymous Expectation]"; | |
this->_reset(); | |
} | |
template<typename T> | |
void Expectation<T>::_throw(const Failure &failure) { | |
if (this->listener == nullptr) { | |
throw failure; | |
} else { | |
this->listener->fail(failure); | |
} | |
} | |
template<typename T> | |
void Expectation<T>::_reset() { | |
this->negate = false; | |
} | |
/** | |
* Expect a specific string is sent to a stream. | |
* @param func The function to run. | |
* @param expected The expected value sent to cout. | |
*/ | |
template<typename T> | |
void Expectation<T>::_streamEquals(std::ostream* stream, std::function<void()> func, std::string expected) { | |
// Cache settings and reset. | |
std::string name = this->name; | |
bool negate = this->negate; | |
this->_reset(); | |
// Redirect stream. | |
std::stringstream buffer; | |
std::streambuf* backup = stream->rdbuf(); | |
stream->rdbuf(buffer.rdbuf()); | |
// Run lambda. | |
std::exception_ptr exception = nullptr; | |
try { | |
func(); | |
} catch (...) { | |
exception = std::current_exception(); | |
} | |
// Restore stream. | |
stream->rdbuf(backup); | |
// Handle exception. | |
if (exception != nullptr) { | |
rethrow_exception(exception); | |
} | |
// Compare. | |
if ((buffer.str() == expected) == negate) { | |
this->_throw(Failure(name, std::make_shared<ExpectationReason>(expected, buffer.str(), negate))); | |
} | |
} | |
template<typename T> | |
T &Expectation<T>::that(std::string name) { | |
this->name = name; | |
return *static_cast<T*>(this); | |
} | |
template<typename T> | |
T &Expectation<T>::never() { | |
this->negate = true; | |
return *static_cast<T*>(this); | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// ValueExpectation: | |
template<typename T> | |
ValueExpectation<T>::ValueExpectation(const T &value, FailureListener* listener) | |
: Expectation<ValueExpectation<T>>(listener), | |
value(value) { | |
this->_reset(); | |
} | |
template<typename T> | |
ValueExpectation<T> &ValueExpectation<T>::equals(T value) { | |
static_assert(META::comparable<T>::value == std::true_type::value, | |
"testy.h: Cannot run assertion equals(T); Type T must have an overloaded == operator."); | |
// Cache settings and reset. | |
std::string name = this->name; | |
bool negate = this->negate; | |
this->_reset(); | |
// Compare values. | |
if ((this->value == value) == negate) { | |
this->_throw(Failure(name, std::make_shared<ExpectationReason>( | |
Util::to_string<T>(this->value), | |
Util::to_string<T>(value), | |
negate | |
))); | |
} | |
return *this; | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Expectation<std::function<T>>: | |
FunctionExpectation &FunctionExpectation::couts(const std::string &expected) { | |
this->_streamEquals(&std::cout, this->func, expected); | |
return *this; | |
} | |
FunctionExpectation &FunctionExpectation::cerrs(const std::string &expected) { | |
this->_streamEquals(&std::cerr, this->func, expected); | |
return *this; | |
} | |
template<typename E> | |
FunctionExpectation &FunctionExpectation::throws() { | |
// Cache settings and reset. | |
std::string name = this->name; | |
bool negate = this->negate; | |
this->_reset(); | |
// Run the function, returning if the expected action occurred. | |
try { | |
func(); | |
if (negate) { | |
return *this; | |
} | |
} catch (const E &) { | |
if (!negate) { | |
return *this; | |
} | |
} | |
// The expected action did not occur. | |
// Generate a failure and throw it. | |
this->_throw(Failure( | |
name, | |
std::make_shared<ExpectationReason>((std::string) "throw " + typeid(E).name(), "<nothing>", negate) | |
)); | |
// Return this. | |
return *this; | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Expect: | |
Expect::Expect() {} | |
Expect::Expect(FailureListener* listener) | |
: listener(listener) {} | |
template<typename T> | |
ValueExpectation<T> Expect::operator()(const T &value) { | |
return ValueExpectation<T>(value, this->suppress ? this->listener : nullptr); | |
} | |
FunctionExpectation Expect::function(const std::function<void()> &func) { | |
return FunctionExpectation(func, this->suppress ? this->listener : nullptr); | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Failure: | |
Failure::Failure(const std::string &what, std::shared_ptr <FailureReason> reason) | |
: std::runtime_error(what), | |
failureReason(reason) { | |
} | |
const std::shared_ptr <FailureReason> Failure::reason() const { | |
return this->failureReason; | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// FailureReason: | |
FailureReason::operator std::string() const { | |
return this->toString(); | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// UncaughtExceptionReason: | |
UncaughtExceptionReason::UncaughtExceptionReason(const std::string &what) | |
: FailureReason(), | |
whatValue(what) {} | |
const std::string &UncaughtExceptionReason::what() const { | |
return this->whatValue; | |
} | |
std::string UncaughtExceptionReason::kind() const { | |
return typeid(UncaughtExceptionReason).name(); | |
} | |
std::string UncaughtExceptionReason::toString(bool formatted) const { | |
std::stringstream buffer; | |
const std::string &RED = formatted ? Color::RED.asANSI() : ""; | |
const std::string &RESET = formatted ? Color::RESET.asANSI() : ""; | |
buffer << RED << "Exception: " << this->what() << RESET << std::endl; | |
return buffer.str(); | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// ExpectationReason: | |
ExpectationReason::ExpectationReason(std::string expected, std::string received, bool negated) | |
: FailureReason(), | |
valueExpected(expected), | |
valueReceived(received), | |
conditionNegated(negated) { | |
} | |
const std::string &ExpectationReason::expected() const { | |
return this->valueExpected; | |
} | |
const std::string &ExpectationReason::received() const { | |
return this->valueReceived; | |
} | |
std::string ExpectationReason::kind() const { | |
return typeid(ExpectationReason).name(); | |
} | |
std::string ExpectationReason::toString(bool formatted) const { | |
std::stringstream buffer; | |
const std::string &YELLOW = formatted ? Color::YELLOW.asANSI() : ""; | |
const std::string &GREEN = formatted ? Color::GREEN.asANSI() : ""; | |
const std::string &RED = formatted ? Color::RED.asANSI() : ""; | |
const std::string &RESET = formatted ? Color::RESET.asANSI() : ""; | |
buffer << YELLOW << (this->conditionNegated ? "Expected Not: " : "Expected: ") << GREEN << this->expected() << std::endl | |
<< YELLOW << (this->conditionNegated ? "Received: " : "Received: ") << RED << this->received() << RESET << std::endl; | |
return buffer.str(); | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Test: | |
Test::Test(std::string name, std::function<void(Expect &)> func) | |
: testName(name), | |
runnable(func) {} | |
std::stringstream Test::stdout() const { | |
return std::stringstream(this->testStdout); | |
} | |
std::stringstream Test::stderr() const { | |
return std::stringstream(this->testStderr); | |
} | |
bool Test::passed() const { | |
return this->testPassed; | |
} | |
const std::vector <Failure> &Test::failures() const { | |
return this->testFailures; | |
} | |
const std::string &Test::name() const { | |
return this->testName; | |
} | |
bool Test::run() { | |
Expect expect = Expect(this); | |
// Replace cout/cerr. | |
std::stringstream captureStdout; | |
std::stringstream captureStderr; | |
std::streambuf* coutBackup = std::cout.rdbuf(); | |
std::streambuf* cerrBackup = std::cerr.rdbuf(); | |
std::cout.rdbuf(captureStdout.rdbuf()); | |
std::cerr.rdbuf(captureStderr.rdbuf()); | |
// Run the test. | |
this->testPassed = true; | |
this->testFailures.clear(); | |
try { | |
this->runnable(expect); | |
if (this->testFailures.size() > 0) { | |
this->testPassed = false; | |
} | |
} catch (const Failure &failure) { | |
this->testPassed = false; | |
this->testFailures.push_back(failure); | |
} catch (const std::exception &exception) { | |
this->testPassed = false; | |
this->testFailures.push_back(Failure("Uncaught exception.", std::make_shared<UncaughtExceptionReason>(exception.what()))); | |
} catch (...) { | |
this->testPassed = false; | |
this->testFailures.push_back(Failure("Uncaught exception?", std::make_shared<UncaughtExceptionReason>("???"))); | |
} | |
// Restore cout/cerr and copy captured. | |
std::cout.rdbuf(coutBackup); | |
std::cerr.rdbuf(cerrBackup); | |
this->testStdout = captureStdout.str(); | |
this->testStderr = captureStderr.str(); | |
// Return. | |
return this->testPassed; | |
} | |
void Test::fail(const Failure &failure) { | |
this->testFailures.push_back(failure); | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// Testy: | |
Testy::Testy() | |
: testNameWidth(40) { | |
} | |
void Testy::_printStream(std::istream &in, std::ostream &out, const std::string &prefix) const { | |
std::stringstream buffer; | |
std::string line; | |
while (std::getline(in, line)) { | |
if (line.length() == 0) { | |
buffer << prefix << std::endl; | |
} else { | |
out << buffer.str() | |
<< prefix << line << std::endl; | |
buffer.clear(); | |
} | |
} | |
out << Color::RESET; | |
} | |
std::vector <Test> Testy::tests() { | |
return this->testList; | |
} | |
void Testy::test(std::string name, std::function<void(Expect)> test) { | |
if (name.length() > this->testNameWidth) { | |
this->testNameWidth = name.length(); | |
} | |
this->testList.push_back(Test(name, test)); | |
} | |
void Testy::run() { | |
for (auto &test: this->testList) { | |
test.run(); | |
} | |
} | |
void Testy::report(bool verbose, std::ostream &stream) { | |
size_t passed = 0; | |
size_t total = 0; | |
for (auto &test: this->testList) { | |
total++; | |
passed += test.passed() ? 1 : 0; | |
// Display test name. | |
const std::string &name = test.name(); | |
size_t nameLength = name.length(); | |
std::string namePadding = std::string(this->testNameWidth - nameLength, '.'); | |
stream << Color::BLUE << name << Color::RESET << ": " << namePadding << " "; | |
// Display test status. | |
if (test.passed()) { | |
stream << Color::GREEN << "[passed]" << Color::RESET << std::endl; | |
} else { | |
stream << Color::RED << "[failed]" << Color::RESET << std::endl; | |
} | |
// Skip to the next one if passed and not verbose. | |
if (test.passed() && !verbose) { | |
continue; | |
} | |
// Display test failures. | |
std::vector <Failure> failures = test.failures(); | |
for (auto &failure: failures) { | |
stream << Color::RED << "! Failure: " << failure.what() << Color::RESET << std::endl; | |
std::stringstream reason; | |
reason << failure.reason()->toString(); | |
this->_printStream(reason, stream, Color::RED.asANSI() + "! " + Color::RESET.asANSI()); | |
} | |
// Display cout/cerr. | |
std::stringstream temp; | |
this->_printStream((temp = test.stdout()), stream, Color::YELLOW.asANSI() + "| " + Color::RESET.asANSI()); | |
this->_printStream((temp = test.stderr()), stream, Color::RED.asANSI() + "| " + Color::RESET.asANSI()); | |
} | |
// // Total. | |
// stream << std::endl << std::endl | |
// << Color::GREEN << passed << " tests passed." << Color::RESET << std::endl; | |
// | |
// if (passed == total) { | |
// stream << Color::GREEN << "ALL TESTS HAVE PASSED!" << Color::RESET << std::endl; | |
// } else { | |
// stream << Color::RED << (total - passed) << " tests failed." << Color::RESET << std::endl; | |
// } | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// immediate: | |
void immediate(std::string name, std::function<void(Expect)> test) { | |
Testy suite; | |
suite.test(name, test); | |
suite.run(); | |
suite.report(false); | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment